Compare commits

...

6 Commits

10 changed files with 518 additions and 156 deletions

View File

@ -90,8 +90,8 @@ The file condition will check if a file exists or not inside the LXC/VM.
"conditions": {
// use a single file
"file": "/var/data/traefikv2/traefik.toml"
// or use a list, but only use one
"files": ["/var/data/traefikv2/traefik.toml", "/var/data/config/traefikv2/docker-compose.yml"]
// or use a list
"file": ["/var/data/traefikv2/traefik.toml", "/var/data/config/traefikv2/docker-compose.yml"]
}
```
It can be an array of file using ``["file1", "file2"]`` or just one file in double quote ``"file"``.
@ -102,8 +102,8 @@ The folder condition will check if a folder exists or not inside the LXC/VM.
"conditions": {
// use a single folder
"folder": "/var/data/traefikv2"
// or use a list, but only use one
"folders": ["/var/data/traefikv2", "/var/data/config/traefikv2"]
// or use a list
"folder": ["/var/data/traefikv2", "/var/data/config/traefikv2"]
}
```
It can be an array of folders using ``["folder1", "folder2"]`` or just one folder in double quote ``"file"``.
@ -114,8 +114,8 @@ The programs condition will check if a program is installed or not in the LXC/VM
"conditions": {
// use a single program
"program": "docker"
// or use a list, but only use one
"programs": ["docker", "docker-compose"]
// or use a list
"program": ["docker", "docker-compose"]
}
```
It can be an array of programs using ``["program1", "program2"]`` or just one program in double quote ``"program"``.
@ -138,7 +138,7 @@ The docker condition will check if a docker (or podman) container is running or
"conditions": {
// use a single container
"container": "traefikv2"
// or use a list, but only use one
// or use a list
"container": ["traefikv2", "portainer"]
}
```
@ -157,7 +157,7 @@ The script step will execute a script.
{
"type": "script",
// use path for local scripts
"path": "/global/install-docker.sh" // local path (here: resources/scripts/install-docker.sh)
"local_path": "global/install-docker.sh" // local path (here: resources/scripts/install-docker.sh)
// or use url for remote scripts
"url": "https://xyz.abc/scripts/install-docker.sh" // remote url
// or use lxc_path for scripts inside the lxc
@ -182,7 +182,7 @@ The file step will create / copy a file to the VM/LXC.
{
"type": "file_copy",
"path": "traefik.toml", // local path (here: resources/lxc/<id>/traefik.toml)
"destination": "/var/data/traefikv2/traefik.toml", // lxc/vm path
"destination": "/var/data/traefikv2/", // lxc/vm path
"permissions": "644" // (optional) permissions of the file
}
]
@ -204,8 +204,8 @@ The folder step will copy a folder to the VM/LXC.
"steps": [
{
"type": "folder_copy",
"path": "/data/", // local path (here: resources/lxc/<id>/data/)
"destination": "/var/data/traefikv2", // lxc/vm path
"path": "data/", // local path (here: resources/lxc/<id>/data/)
"destination": "/var/", // lxc/vm path
"permissions": "755" // (optional) permissions of the folder
}
]
@ -260,7 +260,7 @@ The git step will clone a git repo on the VM/LXC.
}
]
```
*Note: At the the moment, no authentication is supported, so only works on public repo.*
*Note: At the moment, no authentication is supported, so only works on public repo.*
### Download
The download step will download a file on the VM/LXC.
@ -270,8 +270,8 @@ The download step will download a file on the VM/LXC.
"type": "download",
// download a single file
"url": "https://git.abc.xyz/file.tar.gz", // download url
// or use a list of urls, but only use one
"urls": ["https://git.abc.xyz/file1.tar.gz", "https://git.abc.xyz/file2.tar.gz"] // download urls
// or use a list of urls
"url": ["https://git.abc.xyz/file1.tar.gz", "https://git.abc.xyz/file2.tar.gz"] // download urls
"destination": "/tmp/" // lxc/vm path
}
]
@ -298,8 +298,8 @@ The install-package step will install a package on the VM/LXC.
"type": "install-package",
// install a single package
"package": "git",
// or use a list of packages, but only use one
"packages": ["git", "docker"]
// or use a list of packages
"package": ["git", "docker"]
}
]
```
@ -314,8 +314,8 @@ The remove-package step will remove a package on the VM/LXC.
"type": "remove-package",
// remove a single package
"package": "git"
// or use a list of packages, but only use one
"packages": ["git", "docker"]
// or use a list of packages
"package": ["git", "docker"]
}
]
```
@ -357,8 +357,8 @@ The replace-in-file step will replace a string in a file.
"type": "replace-in-file",
// replace in a single file
"path": "/var/data/config/traefikv2/traefik.toml", // inside lxc/vm
// or use a list of files, but only use one
"paths": ["/var/data/config/traefikv2/traefik.toml", "/var/data/config/traefikv2/traefik2.toml"] // inside lxc/vm
// or use a list of files
"path": ["/var/data/config/traefikv2/traefik.toml", "/var/data/config/traefikv2/traefik2.toml"] // inside lxc/vm
"search": "abc", // string to search
"replace": "xyz", // string to replace
"case_sensitive": true, // (optional) case sensitive? default: true

View File

@ -0,0 +1,62 @@
#!/bin/bash
if which ssh >/dev/null 2>&1; then
echo "SSH is installed"
exit 1
else
echo "SSH is not installed"
fi
if lsb_release -a 2>/dev/null | grep -q -E "Debian|Ubuntu"; then
echo "Running Debian or Ubuntu"
apt-get install openssh-server
systemctl start sshd
elif cat /etc/os-release 2>/dev/null | grep -q -i "alpine"; then
echo "Running Alpine"
# Stop and remove Dropbear
rc-service dropbear stop
rc-update del dropbear
apk del dropbear* -f
# Stop and remove SSHD
rc-service sshd stop
rc-update del sshd
apk del openssh* -f
# Clean up Dropbear and SSH configurations
rm -rf /etc/dropbear
rm -rf /etc/ssh
rm /etc/init.d/ssh
# Reboot now if needed
# reboot now
# Install OpenSSH and necessary packages
apk add openssh gcompat libstdc++ curl bash git grep
apk add procps --no-cache
# Start and add SSHD to startup
rc-service sshd start
rc-update add sshd
# Update AllowTcpForwarding setting
sed -i 's/^#*AllowTcpForwarding.*/AllowTcpForwarding yes/' /etc/ssh/sshd_config
# Update PermitTunnel setting
sed -i 's/^#*PermitTunnel.*/PermitTunnel yes/' /etc/ssh/sshd_config
# Uncomment the line if needed (remove '#' at the beginning)
sed -i 's/^#PermitTunnel.*/PermitTunnel yes/' /etc/ssh/sshd_config
# Restart the SSH service to apply the changes
/etc/init.d/sshd restart
else
echo "Unknown distribution"
exit 1
fi
# Set PermitRootLogin to prohibit-password
sed -i 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config

View File

@ -1,3 +1,5 @@
#!/bin/bash
if which docker >/dev/null 2>&1; then
echo "Docker is installed"
exit 1

View File

@ -7,7 +7,8 @@ from ..utils import proxmox_utils
class LXC:
"""LXC object"""
def __init__(self, lxc_id, lxc_hostname, os, resources, network, options, creation, deploy):
def __init__(self, lxc_id: int, lxc_hostname: str, os: dict, resources: dict, network: dict, options: dict,
creation: dict, deploy: dict):
self.lxc_id = lxc_id
self.lxc_hostname = lxc_hostname
@ -52,9 +53,12 @@ class LXC:
return hash(self.lxc_id)
def get_id(self):
"""
Get LXC ID
:return: lxc id
"""Get LXC ID
Returns
-------
int
LXC ID
"""
return self.lxc_id
@ -95,7 +99,7 @@ class LXC:
# TODO: might have to run "pveam update" before running this command on fresh install of PVE
template_name = proxmox_utils.run_command_on_pve(
f"pveam available --section system | awk /'{self.os_name}-{self.os_release}/' | awk '{{print \\$2}}'")
f"pveam available --section system | awk /\'{self.os_name}-{self.os_release}/\' | awk \'{{print $2}}\'")
is_template_downloaded = proxmox_utils.run_command_on_pve(command=f"pveam list local | awk /'{template_name}/'")
@ -170,7 +174,7 @@ class LXC:
if self.is_running():
if self.has_program("ip"):
return self.run_command(
command="ip addr | grep 'state UP' -A2 | tail -n1 | awk '{print \$2}' | cut -f1 -d'/'")
command="ip addr | grep \'state UP\' -A2 | tail -n1 | awk \'{{print $2}}\' | cut -f1 -d\'/\'")
elif self.has_program("ifconfig"):
return self.run_command(command="ifconfig eth0 | awk '/inet addr/{print substr($2,6)}'")
@ -307,6 +311,11 @@ class LXC:
logging.info(f"Creating LXC {self.lxc_id}")
proxmox_utils.run_command_on_pve(command=self.get_pct_command(create=True), warn_exit_status=True)
self.start()
logging.info("Setting up SSH for LXC")
commands_utils.run_script(lxc=self, step={"local_path": "protected/scripts/install-config-ssh.sh"})
def run_creation(self):
"""
Run the creations checks and steps

View File

@ -1,10 +1,11 @@
from pathlib import Path
from . import LXC
from ..utils import proxmox_utils
from ..utils.resources_utils import get_path
def run_script(lxc, step):
def run_script(lxc: LXC, step: dict):
"""Method use to dispatch the script to the correct method depending on the type of script, being
local (on the machine running this program), remote (url) or in the LXC.
@ -13,7 +14,7 @@ def run_script(lxc, step):
lxc : LXC
The LXC object used to run the script
step: dict
Dictionary containing the step information about the script to run
Dictionary containing the step information and configuration about the script to run
Typically read from the "creation/steps/<step>" in JSON file
"""
@ -23,8 +24,9 @@ def run_script(lxc, step):
install_package(lxc, "bash")
# Run local script
if "path" in step:
_run_local_script_on_lxc(lxc, step["path"])
if "local_path" in step:
path = get_path(lxc, step["local_path"])
_run_local_script_on_lxc(lxc, path)
# Run remote script
elif "url" in step:
@ -32,33 +34,78 @@ def run_script(lxc, step):
# Run script in LXC
elif "lxc_path" in step:
_run_script_on_lxc(lxc, step["lxc_path"])
path = get_path(lxc, step["lxc_path"])
_run_script_on_lxc(lxc, path)
def _run_script_on_lxc(lxc, path: "Path inside the LXC"):
"""Run a script in the LXC"""
def _run_script_on_lxc(lxc: LXC, path: Path):
"""Runs a script present inside the LXC storage, without copying or downloading it
lxc.run_command(f"bash {path}")
Parameters
----------
lxc: LXC
The LXC object used to run the script
path: pathlib.Path
Path to the script inside the LXC storage
"""
lxc.run_command(f"bash {str(path)}")
def _run_local_script_on_lxc(lxc, path: "Path to the script on the machine running this program"):
"""Run a local script in the LXC"""
def _run_local_script_on_lxc(lxc: LXC, path: Path):
"""Runs a script present on the machine running this program, inside the LXC
It works by copying the script from the local machine to the PVE using SCP and
then using "pct push" to send it to the LXC.
path = get_path(lxc, path)
with open(path, "r") as file:
script = file.read()
lxc.run_command("bash <<EOF\n" + script + "\nEOF")
Parameters
----------
lxc: LXC
The LXC object used to run the script
path: pathlib.Path
Path object to the script on the local machine (machine running this program)
"""
# Copy the script to /tmp and run it from there
proxmox_utils.copy_file_to_lxc(lxc.get_id(), str(path), "/tmp/pdj-scripts/")
lxc.run_command(f"bash /tmp/pdj-scripts/{path.name}")
# with open(path, "r") as file:
# script = file.read()
#
# # TODO : Might work with EOF instead of copying the script to /tmp but unable to make it work
# # lxc.run_command("bash <<EOF\n" + script + "\nEOF")
def _run_remote_script_on_lxc(lxc, url: "URL to the script"):
"""Run a remote script in the LXC"""
def _run_remote_script_on_lxc(lxc: LXC, url: str):
"""Runs a script from a URL on a LXC
Usually this should not be called directly but rather through the run_script() method with the
according step configuration (local_path, url or lxc_path) which is itself usually read from the JSON file.
Parameters
----------
lxc: LXC
The LXC object used to run the script
url: str
URL of the script to run
"""
if lxc.has_program("curl"):
lxc.run_command(f"curl -sSL {url} | bash")
def create_folder_in_lxc(lxc, path, permission=755):
"""Create a folder in the LXC"""
def create_folder_in_lxc(lxc: LXC, path: str, permission=755):
"""Create a folder in the LXC
It's possible to specify the permission of the folder, by default it uses 755
Parameters
----------
lxc: LXC
The LXC object of the LXC to create the folder in
path: str
Path to the folder to create in the LXC
permission: int, optional
Permission of the folder to create in the LXC (default is 755)
"""
lxc.run_command(f"mkdir -p {path}")
@ -66,47 +113,72 @@ def create_folder_in_lxc(lxc, path, permission=755):
lxc.run_command(f"chmod -R {permission} {path}")
def create_file_in_lxc(lxc, path, permission=644):
"""Create a file in the LXC"""
def create_file_in_lxc(lxc: LXC, path: str, permission=644):
"""Create a file in the LXC
It's possible to specify the permission of the file, by default it uses 644
Parameters
----------
lxc: LXC
The LXC object of the LXC to create the file in
path: str
Path to the file to create in the LXC
permission: int, optional
Permission of the file to create in the LXC (default is 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):
def remove_file_in_lxc(lxc: LXC, path: str):
"""Remove a file in the LXC
Parameters
----------
lxc: LXC
The LXC object of the LXC to remove the file in
path: str
Path to the file to remove in the LXC
"""
lxc.run_command(f"rm {path}")
def copy_local_file_to_lxc(lxc: LXC, path: Path, destination: str):
"""Copy a local file to the LXC
Parameters
----------
lxc : LXC
The LXC object of the LXC to copy the file to
path: str
Path to the file on the machine running this program
path: pathlib.Path
Path object to the file on the machine running this program
destination: str
Path to the destination in the LXC
"""
proxmox_utils.run_command_locally(f"scp {path} {lxc.get_ssh_string()}:{destination}")
proxmox_utils.run_command_locally(f"scp {str(path)} {lxc.get_ssh_string()}:{destination}")
def copy_local_folder_to_lxc(lxc, path, destination):
def copy_local_folder_to_lxc(lxc: LXC, path: Path, destination: str):
"""Copy a local folder to the LXC
Parameters
----------
lxc : LXC
The LXC object of the LXC to copy the folder to
path: str
Path to the folder on the machine running this program
path: pathlib.Path
Path object to the folder on the machine running this program
destination: str
Path to the destination in the LXC
"""
proxmox_utils.run_command_locally(f"scp -r {path} {lxc.get_ssh_string()}:{destination}")
proxmox_utils.run_command_locally(f"scp -r {str(path)} {lxc.get_ssh_string()}:{destination}")
def run_docker_command(lxc, container, command):
def run_docker_command(lxc: LXC, container: str, command: str):
"""Run a command inside a docker container on the LXC
Parameters
@ -117,11 +189,16 @@ def run_docker_command(lxc, container, command):
Name of the docker container to run the command in
command : str
Command to run in the docker container
Examples
--------
>>> run_docker_command(lxc, "<container-name>", "<command>")
"""
lxc.run_command(f"docker exec -it {container} {command}")
def run_docker_compose_command(lxc, command, working_directory=None):
def run_docker_compose_command(lxc: LXC, command: str, working_directory: str = None):
"""Run a docker-compose command in the LXC
Parameters
@ -130,13 +207,14 @@ def run_docker_compose_command(lxc, command, working_directory=None):
The LXC object of the LXC to run the docker-compose command in
command: str
The docker-compose command to run
working_directory: str
working_directory: str, optional
The working directory to run the command in
Examples
--------
>>> run_docker_compose_command(lxc, "up -d", "/home/user/traefik")
"""
docker_compose_exec = "docker-compose"
if not lxc.has_program(docker_compose_exec):
docker_compose_exec = "docker compose"
@ -147,16 +225,33 @@ def run_docker_compose_command(lxc, command, working_directory=None):
lxc.run_command(f"{docker_compose_exec} {command}")
def download_file(lxc, url, destination):
def download_file(lxc: LXC, url: str, destination: str):
"""Download a file from a URL to the LXC and save it to the destination
Parameters
----------
lxc: LXC
The LXC object of the LXC to download the file to
url: str
URL of the file to download
destination: str
Path to the destination to save the file to
Needs to end with a trailing slash
Examples
--------
>>> download_file(lxc, "https://example.com/file.zip", "/home/user/")
"""
"""Download a file from a URL to the LXC"""
if type(url) is list:
for u in url:
lxc.run_command(f"wget {u} -O {destination}")
lxc.run_command(f"wget {u} --directory-prefix={destination}")
else:
lxc.run_command(f"wget {url} -O {destination}")
lxc.run_command(f"wget {url} --directory-prefix={destination}")
def unzip_file(lxc, path, destination=None):
def unzip_file(lxc: LXC, path: str, destination: str = None):
"""Unzip a file in the LXC
Parameters
@ -165,13 +260,15 @@ def unzip_file(lxc, path, destination=None):
The LXC object of the LXC to unzip the file in
path: str
Path to the file to unzip
destination: str
destination: str, optional
Path to the destination to unzip the file to
If not specified, it will unzip the file in the same directory as the file
Needs to end with a trailing slash
Examples
--------
>>> unzip_file(lxc, "/home/user/file.zip", "/home/user/destination")
>>> unzip_file(lxc, "/home/user/file.tar.gz", "/home/user/destination")
>>> unzip_file(lxc, "/home/user/file.zip", "/home/user/extracted_files/")
>>> unzip_file(lxc, "/home/user/file.tar.gz", "/home/user/extracted_files/")
"""
if destination is None:
@ -180,10 +277,10 @@ def unzip_file(lxc, path, destination=None):
if ".zip" in path:
lxc.run_command(f"unzip {path} -d {destination}")
elif ".tar.gz" in path:
lxc.run_command(f"tar -xzf {path} -C {destination}")
lxc.run_command(f"mkdir -p {destination} && tar -xzf {path} --directory {destination}")
def install_package(lxc, package):
def install_package(lxc: LXC, package: str or list):
"""Install a package in the LXC
Parameters
@ -191,7 +288,7 @@ def install_package(lxc, package):
lxc: LXC
The LXC object of the LXC to install the package in
package: str or list
Name of the package to install
Name(s) of the package(s) to install
Examples
--------
@ -206,7 +303,7 @@ def install_package(lxc, package):
lxc.run_command(f"{proxmox_utils.get_install_package_command(lxc.get_os_name())} {package}")
def remove_package(lxc, package):
def remove_package(lxc: LXC, package: str or list):
"""Remove a package in the LXC
Parameters
@ -214,7 +311,7 @@ def remove_package(lxc, package):
lxc: LXC
The LXC object of the LXC to remove the package in
package: str or list
Name of the package to remove
Name(s) of the package(s) to remove
Examples
--------
@ -232,20 +329,20 @@ def remove_package(lxc, package):
lxc.run_command(f"{proxmox_utils.get_remove_package_command(lxc.get_os_name())} {package}")
def replace_in_files(lxc, path, search, replace, case_sensitive=False):
def replace_in_files(lxc: LXC, path: str or list, search: str, replace: str, case_sensitive: bool = False):
"""Replace a string in one or multiples files in the LXC
Parameters
----------
lxc : LXC
The LXC object of the LXC to replace the string in
path : str or list
path : str or list of str
Path to the file(s) to replace the string in
search : str
String to search for
replace : str
String to replace the search string with
case_sensitive : bool
case_sensitive : bool, optional
Whether the search should be case sensitive or not
Examples
@ -258,6 +355,6 @@ def replace_in_files(lxc, path, search, replace, case_sensitive=False):
if type(path) is list:
for p in path:
lxc.run_command(f"sed -i {'-i' if case_sensitive else ''} 's/{search}/{replace}/g' {p}")
lxc.run_command(f"sed {'-i' if case_sensitive else ''} 's/{search}/{replace}/g' {p}")
else:
lxc.run_command(f"sed -i {'-i' if case_sensitive else ''} 's/{search}/{replace}/g' {path}")
lxc.run_command(f"sed {'-i' if case_sensitive else ''} 's/{search}/{replace}/g' {path}")

View File

@ -1,10 +1,11 @@
import logging
from pathlib import Path
from . import commands_utils
from . import commands_utils, LXC
from ..utils.resources_utils import get_path
def are_all_conditions_met(lxc):
def are_all_conditions_met(lxc: LXC):
"""
Check conditions for running the creation steps for an LXC.
@ -43,28 +44,29 @@ def are_all_conditions_met(lxc):
return all(result)
def optional(var, placeholder):
def optional(var: any, placeholder: any):
"""Return a placeholder if the given variable is None, otherwise return the variable
Parameters
----------
var
var: any
Variable to check if it is None
placeholder
placeholder: any
Placeholder to return if the variable is None
Returns
-------
var or placeholder
any[any]
The variable if it is not None, otherwise the placeholder
"""
if var is None:
return placeholder
else:
return var
def run_steps(lxc):
def run_steps(lxc: LXC):
"""Run the creation steps for the given LXC
Parameters
@ -85,11 +87,11 @@ def run_steps(lxc):
case "file_create":
commands_utils.create_file_in_lxc(lxc, step["path"], optional(step["permission"], 644))
case "file_copy":
commands_utils.copy_local_file_to_lxc(lxc, step["path"], step["destination"])
commands_utils.copy_local_file_to_lxc(lxc, get_path(lxc, step["path"]), step["destination"])
case "folder":
commands_utils.create_folder_in_lxc(lxc, step["path"], optional(step["permission"], 755))
case "folder_copy":
commands_utils.copy_local_folder_to_lxc(lxc, step["path"], step["destination"])
commands_utils.copy_local_folder_to_lxc(lxc, get_path(lxc, step["path"]), step["destination"])
case "command":
lxc.run_command(command=step["command"], working_directory=optional(step["working_directory"], None))
case "docker":

View File

@ -1,4 +1,5 @@
import json
from pathlib import Path
from . import LXC
@ -17,12 +18,12 @@ def get_all_lxcs():
return lxcs
def get_lxc(lxc_id):
def get_lxc(lxc_id: int):
"""Get LXC by ID
Parameters
----------
lxc_id : str
lxc_id : int
ID of the LXC to get
Returns
@ -38,23 +39,23 @@ def get_lxc(lxc_id):
return None
def load_lxc(file, lxc_id):
def load_lxc(lxc_id: int, filepath: Path):
"""Load LXC from JSON file
Parameters
----------
file : str
Path to the JSON file of the LXC to load
lxc_id : str
lxc_id : int
ID of the LXC to load
filepath : pathlib.Path
Path object to the JSON file of the LXC to load
Examples
--------
>>> load_lxc("./resources/lxc/100/config.json", "100")
>>> load_lxc(100, Path("<full-path>/resources/lxc/100/config.json"))
"""
# Load JSON data
data = json.loads(file)
data = json.loads(filepath.read_text())
# Extract values from JSON
lxc_id = lxc_id

View File

@ -1,5 +1,5 @@
import logging
import os
from pathlib import Path
from . import project_path
from .lxc.utils import load_lxc, get_all_lxcs
@ -7,19 +7,17 @@ from .lxc.utils import load_lxc, get_all_lxcs
def run():
# Read all files in the resources directory
resources = os.listdir(project_path / "resources")
resources = Path(project_path).joinpath("resources").glob("*")
# Go through each LXC file
for resource in resources:
if resource == "lxc":
# Read all files in the LXC directory
lxc_folders = os.listdir(project_path / "resources" / "lxc")
lxc_folders = Path(project_path).joinpath("resources", "lxc").glob("*")
for lxc_folder in lxc_folders:
lxc_file = os.path.join(project_path / "resources" / "lxc", lxc_folder, "config.json")
# Open the file
with open(lxc_file, "r") as file:
# Load the LXC
load_lxc(file.read(), lxc_id=lxc_folder)
lxc_file = Path(project_path).joinpath("resources", "lxc", lxc_folder, "config.json")
load_lxc(filepath=lxc_file, lxc_id=int(lxc_folder.name))
for lxc in get_all_lxcs():
logging.info(f"Loading LXC {lxc.lxc_id}")

View File

@ -1,56 +1,84 @@
import json
import logging
import os
import pathlib
import subprocess
from pathlib import Path
from .. import project_path
def get_pve_version():
"""
Get PVE version
:return: pve version
"""Get PVE version
Returns
-------
str
PVE version
"""
return run_command_on_pve(command="pveversion", warn_exit_status=True)
def get_pve_hostname():
"""
Get PVE hostname
:return: pve hostname
"""Get PVE hostname
Returns
-------
str
PVE hostname
"""
return run_command_on_pve(command="hostname", warn_exit_status=True)
def does_lxc_exist(lxc_id):
"""
Check if LXC exists
def does_lxc_exist(lxc_id: int):
"""Check if an LXC exists with the given ID
:param lxc_id: lxc id
:return: does lxc exists
Parameters
----------
lxc_id: int
Returns
-------
bool
Does LXC exist?
"""
# TODO: only check in VMID column
return lxc_id in run_command_on_pve(command=f"pct list | awk '/{lxc_id}/'")
return str(lxc_id) in run_command_on_pve(command=f"pct list | awk '/{lxc_id}/'")
def does_qemu_vm_exist(vm_id):
def does_qemu_vm_exist(vm_id: int):
"""Check if a QEMU VM exists with the given ID
Parameters
----------
vm_id: int
Returns
-------
bool
Does QEMU VM exist?
"""
Check if QEMU VM exists
:param vm_id: vm id
:return: does qemu vm exists
"""
# TODO: only check in VMID column
return vm_id in run_command_on_pve(command=f"qm list {vm_id}")
return str(vm_id) in run_command_on_pve(command=f"qm list {vm_id}")
def execute_tteck_script(script_url, env_variables):
"""
Execute TTECK script with already filled environment variables to run silently (non-interactive)
def execute_tteck_script(script_url: str, env_variables: list):
"""Execute TTECK script with already filled environment variables to run silently (non-interactive)
:param script_url: script url (github or other)
:param env_variables: list of environment variables
:return: status code
Parameters
----------
script_url: str
script url (github or other)
env_variables: list
list of environment variables
Returns
-------
int
status code
"""
env_variables = " ".join(env_variables)
@ -59,20 +87,46 @@ def execute_tteck_script(script_url, env_variables):
def get_config():
"""Get the JSON content of config.json file as a dict
Returns
-------
dict
JSON content of config.json file
"""
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
def run_command_on_pve(command: str, warn_exit_status: bool = False, only_code: bool = False, local: bool = False):
"""Run command on the Proxmox VE host
Parameters
----------
command: str
command to run
warn_exit_status: bool, optional
should an exception be thrown if the exit status is not 0
only_code: bool, optional
should it only return the exit code and not the stdout
local: bool, optional
should it be run locally not via ssh even with local mode off in the pve section of config.json
Returns
-------
str
command output by default
int
command exit code if only_code is True
Raises
------
Exception
if the exit status is not 0 and warn_exit_status is True
: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 warn_exit_status: should an exception be thrown if the exit status is not 0
:param command: command
:return: command
"""
# Get config
@ -81,7 +135,7 @@ def run_command_on_pve(command, warn_exit_status=False, only_code=False, local=F
# 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): {command}")
logging.debug(f"Running command on PVE/Machine running this script (locally): {command}")
command = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
encoding="utf-8")
@ -117,12 +171,45 @@ def run_command_on_pve(command, warn_exit_status=False, only_code=False, local=F
return command.stdout.rstrip()
def run_command_locally(command, warn_exit_status=False, only_code=False):
def run_command_locally(command: str, warn_exit_status: bool = False, only_code: bool = False):
"""Force running command locally even if local mode is off in config.json
See Also
--------
run_command_on_pve
"""
run_command_on_pve(command=command, warn_exit_status=warn_exit_status, only_code=only_code, local=True)
def run_command_ssh(command, host, username, port=22, warn_exit_status=False, only_code=False):
# Run command on PVE via SSH and return output
def run_command_ssh(command: str, host: str, username: str, port: int = 22, warn_exit_status: bool = False,
only_code: bool = False):
"""Run command on a remote host via SSH
Parameters
----------
command: str
command to run
host: str
host to run the command on
username: str
username to use to connect to the host (usually root for lxc and vm)
port: int, optional
port to use to connect to the host (default: 22)
warn_exit_status: bool, optional
should an exception be thrown if the exit status is not 0
only_code: bool, optional
should it only return the exit code and not the stdout
Returns
-------
str
command output by default
int
command exit code if only_code is True
"""
logging.debug(f"Running command on host {host} (ssh): {command}")
# catch errors code
@ -139,12 +226,24 @@ def run_command_ssh(command, host, username, port=22, warn_exit_status=False, on
return command.stdout.rstrip()
def get_install_package_command(distribution):
"""
Get the install package without interaction command based on the distribution.
def get_install_package_command(distribution: str):
"""Retrieve the correct command to install a package based on the distribution specified
It supports all the distribution supported by Proxmox VE (from the pct command documentation).
Debian, Ubuntu, CentOS, Fedora, Gentoo, Alpine, ArchLinux, Devuan, NixOS, OpenSUSE
:param distribution: Distribution name
:return: Install package command
Parameters
----------
distribution: str
Name of the distribution as specific in the proxmox pct command documentation
See Also
--------
https://pve.proxmox.com/pve-docs/pct.1.html
Returns
-------
str
Beginning of the command to install a package, the package name should be appended to it
"""
distribution = distribution.lower()
@ -173,12 +272,24 @@ def get_install_package_command(distribution):
raise Exception(f"Unsupported distribution: {distribution}")
def get_remove_package_command(distribution):
"""
Get the remove package without interaction command based on the distribution.
def get_remove_package_command(distribution: str):
"""Retrieve the correct command to uninstall a package based on the distribution specified
It supports all the distribution supported by Proxmox VE (from the pct command documentation).
Debian, Ubuntu, CentOS, Fedora, Gentoo, Alpine, ArchLinux, Devuan, NixOS, OpenSUSE
:param distribution: Distribution name
:return: Remove package command
Parameters
----------
distribution: str
Name of the distribution as specific in the proxmox pct command documentation
See Also
--------
https://pve.proxmox.com/pve-docs/pct.1.html
Returns
-------
str
Beginning of the command to uninstall a package, the package name should be appended to it
"""
distribution = distribution.lower()
@ -208,8 +319,59 @@ def get_remove_package_command(distribution):
def get_ssh_public_key():
""" Retrieve the SSH public key of the machine running this script
Returns
-------
str
SSH public key
"""
Get SSH public key
:return: ssh public key
"""
# TODO: maybe implement custom path for the key
return run_command_on_pve(command="cat ~/.ssh/id_rsa.pub", warn_exit_status=True)
def copy_file_to_pve(path: Path, destination: str):
"""Copy a local file (on the machine running this script) to the PVE
Parameters
----------
path: pathlib.Path
Path object to file on local machine
destination: str
Destination on PVE
Examples
--------
>>> copy_file_to_pve(Path("/home/user/file.txt"), "/root/file.txt")
"""
config = get_config()
run_command_locally(f"scp {str(path)} {config['pve']['root']}@{config['pve']['host']}:{destination}")
def copy_file_to_lxc(lxc_id: int, path: Path, destination: str):
"""Copy a local file (on the machine running this script) to the PVE and finally to the LXC
It works by copying the script from the local machine to the PVE using SCP and
then using "pct push" to send it to the LXC.
Sometimes SSH is not installed on freshs LXC, so by using pct push we are sure that it will work.
Parameters
----------
lxc_id: int
LXC ID
path: pathlib.Path
Path object to file on local machine
destination: str
Destination on LXC
Examples
--------
>>> copy_file_to_lxc(100, Path("/home/user/file.txt"), "/root/")
"""
file = path.name
temp_path = f"/tmp/proxmoxdeployscripts/{file}"
copy_file_to_pve(path, temp_path)
run_command_on_pve(f"pct push {lxc_id} {temp_path} {destination}")

View File

@ -1,15 +1,44 @@
import os
import pathlib
from .. import project_path
from ..lxc import LXC
def get_path(lxc, path):
parent = os.path.join(project_path / "resources")
def get_path(lxc: LXC, path: str):
"""Returns the complete path to a global, protected or LXC-relative resource file.
if path.startswith("/global/"):
For example, if the path is "global/test.txt", the function will return the complete path to
the file in the resources/ folder.
Another example, if the path is "protected/test.txt", the function will return the complete path to
the file in the protected_resources/ folder.
Other folders can be added like "global/scripts/test.sh" or "protected/scripts/test.sh".
If none of the above conditions are met, the function will return the path relative to the LXC folder
depending on the given LXC object.
Parameters
----------
lxc: LXC
LXC object to use to get the relative path
Can be omitted if you know what you are doing and the path is either global or protected.
path: str
Path to the resource file
Returns
-------
pathlib.Path
Complete path to the resource file
"""
parent = pathlib.Path(project_path).joinpath("resources")
if path.startswith("global/"):
# Use global folder
return parent + "/scripts/" + path[len("/global/"):]
return parent.joinpath(path[len("global/"):])
elif path.startswith("protected/"):
# Use protected folder
return parent.parent.joinpath("protected_resources", path[len("protected/"):])
else:
# Use VM/LXC folder
lxc_id = lxc.get_id()
return parent + f"/resources/lxc/{lxc_id}/{path}"
return parent.joinpath("lxc", lxc_id, path)