From 9d38329849ccd5f4ab25c79a5719e47a1405ec41 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 16 Jun 2026 12:50:29 -0400 Subject: [PATCH 01/26] feat(brew): add support for brew (un)trust --- src/pyinfra/facts/brew.py | 106 +++++++++++++++--- src/pyinfra/operations/brew.py | 98 ++++++++++++++-- .../facts/brew.BrewTrusted/empty_output.json | 6 + tests/facts/brew.BrewTrusted/no_output.json | 6 + .../facts/brew.BrewTrusted/valid_output.json | 13 +++ tests/operations/brew.tap/add_exists.json | 7 +- .../brew.tap/add_exists_and_trust.json | 10 ++ tests/operations/brew.tap/add_tap.json | 7 +- .../brew.tap/add_tap_and_trust.json | 9 ++ .../brew.tap/add_tap_url_and_trust.json | 12 ++ .../brew.tap/add_tap_url_exists.json | 15 +-- .../brew.tap/add_tap_url_no_src.json | 9 +- .../brew.trust/trust_already_trusted.json | 13 +++ ...ust_item_names_trusted_different_kind.json | 12 ++ .../brew.trust/trust_needs_trust.json | 6 + .../brew.trust/untrust_already_untrusted.json | 6 + .../brew.trust/untrust_currently_trusted.json | 8 ++ .../brew.trust/zero_length_item_name.json | 9 ++ .../brew.trust/zero_length_names.json | 9 ++ 19 files changed, 314 insertions(+), 47 deletions(-) create mode 100644 tests/facts/brew.BrewTrusted/empty_output.json create mode 100644 tests/facts/brew.BrewTrusted/no_output.json create mode 100644 tests/facts/brew.BrewTrusted/valid_output.json create mode 100644 tests/operations/brew.tap/add_exists_and_trust.json create mode 100644 tests/operations/brew.tap/add_tap_and_trust.json create mode 100644 tests/operations/brew.tap/add_tap_url_and_trust.json create mode 100644 tests/operations/brew.trust/trust_already_trusted.json create mode 100644 tests/operations/brew.trust/trust_item_names_trusted_different_kind.json create mode 100644 tests/operations/brew.trust/trust_needs_trust.json create mode 100644 tests/operations/brew.trust/untrust_already_untrusted.json create mode 100644 tests/operations/brew.trust/untrust_currently_trusted.json create mode 100644 tests/operations/brew.trust/zero_length_item_name.json create mode 100644 tests/operations/brew.trust/zero_length_names.json diff --git a/src/pyinfra/facts/brew.py b/src/pyinfra/facts/brew.py index 1a3e41e89..28961dfcc 100644 --- a/src/pyinfra/facts/brew.py +++ b/src/pyinfra/facts/brew.py @@ -1,6 +1,10 @@ from __future__ import annotations +import json import re +from collections.abc import Iterable, Mapping, Sequence +from enum import StrEnum, unique +from typing import cast from typing_extensions import override @@ -12,7 +16,15 @@ BREW_REGEX = r"^([^\s]+)\s([0-9\._+a-z\-]+)" -def new_cask_cli(version): +@unique +class BrewTrustKind(StrEnum): + CASK = "casks" + COMMAND = "commands" + FORMULA = "formulae" + TAP = "taps" + + +def _new_cask_cli(version: Sequence[int]) -> bool: """ Returns true if brew is version 2.6.0 or later and thus has the new CLI for casks. i.e. we need to use brew list --cask instead of brew cask list @@ -25,13 +37,12 @@ def new_cask_cli(version): VERSION_MATCHER = re.compile(r"^Homebrew\s+(?P\d+)\.(?P\d+)\.(?P\d+).*$") -def unknown_version(): - return [0, 0, 0] +BrewVersionType = list[int] -class BrewVersion(FactBase): +class BrewVersion(FactBase[Sequence[int]]): """ - Returns the version of brew installed as a semantic versioning tuple: + Returns the version of brew installed as a semantic versioning list: .. code:: python @@ -49,20 +60,23 @@ def requires_command(self) -> str: @override @staticmethod - def default(): + def default() -> BrewVersionType: return [0, 0, 0] @override - def process(self, output): - out = list(output)[0] - m = VERSION_MATCHER.match(out) - if m is not None: + def process(self, output: Iterable[str]) -> BrewVersionType: + if ((out := next(iter(output), None)) is not None) and ( + (m := VERSION_MATCHER.match(out)) is not None + ): return [int(m.group(key)) for key in ["major", "minor", "patch"]] - logger.warning("could not parse version string from brew: %s", out) + logger.warning(f"could not parse version string from brew: '{out}'") return self.default() -class BrewPackages(FactBase): +BrewPackingMapping = dict[str, set[str]] + + +class BrewPackages(FactBase[BrewPackingMapping]): """ Returns a dict of installed brew packages: @@ -84,7 +98,7 @@ def requires_command(self) -> str: default = dict @override - def process(self, output): + def process(self, output: Iterable[str]) -> BrewPackingMapping: return parse_packages(BREW_REGEX, output) @@ -111,9 +125,21 @@ def requires_command(self) -> str: return "brew" -class BrewTaps(FactBase): +BrewTapList = Iterable[str] + + +class BrewTaps(FactBase[BrewTapList]): """ Returns a list of brew taps. + + .. code:: python + { + "@local": [ + "homebrew/cask", + "homebrew/core", + "homebrew/services", + ] + } """ @override @@ -127,5 +153,55 @@ def requires_command(self) -> str: default = list @override - def process(self, output): + def process(self, output: Iterable[str]) -> BrewTapList: return output + + +BrewTrustMapping = Mapping[str, Sequence[str]] + + +class BrewTrusted(FactBase[BrewTrustMapping]): + """ + Returns a dict with lists of the casks, commands, formulae and taps that have + been marked as trusted + + .. code:: python + { + "@local": { + "taps": [ + "borgbackup/tap" + ], + "formulae": [], + "casks": [], + "commands": [] + } + } + """ + + @override + def command(self) -> str: + return "brew trust --json v1" + + @override + def requires_command(self) -> str: + return "brew" + + @override + @staticmethod + def default() -> BrewTrustMapping: + return {kind: [] for kind in BrewTrustKind.__members__.values()} + + @override + def process(self, output: Iterable[str]) -> BrewTrustMapping: + error = False + body = "\n".join(s for s in output) + try: + result = cast("BrewTrustMapping", json.loads(body)) + except (json.JSONDecodeError, TypeError, RecursionError): + error = True + + if error or not all(kind in result for kind in BrewTrustKind.__members__.values()): + logger.warning(f"unexpected output from brew trust: '{body}'") + result = self.default() + + return result diff --git a/src/pyinfra/operations/brew.py b/src/pyinfra/operations/brew.py index 329deb340..6d59d9c93 100644 --- a/src/pyinfra/operations/brew.py +++ b/src/pyinfra/operations/brew.py @@ -8,7 +8,17 @@ from pyinfra import host from pyinfra.api import operation -from pyinfra.facts.brew import BrewCasks, BrewPackages, BrewTaps, BrewVersion, new_cask_cli +from pyinfra.api.command import QuoteString, StringCommand +from pyinfra.api.exceptions import OperationValueError +from pyinfra.facts.brew import ( + BrewCasks, + BrewPackages, + BrewTaps, + BrewTrusted, + BrewTrustKind, + BrewVersion, + _new_cask_cli, +) from .util.packaging import ensure_packages @@ -97,7 +107,7 @@ def packages( def cask_args(): - return ("", " --cask") if new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "") + return ("", " --cask") if _new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "") @operation(is_idempotent=False) @@ -159,12 +169,18 @@ def casks( @operation() -def tap(src: str | None = None, present=True, url: str | None = None): +def tap( + src: str | None = None, + present: bool = True, + trusted: bool | None = None, + url: str | None = None, +): """ Add/remove brew taps. + src: the name of the tap - + present: whether this tap should be present or not + + present: whether this tap should be present or not. Default True. + + trusted: whether or not this tap should be trusted. Default False. + url: the url of the tap. See https://docs.brew.sh/Taps **Examples:** @@ -174,12 +190,14 @@ def tap(src: str | None = None, present=True, url: str | None = None): brew.tap( name="Add a brew tap", src="includeos/includeos", + trusted=True, ) # Just url is equivalent to # `brew tap kptdev/kpt https://github.com/kptdev/kpt` brew.tap( url="https://github.com/kptdev/kpt", + trusted=True, ) # src and url is equivalent to @@ -187,6 +205,7 @@ def tap(src: str | None = None, present=True, url: str | None = None): brew.tap( src="example/project", url="https://github.example.com/project", + trusted=True, ) # Multiple taps @@ -194,10 +213,16 @@ def tap(src: str | None = None, present=True, url: str | None = None): brew.tap( name={f"Add brew tap {tap}"}, src=tap, + trusted=True, ) """ + def mk_trust_cmd(tap: str, *, trust: bool | None = None) -> StringCommand: + return StringCommand("brew", "trust" if trust else "untrust", "--tap", QuoteString(tap)) + + trusted = trusted or False + if not (src or url): host.noop("no tap was specified") return @@ -213,20 +238,75 @@ def tap(src: str | None = None, present=True, url: str | None = None): if present and already_tapped: host.noop(f"tap {src} already exists") + trusted_taps = host.get_fact(BrewTrusted).get("taps", []) + if (trusted and (src not in trusted_taps)) or ((not trusted) and (src in trusted_taps)): + yield mk_trust_cmd(src, trust=trusted) return if already_tapped: - yield f"brew untap {src}" + yield StringCommand("brew", "untap", QuoteString(src)) return if not present: host.noop(f"tap {src} does not exist") return - cmd = f"brew tap {src}" - + args = [QuoteString(src)] if url is not None: - cmd = " ".join([cmd, url]) + args.append(QuoteString(url)) + + yield StringCommand("brew", "tap", *args) + + if trusted: # if not already present, can't be trusted so no check of BrewTrusted + yield mk_trust_cmd(src, trust=True) - yield cmd return + + +TRUST_SRC_AND_OPTION = { + BrewTrustKind.CASK.value: "--cask", + BrewTrustKind.COMMAND.value: "--command", + BrewTrustKind.FORMULA.value: "--formula", + BrewTrustKind.TAP.value: "--tap", +} + + +@operation() +def trust(items: str | list[str], kind: BrewTrustKind, trusted: bool): + """ + Trust/untrust brew casks, commands, formulae and/or taps (see https://docs.brew.sh/Tap-Trust) + + + item: the cask, command, formula or tap to be trusted or untrusted + + kind: whether the item is a CASK, COMMAND, FORMULA or TAP (using BrewTrustKind enum) + + trusted: whether this item should be trusted or not. no default, must be specified + + **Examples:** + + .. code:: python + + brew.trust( + name="Mark magic tap as trusted", + item="includeos/includeos", + kind=BrewTrustKind.TAP, + trust=True + ) + """ + item_set = set(items if isinstance(items, list) else [items]) + if any(len(item) < 1 for item in item_set): + raise OperationValueError("all items must have non-zero length names") + # TODO: remove this once the test infrastructure supports enums + if isinstance(kind, str): + try: + kind = BrewTrustKind(kind) + except (TypeError, ValueError): + raise OperationValueError from None + desired_state = "trust" if trusted else "untrust" + trusted_items = set(host.get_fact(BrewTrusted).get(kind.value, [])) + found = item_set & trusted_items + need_to_change = (item_set - found) if trusted else found + already_ok = item_set - need_to_change + + for item in sorted(need_to_change): + yield StringCommand("brew", desired_state, TRUST_SRC_AND_OPTION[kind], QuoteString(item)) + if len(already_ok) > 0: + host.noop(f"{', '.join(sorted(already_ok))} {kind.value} already {desired_state}ed") diff --git a/tests/facts/brew.BrewTrusted/empty_output.json b/tests/facts/brew.BrewTrusted/empty_output.json new file mode 100644 index 000000000..3ea65074e --- /dev/null +++ b/tests/facts/brew.BrewTrusted/empty_output.json @@ -0,0 +1,6 @@ +{ + "command": "brew trust --json v1", + "requires_command": "brew", + "output": ["{}"], + "fact": {"casks": [], "commands": [], "formulae": [], "taps": []}, +} diff --git a/tests/facts/brew.BrewTrusted/no_output.json b/tests/facts/brew.BrewTrusted/no_output.json new file mode 100644 index 000000000..77ff1607d --- /dev/null +++ b/tests/facts/brew.BrewTrusted/no_output.json @@ -0,0 +1,6 @@ +{ + "command": "brew trust --json v1", + "requires_command": "brew", + "output": [], + "fact": {"casks": [], "commands": [], "formulae": [], "taps": []}, +} diff --git a/tests/facts/brew.BrewTrusted/valid_output.json b/tests/facts/brew.BrewTrusted/valid_output.json new file mode 100644 index 000000000..f45c58f14 --- /dev/null +++ b/tests/facts/brew.BrewTrusted/valid_output.json @@ -0,0 +1,13 @@ +{ + "command": "brew trust --json v1", + "requires_command": "brew", + "output": [ + "{", + '"casks":["a"],', + '"taps":["d"],', + '"formulae":["c"],', + '"commands":["b"]', + "}", + ], + "fact": {"casks": ["a"], "commands": ["b"], "formulae": ["c"], "taps": ["d"]}, +} diff --git a/tests/operations/brew.tap/add_exists.json b/tests/operations/brew.tap/add_exists.json index faea93800..6bf8434da 100644 --- a/tests/operations/brew.tap/add_exists.json +++ b/tests/operations/brew.tap/add_exists.json @@ -1,10 +1,9 @@ { "args": ["homebrew/cask"], "facts": { - "brew.BrewTaps": [ - "homebrew/cask" - ] + "brew.BrewTaps": ["homebrew/cask"], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, }, "commands": [], - "noop_description": "tap homebrew/cask already exists" + "noop_description": "tap homebrew/cask already exists", } diff --git a/tests/operations/brew.tap/add_exists_and_trust.json b/tests/operations/brew.tap/add_exists_and_trust.json new file mode 100644 index 000000000..708a9969a --- /dev/null +++ b/tests/operations/brew.tap/add_exists_and_trust.json @@ -0,0 +1,10 @@ +{ + "args": ["homebrew/cask"], + "kwargs": {"trusted": true}, + "facts": { + "brew.BrewTaps": ["homebrew/cask"], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + }, + "commands": ["brew trust --tap homebrew/cask"], + "noop_description": "tap homebrew/cask already exists", +} diff --git a/tests/operations/brew.tap/add_tap.json b/tests/operations/brew.tap/add_tap.json index 9ccfdd182..07bc619e9 100644 --- a/tests/operations/brew.tap/add_tap.json +++ b/tests/operations/brew.tap/add_tap.json @@ -1,9 +1,8 @@ { "args": ["homebrew/cask"], "facts": { - "brew.BrewTaps": [] + "brew.BrewTaps": [], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, }, - "commands": [ - "brew tap homebrew/cask" - ] + "commands": ["brew tap homebrew/cask"], } diff --git a/tests/operations/brew.tap/add_tap_and_trust.json b/tests/operations/brew.tap/add_tap_and_trust.json new file mode 100644 index 000000000..cb5173f84 --- /dev/null +++ b/tests/operations/brew.tap/add_tap_and_trust.json @@ -0,0 +1,9 @@ +{ + "args": ["homebrew/cask"], + "kwargs": {"trusted": true}, + "facts": { + "brew.BrewTaps": [], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + }, + "commands": ["brew tap homebrew/cask", "brew trust --tap homebrew/cask"], +} diff --git a/tests/operations/brew.tap/add_tap_url_and_trust.json b/tests/operations/brew.tap/add_tap_url_and_trust.json new file mode 100644 index 000000000..090b15a12 --- /dev/null +++ b/tests/operations/brew.tap/add_tap_url_and_trust.json @@ -0,0 +1,12 @@ +{ + "args": ["homebrew/cask"], + "kwargs": {"trusted": true, "url": "https://github.com/homebrew/cask"}, + "facts": { + "brew.BrewTaps": [], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + }, + "commands": [ + "brew tap homebrew/cask https://github.com/homebrew/cask", + "brew trust --tap homebrew/cask", + ], +} diff --git a/tests/operations/brew.tap/add_tap_url_exists.json b/tests/operations/brew.tap/add_tap_url_exists.json index 0ad83370e..6adaca23c 100644 --- a/tests/operations/brew.tap/add_tap_url_exists.json +++ b/tests/operations/brew.tap/add_tap_url_exists.json @@ -1,15 +1,10 @@ { - "args": [ - "homebrew/cask" - ], - "kwargs": { - "url": "https://github.com/homebrew/cask" - }, + "args": ["homebrew/cask"], + "kwargs": {"url": "https://github.com/homebrew/cask"}, "facts": { - "brew.BrewTaps": [ - "homebrew/cask" - ] + "brew.BrewTaps": ["homebrew/cask"], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, }, "commands": [], - "noop_description": "tap homebrew/cask already exists" + "noop_description": "tap homebrew/cask already exists", } diff --git a/tests/operations/brew.tap/add_tap_url_no_src.json b/tests/operations/brew.tap/add_tap_url_no_src.json index 241256cf9..d5bf30c5c 100644 --- a/tests/operations/brew.tap/add_tap_url_no_src.json +++ b/tests/operations/brew.tap/add_tap_url_no_src.json @@ -1,10 +1,9 @@ { "args": [], - "kwargs": { - "url": "https://github.com/homebrew/cask" - }, + "kwargs": {"url": "https://github.com/homebrew/cask"}, "facts": { - "brew.BrewTaps": [] + "brew.BrewTaps": [], + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, }, - "commands": ["brew tap homebrew/cask https://github.com/homebrew/cask"] + "commands": ["brew tap homebrew/cask https://github.com/homebrew/cask"], } diff --git a/tests/operations/brew.trust/trust_already_trusted.json b/tests/operations/brew.trust/trust_already_trusted.json new file mode 100644 index 000000000..c6dc73b98 --- /dev/null +++ b/tests/operations/brew.trust/trust_already_trusted.json @@ -0,0 +1,13 @@ +{ + "args": [["foo", "bar", "baz"], "casks", true], + "facts": { + "brew.BrewTrusted": { + "casks": ["foo", "bar", "baz"], + "commands": [], + "formulae": [], + "taps": [], + } + }, + "commands": [], + "noop_description": "bar, baz, foo casks already trusted", +} diff --git a/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json b/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json new file mode 100644 index 000000000..470f34d0b --- /dev/null +++ b/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json @@ -0,0 +1,12 @@ +{ + "args": [["foo", "bar", "baz"], "casks", true], + "facts": { + "brew.BrewTrusted": { + "casks": [], + "commands": [], + "formulae": ["foo", "bar", "baz"], + "taps": [], + } + }, + "commands": ["brew trust --cask bar", "brew trust --cask baz", "brew trust --cask foo"], +} diff --git a/tests/operations/brew.trust/trust_needs_trust.json b/tests/operations/brew.trust/trust_needs_trust.json new file mode 100644 index 000000000..7f0fbb35a --- /dev/null +++ b/tests/operations/brew.trust/trust_needs_trust.json @@ -0,0 +1,6 @@ +{ + "args": [["foo", "bar", "baz"], "commands", true], + "facts": {"brew.BrewTrusted": {"casks": [], "commands": ["bar"], "formulae": [], "taps": []}}, + "commands": ["brew trust --command baz", "brew trust --command foo"], + "noop_description": "bar commands already trusted", +} diff --git a/tests/operations/brew.trust/untrust_already_untrusted.json b/tests/operations/brew.trust/untrust_already_untrusted.json new file mode 100644 index 000000000..0311d427c --- /dev/null +++ b/tests/operations/brew.trust/untrust_already_untrusted.json @@ -0,0 +1,6 @@ +{ + "args": [["foo", "bar", "baz"], "formulae", false], + "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, + "commands": [], + "noop_description": "bar, baz, foo formulae already untrusted", +} diff --git a/tests/operations/brew.trust/untrust_currently_trusted.json b/tests/operations/brew.trust/untrust_currently_trusted.json new file mode 100644 index 000000000..2a38fac2e --- /dev/null +++ b/tests/operations/brew.trust/untrust_currently_trusted.json @@ -0,0 +1,8 @@ +{ + "args": [["foo", "bar", "baz"], "taps", false], + "facts": { + "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": ["foo", "baz"]} + }, + "commands": ["brew untrust --tap baz", "brew untrust --tap foo"], + "noop_description": "bar taps already untrusted", +} diff --git a/tests/operations/brew.trust/zero_length_item_name.json b/tests/operations/brew.trust/zero_length_item_name.json new file mode 100644 index 000000000..53d3183b4 --- /dev/null +++ b/tests/operations/brew.trust/zero_length_item_name.json @@ -0,0 +1,9 @@ +{ + "args": ["", "tap", true], + "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, + "commands": [], + "exception": { + "names": "OperationValueError", + "message": "all items must have non-zero length names", + }, +} diff --git a/tests/operations/brew.trust/zero_length_names.json b/tests/operations/brew.trust/zero_length_names.json new file mode 100644 index 000000000..d8442a094 --- /dev/null +++ b/tests/operations/brew.trust/zero_length_names.json @@ -0,0 +1,9 @@ +{ + "args": [["a", "", "c"], "tap", true], + "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, + "commands": [], + "exception": { + "names": "OperationValueError", + "message": "all items must have non-zero length names", + }, +} From 3295548eb86ac866649c5ce0de3d28ae6f6c3921 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 16 Jun 2026 13:18:27 -0400 Subject: [PATCH 02/26] undo erroneous commit --- src/pyinfra/facts/brew.py | 106 +++--------------- src/pyinfra/operations/brew.py | 98 ++-------------- .../facts/brew.BrewTrusted/empty_output.json | 6 - tests/facts/brew.BrewTrusted/no_output.json | 6 - .../facts/brew.BrewTrusted/valid_output.json | 13 --- tests/operations/brew.tap/add_exists.json | 7 +- .../brew.tap/add_exists_and_trust.json | 10 -- tests/operations/brew.tap/add_tap.json | 7 +- .../brew.tap/add_tap_and_trust.json | 9 -- .../brew.tap/add_tap_url_and_trust.json | 12 -- .../brew.tap/add_tap_url_exists.json | 15 ++- .../brew.tap/add_tap_url_no_src.json | 9 +- .../brew.trust/trust_already_trusted.json | 13 --- ...ust_item_names_trusted_different_kind.json | 12 -- .../brew.trust/trust_needs_trust.json | 6 - .../brew.trust/untrust_already_untrusted.json | 6 - .../brew.trust/untrust_currently_trusted.json | 8 -- .../brew.trust/zero_length_item_name.json | 9 -- .../brew.trust/zero_length_names.json | 9 -- 19 files changed, 47 insertions(+), 314 deletions(-) delete mode 100644 tests/facts/brew.BrewTrusted/empty_output.json delete mode 100644 tests/facts/brew.BrewTrusted/no_output.json delete mode 100644 tests/facts/brew.BrewTrusted/valid_output.json delete mode 100644 tests/operations/brew.tap/add_exists_and_trust.json delete mode 100644 tests/operations/brew.tap/add_tap_and_trust.json delete mode 100644 tests/operations/brew.tap/add_tap_url_and_trust.json delete mode 100644 tests/operations/brew.trust/trust_already_trusted.json delete mode 100644 tests/operations/brew.trust/trust_item_names_trusted_different_kind.json delete mode 100644 tests/operations/brew.trust/trust_needs_trust.json delete mode 100644 tests/operations/brew.trust/untrust_already_untrusted.json delete mode 100644 tests/operations/brew.trust/untrust_currently_trusted.json delete mode 100644 tests/operations/brew.trust/zero_length_item_name.json delete mode 100644 tests/operations/brew.trust/zero_length_names.json diff --git a/src/pyinfra/facts/brew.py b/src/pyinfra/facts/brew.py index 28961dfcc..1a3e41e89 100644 --- a/src/pyinfra/facts/brew.py +++ b/src/pyinfra/facts/brew.py @@ -1,10 +1,6 @@ from __future__ import annotations -import json import re -from collections.abc import Iterable, Mapping, Sequence -from enum import StrEnum, unique -from typing import cast from typing_extensions import override @@ -16,15 +12,7 @@ BREW_REGEX = r"^([^\s]+)\s([0-9\._+a-z\-]+)" -@unique -class BrewTrustKind(StrEnum): - CASK = "casks" - COMMAND = "commands" - FORMULA = "formulae" - TAP = "taps" - - -def _new_cask_cli(version: Sequence[int]) -> bool: +def new_cask_cli(version): """ Returns true if brew is version 2.6.0 or later and thus has the new CLI for casks. i.e. we need to use brew list --cask instead of brew cask list @@ -37,12 +25,13 @@ def _new_cask_cli(version: Sequence[int]) -> bool: VERSION_MATCHER = re.compile(r"^Homebrew\s+(?P\d+)\.(?P\d+)\.(?P\d+).*$") -BrewVersionType = list[int] +def unknown_version(): + return [0, 0, 0] -class BrewVersion(FactBase[Sequence[int]]): +class BrewVersion(FactBase): """ - Returns the version of brew installed as a semantic versioning list: + Returns the version of brew installed as a semantic versioning tuple: .. code:: python @@ -60,23 +49,20 @@ def requires_command(self) -> str: @override @staticmethod - def default() -> BrewVersionType: + def default(): return [0, 0, 0] @override - def process(self, output: Iterable[str]) -> BrewVersionType: - if ((out := next(iter(output), None)) is not None) and ( - (m := VERSION_MATCHER.match(out)) is not None - ): + def process(self, output): + out = list(output)[0] + m = VERSION_MATCHER.match(out) + if m is not None: return [int(m.group(key)) for key in ["major", "minor", "patch"]] - logger.warning(f"could not parse version string from brew: '{out}'") + logger.warning("could not parse version string from brew: %s", out) return self.default() -BrewPackingMapping = dict[str, set[str]] - - -class BrewPackages(FactBase[BrewPackingMapping]): +class BrewPackages(FactBase): """ Returns a dict of installed brew packages: @@ -98,7 +84,7 @@ def requires_command(self) -> str: default = dict @override - def process(self, output: Iterable[str]) -> BrewPackingMapping: + def process(self, output): return parse_packages(BREW_REGEX, output) @@ -125,21 +111,9 @@ def requires_command(self) -> str: return "brew" -BrewTapList = Iterable[str] - - -class BrewTaps(FactBase[BrewTapList]): +class BrewTaps(FactBase): """ Returns a list of brew taps. - - .. code:: python - { - "@local": [ - "homebrew/cask", - "homebrew/core", - "homebrew/services", - ] - } """ @override @@ -153,55 +127,5 @@ def requires_command(self) -> str: default = list @override - def process(self, output: Iterable[str]) -> BrewTapList: + def process(self, output): return output - - -BrewTrustMapping = Mapping[str, Sequence[str]] - - -class BrewTrusted(FactBase[BrewTrustMapping]): - """ - Returns a dict with lists of the casks, commands, formulae and taps that have - been marked as trusted - - .. code:: python - { - "@local": { - "taps": [ - "borgbackup/tap" - ], - "formulae": [], - "casks": [], - "commands": [] - } - } - """ - - @override - def command(self) -> str: - return "brew trust --json v1" - - @override - def requires_command(self) -> str: - return "brew" - - @override - @staticmethod - def default() -> BrewTrustMapping: - return {kind: [] for kind in BrewTrustKind.__members__.values()} - - @override - def process(self, output: Iterable[str]) -> BrewTrustMapping: - error = False - body = "\n".join(s for s in output) - try: - result = cast("BrewTrustMapping", json.loads(body)) - except (json.JSONDecodeError, TypeError, RecursionError): - error = True - - if error or not all(kind in result for kind in BrewTrustKind.__members__.values()): - logger.warning(f"unexpected output from brew trust: '{body}'") - result = self.default() - - return result diff --git a/src/pyinfra/operations/brew.py b/src/pyinfra/operations/brew.py index 6d59d9c93..329deb340 100644 --- a/src/pyinfra/operations/brew.py +++ b/src/pyinfra/operations/brew.py @@ -8,17 +8,7 @@ from pyinfra import host from pyinfra.api import operation -from pyinfra.api.command import QuoteString, StringCommand -from pyinfra.api.exceptions import OperationValueError -from pyinfra.facts.brew import ( - BrewCasks, - BrewPackages, - BrewTaps, - BrewTrusted, - BrewTrustKind, - BrewVersion, - _new_cask_cli, -) +from pyinfra.facts.brew import BrewCasks, BrewPackages, BrewTaps, BrewVersion, new_cask_cli from .util.packaging import ensure_packages @@ -107,7 +97,7 @@ def packages( def cask_args(): - return ("", " --cask") if _new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "") + return ("", " --cask") if new_cask_cli(host.get_fact(BrewVersion)) else ("cask ", "") @operation(is_idempotent=False) @@ -169,18 +159,12 @@ def casks( @operation() -def tap( - src: str | None = None, - present: bool = True, - trusted: bool | None = None, - url: str | None = None, -): +def tap(src: str | None = None, present=True, url: str | None = None): """ Add/remove brew taps. + src: the name of the tap - + present: whether this tap should be present or not. Default True. - + trusted: whether or not this tap should be trusted. Default False. + + present: whether this tap should be present or not + url: the url of the tap. See https://docs.brew.sh/Taps **Examples:** @@ -190,14 +174,12 @@ def tap( brew.tap( name="Add a brew tap", src="includeos/includeos", - trusted=True, ) # Just url is equivalent to # `brew tap kptdev/kpt https://github.com/kptdev/kpt` brew.tap( url="https://github.com/kptdev/kpt", - trusted=True, ) # src and url is equivalent to @@ -205,7 +187,6 @@ def tap( brew.tap( src="example/project", url="https://github.example.com/project", - trusted=True, ) # Multiple taps @@ -213,16 +194,10 @@ def tap( brew.tap( name={f"Add brew tap {tap}"}, src=tap, - trusted=True, ) """ - def mk_trust_cmd(tap: str, *, trust: bool | None = None) -> StringCommand: - return StringCommand("brew", "trust" if trust else "untrust", "--tap", QuoteString(tap)) - - trusted = trusted or False - if not (src or url): host.noop("no tap was specified") return @@ -238,75 +213,20 @@ def mk_trust_cmd(tap: str, *, trust: bool | None = None) -> StringCommand: if present and already_tapped: host.noop(f"tap {src} already exists") - trusted_taps = host.get_fact(BrewTrusted).get("taps", []) - if (trusted and (src not in trusted_taps)) or ((not trusted) and (src in trusted_taps)): - yield mk_trust_cmd(src, trust=trusted) return if already_tapped: - yield StringCommand("brew", "untap", QuoteString(src)) + yield f"brew untap {src}" return if not present: host.noop(f"tap {src} does not exist") return - args = [QuoteString(src)] - if url is not None: - args.append(QuoteString(url)) - - yield StringCommand("brew", "tap", *args) + cmd = f"brew tap {src}" - if trusted: # if not already present, can't be trusted so no check of BrewTrusted - yield mk_trust_cmd(src, trust=True) + if url is not None: + cmd = " ".join([cmd, url]) + yield cmd return - - -TRUST_SRC_AND_OPTION = { - BrewTrustKind.CASK.value: "--cask", - BrewTrustKind.COMMAND.value: "--command", - BrewTrustKind.FORMULA.value: "--formula", - BrewTrustKind.TAP.value: "--tap", -} - - -@operation() -def trust(items: str | list[str], kind: BrewTrustKind, trusted: bool): - """ - Trust/untrust brew casks, commands, formulae and/or taps (see https://docs.brew.sh/Tap-Trust) - - + item: the cask, command, formula or tap to be trusted or untrusted - + kind: whether the item is a CASK, COMMAND, FORMULA or TAP (using BrewTrustKind enum) - + trusted: whether this item should be trusted or not. no default, must be specified - - **Examples:** - - .. code:: python - - brew.trust( - name="Mark magic tap as trusted", - item="includeos/includeos", - kind=BrewTrustKind.TAP, - trust=True - ) - """ - item_set = set(items if isinstance(items, list) else [items]) - if any(len(item) < 1 for item in item_set): - raise OperationValueError("all items must have non-zero length names") - # TODO: remove this once the test infrastructure supports enums - if isinstance(kind, str): - try: - kind = BrewTrustKind(kind) - except (TypeError, ValueError): - raise OperationValueError from None - desired_state = "trust" if trusted else "untrust" - trusted_items = set(host.get_fact(BrewTrusted).get(kind.value, [])) - found = item_set & trusted_items - need_to_change = (item_set - found) if trusted else found - already_ok = item_set - need_to_change - - for item in sorted(need_to_change): - yield StringCommand("brew", desired_state, TRUST_SRC_AND_OPTION[kind], QuoteString(item)) - if len(already_ok) > 0: - host.noop(f"{', '.join(sorted(already_ok))} {kind.value} already {desired_state}ed") diff --git a/tests/facts/brew.BrewTrusted/empty_output.json b/tests/facts/brew.BrewTrusted/empty_output.json deleted file mode 100644 index 3ea65074e..000000000 --- a/tests/facts/brew.BrewTrusted/empty_output.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "command": "brew trust --json v1", - "requires_command": "brew", - "output": ["{}"], - "fact": {"casks": [], "commands": [], "formulae": [], "taps": []}, -} diff --git a/tests/facts/brew.BrewTrusted/no_output.json b/tests/facts/brew.BrewTrusted/no_output.json deleted file mode 100644 index 77ff1607d..000000000 --- a/tests/facts/brew.BrewTrusted/no_output.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "command": "brew trust --json v1", - "requires_command": "brew", - "output": [], - "fact": {"casks": [], "commands": [], "formulae": [], "taps": []}, -} diff --git a/tests/facts/brew.BrewTrusted/valid_output.json b/tests/facts/brew.BrewTrusted/valid_output.json deleted file mode 100644 index f45c58f14..000000000 --- a/tests/facts/brew.BrewTrusted/valid_output.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "command": "brew trust --json v1", - "requires_command": "brew", - "output": [ - "{", - '"casks":["a"],', - '"taps":["d"],', - '"formulae":["c"],', - '"commands":["b"]', - "}", - ], - "fact": {"casks": ["a"], "commands": ["b"], "formulae": ["c"], "taps": ["d"]}, -} diff --git a/tests/operations/brew.tap/add_exists.json b/tests/operations/brew.tap/add_exists.json index 6bf8434da..faea93800 100644 --- a/tests/operations/brew.tap/add_exists.json +++ b/tests/operations/brew.tap/add_exists.json @@ -1,9 +1,10 @@ { "args": ["homebrew/cask"], "facts": { - "brew.BrewTaps": ["homebrew/cask"], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + "brew.BrewTaps": [ + "homebrew/cask" + ] }, "commands": [], - "noop_description": "tap homebrew/cask already exists", + "noop_description": "tap homebrew/cask already exists" } diff --git a/tests/operations/brew.tap/add_exists_and_trust.json b/tests/operations/brew.tap/add_exists_and_trust.json deleted file mode 100644 index 708a9969a..000000000 --- a/tests/operations/brew.tap/add_exists_and_trust.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "args": ["homebrew/cask"], - "kwargs": {"trusted": true}, - "facts": { - "brew.BrewTaps": ["homebrew/cask"], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, - }, - "commands": ["brew trust --tap homebrew/cask"], - "noop_description": "tap homebrew/cask already exists", -} diff --git a/tests/operations/brew.tap/add_tap.json b/tests/operations/brew.tap/add_tap.json index 07bc619e9..9ccfdd182 100644 --- a/tests/operations/brew.tap/add_tap.json +++ b/tests/operations/brew.tap/add_tap.json @@ -1,8 +1,9 @@ { "args": ["homebrew/cask"], "facts": { - "brew.BrewTaps": [], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + "brew.BrewTaps": [] }, - "commands": ["brew tap homebrew/cask"], + "commands": [ + "brew tap homebrew/cask" + ] } diff --git a/tests/operations/brew.tap/add_tap_and_trust.json b/tests/operations/brew.tap/add_tap_and_trust.json deleted file mode 100644 index cb5173f84..000000000 --- a/tests/operations/brew.tap/add_tap_and_trust.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": ["homebrew/cask"], - "kwargs": {"trusted": true}, - "facts": { - "brew.BrewTaps": [], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, - }, - "commands": ["brew tap homebrew/cask", "brew trust --tap homebrew/cask"], -} diff --git a/tests/operations/brew.tap/add_tap_url_and_trust.json b/tests/operations/brew.tap/add_tap_url_and_trust.json deleted file mode 100644 index 090b15a12..000000000 --- a/tests/operations/brew.tap/add_tap_url_and_trust.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args": ["homebrew/cask"], - "kwargs": {"trusted": true, "url": "https://github.com/homebrew/cask"}, - "facts": { - "brew.BrewTaps": [], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, - }, - "commands": [ - "brew tap homebrew/cask https://github.com/homebrew/cask", - "brew trust --tap homebrew/cask", - ], -} diff --git a/tests/operations/brew.tap/add_tap_url_exists.json b/tests/operations/brew.tap/add_tap_url_exists.json index 6adaca23c..0ad83370e 100644 --- a/tests/operations/brew.tap/add_tap_url_exists.json +++ b/tests/operations/brew.tap/add_tap_url_exists.json @@ -1,10 +1,15 @@ { - "args": ["homebrew/cask"], - "kwargs": {"url": "https://github.com/homebrew/cask"}, + "args": [ + "homebrew/cask" + ], + "kwargs": { + "url": "https://github.com/homebrew/cask" + }, "facts": { - "brew.BrewTaps": ["homebrew/cask"], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + "brew.BrewTaps": [ + "homebrew/cask" + ] }, "commands": [], - "noop_description": "tap homebrew/cask already exists", + "noop_description": "tap homebrew/cask already exists" } diff --git a/tests/operations/brew.tap/add_tap_url_no_src.json b/tests/operations/brew.tap/add_tap_url_no_src.json index d5bf30c5c..241256cf9 100644 --- a/tests/operations/brew.tap/add_tap_url_no_src.json +++ b/tests/operations/brew.tap/add_tap_url_no_src.json @@ -1,9 +1,10 @@ { "args": [], - "kwargs": {"url": "https://github.com/homebrew/cask"}, + "kwargs": { + "url": "https://github.com/homebrew/cask" + }, "facts": { - "brew.BrewTaps": [], - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}, + "brew.BrewTaps": [] }, - "commands": ["brew tap homebrew/cask https://github.com/homebrew/cask"], + "commands": ["brew tap homebrew/cask https://github.com/homebrew/cask"] } diff --git a/tests/operations/brew.trust/trust_already_trusted.json b/tests/operations/brew.trust/trust_already_trusted.json deleted file mode 100644 index c6dc73b98..000000000 --- a/tests/operations/brew.trust/trust_already_trusted.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "args": [["foo", "bar", "baz"], "casks", true], - "facts": { - "brew.BrewTrusted": { - "casks": ["foo", "bar", "baz"], - "commands": [], - "formulae": [], - "taps": [], - } - }, - "commands": [], - "noop_description": "bar, baz, foo casks already trusted", -} diff --git a/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json b/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json deleted file mode 100644 index 470f34d0b..000000000 --- a/tests/operations/brew.trust/trust_item_names_trusted_different_kind.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args": [["foo", "bar", "baz"], "casks", true], - "facts": { - "brew.BrewTrusted": { - "casks": [], - "commands": [], - "formulae": ["foo", "bar", "baz"], - "taps": [], - } - }, - "commands": ["brew trust --cask bar", "brew trust --cask baz", "brew trust --cask foo"], -} diff --git a/tests/operations/brew.trust/trust_needs_trust.json b/tests/operations/brew.trust/trust_needs_trust.json deleted file mode 100644 index 7f0fbb35a..000000000 --- a/tests/operations/brew.trust/trust_needs_trust.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": [["foo", "bar", "baz"], "commands", true], - "facts": {"brew.BrewTrusted": {"casks": [], "commands": ["bar"], "formulae": [], "taps": []}}, - "commands": ["brew trust --command baz", "brew trust --command foo"], - "noop_description": "bar commands already trusted", -} diff --git a/tests/operations/brew.trust/untrust_already_untrusted.json b/tests/operations/brew.trust/untrust_already_untrusted.json deleted file mode 100644 index 0311d427c..000000000 --- a/tests/operations/brew.trust/untrust_already_untrusted.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": [["foo", "bar", "baz"], "formulae", false], - "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, - "commands": [], - "noop_description": "bar, baz, foo formulae already untrusted", -} diff --git a/tests/operations/brew.trust/untrust_currently_trusted.json b/tests/operations/brew.trust/untrust_currently_trusted.json deleted file mode 100644 index 2a38fac2e..000000000 --- a/tests/operations/brew.trust/untrust_currently_trusted.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "args": [["foo", "bar", "baz"], "taps", false], - "facts": { - "brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": ["foo", "baz"]} - }, - "commands": ["brew untrust --tap baz", "brew untrust --tap foo"], - "noop_description": "bar taps already untrusted", -} diff --git a/tests/operations/brew.trust/zero_length_item_name.json b/tests/operations/brew.trust/zero_length_item_name.json deleted file mode 100644 index 53d3183b4..000000000 --- a/tests/operations/brew.trust/zero_length_item_name.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": ["", "tap", true], - "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, - "commands": [], - "exception": { - "names": "OperationValueError", - "message": "all items must have non-zero length names", - }, -} diff --git a/tests/operations/brew.trust/zero_length_names.json b/tests/operations/brew.trust/zero_length_names.json deleted file mode 100644 index d8442a094..000000000 --- a/tests/operations/brew.trust/zero_length_names.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": [["a", "", "c"], "tap", true], - "facts": {"brew.BrewTrusted": {"casks": [], "commands": [], "formulae": [], "taps": []}}, - "commands": [], - "exception": { - "names": "OperationValueError", - "message": "all items must have non-zero length names", - }, -} From 399436e31136a5cc0e89fa2accbd89c838fbbd16 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 18:22:06 -0400 Subject: [PATCH 03/26] test: support packages of facts - make same change as was done for same reason for operations --- tests/test_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_facts.py b/tests/test_facts.py index 31b986b8a..bb267cd95 100644 --- a/tests/test_facts.py +++ b/tests/test_facts.py @@ -31,7 +31,7 @@ def _make_command(command_attribute, args): def make_fact_tests(folder_name): - module_name, fact_name = folder_name.split(".") + module_name, fact_name = folder_name.rsplit(".", maxsplit=1) module = import_module(f"pyinfra.facts.{module_name}") fact = getattr(module, fact_name)() From a6b061fe2803eadee58218d60c67a12fc3d72f91 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 18:47:42 -0400 Subject: [PATCH 04/26] feat(facts.openwrt, operations.openwrt) - create openwrt package for OpenWrt specific modules - simplifies top-level name space when packages, procd and uci arrive - move opkg under openwrt (will be backfilled with deprecated version that can be removed) --- src/pyinfra/facts/openwrt/__init__.py | 20 ++++++++++ src/pyinfra/facts/{ => openwrt}/opkg.py | 10 ++--- src/pyinfra/operations/openwrt/__init__.py | 20 ++++++++++ src/pyinfra/operations/{ => openwrt}/opkg.py | 10 ++--- .../opkg_conf.json | 19 +++------ .../opkg_feeds.json | 18 ++++----- .../opkg_installable_architectures.json | 9 +---- .../opkg_packages.json | 24 +++++++++++ .../opkg_upgradeable_packages.json | 20 ++++++++++ .../opkg.OpkgPackages/opkg_packages.json | 40 ------------------- .../opkg_upgradeable_packages.json | 35 ---------------- .../add_existing_package.json | 7 ++++ .../add_multiple_packages.json | 6 +++ .../add_one_package.json | 6 +++ .../add_with_unallowed_pinning.json | 12 +++--- .../list_of_nulls_package_list.json | 8 ++-- .../null_package_list.json | 8 ++-- .../remove_existing_package.json | 6 +++ ...e_existing_package_and_require_update.json | 6 +++ .../remove_not_existing_package.json | 7 ++++ .../update_then_add_one.json | 6 +++ .../openwrt.opkg.update/first_update.json | 1 + .../opkg.packages/add_existing_package.json | 12 ------ .../opkg.packages/add_multiple_packages.json | 12 ------ .../opkg.packages/add_one_package.json | 12 ------ .../remove_existing_package.json | 13 ------ ...e_existing_package_and_require_update.json | 14 ------- .../remove_not_existing_package.json | 13 ------ .../opkg.packages/update_then_add_one.json | 11 ----- .../operations/opkg.update/first_update.json | 9 ----- 30 files changed, 167 insertions(+), 227 deletions(-) create mode 100644 src/pyinfra/facts/openwrt/__init__.py rename src/pyinfra/facts/{ => openwrt}/opkg.py (95%) create mode 100644 src/pyinfra/operations/openwrt/__init__.py rename src/pyinfra/operations/{ => openwrt}/opkg.py (89%) rename tests/facts/{opkg.OpkgConf => openwrt.opkg.OpkgConf}/opkg_conf.json (81%) rename tests/facts/{opkg.OpkgFeeds => openwrt.opkg.OpkgFeeds}/opkg_feeds.json (90%) rename tests/facts/{opkg.OpkgInstallableArchitectures => openwrt.opkg.OpkgInstallableArchitectures}/opkg_installable_architectures.json (64%) create mode 100644 tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json create mode 100644 tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json delete mode 100644 tests/facts/opkg.OpkgPackages/opkg_packages.json delete mode 100644 tests/facts/opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json create mode 100644 tests/operations/openwrt.opkg.packages/add_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/add_multiple_packages.json create mode 100644 tests/operations/openwrt.opkg.packages/add_one_package.json rename tests/operations/{opkg.packages => openwrt.opkg.packages}/add_with_unallowed_pinning.json (51%) rename tests/operations/{opkg.packages => openwrt.opkg.packages}/list_of_nulls_package_list.json (55%) rename tests/operations/{opkg.packages => openwrt.opkg.packages}/null_package_list.json (53%) create mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json create mode 100644 tests/operations/openwrt.opkg.packages/remove_not_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/update_then_add_one.json create mode 100644 tests/operations/openwrt.opkg.update/first_update.json delete mode 100644 tests/operations/opkg.packages/add_existing_package.json delete mode 100644 tests/operations/opkg.packages/add_multiple_packages.json delete mode 100644 tests/operations/opkg.packages/add_one_package.json delete mode 100644 tests/operations/opkg.packages/remove_existing_package.json delete mode 100644 tests/operations/opkg.packages/remove_existing_package_and_require_update.json delete mode 100644 tests/operations/opkg.packages/remove_not_existing_package.json delete mode 100644 tests/operations/opkg.packages/update_then_add_one.json delete mode 100644 tests/operations/opkg.update/first_update.json diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py new file mode 100644 index 000000000..81c3e254e --- /dev/null +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -0,0 +1,20 @@ +import importlib + +ALL = { + "opkg": "opkg", +} + +__all__ = list(ALL.keys()) + + +def __getattr__(name): + # On-demand import of OpenWrt facts, so we don't have to import them all at once + # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL + # Also, Pyinfra is py3.11>=, so this is not a breaking change. + if name in __all__: + pieces = ALL[name].split(".") + module = importlib.import_module(f".{pieces[0]}", package=__name__) + if len(pieces) > 1: + return getattr(module, pieces[1]) + return module + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/pyinfra/facts/opkg.py b/src/pyinfra/facts/openwrt/opkg.py similarity index 95% rename from src/pyinfra/facts/opkg.py rename to src/pyinfra/facts/openwrt/opkg.py index d4b71174d..8469cdcb5 100644 --- a/src/pyinfra/facts/opkg.py +++ b/src/pyinfra/facts/openwrt/opkg.py @@ -21,7 +21,7 @@ from pyinfra.api import FactBase from pyinfra.facts.util.packaging import parse_packages -# TODO - change NamedTuple to dataclass Opkgbut need to figure out how to get json serialization +# TODO - change NamedTuple to dataclass Opkg but need to figure out how to get json serialization # to work without changing core code @@ -83,7 +83,7 @@ def requires_command(self) -> str: (?:\s*\#.*)? $ """, - re.X, + re.VERBOSE, ) @override @@ -125,9 +125,9 @@ class OpkgFeeds(FactBase): { 'openwrt_base': FeedInfo(url='http://downloads ... /i386_pentium/base', fmt='src/gz', kind='distribution'), # noqa: E501 'openwrt_core': FeedInfo(url='http://downloads ... /x86/geode/packages', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_luci': FeedInfo(url='http://downloads ... /i386_pentium/luci', fmt='src/gz', kind='distribution'),# noqa: E501 - 'openwrt_packages': FeedInfo(url='http://downloads ... /i386_pentium/packages', fmt='src/gz', kind='distribution'),# noqa: E501 - 'openwrt_routing': FeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'),# noqa: E501 + 'openwrt_luci': FeedInfo(url='http://downloads ... /i386_pentium/luci', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_packages': FeedInfo(url='http://downloads ... /i386_pentium/packages', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_routing': FeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'), # noqa: E501 'openwrt_telephony': FeedInfo(url='http://downloads ... /i386_pentium/telephony', fmt='src/gz', kind='distribution') # noqa: E501 } """ diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py new file mode 100644 index 000000000..81c3e254e --- /dev/null +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -0,0 +1,20 @@ +import importlib + +ALL = { + "opkg": "opkg", +} + +__all__ = list(ALL.keys()) + + +def __getattr__(name): + # On-demand import of OpenWrt facts, so we don't have to import them all at once + # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL + # Also, Pyinfra is py3.11>=, so this is not a breaking change. + if name in __all__: + pieces = ALL[name].split(".") + module = importlib.import_module(f".{pieces[0]}", package=__name__) + if len(pieces) > 1: + return getattr(module, pieces[1]) + return module + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/pyinfra/operations/opkg.py b/src/pyinfra/operations/openwrt/opkg.py similarity index 89% rename from src/pyinfra/operations/opkg.py rename to src/pyinfra/operations/openwrt/opkg.py index f976140dc..75346f66c 100644 --- a/src/pyinfra/operations/opkg.py +++ b/src/pyinfra/operations/openwrt/opkg.py @@ -14,7 +14,7 @@ from pyinfra import host from pyinfra.api import StringCommand, operation -from pyinfra.facts.opkg import OpkgPackages +from pyinfra.facts.openwrt.opkg import OpkgPackages from pyinfra.operations.util.packaging import ensure_packages EQUALS = "=" @@ -57,10 +57,10 @@ def packages( from pyinfra.operations import opkg # Ensure packages are installed (will not force package upgrade) - opkg.packages(['asterisk', 'vim'], name="Install Asterisk and Vim") + openwrt.opkg.packages(['asterisk', 'vim'], name="Install Asterisk and Vim") # Install the latest versions of packages (always check) - opkg.packages( + openwrt.opkg.packages( 'vim', latest=True, name="Ensure we have the latest version of Vim" @@ -69,7 +69,7 @@ def packages( if str(packages) == "" or ( isinstance(packages, list) and (len(packages) < 1 or all(len(p) < 1 for p in packages)) ): - host.noop("empty or invalid package list provided to opkg.packages") + host.noop("empty or invalid package list provided to openwrt.opkg.packages") return pkg_list = packages if isinstance(packages, list) else [packages] @@ -78,7 +78,7 @@ def packages( raise ValueError(f"opkg does not support version pinning but found for: '{have_equals}'") if update: - yield from _update._inner() + yield from _update._inner() # noqa: SLF001 yield from ensure_packages( host, diff --git a/tests/facts/opkg.OpkgConf/opkg_conf.json b/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json similarity index 81% rename from tests/facts/opkg.OpkgConf/opkg_conf.json rename to tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json index b7d05b885..b6fc4db2e 100644 --- a/tests/facts/opkg.OpkgConf/opkg_conf.json +++ b/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json @@ -20,24 +20,17 @@ "arch noarch 2", "arch brcm4716 200", "arch brcm47xx 300 # generic has lower priority than specific", - "arch zzz" + "arch zzz", ], "fact": [ - { - "root": "/", - "ram": "/tmp" - }, + {"root": "/", "ram": "/tmp"}, "/var/opkg-lists", { "overlay_root": "/overlay", "check_signature": true, "http_proxy": "http://username:password@proxy.example.org:8080/", - "ftp_proxy": "http://username:password@proxy.example.org:2121/" }, - { - "all": 1, - "noarch": 2, - "brcm4716": 200, - "brcm47xx": 300 - } - ] + "ftp_proxy": "http://username:password@proxy.example.org:2121/", + }, + {"all": 1, "noarch": 2, "brcm4716": 200, "brcm47xx": 300}, + ], } diff --git a/tests/facts/opkg.OpkgFeeds/opkg_feeds.json b/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json similarity index 90% rename from tests/facts/opkg.OpkgFeeds/opkg_feeds.json rename to tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json index 983dc9df8..b85402128 100644 --- a/tests/facts/opkg.OpkgFeeds/opkg_feeds.json +++ b/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json @@ -15,38 +15,38 @@ "CUSTOM", "# add your custom package feeds here", "#", - "# src/gz example_feed_name http://www.example.com/path/to/files" + "# src/gz example_feed_name http://www.example.com/path/to/files", ], "fact": { "openwrt_core": [ "http://downloads.openwrt.org/releases/19.07.2/targets/x86/geode/packages", "src/gz", - "distribution" + "distribution", ], "openwrt_base": [ "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/base", "src/gz", - "distribution" + "distribution", ], "openwrt_luci": [ "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/luci", "src/gz", - "distribution" + "distribution", ], "openwrt_packages": [ "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/packages", "src/gz", - "distribution" + "distribution", ], "openwrt_routing": [ "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/routing", "src/gz", - "distribution" + "distribution", ], "openwrt_telephony": [ "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/telephony", "src/gz", - "distribution" - ] - } + "distribution", + ], + }, } diff --git a/tests/facts/opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json b/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json similarity index 64% rename from tests/facts/opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json rename to tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json index 953a7e723..7b6211f62 100644 --- a/tests/facts/opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json +++ b/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json @@ -10,12 +10,7 @@ "arch", "arch thisarch", "arch xray zulu", - "arch i386_pentium 10 # some sort of comment" + "arch i386_pentium 10 # some sort of comment", ], - "fact": - { - "all": 1, - "noarch": 1, - "i386_pentium": 10 - } + "fact": {"all": 1, "noarch": 1, "i386_pentium": 10}, } diff --git a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json new file mode 100644 index 000000000..024d34766 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json @@ -0,0 +1,24 @@ +{ + "command": "opkg list-installed", + "requires_command": "opkg", + "output": [ + "urandom-seed - 1.0-1", + "urngd - 2020-01-21-c7f7b6b6-1", + "usign - 2019-08-06-5a52b379-1", + "wget - 1.20.3-4", + "wget-nossl - 1.20.3-4", + "wireless-regdb - 2019.06.03", + "wpad-basic - 2019-08-08-ca8c2bd2-2", + "zlib - 1.2.11-3", + ], + "fact": { + "urandom-seed": ["1.0-1"], + "urngd": ["2020-01-21-c7f7b6b6-1"], + "usign": ["2019-08-06-5a52b379-1"], + "wget": ["1.20.3-4"], + "wget-nossl": ["1.20.3-4"], + "wireless-regdb": ["2019.06.03"], + "wpad-basic": ["2019-08-08-ca8c2bd2-2"], + "zlib": ["1.2.11-3"], + }, +} diff --git a/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json b/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json new file mode 100644 index 000000000..f4bd7dfe2 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json @@ -0,0 +1,20 @@ +{ + "command": "opkg list-upgradable", + "requires_command": "opkg", + "output": [ + "", + "rpcd-mod-iwinfo - 2019-12-10-aaa08366-2 - 2020-05-26-67c8a3fd-1", + "luci-mod-network - git-20.115.52331-39a8290-1 - git-20.319.48994-50b7ab5-1", + "hostapd-common - 2019-08-08-ca8c2bd2-2 - 2019-08-08-ca8c2bd2-4", + "libuv1 - 1.34.2-1 - 1.40.0-1", + "xray123-123", + "wireless-regdb - 2019.06.03 - 2019.06.03-1", + ], + "fact": { + "rpcd-mod-iwinfo": ["2019-12-10-aaa08366-2", "2020-05-26-67c8a3fd-1"], + "luci-mod-network": ["git-20.115.52331-39a8290-1", "git-20.319.48994-50b7ab5-1"], + "hostapd-common": ["2019-08-08-ca8c2bd2-2", "2019-08-08-ca8c2bd2-4"], + "libuv1": ["1.34.2-1", "1.40.0-1"], + "wireless-regdb": ["2019.06.03", "2019.06.03-1"], + }, +} diff --git a/tests/facts/opkg.OpkgPackages/opkg_packages.json b/tests/facts/opkg.OpkgPackages/opkg_packages.json deleted file mode 100644 index 4d04d3f18..000000000 --- a/tests/facts/opkg.OpkgPackages/opkg_packages.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "command": "opkg list-installed", - "requires_command": "opkg", - "output": [ - "urandom-seed - 1.0-1", - "urngd - 2020-01-21-c7f7b6b6-1", - "usign - 2019-08-06-5a52b379-1", - "wget - 1.20.3-4", - "wget-nossl - 1.20.3-4", - "wireless-regdb - 2019.06.03", - "wpad-basic - 2019-08-08-ca8c2bd2-2", - "zlib - 1.2.11-3" - ], - "fact": { - "urandom-seed": [ - "1.0-1" - ], - "urngd": [ - "2020-01-21-c7f7b6b6-1" - ], - "usign": [ - "2019-08-06-5a52b379-1" - ], - "wget": [ - "1.20.3-4" - ], - "wget-nossl": [ - "1.20.3-4" - ], - "wireless-regdb": [ - "2019.06.03" - ], - "wpad-basic": [ - "2019-08-08-ca8c2bd2-2" - ], - "zlib": [ - "1.2.11-3" - ] - } -} diff --git a/tests/facts/opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json b/tests/facts/opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json deleted file mode 100644 index 094358e9f..000000000 --- a/tests/facts/opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "command": "opkg list-upgradable", - "requires_command": "opkg", - "output": [ - "", - "rpcd-mod-iwinfo - 2019-12-10-aaa08366-2 - 2020-05-26-67c8a3fd-1", - "luci-mod-network - git-20.115.52331-39a8290-1 - git-20.319.48994-50b7ab5-1", - "hostapd-common - 2019-08-08-ca8c2bd2-2 - 2019-08-08-ca8c2bd2-4", - "libuv1 - 1.34.2-1 - 1.40.0-1", - "xray123-123", - "wireless-regdb - 2019.06.03 - 2019.06.03-1" - ], - "fact": { - "rpcd-mod-iwinfo": [ - "2019-12-10-aaa08366-2", - "2020-05-26-67c8a3fd-1" - ], - "luci-mod-network": [ - "git-20.115.52331-39a8290-1", - "git-20.319.48994-50b7ab5-1" - ], - "hostapd-common": [ - "2019-08-08-ca8c2bd2-2", - "2019-08-08-ca8c2bd2-4" - ], - "libuv1": [ - "1.34.2-1", - "1.40.0-1" - ], - "wireless-regdb": [ - "2019.06.03", - "2019.06.03-1" - ] - } -} diff --git a/tests/operations/openwrt.opkg.packages/add_existing_package.json b/tests/operations/openwrt.opkg.packages/add_existing_package.json new file mode 100644 index 000000000..e806d995f --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_existing_package.json @@ -0,0 +1,7 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": [], + "noop_description": "package curl is installed (7.66.0-2)", +} diff --git a/tests/operations/openwrt.opkg.packages/add_multiple_packages.json b/tests/operations/openwrt.opkg.packages/add_multiple_packages.json new file mode 100644 index 000000000..a5517216b --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_multiple_packages.json @@ -0,0 +1,6 @@ +{ + "args": [["curl", "wget"]], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg install curl wget"], +} diff --git a/tests/operations/openwrt.opkg.packages/add_one_package.json b/tests/operations/openwrt.opkg.packages/add_one_package.json new file mode 100644 index 000000000..23187734f --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_one_package.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg install curl"], +} diff --git a/tests/operations/opkg.packages/add_with_unallowed_pinning.json b/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json similarity index 51% rename from tests/operations/opkg.packages/add_with_unallowed_pinning.json rename to tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json index ae6c98db0..69defc1d4 100644 --- a/tests/operations/opkg.packages/add_with_unallowed_pinning.json +++ b/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json @@ -1,11 +1,9 @@ { "args": ["curl=7.66.0-2"], - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ ], + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], "exception": { - "name":"ValueError", - "message": "opkg does not support version pinning but found for: 'curl'" - } + "name": "ValueError", + "message": "opkg does not support version pinning but found for: 'curl'", + }, } diff --git a/tests/operations/opkg.packages/list_of_nulls_package_list.json b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json similarity index 55% rename from tests/operations/opkg.packages/list_of_nulls_package_list.json rename to tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json index 70ecfdd57..28aa77002 100644 --- a/tests/operations/opkg.packages/list_of_nulls_package_list.json +++ b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json @@ -1,9 +1,7 @@ { "args": ["", "", ""], - "facts": { - "opkg.OpkgPackages": {} - }, + "facts": {"opkg.OpkgPackages": {}}, "commands": [], - "noop_description": "empty or invalid package list provided to opkg.packages", - "logs": "foo" + "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", + "logs": "foo", } diff --git a/tests/operations/opkg.packages/null_package_list.json b/tests/operations/openwrt.opkg.packages/null_package_list.json similarity index 53% rename from tests/operations/opkg.packages/null_package_list.json rename to tests/operations/openwrt.opkg.packages/null_package_list.json index 1d8ee123d..fa0ce88ab 100644 --- a/tests/operations/opkg.packages/null_package_list.json +++ b/tests/operations/openwrt.opkg.packages/null_package_list.json @@ -1,9 +1,7 @@ { "args": [], - "facts": { - "opkg.OpkgPackages": {} - }, + "facts": {"opkg.OpkgPackages": {}}, "commands": [], - "noop_description": "empty or invalid package list provided to opkg.packages", - "logs": "foo" + "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", + "logs": "foo", } diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package.json b/tests/operations/openwrt.opkg.packages/remove_existing_package.json new file mode 100644 index 000000000..6cc6370be --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_existing_package.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": false}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": ["opkg remove curl"], +} diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json b/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json new file mode 100644 index 000000000..193223a0d --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": true}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": ["opkg update", "opkg remove curl"], +} diff --git a/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json b/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json new file mode 100644 index 000000000..cef56213a --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json @@ -0,0 +1,7 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], + "noop_description": "package curl is not installed", +} diff --git a/tests/operations/openwrt.opkg.packages/update_then_add_one.json b/tests/operations/openwrt.opkg.packages/update_then_add_one.json new file mode 100644 index 000000000..9c8e03db6 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/update_then_add_one.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"update": true}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg update", "opkg install curl"], +} diff --git a/tests/operations/openwrt.opkg.update/first_update.json b/tests/operations/openwrt.opkg.update/first_update.json new file mode 100644 index 000000000..4d8479faa --- /dev/null +++ b/tests/operations/openwrt.opkg.update/first_update.json @@ -0,0 +1 @@ +{"args": [], "facts": {"opkg.OpkgPackages": {}}, "commands": ["opkg update"]} diff --git a/tests/operations/opkg.packages/add_existing_package.json b/tests/operations/opkg.packages/add_existing_package.json deleted file mode 100644 index 6d8c67360..000000000 --- a/tests/operations/opkg.packages/add_existing_package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args": ["curl"], - "kwargs": { - "update": false - }, - "facts": { - "opkg.OpkgPackages": {"curl": ["7.66.0-2"]} - }, - "commands": [ - ], - "noop_description": "package curl is installed (7.66.0-2)" -} diff --git a/tests/operations/opkg.packages/add_multiple_packages.json b/tests/operations/opkg.packages/add_multiple_packages.json deleted file mode 100644 index ba3eb5ee4..000000000 --- a/tests/operations/opkg.packages/add_multiple_packages.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args":[["curl", "wget"]], - "kwargs": { - "update": false - }, - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ - "opkg install curl wget" - ] -} diff --git a/tests/operations/opkg.packages/add_one_package.json b/tests/operations/opkg.packages/add_one_package.json deleted file mode 100644 index 53f774103..000000000 --- a/tests/operations/opkg.packages/add_one_package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "args": ["curl"], - "kwargs": { - "update": false - }, - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ - "opkg install curl" - ] -} diff --git a/tests/operations/opkg.packages/remove_existing_package.json b/tests/operations/opkg.packages/remove_existing_package.json deleted file mode 100644 index e7adb868e..000000000 --- a/tests/operations/opkg.packages/remove_existing_package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "args": ["curl"], - "kwargs": { - "present": false, - "update": false - }, - "facts": { - "opkg.OpkgPackages": {"curl": ["7.66.0-2"]} - }, - "commands": [ - "opkg remove curl" - ] -} diff --git a/tests/operations/opkg.packages/remove_existing_package_and_require_update.json b/tests/operations/opkg.packages/remove_existing_package_and_require_update.json deleted file mode 100644 index d083fe4af..000000000 --- a/tests/operations/opkg.packages/remove_existing_package_and_require_update.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "args": ["curl"], - "kwargs": { - "present": false, - "update": true - }, - "facts": { - "opkg.OpkgPackages": {"curl": ["7.66.0-2"]} - }, - "commands": [ - "opkg update", - "opkg remove curl" - ] -} diff --git a/tests/operations/opkg.packages/remove_not_existing_package.json b/tests/operations/opkg.packages/remove_not_existing_package.json deleted file mode 100644 index b68ce53d8..000000000 --- a/tests/operations/opkg.packages/remove_not_existing_package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "args": ["curl"], - "kwargs": { - "present": false, - "update": false - }, - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ - ], - "noop_description": "package curl is not installed" -} diff --git a/tests/operations/opkg.packages/update_then_add_one.json b/tests/operations/opkg.packages/update_then_add_one.json deleted file mode 100644 index 907b23e9a..000000000 --- a/tests/operations/opkg.packages/update_then_add_one.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": true}, - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ - "opkg update", - "opkg install curl" - ] -} diff --git a/tests/operations/opkg.update/first_update.json b/tests/operations/opkg.update/first_update.json deleted file mode 100644 index ea2e6b3ba..000000000 --- a/tests/operations/opkg.update/first_update.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": [], - "facts": { - "opkg.OpkgPackages": {} - }, - "commands": [ - "opkg update" - ] -} From 0dd972b30ee44d0f9773828abe28ddb982221ec2 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 18:54:22 -0400 Subject: [PATCH 05/26] feat(facts.openwrt): add OpenWrtHasFeature fact - takes a OpenWrtFeature enum instance and tells if the feature is supported by the release - use to determine opkg vs. apk or DSA vs. switch or FW4 vs. FW3, etc --- src/pyinfra/facts/openwrt/__init__.py | 2 + src/pyinfra/facts/openwrt/features.py | 127 ++++++++++++++++++ src/pyinfra/operations/openwrt/__init__.py | 2 + .../has_dsa_is_false_for_19_07.json | 15 +++ .../missing_distrib_release.json | 14 ++ .../no_input.json | 6 + .../no_patch_works.json | 15 +++ .../release_bad_major.json | 15 +++ .../release_bad_minor.json | 15 +++ .../release_empty.json | 15 +++ .../release_only_1_piece.json | 15 +++ .../uses_apk_is_true_for_26_4.json | 15 +++ tests/test_facts_other.py | 43 ++++++ 13 files changed, 299 insertions(+) create mode 100644 src/pyinfra/facts/openwrt/features.py create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json create mode 100644 tests/test_facts_other.py diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index 81c3e254e..8a42212f0 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -1,6 +1,8 @@ import importlib ALL = { + "OpenWrtFeature": "features.OpenWrtFeature", + "OpenWrtHasFeature": "features.OpenWrtHasFeature", "opkg": "opkg", } diff --git a/src/pyinfra/facts/openwrt/features.py b/src/pyinfra/facts/openwrt/features.py new file mode 100644 index 000000000..58b52c76f --- /dev/null +++ b/src/pyinfra/facts/openwrt/features.py @@ -0,0 +1,127 @@ +""" +Provides the OpenWrt version and feature support information + + + whether the system uses ``apk`` ? + +""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum, unique + +from typing_extensions import override + +from pyinfra import logger +from pyinfra.api import FactBase + + +@unique +class OpenWrtFeature(Enum): + USES_APK = "uses_apk" + HAS_DSA = "has_dsa" + HAS_FW4 = "has_fw4" + + +@dataclass(frozen=True) +class Release: + """ + A release with major and minor components + """ + major: int + minor: int + + + +@dataclass(frozen=True) +class ReleaseRange: + """ + A range of releases, usually used for validity. + Any release is newer than a start of None and older than an end of None + """ + start: Release | None + end: Release | None + + def contains(self, release: Release) -> bool: + return ( + (self.start is None) + or (release.major > self.start.major) + or ((release.major == self.start.major) and (release.minor >= self.start.minor)) + ) and ( + (self.end is None) + or (release.major < self.end.major) + or ((release.major == self.end.major) and (release.minor <= self.end.minor)) + ) + + +# +# References for release ranges in FEATURES table +# +# Feature.USES_APK - https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk +# Feature.HAS_DSA - https://openwrt.org/releases/21.02/notes-21.02.0#initial_dsa_support +# Feature.HAS_FW4 - https://openwrt.org/releases/22.03/notes-22.03.0#firewall4_based_on_nftables + +FEATURES = { + OpenWrtFeature.USES_APK: ReleaseRange(Release(25, 12), None), + # the following is a bit optimistic as it is really target-specific + OpenWrtFeature.HAS_DSA: ReleaseRange(Release(21, 2), None), + OpenWrtFeature.HAS_FW4: ReleaseRange(Release(22, 3), None), +} + +DOES_NOT_EXIST = ReleaseRange(Release(9999, 0), Release(9999, 0)) + +# line in /etc/openwrt-release with release number looks like: DISTRIB_RELEASE='19.07.2' +THE_FILE = "/etc/openwrt_release" +PREFIX = "DISTRIB_RELEASE='" +SUFFIX = "'" + + +class OpenWrtHasFeature(FactBase[bool]): + """ + Returns true if the running version of OpenWrt supports the specified feature. + .. code:: python + + if host.get_fact(OpenWrtHasFeature, Feature.HAS_DSA): + # setup configuration using the Distribution Switching Architecture + else: + # setup configuration using switch + """ + + # this isn't a ShortFact using LinuxDistribution because short facts can't have parameters + + @override + def command(self, feature: OpenWrtFeature) -> str: + # TODO: remove this once CLI and fact tests from fixtures support enums + if isinstance(feature, str): + feature = OpenWrtFeature(feature) + + return f"echo {feature.value} && cat {THE_FILE}" + + @override + def default(self) -> bool: + return False + + @override + def process(self, output: list[str]) -> bool: + if len(output) < 2: # usually will be 8 but conceptually we just need 2 + logger.error(f"not enough lines of output from {THE_FILE}") + return False + + feature = OpenWrtFeature(output[0]) + for line in output[1:]: + if not line.startswith(PREFIX): + continue + if len(pieces := line.removeprefix(PREFIX).strip("'").split(".")) >= 2: + try: + major, minor = int(pieces[0]), int(pieces[1]) + except (ValueError, TypeError): + logger.error(f"could not decode OpenWrt release from '{'.'.join(pieces)}'") + else: + return FEATURES.get(feature, DOES_NOT_EXIST).contains(Release(major, minor)) + else: + logger.error(f"unrecognized format for OpenWrt release: '{'.'.join(pieces)}'") + break + else: + logger.error(f"'{PREFIX}' not found in {THE_FILE}") + + return False diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 81c3e254e..410acdfc5 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -2,6 +2,8 @@ ALL = { "opkg": "opkg", + "packages": "packages.packages", + "update": "packages.update", } __all__ = list(ALL.keys()) diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json new file mode 100644 index 000000000..d1d65edcd --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json @@ -0,0 +1,15 @@ +{ + "arg": ["has_dsa"], + "command": "echo has_dsa && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='19.07.2'", + "DISTRIB_REVISION='r10947-65030d81f3'", + "DISTRIB_TARGET='x86/geode'", + "DISTRIB_ARCH='i386_pentium'", + "DISTRIB_DESCRIPTION='OpenWrt 19.07.2 r10947-65030d81f3'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json b/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json new file mode 100644 index 000000000..b23fedd81 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json @@ -0,0 +1,14 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json b/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json new file mode 100644 index 000000000..758350d16 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json @@ -0,0 +1,6 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json b/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json new file mode 100644 index 000000000..50d13ac62 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='25.12'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": true +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json new file mode 100644 index 000000000..058445f9f --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='A.12.4'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json new file mode 100644 index 000000000..0cb0cbf08 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='25.B.4'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json new file mode 100644 index 000000000..bf372b4e6 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE=''", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json new file mode 100644 index 000000000..abd775c88 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='25'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": false +} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json b/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json new file mode 100644 index 000000000..92be58194 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json @@ -0,0 +1,15 @@ +{ + "arg": ["uses_apk"], + "command": "echo uses_apk && cat /etc/openwrt_release", + "output": [ + "uses_apk", + "DISTRIB_ID='OpenWrt'", + "DISTRIB_RELEASE='25.12.4'", + "DISTRIB_REVISION='r32933-4ccb782af7'", + "DISTRIB_TARGET='rockchip/armv8'", + "DISTRIB_ARCH='aarch64_generic'", + "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", + "DISTRIB_TAINTS=''" + ], + "fact": true +} diff --git a/tests/test_facts_other.py b/tests/test_facts_other.py new file mode 100644 index 000000000..78aea4908 --- /dev/null +++ b/tests/test_facts_other.py @@ -0,0 +1,43 @@ +from unittest import TestCase + +from pyinfra.facts.openwrt.features import Release, ReleaseRange + + +class TestReleaseRangeInRange(TestCase): + STD_RANGE = ReleaseRange(Release(3, 5), Release(11, 12)) + + def test_major_in(self): + assert self.STD_RANGE.contains(Release(5, 4)) + + def test_major_below(self): + assert not self.STD_RANGE.contains(Release(1, 99)) + + def test_major_above(self): + assert not self.STD_RANGE.contains(Release(99, 1)) + + def test_major_equal_start_minor_above(self): + assert self.STD_RANGE.contains(Release(3, 11)) + + def test_major_equal_start_minor_equal(self): + assert self.STD_RANGE.contains(Release(3, 5)) + + def test_major_equal_start_minor_below(self): + assert not self.STD_RANGE.contains(Release(3, 3)) + + def test_major_equal_end_minor_above(self): + assert not self.STD_RANGE.contains(Release(11, 15)) + + def test_major_equal_end_minor_equal(self): + assert self.STD_RANGE.contains(Release(11, 12)) + + def test_major_equal_end_minor_below(self): + assert self.STD_RANGE.contains(Release(11, 11)) + + def test_major_below_end_start_none(self): + assert ReleaseRange(None, Release(11, 12)).contains(Release(0, 0)) + + def test_major_equal_end_start_none(self): + assert ReleaseRange(None, Release(11, 12)).contains(Release(11, 12)) + + def test_major_above_end_start_none(self): + assert not ReleaseRange(None, Release(11, 12)).contains(Release(15, 15)) From ee07a8eaee0209367e6eccf37af8ad65378d4c2b Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 18:55:54 -0400 Subject: [PATCH 06/26] feat(operations.openwrt): add packages operation to hide opkg vs. apk choice - use OpenWrtHasFeature to decide which package manager to use --- src/pyinfra/operations/openwrt/packages.py | 76 +++++++++++++++++++ .../openwrt.packages/apk_add_packages.json | 8 ++ .../openwrt.packages/apk_remove_packages.json | 9 +++ .../opkg_add_existing_package.json | 10 +++ .../opkg_add_one_package.json | 9 +++ .../opkg_remove_existing_package.json | 9 +++ 6 files changed, 121 insertions(+) create mode 100644 src/pyinfra/operations/openwrt/packages.py create mode 100644 tests/operations/openwrt.packages/apk_add_packages.json create mode 100644 tests/operations/openwrt.packages/apk_remove_packages.json create mode 100644 tests/operations/openwrt.packages/opkg_add_existing_package.json create mode 100644 tests/operations/openwrt.packages/opkg_add_one_package.json create mode 100644 tests/operations/openwrt.packages/opkg_remove_existing_package.json diff --git a/src/pyinfra/operations/openwrt/packages.py b/src/pyinfra/operations/openwrt/packages.py new file mode 100644 index 000000000..610fe875b --- /dev/null +++ b/src/pyinfra/operations/openwrt/packages.py @@ -0,0 +1,76 @@ +""" +Manage packages on OpenWrt using opkg or apk depending on the `version`_ of OpenWrt. + + ``update`` - update local copy of package information + + ``packages`` - install and remove packages + +See https://openwrt.org/docs/guide-user/additional-software/apk and + https://openwrt.org/docs/guide-user/additional-software/opkg + +TBD - OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function + +.. _version: https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk +""" + +from pyinfra import host +from pyinfra.api import operation +from pyinfra.facts.openwrt import OpenWrtFeature, OpenWrtHasFeature +from pyinfra.operations import apk + +from . import opkg + + +@operation(is_idempotent=False) +def update(): + """ + Update the local package information. + """ + if host.get_fact(OpenWrtHasFeature, OpenWrtFeature.USES_APK): + yield from apk.update._inner() # noqa: SLF001 + else: + yield from opkg.update._inner() # noqa: SLF001 + + +@operation() +def packages( + packages: str | list[str] = "", + present: bool = True, + latest: bool = False, + update: bool = False, +): + """ + Add/remove/update packages using `opkg` or `apk` depending on the OpenWrt version. + + + packages: package or list of packages to that must/must not be present + + present: whether the package(s) should be installed (default True) or removed + + latest: whether to attempt to upgrade the specified package(s) (default False) + + update: run ``apk|opkg update`` before installing packages (default False) + + See TBD and TBD for more details. + + TBD - Not Supported: + Opkg does not support version pinning, i.e. ``=`` is not allowed + and will cause an exception. + + **Examples:** + + .. code:: python + + from pyinfra.operations import openwrt + # Ensure packages are installed (will not force package upgrade) + openwrt.packages(['asterisk', 'vim'], name="Install Asterisk and Vim") + + # Install the latest versions of packages + openwrt.packages( + 'vim', + latest=True, + name="Ensure we have the latest version of Vim" + ) + """ + if host.get_fact(OpenWrtHasFeature, feature=OpenWrtFeature.USES_APK): + yield from apk.packages._inner( # noqa: SLF001 + packages=packages, latest=latest, update=update, present=present + ) + else: + yield from opkg.packages._inner( # noqa: SLF001 + packages=packages, latest=latest, update=update, present=present + ) diff --git a/tests/operations/openwrt.packages/apk_add_packages.json b/tests/operations/openwrt.packages/apk_add_packages.json new file mode 100644 index 000000000..19874951f --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_packages.json @@ -0,0 +1,8 @@ +{ + "args": ["curl"], + "facts": { + "apk.ApkPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true}, + }, + "commands": ["apk add curl"], +} diff --git a/tests/operations/openwrt.packages/apk_remove_packages.json b/tests/operations/openwrt.packages/apk_remove_packages.json new file mode 100644 index 000000000..66dd485e8 --- /dev/null +++ b/tests/operations/openwrt.packages/apk_remove_packages.json @@ -0,0 +1,9 @@ +{ + "args": [["curl", "i-dont-exist"]], + "kwargs": {"present": false}, + "facts": { + "apk.ApkPackages": {"curl": "1"}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true}, + }, + "commands": ["apk del curl"], +} diff --git a/tests/operations/openwrt.packages/opkg_add_existing_package.json b/tests/operations/openwrt.packages/opkg_add_existing_package.json new file mode 100644 index 000000000..2d706dd9a --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_existing_package.json @@ -0,0 +1,10 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": { + "opkg.OpkgPackages": {"curl": ["7.66.0-2"]}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, + }, + "commands": [], + "noop_description": "package curl is installed (7.66.0-2)", +} diff --git a/tests/operations/openwrt.packages/opkg_add_one_package.json b/tests/operations/openwrt.packages/opkg_add_one_package.json new file mode 100644 index 000000000..aa45759cc --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_one_package.json @@ -0,0 +1,9 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": { + "opkg.OpkgPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, + }, + "commands": ["opkg install curl"], +} diff --git a/tests/operations/openwrt.packages/opkg_remove_existing_package.json b/tests/operations/openwrt.packages/opkg_remove_existing_package.json new file mode 100644 index 000000000..32f04265e --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_remove_existing_package.json @@ -0,0 +1,9 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": false}, + "facts": { + "opkg.OpkgPackages": {"curl": ["7.66.0-2"]}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, + }, + "commands": ["opkg remove curl"], +} From f82b5f320f3ea49f7e95cbaff84eecb6b9d75744 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 19:17:42 -0400 Subject: [PATCH 07/26] feat(operations.opkg): replace now-moved to openwrt opkg with a deprecated forwarder - preserves backwards compatibility while allowing deprecation to be noted --- src/pyinfra/operations/opkg.py | 35 +++++++++++++++++++ .../opkg.packages/add_existing_package.json | 7 ++++ .../opkg.packages/add_multiple_packages.json | 6 ++++ .../opkg.packages/add_one_package.json | 6 ++++ .../add_with_unallowed_pinning.json | 9 +++++ .../list_of_nulls_package_list.json | 7 ++++ .../opkg.packages/null_package_list.json | 7 ++++ .../remove_existing_package.json | 6 ++++ ...e_existing_package_and_require_update.json | 6 ++++ .../remove_not_existing_package.json | 7 ++++ .../opkg.packages/update_then_add_one.json | 6 ++++ .../operations/opkg.update/first_update.json | 1 + 12 files changed, 103 insertions(+) create mode 100644 src/pyinfra/operations/opkg.py create mode 100644 tests/operations/opkg.packages/add_existing_package.json create mode 100644 tests/operations/opkg.packages/add_multiple_packages.json create mode 100644 tests/operations/opkg.packages/add_one_package.json create mode 100644 tests/operations/opkg.packages/add_with_unallowed_pinning.json create mode 100644 tests/operations/opkg.packages/list_of_nulls_package_list.json create mode 100644 tests/operations/opkg.packages/null_package_list.json create mode 100644 tests/operations/opkg.packages/remove_existing_package.json create mode 100644 tests/operations/opkg.packages/remove_existing_package_and_require_update.json create mode 100644 tests/operations/opkg.packages/remove_not_existing_package.json create mode 100644 tests/operations/opkg.packages/update_then_add_one.json create mode 100644 tests/operations/opkg.update/first_update.json diff --git a/src/pyinfra/operations/opkg.py b/src/pyinfra/operations/opkg.py new file mode 100644 index 000000000..2c5af3dfc --- /dev/null +++ b/src/pyinfra/operations/opkg.py @@ -0,0 +1,35 @@ +""" +This module is deprecated and will be removed in future version of pyinfra. +Use ``openwrt.opkg`` or ``openwrt.packages`` instead. + +Manage packages on OpenWrt using opkg + + ``update`` - update local copy of package information + + ``packages`` - install and remove packages +""" + +from pyinfra.api import operation +from pyinfra.operations.openwrt.opkg import packages as openwrt_packages, update as openwrt_update + + +@operation(is_deprecated=True, deprecated_for="openwrt.opkg.packages or openwrt.packages") +def packages( + packages: str | list[str] = "", + present: bool = True, + latest: bool = False, + update: bool = True, +): + """ + Install, update or remove the specified packages. + """ + yield from openwrt_packages._inner( # noqa: SLF001 + packages=packages, present=present, latest=latest, update=update + ) + + +@operation(is_idempotent=False, deprecated_for="openwrt.opkg.update or openwrt.update") +def update(): + """ + Update the local package information. + """ + + yield from openwrt_update._inner() # noqa: SLF001 diff --git a/tests/operations/opkg.packages/add_existing_package.json b/tests/operations/opkg.packages/add_existing_package.json new file mode 100644 index 000000000..e806d995f --- /dev/null +++ b/tests/operations/opkg.packages/add_existing_package.json @@ -0,0 +1,7 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": [], + "noop_description": "package curl is installed (7.66.0-2)", +} diff --git a/tests/operations/opkg.packages/add_multiple_packages.json b/tests/operations/opkg.packages/add_multiple_packages.json new file mode 100644 index 000000000..a5517216b --- /dev/null +++ b/tests/operations/opkg.packages/add_multiple_packages.json @@ -0,0 +1,6 @@ +{ + "args": [["curl", "wget"]], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg install curl wget"], +} diff --git a/tests/operations/opkg.packages/add_one_package.json b/tests/operations/opkg.packages/add_one_package.json new file mode 100644 index 000000000..23187734f --- /dev/null +++ b/tests/operations/opkg.packages/add_one_package.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg install curl"], +} diff --git a/tests/operations/opkg.packages/add_with_unallowed_pinning.json b/tests/operations/opkg.packages/add_with_unallowed_pinning.json new file mode 100644 index 000000000..69defc1d4 --- /dev/null +++ b/tests/operations/opkg.packages/add_with_unallowed_pinning.json @@ -0,0 +1,9 @@ +{ + "args": ["curl=7.66.0-2"], + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], + "exception": { + "name": "ValueError", + "message": "opkg does not support version pinning but found for: 'curl'", + }, +} diff --git a/tests/operations/opkg.packages/list_of_nulls_package_list.json b/tests/operations/opkg.packages/list_of_nulls_package_list.json new file mode 100644 index 000000000..28aa77002 --- /dev/null +++ b/tests/operations/opkg.packages/list_of_nulls_package_list.json @@ -0,0 +1,7 @@ +{ + "args": ["", "", ""], + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], + "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", + "logs": "foo", +} diff --git a/tests/operations/opkg.packages/null_package_list.json b/tests/operations/opkg.packages/null_package_list.json new file mode 100644 index 000000000..fa0ce88ab --- /dev/null +++ b/tests/operations/opkg.packages/null_package_list.json @@ -0,0 +1,7 @@ +{ + "args": [], + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], + "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", + "logs": "foo", +} diff --git a/tests/operations/opkg.packages/remove_existing_package.json b/tests/operations/opkg.packages/remove_existing_package.json new file mode 100644 index 000000000..6cc6370be --- /dev/null +++ b/tests/operations/opkg.packages/remove_existing_package.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": false}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": ["opkg remove curl"], +} diff --git a/tests/operations/opkg.packages/remove_existing_package_and_require_update.json b/tests/operations/opkg.packages/remove_existing_package_and_require_update.json new file mode 100644 index 000000000..193223a0d --- /dev/null +++ b/tests/operations/opkg.packages/remove_existing_package_and_require_update.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": true}, + "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, + "commands": ["opkg update", "opkg remove curl"], +} diff --git a/tests/operations/opkg.packages/remove_not_existing_package.json b/tests/operations/opkg.packages/remove_not_existing_package.json new file mode 100644 index 000000000..cef56213a --- /dev/null +++ b/tests/operations/opkg.packages/remove_not_existing_package.json @@ -0,0 +1,7 @@ +{ + "args": ["curl"], + "kwargs": {"present": false, "update": false}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": [], + "noop_description": "package curl is not installed", +} diff --git a/tests/operations/opkg.packages/update_then_add_one.json b/tests/operations/opkg.packages/update_then_add_one.json new file mode 100644 index 000000000..9c8e03db6 --- /dev/null +++ b/tests/operations/opkg.packages/update_then_add_one.json @@ -0,0 +1,6 @@ +{ + "args": ["curl"], + "kwargs": {"update": true}, + "facts": {"opkg.OpkgPackages": {}}, + "commands": ["opkg update", "opkg install curl"], +} diff --git a/tests/operations/opkg.update/first_update.json b/tests/operations/opkg.update/first_update.json new file mode 100644 index 000000000..4d8479faa --- /dev/null +++ b/tests/operations/opkg.update/first_update.json @@ -0,0 +1 @@ +{"args": [], "facts": {"opkg.OpkgPackages": {}}, "commands": ["opkg update"]} From 91cf18f866c299fadcd266e96c5bf35d648ee3fa Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 20:47:06 -0400 Subject: [PATCH 08/26] feature(api.facts): allow facts (incl. ShortFactBase) to be marked as deprecated. - same approach as operations: is_deprecated and, optionally, deprecated_for - only emit one warning per distinct fact regardless of the number of invocations --- src/pyinfra/api/facts.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/pyinfra/api/facts.py b/src/pyinfra/api/facts.py index 9d7dc62ba..35592b558 100644 --- a/src/pyinfra/api/facts.py +++ b/src/pyinfra/api/facts.py @@ -53,6 +53,7 @@ T = TypeVar("T") +already_logged_as_deprecated = set() # used to ensure only one warning per deprecated fact class FactBase(Generic[T]): name: str @@ -63,6 +64,10 @@ class FactBase(Generic[T]): command: Callable[..., str | StringCommand] + is_deprecated = False + + deprecated_for: str|None = None + def requires_command(self, *args, **kwargs) -> str | None: """Return the binary name that must exist on the remote host for this fact to run. If the binary is absent the fact returns its ``default()`` value silently. @@ -118,6 +123,8 @@ def process_pipeline(self, args, output): class ShortFactBase(Generic[T]): name: str fact: type[FactBase] + is_deprecated = False + deprecated_for: str|None = None @override def __init_subclass__(cls) -> None: @@ -195,6 +202,15 @@ def get_fact( ensure_hosts: Any | None = None, apply_failed_hosts: bool = True, ) -> Any: + global already_logged_as_deprecated + + if cls.is_deprecated and (cls not in already_logged_as_deprecated): + already_logged_as_deprecated.add(cls) + msg = f"The {cls.__name__} fact is deprecated" + if cls.deprecated_for is not None: + msg = f"{msg}, please use: {cls.deprecated_for}" + logger.warning(msg) + if issubclass(cls, ShortFactBase): return get_short_facts( state, From 263072a55b60613265e03459af0141e2604b878b Mon Sep 17 00:00:00 2001 From: morrison12 Date: Thu, 18 Jun 2026 20:48:06 -0400 Subject: [PATCH 09/26] feature(facts.opkg): add deprecated opkg facts that forward to openwrt.opkg - preserve backwards compability while giving notice of deprecation. --- src/pyinfra/facts/opkg.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/pyinfra/facts/opkg.py diff --git a/src/pyinfra/facts/opkg.py b/src/pyinfra/facts/opkg.py new file mode 100644 index 000000000..b03f22afb --- /dev/null +++ b/src/pyinfra/facts/opkg.py @@ -0,0 +1,35 @@ +""" +This is deprecated: use ``openwrt.opkg`` or ``openwrt.packages`` instead. + +Gather the information provided by ``opkg`` on OpenWrt systems: + + ``opkg`` configuration + + feeds configuration + + list of installed packages + + list of packages with available upgrades + +""" + +from pyinfra.facts.openwrt.opkg import OpkgConf as OpenWrtConf, OpkgFeeds as OpenWrtFeeds +from pyinfra.facts.openwrt.opkg import OpkgInstallableArchitectures as OpenWrtInstallableArchitectures, OpkgPackages as OpenWrtPackages +from pyinfra.facts.openwrt.opkg import OpkgUpgradeablePackages as OpenWrtUpgradeablePackages + + +class OpkgConf(OpenWrtConf): + is_deprecated = True + deprecated_for = "openwrt.opkg.OpkgConf" + +class OpkgFeeds(OpenWrtFeeds): + is_deprecated = True + deprecated_for = "openwrt.opkg.OpkgFeeds" + +class OpkgInstallableArchitectures(OpenWrtInstallableArchitectures): + is_deprecated = True + deprecated_for = "openwrt.opkg.OpkgInstallableArchitectures" + +class OpkgPackages(OpenWrtPackages): + is_deprecated = True + deprecated_for = "openwrt.opkg.OpkgPackages" + +class OpkgUpgradeablePackages(OpenWrtUpgradeablePackages): + is_deprecated = True + deprecated_for = "openwrt.opkg.OpkgUpgradeablePackages" From 9d14e13814ef71024bd62ad4a935824d1f47afd1 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sat, 20 Jun 2026 17:21:15 -0400 Subject: [PATCH 10/26] feature(facts.openwrt, operations.openwrt): add facts and operations entries for openwrt to pyinfra-metadata - required for them to show up in the documentation cards --- pyinfra-metadata.toml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pyinfra-metadata.toml b/pyinfra-metadata.toml index 88e4361ea..5f6eab4c3 100644 --- a/pyinfra-metadata.toml +++ b/pyinfra-metadata.toml @@ -81,6 +81,24 @@ path = "src/pyinfra/facts/freebsd.py" type = "fact" tags = ["package-manager"] +[pyinfra.plugins."freebsd-ops"] +name = "freebsd" +path = "src/pyinfra/operations/freebsd" +type = "operation" +tags = ["system"] + +[pyinfra.plugins."openwrt-ops"] +name = "openwrt" +path = "src/pyinfra/operations/openwrt" +type = "operation" +tags = ["system"] + +[pyinfra.plugins."openwrt-facts"] +name = "openwrt" +path = "src/pyinfra/facts/openwrt" +type = "fact" +tags = ["system"] + [pyinfra.plugins."opkg-ops"] name = "opkg" path = "src/pyinfra/operations/opkg.py" From 098d6529eaa8eb1cc0f1981e4d04a5bc45aba27f Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sat, 20 Jun 2026 17:31:34 -0400 Subject: [PATCH 11/26] fix(facts, operations): generate docs for fact and implementations that use packages instead of single files - deferred imports broke "docs for packages" fix - change deferred import mechanism to allow equiv. of from .x import y - used updated mechanism for both freebsed operations and openwrt facts and operations - change deferred imports mechanism to dispose of module if only a single element imported - above needed as otherwise importer bypasses getattr - update fact and operation discovery to use table of deferred imports for packages - move remove_dups into discovery code --- scripts/docs_utils.py | 78 ++++++++++++++++------ scripts/generate_facts_docs.py | 21 ++---- scripts/generate_llms_txt.py | 45 +++---------- scripts/generate_operations_docs.py | 33 ++------- src/pyinfra/facts/openwrt/__init__.py | 12 ++-- src/pyinfra/operations/freebsd/__init__.py | 28 +++++--- src/pyinfra/operations/openwrt/__init__.py | 12 ++-- 7 files changed, 115 insertions(+), 114 deletions(-) mode change 100644 => 100755 scripts/generate_llms_txt.py diff --git a/scripts/docs_utils.py b/scripts/docs_utils.py index 01cf71b9c..29dc9e190 100644 --- a/scripts/docs_utils.py +++ b/scripts/docs_utils.py @@ -1,9 +1,10 @@ import re -from inspect import cleandoc, getmembers, ismodule +from collections.abc import Callable +from inspect import cleandoc, getmembers, isfunction, ismodule from pathlib import Path from types import ModuleType -from typing import Any -from collections.abc import Generator + +from pyinfra.api import FactBase, ShortFactBase def format_doc_line(line: str) -> str: @@ -114,19 +115,6 @@ def prepare_docstring(doc: str | None) -> str: return rst_to_md_docstring(cleandoc(doc)) -def including_sub_modules(module: ModuleType) -> Generator[ModuleType, None, None]: - """Yield all modules to be examined, including the base modules.""" - yield module - module_name = module.__name__ - for key, value in getmembers(module): - if ( - ismodule(value) - and value.__name__.startswith(module_name) - and (not key.startswith("__")) - ): - yield from including_sub_modules(value) - - def get_module_names( src_dir: Path, *, @@ -148,11 +136,63 @@ def get_module_names( return module_names -def remove_dups(all: list[tuple[str, Any]]) -> list[tuple[str, Any]]: - """Remove items with duplicate values, i.e. the same function or module found again.""" +def is_fact_class(m: ModuleType, _key: str, value: object) -> bool: + return ( + isinstance(value, type) + and (issubclass(value, FactBase) or issubclass(value, ShortFactBase)) + and value.__module__.startswith(m.__name__) + and value is not FactBase + and not value.__name__.endswith("Base") # hacky! + ) + + +def is_fact_class_in_op(_m: ModuleType, _key: str, value: object) -> bool: + return isinstance(value, type) and ( + issubclass(value, FactBase) or issubclass(value, ShortFactBase) + ) + + +def function_of_interest(module: ModuleType, key: str, value: object) -> bool: + return ( + isfunction(value) + and value.__module__.startswith(module.__name__) + and getattr(value, "_inner", False) + and not value.__name__.startswith("_") + and not key.startswith("_") + ) + + +def get_objects_from_module( + module: ModuleType, + predicate: Callable[[object], bool], + filt: Callable[[ModuleType, str, object], bool], +) -> list[tuple[str, type]]: + if not hasattr(module, "__path__"): # not a package thus a single file + found = [ + (key, value) for key, value in getmembers(module, predicate) if filt(module, key, value) + ] + else: + # for packages, deferred import mechanism gives zero members so need to use __all__ + found = [ + (name, item) + for sym in module.__all__ + for name, item in ( + ( + (f"{sym}.{key}", value) + for key, value in getmembers(getattr(module, sym), predicate) + if filt(module, key, value) + ) + if ismodule(getattr(module, sym)) + else ([(sym, getattr(module, sym))] if predicate(getattr(module, sym)) else []) + ) + if filt(module, name, item) + ] + + # Remove items with duplicate values, i.e. the same function or module found again unique, seen = [], set() - for key, value in all: + for key, value in found: if value not in seen: seen.add(value) unique.append((key, value)) + return unique diff --git a/scripts/generate_facts_docs.py b/scripts/generate_facts_docs.py index 1439d380d..36d3b4cd6 100755 --- a/scripts/generate_facts_docs.py +++ b/scripts/generate_facts_docs.py @@ -2,21 +2,20 @@ import sys from importlib import import_module -from inspect import getfullargspec, getmembers, isclass +from inspect import getfullargspec, isclass from os import makedirs, path from pathlib import Path from types import FunctionType, MethodType -from pyinfra.api.facts import FactBase, ShortFactBase from pyinfra.api.metadata import ALLOWED_TAGS, parse_plugins sys.path.append(path.dirname(path.realpath(__file__))) from docs_utils import ( format_doc_line, get_module_names, - including_sub_modules, + get_objects_from_module, + is_fact_class, prepare_docstring, - remove_dups, ) # noqa: E402 CARD_SCRIPT = """\ @@ -119,20 +118,8 @@ def build_facts_docs(): lines.append(f"See also: [operations/{module_name}](../operations/{module_name}.md).") lines.append("") - all_fact_classes = [ - (key, value) - for m in including_sub_modules(module) - for key, value in getmembers(m) - if ( - isclass(value) - and (issubclass(value, FactBase) or issubclass(value, ShortFactBase)) - and value.__module__.startswith(m.__name__) - and value is not FactBase - and not value.__name__.endswith("Base") # hacky! - ) - ] + fact_classes = get_objects_from_module(module, isclass, is_fact_class) - fact_classes = remove_dups(all_fact_classes) for fact, cls in fact_classes: name = fact args_string_and_brackets = "" diff --git a/scripts/generate_llms_txt.py b/scripts/generate_llms_txt.py old mode 100644 new mode 100755 index a64fb8e97..1c5a19f37 --- a/scripts/generate_llms_txt.py +++ b/scripts/generate_llms_txt.py @@ -11,19 +11,22 @@ from __future__ import annotations import sys -from inspect import getdoc, getfullargspec, getmembers, isclass, signature +from importlib import import_module +from inspect import getdoc, getfullargspec, isclass, isfunction, signature from os import environ, makedirs, path from pathlib import Path from types import FunctionType, MethodType, ModuleType -from importlib import import_module from pyinfra.api import metadata from pyinfra.api.connectors import get_all_connectors -from pyinfra.api.facts import FactBase, ShortFactBase sys.path.append(path.dirname(path.realpath(__file__))) -from docs_utils import including_sub_modules, prepare_docstring, remove_dups # noqa: E402 - +from docs_utils import ( + function_of_interest, + get_objects_from_module, + is_fact_class, + prepare_docstring, +) # noqa: E402 BASE_URL = "https://docs.pyinfra.com/en" VERSION = environ.get("DOCS_VERSION", "latest") @@ -199,39 +202,11 @@ def section(title: str, pages: list[tuple[str, str, str]]) -> None: def _extract_operation_funcs(module: ModuleType) -> list[tuple[str, FunctionType]]: - funcs = [ - ( - f"{m.__name__.split('.')[-1]}.{key}" if m != module else key, - getattr(value, "_inner"), - ) - for m in including_sub_modules(module) - for key, value in getmembers(m) - if ( - isinstance(value, FunctionType) - and value.__module__.startswith(m.__name__) - and getattr(value, "_inner", False) - and not value.__name__.startswith("_") - and not key.startswith("_") - ) - ] - return remove_dups(funcs) + return get_objects_from_module(module, isfunction, function_of_interest) def _extract_fact_classes(module: ModuleType) -> list[tuple[str, type]]: - classes = [ - (key, value) - for m in including_sub_modules(module) - for key, value in getmembers(m) - if ( - isclass(value) - and (issubclass(value, FactBase) or issubclass(value, ShortFactBase)) - and value.__module__.startswith(m.__name__) - and value is not FactBase - and value is not ShortFactBase - and not value.__name__.endswith("Base") - ) - ] - return remove_dups(classes) + return get_objects_from_module(module, isclass, is_fact_class) def render_operation_section(module_name: str) -> list[str]: diff --git a/scripts/generate_operations_docs.py b/scripts/generate_operations_docs.py index 77b0588de..61299e502 100755 --- a/scripts/generate_operations_docs.py +++ b/scripts/generate_operations_docs.py @@ -2,21 +2,20 @@ import sys from importlib import import_module -from inspect import getmembers, isclass, signature +from inspect import isclass, isfunction, signature from os import makedirs, path from pathlib import Path -from types import FunctionType -from pyinfra.api.facts import FactBase from pyinfra.api.metadata import ALLOWED_TAGS, parse_plugins sys.path.append(path.dirname(path.realpath(__file__))) from docs_utils import ( format_doc_line, + function_of_interest, get_module_names, - including_sub_modules, + get_objects_from_module, + is_fact_class_in_op, prepare_docstring, - remove_dups, ) # noqa: E402 MODULE_DEF_LINE_MAX = 90 @@ -118,37 +117,19 @@ def build_operations_docs(): lines.append(module_doc) lines.append("") - operation_facts = [ - (key, value) - for m in including_sub_modules(module) - for key, value in getmembers(m) - if (isclass(value) and issubclass(value, FactBase)) - ] + unique_facts = get_objects_from_module(module, isclass, is_fact_class_in_op) - unique_facts = remove_dups(operation_facts) if unique_facts: items = [] for key, value in unique_facts: - fact_module = value.__module__.replace("pyinfra.facts.", "") + fact_module = value.__module__.replace("pyinfra.facts.", "").split(".")[0] items.append( f"[`{fact_module}.{key}`](../facts/{fact_module}.md#{fact_module}-{key})" ) lines.append("Facts used in these operations: {}.".format(", ".join(items))) lines.append("") - all_operation_functions = [ - (f"{m.__name__.split('.')[-1]}.{key}" if m != module else key, value._inner) - for m in including_sub_modules(module) - for key, value in getmembers(m) - if ( - isinstance(value, FunctionType) - and value.__module__.startswith(m.__name__) - and getattr(value, "_inner", False) - and not value.__name__.startswith("_") - and not key.startswith("_") - ) - ] - operation_functions = remove_dups(all_operation_functions) + operation_functions = get_objects_from_module(module, isfunction, function_of_interest) for name, func in operation_functions: decorated_func = getattr(func, "_inner", None) diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index 8a42212f0..e46b5ca6e 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -1,4 +1,5 @@ import importlib +import sys ALL = { "OpenWrtFeature": "features.OpenWrtFeature", @@ -12,11 +13,14 @@ def __getattr__(name): # On-demand import of OpenWrt facts, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL - # Also, Pyinfra is py3.11>=, so this is not a breaking change. + # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: pieces = ALL[name].split(".") module = importlib.import_module(f".{pieces[0]}", package=__name__) - if len(pieces) > 1: - return getattr(module, pieces[1]) - return module + if len(pieces) < 2: + return module + del sys.modules[module.__name__] + del sys.modules[__name__].__dict__[pieces[0]] + return getattr(module, pieces[1]) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/pyinfra/operations/freebsd/__init__.py b/src/pyinfra/operations/freebsd/__init__.py index fe5f43353..7013ddf7b 100644 --- a/src/pyinfra/operations/freebsd/__init__.py +++ b/src/pyinfra/operations/freebsd/__init__.py @@ -1,17 +1,27 @@ import importlib -from glob import glob -from os import path +import sys -_module_filenames = glob(path.join(path.dirname(__file__), "*.py")) -__all__ = sorted( - set(path.basename(name)[:-3] for name in _module_filenames if not name.endswith("__init__.py")) -) +ALL = { + "freebsd_update": "freebsd_update", + "pkg": "pkg", + "service": "service", + "sysrc": "sysrc", +} + +__all__ = list(ALL.keys()) def __getattr__(name): - # On-demand import of operations modules, so we don't have to import them all at once + # On-demand import of OpenWrt facts, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL - # Also, Pyinfra is py3.11>=, so this is not a breaking change. + # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: - return importlib.import_module(f".{name}", __package__) + pieces = ALL[name].split(".") + module = importlib.import_module(f".{pieces[0]}", package=__name__) + if len(pieces) < 2: + return module + del sys.modules[module.__name__] + del sys.modules[__name__].__dict__[pieces[0]] + return getattr(module, pieces[1]) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 410acdfc5..1f8bf4523 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -1,4 +1,5 @@ import importlib +import sys ALL = { "opkg": "opkg", @@ -12,11 +13,14 @@ def __getattr__(name): # On-demand import of OpenWrt facts, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL - # Also, Pyinfra is py3.11>=, so this is not a breaking change. + # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: pieces = ALL[name].split(".") module = importlib.import_module(f".{pieces[0]}", package=__name__) - if len(pieces) > 1: - return getattr(module, pieces[1]) - return module + if len(pieces) < 2: + return module + del sys.modules[module.__name__] + del sys.modules[__name__].__dict__[pieces[0]] + return getattr(module, pieces[1]) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From 0475bb28bde2a0f9eaa751afaf308a4b5c22fd47 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 13:08:46 -0400 Subject: [PATCH 12/26] chore: clean up documentation for openwrt and opkg --- src/pyinfra/facts/openwrt/__init__.py | 4 ++ src/pyinfra/facts/openwrt/features.py | 23 +++++++---- src/pyinfra/facts/openwrt/opkg.py | 37 ++++++++++++----- src/pyinfra/facts/opkg.py | 47 +++++++++++++++++----- src/pyinfra/operations/openwrt/__init__.py | 4 ++ src/pyinfra/operations/openwrt/opkg.py | 28 ++++++++----- src/pyinfra/operations/openwrt/packages.py | 40 +++++++++++------- src/pyinfra/operations/opkg.py | 12 +++--- 8 files changed, 138 insertions(+), 57 deletions(-) diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index e46b5ca6e..9453ff925 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -1,3 +1,7 @@ +""" +Facts specific to the [OpenWrt](https://www.openwrt.org) distribution. +""" + import importlib import sys diff --git a/src/pyinfra/facts/openwrt/features.py b/src/pyinfra/facts/openwrt/features.py index 58b52c76f..af9c724f9 100644 --- a/src/pyinfra/facts/openwrt/features.py +++ b/src/pyinfra/facts/openwrt/features.py @@ -1,8 +1,11 @@ """ -Provides the OpenWrt version and feature support information +Provides a fact that tells whether a host supports an OpenWrt feature or not. - + whether the system uses ``apk`` ? + + whether the host uses ``apk`` (vs. ``opkg``) + + whether the host has ``DSA`` (vs. ``swconfig``) + + whether the host has ``FW4`` (and ``nftables`` vs. ``FW3/iptables``) +note: this does _not_ show up in the online documentation; the file header in __init__.py does """ from __future__ import annotations @@ -28,17 +31,18 @@ class Release: """ A release with major and minor components """ + major: int minor: int - @dataclass(frozen=True) class ReleaseRange: """ A range of releases, usually used for validity. Any release is newer than a start of None and older than an end of None """ + start: Release | None end: Release | None @@ -78,13 +82,16 @@ def contains(self, release: Release) -> bool: class OpenWrtHasFeature(FactBase[bool]): """ - Returns true if the running version of OpenWrt supports the specified feature. - .. code:: python + Returns `true` if the running version of OpenWrt supports a specific feature and `false` + otherwise. + + .. code:: python + from pyinfra.facts.openwrt import OpenWrtFeature - if host.get_fact(OpenWrtHasFeature, Feature.HAS_DSA): - # setup configuration using the Distribution Switching Architecture + if host.get_fact(OpenWrtHasFeature, OpenWrtFeature.HAS_DSA): + # setup configuration using the Distributed Switching Architecture else: - # setup configuration using switch + # setup configuration using swconfig """ # this isn't a ShortFact using LinuxDistribution because short facts can't have parameters diff --git a/src/pyinfra/facts/openwrt/opkg.py b/src/pyinfra/facts/openwrt/opkg.py index 8469cdcb5..d08e67e6a 100644 --- a/src/pyinfra/facts/openwrt/opkg.py +++ b/src/pyinfra/facts/openwrt/opkg.py @@ -7,9 +7,12 @@ See https://openwrt.org/docs/guide-user/additional-software/opkg -**Note:** as of OpenWrt Release `2025.12`_, OpenWrt uses ``apk``. +.. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) -.. _2025.12: https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk +note: this does _not_ show up in the online documentation; the file header in __init__.py does +and thus the note above is repeated in each fact. """ import re @@ -21,9 +24,6 @@ from pyinfra.api import FactBase from pyinfra.facts.util.packaging import parse_packages -# TODO - change NamedTuple to dataclass Opkg but need to figure out how to get json serialization -# to work without changing core code - class OpkgPkgUpgradeInfo(NamedTuple): installed: str @@ -45,7 +45,7 @@ class OpkgFeedInfo(NamedTuple): class OpkgConf(FactBase): """ - Returns a NamedTuple with the current configuration: + Returns a ``NamedTuple`` with the current ``opkg`` configuration: .. code:: python @@ -65,6 +65,9 @@ class OpkgConf(FactBase): } ) + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) """ @override @@ -118,7 +121,7 @@ def process(self, output): class OpkgFeeds(FactBase): """ Returns a dictionary containing the information for the distribution-provided and - custom opkg feeds: + custom `opkg` feeds: .. code:: python @@ -130,6 +133,10 @@ class OpkgFeeds(FactBase): 'openwrt_routing': FeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'), # noqa: E501 'openwrt_telephony': FeedInfo(url='http://downloads ... /i386_pentium/telephony', fmt='src/gz', kind='distribution') # noqa: E501 } + + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) """ regex = re.compile( @@ -175,6 +182,10 @@ class OpkgInstallableArchitectures(FactBase): 'i386_pentium': 10, 'noarch': 1 } + + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) """ regex = re.compile(r"^(?:\s*arch\s+(?P[\w]+)\s+(?P\d+))?(\s*#.*)?$") @@ -204,7 +215,7 @@ def process(self, output): class OpkgPackages(FactBase): """ - Returns a dict of installed opkg packages: + Returns a dictionary of installed `opkg` packages: .. code:: python @@ -212,6 +223,10 @@ class OpkgPackages(FactBase): 'package_name': ['version'], ... } + + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) """ regex = r"^([a-zA-Z0-9][\w\-\.]*)\s-\s([\w\-\.]+)" @@ -232,7 +247,7 @@ def process(self, output): class OpkgUpgradeablePackages(FactBase): """ - Returns a dict of installed and upgradable opkg packages: + Returns a dict of installed and upgradable `opkg` packages: .. code:: python @@ -240,6 +255,10 @@ class OpkgUpgradeablePackages(FactBase): 'package_name': (installed='1.2.3', available='1.2.8') ... } + + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../facts/apk.md) """ regex = re.compile(r"^([a-zA-Z0-9][\w\-.]*)\s-\s([\w\-.]+)\s-\s([\w\-.]+)") diff --git a/src/pyinfra/facts/opkg.py b/src/pyinfra/facts/opkg.py index b03f22afb..9f23ffe48 100644 --- a/src/pyinfra/facts/opkg.py +++ b/src/pyinfra/facts/opkg.py @@ -1,35 +1,62 @@ """ -This is deprecated: use ``openwrt.opkg`` or ``openwrt.packages`` instead. - -Gather the information provided by ``opkg`` on OpenWrt systems: - + ``opkg`` configuration - + feeds configuration - + list of installed packages - + list of packages with available upgrades +.. warning:: + This module is deprecated and will be removed in future version of pyinfra. + Use [openwrt.opkg](../operations/openwrt.md) or [openwrt.packages](../operations/openwrt.md) + instead. +Gather the information provided by +[opkg](https://openwrt.org/docs/guide-user/additional-software/opkg) on OpenWrt systems: """ -from pyinfra.facts.openwrt.opkg import OpkgConf as OpenWrtConf, OpkgFeeds as OpenWrtFeeds -from pyinfra.facts.openwrt.opkg import OpkgInstallableArchitectures as OpenWrtInstallableArchitectures, OpkgPackages as OpenWrtPackages -from pyinfra.facts.openwrt.opkg import OpkgUpgradeablePackages as OpenWrtUpgradeablePackages +from pyinfra.facts.openwrt.opkg import ( + OpkgConf as OpenWrtConf, + OpkgFeeds as OpenWrtFeeds, + OpkgInstallableArchitectures as OpenWrtInstallableArchitectures, + OpkgPackages as OpenWrtPackages, + OpkgUpgradeablePackages as OpenWrtUpgradeablePackages, +) class OpkgConf(OpenWrtConf): + """ + See [openwrt.opkg.OpkgConf](../facts/openwrt.md#openwrt-opkg.OpkgConf) for details. + """ + is_deprecated = True deprecated_for = "openwrt.opkg.OpkgConf" + class OpkgFeeds(OpenWrtFeeds): + """ + See [openwrt.opkg.OpkgFeeds](../facts/openwrt.md#openwrt-opkg.OpkgFeeds) for details. + """ + is_deprecated = True deprecated_for = "openwrt.opkg.OpkgFeeds" + class OpkgInstallableArchitectures(OpenWrtInstallableArchitectures): + """ + See [openwrt.opkg.OpkgInstallableArchitectures](../facts/openwrt.md#openwrt-opkg.OpkgInstallableArchitectures) for details. + """ + is_deprecated = True deprecated_for = "openwrt.opkg.OpkgInstallableArchitectures" + class OpkgPackages(OpenWrtPackages): + """ + See [openwrt.opkg.OpkgPackages](../facts/openwrt.md#openwrt-opkg.OpkgPackages) for details. + """ + is_deprecated = True deprecated_for = "openwrt.opkg.OpkgPackages" + class OpkgUpgradeablePackages(OpenWrtUpgradeablePackages): + """ + See [openwrt.opkg.OpkgUpgradeablePackages](../facts/openwrt.md#openwrt-opkg.OpkgUpgradeablePackages) for details. + """ + is_deprecated = True deprecated_for = "openwrt.opkg.OpkgUpgradeablePackages" diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 1f8bf4523..539fb3bda 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -1,3 +1,7 @@ +""" +Operations specific to the [OpenWrt](https://openwrt.org) distribution. +""" + import importlib import sys diff --git a/src/pyinfra/operations/openwrt/opkg.py b/src/pyinfra/operations/openwrt/opkg.py index 75346f66c..f032e542b 100644 --- a/src/pyinfra/operations/openwrt/opkg.py +++ b/src/pyinfra/operations/openwrt/opkg.py @@ -1,15 +1,18 @@ """ Manage packages on OpenWrt using opkg - + ``update`` - update local copy of package information - + ``packages`` - install and remove packages + + `packages` - install and remove packages + + `update` - update local copy of package information See https://openwrt.org/docs/guide-user/additional-software/opkg -OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function +OpenWrt recommends against upgrading all packages thus there is no `opkg.upgrade` function -**Note:** as of OpenWrt Release `2025.12`_, OpenWrt uses ``apk``. +.. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../operations/apk.md) -.. _2025.12: https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk + note: this does _not_ show up in the online documentation; the file header in __init__.py does +and thus the note above is repeated in each operation. """ from pyinfra import host @@ -43,12 +46,12 @@ def packages( Add/remove/update opkg packages. + packages: package or list of packages to that must/must not be present - + present: whether the package(s) should be installed (default True) or removed - + latest: whether to attempt to upgrade the specified package(s) (default False) - + update: run ``opkg update`` before installing packages (default True) + + present: whether the package(s) should be installed or removed (default ``True``). + + latest: whether to attempt to upgrade the specified package(s) (default ``False``). + + update: run ``opkg update`` before installing packages (default ``True``). - Not Supported: - Opkg does not support version pinning, i.e. ``=`` is not allowed + **Not Supported:** + ``opkg`` does not support version pinning, i.e. ``=`` is _not_ allowed and will cause an exception. **Examples:** @@ -56,6 +59,7 @@ def packages( .. code:: python from pyinfra.operations import opkg + # Ensure packages are installed (will not force package upgrade) openwrt.opkg.packages(['asterisk', 'vim'], name="Install Asterisk and Vim") @@ -65,6 +69,10 @@ def packages( latest=True, name="Ensure we have the latest version of Vim" ) + + .. note:: + as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) + OpenWrt uses [apk](../operations/apk.md) """ if str(packages) == "" or ( isinstance(packages, list) and (len(packages) < 1 or all(len(p) < 1 for p in packages)) diff --git a/src/pyinfra/operations/openwrt/packages.py b/src/pyinfra/operations/openwrt/packages.py index 610fe875b..e21947f71 100644 --- a/src/pyinfra/operations/openwrt/packages.py +++ b/src/pyinfra/operations/openwrt/packages.py @@ -1,14 +1,17 @@ """ -Manage packages on OpenWrt using opkg or apk depending on the `version`_ of OpenWrt. - + ``update`` - update local copy of package information - + ``packages`` - install and remove packages +Manage packages on OpenWrt using ``apk` or `opkg`` depending on the release of OpenWrt. + + `packages` - install and remove packages + + `update` - update local copy of package information See https://openwrt.org/docs/guide-user/additional-software/apk and https://openwrt.org/docs/guide-user/additional-software/opkg -TBD - OpenWrt recommends against upgrading all packages thus there is no ``opkg.upgrade`` function +TBD - OpenWrt recommends against upgrading all packages thus there is no `opkg.upgrade` function -.. _version: https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk + .. note: As of [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) +OpenWrt uses [apk](../operations/apk.md) + +note: this does _not_ show up in the online documentation; the file header in __init__.py does. """ from pyinfra import host @@ -23,6 +26,17 @@ def update(): """ Update the local package information. + See [apk](../operations/apk.md) and [opkg](../operations/openwrt.md) for more details. + + **Examples:** + + .. code:: python + + from pyinfra.operations import openwrt + + # Ensure local package information is up to date + openwrt.update(name="Update the local package information") + """ if host.get_fact(OpenWrtHasFeature, OpenWrtFeature.USES_APK): yield from apk.update._inner() # noqa: SLF001 @@ -38,18 +52,14 @@ def packages( update: bool = False, ): """ - Add/remove/update packages using `opkg` or `apk` depending on the OpenWrt version. - - + packages: package or list of packages to that must/must not be present - + present: whether the package(s) should be installed (default True) or removed - + latest: whether to attempt to upgrade the specified package(s) (default False) - + update: run ``apk|opkg update`` before installing packages (default False) + Add/remove/update packages using ``apk`` or ``opkg`` depending on the OpenWrt release. - See TBD and TBD for more details. + + packages: package or list of packages to that must/must not be present (default ``True``). + + present: whether the package(s) should be installed or removed (default ``True``). + + latest: whether to attempt to upgrade the specified package(s) (default ``False``). + + update: run ``apk|opkg update`` before installing packages (default ``False``). - TBD - Not Supported: - Opkg does not support version pinning, i.e. ``=`` is not allowed - and will cause an exception. + See [apk](../operations/apk.md) and [opkg](../operations/openwrt.md) for more details. **Examples:** diff --git a/src/pyinfra/operations/opkg.py b/src/pyinfra/operations/opkg.py index 2c5af3dfc..ab4acb7b3 100644 --- a/src/pyinfra/operations/opkg.py +++ b/src/pyinfra/operations/opkg.py @@ -1,10 +1,10 @@ """ -This module is deprecated and will be removed in future version of pyinfra. -Use ``openwrt.opkg`` or ``openwrt.packages`` instead. +.. warning:: + This module is deprecated and will be removed in future version of pyinfra. + Use [openwrt.opkg](../operations/openwrt.md) or + [openwrt.packages](../operations/openwrt.md) instead. -Manage packages on OpenWrt using opkg - + ``update`` - update local copy of package information - + ``packages`` - install and remove packages +Manage packages on OpenWrt using ``opkg``. """ from pyinfra.api import operation @@ -20,6 +20,7 @@ def packages( ): """ Install, update or remove the specified packages. + See [openwrt.opkg.packages](../operations/openwrt.md) for details. """ yield from openwrt_packages._inner( # noqa: SLF001 packages=packages, present=present, latest=latest, update=update @@ -30,6 +31,7 @@ def packages( def update(): """ Update the local package information. + See [openwrt.opkg.update](../operations/openwrt.md) for details. """ yield from openwrt_update._inner() # noqa: SLF001 From a01838aaf3d687614ecfde2a645cd720d1a022b0 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 13:09:51 -0400 Subject: [PATCH 13/26] fix: sort operations by import sorting order in doc pages - i.e. will come before even though and sorts before something --- scripts/generate_operations_docs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate_operations_docs.py b/scripts/generate_operations_docs.py index 61299e502..61d0ac299 100755 --- a/scripts/generate_operations_docs.py +++ b/scripts/generate_operations_docs.py @@ -131,7 +131,7 @@ def build_operations_docs(): operation_functions = get_objects_from_module(module, isfunction, function_of_interest) - for name, func in operation_functions: + for name, func in sorted(operation_functions, key=lambda x : (x[0].count('.'), x[0])): decorated_func = getattr(func, "_inner", None) while decorated_func: func = decorated_func From a70815efb0461b3925fff8ad97f7d68832dfe6dd Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 13:11:55 -0400 Subject: [PATCH 14/26] refactor: change ALL to __ALL__ in package facts and operations __init__.py - it is for local use only --- src/pyinfra/facts/openwrt/__init__.py | 6 +++--- src/pyinfra/operations/freebsd/__init__.py | 6 +++--- src/pyinfra/operations/openwrt/__init__.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index 9453ff925..e14ed6e49 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -5,13 +5,13 @@ import importlib import sys -ALL = { +__ALL__ = { "OpenWrtFeature": "features.OpenWrtFeature", "OpenWrtHasFeature": "features.OpenWrtHasFeature", "opkg": "opkg", } -__all__ = list(ALL.keys()) +__all__ = list(__ALL__.keys()) def __getattr__(name): @@ -19,7 +19,7 @@ def __getattr__(name): # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: - pieces = ALL[name].split(".") + pieces = __ALL__[name].split(".") module = importlib.import_module(f".{pieces[0]}", package=__name__) if len(pieces) < 2: return module diff --git a/src/pyinfra/operations/freebsd/__init__.py b/src/pyinfra/operations/freebsd/__init__.py index 7013ddf7b..16cb92596 100644 --- a/src/pyinfra/operations/freebsd/__init__.py +++ b/src/pyinfra/operations/freebsd/__init__.py @@ -1,14 +1,14 @@ import importlib import sys -ALL = { +__ALL__ = { "freebsd_update": "freebsd_update", "pkg": "pkg", "service": "service", "sysrc": "sysrc", } -__all__ = list(ALL.keys()) +__all__ = list(__ALL__.keys()) def __getattr__(name): @@ -16,7 +16,7 @@ def __getattr__(name): # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: - pieces = ALL[name].split(".") + pieces = __ALL__[name].split(".") module = importlib.import_module(f".{pieces[0]}", package=__name__) if len(pieces) < 2: return module diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 539fb3bda..060eae145 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -5,13 +5,13 @@ import importlib import sys -ALL = { +__ALL__ = { "opkg": "opkg", "packages": "packages.packages", "update": "packages.update", } -__all__ = list(ALL.keys()) +__all__ = list(__ALL__.keys()) def __getattr__(name): @@ -19,7 +19,7 @@ def __getattr__(name): # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: - pieces = ALL[name].split(".") + pieces = __ALL__[name].split(".") module = importlib.import_module(f".{pieces[0]}", package=__name__) if len(pieces) < 2: return module From cc145a27463258daa91f94c027a519fef85ae151 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 14:19:06 -0400 Subject: [PATCH 15/26] chore: correct a couple of formatting issues. - escapes --- scripts/generate_operations_docs.py | 2 +- src/pyinfra/api/facts.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/generate_operations_docs.py b/scripts/generate_operations_docs.py index 61d0ac299..610b43c63 100755 --- a/scripts/generate_operations_docs.py +++ b/scripts/generate_operations_docs.py @@ -131,7 +131,7 @@ def build_operations_docs(): operation_functions = get_objects_from_module(module, isfunction, function_of_interest) - for name, func in sorted(operation_functions, key=lambda x : (x[0].count('.'), x[0])): + for name, func in sorted(operation_functions, key=lambda x: (x[0].count("."), x[0])): decorated_func = getattr(func, "_inner", None) while decorated_func: func = decorated_func diff --git a/src/pyinfra/api/facts.py b/src/pyinfra/api/facts.py index 35592b558..45f4a92f0 100644 --- a/src/pyinfra/api/facts.py +++ b/src/pyinfra/api/facts.py @@ -53,7 +53,8 @@ T = TypeVar("T") -already_logged_as_deprecated = set() # used to ensure only one warning per deprecated fact +already_logged_as_deprecated = set() # used to ensure only one warning per deprecated fact + class FactBase(Generic[T]): name: str @@ -66,7 +67,7 @@ class FactBase(Generic[T]): is_deprecated = False - deprecated_for: str|None = None + deprecated_for: str | None = None def requires_command(self, *args, **kwargs) -> str | None: """Return the binary name that must exist on the remote host for this fact to run. @@ -124,7 +125,7 @@ class ShortFactBase(Generic[T]): name: str fact: type[FactBase] is_deprecated = False - deprecated_for: str|None = None + deprecated_for: str | None = None @override def __init_subclass__(cls) -> None: From 082a2c8c0ba880d1b5d128173d4673782d15c3f8 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 14:37:13 -0400 Subject: [PATCH 16/26] chore: remove line in test fixture that causes a spelling error --- tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json index 024d34766..eb3fbd8fe 100644 --- a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json +++ b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json @@ -4,7 +4,6 @@ "output": [ "urandom-seed - 1.0-1", "urngd - 2020-01-21-c7f7b6b6-1", - "usign - 2019-08-06-5a52b379-1", "wget - 1.20.3-4", "wget-nossl - 1.20.3-4", "wireless-regdb - 2019.06.03", @@ -14,7 +13,6 @@ "fact": { "urandom-seed": ["1.0-1"], "urngd": ["2020-01-21-c7f7b6b6-1"], - "usign": ["2019-08-06-5a52b379-1"], "wget": ["1.20.3-4"], "wget-nossl": ["1.20.3-4"], "wireless-regdb": ["2019.06.03"], From 3ffebda7d1c3563827e2804ba79bc487feb34d86 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Sun, 21 Jun 2026 14:37:38 -0400 Subject: [PATCH 17/26] refactor: correct typing for default method of OpenWrtHasFeature --- src/pyinfra/facts/openwrt/features.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pyinfra/facts/openwrt/features.py b/src/pyinfra/facts/openwrt/features.py index af9c724f9..e22a5472d 100644 --- a/src/pyinfra/facts/openwrt/features.py +++ b/src/pyinfra/facts/openwrt/features.py @@ -105,7 +105,8 @@ def command(self, feature: OpenWrtFeature) -> str: return f"echo {feature.value} && cat {THE_FILE}" @override - def default(self) -> bool: + @staticmethod + def default() -> bool: return False @override From 686e6a0e1f8302068b107410c85c34e17ea1c3fa Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 10:12:32 -0400 Subject: [PATCH 18/26] fix: correct has_dsa_is_false_for_19_07 test - correct first line of output to be has_dsa (not copy paste is_apk) --- .../has_dsa_is_false_for_19_07.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json index d1d65edcd..341f3b788 100644 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json @@ -2,7 +2,7 @@ "arg": ["has_dsa"], "command": "echo has_dsa && cat /etc/openwrt_release", "output": [ - "uses_apk", + "has_dsa", "DISTRIB_ID='OpenWrt'", "DISTRIB_RELEASE='19.07.2'", "DISTRIB_REVISION='r10947-65030d81f3'", From c0f449b8bab616c3e9d9640e7c5ec9a83c87cc95 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 10:17:09 -0400 Subject: [PATCH 19/26] refactor: restore freebsd operations __init__.py - change back to previous approach. --- src/pyinfra/operations/freebsd/__init__.py | 28 +++++++--------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/pyinfra/operations/freebsd/__init__.py b/src/pyinfra/operations/freebsd/__init__.py index 16cb92596..fe5f43353 100644 --- a/src/pyinfra/operations/freebsd/__init__.py +++ b/src/pyinfra/operations/freebsd/__init__.py @@ -1,27 +1,17 @@ import importlib -import sys +from glob import glob +from os import path -__ALL__ = { - "freebsd_update": "freebsd_update", - "pkg": "pkg", - "service": "service", - "sysrc": "sysrc", -} - -__all__ = list(__ALL__.keys()) +_module_filenames = glob(path.join(path.dirname(__file__), "*.py")) +__all__ = sorted( + set(path.basename(name)[:-3] for name in _module_filenames if not name.endswith("__init__.py")) +) def __getattr__(name): - # On-demand import of OpenWrt facts, so we don't have to import them all at once + # On-demand import of operations modules, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL - # Also, pyinfra is py3.11>=, so this is not a breaking change. + # Also, Pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: - pieces = __ALL__[name].split(".") - module = importlib.import_module(f".{pieces[0]}", package=__name__) - if len(pieces) < 2: - return module - del sys.modules[module.__name__] - del sys.modules[__name__].__dict__[pieces[0]] - return getattr(module, pieces[1]) - + return importlib.import_module(f".{name}", __package__) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") From a3531387ae1817212b78cc8df1192503ac902a45 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 10:38:19 -0400 Subject: [PATCH 20/26] chore: add test fixtures for openwrt.update - both apk an opkg cases --- tests/operations/openwrt.update/apk_update.json | 7 +++++++ tests/operations/openwrt.update/opkg_update.json | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 tests/operations/openwrt.update/apk_update.json create mode 100644 tests/operations/openwrt.update/opkg_update.json diff --git a/tests/operations/openwrt.update/apk_update.json b/tests/operations/openwrt.update/apk_update.json new file mode 100644 index 000000000..87d90b12e --- /dev/null +++ b/tests/operations/openwrt.update/apk_update.json @@ -0,0 +1,7 @@ +{ + "args": [], + "facts": { + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} + }, + "commands": ["apk update"] +} diff --git a/tests/operations/openwrt.update/opkg_update.json b/tests/operations/openwrt.update/opkg_update.json new file mode 100644 index 000000000..0bc773b07 --- /dev/null +++ b/tests/operations/openwrt.update/opkg_update.json @@ -0,0 +1,7 @@ +{ + "args": [], + "facts": { + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} + }, + "commands": ["opkg update"] +} From 8c93dc233025ff09840b46b8a2fdc063cfe19fba Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 10:52:32 -0400 Subject: [PATCH 21/26] fix: correct __getattr__ docstring for openwrt operations __init__.py - operations not facts --- src/pyinfra/operations/openwrt/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 060eae145..4e8dd0ee1 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -15,7 +15,7 @@ def __getattr__(name): - # On-demand import of OpenWrt facts, so we don't have to import them all at once + # On-demand import of OpenWrt operations, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. if name in __all__: From a0fffda7b22ad907044a994ad180d7b2e87ff1dd Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 15:51:45 -0400 Subject: [PATCH 22/26] fix: correct packages param to latest idiom - use | None = None for both openwrt.packages and opkg.packages - adjust tests accordingly --- src/pyinfra/operations/openwrt/opkg.py | 8 +++----- src/pyinfra/operations/openwrt/packages.py | 7 ++++++- src/pyinfra/operations/opkg.py | 2 +- .../list_of_nulls_package_list.json | 4 ++-- .../openwrt.packages/apk_add_null_packages.json | 10 ++++++++++ .../operations/openwrt.packages/apk_add_packages.json | 6 +++--- .../openwrt.packages/apk_add_str_package.json | 8 ++++++++ .../openwrt.packages/opkg_add_null_packages.json | 9 +++++++++ ...pkg_add_one_package.json => opkg_add_packages.json} | 6 +++--- .../openwrt.packages/opkg_add_str_package.json | 8 ++++++++ 10 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 tests/operations/openwrt.packages/apk_add_null_packages.json create mode 100644 tests/operations/openwrt.packages/apk_add_str_package.json create mode 100644 tests/operations/openwrt.packages/opkg_add_null_packages.json rename tests/operations/openwrt.packages/{opkg_add_one_package.json => opkg_add_packages.json} (62%) create mode 100644 tests/operations/openwrt.packages/opkg_add_str_package.json diff --git a/src/pyinfra/operations/openwrt/opkg.py b/src/pyinfra/operations/openwrt/opkg.py index f032e542b..6b24f5065 100644 --- a/src/pyinfra/operations/openwrt/opkg.py +++ b/src/pyinfra/operations/openwrt/opkg.py @@ -37,7 +37,7 @@ def update(): @operation() def packages( - packages: str | list[str] = "", + packages: str | list[str] | None = None, present: bool = True, latest: bool = False, update: bool = True, @@ -74,13 +74,11 @@ def packages( as of OpenWrt [Release 25.12](https://openwrt.org/releases/25.12/notes-25.12.0#switch_package_manager_from_opkg_to_apk) OpenWrt uses [apk](../operations/apk.md) """ - if str(packages) == "" or ( - isinstance(packages, list) and (len(packages) < 1 or all(len(p) < 1 for p in packages)) - ): + pkg_list = [packages] if isinstance(packages, str) else (packages or []) + if (len(pkg_list) < 1) or any((len(p) < 1) or (p is None) for p in pkg_list): host.noop("empty or invalid package list provided to openwrt.opkg.packages") return - pkg_list = packages if isinstance(packages, list) else [packages] have_equals = ",".join([pkg.split(EQUALS)[0] for pkg in pkg_list if EQUALS in pkg]) if len(have_equals) > 0: raise ValueError(f"opkg does not support version pinning but found for: '{have_equals}'") diff --git a/src/pyinfra/operations/openwrt/packages.py b/src/pyinfra/operations/openwrt/packages.py index e21947f71..d994d3768 100644 --- a/src/pyinfra/operations/openwrt/packages.py +++ b/src/pyinfra/operations/openwrt/packages.py @@ -46,7 +46,7 @@ def update(): @operation() def packages( - packages: str | list[str] = "", + packages: str | list[str] | None = None, present: bool = True, latest: bool = False, update: bool = False, @@ -76,6 +76,11 @@ def packages( name="Ensure we have the latest version of Vim" ) """ + packages = [packages] if isinstance(packages, str) else (packages or []) + if (len(packages) < 1) or any((len(p) < 1) or (p is None) for p in packages): + host.noop("empty package list provided to openwrt.packages") + return + if host.get_fact(OpenWrtHasFeature, feature=OpenWrtFeature.USES_APK): yield from apk.packages._inner( # noqa: SLF001 packages=packages, latest=latest, update=update, present=present diff --git a/src/pyinfra/operations/opkg.py b/src/pyinfra/operations/opkg.py index ab4acb7b3..9f0c8374d 100644 --- a/src/pyinfra/operations/opkg.py +++ b/src/pyinfra/operations/opkg.py @@ -13,7 +13,7 @@ @operation(is_deprecated=True, deprecated_for="openwrt.opkg.packages or openwrt.packages") def packages( - packages: str | list[str] = "", + packages: str | list[str] | None = None, present: bool = True, latest: bool = False, update: bool = True, diff --git a/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json index 28aa77002..73bbd8365 100644 --- a/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json +++ b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json @@ -1,7 +1,7 @@ { - "args": ["", "", ""], + "args": [["", "", ""]], "facts": {"opkg.OpkgPackages": {}}, "commands": [], "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", - "logs": "foo", + "logs": "foo" } diff --git a/tests/operations/openwrt.packages/apk_add_null_packages.json b/tests/operations/openwrt.packages/apk_add_null_packages.json new file mode 100644 index 000000000..d2b8c6e9e --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_null_packages.json @@ -0,0 +1,10 @@ +{ + "args": [null], + "facts": { + "apk.ApkPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} + }, + "commands": [], + "noop_description": "empty package list provided to openwrt.packages" + +} diff --git a/tests/operations/openwrt.packages/apk_add_packages.json b/tests/operations/openwrt.packages/apk_add_packages.json index 19874951f..0144d8adc 100644 --- a/tests/operations/openwrt.packages/apk_add_packages.json +++ b/tests/operations/openwrt.packages/apk_add_packages.json @@ -1,8 +1,8 @@ { - "args": ["curl"], + "args": [["curl", "wget"]], "facts": { "apk.ApkPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} }, - "commands": ["apk add curl"], + "commands": ["apk add curl wget"] } diff --git a/tests/operations/openwrt.packages/apk_add_str_package.json b/tests/operations/openwrt.packages/apk_add_str_package.json new file mode 100644 index 000000000..dc221c367 --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_str_package.json @@ -0,0 +1,8 @@ +{ + "args": ["curl"], + "facts": { + "apk.ApkPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} + }, + "commands": ["apk add curl"] +} diff --git a/tests/operations/openwrt.packages/opkg_add_null_packages.json b/tests/operations/openwrt.packages/opkg_add_null_packages.json new file mode 100644 index 000000000..20c24a86d --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_null_packages.json @@ -0,0 +1,9 @@ +{ + "args": [null], + "facts": { + "opkg.OpkgPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} + }, + "commands": [], + "noop_description": "empty package list provided to openwrt.packages" +} diff --git a/tests/operations/openwrt.packages/opkg_add_one_package.json b/tests/operations/openwrt.packages/opkg_add_packages.json similarity index 62% rename from tests/operations/openwrt.packages/opkg_add_one_package.json rename to tests/operations/openwrt.packages/opkg_add_packages.json index aa45759cc..54efcd1c5 100644 --- a/tests/operations/openwrt.packages/opkg_add_one_package.json +++ b/tests/operations/openwrt.packages/opkg_add_packages.json @@ -1,9 +1,9 @@ { - "args": ["curl"], + "args": [["curl", "wget"]], "kwargs": {"update": false}, "facts": { "opkg.OpkgPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} }, - "commands": ["opkg install curl"], + "commands": ["opkg install curl wget"] } diff --git a/tests/operations/openwrt.packages/opkg_add_str_package.json b/tests/operations/openwrt.packages/opkg_add_str_package.json new file mode 100644 index 000000000..db3ec0d3f --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_str_package.json @@ -0,0 +1,8 @@ +{ + "args": ["curl"], + "facts": { + "opkg.OpkgPackages": {}, + "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} + }, + "commands": ["opkg install curl"] +} From a5207ec640549a6331b10e42c1090befa110676f Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 16:34:09 -0400 Subject: [PATCH 23/26] chore: improve typing - rework opkg (older code) - parametrize FactBase - set output type to list[str] - set return types for process --- src/pyinfra/facts/openwrt/__init__.py | 2 +- src/pyinfra/facts/openwrt/opkg.py | 58 +++++++++++++++++---------- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index e14ed6e49..eddf878e1 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -14,7 +14,7 @@ __all__ = list(__ALL__.keys()) -def __getattr__(name): +def __getattr__(name: str) -> object: # On-demand import of OpenWrt facts, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. diff --git a/src/pyinfra/facts/openwrt/opkg.py b/src/pyinfra/facts/openwrt/opkg.py index d08e67e6a..7cd0933dd 100644 --- a/src/pyinfra/facts/openwrt/opkg.py +++ b/src/pyinfra/facts/openwrt/opkg.py @@ -15,6 +15,8 @@ and thus the note above is repeated in each fact. """ +from __future__ import annotations + import re from typing import NamedTuple @@ -22,13 +24,16 @@ from pyinfra import logger from pyinfra.api import FactBase -from pyinfra.facts.util.packaging import parse_packages +from pyinfra.facts.util.packaging import PackageVersionDict, parse_packages + +OpkgArchInstallInfo = dict[str, int] class OpkgPkgUpgradeInfo(NamedTuple): installed: str available: str +OpkgPkgUpgradeMap = dict[str, OpkgPkgUpgradeInfo] class OpkgConfInfo(NamedTuple): paths: dict[str, str] # list of paths, e.g. {'root':'/', 'ram':'/tmp} @@ -42,14 +47,16 @@ class OpkgFeedInfo(NamedTuple): fmt: str # format of the feed, e.g. "src/gz" kind: str # whether it comes from the 'distribution' or is 'custom' +OpkgFeedMap = dict[str, OpkgFeedInfo] + -class OpkgConf(FactBase): +class OpkgConf(FactBase[OpkgConfInfo]): """ Returns a ``NamedTuple`` with the current ``opkg`` configuration: .. code:: python - ConfInfo( + OpkgConfInfo( paths = { "root": "/", "ram": "/tmp", @@ -91,7 +98,7 @@ def requires_command(self) -> str: @override @staticmethod - def default(): + def default() -> OpkgConfInfo: return OpkgConfInfo({}, "", {}, {}) @override @@ -99,7 +106,7 @@ def command(self) -> str: return "cat /etc/opkg.conf" @override - def process(self, output): + def process(self, output: list[str]) -> OpkgConfInfo: dest, lists_dir, options, arch_cfg = {}, "", {}, {} for line in output: match = self.regex.match(line) @@ -118,7 +125,7 @@ def process(self, output): return OpkgConfInfo(dest, lists_dir, options, arch_cfg) -class OpkgFeeds(FactBase): +class OpkgFeeds(FactBase[OpkgFeedMap]): """ Returns a dictionary containing the information for the distribution-provided and custom `opkg` feeds: @@ -126,12 +133,12 @@ class OpkgFeeds(FactBase): .. code:: python { - 'openwrt_base': FeedInfo(url='http://downloads ... /i386_pentium/base', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_core': FeedInfo(url='http://downloads ... /x86/geode/packages', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_luci': FeedInfo(url='http://downloads ... /i386_pentium/luci', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_packages': FeedInfo(url='http://downloads ... /i386_pentium/packages', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_routing': FeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'), # noqa: E501 - 'openwrt_telephony': FeedInfo(url='http://downloads ... /i386_pentium/telephony', fmt='src/gz', kind='distribution') # noqa: E501 + 'openwrt_base': OpkgFeedInfo(url='http://downloads ... /i386_pentium/base', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_core': OpkgFeedInfo(url='http://downloads ... /x86/geode/packages', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_luci': OpkgFeedInfo(url='http://downloads ... /i386_pentium/luci', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_packages': OpkgFeedInfo(url='http://downloads ... /i386_pentium/packages', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_routing': OpkgFeedInfo(url='http://downloads ... /i386_pentium/routing', fmt='src/gz', kind='distribution'), # noqa: E501 + 'openwrt_telephony': OpkgFeedInfo(url='http://downloads ... /i386_pentium/telephony', fmt='src/gz', kind='distribution') # noqa: E501 } .. note:: @@ -142,7 +149,11 @@ class OpkgFeeds(FactBase): regex = re.compile( r"^(CUSTOM)|(?:\s*(?P[\w/]+)\s+(?P[\w]+)\s+(?P[\w./:]+))?(?:\s*#.*)?$" ) - default = dict + + @override + @staticmethod + def default() -> OpkgFeedMap: + return OpkgFeedMap({}) @override def requires_command(self) -> str: @@ -153,7 +164,7 @@ def command(self) -> str: return "cat /etc/opkg/distfeeds.conf; echo CUSTOM; cat /etc/opkg/customfeeds.conf" @override - def process(self, output): + def process(self, output:list[str]) -> OpkgFeedMap: feeds, kind = {}, "distribution" for line in output: match = self.regex.match(line) @@ -170,7 +181,7 @@ def process(self, output): return feeds -class OpkgInstallableArchitectures(FactBase): +class OpkgInstallableArchitectures(FactBase[OpkgArchInstallInfo]): """ Returns a dictionary containing the currently installable architectures for this system along with their priority: @@ -200,7 +211,7 @@ def command(self) -> str: return "opkg print-architecture" @override - def process(self, output): + def process(self, output: list[str]) -> OpkgArchInstallInfo: arch_list = {} for line in output: match = self.regex.match(line) @@ -213,7 +224,7 @@ def process(self, output): return arch_list -class OpkgPackages(FactBase): +class OpkgPackages(FactBase[PackageVersionDict]): """ Returns a dictionary of installed `opkg` packages: @@ -241,11 +252,11 @@ def command(self) -> str: return "opkg list-installed" @override - def process(self, output): + def process(self, output: list[str]) -> PackageVersionDict: return parse_packages(self.regex, sorted(output)) -class OpkgUpgradeablePackages(FactBase): +class OpkgUpgradeablePackages(FactBase[OpkgPkgUpgradeMap]): """ Returns a dict of installed and upgradable `opkg` packages: @@ -262,8 +273,11 @@ class OpkgUpgradeablePackages(FactBase): """ regex = re.compile(r"^([a-zA-Z0-9][\w\-.]*)\s-\s([\w\-.]+)\s-\s([\w\-.]+)") - default = dict - use_default_on_error = True + + @override + @staticmethod + def default() -> OpkgPkgUpgradeMap: + return OpkgPkgUpgradeMap({}) @override def requires_command(self) -> str: @@ -274,7 +288,7 @@ def command(self) -> str: return "opkg list-upgradable" # yes, really spelled that way @override - def process(self, output): + def process(self, output: list[str]) -> OpkgPkgUpgradeMap: result = {} for line in output: match = self.regex.match(line) From b1433ac0c7ddf988971be5f0bc487214f47ff562 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 16:55:06 -0400 Subject: [PATCH 24/26] chore: convert tests from json to yaml - both openwrt and opkg --- repros/brew-fixes-2026-02-repro.sh | 5 ++ .../has_dsa_is_false_for_19_07.json | 15 ------ .../has_dsa_is_false_for_19_07.yaml | 13 +++++ .../missing_distrib_release.json | 14 ----- .../missing_distrib_release.yaml | 12 +++++ .../no_input.json | 6 --- .../no_input.yaml | 5 ++ .../no_patch_works.json | 15 ------ .../no_patch_works.yaml | 13 +++++ .../release_bad_major.json | 15 ------ .../release_bad_major.yaml | 13 +++++ .../release_bad_minor.json | 15 ------ .../release_bad_minor.yaml | 13 +++++ .../release_empty.json | 15 ------ .../release_empty.yaml | 13 +++++ .../release_only_1_piece.json | 15 ------ .../release_only_1_piece.yaml | 13 +++++ .../uses_apk_is_true_for_26_4.json | 15 ------ .../uses_apk_is_true_for_26_4.yaml | 13 +++++ .../openwrt.opkg.OpkgConf/opkg_conf.json | 36 ------------- .../openwrt.opkg.OpkgConf/opkg_conf.yaml | 34 ++++++++++++ .../openwrt.opkg.OpkgFeeds/opkg_feeds.json | 52 ------------------- .../openwrt.opkg.OpkgFeeds/opkg_feeds.yaml | 42 +++++++++++++++ .../opkg_installable_architectures.json | 16 ------ .../opkg_installable_architectures.yaml | 16 ++++++ .../opkg_packages.json | 22 -------- .../opkg_packages.yaml | 25 +++++++++ .../opkg_upgradeable_packages.json | 20 ------- .../opkg_upgradeable_packages.yaml | 26 ++++++++++ .../add_existing_package.json | 7 --- .../add_existing_package.yaml | 10 ++++ .../add_multiple_packages.json | 6 --- .../add_multiple_packages.yaml | 9 ++++ .../add_one_package.json | 6 --- .../add_one_package.yaml | 8 +++ .../add_with_unallowed_pinning.json | 9 ---- .../add_with_unallowed_pinning.yaml | 8 +++ .../list_of_nulls_package_list.json | 7 --- .../list_of_nulls_package_list.yaml | 9 ++++ .../null_package_list.json | 7 --- .../null_package_list.yaml | 6 +++ .../remove_existing_package.json | 6 --- .../remove_existing_package.yaml | 11 ++++ ...e_existing_package_and_require_update.json | 6 --- ...e_existing_package_and_require_update.yaml | 12 +++++ .../remove_not_existing_package.json | 7 --- .../remove_not_existing_package.yaml | 9 ++++ .../update_then_add_one.json | 6 --- .../update_then_add_one.yaml | 9 ++++ .../openwrt.opkg.update/first_update.json | 1 - .../openwrt.opkg.update/first_update.yaml | 5 ++ .../apk_add_null_packages.json | 10 ---- .../apk_add_null_packages.yaml | 8 +++ .../openwrt.packages/apk_add_packages.json | 8 --- .../openwrt.packages/apk_add_packages.yaml | 9 ++++ .../openwrt.packages/apk_add_str_package.json | 8 --- .../openwrt.packages/apk_add_str_package.yaml | 8 +++ .../openwrt.packages/apk_remove_packages.json | 9 ---- .../openwrt.packages/apk_remove_packages.yaml | 12 +++++ .../opkg_add_existing_package.json | 10 ---- .../opkg_add_existing_package.yaml | 12 +++++ .../opkg_add_null_packages.json | 9 ---- .../opkg_add_null_packages.yaml | 8 +++ .../openwrt.packages/opkg_add_packages.json | 9 ---- .../openwrt.packages/opkg_add_packages.yaml | 11 ++++ .../opkg_add_str_package.json | 8 --- .../opkg_add_str_package.yaml | 8 +++ .../opkg_remove_existing_package.json | 9 ---- .../opkg_remove_existing_package.yaml | 13 +++++ .../operations/openwrt.update/apk_update.json | 7 --- .../operations/openwrt.update/apk_update.yaml | 6 +++ .../openwrt.update/opkg_update.json | 7 --- .../openwrt.update/opkg_update.yaml | 6 +++ .../opkg.packages/add_existing_package.json | 7 --- .../opkg.packages/add_existing_package.yaml | 10 ++++ .../opkg.packages/add_multiple_packages.json | 6 --- .../opkg.packages/add_multiple_packages.yaml | 9 ++++ .../opkg.packages/add_one_package.json | 6 --- .../opkg.packages/add_one_package.yaml | 8 +++ .../add_with_unallowed_pinning.json | 9 ---- .../add_with_unallowed_pinning.yaml | 8 +++ .../list_of_nulls_package_list.json | 7 --- .../list_of_nulls_package_list.yaml | 9 ++++ .../opkg.packages/null_package_list.json | 7 --- .../opkg.packages/null_package_list.yaml | 6 +++ .../remove_existing_package.json | 6 --- .../remove_existing_package.yaml | 11 ++++ ...e_existing_package_and_require_update.json | 6 --- ...e_existing_package_and_require_update.yaml | 12 +++++ .../remove_not_existing_package.json | 7 --- .../remove_not_existing_package.yaml | 9 ++++ .../opkg.packages/update_then_add_one.json | 6 --- .../opkg.packages/update_then_add_one.yaml | 9 ++++ .../operations/opkg.update/first_update.json | 1 - .../operations/opkg.update/first_update.yaml | 5 ++ 95 files changed, 549 insertions(+), 501 deletions(-) create mode 100755 repros/brew-fixes-2026-02-repro.sh delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_input.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.yaml delete mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json create mode 100644 tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.yaml delete mode 100644 tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json create mode 100644 tests/facts/openwrt.opkg.OpkgConf/opkg_conf.yaml delete mode 100644 tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json create mode 100644 tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.yaml delete mode 100644 tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json create mode 100644 tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.yaml delete mode 100644 tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json create mode 100644 tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.yaml delete mode 100644 tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json create mode 100644 tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/add_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/add_existing_package.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/add_multiple_packages.json create mode 100644 tests/operations/openwrt.opkg.packages/add_multiple_packages.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/add_one_package.json create mode 100644 tests/operations/openwrt.opkg.packages/add_one_package.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json create mode 100644 tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json create mode 100644 tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/null_package_list.json create mode 100644 tests/operations/openwrt.opkg.packages/null_package_list.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json create mode 100644 tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/remove_not_existing_package.json create mode 100644 tests/operations/openwrt.opkg.packages/remove_not_existing_package.yaml delete mode 100644 tests/operations/openwrt.opkg.packages/update_then_add_one.json create mode 100644 tests/operations/openwrt.opkg.packages/update_then_add_one.yaml delete mode 100644 tests/operations/openwrt.opkg.update/first_update.json create mode 100644 tests/operations/openwrt.opkg.update/first_update.yaml delete mode 100644 tests/operations/openwrt.packages/apk_add_null_packages.json create mode 100644 tests/operations/openwrt.packages/apk_add_null_packages.yaml delete mode 100644 tests/operations/openwrt.packages/apk_add_packages.json create mode 100644 tests/operations/openwrt.packages/apk_add_packages.yaml delete mode 100644 tests/operations/openwrt.packages/apk_add_str_package.json create mode 100644 tests/operations/openwrt.packages/apk_add_str_package.yaml delete mode 100644 tests/operations/openwrt.packages/apk_remove_packages.json create mode 100644 tests/operations/openwrt.packages/apk_remove_packages.yaml delete mode 100644 tests/operations/openwrt.packages/opkg_add_existing_package.json create mode 100644 tests/operations/openwrt.packages/opkg_add_existing_package.yaml delete mode 100644 tests/operations/openwrt.packages/opkg_add_null_packages.json create mode 100644 tests/operations/openwrt.packages/opkg_add_null_packages.yaml delete mode 100644 tests/operations/openwrt.packages/opkg_add_packages.json create mode 100644 tests/operations/openwrt.packages/opkg_add_packages.yaml delete mode 100644 tests/operations/openwrt.packages/opkg_add_str_package.json create mode 100644 tests/operations/openwrt.packages/opkg_add_str_package.yaml delete mode 100644 tests/operations/openwrt.packages/opkg_remove_existing_package.json create mode 100644 tests/operations/openwrt.packages/opkg_remove_existing_package.yaml delete mode 100644 tests/operations/openwrt.update/apk_update.json create mode 100644 tests/operations/openwrt.update/apk_update.yaml delete mode 100644 tests/operations/openwrt.update/opkg_update.json create mode 100644 tests/operations/openwrt.update/opkg_update.yaml delete mode 100644 tests/operations/opkg.packages/add_existing_package.json create mode 100644 tests/operations/opkg.packages/add_existing_package.yaml delete mode 100644 tests/operations/opkg.packages/add_multiple_packages.json create mode 100644 tests/operations/opkg.packages/add_multiple_packages.yaml delete mode 100644 tests/operations/opkg.packages/add_one_package.json create mode 100644 tests/operations/opkg.packages/add_one_package.yaml delete mode 100644 tests/operations/opkg.packages/add_with_unallowed_pinning.json create mode 100644 tests/operations/opkg.packages/add_with_unallowed_pinning.yaml delete mode 100644 tests/operations/opkg.packages/list_of_nulls_package_list.json create mode 100644 tests/operations/opkg.packages/list_of_nulls_package_list.yaml delete mode 100644 tests/operations/opkg.packages/null_package_list.json create mode 100644 tests/operations/opkg.packages/null_package_list.yaml delete mode 100644 tests/operations/opkg.packages/remove_existing_package.json create mode 100644 tests/operations/opkg.packages/remove_existing_package.yaml delete mode 100644 tests/operations/opkg.packages/remove_existing_package_and_require_update.json create mode 100644 tests/operations/opkg.packages/remove_existing_package_and_require_update.yaml delete mode 100644 tests/operations/opkg.packages/remove_not_existing_package.json create mode 100644 tests/operations/opkg.packages/remove_not_existing_package.yaml delete mode 100644 tests/operations/opkg.packages/update_then_add_one.json create mode 100644 tests/operations/opkg.packages/update_then_add_one.yaml delete mode 100644 tests/operations/opkg.update/first_update.json create mode 100644 tests/operations/opkg.update/first_update.yaml diff --git a/repros/brew-fixes-2026-02-repro.sh b/repros/brew-fixes-2026-02-repro.sh new file mode 100755 index 000000000..159497779 --- /dev/null +++ b/repros/brew-fixes-2026-02-repro.sh @@ -0,0 +1,5 @@ +#! /bin/bash +pyinfra --version +brew list --versions | grep openssl +pyinfra @local fact brew.BrewPackages |& grep openssl -A 3 +pyinfra --dry @local brew.packages openssl@3.6.1 diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json deleted file mode 100644 index 341f3b788..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["has_dsa"], - "command": "echo has_dsa && cat /etc/openwrt_release", - "output": [ - "has_dsa", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='19.07.2'", - "DISTRIB_REVISION='r10947-65030d81f3'", - "DISTRIB_TARGET='x86/geode'", - "DISTRIB_ARCH='i386_pentium'", - "DISTRIB_DESCRIPTION='OpenWrt 19.07.2 r10947-65030d81f3'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.yaml new file mode 100644 index 000000000..265668dd6 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/has_dsa_is_false_for_19_07.yaml @@ -0,0 +1,13 @@ +arg: + - has_dsa +command: echo has_dsa && cat /etc/openwrt_release +output: + - has_dsa + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='19.07.2' + - DISTRIB_REVISION='r10947-65030d81f3' + - DISTRIB_TARGET='x86/geode' + - DISTRIB_ARCH='i386_pentium' + - DISTRIB_DESCRIPTION='OpenWrt 19.07.2 r10947-65030d81f3' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json b/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json deleted file mode 100644 index b23fedd81..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.yaml new file mode 100644 index 000000000..4715da119 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/missing_distrib_release.yaml @@ -0,0 +1,12 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json b/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json deleted file mode 100644 index 758350d16..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.yaml new file mode 100644 index 000000000..2187a2a3d --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/no_input.yaml @@ -0,0 +1,5 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: [] +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json b/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json deleted file mode 100644 index 50d13ac62..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='25.12'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": true -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.yaml new file mode 100644 index 000000000..4b16c7402 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/no_patch_works.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='25.12' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: true diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json deleted file mode 100644 index 058445f9f..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='A.12.4'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.yaml new file mode 100644 index 000000000..d45e35aa6 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_major.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='A.12.4' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json deleted file mode 100644 index 0cb0cbf08..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='25.B.4'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.yaml new file mode 100644 index 000000000..9dc20d1c9 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_bad_minor.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='25.B.4' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json deleted file mode 100644 index bf372b4e6..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE=''", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.yaml new file mode 100644 index 000000000..df188a20d --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_empty.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json b/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json deleted file mode 100644 index abd775c88..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='25'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": false -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.yaml new file mode 100644 index 000000000..66bb53317 --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/release_only_1_piece.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='25' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: false diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json b/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json deleted file mode 100644 index 92be58194..000000000 --- a/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "arg": ["uses_apk"], - "command": "echo uses_apk && cat /etc/openwrt_release", - "output": [ - "uses_apk", - "DISTRIB_ID='OpenWrt'", - "DISTRIB_RELEASE='25.12.4'", - "DISTRIB_REVISION='r32933-4ccb782af7'", - "DISTRIB_TARGET='rockchip/armv8'", - "DISTRIB_ARCH='aarch64_generic'", - "DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7'", - "DISTRIB_TAINTS=''" - ], - "fact": true -} diff --git a/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.yaml b/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.yaml new file mode 100644 index 000000000..a14d085ca --- /dev/null +++ b/tests/facts/openwrt.features.OpenWrtHasFeature/uses_apk_is_true_for_26_4.yaml @@ -0,0 +1,13 @@ +arg: + - uses_apk +command: echo uses_apk && cat /etc/openwrt_release +output: + - uses_apk + - DISTRIB_ID='OpenWrt' + - DISTRIB_RELEASE='25.12.4' + - DISTRIB_REVISION='r32933-4ccb782af7' + - DISTRIB_TARGET='rockchip/armv8' + - DISTRIB_ARCH='aarch64_generic' + - DISTRIB_DESCRIPTION='OpenWrt 25.12.4 r32933-4ccb782af7' + - DISTRIB_TAINTS='' +fact: true diff --git a/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json b/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json deleted file mode 100644 index b6fc4db2e..000000000 --- a/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "command": "cat /etc/opkg.conf", - "requires_command": "opkg", - "output": [ - "", - "# a comment", - " # another comment", - "extra", - "dest root /", - "dest ram /tmp", - "lists_dir ext /var/opkg-lists", - "list_dir ext /var/foo/bar", - "list_dir zap /var/foo/bar", - "option", - "option overlay_root /overlay", - "option check_signature", - "option http_proxy http://username:password@proxy.example.org:8080/", - "option ftp_proxy http://username:password@proxy.example.org:2121/", - "arch all 1", - "arch noarch 2", - "arch brcm4716 200", - "arch brcm47xx 300 # generic has lower priority than specific", - "arch zzz", - ], - "fact": [ - {"root": "/", "ram": "/tmp"}, - "/var/opkg-lists", - { - "overlay_root": "/overlay", - "check_signature": true, - "http_proxy": "http://username:password@proxy.example.org:8080/", - "ftp_proxy": "http://username:password@proxy.example.org:2121/", - }, - {"all": 1, "noarch": 2, "brcm4716": 200, "brcm47xx": 300}, - ], -} diff --git a/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.yaml b/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.yaml new file mode 100644 index 000000000..2209ebf2e --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgConf/opkg_conf.yaml @@ -0,0 +1,34 @@ +command: cat /etc/opkg.conf +requires_command: opkg +output: + - "" + - '# a comment' + - ' # another comment' + - extra + - dest root / + - dest ram /tmp + - lists_dir ext /var/opkg-lists + - list_dir ext /var/foo/bar + - list_dir zap /var/foo/bar + - option + - option overlay_root /overlay + - option check_signature + - option http_proxy http://username:password@proxy.example.org:8080/ + - option ftp_proxy http://username:password@proxy.example.org:2121/ + - arch all 1 + - arch noarch 2 + - arch brcm4716 200 + - 'arch brcm47xx 300 # generic has lower priority than specific' + - arch zzz +fact: + - root: / + ram: /tmp + - /var/opkg-lists + - overlay_root: /overlay + check_signature: true + http_proxy: http://username:password@proxy.example.org:8080/ + ftp_proxy: http://username:password@proxy.example.org:2121/ + - all: 1 + noarch: 2 + brcm4716: 200 + brcm47xx: 300 diff --git a/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json b/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json deleted file mode 100644 index b85402128..000000000 --- a/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "command": "cat /etc/opkg/distfeeds.conf; echo CUSTOM; cat /etc/opkg/customfeeds.conf", - "requires_command": "opkg", - "output": [ - "", - " # a different comment", - "aaa", - "src/gz", - "src/gz openwrt_core http://downloads.openwrt.org/releases/19.07.2/targets/x86/geode/packages", - "src/gz openwrt_base http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/base", - "src/gz openwrt_luci http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/luci", - "src/gz openwrt_packages http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/packages", - "src/gz openwrt_routing http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/routing", - "src/gz openwrt_telephony http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/telephony", - "CUSTOM", - "# add your custom package feeds here", - "#", - "# src/gz example_feed_name http://www.example.com/path/to/files", - ], - "fact": { - "openwrt_core": [ - "http://downloads.openwrt.org/releases/19.07.2/targets/x86/geode/packages", - "src/gz", - "distribution", - ], - "openwrt_base": [ - "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/base", - "src/gz", - "distribution", - ], - "openwrt_luci": [ - "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/luci", - "src/gz", - "distribution", - ], - "openwrt_packages": [ - "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/packages", - "src/gz", - "distribution", - ], - "openwrt_routing": [ - "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/routing", - "src/gz", - "distribution", - ], - "openwrt_telephony": [ - "http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/telephony", - "src/gz", - "distribution", - ], - }, -} diff --git a/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.yaml b/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.yaml new file mode 100644 index 000000000..0b5c5ce01 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgFeeds/opkg_feeds.yaml @@ -0,0 +1,42 @@ +command: cat /etc/opkg/distfeeds.conf; echo CUSTOM; cat /etc/opkg/customfeeds.conf +requires_command: opkg +output: + - "" + - ' # a different comment' + - aaa + - src/gz + - src/gz openwrt_core http://downloads.openwrt.org/releases/19.07.2/targets/x86/geode/packages + - src/gz openwrt_base http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/base + - src/gz openwrt_luci http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/luci + - src/gz openwrt_packages http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/packages + - src/gz openwrt_routing http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/routing + - src/gz openwrt_telephony http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/telephony + - CUSTOM + - '# add your custom package feeds here' + - '#' + - '# src/gz example_feed_name http://www.example.com/path/to/files' +fact: + openwrt_core: + - http://downloads.openwrt.org/releases/19.07.2/targets/x86/geode/packages + - src/gz + - distribution + openwrt_base: + - http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/base + - src/gz + - distribution + openwrt_luci: + - http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/luci + - src/gz + - distribution + openwrt_packages: + - http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/packages + - src/gz + - distribution + openwrt_routing: + - http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/routing + - src/gz + - distribution + openwrt_telephony: + - http://downloads.openwrt.org/releases/19.07.2/packages/i386_pentium/telephony + - src/gz + - distribution diff --git a/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json b/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json deleted file mode 100644 index 7b6211f62..000000000 --- a/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "command": "opkg print-architecture", - "requires_command": "opkg", - "output": [ - "", - "# a comment to start", - "arch all 1", - " # another comment", - "arch noarch 1", - "arch", - "arch thisarch", - "arch xray zulu", - "arch i386_pentium 10 # some sort of comment", - ], - "fact": {"all": 1, "noarch": 1, "i386_pentium": 10}, -} diff --git a/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.yaml b/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.yaml new file mode 100644 index 000000000..37fcfc9d7 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgInstallableArchitectures/opkg_installable_architectures.yaml @@ -0,0 +1,16 @@ +command: opkg print-architecture +requires_command: opkg +output: + - "" + - '# a comment to start' + - arch all 1 + - ' # another comment' + - arch noarch 1 + - arch + - arch thisarch + - arch xray zulu + - 'arch i386_pentium 10 # some sort of comment' +fact: + all: 1 + noarch: 1 + i386_pentium: 10 diff --git a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json deleted file mode 100644 index eb3fbd8fe..000000000 --- a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "command": "opkg list-installed", - "requires_command": "opkg", - "output": [ - "urandom-seed - 1.0-1", - "urngd - 2020-01-21-c7f7b6b6-1", - "wget - 1.20.3-4", - "wget-nossl - 1.20.3-4", - "wireless-regdb - 2019.06.03", - "wpad-basic - 2019-08-08-ca8c2bd2-2", - "zlib - 1.2.11-3", - ], - "fact": { - "urandom-seed": ["1.0-1"], - "urngd": ["2020-01-21-c7f7b6b6-1"], - "wget": ["1.20.3-4"], - "wget-nossl": ["1.20.3-4"], - "wireless-regdb": ["2019.06.03"], - "wpad-basic": ["2019-08-08-ca8c2bd2-2"], - "zlib": ["1.2.11-3"], - }, -} diff --git a/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.yaml b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.yaml new file mode 100644 index 000000000..2c051c265 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgPackages/opkg_packages.yaml @@ -0,0 +1,25 @@ +command: opkg list-installed +requires_command: opkg +output: + - urandom-seed - 1.0-1 + - urngd - 2020-01-21-c7f7b6b6-1 + - wget - 1.20.3-4 + - wget-nossl - 1.20.3-4 + - wireless-regdb - 2019.06.03 + - wpad-basic - 2019-08-08-ca8c2bd2-2 + - zlib - 1.2.11-3 +fact: + urandom-seed: + - 1.0-1 + urngd: + - 2020-01-21-c7f7b6b6-1 + wget: + - 1.20.3-4 + wget-nossl: + - 1.20.3-4 + wireless-regdb: + - 2019.06.03 + wpad-basic: + - 2019-08-08-ca8c2bd2-2 + zlib: + - 1.2.11-3 diff --git a/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json b/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json deleted file mode 100644 index f4bd7dfe2..000000000 --- a/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "command": "opkg list-upgradable", - "requires_command": "opkg", - "output": [ - "", - "rpcd-mod-iwinfo - 2019-12-10-aaa08366-2 - 2020-05-26-67c8a3fd-1", - "luci-mod-network - git-20.115.52331-39a8290-1 - git-20.319.48994-50b7ab5-1", - "hostapd-common - 2019-08-08-ca8c2bd2-2 - 2019-08-08-ca8c2bd2-4", - "libuv1 - 1.34.2-1 - 1.40.0-1", - "xray123-123", - "wireless-regdb - 2019.06.03 - 2019.06.03-1", - ], - "fact": { - "rpcd-mod-iwinfo": ["2019-12-10-aaa08366-2", "2020-05-26-67c8a3fd-1"], - "luci-mod-network": ["git-20.115.52331-39a8290-1", "git-20.319.48994-50b7ab5-1"], - "hostapd-common": ["2019-08-08-ca8c2bd2-2", "2019-08-08-ca8c2bd2-4"], - "libuv1": ["1.34.2-1", "1.40.0-1"], - "wireless-regdb": ["2019.06.03", "2019.06.03-1"], - }, -} diff --git a/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.yaml b/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.yaml new file mode 100644 index 000000000..5b35c2282 --- /dev/null +++ b/tests/facts/openwrt.opkg.OpkgUpgradeablePackages/opkg_upgradeable_packages.yaml @@ -0,0 +1,26 @@ +command: opkg list-upgradable +requires_command: opkg +output: + - "" + - rpcd-mod-iwinfo - 2019-12-10-aaa08366-2 - 2020-05-26-67c8a3fd-1 + - luci-mod-network - git-20.115.52331-39a8290-1 - git-20.319.48994-50b7ab5-1 + - hostapd-common - 2019-08-08-ca8c2bd2-2 - 2019-08-08-ca8c2bd2-4 + - libuv1 - 1.34.2-1 - 1.40.0-1 + - xray123-123 + - wireless-regdb - 2019.06.03 - 2019.06.03-1 +fact: + rpcd-mod-iwinfo: + - 2019-12-10-aaa08366-2 + - 2020-05-26-67c8a3fd-1 + luci-mod-network: + - git-20.115.52331-39a8290-1 + - git-20.319.48994-50b7ab5-1 + hostapd-common: + - 2019-08-08-ca8c2bd2-2 + - 2019-08-08-ca8c2bd2-4 + libuv1: + - 1.34.2-1 + - 1.40.0-1 + wireless-regdb: + - 2019.06.03 + - 2019.06.03-1 diff --git a/tests/operations/openwrt.opkg.packages/add_existing_package.json b/tests/operations/openwrt.opkg.packages/add_existing_package.json deleted file mode 100644 index e806d995f..000000000 --- a/tests/operations/openwrt.opkg.packages/add_existing_package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": [], - "noop_description": "package curl is installed (7.66.0-2)", -} diff --git a/tests/operations/openwrt.opkg.packages/add_existing_package.yaml b/tests/operations/openwrt.opkg.packages/add_existing_package.yaml new file mode 100644 index 000000000..ca3d4bfd7 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_existing_package.yaml @@ -0,0 +1,10 @@ +args: + - curl +kwargs: + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: [] +noop_description: package curl is installed (7.66.0-2) diff --git a/tests/operations/openwrt.opkg.packages/add_multiple_packages.json b/tests/operations/openwrt.opkg.packages/add_multiple_packages.json deleted file mode 100644 index a5517216b..000000000 --- a/tests/operations/openwrt.opkg.packages/add_multiple_packages.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": [["curl", "wget"]], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg install curl wget"], -} diff --git a/tests/operations/openwrt.opkg.packages/add_multiple_packages.yaml b/tests/operations/openwrt.opkg.packages/add_multiple_packages.yaml new file mode 100644 index 000000000..e3b0fa3f5 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_multiple_packages.yaml @@ -0,0 +1,9 @@ +args: + - - curl + - wget +kwargs: + update: false +facts: + opkg.OpkgPackages: {} +commands: + - opkg install curl wget diff --git a/tests/operations/openwrt.opkg.packages/add_one_package.json b/tests/operations/openwrt.opkg.packages/add_one_package.json deleted file mode 100644 index 23187734f..000000000 --- a/tests/operations/openwrt.opkg.packages/add_one_package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg install curl"], -} diff --git a/tests/operations/openwrt.opkg.packages/add_one_package.yaml b/tests/operations/openwrt.opkg.packages/add_one_package.yaml new file mode 100644 index 000000000..9e4491362 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_one_package.yaml @@ -0,0 +1,8 @@ +args: + - curl +kwargs: + update: false +facts: + opkg.OpkgPackages: {} +commands: + - opkg install curl diff --git a/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json b/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json deleted file mode 100644 index 69defc1d4..000000000 --- a/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": ["curl=7.66.0-2"], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "exception": { - "name": "ValueError", - "message": "opkg does not support version pinning but found for: 'curl'", - }, -} diff --git a/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.yaml b/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.yaml new file mode 100644 index 000000000..56120a9ee --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/add_with_unallowed_pinning.yaml @@ -0,0 +1,8 @@ +args: + - curl=7.66.0-2 +facts: + opkg.OpkgPackages: {} +commands: [] +exception: + name: ValueError + message: 'opkg does not support version pinning but found for: ''curl''' diff --git a/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json deleted file mode 100644 index 73bbd8365..000000000 --- a/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": [["", "", ""]], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", - "logs": "foo" -} diff --git a/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.yaml b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.yaml new file mode 100644 index 000000000..368184345 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/list_of_nulls_package_list.yaml @@ -0,0 +1,9 @@ +args: + - - "" + - "" + - "" +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: empty or invalid package list provided to openwrt.opkg.packages +logs: foo diff --git a/tests/operations/openwrt.opkg.packages/null_package_list.json b/tests/operations/openwrt.opkg.packages/null_package_list.json deleted file mode 100644 index fa0ce88ab..000000000 --- a/tests/operations/openwrt.opkg.packages/null_package_list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": [], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", - "logs": "foo", -} diff --git a/tests/operations/openwrt.opkg.packages/null_package_list.yaml b/tests/operations/openwrt.opkg.packages/null_package_list.yaml new file mode 100644 index 000000000..77e58f865 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/null_package_list.yaml @@ -0,0 +1,6 @@ +args: [] +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: empty or invalid package list provided to openwrt.opkg.packages +logs: foo diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package.json b/tests/operations/openwrt.opkg.packages/remove_existing_package.json deleted file mode 100644 index 6cc6370be..000000000 --- a/tests/operations/openwrt.opkg.packages/remove_existing_package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": false}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": ["opkg remove curl"], -} diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package.yaml b/tests/operations/openwrt.opkg.packages/remove_existing_package.yaml new file mode 100644 index 000000000..a95d879bf --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_existing_package.yaml @@ -0,0 +1,11 @@ +args: + - curl +kwargs: + present: false + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: + - opkg remove curl diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json b/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json deleted file mode 100644 index 193223a0d..000000000 --- a/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": true}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": ["opkg update", "opkg remove curl"], -} diff --git a/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.yaml b/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.yaml new file mode 100644 index 000000000..e2aaca124 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_existing_package_and_require_update.yaml @@ -0,0 +1,12 @@ +args: + - curl +kwargs: + present: false + update: true +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: + - opkg update + - opkg remove curl diff --git a/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json b/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json deleted file mode 100644 index cef56213a..000000000 --- a/tests/operations/openwrt.opkg.packages/remove_not_existing_package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "package curl is not installed", -} diff --git a/tests/operations/openwrt.opkg.packages/remove_not_existing_package.yaml b/tests/operations/openwrt.opkg.packages/remove_not_existing_package.yaml new file mode 100644 index 000000000..2710239ac --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/remove_not_existing_package.yaml @@ -0,0 +1,9 @@ +args: + - curl +kwargs: + present: false + update: false +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: package curl is not installed diff --git a/tests/operations/openwrt.opkg.packages/update_then_add_one.json b/tests/operations/openwrt.opkg.packages/update_then_add_one.json deleted file mode 100644 index 9c8e03db6..000000000 --- a/tests/operations/openwrt.opkg.packages/update_then_add_one.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": true}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg update", "opkg install curl"], -} diff --git a/tests/operations/openwrt.opkg.packages/update_then_add_one.yaml b/tests/operations/openwrt.opkg.packages/update_then_add_one.yaml new file mode 100644 index 000000000..3595e5376 --- /dev/null +++ b/tests/operations/openwrt.opkg.packages/update_then_add_one.yaml @@ -0,0 +1,9 @@ +args: + - curl +kwargs: + update: true +facts: + opkg.OpkgPackages: {} +commands: + - opkg update + - opkg install curl diff --git a/tests/operations/openwrt.opkg.update/first_update.json b/tests/operations/openwrt.opkg.update/first_update.json deleted file mode 100644 index 4d8479faa..000000000 --- a/tests/operations/openwrt.opkg.update/first_update.json +++ /dev/null @@ -1 +0,0 @@ -{"args": [], "facts": {"opkg.OpkgPackages": {}}, "commands": ["opkg update"]} diff --git a/tests/operations/openwrt.opkg.update/first_update.yaml b/tests/operations/openwrt.opkg.update/first_update.yaml new file mode 100644 index 000000000..9c9dd4196 --- /dev/null +++ b/tests/operations/openwrt.opkg.update/first_update.yaml @@ -0,0 +1,5 @@ +args: [] +facts: + opkg.OpkgPackages: {} +commands: + - opkg update diff --git a/tests/operations/openwrt.packages/apk_add_null_packages.json b/tests/operations/openwrt.packages/apk_add_null_packages.json deleted file mode 100644 index d2b8c6e9e..000000000 --- a/tests/operations/openwrt.packages/apk_add_null_packages.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "args": [null], - "facts": { - "apk.ApkPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} - }, - "commands": [], - "noop_description": "empty package list provided to openwrt.packages" - -} diff --git a/tests/operations/openwrt.packages/apk_add_null_packages.yaml b/tests/operations/openwrt.packages/apk_add_null_packages.yaml new file mode 100644 index 000000000..f2f067e47 --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_null_packages.yaml @@ -0,0 +1,8 @@ +args: + - null +facts: + apk.ApkPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: true +commands: [] +noop_description: empty package list provided to openwrt.packages diff --git a/tests/operations/openwrt.packages/apk_add_packages.json b/tests/operations/openwrt.packages/apk_add_packages.json deleted file mode 100644 index 0144d8adc..000000000 --- a/tests/operations/openwrt.packages/apk_add_packages.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "args": [["curl", "wget"]], - "facts": { - "apk.ApkPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} - }, - "commands": ["apk add curl wget"] -} diff --git a/tests/operations/openwrt.packages/apk_add_packages.yaml b/tests/operations/openwrt.packages/apk_add_packages.yaml new file mode 100644 index 000000000..57d660fbb --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_packages.yaml @@ -0,0 +1,9 @@ +args: + - - curl + - wget +facts: + apk.ApkPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: true +commands: + - apk add curl wget diff --git a/tests/operations/openwrt.packages/apk_add_str_package.json b/tests/operations/openwrt.packages/apk_add_str_package.json deleted file mode 100644 index dc221c367..000000000 --- a/tests/operations/openwrt.packages/apk_add_str_package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "args": ["curl"], - "facts": { - "apk.ApkPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} - }, - "commands": ["apk add curl"] -} diff --git a/tests/operations/openwrt.packages/apk_add_str_package.yaml b/tests/operations/openwrt.packages/apk_add_str_package.yaml new file mode 100644 index 000000000..79ac8b286 --- /dev/null +++ b/tests/operations/openwrt.packages/apk_add_str_package.yaml @@ -0,0 +1,8 @@ +args: + - curl +facts: + apk.ApkPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: true +commands: + - apk add curl diff --git a/tests/operations/openwrt.packages/apk_remove_packages.json b/tests/operations/openwrt.packages/apk_remove_packages.json deleted file mode 100644 index 66dd485e8..000000000 --- a/tests/operations/openwrt.packages/apk_remove_packages.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": [["curl", "i-dont-exist"]], - "kwargs": {"present": false}, - "facts": { - "apk.ApkPackages": {"curl": "1"}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true}, - }, - "commands": ["apk del curl"], -} diff --git a/tests/operations/openwrt.packages/apk_remove_packages.yaml b/tests/operations/openwrt.packages/apk_remove_packages.yaml new file mode 100644 index 000000000..d43107620 --- /dev/null +++ b/tests/operations/openwrt.packages/apk_remove_packages.yaml @@ -0,0 +1,12 @@ +args: + - - curl + - i-dont-exist +kwargs: + present: false +facts: + apk.ApkPackages: + curl: "1" + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: true +commands: + - apk del curl diff --git a/tests/operations/openwrt.packages/opkg_add_existing_package.json b/tests/operations/openwrt.packages/opkg_add_existing_package.json deleted file mode 100644 index 2d706dd9a..000000000 --- a/tests/operations/openwrt.packages/opkg_add_existing_package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": false}, - "facts": { - "opkg.OpkgPackages": {"curl": ["7.66.0-2"]}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, - }, - "commands": [], - "noop_description": "package curl is installed (7.66.0-2)", -} diff --git a/tests/operations/openwrt.packages/opkg_add_existing_package.yaml b/tests/operations/openwrt.packages/opkg_add_existing_package.yaml new file mode 100644 index 000000000..bb86f99d4 --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_existing_package.yaml @@ -0,0 +1,12 @@ +args: + - curl +kwargs: + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: [] +noop_description: package curl is installed (7.66.0-2) diff --git a/tests/operations/openwrt.packages/opkg_add_null_packages.json b/tests/operations/openwrt.packages/opkg_add_null_packages.json deleted file mode 100644 index 20c24a86d..000000000 --- a/tests/operations/openwrt.packages/opkg_add_null_packages.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": [null], - "facts": { - "opkg.OpkgPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} - }, - "commands": [], - "noop_description": "empty package list provided to openwrt.packages" -} diff --git a/tests/operations/openwrt.packages/opkg_add_null_packages.yaml b/tests/operations/openwrt.packages/opkg_add_null_packages.yaml new file mode 100644 index 000000000..b9cce7951 --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_null_packages.yaml @@ -0,0 +1,8 @@ +args: + - null +facts: + opkg.OpkgPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: [] +noop_description: empty package list provided to openwrt.packages diff --git a/tests/operations/openwrt.packages/opkg_add_packages.json b/tests/operations/openwrt.packages/opkg_add_packages.json deleted file mode 100644 index 54efcd1c5..000000000 --- a/tests/operations/openwrt.packages/opkg_add_packages.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": [["curl", "wget"]], - "kwargs": {"update": false}, - "facts": { - "opkg.OpkgPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} - }, - "commands": ["opkg install curl wget"] -} diff --git a/tests/operations/openwrt.packages/opkg_add_packages.yaml b/tests/operations/openwrt.packages/opkg_add_packages.yaml new file mode 100644 index 000000000..fc0df18bb --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_packages.yaml @@ -0,0 +1,11 @@ +args: + - - curl + - wget +kwargs: + update: false +facts: + opkg.OpkgPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: + - opkg install curl wget diff --git a/tests/operations/openwrt.packages/opkg_add_str_package.json b/tests/operations/openwrt.packages/opkg_add_str_package.json deleted file mode 100644 index db3ec0d3f..000000000 --- a/tests/operations/openwrt.packages/opkg_add_str_package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "args": ["curl"], - "facts": { - "opkg.OpkgPackages": {}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} - }, - "commands": ["opkg install curl"] -} diff --git a/tests/operations/openwrt.packages/opkg_add_str_package.yaml b/tests/operations/openwrt.packages/opkg_add_str_package.yaml new file mode 100644 index 000000000..5159d793f --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_add_str_package.yaml @@ -0,0 +1,8 @@ +args: + - curl +facts: + opkg.OpkgPackages: {} + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: + - opkg install curl diff --git a/tests/operations/openwrt.packages/opkg_remove_existing_package.json b/tests/operations/openwrt.packages/opkg_remove_existing_package.json deleted file mode 100644 index 32f04265e..000000000 --- a/tests/operations/openwrt.packages/opkg_remove_existing_package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": false}, - "facts": { - "opkg.OpkgPackages": {"curl": ["7.66.0-2"]}, - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false}, - }, - "commands": ["opkg remove curl"], -} diff --git a/tests/operations/openwrt.packages/opkg_remove_existing_package.yaml b/tests/operations/openwrt.packages/opkg_remove_existing_package.yaml new file mode 100644 index 000000000..ad605e98b --- /dev/null +++ b/tests/operations/openwrt.packages/opkg_remove_existing_package.yaml @@ -0,0 +1,13 @@ +args: + - curl +kwargs: + present: false + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: + - opkg remove curl diff --git a/tests/operations/openwrt.update/apk_update.json b/tests/operations/openwrt.update/apk_update.json deleted file mode 100644 index 87d90b12e..000000000 --- a/tests/operations/openwrt.update/apk_update.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": [], - "facts": { - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": true} - }, - "commands": ["apk update"] -} diff --git a/tests/operations/openwrt.update/apk_update.yaml b/tests/operations/openwrt.update/apk_update.yaml new file mode 100644 index 000000000..c66aa871a --- /dev/null +++ b/tests/operations/openwrt.update/apk_update.yaml @@ -0,0 +1,6 @@ +args: [] +facts: + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: true +commands: + - apk update diff --git a/tests/operations/openwrt.update/opkg_update.json b/tests/operations/openwrt.update/opkg_update.json deleted file mode 100644 index 0bc773b07..000000000 --- a/tests/operations/openwrt.update/opkg_update.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": [], - "facts": { - "features.OpenWrtHasFeature": {"feature=OpenWrtFeature.USES_APK": false} - }, - "commands": ["opkg update"] -} diff --git a/tests/operations/openwrt.update/opkg_update.yaml b/tests/operations/openwrt.update/opkg_update.yaml new file mode 100644 index 000000000..bd0ff13d2 --- /dev/null +++ b/tests/operations/openwrt.update/opkg_update.yaml @@ -0,0 +1,6 @@ +args: [] +facts: + features.OpenWrtHasFeature: + feature=OpenWrtFeature.USES_APK: false +commands: + - opkg update diff --git a/tests/operations/opkg.packages/add_existing_package.json b/tests/operations/opkg.packages/add_existing_package.json deleted file mode 100644 index e806d995f..000000000 --- a/tests/operations/opkg.packages/add_existing_package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": [], - "noop_description": "package curl is installed (7.66.0-2)", -} diff --git a/tests/operations/opkg.packages/add_existing_package.yaml b/tests/operations/opkg.packages/add_existing_package.yaml new file mode 100644 index 000000000..ca3d4bfd7 --- /dev/null +++ b/tests/operations/opkg.packages/add_existing_package.yaml @@ -0,0 +1,10 @@ +args: + - curl +kwargs: + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: [] +noop_description: package curl is installed (7.66.0-2) diff --git a/tests/operations/opkg.packages/add_multiple_packages.json b/tests/operations/opkg.packages/add_multiple_packages.json deleted file mode 100644 index a5517216b..000000000 --- a/tests/operations/opkg.packages/add_multiple_packages.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": [["curl", "wget"]], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg install curl wget"], -} diff --git a/tests/operations/opkg.packages/add_multiple_packages.yaml b/tests/operations/opkg.packages/add_multiple_packages.yaml new file mode 100644 index 000000000..e3b0fa3f5 --- /dev/null +++ b/tests/operations/opkg.packages/add_multiple_packages.yaml @@ -0,0 +1,9 @@ +args: + - - curl + - wget +kwargs: + update: false +facts: + opkg.OpkgPackages: {} +commands: + - opkg install curl wget diff --git a/tests/operations/opkg.packages/add_one_package.json b/tests/operations/opkg.packages/add_one_package.json deleted file mode 100644 index 23187734f..000000000 --- a/tests/operations/opkg.packages/add_one_package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg install curl"], -} diff --git a/tests/operations/opkg.packages/add_one_package.yaml b/tests/operations/opkg.packages/add_one_package.yaml new file mode 100644 index 000000000..9e4491362 --- /dev/null +++ b/tests/operations/opkg.packages/add_one_package.yaml @@ -0,0 +1,8 @@ +args: + - curl +kwargs: + update: false +facts: + opkg.OpkgPackages: {} +commands: + - opkg install curl diff --git a/tests/operations/opkg.packages/add_with_unallowed_pinning.json b/tests/operations/opkg.packages/add_with_unallowed_pinning.json deleted file mode 100644 index 69defc1d4..000000000 --- a/tests/operations/opkg.packages/add_with_unallowed_pinning.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "args": ["curl=7.66.0-2"], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "exception": { - "name": "ValueError", - "message": "opkg does not support version pinning but found for: 'curl'", - }, -} diff --git a/tests/operations/opkg.packages/add_with_unallowed_pinning.yaml b/tests/operations/opkg.packages/add_with_unallowed_pinning.yaml new file mode 100644 index 000000000..56120a9ee --- /dev/null +++ b/tests/operations/opkg.packages/add_with_unallowed_pinning.yaml @@ -0,0 +1,8 @@ +args: + - curl=7.66.0-2 +facts: + opkg.OpkgPackages: {} +commands: [] +exception: + name: ValueError + message: 'opkg does not support version pinning but found for: ''curl''' diff --git a/tests/operations/opkg.packages/list_of_nulls_package_list.json b/tests/operations/opkg.packages/list_of_nulls_package_list.json deleted file mode 100644 index 28aa77002..000000000 --- a/tests/operations/opkg.packages/list_of_nulls_package_list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": ["", "", ""], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", - "logs": "foo", -} diff --git a/tests/operations/opkg.packages/list_of_nulls_package_list.yaml b/tests/operations/opkg.packages/list_of_nulls_package_list.yaml new file mode 100644 index 000000000..2386e3817 --- /dev/null +++ b/tests/operations/opkg.packages/list_of_nulls_package_list.yaml @@ -0,0 +1,9 @@ +args: + - "" + - "" + - "" +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: empty or invalid package list provided to openwrt.opkg.packages +logs: foo diff --git a/tests/operations/opkg.packages/null_package_list.json b/tests/operations/opkg.packages/null_package_list.json deleted file mode 100644 index fa0ce88ab..000000000 --- a/tests/operations/opkg.packages/null_package_list.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": [], - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "empty or invalid package list provided to openwrt.opkg.packages", - "logs": "foo", -} diff --git a/tests/operations/opkg.packages/null_package_list.yaml b/tests/operations/opkg.packages/null_package_list.yaml new file mode 100644 index 000000000..77e58f865 --- /dev/null +++ b/tests/operations/opkg.packages/null_package_list.yaml @@ -0,0 +1,6 @@ +args: [] +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: empty or invalid package list provided to openwrt.opkg.packages +logs: foo diff --git a/tests/operations/opkg.packages/remove_existing_package.json b/tests/operations/opkg.packages/remove_existing_package.json deleted file mode 100644 index 6cc6370be..000000000 --- a/tests/operations/opkg.packages/remove_existing_package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": false}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": ["opkg remove curl"], -} diff --git a/tests/operations/opkg.packages/remove_existing_package.yaml b/tests/operations/opkg.packages/remove_existing_package.yaml new file mode 100644 index 000000000..a95d879bf --- /dev/null +++ b/tests/operations/opkg.packages/remove_existing_package.yaml @@ -0,0 +1,11 @@ +args: + - curl +kwargs: + present: false + update: false +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: + - opkg remove curl diff --git a/tests/operations/opkg.packages/remove_existing_package_and_require_update.json b/tests/operations/opkg.packages/remove_existing_package_and_require_update.json deleted file mode 100644 index 193223a0d..000000000 --- a/tests/operations/opkg.packages/remove_existing_package_and_require_update.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": true}, - "facts": {"opkg.OpkgPackages": {"curl": ["7.66.0-2"]}}, - "commands": ["opkg update", "opkg remove curl"], -} diff --git a/tests/operations/opkg.packages/remove_existing_package_and_require_update.yaml b/tests/operations/opkg.packages/remove_existing_package_and_require_update.yaml new file mode 100644 index 000000000..e2aaca124 --- /dev/null +++ b/tests/operations/opkg.packages/remove_existing_package_and_require_update.yaml @@ -0,0 +1,12 @@ +args: + - curl +kwargs: + present: false + update: true +facts: + opkg.OpkgPackages: + curl: + - 7.66.0-2 +commands: + - opkg update + - opkg remove curl diff --git a/tests/operations/opkg.packages/remove_not_existing_package.json b/tests/operations/opkg.packages/remove_not_existing_package.json deleted file mode 100644 index cef56213a..000000000 --- a/tests/operations/opkg.packages/remove_not_existing_package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"present": false, "update": false}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": [], - "noop_description": "package curl is not installed", -} diff --git a/tests/operations/opkg.packages/remove_not_existing_package.yaml b/tests/operations/opkg.packages/remove_not_existing_package.yaml new file mode 100644 index 000000000..2710239ac --- /dev/null +++ b/tests/operations/opkg.packages/remove_not_existing_package.yaml @@ -0,0 +1,9 @@ +args: + - curl +kwargs: + present: false + update: false +facts: + opkg.OpkgPackages: {} +commands: [] +noop_description: package curl is not installed diff --git a/tests/operations/opkg.packages/update_then_add_one.json b/tests/operations/opkg.packages/update_then_add_one.json deleted file mode 100644 index 9c8e03db6..000000000 --- a/tests/operations/opkg.packages/update_then_add_one.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "args": ["curl"], - "kwargs": {"update": true}, - "facts": {"opkg.OpkgPackages": {}}, - "commands": ["opkg update", "opkg install curl"], -} diff --git a/tests/operations/opkg.packages/update_then_add_one.yaml b/tests/operations/opkg.packages/update_then_add_one.yaml new file mode 100644 index 000000000..3595e5376 --- /dev/null +++ b/tests/operations/opkg.packages/update_then_add_one.yaml @@ -0,0 +1,9 @@ +args: + - curl +kwargs: + update: true +facts: + opkg.OpkgPackages: {} +commands: + - opkg update + - opkg install curl diff --git a/tests/operations/opkg.update/first_update.json b/tests/operations/opkg.update/first_update.json deleted file mode 100644 index 4d8479faa..000000000 --- a/tests/operations/opkg.update/first_update.json +++ /dev/null @@ -1 +0,0 @@ -{"args": [], "facts": {"opkg.OpkgPackages": {}}, "commands": ["opkg update"]} diff --git a/tests/operations/opkg.update/first_update.yaml b/tests/operations/opkg.update/first_update.yaml new file mode 100644 index 000000000..9c9dd4196 --- /dev/null +++ b/tests/operations/opkg.update/first_update.yaml @@ -0,0 +1,5 @@ +args: [] +facts: + opkg.OpkgPackages: {} +commands: + - opkg update From d2e5087af8fc2827a194b8bdb50b74c105d437af Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 16:57:58 -0400 Subject: [PATCH 25/26] chore: fix some formatting - src/pyinfra/facts/openwrt/opkg.py --- src/pyinfra/facts/openwrt/opkg.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pyinfra/facts/openwrt/opkg.py b/src/pyinfra/facts/openwrt/opkg.py index 7cd0933dd..bfea62222 100644 --- a/src/pyinfra/facts/openwrt/opkg.py +++ b/src/pyinfra/facts/openwrt/opkg.py @@ -33,8 +33,10 @@ class OpkgPkgUpgradeInfo(NamedTuple): installed: str available: str + OpkgPkgUpgradeMap = dict[str, OpkgPkgUpgradeInfo] + class OpkgConfInfo(NamedTuple): paths: dict[str, str] # list of paths, e.g. {'root':'/', 'ram':'/tmp} list_dir: str # where package lists are stored, e.g. /var/opkg-lists @@ -47,6 +49,7 @@ class OpkgFeedInfo(NamedTuple): fmt: str # format of the feed, e.g. "src/gz" kind: str # whether it comes from the 'distribution' or is 'custom' + OpkgFeedMap = dict[str, OpkgFeedInfo] @@ -164,7 +167,7 @@ def command(self) -> str: return "cat /etc/opkg/distfeeds.conf; echo CUSTOM; cat /etc/opkg/customfeeds.conf" @override - def process(self, output:list[str]) -> OpkgFeedMap: + def process(self, output: list[str]) -> OpkgFeedMap: feeds, kind = {}, "distribution" for line in output: match = self.regex.match(line) From 53513cffceb2b445afd7aae21f0b2b905d3008f6 Mon Sep 17 00:00:00 2001 From: morrison12 Date: Tue, 14 Jul 2026 17:01:26 -0400 Subject: [PATCH 26/26] chore: rework typing - change from object to Any for __init__.py in openwrt --- src/pyinfra/facts/openwrt/__init__.py | 3 ++- src/pyinfra/operations/openwrt/__init__.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pyinfra/facts/openwrt/__init__.py b/src/pyinfra/facts/openwrt/__init__.py index eddf878e1..0a389c460 100644 --- a/src/pyinfra/facts/openwrt/__init__.py +++ b/src/pyinfra/facts/openwrt/__init__.py @@ -4,6 +4,7 @@ import importlib import sys +from typing import Any __ALL__ = { "OpenWrtFeature": "features.OpenWrtFeature", @@ -14,7 +15,7 @@ __all__ = list(__ALL__.keys()) -def __getattr__(name: str) -> object: +def __getattr__(name: str) -> Any: # On-demand import of OpenWrt facts, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change. diff --git a/src/pyinfra/operations/openwrt/__init__.py b/src/pyinfra/operations/openwrt/__init__.py index 4e8dd0ee1..6d9250997 100644 --- a/src/pyinfra/operations/openwrt/__init__.py +++ b/src/pyinfra/operations/openwrt/__init__.py @@ -4,6 +4,7 @@ import importlib import sys +from typing import Any __ALL__ = { "opkg": "opkg", @@ -14,7 +15,7 @@ __all__ = list(__ALL__.keys()) -def __getattr__(name): +def __getattr__(name: str) -> Any: # On-demand import of OpenWrt operations, so we don't have to import them all at once # this forces py3.7>=, but that's fine as py2 is EOL and py3.6 is also EOL # Also, pyinfra is py3.11>=, so this is not a breaking change.