From be285606e2ac53df77f03dab593e829b78e82079 Mon Sep 17 00:00:00 2001 From: Mathieu Broillet Date: Sun, 11 Jun 2023 21:06:22 +0200 Subject: [PATCH] started creations conditions and steps checking/running --- .idea/ProxmoxDeploy.iml | 2 +- .idea/workspace.xml | 144 ++++++++++++++++++++++++--------- resources/config.json | 2 +- resources/lxc/0000/config.json | 31 ------- resources/lxc/100/config.json | 50 ++++++++++++ src/main.py | 14 ++-- src/utils/creation_utils.py | 57 +++++++++++++ src/utils/lxc_utils.py | 103 +++++++++++++++++------ src/utils/proxmox_utils.py | 45 ++++++++--- 9 files changed, 335 insertions(+), 113 deletions(-) delete mode 100644 resources/lxc/0000/config.json create mode 100644 resources/lxc/100/config.json create mode 100644 src/utils/creation_utils.py diff --git a/.idea/ProxmoxDeploy.iml b/.idea/ProxmoxDeploy.iml index 74d515a..fd477cc 100644 --- a/.idea/ProxmoxDeploy.iml +++ b/.idea/ProxmoxDeploy.iml @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 9f05c80..3013028 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,16 +1,20 @@ + + - - - - - - - - + + + + - + + + + + + @@ -51,29 +61,7 @@ - - - - + @@ -139,10 +207,14 @@ - + - \ No newline at end of file diff --git a/resources/config.json b/resources/config.json index 036a127..3f0ca86 100644 --- a/resources/config.json +++ b/resources/config.json @@ -1,6 +1,6 @@ { "pve":{ - "host": "192.168.10.99", + "host": "192.168.11.99", "user": "root", "port": 22, "local": false diff --git a/resources/lxc/0000/config.json b/resources/lxc/0000/config.json deleted file mode 100644 index 826e1bc..0000000 --- a/resources/lxc/0000/config.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "lxc_hostname": "test", - "os": { - "name": "alpine", - "release": "3.17" - }, - "resources": { - "cpu": "1", - "memory": "512", - "swap": "512", - "disk": "10", - "storage": "local-zfs" - }, - "network": { - "bridge": "vmbr0", - "ipv4": "dhcp", - "ipv6": "auto", - "mac": "00:00:00:00:00:00", - "gateway4": "", - "gateway6": "", - "vlan": "" - }, - "options": { - "privileged": "false", - "start_on_boot": "false", - "password": "qwertz1234", - "ssh": false - }, - "creation": {}, - "deploy": [] -} \ No newline at end of file diff --git a/resources/lxc/100/config.json b/resources/lxc/100/config.json new file mode 100644 index 0000000..b7179af --- /dev/null +++ b/resources/lxc/100/config.json @@ -0,0 +1,50 @@ +{ + "lxc_hostname": "traefik", + "os": { + "name": "alpine", + "release": "3.17" + }, + "resources": { + "cpu": "2", + "memory": "1024", + "swap": "256", + "disk": "8", + "storage": "local-lvm" + }, + "network": { + "bridge": "vmbr0", + "ipv4": "dhcp", + "ipv6": "auto", + "mac": "92:A6:71:77:8E:D8", + "gateway4": "", + "gateway6": "", + "vlan": "" + }, + "options": { + "privileged": "false", + "start_on_boot": "false", + "startup_order": 2, + "password": "qwertz1234", + "ssh": false, + "tags": "2-proxy+auth" + }, + "creation": { + "conditions": [ + { + "type": "program", + "program": "docker" + }, + { + "type": "folder", + "path": "/var/data/traefik" + } + ], + "steps":[ + { + "type": "script", + "path": "/global/install-docker.sh" + } + ] + }, + "deploy": [] +} \ No newline at end of file diff --git a/src/main.py b/src/main.py index 69a5da6..f931c27 100644 --- a/src/main.py +++ b/src/main.py @@ -8,25 +8,21 @@ from src.utils.lxc_utils import load_lxc, get_all_lxcs def run(): # Read all files in the resources directory resources = os.listdir(project_path / "resources") - logging.info(f"Resources found: {resources}") # Go through each LXC file for resource in resources: if resource == "lxc": - logging.info("LXC folder found") - # Read all files in the LXC directory lxc_folders = os.listdir(project_path / "resources" / "lxc") for lxc_folder in lxc_folders: lxc_file = os.path.join(project_path / "resources" / "lxc", lxc_folder, "config.json") - logging.info(f"Reading LXC ID {lxc_folder}") - # Open the file with open(lxc_file, "r") as file: # Load the LXC load_lxc(file.read(), lxc_id=lxc_folder) - print(get_all_lxcs()) - - # print(get_all_lxcs()[0].get_tteck_env_variables()) - print(get_all_lxcs()[0].get_pct_command()) \ No newline at end of file + for lxc in get_all_lxcs(): + logging.info(f"Loading LXC {lxc.lxc_id}") + lxc.create() + lxc.start() + lxc.check_creation_conditions() diff --git a/src/utils/creation_utils.py b/src/utils/creation_utils.py new file mode 100644 index 0000000..fc5021c --- /dev/null +++ b/src/utils/creation_utils.py @@ -0,0 +1,57 @@ +import logging +import os +from pathlib import Path + + +def are_all_conditions_met(lxc): + """ + Check conditions for running the creations steps for an LXC + The conditions are for checking if the LXC has already been configured or not + If all conditions are met, the deploy/updates steps are run + Otherwise, we run the creation steps + + :param lxc_id: lxc id + :return: are conditions met + """ + + creation = lxc.get_creation() + conditions = creation["conditions"] + + result = [] + + for condition in conditions: + if condition["type"] == "file": + result.append(Path(condition["path"]).is_file()) + elif condition["type"] == "folder": + result.append(Path(condition["path"]).is_dir()) + elif condition["type"] == "program": + result.append(lxc.run_command("which " + condition["program"], only_code=True) == 0) + else: + raise Exception(f"Unknown condition type {condition['type']}") + + if all(result): + logging.info(f"All creations conditions met for LXC {lxc.lxc_id}, running deploy steps...") + else: + logging.info(f"Not all creations conditions met for LXC {lxc.lxc_id}, running creation steps...") + + return all(result) + + +def run_creations_steps(lxc): + """ + Run creation steps for an LXC + + :param lxc: lxc + :return: None + """ + + creation = lxc.get_creation() + creation_steps = creation["creation_steps"] + + for step in creation_steps: + if step["type"] == "command": + lxc.run_command(step["command"]) + elif step["type"] == "script": + lxc.run_script(step["script"]) + else: + raise Exception(f"Unknown creation step type {step['type']}") diff --git a/src/utils/lxc_utils.py b/src/utils/lxc_utils.py index c63cf03..3139ba8 100644 --- a/src/utils/lxc_utils.py +++ b/src/utils/lxc_utils.py @@ -1,7 +1,7 @@ import json import logging -from src.utils import proxmox_utils +from src.utils import proxmox_utils, creation_utils lxcs = [] @@ -144,15 +144,18 @@ class LXC: :return: os template """ - wanted_template = proxmox_utils.run_command_on_pve( - f"pveam available --section system | grep {self.os_name}-{self.os_release} | awk '{{print \\$2}}'") + # TODO: might have to run "pveam update" before running this command on fresh install of PVE - if wanted_template == "": - logging.warning(f"Template {self.os_name}-{self.os_release} not found, downloading it...") - proxmox_utils.run_command_on_pve(f"pveam download local {wanted_template}") - return None + template_name = proxmox_utils.run_command_on_pve( + f"pveam available --section system | awk /'{self.os_name}-{self.os_release}/' | awk '{{print \\$2}}'") - return f"local:vztmpl/{wanted_template}" + is_template_downloaded = proxmox_utils.run_command_on_pve(f"pveam list local | awk /'{template_name}/'") + + if is_template_downloaded == "": + logging.info(f"Template {template_name} not found, downloading it...") + proxmox_utils.run_command_on_pve(f"pveam download local {template_name}", True) + + return f"local:vztmpl/{template_name}" def get_resources(self): """ @@ -294,6 +297,20 @@ class LXC: """ return self.deploy + def get_creation(self): + """ + Get creation + :return: creation + """ + return self.creation + + def is_running(self): + """ + Is running + :return: is lxc running? (boolean) + """ + return proxmox_utils.run_command_on_pve(f"pct list | awk '/running/ && /{self.lxc_id}/'") != "" + def create(self): """ Create LXC @@ -302,33 +319,71 @@ class LXC: """ if proxmox_utils.does_lxc_exist(self.lxc_id): logging.info(f"LXC {self.lxc_id} already exists, skipping creation") + proxmox_utils.run_command_on_pve(self.get_pct_command(create=False), True) return else: logging.info(f"Creating LXC {self.lxc_id}") - if self.creation['type'] == "tteck": - proxmox_utils.execute_tteck_script(self.creation['script'], self.get_tteck_env_variables()) + proxmox_utils.run_command_on_pve(self.get_pct_command(create=True), True) + + def start(self): + """ + Start LXC + """ + if self.is_running(): + logging.info(f"LXC {self.lxc_id} already running, skipping start") + return + else: + logging.info(f"Starting LXC {self.lxc_id}") + proxmox_utils.run_command_on_pve(f"pct start {self.lxc_id}", True) + + def run_command(self, command, warn_exit_status=False, only_code=False): + """ + Run command on LXC + :param command: command to run + :return: command output + """ + + logging.debug(f"Running command {command} on LXC {self.lxc_id}") + if only_code: + return proxmox_utils.run_command_on_pve(f"pct exec {self.lxc_id} -- {command}", warn_exit_status, only_code) + + return proxmox_utils.run_command_on_pve(f"pct exec {self.lxc_id} -- {command}", warn_exit_status) def deploy(self): pass - def get_pct_command(self): + def check_creation_conditions(self): + return creation_utils.are_all_conditions_met(self) + + def get_pct_command(self, create=True): """ - Get pct command to create LXC + Get pct command to create/edit LXC :return: pct command """ - pct_command = f"pct create {self.lxc_id} {self.get_os_template()} " \ - f"--hostname {self.lxc_hostname} " \ - f"--cores {self.cpu} " \ - f"--memory {self.memory} " \ - f"--swap {self.memory} " \ - f"--net0 name=eth0,bridge={self.bridge},ip={self.ipv4},hwaddr={self.mac} " \ - f"--onboot {int(self.start_on_boot == 0)} " \ - f"--ostype {self.os_name} " \ - f"--password {self.password} " \ - f"--storage {self.storage} " \ - f"--rootfs volume={self.storage}:{self.disk},size={self.disk} " \ - f"--unprivileged {not self.privileged} " + if create: + pct_command = f"pct create {self.lxc_id} {self.get_os_template()} " \ + f"--hostname {self.lxc_hostname} " \ + f"--cores {self.cpu} " \ + f"--memory {self.memory} " \ + f"--swap {self.memory} " \ + f"--net0 name=eth0,bridge={self.bridge},ip={self.ipv4},hwaddr={self.mac},type=veth " \ + f"--onboot {int(self.start_on_boot == 0)} " \ + f"--ostype {self.os_name} " \ + f"--password {self.password} " \ + f"--storage {self.storage} " \ + f"--unprivileged {not self.privileged} " \ + f"--rootfs volume={self.storage}:{self.disk},size={self.disk} " \ + f"--unprivileged {not self.privileged}" + else: + pct_command = f"pct set {self.lxc_id} " \ + f"--hostname {self.lxc_hostname} " \ + f"--cores {self.cpu} " \ + f"--memory {self.memory} " \ + f"--swap {self.memory} " \ + f"--net0 name=eth0,bridge={self.bridge},ip={self.ipv4},hwaddr={self.mac},type=veth " \ + f"--onboot {int(self.start_on_boot == 0)} " \ + f"--ostype {self.os_name} " # TODO: add gateway4 # f"ip6={self.ipv6},gw6={self.gateway6},trunks={self.vlan} " \ # TODO diff --git a/src/utils/proxmox_utils.py b/src/utils/proxmox_utils.py index a5e57d4..d373814 100644 --- a/src/utils/proxmox_utils.py +++ b/src/utils/proxmox_utils.py @@ -11,7 +11,7 @@ def get_pve_version(): Get PVE version :return: pve version """ - return run_command_on_pve("pveversion") + return run_command_on_pve("pveversion", True) def get_pve_hostname(): @@ -19,7 +19,7 @@ def get_pve_hostname(): Get PVE hostname :return: pve hostname """ - return run_command_on_pve("hostname") + return run_command_on_pve("hostname", True) def does_lxc_exist(lxc_id): @@ -30,7 +30,7 @@ def does_lxc_exist(lxc_id): :return: does lxc exists """ # TODO: only check in VMID column - return lxc_id in run_command_on_pve(f"pct list {lxc_id}") + return lxc_id in run_command_on_pve(f"pct list | awk '/{lxc_id}/'") def does_qemu_vm_exist(vm_id): @@ -55,13 +55,15 @@ def execute_tteck_script(script_url, env_variables): env_variables = " ".join(env_variables) - run_command_on_pve(f"{env_variables} && bash -c \"$(wget -qLO - {script_url}\"") + run_command_on_pve(f"{env_variables} && bash -c \"$(wget -qLO - {script_url}\"", True) -def run_command_on_pve(command): +def run_command_on_pve(command, warn_exit_status=False, only_code=False): """ Run command on PVE + :param only_code: should we only return the exit code and not the stdout + :param warn_exit_status: should an exception be thrown if the exit status is not 0 :param command: command :return: command """ @@ -74,9 +76,19 @@ def run_command_on_pve(command): # Check if PVE is local or remote if data['pve']['local']: # Run command and return output (not as bytes) - logging.debug(f"Running command on PVE (ssh): \n{command}") - return subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - encoding="utf-8").stdout.decode().rstrip() + logging.debug(f"Running command on PVE (locally): \n{command}") + + command = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + encoding="utf-8") + + if command.returncode != 0 and warn_exit_status: + logging.error(f"Error while running command on PVE: \n{command.stderr}") + raise Exception(f"Error while running command on PVE: \n{command.stderr}") + + if only_code: + return command.returncode + + return command.stdout.decode().rstrip() else: host = data['pve']['host'] @@ -84,6 +96,17 @@ def run_command_on_pve(command): port = data['pve']['port'] # Run command on PVE via SSH and return output - logging.debug(f"Running command on PVE (locally): \n{command}") - return subprocess.run(f"ssh {username}@{host} -p {port} \"{command}\"", shell=True, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, encoding="utf-8").stdout.rstrip() + logging.debug(f"Running command on PVE (ssh): \n{command}") + + # catch errors code + command = subprocess.run(f"ssh {username}@{host} -p {port} \"{command}\"", shell=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8") + + if command.returncode != 0 and warn_exit_status: + logging.error(f"Error while running command on PVE: \n{command.stderr}") + raise Exception(f"Error while running command on PVE: \n{command.stderr}") + + if only_code: + return command.returncode + + return command.stdout.rstrip()