implemented most creations steps and global improvements
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Mathieu Broillet 2023-06-12 15:15:51 +02:00
parent e4188e8432
commit fb4154c7e5
No known key found for this signature in database
GPG Key ID: 7D4F25BC50A0AA32
8 changed files with 240 additions and 105 deletions

View File

@ -167,28 +167,50 @@ The script step will execute a script.
``` ```
### File ### File
The file step will copy a file to the VM/LXC. The file step will create / copy a file to the VM/LXC.
```json ```json
"steps": [ "steps": [
{ {
"type": "file", "type": "file_create",
"path": "traefik.toml", // local path (here: resources/lxc/<id>/traefik.toml) "path": "/var/data/traefikv2/traefik.toml", // lxc/vm path
"destination": "/var/data/traefikv2/traefik.toml" // lxc/vm path "permissions": "644" // (optional) permissions of the file
} }
] ]
``` ```
```json
"steps": [
{
"type": "file_copy",
"path": "traefik.toml", // local path (here: resources/lxc/<id>/traefik.toml)
"destination": "/var/data/traefikv2/traefik.toml", // lxc/vm path
"permissions": "644" // (optional) permissions of the file
}
]
```
*Note: `file_copy` creates the folder if it doesn't exist*
### Folder ### Folder
The folder step will copy a folder to the VM/LXC. The folder step will copy a folder to the VM/LXC.
```json ```json
"steps": [ "steps": [
{ {
"type": "folder", "type": "folder_create",
"path": "/data/", // local path (here: resources/lxc/<id>/data/) "path": "/var/data/traefikv2", // lxc/vm path
"destination": "/var/data/traefikv2" // lxc/vm path "permissions": "755" // (optional) permissions of the folder
} }
] ]
``` ```
```json
"steps": [
{
"type": "folder_copy",
"path": "/data/", // local path (here: resources/lxc/<id>/data/)
"destination": "/var/data/traefikv2", // lxc/vm path
"permissions": "755" // (optional) permissions of the folder
}
]
```
*Note: `folder_copy` creates the folder if it doesn't exist*
### Command ### Command
The command step will execute a command on the VM/LXC. The command step will execute a command on the VM/LXC.
@ -197,19 +219,19 @@ The command step will execute a command on the VM/LXC.
{ {
"type": "command", "type": "command",
"command": "whoami", // command to execute "command": "whoami", // command to execute
"working_directory": "/var/data/config/traefikv2" // lxc/vm path "working_directory": "/var/data/config/traefikv2" // (optional) lxc/vm path
} }
] ]
``` ```
### Docker ### Docker
The docker step will execute a docker command on the VM/LXC. The docker step will execute a command inside a docker container running on the VM/LXC.
```json ```json
"steps": [ "steps": [
{ {
"type": "docker", "type": "docker",
"command": "run -d --name=traefikv2 --restart=always -p 80:80 -p 443:443 -p 8080:8080 -v /var/data/traefikv2:/etc/traefik -v /var/data/config/traefikv2:/config -v /var/run/docker.sock:/var/run/docker.sock traefik:latest", // docker command to execute "container": "<container-name>", // docker container name
"working_directory": "/var/data/config/traefikv2" // lxc/vm path "command": "<command-to-run-inside>" // docker command to execute
} }
] ]
``` ```

View File

@ -4,5 +4,8 @@
"user": "root", "user": "root",
"port": 22, "port": 22,
"local": false "local": false
},
"settings": {
"setAuthorizedHostsSSH": true
} }
} }

View File

@ -25,7 +25,6 @@
"start_on_boot": "false", "start_on_boot": "false",
"startup_order": 2, "startup_order": 2,
"password": "qwertz1234", "password": "qwertz1234",
"ssh": false,
"tags": "2-proxy+auth" "tags": "2-proxy+auth"
}, },
"creation": { "creation": {

View File

@ -1,7 +1,8 @@
import logging import logging
import os
from pathlib import Path from pathlib import Path
from src.utils import lxc_commands_utils
def are_all_conditions_met(lxc): def are_all_conditions_met(lxc):
""" """
@ -10,7 +11,7 @@ def are_all_conditions_met(lxc):
If all conditions are met, the deploy/updates steps are run If all conditions are met, the deploy/updates steps are run
Otherwise, we run the creation steps Otherwise, we run the creation steps
:param lxc_id: lxc id :param lxc:
:return: are conditions met :return: are conditions met
""" """
@ -34,6 +35,13 @@ def are_all_conditions_met(lxc):
return all(result) return all(result)
def optional(var, placeholder):
if var is None:
return placeholder
else:
return var
def run_steps(lxc): def run_steps(lxc):
""" """
Run creation steps for an LXC Run creation steps for an LXC
@ -49,48 +57,23 @@ def run_steps(lxc):
# Support for scripts # Support for scripts
case "script": case "script":
run_script(step, lxc) lxc_commands_utils.run_script(step, lxc)
case "file_create":
lxc_commands_utils.create_file_in_lxc(lxc, step["path"], optional(step["permission"], 644))
def run_script(step, lxc): case "file_copy":
# Run local script lxc_commands_utils.copy_local_file_to_lxc(lxc, step["path"], step["destination"])
if "path" in step: case "folder":
path = step["path"] lxc_commands_utils.create_folder_in_lxc(lxc, step["path"], optional(step["permission"], 755))
case "folder_copy":
with open(path, "r") as file: lxc_commands_utils.copy_local_folder_to_lxc(lxc, step["path"], step["destination"])
script = file.read() case "command":
lxc.run_command("'bash -s' <<'ENDSSH'\n" + script) lxc.run_command(step["command"], optional(step["working_directory"], None))
case "docker":
# Run remote script lxc_commands_utils.run_docker_command(lxc, step["container"], step["command"])
elif "url" in step: case "docker_compose":
url = step["url"] lxc_commands_utils.run_docker_compose_command(lxc, step["command"],
lxc.has optional(step["working_directory"], None))
pass case "git":
lxc.run_command(f"git clone {step['url']} {step['destination']}")
# Run script in LXC case "download":
elif "lxc_path" in step: lxc_commands_utils.download_file(lxc, step["url"], step["destination"])
lxc_path = step["lxc_path"]
pass
path = step["path"]
if path.startswith("/global/"):
# Use global folder
full_path = "resources/scripts" + path[len("/global/"):]
else:
# Use VM/LXC folder
lxc_id = lxc.get_id()
full_path = f"resources/lxc/{lxc_id}/{path}"
if os.path.isfile(full_path):
lxc.run_command(f"bash {full_path}")
def has_program(lxc, program):
"""
Check if program is installed in LXC
:param lxc: lxc
:param program: program
:return: is program installed
"""
return lxc.run_command("which " + program, only_code=True) == 0

View File

@ -0,0 +1,76 @@
from src.utils import proxmox_utils
from src.utils.resources_utils import get_path
def run_script(lxc, step):
# Run local script
if "path" in step:
_run_local_script_on_lxc(lxc, step["path"])
# Run remote script
elif "url" in step:
_run_remote_script_on_lxc(lxc, step["url"])
# Run script in LXC
elif "lxc_path" in step:
_run_script_on_lxc(lxc, step["lxc_path"])
def _run_script_on_lxc(lxc, path):
lxc.run_command(f"bash {path}")
def _run_local_script_on_lxc(lxc, path):
path = get_path(lxc, path)
with open(path, "r") as file:
script = file.read()
lxc.run_command("'bash -s' <<'ENDSSH'\n" + script)
def _run_remote_script_on_lxc(lxc, url):
if lxc.has_program("curl"):
lxc.run_command(f"curl -sSL {url} | bash")
def create_folder_in_lxc(lxc, path, permission=755):
lxc.run_command(f"mkdir -p {path}")
if permission != 755:
lxc.run_command(f"chmod -R {permission} {path}")
def create_file_in_lxc(lxc, path, permission=644):
lxc.run_command(f"touch {path}")
if permission != 644:
lxc.run_command(f"chmod {permission} {path}")
def copy_local_file_to_lxc(lxc, path, destination):
proxmox_utils.run_command_locally(f"scp {path} {lxc.get_ssh_string()}:{destination}")
def copy_local_folder_to_lxc(lxc, path, destination):
proxmox_utils.run_command_locally(f"scp -r {path} {lxc.get_ssh_string()}:{destination}")
def run_docker_command(lxc, container, command):
lxc.run_command(f"docker exec -it {container} {command}")
def run_docker_compose_command(lxc, command, working_directory=None):
docker_compose_exec = "docker-compose"
if not lxc.has_program(docker_compose_exec):
docker_compose_exec = "docker compose"
if working_directory is not None:
lxc.run_command(f"cd {working_directory} && {docker_compose_exec} {command}")
else:
lxc.run_command(f"{docker_compose_exec} {command}")
def download_file(lxc, url, destination):
if type(url) is list:
for u in url:
lxc.run_command(f"wget {u} -O {destination}")
else:
lxc.run_command(f"wget {url} -O {destination}")

View File

@ -1,7 +1,7 @@
import json import json
import logging import logging
from src.utils import proxmox_utils, creation_utils from src.utils import proxmox_utils, creation_utils, lxc_commands_utils
lxcs = [] lxcs = []
@ -86,7 +86,6 @@ class LXC:
self.privileged = options["privileged"] self.privileged = options["privileged"]
self.start_on_boot = options["start_on_boot"] self.start_on_boot = options["start_on_boot"]
self.password = options["password"] self.password = options["password"]
self.ssh = options["ssh"]
self.creation = creation self.creation = creation
self.deploy = deploy self.deploy = deploy
@ -283,12 +282,9 @@ class LXC:
""" """
return self.password return self.password
def is_ssh_enabled(self): def get_ssh_string(self):
""" # TODO: implement hostname and maybe ipv6?
Is SSH enabled return "root@" + self.get_ipv4()
:return: ssh
"""
return self.ssh
def get_deploy(self): def get_deploy(self):
""" """
@ -311,6 +307,13 @@ class LXC:
""" """
return proxmox_utils.run_command_on_pve(f"pct list | awk '/running/ && /{self.lxc_id}/'") != "" return proxmox_utils.run_command_on_pve(f"pct list | awk '/running/ && /{self.lxc_id}/'") != ""
def does_exist(self):
"""
Does exist
:return: does lxc exist? (boolean)
"""
return proxmox_utils.does_lxc_exist(self.lxc_id)
def start(self): def start(self):
""" """
Start LXC Start LXC
@ -326,13 +329,17 @@ class LXC:
""" """
Create LXC Create LXC
""" """
if proxmox_utils.does_lxc_exist(self.lxc_id): if self.does_exist():
logging.info(f"LXC {self.lxc_id} already exists, skipping creation") logging.info(f"LXC {self.lxc_id} already exists, skipping creation")
proxmox_utils.run_command_on_pve(self.get_pct_command(create=False), True) proxmox_utils.run_command_on_pve(self.get_pct_command(create=False), True)
else: else:
logging.info(f"Creating LXC {self.lxc_id}") logging.info(f"Creating LXC {self.lxc_id}")
proxmox_utils.run_command_on_pve(self.get_pct_command(create=True), True) proxmox_utils.run_command_on_pve(self.get_pct_command(create=True), True)
if proxmox_utils.get_config()['settings']['setAuthorizedHostsSSH']:
pve_public_key = proxmox_utils.get_ssh_public_key()
self.run_command(f"echo '{pve_public_key}' >> /root/.ssh/authorized_keys")
def creation(self): def creation(self):
""" """
Run the creations checks and steps Run the creations checks and steps
@ -343,21 +350,37 @@ class LXC:
self.start() self.start()
# Check if all creation conditions are met # Check if all creation conditions are met
if not creation_utils.creation_conditions_met(self): if not creation_utils.are_all_conditions_met(self):
logging.info(f"Not all creation conditions met for LXC {self.lxc_id}, running creation steps...") logging.info(f"Not all creation conditions met for LXC {self.lxc_id}, running creation steps...")
# Run creation steps # Run creation steps
creation_utils.run_creation_steps(self) creation_utils.run_steps(self)
def deploy(self): def deploy(self):
pass pass
def has_program(self, program): def has_program(self, program):
return creation_utils.has_program(self, program) """
Check if program is installed on LXC
:param program: program executable name
def run_command(self, command, warn_exit_status=False, only_code=False): :return: boolean
"""
if type(program) == str:
return self.run_command("which " + program, only_code=True) == 0
elif type(program) == list:
return all((self.run_command("which " + p, only_code=True) == 0) for p in program)
def run_script(self, script_path):
"""
Run script on LXC filesystem using bash
:param script_path:
:return:
"""
return lxc_commands_utils.run_script(self, {"lxc_path": script_path})
def run_command(self, command, warn_exit_status=False, only_code=False, working_directory=None):
""" """
Run command on LXC Run command on LXC
:param command: command to run :param command: command to run
@ -365,6 +388,10 @@ class LXC:
""" """
logging.debug(f"Running command {command} on LXC {self.lxc_id}") logging.debug(f"Running command {command} on LXC {self.lxc_id}")
if working_directory:
command = f"cd {working_directory} && {command}"
if only_code: 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, only_code)

View File

@ -58,55 +58,72 @@ def execute_tteck_script(script_url, env_variables):
run_command_on_pve(f"{env_variables} && bash -c \"$(wget -qLO - {script_url}\"", True) run_command_on_pve(f"{env_variables} && bash -c \"$(wget -qLO - {script_url}\"", True)
def run_command_on_pve(command, warn_exit_status=False, only_code=False): def get_config():
config_file = os.path.join(project_path / "resources" / "config.json")
with open(config_file, "r") as file:
return json.loads(file.read())
def run_command_on_pve(command, warn_exit_status=False, only_code=False, local=False):
""" """
Run command on PVE Run command on PVE
:param local: should be run locally not via ssh even with local mode off
:param only_code: should we only return the exit code and not the stdout :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 warn_exit_status: should an exception be thrown if the exit status is not 0
:param command: command :param command: command
:return: command :return: command
""" """
config_file = os.path.join(project_path / "resources" / "config.json") # Get config
with open(config_file, "r") as file: config = get_config()
data = json.loads(file.read()) # Check if PVE is local or remote
if config['pve']['local'] or local:
# Run command and return output (not as bytes)
logging.debug(f"Running command on PVE (locally): \n{command}")
# Check if PVE is local or remote command = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
if data['pve']['local']: encoding="utf-8")
# Run command and return output (not as bytes)
logging.debug(f"Running command on PVE (locally): \n{command}")
command = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, if command.returncode != 0 and warn_exit_status:
encoding="utf-8") 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 command.returncode != 0 and warn_exit_status: if only_code:
logging.error(f"Error while running command on PVE: \n{command.stderr}") return command.returncode
raise Exception(f"Error while running command on PVE: \n{command.stderr}")
if only_code: return command.stdout.decode().rstrip()
return command.returncode
return command.stdout.decode().rstrip() else:
host = config['pve']['host']
username = config['pve']['user']
port = config['pve']['port']
else: # Run command on PVE via SSH and return output
host = data['pve']['host'] logging.debug(f"Running command on PVE (ssh): \n{command}")
username = data['pve']['user']
port = data['pve']['port']
# Run command on PVE via SSH and return output # catch errors code
logging.debug(f"Running command on PVE (ssh): \n{command}") command = subprocess.run(f"ssh {username}@{host} -p {port} \"{command}\"", shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
# catch errors code if command.returncode != 0 and warn_exit_status:
command = subprocess.run(f"ssh {username}@{host} -p {port} \"{command}\"", shell=True, logging.error(f"Error while running command on PVE: \n{command.stderr}")
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8") raise Exception(f"Error while running command on PVE: \n{command.stderr}")
if command.returncode != 0 and warn_exit_status: if only_code:
logging.error(f"Error while running command on PVE: \n{command.stderr}") return command.returncode
raise Exception(f"Error while running command on PVE: \n{command.stderr}")
if only_code: return command.stdout.rstrip()
return command.returncode
return command.stdout.rstrip()
def run_command_locally(command, warn_exit_status=False, only_code=False):
run_command_on_pve(command, warn_exit_status, only_code, local=True)
def get_ssh_public_key():
"""
Get SSH public key
:return: ssh public key
"""
return run_command_on_pve("cat ~/.ssh/id_rsa.pub", True)

View File

@ -0,0 +1,8 @@
def get_path(lxc, path):
if path.startswith("/global/"):
# Use global folder
return "resources/scripts" + path[len("/global/"):]
else:
# Use VM/LXC folder
lxc_id = lxc.get_id()
return f"resources/lxc/{lxc_id}/{path}"