From eccace60f44607791e738f02241add917ee1d60b Mon Sep 17 00:00:00 2001 From: Kevin Veen-Birkenbach Date: Mon, 7 Jul 2025 04:41:42 +0200 Subject: [PATCH] Optimized docker creation script --- .gitignore | 3 +- cli/create_docker_role.py | 170 +++++++++++++++----------------------- 2 files changed, 68 insertions(+), 105 deletions(-) diff --git a/.gitignore b/.gitignore index 3f8e7cdc..ef092b76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ site.retry *__pycache__ venv -*.log \ No newline at end of file +*.log +*.bak \ No newline at end of file diff --git a/cli/create_docker_role.py b/cli/create_docker_role.py index 4fdaa3e8..4a81028f 100644 --- a/cli/create_docker_role.py +++ b/cli/create_docker_role.py @@ -26,23 +26,25 @@ def dump_yaml(data, path): def get_next_network(networks_dict, prefixlen): - """Select the first available local subnet with the given prefix length.""" + """Select the next contiguous subnet by taking the highest existing subnet and adding one network-size offset.""" nets = [] for info in networks_dict['defaults_networks']['local'].values(): net = ipaddress.ip_network(info['subnet']) if net.prefixlen == prefixlen: nets.append(net) + if not nets: + raise RuntimeError(f"No existing /{prefixlen} networks to base allocation on.") nets.sort(key=lambda n: int(n.network_address)) - return nets[0] + last = nets[-1] + offset = last.num_addresses + next_network_address = int(last.network_address) + offset + return ipaddress.ip_network((next_network_address, prefixlen)) 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 + """Assign the next port by taking max existing and adding one.""" + existing = [int(p) for p in ports_dict['ports']['localhost'].get(category, {}).values()] + return (max(existing) + 1) if existing else 1 def prompt_conflict(dst_file): @@ -55,145 +57,105 @@ def prompt_conflict(dst_file): return choice -def render_template(src_dir, dst_dir, context): +def render_templates(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) - os.makedirs(target_path, exist_ok=True) - for filename in files: - template = env.get_template(os.path.join(rel_path, filename)) - rendered = template.render(**context) - out_name = filename[:-3] if filename.endswith('.j2') else filename - dst_file = os.path.join(target_path, out_name) + rel = os.path.relpath(root, src_dir) + target = os.path.join(dst_dir, rel) + os.makedirs(target, exist_ok=True) + for fn in files: + tpl = env.get_template(os.path.join(rel, fn)) + rendered = tpl.render(**context) + out_name = fn[:-3] if fn.endswith('.j2') else fn + dst_file = os.path.join(target, out_name) if os.path.exists(dst_file): choice = prompt_conflict(dst_file) - if choice == '2': + if choice == '2': # skip print(f"Skipping {dst_file}") continue - if choice == '3': + if choice == '3': # merge: append lines not present 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.") + old = f_old.readlines() + new = rendered.splitlines(keepends=True) + add = [l for l in new if l not in old] + if add: + with open(dst_file, 'a') as f: + f.writelines(add) + print(f"Merged {len(add)} new lines into {dst_file}") + else: + print(f"Nothing new to merge in {dst_file}") continue - # Overwrite + # overwrite print(f"Overwriting {dst_file}") - - with open(dst_file, 'w') as f: - f.write(rendered) + with open(dst_file, 'w') as f: + f.write(rendered) + else: + # new file + with open(dst_file, 'w') as f: + f.write(rendered) def main(): - # Load current port categories dynamically + # load dynamic port categories ports_yaml = load_yaml(PORTS_FILE) categories = list(ports_yaml.get('ports', {}).get('localhost', {}).keys()) parser = argparse.ArgumentParser( - description="Create or update a Docker Ansible role, and assign network and ports" - ) - parser.add_argument( - '-a', '--application-id', required=True, - help="Unique application ID" - ) - parser.add_argument( - '-n', '--network', choices=['24', '28'], required=True, - help="Network prefix length (/24 or /28)" - ) - parser.add_argument( - '-p', '--ports', nargs='+', choices=categories, required=True, - help=f"Port categories to assign (allowed: {', '.join(categories)})" + description="Create or update a Docker Ansible role, assign network and ports globally" ) + parser.add_argument('-a', '--application-id', required=True, help="Unique application ID") + parser.add_argument('-n', '--network', choices=['24', '28'], required=True, + help="Network prefix length (/24 or /28)") + parser.add_argument('-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}" - role_dir = os.path.join(ROLES_DIR, role_name) + app = args.application_id + role = f"docker-{app}" + role_dir = os.path.join(ROLES_DIR, role) - # If role directory exists, ask whether to continue + # ensure role dir exists, prompt if updating if os.path.exists(role_dir): - cont = input(f"Role {role_name} already exists. Continue updating? [y/N]: ").strip().lower() - if cont != 'y': - print("Aborting.") + if input(f"Role {role} exists. Continue update? [y/N]: ").strip().lower() != 'y': + print("Aborted.") 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"→ Templates rendered into {role_dir}") + # 1) render templates + render_templates(ROLE_TEMPLATE_DIR, role_dir, {'application_id': app, + 'role_name': role, + 'database_type': 0}) + print(f"→ Templates applied to {role_dir}") - # 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}") + # 2) assign and update global networks + nets = load_yaml(NETWORKS_FILE) + net = get_next_network(nets, int(args.network)) + nets['defaults_networks']['local'][app] = str(net) + dump_yaml(nets, NETWORKS_FILE) + print(f"→ Assigned network {net} to {app} in {NETWORKS_FILE}") - # 3) Assign ports if not present + # 3) assign and update global ports assigned = {} 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] = port + ports_yaml['ports']['localhost'].setdefault(cat, {})[app] = port assigned[cat] = port if assigned: shutil.copy(PORTS_FILE, PORTS_FILE + '.bak') dump_yaml(ports_yaml, PORTS_FILE) - print(f"→ Assigned new ports: {assigned}") + print(f"→ Assigned ports {assigned} for {app} in {PORTS_FILE}") else: - print("→ No new ports to assign, skipping update of 09_ports.yml.") - - # 4) Write or merge application-specific ports 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}") - + print("→ No ports assigned (already existed)") if __name__ == '__main__': main()