diff --git a/pyinfra-metadata.toml b/pyinfra-metadata.toml index 88e4361ea..aeea935c8 100644 --- a/pyinfra-metadata.toml +++ b/pyinfra-metadata.toml @@ -275,6 +275,18 @@ path = "src/pyinfra/facts/launchd.py" type = "fact" tags = ["service-management", "system"] +[pyinfra.plugins."s6-ops"] +name = "s6" +path = "src/pyinfra/operations/s6.py" +type = "operation" +tags = ["service-management"] + +[pyinfra.plugins."s6-facts"] +name = "s6" +path = "src/pyinfra/facts/s6.py" +type = "fact" +tags = ["service-management", "system"] + [pyinfra.plugins."systemd-ops"] name = "systemd" path = "src/pyinfra/operations/systemd.py" diff --git a/src/pyinfra/facts/s6.py b/src/pyinfra/facts/s6.py new file mode 100644 index 000000000..68dfdc098 --- /dev/null +++ b/src/pyinfra/facts/s6.py @@ -0,0 +1,115 @@ +from typing_extensions import override + +from pyinfra.api import FactBase, QuoteString +from pyinfra.api.command import make_formatted_string_command, StringCommand + + +class S6RepositoryList(FactBase[list[str]]): + """Returns the name of every set in a repository, including the set named "current".""" + + @override + def requires_command(self, repository=None): + return "s6-rc-repo-list" + # and envfile, but that comes bundled in execline dependency of s6 + + @override + def command(self, repository=None): + """ + + repository: path of the repository to inspect. If `None`, the compiled-in default will be used, most likely `/var/lib/s6/repository`. + """ + if repository: + return make_formatted_string_command("s6-rc-repo-list -r {0}", QuoteString(repository)) + + return StringCommand("s6-rc-repo-list") + + # if no repository passed, try to get its location from the s6-frontend configuration file + # return StringCommand( + # '[ ! -z "$S6_CONF" ] || S6_CONF=/etc/s6.conf && envfile "$S6_CONF" sh -c \'s6-rc-repo-list -r "$repodir"\'; echo EXIT CODE: $?' + # ) + + @override + def process(self, output): + return output + + +class S6SetStatus(FactBase[dict[str, str]]): + """Returns a dict of name -> rx (prescription) for each service in a given set. + + If the set does not exist, nothing is returned. + + > [!IMPORTANT] + > The fact only returns `None` when the set doesn't exist if `3` is in the `_success_exit_codes` + > parameter for the fact. It will throw an exception otherwise due to a limitation in pyinfra. + + """ + + @override + def requires_command(self, the_set="current", repository=None): + return "s6-rc-set-status" + # and sh + + @override + def command(self, the_set="current", repository=None): + """ + + the_set: the set to inspect. + + repository: path of the repository to inspect, default `None`, which means to use the compiled-in default repository most likely `/var/lib/s6/repository`. + """ + if repository: + return make_formatted_string_command( + "s6-rc-set-status -r {0} {1}; echo EXIT CODE: $?", + QuoteString(repository), + QuoteString(the_set), + ) + + return make_formatted_string_command( + "s6-rc-set-status {0}; echo EXIT CODE: $?", QuoteString(the_set) + ) + # extra escaping needed for make_formatted_string_command, but not in StringCommand + # return make_formatted_string_command( + # '[ ! -z \\"$S6_CONF\\" ] || S6_CONF=/etc/s6.conf && envfile \\"$S6_CONF\\" sh -c \\\'s6-rc-set-status -r \\"$repodir\\" {0}\\\'; echo EXIT CODE: $?', + # QuoteString(the_set), + # ) + + @override + def process(self, output): + # exit code 3: nonexistent set + # NOTE: will have to always specify 3 as success exit code when using this fact + if output[-1] == "EXIT CODE: 3": + return + + return { + triplet[0]: triplet[-1] + for triplet in map(lambda line: line.partition("/"), output[:-1]) + } + + +class S6LiveStatus(FactBase[dict[str, bool]]): + """Returns a dict of name -> status for each service in the live state. + + True when the service is "running", meaning the service is managed by an `s6-supervise`s, False + otherwise. + """ + + @override + def requires_command(self): + return "s6-rc" + + # @override + # def check_preconditions(self, state, host): + # if not host.run_shell_command('[ ! -z "$S6_CONF" ] || [ -f /etc/s6.conf ]')[0]: + # return "couldn't find s6-frontend configuration" + + @override + def command(self): + return "s6-rc -c list" + + @override + def process(self, output): + # example of an output line: + # seatd-srv/longrun//up/explicit + # returns + # { "seatd-srv": True } + return { + statusline[0]: True if statusline[3] == "up" else False + for statusline in map(lambda line: line.split("/"), output) + } diff --git a/src/pyinfra/operations/s6.py b/src/pyinfra/operations/s6.py new file mode 100644 index 000000000..a13c7a517 --- /dev/null +++ b/src/pyinfra/operations/s6.py @@ -0,0 +1,615 @@ +"""Manage s6-rc services (https://www.skarnet.org/software/s6-rc/).""" + +import re +import shlex +from collections.abc import Sequence + +from pyinfra import host, logger +from pyinfra.api import ( + QuoteString, + StringCommand, + OperationError, + OperationValueError, + operation, + Host, +) +from pyinfra.api.command import make_formatted_string_command +from pyinfra.facts.s6 import S6LiveStatus, S6SetStatus, S6RepositoryList +from pyinfra.facts.files import FileContents +from pyinfra.facts.server import Command + + +def _get_s6_frontend_conf_contents(host: Host) -> list[str] | None: + """Attempts to locate and return the contents of an s6-frontend configuration. + + Does not return anything if no configuration is found. + + + host: pyinfra host object on which to perform this lookup. + """ + conf_envvar = host.get_fact(Command, 'printf %s "$S6_CONF"') + if conf_envvar != "": + if contents := host.get_fact(FileContents, conf_envvar): + return contents + + if contents := host.get_fact(FileContents, "/etc/s6.conf"): + return contents + + return None + + +def _get_value_from_conf(key: str, content: list[str] | None) -> str | None: + """Get the value associated with a key in an s6-frontend configuration. + + If no repodir is found, nothing is returned. + + + key: the key associated with the value, e.g. "repodir". + + content: the file contents, as a list of strings. if `None`, this function returns nothing. + """ + # simplistic check for now, avoiding complicated syntax + # does not account for a statement broken over multiple lines with backslashes + # assume the key is unique + if content is not None: + if match := re.search( + # https://skarnet.org/software/execline/envfile.html#syntax + r"^\s*" + re.escape(key) + r'\s*=\s*(/[^\s]*|"/.*")\s*$', + "\n".join(content), + re.MULTILINE, + ): + return match.group(1) + + return None + + ## TODO edge case + # for line, next_line in itertools.pairwise(content): + # # ignore commented, empty and whitespace lines + # if line == "": + # continue + # elif re.search(r'^\s*#', line, re.ASCII): + # continue + # elif re.fullmatch(r'\s+', line, re.ASCII): + # continue + + # # line continuation detected + # if line.endswith("\\"): + # if next_line.endswith("\\"): + # pass + # elif "=" not in line: + # return False + + +# def _s6_frontend_repo_lookup(host: Host): +# """Attempts to find an s6-rc repository. +# +# The algorithm is as follows: +# +# The environment variable `S6_CONF` is checked for a valid filesystem path, and if it is, check +# whether the file defines a repodir. Otherwise check whether the file `/etc/s6.conf` exists and +# if it defines a repodir. If both fail, no value is returned. +# """ +# conf_envvar = host.get_fact(Command, 'printf %s "$S6_CONF"') +# +# if conf_envvar != "": +# conf = host.get_fact(FileContents, conf_envvar) +# if repodir := _get_repodir_from_conf(conf): +# return repodir +# +# if s6_conf := host.get_fact(FileContents, "/etc/s6.conf"): +# if repodir := _get_repodir_from_conf(s6_conf): +# return repodir +# +# # for compatibility with older versions of s6-frontend. the envvar used to have a different name +# # and config file was called something else. +# conf_envvar_old = host.get_fact(Command, 'printf %s "$S6_FRONTEND_CONF"') +# +# if conf_envvar_old != "": +# conf = host.get_fact(FileContents, conf_envvar_old) +# if repodir := _get_repodir_from_conf(conf): +# return repodir +# +# if frontend_conf := host.get_fact(FileContents, "/etc/s6-frontend.conf"): +# if repodir := _get_repodir_from_conf(frontend_conf): +# return repodir +# +# return + + +def _make_format_fields(n): + """Returns "{0} {1} ... {n-1}".""" + return " ".join([f"{{{i}}}" for i in range(n)]) + + +def _make_live_command(op: str, services: Sequence): + """ + + op: the operation, e.g. "start", "stop", "restart". + + services: the service(s) to operate on. + """ + return make_formatted_string_command( + f"s6 live {op} " + _make_format_fields(len(services)), *map(QuoteString, services) + ) + + +def _make_set_change_command( + services: list, + current_rxs: dict, + wanted_rx: str, + repository: str | None = None, + the_set: str = "current", +): + """Returns a command like "s6-rc-set-change -r /etc/s6/repo current active httpd". + + + services: the services to be assigned a specific prescription. + + current_rxs: the current prescriptions for all services (from the `S6SetStatus` fact). + + wanted_rx: the prescription to assign to each service. + + repository: path to the repository containing the set. + + the_set: name of the set to operate on. + + If every service already matches the desired prescription, None is returned. + """ + if wanted_rx not in {"always", "active", "usable", "masked"}: + raise ValueError( + f'wanted_rx must be one of "always", "active", "usable", or "masked", got {wanted_rx}.' + ) + + # services that need their prescription changed (not all of them; those that are already in the + # desired state are excluded from this list) + service_subset = [] + + for srv in services: + try: + if current_rxs[srv] != wanted_rx: + service_subset.append(srv) + except KeyError: + service_subset.append(srv) + + if service_subset: + # _rx_to_subcommand = { + # "always": "make-essential", + # "active": "enable", + # "usable": "disable", + # "masked": "mask", + # } + + # example of the string passed into make_formatted_string_command: + # s6-rc-set-change -r {5} {6} usable {0} {1} {2} {3} {4} + if repository: + return make_formatted_string_command( + f"s6-rc-set-change -r {{{len(service_subset)}}} {{{len(service_subset) + 1}}} {wanted_rx} " + + _make_format_fields(len(service_subset)), + *map(QuoteString, service_subset), + QuoteString(repository), + QuoteString(the_set), + ) + + return make_formatted_string_command( + f"s6-rc-set-change {{{len(service_subset)}}} {wanted_rx} " + + _make_format_fields(len(service_subset)), + *map(QuoteString, service_subset), + QuoteString(the_set), + ) + + return None + + +def _make_all_set_change_commands( + prescriptions: dict, + repository: str | None = None, + the_set: str = "current", + force_prescriptions: bool = True, +): + """Returns all commands necessary to bring the prescriptions to the desired state. + + + prescriptions: map of service -> prescription, which is one of "always", "active", "usable", "masked". + + repository: path to a repository containing the set. + + the_set: the set to change the prescriptions of. + + force_prescriptions: whether to ensure there are no other services in the set or to only modify the prescriptions of the specified services, leaving others untouched. + + Returns a length-4 array of `StringCommand`s or `None`s, for each prescription type, depending + on whether any services needed to be switched to that prescription. This is essentially 4 + invocations of `_make_rx_command` for each type of prescription. + """ + + _working_rx_set = set(prescriptions.values()) + if not _working_rx_set <= {"always", "active", "usable", "masked"}: + raise OperationValueError( + 'prescriptions must be one of "always", "active", "usable", or "masked"' + ) + + # bin services by desired prescription + service_bins: dict[str, list[str]] = { + "wanted_always": [], + "wanted_active": [], + "wanted_usable": [], + "wanted_masked": [], + } + + if "always" in _working_rx_set: + service_bins["wanted_always"].extend( + [srv for srv in prescriptions if prescriptions[srv] == "always"] + ) + if "active" in _working_rx_set: + service_bins["wanted_active"].extend( + [srv for srv in prescriptions if prescriptions[srv] == "active"] + ) + if "usable" in _working_rx_set: + service_bins["wanted_usable"].extend( + [srv for srv in prescriptions if prescriptions[srv] == "usable"] + ) + if "masked" in _working_rx_set: + service_bins["wanted_masked"].extend( + [srv for srv in prescriptions if prescriptions[srv] == "masked"] + ) + + current_rxs = host.get_fact(S6SetStatus, the_set, repository) + + if force_prescriptions: + # mask all services not present in `prescriptions` arg + service_bins["wanted_masked"].extend( + [srv for srv in current_rxs if srv not in prescriptions] + ) + + return [ + _make_set_change_command( + service_set, current_rxs, wanted_rx.removeprefix("wanted_"), repository, the_set=the_set + ) + for wanted_rx, service_set in service_bins.items() + ] + + +@operation(is_idempotent=False) +def set_create(name: str, repository: str | None = None): + """Create a new set. + + + name: name for the new set. + + repository: repository to save the set in. + """ + if repository: + yield make_formatted_string_command( + "s6-rc-set-new -r {0} {1}", QuoteString(repository), QuoteString(name) + ) + else: + yield make_formatted_string_command("s6-rc-set-new -r {0}", QuoteString(name)) + + +@operation(is_idempotent=False) +def set_delete(the_sets: str | Sequence[str], repository: str | None = None): + """Delete sets. + + + the_sets: name or list of names of the sets to delete. + + repository: path to the repository containing the sets + """ + if isinstance(the_sets, str): + the_sets = (the_sets,) + + if "current" in the_sets: + raise OperationValueError('cannot delete the set "current"') + + if repository: + yield make_formatted_string_command( + f"s6-rc-set-delete -r {{{len(the_sets)}}} " + _make_format_fields(len(the_sets)), + *map(QuoteString, the_sets), + QuoteString(repository), + ) + else: + yield make_formatted_string_command( + "s6-rc-set-delete " + _make_format_fields(len(the_sets)), *map(QuoteString, the_sets) + ) + + +@operation() +def set_copy(dest: str, repository: str | None = None, source="current", force: bool = False): + """Save the contents of the given set as a new set. + + + dest: name of the saved copy. + + repository: path to the repository containing the set. + + source: name of the set to copy. + + force: whether to overwrite an existing set of the same name if it exists. + """ + source_set_contents = host.get_fact(S6SetStatus, the_set=source, repository=repository) + dest_set_contents = host.get_fact(S6SetStatus, the_set=dest, repository=repository) + if source_set_contents != dest_set_contents: + if repository: + yield make_formatted_string_command( + f"s6-rc-set-copy -r {{0}}{' -f' if force else ''} {{1}} {{2}}", + QuoteString(repository), + QuoteString(source), + QuoteString(dest), + ) + else: + yield make_formatted_string_command( + f"s6-rc-set-copy{' -f' if force else ''} {{0}} {{1}}", + QuoteString(source), + QuoteString(dest), + ) + else: + host.noop(f'the set "{dest}" exists and is identical to "{source}"') + + # TODO + # backing up a set when it would have been overwritten by -f requires more thought. try to use + # $S6_CONF, envfile, and whether the fact that the friendly set names are symlinks to unique + # names changes anything (e.g. `readlink /etc/s6/repo/sources/default` gives `.default:YN2tP3`). + + # + force_backup: whether to backup an existing set that would be overwritten by `force`. + # if force_backup: + # # requires knowing path of the repository. + # # regex will break if repodir key pair in /etc/s6-frontend.conf spans several lines. + # lines = host.get_fact( + # FindInFile, + # "/etc/s6-frontend.conf", + # r"repodir\s*=", + # interpolate_variables=False, + # extended_regex=True, + # ) + # if lines is None: + # raise OperationError( + # "no repodir found in /etc/s6-frontend.conf, or file doesn't exist" + # ) + # if len(lines) != 1: + # # no OperationWarning + # logger.warning( + # "multiple repodir definitions found in /etc/s6-frontend.conf, using the first one" + # ) + # if (m := _repodir_pattern.fullmatch(lines[0])) is None: + # raise OperationError("failed to match repodir line in /etc/s6-frontend.conf") + # repodir = m[1] + + # if host.get_fact(Directory, os.path.join(repodir, dest)): + # yield from _raise_or_remove_invalid_path( + # "directory", os.path.join(repodir, dest), True, True, False + # ) + + # # no -f since the old set has already been moved + # yield make_formatted_string_command( + # "s6 set copy {0} {1}", QuoteString(source), QuoteString(dest) + # ) + + +@operation(is_idempotent=False) +def set_commit(repository: str | None = None, the_set: str = "current"): + """Check the given set and commit it. + + + repository: path to the repository containing the set. + + the_set: name of the set to check and commit. + """ + if repository is not None: + yield make_formatted_string_command( + "s6-rc-set-fix -r {0} {1}", QuoteString(repository), QuoteString(the_set) + ) + yield make_formatted_string_command( + "s6-rc-set-commit -r {0} {1}", QuoteString(repository), QuoteString(the_set) + ) + else: + yield make_formatted_string_command("s6-rc-set-fix {0}", QuoteString(the_set)) + yield make_formatted_string_command("s6-rc-set-commit {0}", QuoteString(the_set)) + + +# TODO switch to s6-rc-set-install, need to find livedir dynamically like repodir, add repository argument +@operation(is_idempotent=False) +def live_install(the_set: str = "current"): + """Install a compiled (committed) service database into the live state. + + + repository: path to the repository containing the set. + + the_set: name of the set containing an already compiled service database to be installed into the live state. + """ + yield make_formatted_string_command("s6 live install -s {0}", QuoteString(the_set)) + + +@operation( + is_idempotent=False, + # TODO verify idempotency paths + idempotent_notice="Not idempotent by default. If `do_commit=False`, then the operation is idempotent.", +) +def manage_set( + the_set: str = "current", + present: bool = True, + prescriptions: dict[str, str] | None = None, + force_prescriptions: bool = True, + do_commit: bool = True, +): + """Manage sets in a repository. + + The repository is automatically found by inspecting the `S6_CONF` environment variable or + trying the hardcoded path `/etc/s6.conf`, in line with `s6-frontend` behavior. `manage_set` + combines smaller operations into one: + - ensuring existence or non-existence, like `files.file` does + - ensuring specific prescriptions within sets, with support for ensuring only prescriptions + on only a subset of the services in a set + - committing a set, which is a stateless operation + + + the_set: name of the set to manage. + + present: whether the set should be present in the repository. + + prescriptions: the prescriptions to ensure in the set. A map of service name -> prescription, where the prescription is any of "always", "active", "usable", "masked". May be `None`, which allows management of set presence only. + + force_prescriptions: whether the `prescriptions` should be the *only* prescriptions in the set (i.e. other services will be removed) + + do_commit: whether to commit the current(ly loaded) set. delaying this step can allow for other operations to modify the current set, with the final result being committed at the end. + + """ + + # automatically find the configured s6-frontend repository to use in commands. the value can be + # `None`, which happens when no configuration file was found. in this case, s6 commands will use + # the compiled-in default repository. + s6_conf_lines = _get_s6_frontend_conf_contents(host) + if s6_conf_lines is None: + # TODO should it really be a warning instead of info? + logger.warning( + "could not find an s6-frontend configuration on remote host, will use compiled-in default repository for s6 commands" + ) + elif s6_conf_lines == []: + logger.warning( + "remote host's s6-frontend configuration is an empty file, will use compiled-in default repository for s6 commands" + ) + # repodir will be `None` if not found, but possibility of None is handled, resulting in the + # compiled-in default repository being used. + repodir = _get_value_from_conf(key="repodir", content=s6_conf_lines) + + # if not (repodir := _get_value_from_conf(key="repodir", content=s6_conf_lines)): + # raise OperationError( + # f"failed to extract a repodir from remote s6-frontend config: {s6_conf_lines}" + # ) + + if present: + # remove noops from this set as conditions are satisfied + noops = {"create", "prescribe", "commit"} + + # create a new set if it doesn't exist + existing_sets = host.get_fact(S6RepositoryList, repository=repodir) + if the_set not in existing_sets: + noops.remove("create") + if repodir is not None: + yield make_formatted_string_command( + "s6-rc-set-new -r {0} {1}", QuoteString(repodir), QuoteString(the_set) + ) + else: + yield make_formatted_string_command("s6-rc-set-new {0}", QuoteString(the_set)) + + if prescriptions: + if any( + cmds := _make_all_set_change_commands( + prescriptions, repodir, the_set=the_set, force_prescriptions=force_prescriptions + ) + ): + noops.remove("prescribe") + yield from filter(lambda cmd: cmd is not None, cmds) + + # non-idempotent, I don't know of a way of checking whether the to-be compiled database + # matches the existing compiled service database + if do_commit: + noops.remove("commit") + yield from set_commit._inner(repository=repodir, the_set=the_set) + + # "global" noop if all 3 branches noop + if noops == {"create", "prescribe", "commit"}: + host.noop( + f'the set "{the_set}" already exists, has the desired prescriptions, and no commit was requested' + ) + + # present=False + else: + if the_set in host.get_fact(S6RepositoryList, repository=repodir): + if repodir is not None: + yield make_formatted_string_command( + "s6-rc-set-delete -r {0} {1}", QuoteString(repodir), QuoteString(the_set) + ) + else: + yield make_formatted_string_command("s6-rc-set-delete {0}", QuoteString(the_set)) + else: + host.noop(f'the set "{the_set}" is already nonexistent') + + +@operation( + # TODO verify idempotency paths + is_idempotent=False, + idempotent_notice="It is not idempotent only when at least one of `commit_set` or `install_set` are `True`.", +) +def service( + service: str | Sequence[str], + running: bool | None = None, + restarted: bool | None = None, + reloaded: bool | None = None, + command: str | None = None, + enabled: bool | None = None, + reload_signal: str = "SIGHUP", + the_set: str = "current", + enabled_rx: str = "active", + disabled_rx: str = "usable", + commit_set: bool = False, + install_set: bool = False, +): + """ + Manage the state of s6-supervised services. + + + service: name(s) of the service(s) to manage. + + running: whether the service(s) should be under an s6-supervise. + + restarted: whether the service(s) should be restarted. + + reloaded: whether the service(s) should be reloaded by sending a signal, SIGHUP by default. Whether the service is reloaded depends on how it handles the signal. + + command: custom command to run after the auto-computed commands. This must be an s6 subcommand, e.g. "system reboot" gives the command "s6 system reboot". + + enabled: whether the service should be given an "active" or "usable" prescription. + + reload_signal: the signal to send to the service(s) when a reload is desired. + + the_set: name of the set to use when managing enabled status, using the set named "current" by default. + + enabled_rx: name of the prescription to assign to the service(s) when enabled, which could be either "active" or "always". + + disabled_rx: name of the prescription to assign to the service(s) when disabled, which could be either "usable" or "masked". + + commit_set: whether to commit the current(ly loaded) set. Delaying this step can allow for other operations to modify the current set, with the final result being committed at the end. + + install_set: whether to install the compiled service database (the result of a commit operation) into the live state. This is analogous to systemd's daemon-reload, but not completely: systemd recognizes changes to service files after a reboot, but s6 does not. It only recognizes changes when an s6 live install command is executed. Live state replacement and enablement/disablement of services are coupled in s6. + + Specifying multiple services results in fewer commands executed, especially in the case of + changing the enabled status of the service, where the service database is recompiled per command + by default. + + Note that this operation does not give as granular control over prescriptions as the + `manage_set` operation does; all services will be assigned the same prescription. + """ + if enabled_rx not in {"active", "always"}: + raise ValueError('enabled_rx must be either "active" or "always"') + if disabled_rx not in {"usable", "masked"}: + raise ValueError('disabled_rx must be either "usable" or "masked"') + + if isinstance(service, str): + service = (service,) + + if (running, restarted, reloaded) != (None,) * 3: + # dict[str, bool] whether each service given in the services arg are running. + live_statuses = {srv: host.get_fact(S6LiveStatus)[srv] for srv in service} + all_up = all(live_statuses.values()) + some_up = any(live_statuses.values()) + all_down_services = [srv for srv, stat in live_statuses.items() if not stat] + all_up_services = [srv for srv, stat in live_statuses.items() if stat] + + if running is False: + if some_up: + yield _make_live_command("stop", all_up_services) + else: + host.noop(f"all specified services are already down: {', '.join(service)}") + + if running is True: + if not all_up: + yield _make_live_command("start", all_down_services) + else: + host.noop(f"all specified services are already up: {', '.join(service)}") + + if restarted: + if some_up: + yield _make_live_command("restart", all_up_services) + else: + host.noop(f"all specified services are down: {', '.join(service)}") + + if reloaded: + if some_up: + # TODO use s6-svc instead of frontend? + yield make_formatted_string_command( + "s6 process kill -s {0} " + + " ".join([f"{{{i + 1}}}" for i in range(len(all_up_services))]), + QuoteString(reload_signal), + *map(QuoteString, all_up_services), + ) + else: + host.noop(f"all specified services are down: {', '.join(service)}") + + # TODO: test masked services present in `services` arg on a real system + if enabled is not None: + if enabled is True: + yield from manage_set._inner( + the_set=the_set, prescriptions={srv: enabled_rx for srv in service} + ) + + if enabled is False: + yield from manage_set._inner( + the_set=the_set, prescriptions={srv: disabled_rx for srv in service} + ) + + if commit_set: + if not (s6_conf_lines := _get_s6_frontend_conf_contents(host)): + raise OperationError( + "failed to find an s6-frontend configuration on the remote host" + ) + repodir = _get_value_from_conf(key="repodir", content=s6_conf_lines) + + # if not (repodir := _get_value_from_conf(key="repodir", content=s6_conf_lines)): + # raise OperationError( + # f"failed to extract a repodir from remote s6-frontend config: {s6_conf_lines}" + # ) + + yield from set_commit._inner(repository=repodir) + + if install_set: + yield from live_install._inner() + + if command: + yield StringCommand("s6", *map(QuoteString, shlex.split(command))) diff --git a/src/pyinfra/operations/server.py b/src/pyinfra/operations/server.py index 448f36d1a..e7ad9dbf8 100644 --- a/src/pyinfra/operations/server.py +++ b/src/pyinfra/operations/server.py @@ -47,6 +47,7 @@ pacman, pkg, runit, + s6, systemd, sysvinit, upstart, @@ -716,6 +717,9 @@ def service( elif host.get_fact(Which, command="sv"): service_operation = runit.service + elif host.get_fact(Which, command="s6"): + service_operation = s6.service + # NOTE: must run before the sysvinit check: BSDs ship `service` in base (distinct from the # Linux sysvinit wrapper), so matching on Which command="service" first would misroute BSD # hosts to sysvinit. See https://github.com/pyinfra-dev/pyinfra/issues/1496. @@ -736,7 +740,9 @@ def service( else: raise OperationError( - ("No init system found (no systemctl, initctl, /etc/init.d or /etc/rc.d found)"), + ( + "No init system found (no systemctl, rc-service, initctl, sv, s6, /etc/init.d or /etc/rc.d found)" + ), ) yield from service_operation._inner( diff --git a/tests/facts/s6.S6LiveStatus/no_running_services.yaml b/tests/facts/s6.S6LiveStatus/no_running_services.yaml new file mode 100644 index 000000000..8b27f25b7 --- /dev/null +++ b/tests/facts/s6.S6LiveStatus/no_running_services.yaml @@ -0,0 +1,13 @@ +command: s6-rc -c list +requires_command: s6-rc +output: | + NetworkManager-srv/longrun//down/ + NetworkManager-log/longrun//down/ + avahi-daemon-log/longrun//down/ + avahi-daemon-srv/longrun//down/ +fact: + NetworkManager-srv: false + NetworkManager-log: false + avahi-daemon-log: false + avahi-daemon-srv: false + diff --git a/tests/facts/s6.S6LiveStatus/no_stopped_services.yaml b/tests/facts/s6.S6LiveStatus/no_stopped_services.yaml new file mode 100644 index 000000000..6236ce33d --- /dev/null +++ b/tests/facts/s6.S6LiveStatus/no_stopped_services.yaml @@ -0,0 +1,12 @@ +command: s6-rc -c list +requires_command: s6-rc +output: | + NetworkManager-log/longrun//up/explicit + NetworkManager-srv/longrun//up/explicit + avahi-daemon-srv/longrun//up/explicit + avahi-daemon-log/longrun//up/explicit +fact: + NetworkManager-srv: true + NetworkManager-log: true + avahi-daemon-srv: true + avahi-daemon-log: true diff --git a/tests/facts/s6.S6LiveStatus/standard.yaml b/tests/facts/s6.S6LiveStatus/standard.yaml new file mode 100644 index 000000000..a3fe2c630 --- /dev/null +++ b/tests/facts/s6.S6LiveStatus/standard.yaml @@ -0,0 +1,13 @@ +command: s6-rc -c list +requires_command: s6-rc +output: | + NetworkManager-log/longrun//up/explicit + NetworkManager-srv/longrun//up/explicit + avahi-daemon-srv/longrun//down/ + avahi-daemon-log/longrun//down/ +fact: + NetworkManager-log: true + NetworkManager-srv: true + avahi-daemon-log: false + avahi-daemon-srv: false + diff --git a/tests/facts/s6.S6RepositoryList/explicit.yaml b/tests/facts/s6.S6RepositoryList/explicit.yaml new file mode 100644 index 000000000..1a226bc1a --- /dev/null +++ b/tests/facts/s6.S6RepositoryList/explicit.yaml @@ -0,0 +1,17 @@ +arg: + repository: /etc/s6/repo +command: s6-rc-repo-list -r /etc/s6/repo +requires_command: s6-rc-repo-list +output: | + kiosk + current + default + minimal + recovery +fact: [ + kiosk, + current, + default, + minimal, + recovery +] diff --git a/tests/facts/s6.S6RepositoryList/implicit.yaml b/tests/facts/s6.S6RepositoryList/implicit.yaml new file mode 100644 index 000000000..289edb7d7 --- /dev/null +++ b/tests/facts/s6.S6RepositoryList/implicit.yaml @@ -0,0 +1,15 @@ +command: s6-rc-repo-list +requires-command: s6-rc-repo-list +output: | + kiosk + current + default + minimal + recovery +fact: [ + kiosk, + current, + default, + minimal, + recovery +] diff --git a/tests/facts/s6.S6SetStatus/explicit_repo.yaml b/tests/facts/s6.S6SetStatus/explicit_repo.yaml new file mode 100644 index 000000000..6d9d5dc5e --- /dev/null +++ b/tests/facts/s6.S6SetStatus/explicit_repo.yaml @@ -0,0 +1,17 @@ +arg: + repository: "/etc/s6/repo" +command: "s6-rc-set-status -r /etc/s6/repo current; echo EXIT CODE: $?" +requires_command: s6-rc-set-status +output: | + swap/always + NetworkManager-srv/active + NetworkManager-log/active + avahi-daemon-srv/usable + avahi-daemon-log/usable + EXIT CODE: 0 +fact: + swap: always + NetworkManager-srv: active + NetworkManager-log: active + avahi-daemon-srv: usable + avahi-daemon-log: usable diff --git a/tests/facts/s6.S6SetStatus/implicit_repo.yaml b/tests/facts/s6.S6SetStatus/implicit_repo.yaml new file mode 100644 index 000000000..59e7587c3 --- /dev/null +++ b/tests/facts/s6.S6SetStatus/implicit_repo.yaml @@ -0,0 +1,15 @@ +command: 's6-rc-set-status current; echo EXIT CODE: $?' +requires_command: s6-rc-set-status +output: | + swap/always + NetworkManager-srv/active + NetworkManager-log/active + avahi-daemon-srv/usable + avahi-daemon-log/usable + EXIT CODE: 0 +fact: + swap: always + NetworkManager-srv: active + NetworkManager-log: active + avahi-daemon-srv: usable + avahi-daemon-log: usable diff --git a/tests/facts/s6.S6SetStatus/nonexistent_set.yaml b/tests/facts/s6.S6SetStatus/nonexistent_set.yaml new file mode 100644 index 000000000..d9e7b4b73 --- /dev/null +++ b/tests/facts/s6.S6SetStatus/nonexistent_set.yaml @@ -0,0 +1,7 @@ +arg: + repository: /etc/s6/repo +command: "s6-rc-set-status -r /etc/s6/repo current; echo EXIT CODE: $?" +requires_command: s6-rc-set-status +output: | + EXIT CODE: 3 +fact: null diff --git a/tests/facts/s6.S6SetStatus/nonstandard_repository.yaml b/tests/facts/s6.S6SetStatus/nonstandard_repository.yaml new file mode 100644 index 000000000..4123c0e8f --- /dev/null +++ b/tests/facts/s6.S6SetStatus/nonstandard_repository.yaml @@ -0,0 +1,17 @@ +arg: + repository: /etc/s6/repo +command: "s6-rc-set-status -r /etc/s6/repo current; echo EXIT CODE: $?" +requires_command: s6-rc-set-status +output: | + swap/always + NetworkManager-srv/active + NetworkManager-log/active + avahi-daemon-srv/usable + avahi-daemon-log/usable + EXIT CODE: 0 +fact: + swap: always + NetworkManager-srv: active + NetworkManager-log: active + avahi-daemon-srv: usable + avahi-daemon-log: usable diff --git a/tests/facts/s6.S6SetStatus/nonstandard_repository_set.yaml b/tests/facts/s6.S6SetStatus/nonstandard_repository_set.yaml new file mode 100644 index 000000000..1fa7d8a22 --- /dev/null +++ b/tests/facts/s6.S6SetStatus/nonstandard_repository_set.yaml @@ -0,0 +1,14 @@ +arg: + the_set: recovery + repository: /etc/s6/repo +command: "s6-rc-set-status -r /etc/s6/repo recovery; echo EXIT CODE: $?" +requires_command: s6-rc-set-status +output: | + swap/always + tty1/active + ttyS/active + EXIT CODE: 0 +fact: + swap: always + tty1: active + ttyS: active diff --git a/tests/facts/s6.S6SetStatus/nonstandard_set.yaml b/tests/facts/s6.S6SetStatus/nonstandard_set.yaml new file mode 100644 index 000000000..2b1a852a2 --- /dev/null +++ b/tests/facts/s6.S6SetStatus/nonstandard_set.yaml @@ -0,0 +1,14 @@ +arg: + repository: /etc/s6/repo + the_set: recovery +command: "s6-rc-set-status -r /etc/s6/repo recovery; echo EXIT CODE: $?" +requires_command: s6-rc-set-status +output: | + swap/always + tty1/active + ttyS/active + EXIT CODE: 0 +fact: + swap: always + tty1: active + ttyS: active diff --git a/tests/operations/s6.manage_set/commit.yaml b/tests/operations/s6.manage_set/commit.yaml new file mode 100644 index 000000000..a342c1fc3 --- /dev/null +++ b/tests/operations/s6.manage_set/commit.yaml @@ -0,0 +1,16 @@ +args: + - default +kwargs: + do_commit: true +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default +commands: + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/compiled-in_repo.yaml b/tests/operations/s6.manage_set/compiled-in_repo.yaml new file mode 100644 index 000000000..a549203ed --- /dev/null +++ b/tests/operations/s6.manage_set/compiled-in_repo.yaml @@ -0,0 +1,18 @@ +kwargs: + the_set: "default" + present: false + prescriptions: null + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + #s6.S6SetStatus: + # repository=None, the_set=current: +commands: + - s6-rc-set-delete default diff --git a/tests/operations/s6.manage_set/create_missing.yaml b/tests/operations/s6.manage_set/create_missing.yaml new file mode 100644 index 000000000..70be02e1e --- /dev/null +++ b/tests/operations/s6.manage_set/create_missing.yaml @@ -0,0 +1,16 @@ +args: + - default +kwargs: + present: true + prescriptions: null + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current +commands: + - s6-rc-set-new default diff --git a/tests/operations/s6.manage_set/delete.yaml b/tests/operations/s6.manage_set/delete.yaml new file mode 100644 index 000000000..805c3f8f5 --- /dev/null +++ b/tests/operations/s6.manage_set/delete.yaml @@ -0,0 +1,21 @@ +args: + - default +kwargs: + present: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + # shows that "default" set exists + s6.S6SetStatus: + repository=None, the_set=default: + tipidee: active +commands: + - s6-rc-set-delete default diff --git a/tests/operations/s6.manage_set/disable.yaml b/tests/operations/s6.manage_set/disable.yaml new file mode 100644 index 000000000..4fd3d5ac2 --- /dev/null +++ b/tests/operations/s6.manage_set/disable.yaml @@ -0,0 +1,23 @@ +args: + - default +kwargs: + prescriptions: + tipidee: "usable" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + tipidee: "active" +commands: + - s6-rc-set-change default usable tipidee + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/enable.yaml b/tests/operations/s6.manage_set/enable.yaml new file mode 100644 index 000000000..d859806f0 --- /dev/null +++ b/tests/operations/s6.manage_set/enable.yaml @@ -0,0 +1,23 @@ +args: + - default +kwargs: + prescriptions: + tipidee: active +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + tipidee: usable +commands: + - s6-rc-set-change default active tipidee + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/force_prescriptions.yaml b/tests/operations/s6.manage_set/force_prescriptions.yaml new file mode 100644 index 000000000..e6799c2d7 --- /dev/null +++ b/tests/operations/s6.manage_set/force_prescriptions.yaml @@ -0,0 +1,26 @@ +args: + - default +kwargs: + prescriptions: + tipidee: "usable" + force_prescriptions: true +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + nftables: "usable" + mysqld: "masked" + tipidee: "usable" +commands: + - s6-rc-set-change default masked nftables + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/lookup_repo_envvar.yaml b/tests/operations/s6.manage_set/lookup_repo_envvar.yaml new file mode 100644 index 000000000..f68197e9f --- /dev/null +++ b/tests/operations/s6.manage_set/lookup_repo_envvar.yaml @@ -0,0 +1,19 @@ +args: + - default +kwargs: + present: true + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': + /etc/s6-stuff/repositories/bleeding-edge + files.FileContents: + path=/etc/s6-stuff/repositories/bleeding-edge: + [ "repodir = /etc/s6-stuff/repositories/bleeding-edge" ] + s6.S6RepositoryList: + repository=/etc/s6-stuff/repositories/bleeding-edge: + - current + - desktop + - kiosk +commands: + - s6-rc-set-new -r /etc/s6-stuff/repositories/bleeding-edge default diff --git a/tests/operations/s6.manage_set/lookup_repo_no_envvar.yaml b/tests/operations/s6.manage_set/lookup_repo_no_envvar.yaml new file mode 100644 index 000000000..5d7a70342 --- /dev/null +++ b/tests/operations/s6.manage_set/lookup_repo_no_envvar.yaml @@ -0,0 +1,18 @@ +args: + - default +kwargs: + present: true + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: + [ "repodir = /etc/s6-stuff/repositories/bleeding-edge" ] + s6.S6RepositoryList: + repository=/etc/s6-stuff/repositories/bleeding-edge: + - current + - desktop + - kiosk +commands: + - s6-rc-set-new -r /etc/s6-stuff/repositories/bleeding-edge default diff --git a/tests/operations/s6.manage_set/multi_disable.yaml b/tests/operations/s6.manage_set/multi_disable.yaml new file mode 100644 index 000000000..23a366b49 --- /dev/null +++ b/tests/operations/s6.manage_set/multi_disable.yaml @@ -0,0 +1,27 @@ +args: + - default +kwargs: + prescriptions: + nftables: "usable" + mysqld: "usable" + tipidee: "usable" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + nftables: "active" + mysqld: "active" + tipidee: "active" +commands: + - s6-rc-set-change default usable nftables mysqld tipidee + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/multi_enable.yaml b/tests/operations/s6.manage_set/multi_enable.yaml new file mode 100644 index 000000000..e8e392b05 --- /dev/null +++ b/tests/operations/s6.manage_set/multi_enable.yaml @@ -0,0 +1,27 @@ +args: + - default +kwargs: + prescriptions: + nftables: "active" + mysqld: "active" + tipidee: "active" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + nftables: "usable" + mysqld: "usable" + tipidee: "usable" +commands: + - s6-rc-set-change default active nftables mysqld tipidee + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/multi_mixed.yaml b/tests/operations/s6.manage_set/multi_mixed.yaml new file mode 100644 index 000000000..f67d90ddc --- /dev/null +++ b/tests/operations/s6.manage_set/multi_mixed.yaml @@ -0,0 +1,32 @@ +args: + - default +kwargs: + prescriptions: + nftables: "always" + mysqld: "active" + php-fpm: "masked" + tipidee: "usable" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + nftables: "active" + mysqld: "usable" + php-fpm: "usable" + tipidee: "active" +commands: + - s6-rc-set-change default always nftables + - s6-rc-set-change default active mysqld + - s6-rc-set-change default usable tipidee + - s6-rc-set-change default masked php-fpm + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.manage_set/noop_commit.yaml b/tests/operations/s6.manage_set/noop_commit.yaml new file mode 100644 index 000000000..5e18615c9 --- /dev/null +++ b/tests/operations/s6.manage_set/noop_commit.yaml @@ -0,0 +1,15 @@ +args: + - default +kwargs: + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default +commands: [] +noop_description: the set "default" already exists, has the desired prescriptions, and no commit was requested diff --git a/tests/operations/s6.manage_set/noop_delete.yaml b/tests/operations/s6.manage_set/noop_delete.yaml new file mode 100644 index 000000000..aae8be4d2 --- /dev/null +++ b/tests/operations/s6.manage_set/noop_delete.yaml @@ -0,0 +1,14 @@ +args: + - default +kwargs: + present: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current +commands: [] +noop_description: the set "default" is already nonexistent diff --git a/tests/operations/s6.manage_set/noop_prescribe.yaml b/tests/operations/s6.manage_set/noop_prescribe.yaml new file mode 100644 index 000000000..e2c741775 --- /dev/null +++ b/tests/operations/s6.manage_set/noop_prescribe.yaml @@ -0,0 +1,27 @@ +args: + - default +kwargs: + prescriptions: + nftables: "active" + mysqld: "masked" + tipidee: "usable" + do_commit: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + nftables: "active" + mysqld: "masked" + tipidee: "usable" +commands: [] +noop_description: the set "default" already exists, has the desired prescriptions, and no commit was requested + diff --git a/tests/operations/s6.manage_set/standard.yaml b/tests/operations/s6.manage_set/standard.yaml new file mode 100644 index 000000000..877038a63 --- /dev/null +++ b/tests/operations/s6.manage_set/standard.yaml @@ -0,0 +1,23 @@ +args: + - default +kwargs: + prescriptions: + tipidee: "active" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - default + - desktop + - kiosk + s6.S6SetStatus: + repository=None, the_set=default: + tipidee: "usable" +commands: + - s6-rc-set-change default active tipidee + - s6-rc-set-fix default + - s6-rc-set-commit default diff --git a/tests/operations/s6.service/all_at_once.yaml b/tests/operations/s6.service/all_at_once.yaml new file mode 100644 index 000000000..c009e49d7 --- /dev/null +++ b/tests/operations/s6.service/all_at_once.yaml @@ -0,0 +1,38 @@ +args: + - [ nftables, mysqld, php-fpm, tipidee ] +kwargs: + running: true + restarted: true + reloaded: true + enabled: true + the_set: webserver + enabled_rx: active +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + - webserver + s6.S6SetStatus: + repository=None, the_set=webserver: + nftables: always + mysqld: active + php-fpm: masked + tipidee: usable + s6.S6LiveStatus: + nftables: true + mysqld: false + php-fpm: false + tipidee: false +commands: + - s6 live start mysqld php-fpm tipidee + - s6 live restart nftables + - s6 process kill -s SIGHUP nftables + # enabled argument applies enabled_rx to all services, hence why nftables is here, despite having + # "always" rx + - s6-rc-set-change webserver active nftables php-fpm tipidee + - s6-rc-set-fix webserver + - s6-rc-set-commit webserver diff --git a/tests/operations/s6.service/bring_down.yaml b/tests/operations/s6.service/bring_down.yaml new file mode 100644 index 000000000..c3ced0550 --- /dev/null +++ b/tests/operations/s6.service/bring_down.yaml @@ -0,0 +1,9 @@ +args: + - tipidee +kwargs: + running: false +facts: + s6.S6LiveStatus: + tipidee: true +commands: + - s6 live stop tipidee diff --git a/tests/operations/s6.service/bring_up.yaml b/tests/operations/s6.service/bring_up.yaml new file mode 100644 index 000000000..011cca817 --- /dev/null +++ b/tests/operations/s6.service/bring_up.yaml @@ -0,0 +1,9 @@ +args: + - tipidee +kwargs: + running: true +facts: + s6.S6LiveStatus: + tipidee: false +commands: + - s6 live start tipidee diff --git a/tests/operations/s6.service/command.yaml b/tests/operations/s6.service/command.yaml new file mode 100644 index 000000000..0c56b5be9 --- /dev/null +++ b/tests/operations/s6.service/command.yaml @@ -0,0 +1,11 @@ +args: + - tipidee +kwargs: + running: true + command: system reboot +facts: + s6.S6LiveStatus: + tipidee: false +commands: + - s6 live start tipidee + - s6 system reboot diff --git a/tests/operations/s6.service/command_injection.yaml b/tests/operations/s6.service/command_injection.yaml new file mode 100644 index 000000000..5d3e80275 --- /dev/null +++ b/tests/operations/s6.service/command_injection.yaml @@ -0,0 +1,11 @@ +args: + - tipidee +kwargs: + running: true + command: system reboot; rm -rf / +facts: + s6.S6LiveStatus: + tipidee: false +commands: + - s6 live start tipidee + - s6 system 'reboot;' rm -rf / diff --git a/tests/operations/s6.service/disable.yaml b/tests/operations/s6.service/disable.yaml new file mode 100644 index 000000000..0e165c47c --- /dev/null +++ b/tests/operations/s6.service/disable.yaml @@ -0,0 +1,20 @@ +args: + - tipidee +kwargs: + running: null + enabled: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: "active" +commands: + - s6-rc-set-change current usable tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/disabled_rx_is_masked.yaml b/tests/operations/s6.service/disabled_rx_is_masked.yaml new file mode 100644 index 000000000..029032cc7 --- /dev/null +++ b/tests/operations/s6.service/disabled_rx_is_masked.yaml @@ -0,0 +1,21 @@ +args: + - tipidee +kwargs: + enabled: false + disabled_rx: "masked" +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: "active" +commands: + - s6-rc-set-change current masked tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current + diff --git a/tests/operations/s6.service/dont_restart_if_stopped.yaml b/tests/operations/s6.service/dont_restart_if_stopped.yaml new file mode 100644 index 000000000..e7f13361f --- /dev/null +++ b/tests/operations/s6.service/dont_restart_if_stopped.yaml @@ -0,0 +1,9 @@ +args: + - tipidee +kwargs: + restarted: true +facts: + s6.S6LiveStatus: + tipidee: false +commands: [] +noop_description: "all specified services are down: tipidee" diff --git a/tests/operations/s6.service/enable.yaml b/tests/operations/s6.service/enable.yaml new file mode 100644 index 000000000..58ed88fee --- /dev/null +++ b/tests/operations/s6.service/enable.yaml @@ -0,0 +1,19 @@ +args: + - tipidee +kwargs: + enabled: true +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: "usable" +commands: + - s6-rc-set-change current active tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/enabled_rx_is_always.yaml b/tests/operations/s6.service/enabled_rx_is_always.yaml new file mode 100644 index 000000000..3628549f9 --- /dev/null +++ b/tests/operations/s6.service/enabled_rx_is_always.yaml @@ -0,0 +1,20 @@ +args: + - tipidee +kwargs: + enabled: true + enabled_rx: always +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: usable +commands: + - s6-rc-set-change current always tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/multi_bring_down.yaml b/tests/operations/s6.service/multi_bring_down.yaml new file mode 100644 index 000000000..c959f5aeb --- /dev/null +++ b/tests/operations/s6.service/multi_bring_down.yaml @@ -0,0 +1,11 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: false +facts: + s6.S6LiveStatus: + nftables: true + mysqld: true + tipidee: true +commands: + - s6 live stop nftables mysqld tipidee diff --git a/tests/operations/s6.service/multi_bring_up.yaml b/tests/operations/s6.service/multi_bring_up.yaml new file mode 100644 index 000000000..08f0f3245 --- /dev/null +++ b/tests/operations/s6.service/multi_bring_up.yaml @@ -0,0 +1,11 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: true +facts: + s6.S6LiveStatus: + nftables: false + mysqld: false + tipidee: false +commands: + - s6 live start nftables mysqld tipidee diff --git a/tests/operations/s6.service/multi_disable.yaml b/tests/operations/s6.service/multi_disable.yaml new file mode 100644 index 000000000..03b6996c4 --- /dev/null +++ b/tests/operations/s6.service/multi_disable.yaml @@ -0,0 +1,22 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: null + enabled: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + nftables: "active" + mysqld: "active" + tipidee: "active" +commands: + - s6-rc-set-change current usable nftables mysqld tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/multi_enable.yaml b/tests/operations/s6.service/multi_enable.yaml new file mode 100644 index 000000000..0de506ff6 --- /dev/null +++ b/tests/operations/s6.service/multi_enable.yaml @@ -0,0 +1,22 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: null + enabled: true +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + nftables: "usable" + mysqld: "usable" + tipidee: "usable" +commands: + - s6-rc-set-change current active nftables mysqld tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/multi_partial_disable.yaml b/tests/operations/s6.service/multi_partial_disable.yaml new file mode 100644 index 000000000..cfa1e63f3 --- /dev/null +++ b/tests/operations/s6.service/multi_partial_disable.yaml @@ -0,0 +1,22 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: null + enabled: false +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + nftables: "active" + mysqld: "usable" + tipidee: "active" +commands: + - s6-rc-set-change current usable nftables tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/multi_partial_enable.yaml b/tests/operations/s6.service/multi_partial_enable.yaml new file mode 100644 index 000000000..e38e08d55 --- /dev/null +++ b/tests/operations/s6.service/multi_partial_enable.yaml @@ -0,0 +1,22 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + running: null + enabled: true +facts: + server.Command: + 'command=printf %s "$S6_CONF"': "" + files.FileContents: + path=/etc/s6.conf: null + s6.S6RepositoryList: + repository=None: + - current + s6.S6SetStatus: + repository=None, the_set=current: + nftables: "usable" + mysqld: "active" + tipidee: "usable" +commands: + - s6-rc-set-change current active nftables tipidee + - s6-rc-set-fix current + - s6-rc-set-commit current diff --git a/tests/operations/s6.service/multi_partial_restart.yaml b/tests/operations/s6.service/multi_partial_restart.yaml new file mode 100644 index 000000000..11dae3171 --- /dev/null +++ b/tests/operations/s6.service/multi_partial_restart.yaml @@ -0,0 +1,11 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + restarted: true +facts: + s6.S6LiveStatus: + nftables: true + mysqld: false + tipidee: true +commands: + - s6 live restart nftables tipidee diff --git a/tests/operations/s6.service/multi_reload.yaml b/tests/operations/s6.service/multi_reload.yaml new file mode 100644 index 000000000..7cff5d2e6 --- /dev/null +++ b/tests/operations/s6.service/multi_reload.yaml @@ -0,0 +1,11 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + reloaded: true +facts: + s6.S6LiveStatus: + nftables: true + mysqld: true + tipidee: true +commands: + - s6 process kill -s SIGHUP nftables mysqld tipidee diff --git a/tests/operations/s6.service/multi_restart.yaml b/tests/operations/s6.service/multi_restart.yaml new file mode 100644 index 000000000..beab531a6 --- /dev/null +++ b/tests/operations/s6.service/multi_restart.yaml @@ -0,0 +1,11 @@ +args: + - [ nftables, mysqld, tipidee ] +kwargs: + restarted: true +facts: + s6.S6LiveStatus: + nftables: true + mysqld: true + tipidee: true +commands: + - s6 live restart nftables mysqld tipidee diff --git a/tests/operations/s6.service/nonstandard_reload_signal.yaml b/tests/operations/s6.service/nonstandard_reload_signal.yaml new file mode 100644 index 000000000..bc0ef2769 --- /dev/null +++ b/tests/operations/s6.service/nonstandard_reload_signal.yaml @@ -0,0 +1,10 @@ +args: + - tipidee +kwargs: + reloaded: true + reload_signal: SIGUSR1 +facts: + s6.S6LiveStatus: + tipidee: true +commands: + - s6 process kill -s SIGUSR1 tipidee diff --git a/tests/operations/s6.service/reload.yaml b/tests/operations/s6.service/reload.yaml new file mode 100644 index 000000000..a4fa1f49c --- /dev/null +++ b/tests/operations/s6.service/reload.yaml @@ -0,0 +1,9 @@ +args: + - tipidee +kwargs: + reloaded: true +facts: + s6.S6LiveStatus: + tipidee: true +commands: + - s6 process kill -s SIGHUP tipidee diff --git a/tests/operations/s6.service/restart.yaml b/tests/operations/s6.service/restart.yaml new file mode 100644 index 000000000..ac7f3ef50 --- /dev/null +++ b/tests/operations/s6.service/restart.yaml @@ -0,0 +1,9 @@ +args: + - tipidee +kwargs: + restarted: true +facts: + s6.S6LiveStatus: + tipidee: true +commands: + - s6 live restart tipidee diff --git a/tests/operations/s6.set_copy/noop.yaml b/tests/operations/s6.set_copy/noop.yaml new file mode 100644 index 000000000..cb5ff7ede --- /dev/null +++ b/tests/operations/s6.set_copy/noop.yaml @@ -0,0 +1,11 @@ +kwargs: + source: current + dest: myset +facts: + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: true + repository=None, the_set=myset: + tipidee: true +commands: [] +noop_description: the set "myset" exists and is identical to "current" diff --git a/tests/operations/s6.set_copy/save.yaml b/tests/operations/s6.set_copy/save.yaml new file mode 100644 index 000000000..826e73cdf --- /dev/null +++ b/tests/operations/s6.set_copy/save.yaml @@ -0,0 +1,12 @@ +args: + # dest + - myset +kwargs: + source: current +facts: + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: true + repository=None, the_set=myset: null +commands: + - s6-rc-set-copy current myset diff --git a/tests/operations/s6.set_copy/save_force.yaml b/tests/operations/s6.set_copy/save_force.yaml new file mode 100644 index 000000000..7e2d39eae --- /dev/null +++ b/tests/operations/s6.set_copy/save_force.yaml @@ -0,0 +1,13 @@ +args: + - myset +kwargs: + source: current + force: true +facts: + s6.S6SetStatus: + repository=None, the_set=current: + tipidee: true + repository=None, the_set=myset: + tipidee: false +commands: + - s6-rc-set-copy -f current myset diff --git a/tests/operations/s6.set_delete/delete.yaml b/tests/operations/s6.set_delete/delete.yaml new file mode 100644 index 000000000..a424b89cc --- /dev/null +++ b/tests/operations/s6.set_delete/delete.yaml @@ -0,0 +1,5 @@ +args: + - myset + - /etc/s6/repo +commands: + - s6-rc-set-delete -r /etc/s6/repo myset diff --git a/tests/operations/s6.set_delete/multi_delete.yaml b/tests/operations/s6.set_delete/multi_delete.yaml new file mode 100644 index 000000000..22ad4ad04 --- /dev/null +++ b/tests/operations/s6.set_delete/multi_delete.yaml @@ -0,0 +1,5 @@ +args: + - [ myset_a, myset_b, myset_c ] + - /etc/s6/repo +commands: + - s6-rc-set-delete -r /etc/s6/repo myset_a myset_b myset_c diff --git a/tests/operations/server.service/invalid.json b/tests/operations/server.service/invalid.json index d8b0fd902..066c6b05a 100644 --- a/tests/operations/server.service/invalid.json +++ b/tests/operations/server.service/invalid.json @@ -6,7 +6,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": false + "command=service": false, + "command=s6": false }, "files.Directory": { "path=/etc/init.d": false, @@ -19,6 +20,6 @@ }, "exception": { "name": "OperationError", - "message": "No init system found (no systemctl, initctl, /etc/init.d or /etc/rc.d found)" + "message": "No init system found (no systemctl, rc-service, initctl, sv, s6, /etc/init.d or /etc/rc.d found)" } } diff --git a/tests/operations/server.service/start_initd.json b/tests/operations/server.service/start_initd.json index 848c4ff3f..0a5b46b1d 100644 --- a/tests/operations/server.service/start_initd.json +++ b/tests/operations/server.service/start_initd.json @@ -10,7 +10,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": false + "command=service": false, + "command=s6": false }, "files.Directory": { "path=/etc/init.d": true, diff --git a/tests/operations/server.service/start_initd_service.json b/tests/operations/server.service/start_initd_service.json index dc76c44e6..54e3f61b8 100644 --- a/tests/operations/server.service/start_initd_service.json +++ b/tests/operations/server.service/start_initd_service.json @@ -10,7 +10,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": false + "command=service": false, + "command=s6": false }, "files.Link": { "path=/etc/init.d": false diff --git a/tests/operations/server.service/start_rcd.json b/tests/operations/server.service/start_rcd.json index f10673108..3e8ae224d 100644 --- a/tests/operations/server.service/start_rcd.json +++ b/tests/operations/server.service/start_rcd.json @@ -10,7 +10,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": false + "command=service": false, + "command=s6": false }, "files.Directory": { "path=/etc/init.d": false, diff --git a/tests/operations/server.service/start_rcd_freebsd_with_service_command.json b/tests/operations/server.service/start_rcd_freebsd_with_service_command.json index b78f5d3aa..724c193e4 100644 --- a/tests/operations/server.service/start_rcd_freebsd_with_service_command.json +++ b/tests/operations/server.service/start_rcd_freebsd_with_service_command.json @@ -10,7 +10,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": "/usr/sbin/service" + "command=service": "/usr/sbin/service", + "command=s6": false }, "files.Directory": { "path=/etc/init.d": false, diff --git a/tests/operations/server.service/start_rcd_netbsd.json b/tests/operations/server.service/start_rcd_netbsd.json index f27130762..427c8fe85 100644 --- a/tests/operations/server.service/start_rcd_netbsd.json +++ b/tests/operations/server.service/start_rcd_netbsd.json @@ -10,7 +10,8 @@ "command=initctl": false, "command=rc-service": false, "command=sv": false, - "command=service": "/usr/sbin/service" + "command=service": "/usr/sbin/service", + "command=s6": false }, "files.Directory": { "path=/etc/init.d": false, diff --git a/tests/operations/server.service/start_s6.yaml b/tests/operations/server.service/start_s6.yaml new file mode 100644 index 000000000..4ce84f966 --- /dev/null +++ b/tests/operations/server.service/start_s6.yaml @@ -0,0 +1,19 @@ +args: + - tipidee +kwargs: + running: true +facts: + server.Which: + command=systemctl: null + command=rc-service: null + command=initctl: null + command=sv: null + command=service: null + command=s6: /usr/bin/s6 + files.Directory: + path=/etc/init.d: false + path=/etc/rc.d: false + s6.S6LiveStatus: + tipidee: false +commands: + - s6 live start tipidee