diff --git a/pyinfra/facts/freebsd.py b/pyinfra/facts/freebsd.py new file mode 100644 index 000000000..dc42297f0 --- /dev/null +++ b/pyinfra/facts/freebsd.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from typing_extensions import Optional + +from pyinfra.api import FactBase +from pyinfra.api.command import QuoteString, make_formatted_string_command + + +class ServiceScript(FactBase): + @staticmethod + def command(srvname: str, jail: Optional[str] = None): + if jail is None: + jail = "" + + return make_formatted_string_command( + ( + "for service in `service -j {0} -l {1}`; do " + 'if [ {1} = \\"$service\\" ]; ' + 'then printf \\"%s\\\\n\\" \\"$service\\"; ' + "fi; " + "done" + ), + QuoteString(jail), + QuoteString(srvname), + ) + + +class ServiceStatus(FactBase): + @staticmethod + def command(srvname: str, jail: Optional[str] = None): + if jail is None: + jail = "" + + return make_formatted_string_command( + ( + "service -j {0} {1} status > /dev/null 2>&1; " + "if [ $? -eq 0 ]; then " + "echo running; " + "fi" + ), + QuoteString(jail), + QuoteString(srvname), + ) + + +class Sysrc(FactBase): + @staticmethod + def command(parameter: str, jail: Optional[str] = None): + if jail is None: + command = make_formatted_string_command( + ("sysrc -in -- {0} || true"), QuoteString(parameter) + ) + else: + command = make_formatted_string_command( + ("sysrc -j {0} -in -- {1} || true"), QuoteString(jail), QuoteString(parameter) + ) + + return command + + +class PkgPackage(FactBase): + @staticmethod + def command(package: str, jail: Optional[str] = None): + if jail is None: + command = make_formatted_string_command( + ("pkg info -E -- {0} 2> /dev/null || true"), QuoteString(package) + ) + else: + command = make_formatted_string_command( + ("pkg -j {0} info -E -- {1} 2> /dev/null || true"), + QuoteString(jail), + QuoteString(package), + ) + + return command diff --git a/pyinfra/operations/freebsd/__init__.py b/pyinfra/operations/freebsd/__init__.py new file mode 100644 index 000000000..6da4fc977 --- /dev/null +++ b/pyinfra/operations/freebsd/__init__.py @@ -0,0 +1,12 @@ +# This file only exists to support: +# from pyinfra.operations import freebsd +# freebsd.X.Y + +from glob import glob +from os import path + +module_filenames = glob(path.join(path.dirname(__file__), "*.py")) +module_names = [path.basename(name)[:-3] for name in module_filenames] +__all__ = [name for name in module_names if name != "__init__"] + +from . import * # noqa diff --git a/pyinfra/operations/freebsd/freebsd_update.py b/pyinfra/operations/freebsd/freebsd_update.py new file mode 100644 index 000000000..c66316814 --- /dev/null +++ b/pyinfra/operations/freebsd/freebsd_update.py @@ -0,0 +1,70 @@ +""" +Fetch and install binary updates to FreeBSD. +""" + +from __future__ import annotations + +from typing_extensions import List, Optional, Union + +from pyinfra.api import QuoteString, StringCommand, operation + + +@operation() +def update( + force: bool = False, + basedir: Optional[str] = None, + workdir: Optional[str] = None, + conffile: Optional[str] = None, + jail: Optional[str] = None, + key: Optional[str] = None, + currently_running: Optional[str] = None, + server: Optional[str] = None, +): + """ + Based on the currently installed world and the configuration options set, fetch + all available binary updates and install them. + + + force: See ``-F`` in ``freebsd-update(8)``. + + basedir: See ``-b`` in ``freebsd-update(8)``. + + workdir: See ``-d`` in ``freebsd-update(8)``. + + conffile: See ``-f`` in ``freebsd-update(8)``. + + jail: See ``-j`` in ``freebsd-update(8)``. + + key: See ``-k`` in ``freebsd-update(8)``. + + currently_running: See ``--currently-running`` in ``freebsd-update(8)``. + + server: See ``-s`` in ``freebsd-update(8)``. + + **Example:** + + .. code:: python + + freebsd_update.update() + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.extend(["PAGER=cat", "freebsd-update", "--not-running-from-cron"]) + + if force: + args.append("-F") + + if basedir is not None: + args.extend(["-b", QuoteString(basedir)]) + + if workdir is not None: + args.extend(["-d", QuoteString(workdir)]) + + if conffile is not None: + args.extend(["-f", QuoteString(conffile)]) + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + if key is not None: + args.extend(["-k", QuoteString(key)]) + + if server is not None: + args.extend(["-s", QuoteString(server)]) + + args.extend(["fetch", "install"]) + + yield StringCommand(*args) diff --git a/pyinfra/operations/freebsd/pkg.py b/pyinfra/operations/freebsd/pkg.py new file mode 100644 index 000000000..dffb1b2a0 --- /dev/null +++ b/pyinfra/operations/freebsd/pkg.py @@ -0,0 +1,219 @@ +""" +Manage FreeBSD packages. +""" + +from __future__ import annotations + +from typing_extensions import List, Optional, Union + +from pyinfra import host +from pyinfra.api import QuoteString, StringCommand, operation +from pyinfra.facts.freebsd import PkgPackage + + +@operation() +def update(jail: Optional[str] = None, force: bool = False, reponame: Optional[str] = None): + """ + Update the local catalogues of the enabled package repositories. + + + jail: See ``-j`` in ``pkg(8)``. + + force: See ``-f`` in ``pkg-update(8)``. + + reponame: See ``-r`` in ``pkg-update(8)`` + + **Examples:** + + .. code:: python + + # host + pkg.update() + + # jail + pkg.update( + jail="nginx" + ) + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["update"]) + + if force: + args.append("-f") + + if reponame is not None: + args.extend(["-r", QuoteString(reponame)]) + + yield StringCommand(*args) + + +@operation() +def upgrade(jail: Optional[str] = None, force: bool = False, reponame: Optional[str] = None): + """ + Perform upgrades of package software distributions. + + + jail: See ``-j`` in ``pkg(8)``. + + force: See ``-f`` in ``pkg-upgrade(8)``. + + reponame: See ``-r`` in ``pkg-upgrade(8)``. + + **Examples:** + + .. code:: python + + # host + pkg.upgrade() + + # jail + pkg.upgrade( + jail="nginx" + ) + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["upgrade", "-y"]) + + if force: + args.append("-f") + + if reponame is not None: + args.extend(["-r", QuoteString(reponame)]) + + yield StringCommand(*args) + + +@operation() +def install(package: str, jail: Optional[str] = None, reponame: Optional[str] = None): + """ + Install packages from remote packages repositories or local archives. + + + package: Package to install. + + jail: See ``-j`` in ``pkg(8)``. + + reponame: See ``-r`` in ``pkg-install(8)``. + + **Example:** + + .. code:: python + + pkg.install("nginx") + """ + + if host.get_fact(PkgPackage, package=package): + host.noop(f"Package '{package}' already installed") + return + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["install", "-y"]) + + if reponame is not None: + args.extend(["-r", QuoteString(reponame)]) + + args.extend(["--", QuoteString(package)]) + + yield StringCommand(*args) + + +@operation() +def remove(package: str, jail: Optional[str] = None): + """ + Deletes packages from the database and the system. + + + package: Package to remove. + + jail: See ``-j`` in ``pkg(8)``. + + **Example:** + + .. code:: python + + pkg.remove("nginx") + """ + + if not host.get_fact(PkgPackage, package=package): + host.noop(f"Package '{package}' cannot be found") + return + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["remove", "-y"]) + + args.extend(["--", QuoteString(package)]) + + yield StringCommand(*args) + + +@operation() +def autoremove(jail: Optional[str] = None): + """ + Remove orphan packages. + + + jail: See ``-j`` in ``pkg(8)``. + + **Example:** + + .. code:: python + + pkg.autoremove() + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["autoremove", "-y"]) + + yield StringCommand(*args) + + +@operation() +def clean(all_pkg: bool = False, jail: Optional[str] = None): + """ + Clean the local cache of fetched remote packages. + + + all_pkg: See ``-a`` in ``pkg-clean(8)``. + + jail: See ``-j`` in ``pkg(8)``. + + **Example:** + + .. code:: python + + pkg.clean( + all_pkg=True + ) + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.append("pkg") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["clean", "-y"]) + + if all_pkg: + args.append("-a") + + yield StringCommand(*args) diff --git a/pyinfra/operations/freebsd/service.py b/pyinfra/operations/freebsd/service.py new file mode 100644 index 000000000..1ff63cdf5 --- /dev/null +++ b/pyinfra/operations/freebsd/service.py @@ -0,0 +1,120 @@ +""" +Manage FreeBSD services. +""" + +from __future__ import annotations + +from enum import Enum + +from typing_extensions import List, Optional, Union + +from pyinfra import host +from pyinfra.api import QuoteString, StringCommand, operation +from pyinfra.api.exceptions import OperationValueError +from pyinfra.facts.freebsd import ServiceScript, ServiceStatus + + +class ServiceStates(Enum): + SRV_STARTED: int = 0 + SRV_STOPPED: int = 1 + SRV_RESTARTED: int = 2 + SRV_RELOADED: int = 3 + SRV_CUSTOM: int = 4 + + +@operation() +def service( + srvname: str, + jail: Optional[str] = None, + state: "ServiceStates" = ServiceStates.SRV_STARTED, + command: Optional[Union[str, List[str]]] = None, + environment: Optional[List[str]] = None, + verbose: bool = False, +): + """ + Control (start/stop/etc.) ``rc(8)`` scripts. + + + srvname: Service. + + jail: See ``-j`` in ``service(8)``. + + state: Desire state of the service. + + command: When ``state`` is ``SRV_CUSTOM``, the command to execute. + + environment: See ``-E`` in ``service(8)``. + + verbose: See ``-v`` in ``service(8)``. + + States: + There are a few states you can use to manipulate the service: + + - SRV_STARTED: The service must be started. + - SRV_STOPPED: The service must be stopped. + - SRV_RESTARTED: The service must be restarted. + - SRV_RELOADED: The service must be reloaded. + - SRV_CUSTOM: Run a custom command for this service. + + **Examples:** + + .. code:: python + + # Start a service. + service.service( + "beanstalkd", + state=service.ServiceStates.SRV_STARTED + ) + + # Execute a custom command. + service.service( + "sopel", + state=service.ServiceStates.SRV_CUSTOM, + command="configure" + ) + """ + + if not host.get_fact(ServiceScript, srvname=srvname, jail=jail): + host.noop(f"Cannot find rc(8) script '{srvname}'") + return + + args: List[Union[str, "QuoteString"]] = [] + + args.append("service") + + if verbose: + args.append("-v") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + if environment is not None: + for env_var in environment: + args.extend(["-E", QuoteString(env_var)]) + + if state == ServiceStates.SRV_STARTED: + if host.get_fact(ServiceStatus, srvname=srvname, jail=jail): + host.noop(f"Service '{srvname}' already started") + return + + args.extend([QuoteString(srvname), "start"]) + + elif state == ServiceStates.SRV_STOPPED: + if not host.get_fact(ServiceStatus, srvname=srvname, jail=jail): + host.noop(f"Service '{srvname}' already stopped") + return + + args.extend([QuoteString(srvname), "stop"]) + elif state == ServiceStates.SRV_RESTARTED: + args.extend([QuoteString(srvname), "restart"]) + + elif state == ServiceStates.SRV_RELOADED: + args.extend([QuoteString(srvname), "reload"]) + + elif state == ServiceStates.SRV_CUSTOM: + args.append(QuoteString(srvname)) + + if command is not None: + if isinstance(command, str): + command = [command] + + args.extend([QuoteString(c) for c in command]) + + else: + raise OperationValueError("Invalid service command!") + + yield StringCommand(*args) diff --git a/pyinfra/operations/freebsd/sysrc.py b/pyinfra/operations/freebsd/sysrc.py new file mode 100644 index 000000000..c3858f5ce --- /dev/null +++ b/pyinfra/operations/freebsd/sysrc.py @@ -0,0 +1,94 @@ +""" +Manipulate system rc files. +""" + +from __future__ import annotations + +from enum import Enum + +from typing_extensions import List, Optional, Union + +from pyinfra import host +from pyinfra.api import QuoteString, StringCommand, operation +from pyinfra.api.exceptions import OperationValueError +from pyinfra.facts.freebsd import Sysrc + + +class SysrcCommands(Enum): + SYSRC_ADD: int = 0 + SYSRC_SUB: int = 1 + SYSRC_SET: int = 2 + SYSRC_DEL: int = 3 + + +@operation() +def sysrc( + parameter: str, + value: str, + jail: Optional[str] = None, + command: "SysrcCommands" = SysrcCommands.SYSRC_SET, + overwrite: bool = False, +): + """ + Safely edit system rc files. + + + parameter: Parameter to manipulate. + + value: Value, if the parameter requires it. + + jail: See ``-j`` in ``sysrc(8)``. + + command: Desire state of the parameter. + + overwrite: Overwrite the value of the parameter when ``command`` is set to ``SYSRC_SET``. + + Commands: + There are a few commands you can use to manipulate the rc file: + + - SYSRC_ADD: Adds the value to the parameter. + - SYSRC_SUB: Delete the parameter value. + - SYSRC_SET: Change the parameter value. If the parameter already has a value + set, the changes will not be applied unless ``overwrite`` is set + to ``True``. + - SYSRC_DEL: Delete the parameter. + + **Example:** + + .. code:: python + + sysrc.sysrc( + "beanstalkd_enable", + "YES", + command=sysrc.sysrc.SysrcCommands.SYSRC_SET + ) + """ + + args: List[Union[str, "QuoteString"]] = [] + + args.extend(["sysrc", "-i"]) + + if command == SysrcCommands.SYSRC_DEL: + sign = "=" + + if not host.get_fact(Sysrc, parameter=parameter, jail=jail): + host.noop(f"Cannot find sysrc(8) parameter '{parameter}'") + return + + elif command == SysrcCommands.SYSRC_SET: + sign = "=" + + if not overwrite and host.get_fact(Sysrc, parameter=parameter, jail=jail): + host.noop(f"sysrc(8) parameter '{parameter}' already set") + return + + elif command == SysrcCommands.SYSRC_ADD: + sign = "+=" + + elif command == SysrcCommands.SYSRC_SUB: + sign = "-=" + + else: + raise OperationValueError("Invalid sysrc command!") + + if jail is not None: + args.extend(["-j", QuoteString(jail)]) + + args.extend(["--", QuoteString(f"{parameter}{sign}{value}")]) + + yield StringCommand(*args)