Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

systemd.service() does not support wildcards #1207

Open
NichtJens opened this issue Sep 17, 2024 · 1 comment
Open

systemd.service() does not support wildcards #1207

NichtJens opened this issue Sep 17, 2024 · 1 comment

Comments

@NichtJens
Copy link

NichtJens commented Sep 17, 2024

Is your feature request related to a problem? Please describe

Commands like systemctl stop abc* support glob-style wildcards.

However, systemd.service() does not.

Describe the solution you'd like

The obvious "user space" solution is probably something like this:

from fnmatch import fnmatch

from pyinfra import host
from pyinfra.facts.systemd import SystemdEnabled
from pyinfra.operations import systemd

def services(pattern, name=None, **kwargs):
    """
    Manage the state of all systemd services matching a glob pattern
    """
    services = get_enabled_services()

    for service in services:
        if fnmatch(service, pattern):
            iname = None if name is None else f"{name}: {service}" 

            systemd.service(
                name=iname,
                service=service,
                **kwargs
            )

def get_enabled_services():
    """
    Return a list of enabled systemd services
    """
    units = host.get_fact(SystemdEnabled)
    return [name for name, enabled in units.items() if enabled and name.endswith(".service")]

However, this works through the list of services step by step, which means it accumulates the run time. Doing the same operation via the systemctl command will perform all changes in parallel and needs only approximately the longest run time of any of the changes.

Reading the underlying code, it turns out that this basically already works. The smallest possible change that resolves the issue would be the following:

In pyinfra/pyinfra/operations/util/service.py replace line 19

is_running = statuses.get(name, None)

with

is_running = any(s for n, s in statuses.items() if fnmatch(n, name))

I.e., if any of the services that match the pattern is running, treat the pattern as running. This is compatible with the current non-wildcard name behavior since fnmatch matches non-wildcard strings to themselves.

The question is whether this is enough or if it is wished that the list of services that is interacted with is shown in the printed output. This would need a much bigger change, I fear.

Either way, I would be happy to provide a PR for the above solution (plus the needed docs changes, etc.) or work on a more complex solution as I would really like this feature to exist.

@NichtJens
Copy link
Author

Thinking about this further, there is a chance for a performance impact for the non-wildcard case if there are many services -- simply because matching every item in a large dict is worse than a single lookup in the dict. To mitigate this:

is_running = statuses.get(name, None)
if is_running is None:
    matching_is_running = [s for n, s in statuses.items() if fnmatch(n, name)]
    if matching_is_running:
        is_running = any(matching_is_running)

So, only if the lookup fails find the matches. If there is no match at all, stay with is_running=None (this was potentially wrong in the simpler solution above). If there are matches, use any() to get the overall state of the pattern matches.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant