@@ -2,14 +2,16 @@
 
		
	
		
			
				import  argparse  
		
	
		
			
				import  os  
		
	
		
			
				import  shutil  
		
	
		
			
				import  sys  
		
	
		
			
				import  yaml  
		
	
		
			
				import  ipaddress  
		
	
		
			
				import  difflib  
		
	
		
			
				from  jinja2  import  Environment ,  FileSystemLoader  
		
	
		
			
				 
		
	
		
			
				# Paths to the group-vars files  
		
	
		
			
				PORTS_FILE  =  ' ./group_vars/all/09_ports.yml '  
		
	
		
			
				NETWORKS_FILE  =  ' ./group_vars/all/10_networks.yml '  
		
	
		
			
				ROLE_TEMPLATE_DIR  =  ' ./docker- template '  
		
	
		
			
				ROLE_TEMPLATE_DIR  =  ' ./templates/docker_rol e '  
		
	
		
			
				ROLES_DIR  =  ' ./roles '  
		
	
		
			
				 
		
	
		
			
				 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -24,35 +26,44 @@ def dump_yaml(data, path):
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				def  get_next_network ( networks_dict ,  prefixlen ) :  
		
	
		
			
				    # Collect all  local subnets matching  the given prefix length
 
		
	
		
			
				    """ Select the first available  local subnet with  the given prefix length. """ 
 
		
	
		
			
				    nets  =  [ ] 
 
		
	
		
			
				    for  name ,  in  networks_dict [ ' defaults_networks ' ] [ ' local ' ] . item s( ) : 
 
		
	
		
			
				    for  info  in  networks_dict [ ' defaults_networks ' ] [ ' local ' ] . value s( ) : 
 
		
	
		
			
				        net  =  ipaddress . ip_network ( info [ ' subnet ' ] ) 
 
		
	
		
			
				        if  net . prefixlen  ==  prefixlen : 
 
		
	
		
			
				            nets . append ( net ) 
 
		
	
		
			
				    # Sort by network address and return the first one 
 
		
	
		
			
				    nets . sort ( key = lambda  n :  int ( n . network_address ) ) 
 
		
	
		
			
				    return  nets [ 0 ] 
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				def  get_next_port ( ports_dict ,  category ,  service :  
		
	
		
			
				    used  =  set ( ) 
 
		
	
		
			
				    # Gather already taken ports under localhost.category 
 
		
	
		
			
				    for  svc ,  port  in  ports_dict [ ' ports ' ] [ ' localhost ' ] . get ( category ,  { } ) . items ( ) : 
 
		
	
		
			
				        used . add ( int ( port ) ) 
 
		
	
		
			
				    # Start searching from port 1 upwards 
 
		
	
		
			
				def  get_next_port ( ports_dict ,  category ) :  
		
	
		
			
				    """ Find the next unused port in the given localhost category. """ 
 
		
	
		
			
				    used  =  { int ( p )  for  p  in  ports_dict [ ' ports ' ] [ ' localhost ' ] . get ( category ,  { } ) . values ( ) } 
 
		
	
		
			
				    candidate  =  1 
 
		
	
		
			
				    while  candidate  in  used : 
 
		
	
		
			
				        candidate  + =  1 
 
		
	
		
			
				    return  candidate 
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				def  prompt_conflict ( dst_file ) :  
		
	
		
			
				    """ Prompt the user to resolve a file conflict. """ 
 
		
	
		
			
				    print ( f " Conflict detected:  { dst_file } " ) 
 
		
	
		
			
				    print ( " Choose action: [1] overwrite, [2] skip, [3] merge " ) 
 
		
	
		
			
				    choice  =  None 
 
		
	
		
			
				    while  choice  not  in  ( ' 1 ' ,  ' 2 ' ,  ' 3 ' ) : 
 
		
	
		
			
				        choice  =  input ( " Enter 1, 2, or 3:  " ) . strip ( ) 
 
		
	
		
			
				    return  choice 
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				def  render_template ( src_dir ,  dst_dir ,  context ) :  
		
	
		
			
				    """ Recursively render all templates from src_dir into dst_dir, handling conflicts. """ 
 
		
	
		
			
				    env  =  Environment ( 
 
		
	
		
			
				        loader = FileSystemLoader ( src_dir ) , 
 
		
	
		
			
				        keep_trailing_newline = True , 
 
		
	
		
			
				        autoescape = False , 
 
		
	
		
			
				    ) 
 
		
	
		
			
				    # Add a bool filter for Jinja evaluation 
 
		
	
		
			
				    env . filters [ ' bool ' ]  =  lambda  x :  bool ( x ) 
 
		
	
		
			
				    for  root ,  _ ,  files  in  os . walk ( src_dir ) : 
 
		
	
		
			
				        rel_path  =  os . path . relpath ( root ,  src_dir ) 
 
		
	
		
			
				        target_path  =  os . path . join ( dst_dir ,  rel_path ) 
 
		
	
	
		
			
				
					
					
						
					 
				
			
			@@ -61,83 +72,127 @@ def render_template(src_dir, dst_dir, context):
 
		
	
		
			
				            template  =  env . get_template ( os . path . join ( rel_path ,  filename ) ) 
 
		
	
		
			
				            rendered  =  template . render ( * * context ) 
 
		
	
		
			
				            out_name  =  filename [ : - 3 ]  if  filename . endswith ( ' .j2 ' )  else  filename 
 
		
	
		
			
				            with  open ( os . path . join ( target_path ,  out_name ) ,  ' w ' )  as  f : 
 
		
	
		
			
				            dst_file  =  os . path . join ( target_path ,  out_name ) 
 
		
	
		
			
				 
		
	
		
			
				            if  os . path . exists ( dst_file ) : 
 
		
	
		
			
				                choice  =  prompt_conflict ( dst_file ) 
 
		
	
		
			
				                if  choice  ==  ' 2 ' : 
 
		
	
		
			
				                    print ( f " Skipping  { dst_file } " ) 
 
		
	
		
			
				                    continue 
 
		
	
		
			
				                if  choice  ==  ' 3 ' : 
 
		
	
		
			
				                    with  open ( dst_file )  as  f_old : 
 
		
	
		
			
				                        old_lines  =  f_old . readlines ( ) 
 
		
	
		
			
				                    new_lines  =  rendered . splitlines ( keepends = True ) 
 
		
	
		
			
				                    diff  =  difflib . unified_diff ( 
 
		
	
		
			
				                        old_lines ,  new_lines , 
 
		
	
		
			
				                        fromfile = f " a/ { out_name } " , 
 
		
	
		
			
				                        tofile = f " b/ { out_name } " , 
 
		
	
		
			
				                        lineterm = ' ' 
 
		
	
		
			
				                    ) 
 
		
	
		
			
				                    diff_path  =  dst_file  +  ' .diff ' 
 
		
	
		
			
				                    with  open ( diff_path ,  ' w ' )  as  fd : 
 
		
	
		
			
				                        fd . writelines ( diff ) 
 
		
	
		
			
				                    print ( f " Diff written to  { diff_path } ; please merge manually. " ) 
 
		
	
		
			
				                    continue 
 
		
	
		
			
				                # Overwrite 
 
		
	
		
			
				                print ( f " Overwriting  { dst_file } " ) 
 
		
	
		
			
				 
		
	
		
			
				            with  open ( dst_file ,  ' w ' )  as  f : 
 
		
	
		
			
				                f . write ( rendered ) 
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				def  main ( ) :  
		
	
		
			
				    # Load current port categories dynamically 
 
		
	
		
			
				    ports_yaml  =  load_yaml ( PORTS_FILE ) 
 
		
	
		
			
				    categories  =  list ( ports_yaml . get ( ' ports ' ,  { } ) . get ( ' localhost ' ,  { } ) . keys ( ) ) 
 
		
	
		
			
				 
		
	
		
			
				    parser  =  argparse . ArgumentParser ( 
 
		
	
		
			
				        description = " Create a Docker Ansible role with Jinja2 templates , and assign network and ports " 
 
		
	
		
			
				        description = " Create or update  a Docker Ansible role, and assign network and ports " 
 
		
	
		
			
				    ) 
 
		
	
		
			
				    parser . add_argument ( 
 
		
	
		
			
				        ' --application-id ' ,  ' -a  ' ,  required = True , 
 
		
	
		
			
				        help = " Unique ID of the  application (used in the role name)  " 
 
		
	
		
			
				        ' -a ' ,  ' --application-id  ' ,  required = True , 
 
		
	
		
			
				        help = " Unique application ID  " 
 
		
	
		
			
				    ) 
 
		
	
		
			
				    parser . add_argument ( 
 
		
	
		
			
				        ' --network  ' ,  ' -n  ' ,  choices = [ ' 24 ' ,  ' 28 ' ] ,  required = True , 
 
		
	
		
			
				        help = " Network prefix length to assign  (/24 or /28) " 
 
		
	
		
			
				        ' -n  ' ,  ' --network  ' ,  choices = [ ' 24 ' ,  ' 28 ' ] ,  required = True , 
 
		
	
		
			
				        help = " Network prefix length (/24 or /28) " 
 
		
	
		
			
				    ) 
 
		
	
		
			
				    parser . add_argument ( 
 
		
	
		
			
				        ' --ports  ' ,  ' -p  ' ,  nargs = ' + ' ,  metavar = " CATEGORY.SERVICE " ,  required = True , 
 
		
	
		
			
				        help = " List of ports in the format category.service (e.g. http.nextcloud )" 
 
		
	
		
			
				        ' -p  ' ,  ' --ports  ' ,  nargs = ' + ' ,  choices = categories ,  required = True , 
 
		
	
		
			
				        help = f " Port categories to assign (allowed:  { ' ,  ' . join ( categories ) } " 
 
		
	
		
			
				    ) 
 
		
	
		
			
				    args  =  parser . parse_args ( ) 
 
		
	
		
			
				 
		
	
		
			
				    app_id  =  args . application_id 
 
		
	
		
			
				    role_name  =  f " docker- { app_id } " 
 
		
	
		
			
				 
		
	
		
			
				    # 1) Create the role from the template 
 
		
	
		
			
				    role_dir  =  os . path . join ( ROLES_DIR ,  role_name ) 
 
		
	
		
			
				 
		
	
		
			
				    # If role directory exists, ask whether to continue 
 
		
	
		
			
				    if  os . path . exists ( role_dir ) : 
 
		
	
		
			
				        parser . error ( f " Role  { role_name }  already exists at  { role_dir } "  ) 
 
		
	
		
			
				        cont  =  input ( f " Role  { role_name }  already exists. Continue updating? [y/N]:  " ) . strip ( ) . lower (  ) 
 
		
	
		
			
				        if  cont  !=  ' y ' : 
 
		
	
		
			
				            print ( " Aborting. " ) 
 
		
	
		
			
				            sys . exit ( 1 ) 
 
		
	
		
			
				    else : 
 
		
	
		
			
				        os . makedirs ( role_dir ) 
 
		
	
		
			
				 
		
	
		
			
				    # 1) Render and copy templates, with conflict resolution 
 
		
	
		
			
				    # Provide database_type=0 in context for task template logic 
 
		
	
		
			
				    render_template ( ROLE_TEMPLATE_DIR ,  role_dir ,  { 
 
		
	
		
			
				        ' application_id ' :  app_id , 
 
		
	
		
			
				        ' role_name ' :  role_name , 
 
		
	
		
			
				        ' database_type ' :  0 , 
 
		
	
		
			
				    } ) 
 
		
	
		
			
				    print ( f " → Role  { role_name }  created at   { role_dir } " ) 
 
		
	
		
			
				    print ( f " → Templates rendered into   { role_dir } " ) 
 
		
	
		
			
				 
		
	
		
			
				    # 2) Assign network 
 
		
	
		
			
				    networks  =  load_yaml ( NETWORKS_FILE )  
 
		
	
		
			
				    prefix  =  int ( args . network ) 
 
		
	
		
			
				    chosen _net=  get_next_network ( networks ,  prefix ) 
 
		
	
		
			
				    out_net  =  { 
 
		
	
		
			
				        ' defaults_networks ' :  { 
 
		
	
		
			
				            ' application ' :  { 
 
		
	
		
			
				                app_id :  str ( chosen_net  ) 
 
		
	
		
			
				            } 
 
		
	
		
			
				        } 
 
		
	
		
			
				    } 
 
		
	
		
			
				    net_file  =  f ' ./group_vars/ { app_id } _network.yml ' 
 
		
	
		
			
				    dump_yaml ( out_net ,  net_file ) 
 
		
	
		
			
				    print ( f " → Assigned network  { chosen_net }  (/ { prefix } ) and wrote to  { net_file } " ) 
 
		
	
		
			
				    # 2) Assign network if not already set  
 
		
	
		
			
				    net_vars_file  =  f ' ./group_vars/ { app_id } _network.yml '  
 
		
	
		
			
				    if  os . path . exists ( net_vars_file ) : 
 
		
	
		
			
				         existing _net=  load_yaml ( net_vars_file )  
		
	
		
			
				         apps  =  existing_net . get ( ' defaults_networks ' ,  { } ) . get ( ' application ' ,  { } )  
		
	
		
			
				        if  app_id  in  apps : 
 
		
	
		
			
				            print ( f " → Network for  { app_id }  already configured ( { apps [ app_id ] } ), skipping. " ) 
 
		
	
		
			
				        else : 
 
		
	
		
			
				            networks  =  load_yaml ( NETWORKS_FILE ) 
 
		
	
		
			
				            net  =  get_next_network ( networks ,  int ( args . network ) )  
 
		
	
		
			
				            apps [ app_id ]  =  str ( net )  
 
		
	
		
			
				            dump_yaml ( existing_net ,  net_vars_file )  
 
		
	
		
			
				            print ( f " → Appended network  { net }  for  { app_id } "  ) 
 
		
	
		
			
				    else : 
 
		
	
		
			
				        networks  =  load_yaml ( NETWORKS_FILE ) 
 
		
	
		
			
				        net  =  get_next_network ( networks ,  int ( args . network ) ) 
 
		
	
		
			
				        dump_yaml ( { ' defaults_networks ' :  { ' application ' :  { app_id :  str ( net ) } } } ,  net_vars_file ) 
 
		
	
		
			
				        print ( f " → Created network vars file with  { net }  for  { app_id } " ) 
 
		
	
		
			
				 
		
	
		
			
				    # 3) Assign ports 
 
		
	
		
			
				    ports_yaml  =  load_yaml ( PORTS_FILE ) 
 
		
	
		
			
				    # 3) Assign ports if not present  
 
		
	
		
			
				    assigned  =  { } 
 
		
	
		
			
				    for  entry in  args . ports : 
 
		
	
		
			
				        try : 
 
		
	
		
			
				            category ,  service  =  entry . split ( ' . ' ,  1 )  
 
		
	
		
			
				        except  ValueError : 
 
		
	
		
			
				            parser . error ( f " Invalid port spec:  { entry } . Must be CATEGORY.SERVICE " ) 
 
		
	
		
			
				        port  =  get_next_port ( ports_yaml ,  category ,  service  ) 
 
		
	
		
			
				        # Insert into the in-memory ports data under localhos t
 
		
	
		
			
				        ports_yaml [ ' ports ' ] [ ' localhost ' ] . setdefault ( category ,  { } ) [ service  ]  =  port 
 
		
	
		
			
				        assigned [ entry ]  =  port 
 
		
	
		
			
				    for  cat in  args . ports : 
 
		
	
		
			
				        loc  =  ports_yaml [ ' ports ' ] [ ' localhost ' ] . setdefault ( cat ,  { } ) 
 
		
	
		
			
				        if  app_id  in  loc : 
 
		
	
		
			
				            print ( f " → Port for category  ' { cat } '  and  ' { app_id } '  already exists ( { loc [ app_id ] } ), skipping. "  ) 
 
		
	
		
			
				            continue 
 
		
	
		
			
				        port  =  get_next_port ( ports_yaml ,  cat ) 
 
		
	
		
			
				        loc [ app_id ]  =  por t
 
		
	
		
			
				        assigned [ cat ]  =  port 
 
		
	
		
			
				 
		
	
		
			
				    # Backup and write updated all/09_ports.yml 
 
		
	
		
			
				    backup_file  = PORTS_FILE  +  ' .bak ' 
 
		
	
		
			
				    shutil . copy ( PORTS_FILE ,  backup_file ) 
 
		
	
		
			
				    dump_yaml ( ports_yaml ,  PORTS_FILE ) 
 
		
	
		
			
				    print ( f " → Assigned ports:  { assigned } . Updated  { PORTS_FILE }  (backup at  { backup_file } ) " ) 
 
		
	
		
			
				    if  assigned : 
 
		
	
		
			
				         shutil . copy ( PORTS_FILE , PORTS_FILE  +  ' .bak ' )  
		
	
		
			
				         dump_yaml ( ports_yaml ,  PORTS_FILE )  
		
	
		
			
				        print ( f " → Assigned new ports:  { assigned } "  ) 
 
		
	
		
			
				    else : 
 
		
	
		
			
				        print ( " → No new ports to assign, skipping update of 09_ports.yml. " ) 
 
		
	
		
			
				 
		
	
		
			
				    # Also w rite p orts to th e application’  
 
		
	
		
			
				    out _ports=  { ' ports ' :  { ' localhost ' :  { } } } 
 
		
	
		
			
				    for  entry ,  port  in  assigned . items ( ) : 
 
		
	
		
			
				        category ,  service  =  entry . split ( ' . ' ,  1 ) 
 
		
	
		
			
				        out_ports [ ' ports ' ] [ ' localhost ' ] . setdefault ( category ,  { } ) [ service ]  =  port 
 
		
	
		
			
				    ports_file  =  f ' ./group_vars/ { app_id } _ports.yml ' 
 
		
	
		
			
				    dump_yaml ( out_ports ,  ports_file ) 
 
		
	
		
			
				    print ( f " → Wrote assigned ports to  { } " ) 
 
		
	
		
			
				    # 4) W rite or merg e application-specific port s file 
 
		
	
		
			
				    app _ports_file =  f ' ./group_vars/ { app_id } _ports.yml ' 
 
		
	
		
			
				    if  os . path . exists ( app_ports_file ) : 
 
		
	
		
			
				        app_ports  =  load_yaml ( app_ports_file ) 
 
		
	
		
			
				        dest  =  app_ports . setdefault ( ' ports ' ,  { } ) . setdefault ( ' localhost ' ,  { } ) 
 
		
	
		
			
				        for  cat ,  port  in  assigned . items ( ) :  
 
		
	
		
			
				            dest  [ cat ]  =  port 
 
		
	
		
			
				        dump_yaml ( app_ports ,  app_ ports_file ) 
 
		
	
		
			
				    else : 
 
		
	
		
			
				        dump_yaml ( { ' ports ' :  { ' localhost ' :  assigned } } ,  app_ports_file ) 
 
		
	
		
			
				    print ( f " → App-specific ports written to  { app_ports_file } " ) 
 
		
	
		
			
				 
		
	
		
			
				 
		
	
		
			
				if  __name__  ==  ' __main__ ' :