pyinfra 3.1.1__tar.gz → 3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pyinfra-3.1.1 → pyinfra-3.2}/CHANGELOG.md +38 -0
- {pyinfra-3.1.1/pyinfra.egg-info → pyinfra-3.2}/PKG-INFO +11 -12
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/arguments.py +9 -2
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/deploy.py +4 -2
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/host.py +5 -3
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/docker.py +17 -6
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/client.py +26 -14
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/apk.py +3 -1
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/apt.py +60 -0
- pyinfra-3.2/pyinfra/facts/crontab.py +190 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/docker.py +6 -0
- pyinfra-3.2/pyinfra/facts/efibootmgr.py +108 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/files.py +93 -6
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/git.py +3 -2
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/mysql.py +1 -2
- pyinfra-3.2/pyinfra/facts/opkg.py +233 -0
- pyinfra-3.2/pyinfra/facts/pipx.py +74 -0
- pyinfra-3.2/pyinfra/facts/podman.py +47 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/postgres.py +2 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/server.py +39 -77
- pyinfra-3.2/pyinfra/facts/util/units.py +30 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/zfs.py +22 -19
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/local.py +3 -2
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/apt.py +27 -20
- pyinfra-3.2/pyinfra/operations/crontab.py +189 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/docker.py +13 -12
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/files.py +18 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/git.py +23 -7
- pyinfra-3.2/pyinfra/operations/opkg.py +88 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pip.py +3 -2
- pyinfra-3.2/pyinfra/operations/pipx.py +90 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/postgres.py +15 -11
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/runit.py +2 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/server.py +3 -177
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/zfs.py +3 -3
- {pyinfra-3.1.1 → pyinfra-3.2/pyinfra.egg-info}/PKG-INFO +11 -12
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/SOURCES.txt +10 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/requires.txt +10 -11
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/inventory.py +26 -9
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/prints.py +18 -3
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/util.py +3 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/setup.py +5 -6
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_deploy.py +15 -13
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_inventory.py +53 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_sshuserclient.py +68 -1
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_facts.py +3 -0
- pyinfra-3.2/tests/test_units.py +30 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/LICENSE.md +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/MANIFEST.in +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/README.md +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/__main__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/arguments_typed.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/command.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/config.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/connect.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/connectors.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/exceptions.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/facts.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/inventory.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/operation.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/operations.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/state.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/api/util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/base.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/chroot.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/dockerssh.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/local.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/ssh.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/ssh_util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/sshuserclient/config.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/terraform.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/connectors/vagrant.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/context.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/brew.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/bsdinit.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/cargo.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/choco.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/deb.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/dnf.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/flatpak.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/gem.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/gpg.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/hardware.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/iptables.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/launchd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/lxd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/npm.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/openrc.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pacman.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pip.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pkg.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/pkgin.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/postgresql.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/rpm.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/runit.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/selinux.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/snap.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/systemd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/sysvinit.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/upstart.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/databases.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/packaging.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/util/win_files.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/vzctl.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/xbps.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/yum.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/facts/zypper.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/apk.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/brew.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/bsdinit.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/cargo.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/choco.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/dnf.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/flatpak.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/gem.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/iptables.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/launchd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/lxd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/mysql.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/npm.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/openrc.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pacman.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pkg.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/pkgin.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/postgresql.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/puppet.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/python.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/selinux.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/snap.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/ssh.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/systemd.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/sysvinit.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/upstart.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/docker.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/files.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/packaging.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/util/service.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/vzctl.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/xbps.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/yum.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/operations/zypper.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/progress.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/py.typed +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra/version.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/dependency_links.txt +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/entry_points.txt +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra.egg-info/top_level.txt +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/__main__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/commands.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/exceptions.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/log.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/main.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyinfra_cli/virtualenv.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/pyproject.toml +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/setup.cfg +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_arguments.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_command.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_config.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_deploys.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_facts.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_host.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_inventory.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_operations.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_api/test_api_util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_exceptions.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_cli_util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/test_context_objects.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_cli/util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/__init__.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_chroot.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_docker.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_dockerssh.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_local.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_ssh.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_terraform.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_util.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_connectors/test_vagrant.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_global_arguments.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_operations.py +0 -0
- {pyinfra-3.1.1 → pyinfra-3.2}/tests/test_operations_utils.py +0 -0
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
# v3.2
|
|
2
|
+
|
|
3
|
+
Hello 2025! Here's pyinfra 3.2 - with another incredible round of contributions from the community, THANK YOU ALL. New stuff:
|
|
4
|
+
|
|
5
|
+
- Add total counts to results summary (@NichtJens)
|
|
6
|
+
- Enable passing extra data via `local.include` (@TimothyWillard)
|
|
7
|
+
- Validate inventory files and display warnings for unexpected variables (@simonhammes)
|
|
8
|
+
|
|
9
|
+
New operations/facts:
|
|
10
|
+
|
|
11
|
+
- Add `pipx` operations (`packages`, `upgrade_all`, `ensure_path`) facts (`PipxPackages`, `PipxEnvironment`) and operations (@maisim)
|
|
12
|
+
- Add `server.OsRelease` fact (@wowi42)
|
|
13
|
+
- Add `podman.PodmanSystemInfo` and `podman.PodmanPs` facts (@bauen1)
|
|
14
|
+
- Add many extra arguments (including generic args) to `files.FindFiles*` facts (@JakkuSakura)
|
|
15
|
+
- Add `system` argument to `git.config` operation (@Pirols)
|
|
16
|
+
- Add `psql_database` argument to postgres operations & facts (@hamishfagg)
|
|
17
|
+
- Add `files.Sha384File` fact and `sha384sum` argument to `files.download` operation (@simonhammes)
|
|
18
|
+
- Add `apt.SimulateOperationWillChange` fact (@bauen1)
|
|
19
|
+
- Detect changes in `apt.upgrade` and `apt.dist_upgrade` operations (@bauen1)
|
|
20
|
+
- Add `fibootmgr.EFIBootMgr` fact (@bauen1)
|
|
21
|
+
- Add opkg facts and operations (@morrison12)
|
|
22
|
+
|
|
23
|
+
Fixes:
|
|
24
|
+
|
|
25
|
+
- Multiple fixes for `server.crontab` operation and facts (@JakkuSakura)
|
|
26
|
+
- Correctly handle `latest` argument with requirements file in `pip.packages` operation (@amiraliakbari)
|
|
27
|
+
- Fix regex used to parse installed apk packages (@simonhammes)
|
|
28
|
+
- Fix SSH connector overwriting known hosts files (@vo452)
|
|
29
|
+
|
|
30
|
+
Docs/internal tweaks:
|
|
31
|
+
|
|
32
|
+
- Add type annotations for many more operations (@simonhammes)
|
|
33
|
+
- Add typos CI checking to replace flake8-spellcheck (@simonhammes)
|
|
34
|
+
- Bump CI actions and dependencies (@simonhammes)
|
|
35
|
+
- Require JSON tests to include all arguments
|
|
36
|
+
- Remove unused `configparser` dependency (@bkmgit)
|
|
37
|
+
- Many small documentation fixes/tweaks
|
|
38
|
+
|
|
1
39
|
# v3.1.1
|
|
2
40
|
|
|
3
41
|
- Improve errors with 2.x style `@decorator` (vs `@decorator()`) functions
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyinfra
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.2
|
|
4
4
|
Summary: pyinfra automates/provisions/manages/deploys infrastructure.
|
|
5
5
|
Home-page: https://pyinfra.com
|
|
6
6
|
Author: Nick / Fizzadar
|
|
@@ -33,7 +33,6 @@ Requires-Dist: click>2
|
|
|
33
33
|
Requires-Dist: jinja2<4,>2
|
|
34
34
|
Requires-Dist: python-dateutil<3,>2
|
|
35
35
|
Requires-Dist: setuptools
|
|
36
|
-
Requires-Dist: configparser
|
|
37
36
|
Requires-Dist: pywinrm
|
|
38
37
|
Requires-Dist: typeguard
|
|
39
38
|
Requires-Dist: distro<2,>=1.6
|
|
@@ -42,12 +41,12 @@ Requires-Dist: graphlib_backport; python_version < "3.9"
|
|
|
42
41
|
Requires-Dist: typing-extensions; python_version < "3.11"
|
|
43
42
|
Requires-Dist: importlib_metadata>=3.6; python_version < "3.10"
|
|
44
43
|
Provides-Extra: test
|
|
45
|
-
Requires-Dist: pytest==8.
|
|
46
|
-
Requires-Dist: coverage==7.
|
|
44
|
+
Requires-Dist: pytest==8.3.3; extra == "test"
|
|
45
|
+
Requires-Dist: coverage==7.6.1; extra == "test"
|
|
47
46
|
Requires-Dist: pytest-cov==5.0.0; extra == "test"
|
|
48
|
-
Requires-Dist: black==24.
|
|
47
|
+
Requires-Dist: black==24.8.0; extra == "test"
|
|
49
48
|
Requires-Dist: isort==5.13.2; extra == "test"
|
|
50
|
-
Requires-Dist: flake8==7.
|
|
49
|
+
Requires-Dist: flake8==7.1.1; extra == "test"
|
|
51
50
|
Requires-Dist: flake8-black==0.3.6; extra == "test"
|
|
52
51
|
Requires-Dist: flake8-isort==6.1.1; extra == "test"
|
|
53
52
|
Requires-Dist: mypy; extra == "test"
|
|
@@ -58,15 +57,15 @@ Requires-Dist: types-PyYAML; extra == "test"
|
|
|
58
57
|
Requires-Dist: types-setuptools; extra == "test"
|
|
59
58
|
Provides-Extra: docs
|
|
60
59
|
Requires-Dist: pyinfra-guzzle_sphinx_theme==0.16; extra == "docs"
|
|
61
|
-
Requires-Dist: myst-parser==
|
|
60
|
+
Requires-Dist: myst-parser==3.0.1; extra == "docs"
|
|
62
61
|
Requires-Dist: sphinx==6.2.1; extra == "docs"
|
|
63
62
|
Provides-Extra: dev
|
|
64
|
-
Requires-Dist: pytest==8.
|
|
65
|
-
Requires-Dist: coverage==7.
|
|
63
|
+
Requires-Dist: pytest==8.3.3; extra == "dev"
|
|
64
|
+
Requires-Dist: coverage==7.6.1; extra == "dev"
|
|
66
65
|
Requires-Dist: pytest-cov==5.0.0; extra == "dev"
|
|
67
|
-
Requires-Dist: black==24.
|
|
66
|
+
Requires-Dist: black==24.8.0; extra == "dev"
|
|
68
67
|
Requires-Dist: isort==5.13.2; extra == "dev"
|
|
69
|
-
Requires-Dist: flake8==7.
|
|
68
|
+
Requires-Dist: flake8==7.1.1; extra == "dev"
|
|
70
69
|
Requires-Dist: flake8-black==0.3.6; extra == "dev"
|
|
71
70
|
Requires-Dist: flake8-isort==6.1.1; extra == "dev"
|
|
72
71
|
Requires-Dist: mypy; extra == "dev"
|
|
@@ -76,7 +75,7 @@ Requires-Dist: types-python-dateutil; extra == "dev"
|
|
|
76
75
|
Requires-Dist: types-PyYAML; extra == "dev"
|
|
77
76
|
Requires-Dist: types-setuptools; extra == "dev"
|
|
78
77
|
Requires-Dist: pyinfra-guzzle_sphinx_theme==0.16; extra == "dev"
|
|
79
|
-
Requires-Dist: myst-parser==
|
|
78
|
+
Requires-Dist: myst-parser==3.0.1; extra == "dev"
|
|
80
79
|
Requires-Dist: sphinx==6.2.1; extra == "dev"
|
|
81
80
|
Requires-Dist: wheel; extra == "dev"
|
|
82
81
|
Requires-Dist: twine; extra == "dev"
|
|
@@ -248,6 +248,12 @@ __argument_docs__ = {
|
|
|
248
248
|
"Privilege & user escalation": (
|
|
249
249
|
auth_argument_meta,
|
|
250
250
|
"""
|
|
251
|
+
.. caution::
|
|
252
|
+
When combining privilege escalation arguments it is important to know the order they
|
|
253
|
+
are applied: ``doas`` -> ``sudo`` -> ``su``. For example
|
|
254
|
+
``_sudo=True,_su_user="pyinfra"`` yields a command like ``sudo su pyinfra..``.
|
|
255
|
+
""",
|
|
256
|
+
"""
|
|
251
257
|
.. code:: python
|
|
252
258
|
|
|
253
259
|
# Execute a command with sudo
|
|
@@ -268,6 +274,7 @@ __argument_docs__ = {
|
|
|
268
274
|
),
|
|
269
275
|
"Shell control & features": (
|
|
270
276
|
shell_argument_meta,
|
|
277
|
+
"",
|
|
271
278
|
"""
|
|
272
279
|
.. code:: python
|
|
273
280
|
|
|
@@ -279,8 +286,8 @@ __argument_docs__ = {
|
|
|
279
286
|
)
|
|
280
287
|
""",
|
|
281
288
|
),
|
|
282
|
-
"Operation meta & callbacks": (meta_argument_meta, ""),
|
|
283
|
-
"Execution strategy": (execution_argument_meta, ""),
|
|
289
|
+
"Operation meta & callbacks": (meta_argument_meta, "", ""),
|
|
290
|
+
"Execution strategy": (execution_argument_meta, "", ""),
|
|
284
291
|
}
|
|
285
292
|
|
|
286
293
|
|
|
@@ -23,7 +23,7 @@ if TYPE_CHECKING:
|
|
|
23
23
|
from pyinfra.api.state import State
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs):
|
|
26
|
+
def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs) -> None:
|
|
27
27
|
"""
|
|
28
28
|
Prepare & add an deploy to pyinfra.state by executing it on all hosts.
|
|
29
29
|
|
|
@@ -54,7 +54,9 @@ def add_deploy(state: "State", deploy_func: Callable[..., Any], *args, **kwargs)
|
|
|
54
54
|
P = ParamSpec("P")
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
def deploy(
|
|
57
|
+
def deploy(
|
|
58
|
+
name: Optional[str] = None, data_defaults: Optional[dict] = None
|
|
59
|
+
) -> Callable[[Callable[P, Any]], PyinfraOperation[P]]:
|
|
58
60
|
"""
|
|
59
61
|
Decorator that takes a deploy function (normally from a pyinfra_* package)
|
|
60
62
|
and wraps any operations called inside with any deploy-wide kwargs/data.
|
|
@@ -218,17 +218,19 @@ class Host:
|
|
|
218
218
|
self.print_prefix_padding,
|
|
219
219
|
)
|
|
220
220
|
|
|
221
|
-
def log(self, message, log_func=logger.info):
|
|
221
|
+
def log(self, message: str, log_func: Callable[[str], Any] = logger.info) -> None:
|
|
222
222
|
log_func(f"{self.print_prefix}{message}")
|
|
223
223
|
|
|
224
|
-
def log_styled(
|
|
224
|
+
def log_styled(
|
|
225
|
+
self, message: str, log_func: Callable[[str], Any] = logger.info, **kwargs
|
|
226
|
+
) -> None:
|
|
225
227
|
message_styled = click.style(message, **kwargs)
|
|
226
228
|
self.log(message_styled, log_func=log_func)
|
|
227
229
|
|
|
228
230
|
def get_deploy_data(self):
|
|
229
231
|
return self.current_op_deploy_data or self.current_deploy_data or {}
|
|
230
232
|
|
|
231
|
-
def noop(self, description):
|
|
233
|
+
def noop(self, description: str) -> None:
|
|
232
234
|
"""
|
|
233
235
|
Log a description for a noop operation.
|
|
234
236
|
"""
|
|
@@ -58,13 +58,20 @@ def _start_docker_image(image_name):
|
|
|
58
58
|
|
|
59
59
|
class DockerConnector(BaseConnector):
|
|
60
60
|
"""
|
|
61
|
-
The
|
|
62
|
-
Docker containers.
|
|
61
|
+
The Docker connector allows you to use pyinfra to create new Docker images or modify running
|
|
62
|
+
Docker containers.
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
.. note::
|
|
65
|
+
|
|
66
|
+
The Docker connector allows pyinfra to target Docker containers as inventory and is
|
|
67
|
+
unrelated to the :doc:`../operations/docker` & :doc:`../facts/docker`.
|
|
68
|
+
|
|
69
|
+
You can pass either an image name or existing container ID:
|
|
70
|
+
|
|
71
|
+
+ Image - will create a new container from the image, execute operations against it, save into \
|
|
72
|
+
a new Docker image and remove the container
|
|
73
|
+
+ Existing container ID - will execute operations against the running container, leaving it \
|
|
74
|
+
running
|
|
68
75
|
|
|
69
76
|
.. code:: shell
|
|
70
77
|
|
|
@@ -76,6 +83,10 @@ class DockerConnector(BaseConnector):
|
|
|
76
83
|
|
|
77
84
|
# Execute against a running container
|
|
78
85
|
pyinfra @docker/2beb8c15a1b1 ...
|
|
86
|
+
|
|
87
|
+
The Docker connector is great for testing pyinfra operations locally, rather than connecting to
|
|
88
|
+
a remote host over SSH each time. This gives you a fast, local-first devloop to iterate on when
|
|
89
|
+
writing deploys, operations or facts.
|
|
79
90
|
"""
|
|
80
91
|
|
|
81
92
|
handles_execution = True
|
|
@@ -14,6 +14,7 @@ from paramiko import (
|
|
|
14
14
|
SSHException,
|
|
15
15
|
)
|
|
16
16
|
from paramiko.agent import AgentRequestHandler
|
|
17
|
+
from paramiko.hostkeys import HostKeyEntry
|
|
17
18
|
|
|
18
19
|
from pyinfra import logger
|
|
19
20
|
from pyinfra.api.util import memoize
|
|
@@ -31,6 +32,28 @@ class StrictPolicy(MissingHostKeyPolicy):
|
|
|
31
32
|
)
|
|
32
33
|
|
|
33
34
|
|
|
35
|
+
def append_hostkey(client, hostname, key):
|
|
36
|
+
"""Append hostname to the clients host_keys_file"""
|
|
37
|
+
|
|
38
|
+
with HOST_KEYS_LOCK:
|
|
39
|
+
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
40
|
+
# this correctly, so use that with the client filename variable.
|
|
41
|
+
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
42
|
+
host_key_entry = HostKeyEntry([hostname], key)
|
|
43
|
+
if host_key_entry is None:
|
|
44
|
+
raise SSHException(
|
|
45
|
+
"Append Hostkey: Failed to parse host {0}, could not append to hostfile".format(
|
|
46
|
+
hostname
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
with open(client._host_keys_filename, "a") as host_keys_file:
|
|
50
|
+
hk_entry = host_key_entry.to_line()
|
|
51
|
+
if hk_entry is None:
|
|
52
|
+
raise SSHException(f"Append Hostkey: Failed to append hostkey ({host_key_entry})")
|
|
53
|
+
|
|
54
|
+
host_keys_file.write(hk_entry)
|
|
55
|
+
|
|
56
|
+
|
|
34
57
|
class AcceptNewPolicy(MissingHostKeyPolicy):
|
|
35
58
|
def missing_host_key(self, client, hostname, key):
|
|
36
59
|
logger.warning(
|
|
@@ -40,13 +63,8 @@ class AcceptNewPolicy(MissingHostKeyPolicy):
|
|
|
40
63
|
),
|
|
41
64
|
)
|
|
42
65
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
host_keys.add(hostname, key.get_name(), key)
|
|
46
|
-
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
47
|
-
# this correctly, so use that with the client filename variable.
|
|
48
|
-
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
49
|
-
host_keys.save(client._host_keys_filename)
|
|
66
|
+
append_hostkey(client, hostname, key)
|
|
67
|
+
logger.warning("Added host key for {0} to known_hosts".format(hostname))
|
|
50
68
|
|
|
51
69
|
|
|
52
70
|
class AskPolicy(MissingHostKeyPolicy):
|
|
@@ -60,13 +78,7 @@ class AskPolicy(MissingHostKeyPolicy):
|
|
|
60
78
|
raise SSHException(
|
|
61
79
|
"AskPolicy: No host key for {0} found in known_hosts".format(hostname),
|
|
62
80
|
)
|
|
63
|
-
|
|
64
|
-
host_keys = client.get_host_keys()
|
|
65
|
-
host_keys.add(hostname, key.get_name(), key)
|
|
66
|
-
# The paramiko client saves host keys incorrectly whereas the host keys object does
|
|
67
|
-
# this correctly, so use that with the client filename variable.
|
|
68
|
-
# See: https://github.com/paramiko/paramiko/pull/1989
|
|
69
|
-
host_keys.save(client._host_keys_filename)
|
|
81
|
+
append_hostkey(client, hostname, key)
|
|
70
82
|
logger.warning("Added host key for {0} to known_hosts".format(hostname))
|
|
71
83
|
return
|
|
72
84
|
|
|
@@ -4,7 +4,9 @@ from pyinfra.api import FactBase
|
|
|
4
4
|
|
|
5
5
|
from .util.packaging import parse_packages
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
# Source: https://superuser.com/a/1472405
|
|
8
|
+
# Modified to return version and release inside a single group and removed extra capturing groups
|
|
9
|
+
APK_REGEX = r"(.+)-([^-]+-r[^-]+) \S+ \{\S+\} \(.+?\)"
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class ApkPackages(FactBase):
|
|
@@ -2,12 +2,36 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
+
from typing_extensions import TypedDict
|
|
6
|
+
|
|
5
7
|
from pyinfra.api import FactBase
|
|
6
8
|
|
|
7
9
|
from .gpg import GpgFactBase
|
|
8
10
|
from .util import make_cat_files_command
|
|
9
11
|
|
|
10
12
|
|
|
13
|
+
def noninteractive_apt(command: str, force=False):
|
|
14
|
+
args = ["DEBIAN_FRONTEND=noninteractive apt-get -y"]
|
|
15
|
+
|
|
16
|
+
if force:
|
|
17
|
+
args.append("--force-yes")
|
|
18
|
+
|
|
19
|
+
args.extend(
|
|
20
|
+
(
|
|
21
|
+
'-o Dpkg::Options::="--force-confdef"',
|
|
22
|
+
'-o Dpkg::Options::="--force-confold"',
|
|
23
|
+
command,
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return " ".join(args)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
APT_CHANGES_RE = re.compile(
|
|
31
|
+
r"^(\d+) upgraded, (\d+) newly installed, (\d+) to remove and (\d+) not upgraded.$"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
11
35
|
def parse_apt_repo(name):
|
|
12
36
|
regex = r"^(deb(?:-src)?)(?:\s+\[([^\]]+)\])?\s+([^\s]+)\s+([^\s]+)\s+([a-z-\s\d]*)$"
|
|
13
37
|
|
|
@@ -94,3 +118,39 @@ class AptKeys(GpgFactBase):
|
|
|
94
118
|
|
|
95
119
|
def requires_command(self) -> str:
|
|
96
120
|
return "apt-key"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class AptSimulationDict(TypedDict):
|
|
124
|
+
upgraded: int
|
|
125
|
+
newly_installed: int
|
|
126
|
+
removed: int
|
|
127
|
+
not_upgraded: int
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class SimulateOperationWillChange(FactBase[AptSimulationDict]):
|
|
131
|
+
"""
|
|
132
|
+
Simulate an 'apt-get' operation and try to detect if any changes would be performed.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def command(self, command: str) -> str:
|
|
136
|
+
# LC_ALL=C: Ensure the output is in english, as we want to parse it
|
|
137
|
+
return "LC_ALL=C " + noninteractive_apt(f"{command} --dry-run")
|
|
138
|
+
|
|
139
|
+
def requires_command(self, command: str) -> str:
|
|
140
|
+
return "apt-get"
|
|
141
|
+
|
|
142
|
+
def process(self, output) -> AptSimulationDict:
|
|
143
|
+
# We are looking for a line similar to
|
|
144
|
+
# "3 upgraded, 0 newly installed, 0 to remove and 0 not upgraded."
|
|
145
|
+
for line in output:
|
|
146
|
+
result = APT_CHANGES_RE.match(line)
|
|
147
|
+
if result is not None:
|
|
148
|
+
return {
|
|
149
|
+
"upgraded": int(result[1]),
|
|
150
|
+
"newly_installed": int(result[2]),
|
|
151
|
+
"removed": int(result[3]),
|
|
152
|
+
"not_upgraded": int(result[4]),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# We did not find the line we expected:
|
|
156
|
+
raise Exception("Did not find proposed changes in output")
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Optional, TypedDict, Union
|
|
3
|
+
|
|
4
|
+
from typing_extensions import NotRequired
|
|
5
|
+
|
|
6
|
+
from pyinfra.api import FactBase
|
|
7
|
+
from pyinfra.api.util import try_int
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CrontabDict(TypedDict):
|
|
11
|
+
command: NotRequired[str]
|
|
12
|
+
# handles cases like CRON_TZ=UTC
|
|
13
|
+
env: NotRequired[str]
|
|
14
|
+
minute: NotRequired[Union[int, str]]
|
|
15
|
+
hour: NotRequired[Union[int, str]]
|
|
16
|
+
month: NotRequired[Union[int, str]]
|
|
17
|
+
day_of_month: NotRequired[Union[int, str]]
|
|
18
|
+
day_of_week: NotRequired[Union[int, str]]
|
|
19
|
+
comments: NotRequired[List[str]]
|
|
20
|
+
special_time: NotRequired[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# for compatibility, also keeps a dict of command -> crontab dict
|
|
24
|
+
class CrontabFile:
|
|
25
|
+
commands: List[CrontabDict]
|
|
26
|
+
|
|
27
|
+
def __init__(self, input_dict: Optional[Dict[str, CrontabDict]] = None):
|
|
28
|
+
super().__init__()
|
|
29
|
+
self.commands = []
|
|
30
|
+
if input_dict:
|
|
31
|
+
for command, others in input_dict.items():
|
|
32
|
+
val = others.copy()
|
|
33
|
+
val["command"] = command
|
|
34
|
+
self.add_item(val)
|
|
35
|
+
|
|
36
|
+
def add_item(self, item: CrontabDict):
|
|
37
|
+
self.commands.append(item)
|
|
38
|
+
|
|
39
|
+
def __len__(self):
|
|
40
|
+
return len(self.commands)
|
|
41
|
+
|
|
42
|
+
def __bool__(self):
|
|
43
|
+
return len(self) > 0
|
|
44
|
+
|
|
45
|
+
def items(self):
|
|
46
|
+
return {item.get("command") or item.get("env"): item for item in self.commands}
|
|
47
|
+
|
|
48
|
+
def get_command(
|
|
49
|
+
self, command: Optional[str] = None, name: Optional[str] = None
|
|
50
|
+
) -> Optional[CrontabDict]:
|
|
51
|
+
assert command or name, "Either command or name must be provided"
|
|
52
|
+
|
|
53
|
+
name_comment = "# pyinfra-name={0}".format(name)
|
|
54
|
+
for cmd in self.commands:
|
|
55
|
+
if cmd.get("command") == command:
|
|
56
|
+
return cmd
|
|
57
|
+
if cmd.get("comments") and name_comment in cmd["comments"]:
|
|
58
|
+
return cmd
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def get_env(self, env: str) -> Optional[CrontabDict]:
|
|
62
|
+
for cmd in self.commands:
|
|
63
|
+
if cmd.get("env") == env:
|
|
64
|
+
return cmd
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def get(self, item: str) -> Optional[CrontabDict]:
|
|
68
|
+
return self.get_command(command=item, name=item) or self.get_env(item)
|
|
69
|
+
|
|
70
|
+
def __getitem__(self, item) -> Optional[CrontabDict]:
|
|
71
|
+
return self.get(item)
|
|
72
|
+
|
|
73
|
+
def __repr__(self):
|
|
74
|
+
return f"CrontabResult({self.commands})"
|
|
75
|
+
|
|
76
|
+
# noinspection PyMethodMayBeStatic
|
|
77
|
+
def format_item(self, item: CrontabDict):
|
|
78
|
+
lines = []
|
|
79
|
+
for comment in item.get("comments", []):
|
|
80
|
+
lines.append(comment)
|
|
81
|
+
|
|
82
|
+
if "env" in item:
|
|
83
|
+
lines.append(item["env"])
|
|
84
|
+
elif "special_time" in item:
|
|
85
|
+
lines.append(f"{item['special_time']} {item['command']}")
|
|
86
|
+
else:
|
|
87
|
+
lines.append(
|
|
88
|
+
f"{item['minute']} {item['hour']} "
|
|
89
|
+
f"{item['day_of_month']} {item['month']} {item['day_of_week']} "
|
|
90
|
+
f"{item['command']}"
|
|
91
|
+
)
|
|
92
|
+
return "\n".join(lines)
|
|
93
|
+
|
|
94
|
+
def __str__(self):
|
|
95
|
+
return "\n".join(self.format_item(item) for item in self.commands)
|
|
96
|
+
|
|
97
|
+
def to_json(self):
|
|
98
|
+
return self.commands
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
_crontab_env_re = re.compile(r"^\s*([A-Z_]+)=(.*)$")
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Crontab(FactBase[CrontabFile]):
|
|
105
|
+
"""
|
|
106
|
+
Returns a dictionary of CrontabFile.
|
|
107
|
+
|
|
108
|
+
.. code:: python
|
|
109
|
+
|
|
110
|
+
# CrontabFile.items()
|
|
111
|
+
{
|
|
112
|
+
"/path/to/command": {
|
|
113
|
+
"minute": "*",
|
|
114
|
+
"hour": "*",
|
|
115
|
+
"month": "*",
|
|
116
|
+
"day_of_month": "*",
|
|
117
|
+
"day_of_week": "*",
|
|
118
|
+
},
|
|
119
|
+
"echo another command": {
|
|
120
|
+
"special_time": "@daily",
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
# or CrontabFile.to_json()
|
|
124
|
+
[
|
|
125
|
+
{
|
|
126
|
+
command: "/path/to/command",
|
|
127
|
+
minute: "*",
|
|
128
|
+
hour: "*",
|
|
129
|
+
month: "*",
|
|
130
|
+
day_of_month: "*",
|
|
131
|
+
day_of_week: "*",
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"command": "echo another command
|
|
135
|
+
"special_time": "@daily",
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
default = CrontabFile
|
|
141
|
+
|
|
142
|
+
def requires_command(self, user=None) -> str:
|
|
143
|
+
return "crontab"
|
|
144
|
+
|
|
145
|
+
def command(self, user=None):
|
|
146
|
+
if user:
|
|
147
|
+
return "crontab -l -u {0} || true".format(user)
|
|
148
|
+
return "crontab -l || true"
|
|
149
|
+
|
|
150
|
+
def process(self, output):
|
|
151
|
+
crons = CrontabFile()
|
|
152
|
+
current_comments = []
|
|
153
|
+
|
|
154
|
+
for line in output:
|
|
155
|
+
line = line.strip()
|
|
156
|
+
if not line or line.startswith("#"):
|
|
157
|
+
current_comments.append(line)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
if line.startswith("@"):
|
|
161
|
+
special_time, command = line.split(None, 1)
|
|
162
|
+
item = CrontabDict(
|
|
163
|
+
command=command,
|
|
164
|
+
special_time=special_time,
|
|
165
|
+
comments=current_comments,
|
|
166
|
+
)
|
|
167
|
+
crons.add_item(item)
|
|
168
|
+
|
|
169
|
+
elif _crontab_env_re.match(line):
|
|
170
|
+
# handle environment variables
|
|
171
|
+
item = CrontabDict(
|
|
172
|
+
env=line,
|
|
173
|
+
comments=current_comments,
|
|
174
|
+
)
|
|
175
|
+
crons.add_item(item)
|
|
176
|
+
else:
|
|
177
|
+
minute, hour, day_of_month, month, day_of_week, command = line.split(None, 5)
|
|
178
|
+
item = CrontabDict(
|
|
179
|
+
command=command,
|
|
180
|
+
minute=try_int(minute),
|
|
181
|
+
hour=try_int(hour),
|
|
182
|
+
month=try_int(month),
|
|
183
|
+
day_of_month=try_int(day_of_month),
|
|
184
|
+
day_of_week=try_int(day_of_week),
|
|
185
|
+
comments=current_comments,
|
|
186
|
+
)
|
|
187
|
+
crons.add_item(item)
|
|
188
|
+
|
|
189
|
+
current_comments = []
|
|
190
|
+
return crons
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Facts about Docker containers, volumes and networks. These facts give you information from the view
|
|
3
|
+
of the current inventory host. See the :doc:`../connectors/docker` to use Docker containers as
|
|
4
|
+
inventory directly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
1
7
|
from __future__ import annotations
|
|
2
8
|
|
|
3
9
|
import json
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple, TypedDict
|
|
4
|
+
|
|
5
|
+
from pyinfra.api import FactBase
|
|
6
|
+
|
|
7
|
+
BootEntry = Tuple[bool, str]
|
|
8
|
+
EFIBootMgrInfoDict = TypedDict(
|
|
9
|
+
"EFIBootMgrInfoDict",
|
|
10
|
+
{
|
|
11
|
+
"BootNext": Optional[int],
|
|
12
|
+
"BootCurrent": Optional[int],
|
|
13
|
+
"Timeout": Optional[int],
|
|
14
|
+
"BootOrder": Optional[List[int]],
|
|
15
|
+
"Entries": Dict[int, BootEntry],
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EFIBootMgr(FactBase[Optional[EFIBootMgrInfoDict]]):
|
|
21
|
+
"""
|
|
22
|
+
Returns information about the UEFI boot variables:
|
|
23
|
+
|
|
24
|
+
.. code:: python
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"BootNext": 6,
|
|
28
|
+
"BootCurrent": 6,
|
|
29
|
+
"Timeout": 0,
|
|
30
|
+
"BootOrder": [1,4,3],
|
|
31
|
+
"Entries": {
|
|
32
|
+
1: (True, "myefi1"),
|
|
33
|
+
2: (False, "myefi2.efi"),
|
|
34
|
+
3: (True, "myefi3.efi"),
|
|
35
|
+
4: (True, "grub2.efi"),
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def requires_command(self, *args: Any, **kwargs: Any) -> str:
|
|
41
|
+
return "efibootmgr"
|
|
42
|
+
|
|
43
|
+
def command(self) -> str:
|
|
44
|
+
# FIXME: Use '|| true' to properly handle the case where
|
|
45
|
+
# 'efibootmgr' is run on a non-UEFI system
|
|
46
|
+
return "efibootmgr || true"
|
|
47
|
+
|
|
48
|
+
def process(self, output: Iterable[str]) -> Optional[EFIBootMgrInfoDict]:
|
|
49
|
+
# This parsing code closely follows the printing code of efibootmgr
|
|
50
|
+
# at <https://github.com/rhboot/efibootmgr/blob/main/src/efibootmgr.c#L2020-L2048>
|
|
51
|
+
|
|
52
|
+
info: EFIBootMgrInfoDict = {
|
|
53
|
+
"BootNext": None,
|
|
54
|
+
"BootCurrent": None,
|
|
55
|
+
"Timeout": None,
|
|
56
|
+
"BootOrder": [],
|
|
57
|
+
"Entries": {},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
output = iter(output)
|
|
61
|
+
|
|
62
|
+
line: Optional[str] = next(output, None)
|
|
63
|
+
|
|
64
|
+
if line is None:
|
|
65
|
+
# efibootmgr run on a non-UEFI system, likely printed
|
|
66
|
+
# "EFI variables are not supported on this system."
|
|
67
|
+
# to stderr
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# 1. Maybe have BootNext
|
|
71
|
+
if line and line.startswith("BootNext: "):
|
|
72
|
+
info["BootNext"] = int(line[len("BootNext: ") :], 16)
|
|
73
|
+
line = next(output, None)
|
|
74
|
+
|
|
75
|
+
# 2. Maybe have BootCurrent
|
|
76
|
+
if line and line.startswith("BootCurrent: "):
|
|
77
|
+
info["BootCurrent"] = int(line[len("BootCurrent: ") :], 16)
|
|
78
|
+
line = next(output, None)
|
|
79
|
+
|
|
80
|
+
# 3. Maybe have Timeout
|
|
81
|
+
if line and line.startswith("Timeout: "):
|
|
82
|
+
info["Timeout"] = int(line[len("Timeout: ") : -len(" seconds")])
|
|
83
|
+
line = next(output, None)
|
|
84
|
+
|
|
85
|
+
# 4. `show_order`
|
|
86
|
+
if line and line.startswith("BootOrder: "):
|
|
87
|
+
entries = line[len("BootOrder: ") :]
|
|
88
|
+
info["BootOrder"] = list(map(lambda x: int(x, 16), entries.split(",")))
|
|
89
|
+
line = next(output, None)
|
|
90
|
+
|
|
91
|
+
# 5. `show_vars`: The actual boot entries
|
|
92
|
+
while line is not None and line.startswith("Boot"):
|
|
93
|
+
number = int(line[4:8], 16)
|
|
94
|
+
|
|
95
|
+
# Entries marked with a * are active
|
|
96
|
+
active = line[8:9] == "*"
|
|
97
|
+
|
|
98
|
+
# TODO: Maybe split and parse (name vs. arguments ?), might require --verbose ?
|
|
99
|
+
entry = line[10:]
|
|
100
|
+
info["Entries"][number] = (active, entry)
|
|
101
|
+
line = next(output, None)
|
|
102
|
+
|
|
103
|
+
# 6. `show_mirror`
|
|
104
|
+
# Currently not implemented, since I haven't actually encountered this in the wild.
|
|
105
|
+
if line is not None:
|
|
106
|
+
raise ValueError(f"Unexpected line '{line}' while parsing")
|
|
107
|
+
|
|
108
|
+
return info
|