mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-07-08 01:25:14 +02:00
Optimized docker creation script
This commit is contained in:
parent
634f1835fc
commit
eccace60f4
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
site.retry
|
site.retry
|
||||||
*__pycache__
|
*__pycache__
|
||||||
venv
|
venv
|
||||||
*.log
|
*.log
|
||||||
|
*.bak
|
@ -26,23 +26,25 @@ def dump_yaml(data, path):
|
|||||||
|
|
||||||
|
|
||||||
def get_next_network(networks_dict, prefixlen):
|
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 = []
|
nets = []
|
||||||
for info in networks_dict['defaults_networks']['local'].values():
|
for info in networks_dict['defaults_networks']['local'].values():
|
||||||
net = ipaddress.ip_network(info['subnet'])
|
net = ipaddress.ip_network(info['subnet'])
|
||||||
if net.prefixlen == prefixlen:
|
if net.prefixlen == prefixlen:
|
||||||
nets.append(net)
|
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))
|
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):
|
def get_next_port(ports_dict, category):
|
||||||
"""Find the next unused port in the given localhost category."""
|
"""Assign the next port by taking max existing and adding one."""
|
||||||
used = {int(p) for p in ports_dict['ports']['localhost'].get(category, {}).values()}
|
existing = [int(p) for p in ports_dict['ports']['localhost'].get(category, {}).values()]
|
||||||
candidate = 1
|
return (max(existing) + 1) if existing else 1
|
||||||
while candidate in used:
|
|
||||||
candidate += 1
|
|
||||||
return candidate
|
|
||||||
|
|
||||||
|
|
||||||
def prompt_conflict(dst_file):
|
def prompt_conflict(dst_file):
|
||||||
@ -55,145 +57,105 @@ def prompt_conflict(dst_file):
|
|||||||
return choice
|
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."""
|
"""Recursively render all templates from src_dir into dst_dir, handling conflicts."""
|
||||||
env = Environment(
|
env = Environment(
|
||||||
loader=FileSystemLoader(src_dir),
|
loader=FileSystemLoader(src_dir),
|
||||||
keep_trailing_newline=True,
|
keep_trailing_newline=True,
|
||||||
autoescape=False,
|
autoescape=False,
|
||||||
)
|
)
|
||||||
# Add a bool filter for Jinja evaluation
|
|
||||||
env.filters['bool'] = lambda x: bool(x)
|
env.filters['bool'] = lambda x: bool(x)
|
||||||
|
|
||||||
for root, _, files in os.walk(src_dir):
|
for root, _, files in os.walk(src_dir):
|
||||||
rel_path = os.path.relpath(root, src_dir)
|
rel = os.path.relpath(root, src_dir)
|
||||||
target_path = os.path.join(dst_dir, rel_path)
|
target = os.path.join(dst_dir, rel)
|
||||||
os.makedirs(target_path, exist_ok=True)
|
os.makedirs(target, exist_ok=True)
|
||||||
for filename in files:
|
for fn in files:
|
||||||
template = env.get_template(os.path.join(rel_path, filename))
|
tpl = env.get_template(os.path.join(rel, fn))
|
||||||
rendered = template.render(**context)
|
rendered = tpl.render(**context)
|
||||||
out_name = filename[:-3] if filename.endswith('.j2') else filename
|
out_name = fn[:-3] if fn.endswith('.j2') else fn
|
||||||
dst_file = os.path.join(target_path, out_name)
|
dst_file = os.path.join(target, out_name)
|
||||||
|
|
||||||
if os.path.exists(dst_file):
|
if os.path.exists(dst_file):
|
||||||
choice = prompt_conflict(dst_file)
|
choice = prompt_conflict(dst_file)
|
||||||
if choice == '2':
|
if choice == '2': # skip
|
||||||
print(f"Skipping {dst_file}")
|
print(f"Skipping {dst_file}")
|
||||||
continue
|
continue
|
||||||
if choice == '3':
|
if choice == '3': # merge: append lines not present
|
||||||
with open(dst_file) as f_old:
|
with open(dst_file) as f_old:
|
||||||
old_lines = f_old.readlines()
|
old = f_old.readlines()
|
||||||
new_lines = rendered.splitlines(keepends=True)
|
new = rendered.splitlines(keepends=True)
|
||||||
diff = difflib.unified_diff(
|
add = [l for l in new if l not in old]
|
||||||
old_lines, new_lines,
|
if add:
|
||||||
fromfile=f"a/{out_name}",
|
with open(dst_file, 'a') as f:
|
||||||
tofile=f"b/{out_name}",
|
f.writelines(add)
|
||||||
lineterm=''
|
print(f"Merged {len(add)} new lines into {dst_file}")
|
||||||
)
|
else:
|
||||||
diff_path = dst_file + '.diff'
|
print(f"Nothing new to merge in {dst_file}")
|
||||||
with open(diff_path, 'w') as fd:
|
|
||||||
fd.writelines(diff)
|
|
||||||
print(f"Diff written to {diff_path}; please merge manually.")
|
|
||||||
continue
|
continue
|
||||||
# Overwrite
|
# overwrite
|
||||||
print(f"Overwriting {dst_file}")
|
print(f"Overwriting {dst_file}")
|
||||||
|
with open(dst_file, 'w') as f:
|
||||||
with open(dst_file, 'w') as f:
|
f.write(rendered)
|
||||||
f.write(rendered)
|
else:
|
||||||
|
# new file
|
||||||
|
with open(dst_file, 'w') as f:
|
||||||
|
f.write(rendered)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Load current port categories dynamically
|
# load dynamic port categories
|
||||||
ports_yaml = load_yaml(PORTS_FILE)
|
ports_yaml = load_yaml(PORTS_FILE)
|
||||||
categories = list(ports_yaml.get('ports', {}).get('localhost', {}).keys())
|
categories = list(ports_yaml.get('ports', {}).get('localhost', {}).keys())
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Create or update a Docker Ansible role, and assign network and ports"
|
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)})"
|
|
||||||
)
|
)
|
||||||
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
app_id = args.application_id
|
app = args.application_id
|
||||||
role_name = f"docker-{app_id}"
|
role = f"docker-{app}"
|
||||||
role_dir = os.path.join(ROLES_DIR, role_name)
|
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):
|
if os.path.exists(role_dir):
|
||||||
cont = input(f"Role {role_name} already exists. Continue updating? [y/N]: ").strip().lower()
|
if input(f"Role {role} exists. Continue update? [y/N]: ").strip().lower() != 'y':
|
||||||
if cont != 'y':
|
print("Aborted.")
|
||||||
print("Aborting.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
else:
|
else:
|
||||||
os.makedirs(role_dir)
|
os.makedirs(role_dir)
|
||||||
|
|
||||||
# 1) Render and copy templates, with conflict resolution
|
# 1) render templates
|
||||||
# Provide database_type=0 in context for task template logic
|
render_templates(ROLE_TEMPLATE_DIR, role_dir, {'application_id': app,
|
||||||
render_template(ROLE_TEMPLATE_DIR, role_dir, {
|
'role_name': role,
|
||||||
'application_id': app_id,
|
'database_type': 0})
|
||||||
'role_name': role_name,
|
print(f"→ Templates applied to {role_dir}")
|
||||||
'database_type': 0,
|
|
||||||
})
|
|
||||||
print(f"→ Templates rendered into {role_dir}")
|
|
||||||
|
|
||||||
# 2) Assign network if not already set
|
# 2) assign and update global networks
|
||||||
net_vars_file = f'./group_vars/{app_id}_network.yml'
|
nets = load_yaml(NETWORKS_FILE)
|
||||||
if os.path.exists(net_vars_file):
|
net = get_next_network(nets, int(args.network))
|
||||||
existing_net = load_yaml(net_vars_file)
|
nets['defaults_networks']['local'][app] = str(net)
|
||||||
apps = existing_net.get('defaults_networks', {}).get('application', {})
|
dump_yaml(nets, NETWORKS_FILE)
|
||||||
if app_id in apps:
|
print(f"→ Assigned network {net} to {app} in {NETWORKS_FILE}")
|
||||||
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 if not present
|
# 3) assign and update global ports
|
||||||
assigned = {}
|
assigned = {}
|
||||||
for cat in args.ports:
|
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)
|
port = get_next_port(ports_yaml, cat)
|
||||||
loc[app_id] = port
|
ports_yaml['ports']['localhost'].setdefault(cat, {})[app] = port
|
||||||
assigned[cat] = port
|
assigned[cat] = port
|
||||||
|
|
||||||
if assigned:
|
if assigned:
|
||||||
shutil.copy(PORTS_FILE, PORTS_FILE + '.bak')
|
shutil.copy(PORTS_FILE, PORTS_FILE + '.bak')
|
||||||
dump_yaml(ports_yaml, PORTS_FILE)
|
dump_yaml(ports_yaml, PORTS_FILE)
|
||||||
print(f"→ Assigned new ports: {assigned}")
|
print(f"→ Assigned ports {assigned} for {app} in {PORTS_FILE}")
|
||||||
else:
|
else:
|
||||||
print("→ No new ports to assign, skipping update of 09_ports.yml.")
|
print("→ No ports assigned (already existed)")
|
||||||
|
|
||||||
# 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}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user