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 @@
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
+
+
+
+
+
+
@@ -43,7 +47,13 @@
"RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"WebServerToolWindowFactoryState": "false",
- "settings.editor.selected.configurable": "preferences.pluginManager"
+ "last_opened_file_path": "/home/mathieu/Documents/Local/Developpement/PycharmProjects/ProxmoxDeploy",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "settings.editor.selected.configurable": "vcs.Git",
+ "vue.rearranger.settings.migration": "true"
}
}
@@ -51,29 +61,7 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -112,6 +100,9 @@
1686293135999
+
+
+
1686293500295
@@ -120,7 +111,84 @@
1686293500295
-
+
+ 1686342404046
+
+
+
+ 1686342404046
+
+
+ 1686342511585
+
+
+
+ 1686342511585
+
+
+ 1686342681849
+
+
+
+ 1686342681849
+
+
+ 1686342794207
+
+
+
+ 1686342794207
+
+
+ 1686342966430
+
+
+
+ 1686342966430
+
+
+ 1686344421245
+
+
+
+ 1686344421245
+
+
+ 1686345032503
+
+
+
+ 1686345032503
+
+
+ 1686346024833
+
+
+
+ 1686346024833
+
+
+ 1686406538341
+
+
+
+ 1686406538341
+
+
+ 1686503708967
+
+
+
+ 1686503708967
+
+
+ 1686504338382
+
+
+
+ 1686504338382
+
+
@@ -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()