1
0
Fork 0

Compare commits

..

10 commits

Author SHA1 Message Date
d1e4452a5d
containerctl: Fix log_error() not logging a timestamp
As ${TODAY} was forgotten, the first argument from ${@} was used as a
timestamp instead, resulting in malformed log messages. Fix this by
adding ${TDOAY} before ${@}.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-26 16:51:40 +02:00
131ff71f34
generate: container: Use ",".join() and fix Secret.command()
Use ",".join() instead of the custom join() method and fix
Secret.command() by checking if secret_type and not target is mount...

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 16:50:24 +02:00
2216bf68da
generate: generate: Remove debug statements
No longer needed.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 16:49:55 +02:00
ca9d914449
generate: container: Remove command_join()
It is not used anywhere, so it can be safely removed.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 16:20:51 +02:00
6963fdea2a
generate: generate: Explicitly catch ConfigError
Catch ConfigError instead of Exception, which also renders noqa BLE001
obsolete.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 16:17:46 +02:00
72062edbf5
generate: container: Fix some issues reported by pylint
This fixes various issues reported by pylint. It now only reports to
issues, that can be ignored, since they're only about too many local
variables or class attributes.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 16:15:47 +02:00
e2f2b3e35f
generate: container: Fix spelling mistake
Search, not serach...

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 15:58:53 +02:00
53622827a0
generate: log: Add encoding=utf-8 to Path.open
Specify the encoding to use for the logfile.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 15:57:23 +02:00
7200d09621
containerctl: Add containerctl script
Add script to generate and exec control scripts.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 15:51:26 +02:00
b0df681fea
generate: generate: Implement writing the control scripts
Implement writing all control scripts and making them executable.

Signed-off-by: Enno Tensing <tenno@suij.in>
2025-07-22 14:45:52 +02:00
4 changed files with 196 additions and 41 deletions

134
containerctl Executable file
View file

@ -0,0 +1,134 @@
#!/bin/sh
BASEDIR="/var/lib/containerctl"
CONTAINERDIR="${BASEDIR}/containers"
CONFIGDIR="${BASEDIR}/configs"
LOGDIR="/var/log/containerctl"
TODAY="$(date '+%F')"
LOG="${LOGDIR}/${TODAY}"
log_error()
{
printf '[%b] (EE) %b\n' "${TODAY}" "${@}" | tee -a "${LOG}"
}
list_dir()
{
find "${1}" -mindepth 1 -type d
}
list_containers()
{
list_dir "${CONTAINERDIR}"
}
list_configs()
{
list_dir "${CONFIGDIR}"
}
exec_script()
{
container="${1}"
script="${2}"
if [ -z "${container}" ]
then
log_error "No container passed"
exit 1
fi
if [ -z "${script}" ]
then
log_error "No script passed"
exit 1
fi
if ! [ -e "${CONTAINERDIR}/${container}" ]
then
log_error "${container} does not exist, did you forget to generate it?"
exit 1
fi
if ! [ -d "${CONTAINERDIR}/${container}" ]
then
log_error "${container} does not exist"
exit 1
fi
if ! [ -e "${CONTAINERDIR}/${container}/${script}" ]
then
log_error "${script} is invalid, maybe try regenerating the container?"
exit 1
fi
if ! [ -f "${CONTAINERDIR}/${container}/${script}" ]
then
log_error "${script} is invalid, please recreate the container"
exit 1
fi
/bin/sh "${CONTAINERDIR}/${container}/${script}" || log_error "${script} failed!"
}
generate_container()
{
config="${1}"
if [ -z "${config}" ]
then
log_error "No config was passed."
exit 1
fi
if ! [ -e "${CONFIGDIR}" ]
then
log_error "${CONFIGDIR} does not exist"
exit 1
fi
if ! [ -d "${CONFIGDIR}" ]
then
log_error "${CONFIGDIR} is not a directory"
exit 1
fi
if ! [ -e "${CONFIGDIR}/${config}" ]
then
log_error "${config} does not exist!" \
"Please check the name and or location of the desired config."
exit 1
fi
if ! [ -f "${CONFIGDIR}/${config}" ]
then
log_error "${config} is not a file."
exit 1
fi
/usr/bin/env python3 "${BASEDIR}/generate/generate.py" \
"${CONFIGDIR}/${config}" "${LOG}" "${CONTAINERDIR}"
if [ "${?}" = 1 ]
then
log_error "Container generation failed."
exit 1
fi
}
generate_all()
{
list_configs | while read -r config
do
generate_config "$(printf '%b' "${config}" | sed -e "s|${CONFIGDIR}||g")"
done
}
case "${1}" in
"list-containers") list_containers ;;
"list-configs") list_configs ;;
"generate-all") generate_all ;;
"generate") shift; generate_config "${@}" ;;
*) exec_script "${@}" ;;
esac

View file

@ -17,25 +17,13 @@ class ConfigError(Exception):
def __init__(self, message: str = "") -> None: def __init__(self, message: str = "") -> None:
"""Create Exception object.""" """Create Exception object."""
self.message = message self.message = message
self.super.__init__(message) super().__init__(message)
def __str__(self) -> str: def __str__(self) -> str:
"""Convert Exception object to a string.""" """Convert Exception object to a string."""
return f"Configuration error: {self.message}" return f"Configuration error: {self.message}"
def join(data: list, separator: str = ",") -> str:
"""Join a list together."""
ret: str = ""
x = data.copy()
if len(x) == 0:
return ret
while len(x) > 1:
ret += x.pop() + separator
ret += x.pop()
return ret
def maybe(json: dict, key: str) -> str | dict | list | bool | None: def maybe(json: dict, key: str) -> str | dict | list | bool | None:
"""Maybe get a value.""" """Maybe get a value."""
try: try:
@ -44,20 +32,11 @@ def maybe(json: dict, key: str) -> str | dict | list | bool | None:
return None return None
def command_join(items: list | None) -> str:
"""Like join(), but call command()."""
joined = ""
if items is not None:
for item in items:
joined += " " + item.command()
return joined
def trim(s: str) -> str: def trim(s: str) -> str:
"""Remove sequential whitespace.""" """Remove sequential whitespace."""
s = s.replace("\t ", "\t") s = s.replace("\t ", "\t")
s = s.replace("\t\\\n", " ") s = s.replace("\t\\\n", " ")
while s.__contains__(" "): while " " in s:
s = s.replace(" ", " ") s = s.replace(" ", " ")
return s return s
@ -141,7 +120,8 @@ class Secret:
cmd = ( cmd = (
f"--secret {self.name},type={self.secret_type},target={self.target}" f"--secret {self.name},type={self.secret_type},target={self.target}"
) )
if self.target == "mount" and self.options != "": # Not a password, ruff...
if self.secret_type == "mount" and self.options != "": # noqa: S105
cmd = f"{cmd},{self.options}" cmd = f"{cmd},{self.options}"
return cmd return cmd
@ -236,10 +216,15 @@ class Ports:
def command(self) -> str: def command(self) -> str:
"""Option for podman container create.""" """Option for podman container create."""
ports = "" ports = ""
for port in self.tcp_ports: seperator = " \\\n"
ports += f"\t--port {port}/tcp \\\n" ports += seperator.join(
for port in self.udp_ports: [f"\t--port {port}/tcp" for port in self.tcp_ports]
ports += f"\t--port {port}/udp \\\n" )
ports += seperator
ports += seperator.join(
[f"\t--port {port}/udp" for port in self.udp_ports]
)
ports += seperator
return ports return ports
@ -269,7 +254,7 @@ class Network:
if self.mode == "": if self.mode == "":
return "" return ""
cmd = f"--network={self.mode}" cmd = f"--network={self.mode}"
opts = join(self.options) opts = ",".join(self.options)
if opts != "": if opts != "":
cmd += f":{opts}" cmd += f":{opts}"
return cmd return cmd
@ -345,15 +330,15 @@ class Dns:
def command(self) -> str: def command(self) -> str:
"""Option for podman container create.""" """Option for podman container create."""
if len(self.servers) == 0 and self.serach == "": if len(self.servers) == 0 and self.search == "":
return "" return ""
if len(self.servers) == 0: if len(self.servers) == 0:
return f"--dns-search={self.search}" return f"--dns-search={self.search}"
if self.search == "": if self.search == "":
return f"--dns={join(self.servers)}" return f"--dns={','.join(self.servers)}"
cmd = f"--dns-search={self.search} \\\n\t--dns=" cmd = f"--dns-search={self.search} \\\n\t--dns="
cmd += join(self.servers) cmd += ",".join(self.servers)
return cmd return cmd
@ -447,7 +432,7 @@ class Container:
def create_container(self) -> str: def create_container(self) -> str:
"""Generate podman container create command.""" """Generate podman container create command."""
cmd = f"# Create container {self.name}" cmd = f"# Create container {self.name}\n"
cmd += "podman container crate \\\n" cmd += "podman container crate \\\n"
cmd += f"\t--name={self.name} \\\n" cmd += f"\t--name={self.name} \\\n"
if self.privileged: if self.privileged:

View file

@ -2,6 +2,7 @@
"""Generate the control files..""" """Generate the control files.."""
import json import json
import stat
import sys import sys
from pathlib import Path from pathlib import Path
@ -48,17 +49,21 @@ def create_container_from_config(data: dict, log: Log) -> Container | None:
def main() -> None: def main() -> None:
"""Run the program.""" """Run the program."""
argc_threshold = 2 config_threshold = 2
if len(sys.argv) < argc_threshold: log_threshold = 3
if len(sys.argv) < config_threshold:
logger = Log("/dev/stdout") logger = Log("/dev/stdout")
logger.log_error("No arguments passed!") logger.log_error("No arguments passed!")
sys.exit(1) sys.exit(1)
config_file = "" config_file = ""
log_file = "" log_file = ""
if len(sys.argv) >= argc_threshold: base = "/var/lib/containerctl/containers"
if len(sys.argv) >= config_threshold:
config_file = sys.argv[1] config_file = sys.argv[1]
if len(sys.argv) > argc_threshold: if len(sys.argv) >= log_threshold:
log_file = sys.argv[2] log_file = sys.argv[2]
if len(sys.argv) > log_threshold:
base = sys.argv[3]
logger = Log(log_file) logger = Log(log_file)
data = load_container_config(Path(config_file), logger) data = load_container_config(Path(config_file), logger)
if data is None: if data is None:
@ -66,11 +71,42 @@ def main() -> None:
sys.exit(1) sys.exit(1)
try: try:
ct = create_container_from_config(data, logger) ct = create_container_from_config(data, logger)
except Exception as e: # noqa: BLE001 except ConfigError as e:
logger.log_error(e) logger.log_error(e)
sys.exit(1) sys.exit(1)
print(ct) scripts = {
print(vars(ct)) "create-volumes": ct.create_volumes,
"create-secrets": ct.create_secrets,
"create-environment": ct.create_environment,
"remove-volumes": ct.remove_volumes,
"remove-secrets": ct.remove_secrets,
"create": ct.create_container,
"remove": ct.remove_container,
"purge": ct.purge_container,
"start": ct.start_container,
"stop": ct.stop_container,
"restart": ct.restart_container,
"upgrade": ct.upgrade_container,
}
base = f"{base}/{ct.name}"
if not Path(base).exists():
Path(base).mkdir()
for script, method in scripts.items():
s = Path(f"{base}/{script}")
with s.open("w+", encoding="utf-8") as f:
f.write("#!/bin/sh\n")
if script == "create":
f.write(ct.create_volumes())
f.write(ct.create_secrets())
f.write(ct.create_environment())
f.write(method())
s.chmod(
stat.S_IRWXU
| stat.S_IRGRP
| stat.S_IXGRP
| stat.S_IROTH
| stat.S_IXOTH,
)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -64,7 +64,7 @@ class Log:
print(msg) print(msg)
if self.logfile == Path("/dev/stdout"): if self.logfile == Path("/dev/stdout"):
return return
with self.logfile.open("a+") as f: with self.logfile.open("a+", encoding="utf-8") as f:
f.write(msg) f.write(msg)
def timestamp(self) -> str: def timestamp(self) -> str: