From 63c83027e433bf4cf0a44531984e8e826749b234 Mon Sep 17 00:00:00 2001 From: Kivi Kaitaniemi Date: Thu, 2 May 2024 01:07:15 +0300 Subject: [PATCH] Add complete example --- example/files/app-config/config.cfg | 1 + example/files/app-config/f.txt | 3 + .../sub-dir/i-will-be-copied-too.txt | 1 + example/files/user-script.sh | 2 + example/my_module.py | 99 ++++++- example/source.py | 254 +++++++++++++++++- 6 files changed, 354 insertions(+), 6 deletions(-) create mode 100644 example/files/app-config/config.cfg create mode 100644 example/files/app-config/f.txt create mode 100644 example/files/app-config/sub-dir/i-will-be-copied-too.txt create mode 100644 example/files/user-script.sh diff --git a/example/files/app-config/config.cfg b/example/files/app-config/config.cfg new file mode 100644 index 0000000..bff80b8 --- /dev/null +++ b/example/files/app-config/config.cfg @@ -0,0 +1 @@ +# Imagine something here diff --git a/example/files/app-config/f.txt b/example/files/app-config/f.txt new file mode 100644 index 0000000..cf66dbf --- /dev/null +++ b/example/files/app-config/f.txt @@ -0,0 +1,3 @@ +Why are you looking here? + +What is '%msg%'? diff --git a/example/files/app-config/sub-dir/i-will-be-copied-too.txt b/example/files/app-config/sub-dir/i-will-be-copied-too.txt new file mode 100644 index 0000000..44a3b38 --- /dev/null +++ b/example/files/app-config/sub-dir/i-will-be-copied-too.txt @@ -0,0 +1 @@ +Thats right! diff --git a/example/files/user-script.sh b/example/files/user-script.sh new file mode 100644 index 0000000..3ed70fb --- /dev/null +++ b/example/files/user-script.sh @@ -0,0 +1,2 @@ +#!/usr/bin/env bash +echo "Hello, World!" diff --git a/example/my_module.py b/example/my_module.py index 155eaf1..38afffc 100644 --- a/example/my_module.py +++ b/example/my_module.py @@ -1,19 +1,110 @@ # from import is ok for importing classes and functions # just remember to not import variables this way -from decman import Module, sh - -import decman +from decman import Module, File, Directory, UserPackage, sh, prg class MyModule(Module): def __init__(self): self.pkgs = ["rust"] - super().__init__("Example module", True, "1") + self.update_rustup = False + # Modules have names and versions. + # Names must be unique. + # If you disable a module, all packages, files etc assocated with module are removed. + super().__init__(name="Example module", enabled=True, version="1") + + # You can add any methods etc to your modules. def enable_my_custom_feature(self, b: bool): if b: self.pkgs = ["rustup"] + self.update_rustup = True + + # This is ran, when the module gets enabled + def on_enable(self): + # Run arbitary shell code easily with the included sh function. + sh("groupadd mygroup") + + # or run a program with arguments. + prg(["usermod", "--append", "--groups", "mygroup", "kk"]) + + def on_disable(self): + # You can run commands as any user + sh("whoami", user="kk") + + # And override environment variables + sh("echo $HI", env_overrides={"HI": "Hello!"}) + + # Same options apply to prg as well. + + def after_update(self): + # Run code after running decman. + if self.update_rustup: + prg(["rustup", "update"], user="kk") + + def after_version_change(self): + # Modules have version numbers to allow conditionally running code. + # You could for example run mkinitcpio only after your config has changed. + # Just remember to change the version number. + prg(["mkinitcpio", "-P"]) + + # Files defined here are the same as outside of modules. + # There is however an additional feature: + # You may add variables to text files, that will be replaced with the given value. + + def file_variables(self) -> dict[str, str]: + return {"%msg%": "Hello, world!"} + + def files(self) -> dict[str, File]: + # Variables are substituted in text files automatically. + return { + "/usr/local/bin/say-hello": + File(content="#!/usr/bin/env bash\necho %msg%", permissions=0o755), + # Variables are not substituted in binary files. + "/usr/local/share/say-hello/image.png": + File(source_file="files/i-dont-exist.png", bin_file=True), + } + + def directories(self) -> dict[str, Directory]: + # Directories are handeled the same way. Variables are substituted in text files. + return { + "/home/kk/.config/mod-app/": + Directory(source_directory="files/app-config", owner="kk") + } + + # Packages and systemd units are basically the same with modules as without modules. def pacman_packages(self) -> list[str]: + # Return pacman packages depending on the usage of this module. return self.pkgs + + def user_packages(self) -> list[UserPackage]: + return [ + UserPackage( + pkgname="decman", + version="0.0.1", + dependencies=[ + "python", + "python-requests", + "devtools", + "systemd", + "pacman", + ], + make_dependencies=[ + "python-setuptools", + "python-build", + "python-installer", + "python-wheel", + ], + git_url="https://github.com/kiviktnm/decman-pkgbuild.git", + ) + ] + + def aur_packages(self) -> list[str]: + return ["protonvpn"] + + def systemd_units(self) -> list[str]: + return ["reflector.timer"] + + def systemd_user_units(self) -> dict[str, list[str]]: + return {"kk": ["syncthing.service"]} diff --git a/example/source.py b/example/source.py index 5d6e2b4..4b70d28 100644 --- a/example/source.py +++ b/example/source.py @@ -1,9 +1,259 @@ +# This example covers all decman features and many useful ways of configuring a system. +# Configuration can be as simple or as complex as is needed. + +import socket +import os + +# Remember: Do NOT use from imports for global variables +# BAD: from decman import packages/modules/etc import decman +import decman.config + +# This is fine since the thing being imported is a class and not a global variable. +from decman import UserPackage, File, Directory + +# Configuring what packages are installed is easy. +# Duplicates are OK, so if you have multiple modules that want to ensure a package is installed, +# you can add the same package multiple times. +decman.packages += ["python", "python", "devtools", "git", "networkmanager"] + +# Decman matches installed packages to those defined in the configuration. +# This means that: +# - all packages not installed on the system but defined in the source are installed +# - all packages installed on the system but not defined in the source are removed +# To make decman not care if a package is installed or not, add it to ignored_packages. +# Ignored packages can be normal packages or aur packages. +decman.ignored_packages += ["rustup", "yay"] + +# Installing AUR packages is easy. +decman.aur_packages += ["protonvpn"] + +# To import GPG keys, set the GNUPGHOME environment variable. +# It can easily be done with python as well. +os.environ["GNUPGHOME"] = "/home/kk/.gnupg/" +# You then must set the user that builds the packages to the owner of the GPG home. +decman.config.makepkg_user = "kk" + +# You can also install packages from anywhere, but then you must include some +# information about the package. The git_url is the url to the PKGBUILD, +# This example may not be up to date, but you should keep these up to date with the PKGBUILD. +decman.user_packages.append( + UserPackage( + pkgname="decman", + version="0.0.1", + dependencies=[ + "python", + "python-requests", + "devtools", + "systemd", + "pacman", + ], + make_dependencies=[ + "python-setuptools", + "python-build", + "python-installer", + "python-wheel", + ], + git_url="https://github.com/kiviktnm/decman-pkgbuild.git", + )) + +# Managing only packages with decman is not that interesting. +# Decman also has really powerful ways of managing config files, scripts etc. + +# IMPORTANT: Decman will remove files that were created by decman, but are no longer in the decman source. +# Keep your decman source in version control to avoid losing important files accidentally. + +# Define file content inline. +# By default text file encoding is utf-8 but it can be changed. +decman.files["/etc/vconsole.conf"] = File(content="KEYMAP=us", + encoding="utf-8") + +# Include file content from another file, set the file owner and permissions. +# The source_file is relative to the directory where the main decman source.py is located. +# By default, the file group is set to the group of the owner, but it can be overridden with the group argument. +decman.files["/home/kk/.bin/user-script.sh"] = File( + source_file="files/user-script.sh", owner="kk", permissions=0o744) + +# Non-text files such as images can also be managed. +decman.files["/home/kk/.background.png"] = File( + source_file="files/i-dont-actually-exist.png", bin_file=True, owner="kk") + +# If you need to install multiple files at once, use directories. +# All files from the source directory will be copied recursively to the target. +decman.directories["/home/kk/.config/app/"] = Directory( + source_directory="files/app-config", owner="kk") + +# Decman has built in support for managing systemd units as well. +# Decman will enable services declared here, and disable services removed from here. +# If you don't want decman to manage a service, don't add it here. It will ignore all units that +# weren't enabled here. +decman.enabled_systemd_units += ["NetworkManager.service"] + +# You can manage units for users as well. + +# Ensure that previous user unit declarations aren't overwritten and they are initialized. +decman.enabled_systemd_user_units[ + "kk"] = decman.enabled_systemd_user_units.get("kk", []) +# Add user unit. +decman.enabled_systemd_user_units["kk"].append("syncthing.service") + +# Most powerful feature of decman are modules. +# In this file you see how to include your module, but to really see what modules are capable of +# look at the MyModule class. from my_module import MyModule my_own_mod = MyModule() -my_own_mod.enable_my_custom_feature(True) +# You have full access to python, which makes your configuration very dynamic. +# For example: do something if the computers hostname is arch-1 +if socket.gethostname() == "arch-1": + # Modules make dynamic configuration easy. + # This executes code defined in MyModule which can affect for example what packages are + # installed as a part of this module. + my_own_mod.enable_my_custom_feature(True) -decman.packages += ["python", "python"] decman.modules += [my_own_mod] + +# Configuring the behavior of decman is also done here. +# These are the default values. + +# Show debug output +decman.config.debug_output = False + +# Make output less verbose. Summaries are still printed. +decman.config.quiet_output = False + +# Suppress output of some commands that you probably don't want to see. +decman.config.suppress_command_output = True + +# The user which builds aur and user packages. +# decman.config.makepkg_user = "nobody" # This was set in a previous example. Let's not override it. + +# The build directory decman uses for creating a chroot etc. +decman.config.build_dir = "/tmp/decman/build" + +# Built packages are stored here. +decman.config.pkg_cache_dir = "/var/cache/decman" + +# Timeout in seconds for fetching aur package details. +decman.config.aur_rpc_timeout = 30 + +# Enable installing and upgrading foreign packages. +decman.config.enable_fpm = True + + +# Changing the default commands decman uses for things is a bit more complex. +# Create a child class of the decman.config.Commands class and override methods. +# These are the defaults. +class MyCommands(decman.config.Commands): + + def list_pkgs(self) -> list[str]: + return ["pacman", "-Qeq", "--color=never"] + + def list_foreign_pkgs_versioned(self) -> list[str]: + return ["pacman", "-Qm", "--color=never"] + + def install_pkgs(self, pkgs: list[str]) -> list[str]: + return ["pacman", "-S", "--asexplicit"] + pkgs + + def install_files(self, pkg_files: list[str]) -> list[str]: + return ["pacman", "-U", "--asdeps"] + pkg_files + + def set_as_explicitly_installed(self, pkgs: list[str]) -> list[str]: + return ["pacman", "-D", "--asexplicit"] + pkgs + + def install_deps(self, deps: list[str]) -> list[str]: + return ["pacman", "-S", "--needed", "--asdeps"] + deps + + def is_installable(self, pkg: str) -> list[str]: + return ["pacman", "-Sddp", pkg] + + def upgrade(self) -> list[str]: + return ["pacman", "-Syu"] + + def remove(self, pkgs: list[str]) -> list[str]: + return ["pacman", "-Rs"] + pkgs + + def enable_units(self, units: list[str]) -> list[str]: + return ["systemctl", "enable", "--now", "--quiet"] + units + + def disable_units(self, units: list[str]) -> list[str]: + return ["systemctl", "disable", "--quiet"] + units + + def enable_user_units(self, units: list[str]) -> list[str]: + return ["systemctl", "enable", "--now", "--quiet", "--user"] + units + + def disable_user_units(self, units: list[str]) -> list[str]: + return ["systemctl", "disable", "--quiet"] + units + + def compare_versions(self, installed_version: str, + new_version: str) -> list[str]: + return ["vercmp", installed_version, new_version] + + def git_clone(self, repo: str, dest: str) -> list[str]: + return ["git", "clone", repo, dest] + + def git_diff(self, from_commit: str) -> list[str]: + return ["git", "diff", from_commit] + + def git_get_commit_id(self) -> list[str]: + return ["git", "rev-parse", "HEAD"] + + def review_file(self, file: str) -> list[str]: + return ["less", file] + + def make_chroot(self, chroot_dir: str, with_pkgs: list[str]) -> list[str]: + return ["mkarchroot", chroot_dir] + with_pkgs + + def install_chroot_packages(self, chroot_dir: str, packages: list[str]): + return [ + "arch-nspawn", chroot_dir, "pacman", "-S", "--needed", + "--noconfirm" + ] + packages + + def remove_chroot_packages(self, chroot_dir: str, packages: list[str]): + return ["arch-nspawn", chroot_dir, "pacman", "-Rsu", "--noconfirm" + ] + packages + + def make_chroot_pkg(self, chroot_wd_dir: str, user: str, + pkgfiles_to_install: list[str]) -> list[str]: + makechrootpkg_cmd = [ + "makechrootpkg", "-c", "-r", chroot_wd_dir, "-U", user + ] + + for pkgfile in pkgfiles_to_install: + makechrootpkg_cmd += ["-I", pkgfile] + + return makechrootpkg_cmd + + +# To apply your overrides, set the commands variable. +decman.config.commands = MyCommands() + +# Alternative to the built in AUR support: +# If you don't want to use the built in AUR helper, you can use some pacman wrapper that can run as root, such as pikaur. +# To do this, override commands and disable fpm. + + +class PikaurWrapperCommands(decman.config.Commands): + + def list_pkgs(self) -> list[str]: + return ["pikaur", "-Qeq"] + + def install_pkgs(self, pkgs: list[str]) -> list[str]: + return ["pikaur", "-S", "--asexplicit"] + pkgs + + def upgrade(self) -> list[str]: + return ["pikaur", "-Syu"] + + def remove(self, pkgs: list[str]) -> list[str]: + return ["pikaur", "-Rs"] + pkgs + + # it doesn't matter if all pacman commands aren't overridden since they wont be used when fpm is disabled. + + +# decman.config.enable_fpm = False +# decman.config.commands = PikaurWrapperCommands() + +# Then simply add all AUR packages to decman.packages +# decman.packages += ["pikaur"]