droch-shuil 0.2.0__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.
- droch_shuil-0.2.0/LICENSE +21 -0
- droch_shuil-0.2.0/PKG-INFO +67 -0
- droch_shuil-0.2.0/README.md +43 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/PKG-INFO +67 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/SOURCES.txt +102 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/dependency_links.txt +1 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/entry_points.txt +6 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/requires.txt +8 -0
- droch_shuil-0.2.0/droch_shuil.egg-info/top_level.txt +1 -0
- droch_shuil-0.2.0/pyproject.toml +47 -0
- droch_shuil-0.2.0/setup.cfg +4 -0
- droch_shuil-0.2.0/suil/__init__.py +1 -0
- droch_shuil-0.2.0/suil/__main__.py +4 -0
- droch_shuil-0.2.0/suil/cli.py +186 -0
- droch_shuil-0.2.0/suil/config.py +16 -0
- droch_shuil-0.2.0/suil/deployment.py +132 -0
- droch_shuil-0.2.0/suil/directory.py +209 -0
- droch_shuil-0.2.0/suil/errors.py +52 -0
- droch_shuil-0.2.0/suil/log.py +47 -0
- droch_shuil-0.2.0/suil/sdk/__init__.py +31 -0
- droch_shuil-0.2.0/suil/sdk/errors.py +47 -0
- droch_shuil-0.2.0/suil/sdk/facter.py +70 -0
- droch_shuil-0.2.0/suil/sdk/libs/__init__.py +6 -0
- droch_shuil-0.2.0/suil/sdk/libs/config/__init__.py +4 -0
- droch_shuil-0.2.0/suil/sdk/libs/config/expand_lookups.py +39 -0
- droch_shuil-0.2.0/suil/sdk/libs/config/strip_secrets.py +42 -0
- droch_shuil-0.2.0/suil/sdk/libs/host/__init__.py +5 -0
- droch_shuil-0.2.0/suil/sdk/libs/host/put_content.py +10 -0
- droch_shuil-0.2.0/suil/sdk/libs/host/run_command.py +12 -0
- droch_shuil-0.2.0/suil/sdk/libs/host/sudo_password.py +12 -0
- droch_shuil-0.2.0/suil/sdk/libs/inventory/__init__.py +3 -0
- droch_shuil-0.2.0/suil/sdk/libs/inventory/lookup.py +57 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/__init__.py +21 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/check_manifest.py +70 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/get_checkout.py +8 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/get_latest_tag.py +14 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/get_revision.py +25 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/get_roots.py +17 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/install_repo.py +37 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/read_manifest.py +18 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/run_git.py +8 -0
- droch_shuil-0.2.0/suil/sdk/libs/manifest/set_versions.py +32 -0
- droch_shuil-0.2.0/suil/sdk/libs/merge/__init__.py +3 -0
- droch_shuil-0.2.0/suil/sdk/libs/merge/deep.py +98 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/__init__.py +49 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/check_facter.py +66 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/collect_facts.py +76 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/collect_files.py +38 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/diff_configs.py +27 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/diff_names.py +10 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/diff_touches.py +9 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/file_digest.py +9 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/file_needs_write.py +14 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_config.py +73 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_defaults.py +13 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_facts.py +7 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_files.py +6 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_names.py +8 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_order.py +40 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_public.py +6 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_requires.py +14 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/get_signature.py +36 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/load_class.py +47 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/load_code.py +58 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/run_check.py +6 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/run_code.py +17 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/run_drift.py +10 -0
- droch_shuil-0.2.0/suil/sdk/libs/module/validate_file.py +29 -0
- droch_shuil-0.2.0/suil/sdk/libs/node/__init__.py +5 -0
- droch_shuil-0.2.0/suil/sdk/libs/node/collect.py +10 -0
- droch_shuil-0.2.0/suil/sdk/libs/node/get_role.py +9 -0
- droch_shuil-0.2.0/suil/sdk/libs/node/probe_os.py +47 -0
- droch_shuil-0.2.0/suil/sdk/libs/pyinfra/__init__.py +4 -0
- droch_shuil-0.2.0/suil/sdk/libs/pyinfra/make_inventory.py +28 -0
- droch_shuil-0.2.0/suil/sdk/libs/pyinfra/run_state.py +125 -0
- droch_shuil-0.2.0/suil/sdk/libs/role/__init__.py +4 -0
- droch_shuil-0.2.0/suil/sdk/libs/role/collect.py +11 -0
- droch_shuil-0.2.0/suil/sdk/libs/role/get_nodes.py +15 -0
- droch_shuil-0.2.0/suil/sdk/libs/run/__init__.py +3 -0
- droch_shuil-0.2.0/suil/sdk/libs/run/build_directory.py +63 -0
- droch_shuil-0.2.0/suil/sdk/libs/string/__init__.py +4 -0
- droch_shuil-0.2.0/suil/sdk/libs/string/decrypt.py +28 -0
- droch_shuil-0.2.0/suil/sdk/libs/string/encrypt.py +21 -0
- droch_shuil-0.2.0/suil/sdk/libs/yaml/__init__.py +4 -0
- droch_shuil-0.2.0/suil/sdk/libs/yaml/load.py +81 -0
- droch_shuil-0.2.0/suil/sdk/libs/yaml/load_data.py +35 -0
- droch_shuil-0.2.0/suil/sdk/models/__init__.py +23 -0
- droch_shuil-0.2.0/suil/sdk/models/config.py +11 -0
- droch_shuil-0.2.0/suil/sdk/models/context.py +13 -0
- droch_shuil-0.2.0/suil/sdk/models/encrypted.py +25 -0
- droch_shuil-0.2.0/suil/sdk/models/lookup.py +47 -0
- droch_shuil-0.2.0/suil/sdk/models/manifest.py +20 -0
- droch_shuil-0.2.0/suil/sdk/models/repo.py +30 -0
- droch_shuil-0.2.0/suil/sdk/models/root.py +16 -0
- droch_shuil-0.2.0/suil/sdk/models/secret.py +29 -0
- droch_shuil-0.2.0/suil/sdk/models/tagged.py +18 -0
- droch_shuil-0.2.0/suil/sdk/module.py +36 -0
- droch_shuil-0.2.0/suil/sdk/protocols.py +33 -0
- droch_shuil-0.2.0/suil/testing.py +44 -0
- droch_shuil-0.2.0/suil/tui.py +38 -0
- droch_shuil-0.2.0/suil/workspace.py +31 -0
- droch_shuil-0.2.0/tests/test_modules_import_sdk.py +24 -0
- droch_shuil-0.2.0/tests/test_modules_read_facts.py +24 -0
- droch_shuil-0.2.0/tests/test_workspace.py +67 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oberon Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: droch-shuil
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Declarative Linux host configuration on top of pyinfra
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://oberon-systems.github.io/droch-shuil/
|
|
7
|
+
Project-URL: Source, https://github.com/oberon-systems/droch-shuil
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: System :: Systems Administration
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: click>=8.5
|
|
16
|
+
Requires-Dist: platformdirs>=4.11
|
|
17
|
+
Requires-Dist: pydantic>=2.13
|
|
18
|
+
Requires-Dist: pydantic-settings>=2.15
|
|
19
|
+
Requires-Dist: pyinfra<4,>=3.10
|
|
20
|
+
Requires-Dist: pyrage>=1.4
|
|
21
|
+
Requires-Dist: PyYAML>=6.0
|
|
22
|
+
Requires-Dist: questionary>=2.1
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# suil
|
|
26
|
+
|
|
27
|
+
*suil* is the Irish and Scottish Gaelic word for eye or vision, and
|
|
28
|
+
*drochshuil* is the evil eye, the eye of the Fomorian king in the old myths.
|
|
29
|
+
The package is named after it.
|
|
30
|
+
|
|
31
|
+
suil provisions Linux hosts from declarative data, on top of
|
|
32
|
+
[pyinfra](https://pyinfra.com). A workspace holds the site data and declares
|
|
33
|
+
its modules in `modules.yaml`; suil resolves them into the configuration of
|
|
34
|
+
each node, collects facts, applies only what differs and checks that the node
|
|
35
|
+
converged.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install droch-shuil
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The package is published as `droch-shuil` and imported as `suil`. It installs
|
|
44
|
+
the same command under two names, `suil` and `droch-shuil`.
|
|
45
|
+
|
|
46
|
+
## Development
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
make install
|
|
50
|
+
make test
|
|
51
|
+
make lint
|
|
52
|
+
make docs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`make install` creates `.venv`, installs the tooling from `requirements.txt`
|
|
56
|
+
and the pre-commit hooks. Commits go through commitizen: `cz commit`.
|
|
57
|
+
|
|
58
|
+
## Releases
|
|
59
|
+
|
|
60
|
+
A `vX.Y.Z` tag that matches the version in `pyproject.toml` builds the sdist
|
|
61
|
+
and the wheel and publishes them to [PyPI](https://pypi.org/project/droch-shuil/).
|
|
62
|
+
The documentation is rebuilt and published to GitHub Pages on every push to
|
|
63
|
+
`main` that changes `docs/`, `README.md` or `mkdocs.yml`.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# suil
|
|
2
|
+
|
|
3
|
+
*suil* is the Irish and Scottish Gaelic word for eye or vision, and
|
|
4
|
+
*drochshuil* is the evil eye, the eye of the Fomorian king in the old myths.
|
|
5
|
+
The package is named after it.
|
|
6
|
+
|
|
7
|
+
suil provisions Linux hosts from declarative data, on top of
|
|
8
|
+
[pyinfra](https://pyinfra.com). A workspace holds the site data and declares
|
|
9
|
+
its modules in `modules.yaml`; suil resolves them into the configuration of
|
|
10
|
+
each node, collects facts, applies only what differs and checks that the node
|
|
11
|
+
converged.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install droch-shuil
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The package is published as `droch-shuil` and imported as `suil`. It installs
|
|
20
|
+
the same command under two names, `suil` and `droch-shuil`.
|
|
21
|
+
|
|
22
|
+
## Development
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
make install
|
|
26
|
+
make test
|
|
27
|
+
make lint
|
|
28
|
+
make docs
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`make install` creates `.venv`, installs the tooling from `requirements.txt`
|
|
32
|
+
and the pre-commit hooks. Commits go through commitizen: `cz commit`.
|
|
33
|
+
|
|
34
|
+
## Releases
|
|
35
|
+
|
|
36
|
+
A `vX.Y.Z` tag that matches the version in `pyproject.toml` builds the sdist
|
|
37
|
+
and the wheel and publishes them to [PyPI](https://pypi.org/project/droch-shuil/).
|
|
38
|
+
The documentation is rebuilt and published to GitHub Pages on every push to
|
|
39
|
+
`main` that changes `docs/`, `README.md` or `mkdocs.yml`.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: droch-shuil
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Declarative Linux host configuration on top of pyinfra
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://oberon-systems.github.io/droch-shuil/
|
|
7
|
+
Project-URL: Source, https://github.com/oberon-systems/droch-shuil
|
|
8
|
+
Classifier: Environment :: Console
|
|
9
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: System :: Systems Administration
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: click>=8.5
|
|
16
|
+
Requires-Dist: platformdirs>=4.11
|
|
17
|
+
Requires-Dist: pydantic>=2.13
|
|
18
|
+
Requires-Dist: pydantic-settings>=2.15
|
|
19
|
+
Requires-Dist: pyinfra<4,>=3.10
|
|
20
|
+
Requires-Dist: pyrage>=1.4
|
|
21
|
+
Requires-Dist: PyYAML>=6.0
|
|
22
|
+
Requires-Dist: questionary>=2.1
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# suil
|
|
26
|
+
|
|
27
|
+
*suil* is the Irish and Scottish Gaelic word for eye or vision, and
|
|
28
|
+
*drochshuil* is the evil eye, the eye of the Fomorian king in the old myths.
|
|
29
|
+
The package is named after it.
|
|
30
|
+
|
|
31
|
+
suil provisions Linux hosts from declarative data, on top of
|
|
32
|
+
[pyinfra](https://pyinfra.com). A workspace holds the site data and declares
|
|
33
|
+
its modules in `modules.yaml`; suil resolves them into the configuration of
|
|
34
|
+
each node, collects facts, applies only what differs and checks that the node
|
|
35
|
+
converged.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install droch-shuil
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The package is published as `droch-shuil` and imported as `suil`. It installs
|
|
44
|
+
the same command under two names, `suil` and `droch-shuil`.
|
|
45
|
+
|
|
46
|
+
## Development
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
make install
|
|
50
|
+
make test
|
|
51
|
+
make lint
|
|
52
|
+
make docs
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`make install` creates `.venv`, installs the tooling from `requirements.txt`
|
|
56
|
+
and the pre-commit hooks. Commits go through commitizen: `cz commit`.
|
|
57
|
+
|
|
58
|
+
## Releases
|
|
59
|
+
|
|
60
|
+
A `vX.Y.Z` tag that matches the version in `pyproject.toml` builds the sdist
|
|
61
|
+
and the wheel and publishes them to [PyPI](https://pypi.org/project/droch-shuil/).
|
|
62
|
+
The documentation is rebuilt and published to GitHub Pages on every push to
|
|
63
|
+
`main` that changes `docs/`, `README.md` or `mkdocs.yml`.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
droch_shuil.egg-info/PKG-INFO
|
|
5
|
+
droch_shuil.egg-info/SOURCES.txt
|
|
6
|
+
droch_shuil.egg-info/dependency_links.txt
|
|
7
|
+
droch_shuil.egg-info/entry_points.txt
|
|
8
|
+
droch_shuil.egg-info/requires.txt
|
|
9
|
+
droch_shuil.egg-info/top_level.txt
|
|
10
|
+
suil/__init__.py
|
|
11
|
+
suil/__main__.py
|
|
12
|
+
suil/cli.py
|
|
13
|
+
suil/config.py
|
|
14
|
+
suil/deployment.py
|
|
15
|
+
suil/directory.py
|
|
16
|
+
suil/errors.py
|
|
17
|
+
suil/log.py
|
|
18
|
+
suil/testing.py
|
|
19
|
+
suil/tui.py
|
|
20
|
+
suil/workspace.py
|
|
21
|
+
suil/sdk/__init__.py
|
|
22
|
+
suil/sdk/errors.py
|
|
23
|
+
suil/sdk/facter.py
|
|
24
|
+
suil/sdk/module.py
|
|
25
|
+
suil/sdk/protocols.py
|
|
26
|
+
suil/sdk/libs/__init__.py
|
|
27
|
+
suil/sdk/libs/config/__init__.py
|
|
28
|
+
suil/sdk/libs/config/expand_lookups.py
|
|
29
|
+
suil/sdk/libs/config/strip_secrets.py
|
|
30
|
+
suil/sdk/libs/host/__init__.py
|
|
31
|
+
suil/sdk/libs/host/put_content.py
|
|
32
|
+
suil/sdk/libs/host/run_command.py
|
|
33
|
+
suil/sdk/libs/host/sudo_password.py
|
|
34
|
+
suil/sdk/libs/inventory/__init__.py
|
|
35
|
+
suil/sdk/libs/inventory/lookup.py
|
|
36
|
+
suil/sdk/libs/manifest/__init__.py
|
|
37
|
+
suil/sdk/libs/manifest/check_manifest.py
|
|
38
|
+
suil/sdk/libs/manifest/get_checkout.py
|
|
39
|
+
suil/sdk/libs/manifest/get_latest_tag.py
|
|
40
|
+
suil/sdk/libs/manifest/get_revision.py
|
|
41
|
+
suil/sdk/libs/manifest/get_roots.py
|
|
42
|
+
suil/sdk/libs/manifest/install_repo.py
|
|
43
|
+
suil/sdk/libs/manifest/read_manifest.py
|
|
44
|
+
suil/sdk/libs/manifest/run_git.py
|
|
45
|
+
suil/sdk/libs/manifest/set_versions.py
|
|
46
|
+
suil/sdk/libs/merge/__init__.py
|
|
47
|
+
suil/sdk/libs/merge/deep.py
|
|
48
|
+
suil/sdk/libs/module/__init__.py
|
|
49
|
+
suil/sdk/libs/module/check_facter.py
|
|
50
|
+
suil/sdk/libs/module/collect_facts.py
|
|
51
|
+
suil/sdk/libs/module/collect_files.py
|
|
52
|
+
suil/sdk/libs/module/diff_configs.py
|
|
53
|
+
suil/sdk/libs/module/diff_names.py
|
|
54
|
+
suil/sdk/libs/module/diff_touches.py
|
|
55
|
+
suil/sdk/libs/module/file_digest.py
|
|
56
|
+
suil/sdk/libs/module/file_needs_write.py
|
|
57
|
+
suil/sdk/libs/module/get_config.py
|
|
58
|
+
suil/sdk/libs/module/get_defaults.py
|
|
59
|
+
suil/sdk/libs/module/get_facts.py
|
|
60
|
+
suil/sdk/libs/module/get_files.py
|
|
61
|
+
suil/sdk/libs/module/get_names.py
|
|
62
|
+
suil/sdk/libs/module/get_order.py
|
|
63
|
+
suil/sdk/libs/module/get_public.py
|
|
64
|
+
suil/sdk/libs/module/get_requires.py
|
|
65
|
+
suil/sdk/libs/module/get_signature.py
|
|
66
|
+
suil/sdk/libs/module/load_class.py
|
|
67
|
+
suil/sdk/libs/module/load_code.py
|
|
68
|
+
suil/sdk/libs/module/run_check.py
|
|
69
|
+
suil/sdk/libs/module/run_code.py
|
|
70
|
+
suil/sdk/libs/module/run_drift.py
|
|
71
|
+
suil/sdk/libs/module/validate_file.py
|
|
72
|
+
suil/sdk/libs/node/__init__.py
|
|
73
|
+
suil/sdk/libs/node/collect.py
|
|
74
|
+
suil/sdk/libs/node/get_role.py
|
|
75
|
+
suil/sdk/libs/node/probe_os.py
|
|
76
|
+
suil/sdk/libs/pyinfra/__init__.py
|
|
77
|
+
suil/sdk/libs/pyinfra/make_inventory.py
|
|
78
|
+
suil/sdk/libs/pyinfra/run_state.py
|
|
79
|
+
suil/sdk/libs/role/__init__.py
|
|
80
|
+
suil/sdk/libs/role/collect.py
|
|
81
|
+
suil/sdk/libs/role/get_nodes.py
|
|
82
|
+
suil/sdk/libs/run/__init__.py
|
|
83
|
+
suil/sdk/libs/run/build_directory.py
|
|
84
|
+
suil/sdk/libs/string/__init__.py
|
|
85
|
+
suil/sdk/libs/string/decrypt.py
|
|
86
|
+
suil/sdk/libs/string/encrypt.py
|
|
87
|
+
suil/sdk/libs/yaml/__init__.py
|
|
88
|
+
suil/sdk/libs/yaml/load.py
|
|
89
|
+
suil/sdk/libs/yaml/load_data.py
|
|
90
|
+
suil/sdk/models/__init__.py
|
|
91
|
+
suil/sdk/models/config.py
|
|
92
|
+
suil/sdk/models/context.py
|
|
93
|
+
suil/sdk/models/encrypted.py
|
|
94
|
+
suil/sdk/models/lookup.py
|
|
95
|
+
suil/sdk/models/manifest.py
|
|
96
|
+
suil/sdk/models/repo.py
|
|
97
|
+
suil/sdk/models/root.py
|
|
98
|
+
suil/sdk/models/secret.py
|
|
99
|
+
suil/sdk/models/tagged.py
|
|
100
|
+
tests/test_modules_import_sdk.py
|
|
101
|
+
tests/test_modules_read_facts.py
|
|
102
|
+
tests/test_workspace.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
suil
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "droch-shuil"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Declarative Linux host configuration on top of pyinfra"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Operating System :: POSIX :: Linux",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Topic :: System :: Systems Administration",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"click>=8.5",
|
|
20
|
+
"platformdirs>=4.11",
|
|
21
|
+
"pydantic>=2.13",
|
|
22
|
+
"pydantic-settings>=2.15",
|
|
23
|
+
"pyinfra>=3.10,<4",
|
|
24
|
+
"pyrage>=1.4",
|
|
25
|
+
"PyYAML>=6.0",
|
|
26
|
+
"questionary>=2.1",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Documentation = "https://oberon-systems.github.io/droch-shuil/"
|
|
31
|
+
Source = "https://github.com/oberon-systems/droch-shuil"
|
|
32
|
+
|
|
33
|
+
# PyPI refuses `suil` as too close to an existing name; the commands keep it.
|
|
34
|
+
[project.scripts]
|
|
35
|
+
suil = "suil.cli:cli"
|
|
36
|
+
droch-shuil = "suil.cli:cli"
|
|
37
|
+
|
|
38
|
+
# A module's tests get the fixtures without a conftest of their own, wherever
|
|
39
|
+
# they live. Registered at install time, so `pip install -e .` again.
|
|
40
|
+
[project.entry-points.pytest11]
|
|
41
|
+
suil = "suil.testing"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
include = ["suil*"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Suil: declarative Linux host configuration on top of pyinfra."""
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from suil.config import Config
|
|
7
|
+
from suil.deployment import Deployment
|
|
8
|
+
from suil.directory import Directory, counted
|
|
9
|
+
from suil.errors import errors_handler
|
|
10
|
+
from suil.log import OK, log_setup
|
|
11
|
+
from suil.sdk.libs.manifest import check_manifest, get_latest_tag, install_repo, read_manifest, set_versions
|
|
12
|
+
from suil.sdk.libs.module import get_order, get_requires
|
|
13
|
+
from suil.sdk.libs.string import decrypt, encrypt
|
|
14
|
+
from suil.tui import tui
|
|
15
|
+
from suil.workspace import Workspace
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _selection(function):
|
|
21
|
+
function = click.option('--role', help='Role to act on.')(function)
|
|
22
|
+
function = click.option('--node', help='Node to act on.')(function)
|
|
23
|
+
|
|
24
|
+
return function
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.group(invoke_without_command=True)
|
|
28
|
+
@click.pass_context
|
|
29
|
+
def main(ctx):
|
|
30
|
+
"""Suil: an infrastructure manager."""
|
|
31
|
+
workspace = Workspace()
|
|
32
|
+
settings = Config()
|
|
33
|
+
log_setup(settings.log_color)
|
|
34
|
+
|
|
35
|
+
if ctx.invoked_subcommand is None:
|
|
36
|
+
tui(workspace, settings)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
ctx.obj = (workspace, settings)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@main.command()
|
|
43
|
+
@_selection
|
|
44
|
+
@click.option('--force', is_flag=True, help='Run the modules even when nothing changed.')
|
|
45
|
+
@click.option('--confirm', is_flag=True, help='Apply; without it the run is only shown.')
|
|
46
|
+
@click.option('--dry-run', is_flag=True, help='Show what would change and stop.')
|
|
47
|
+
@click.pass_obj
|
|
48
|
+
def apply(obj, role, node, force, confirm, dry_run):
|
|
49
|
+
"""Apply the modules of the selected role or node."""
|
|
50
|
+
workspace, settings = obj
|
|
51
|
+
directory = Directory(workspace, role=role, node=node)
|
|
52
|
+
directory.show()
|
|
53
|
+
|
|
54
|
+
if not confirm:
|
|
55
|
+
log.warning('not applied: give --confirm')
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
Deployment(settings, directory, force=force, dry_run=dry_run).run()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@main.command()
|
|
62
|
+
@_selection
|
|
63
|
+
@click.pass_obj
|
|
64
|
+
def config(obj, role, node):
|
|
65
|
+
"""Print the run without applying anything."""
|
|
66
|
+
workspace, settings = obj
|
|
67
|
+
directory = Directory(workspace, role=role, node=node)
|
|
68
|
+
deployment = Deployment(settings, directory)
|
|
69
|
+
directory.show()
|
|
70
|
+
|
|
71
|
+
for item in directory.nodes:
|
|
72
|
+
log.info(f'--- {item.name}')
|
|
73
|
+
print(yaml.safe_dump(deployment.configs(item)[1], default_flow_style=False, sort_keys=False))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@main.command()
|
|
77
|
+
@_selection
|
|
78
|
+
@click.pass_obj
|
|
79
|
+
def facts(obj, role, node):
|
|
80
|
+
"""Collect facts, change nothing."""
|
|
81
|
+
workspace, settings = obj
|
|
82
|
+
directory = Directory(workspace, role=role, node=node)
|
|
83
|
+
deployment = Deployment(settings, directory)
|
|
84
|
+
|
|
85
|
+
for item in directory.nodes:
|
|
86
|
+
_, host, probe = deployment.connect(item)
|
|
87
|
+
item = directory.probe(item.name, probe['family'], probe['release'])
|
|
88
|
+
deployment.facts(host, item, *deployment.configs(item))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@main.command()
|
|
92
|
+
@click.pass_obj
|
|
93
|
+
def nodes(obj):
|
|
94
|
+
"""List the nodes and the role each declares."""
|
|
95
|
+
workspace, _ = obj
|
|
96
|
+
|
|
97
|
+
for name, role in sorted(Directory(workspace).inventory.items()):
|
|
98
|
+
log.info(f'{name} ({role or "no role"})')
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@main.command()
|
|
102
|
+
@click.pass_obj
|
|
103
|
+
def modules(obj):
|
|
104
|
+
"""List the modules and the requires graph."""
|
|
105
|
+
workspace, _ = obj
|
|
106
|
+
roots = _roots(workspace)
|
|
107
|
+
|
|
108
|
+
for name, root in sorted(roots.items()):
|
|
109
|
+
requires = get_requires(root)
|
|
110
|
+
kind = 'meta' if not (root.path / 'code' / 'main.py').is_file() else 'module'
|
|
111
|
+
log.info(f'{name} [{kind}]' + (f' {root.repo} {root.version}' if root.version else ''))
|
|
112
|
+
|
|
113
|
+
if requires:
|
|
114
|
+
log.info(' requires: ' + ', '.join(requires))
|
|
115
|
+
log.info(' order: ' + ' -> '.join(get_order([name], roots)))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@main.command()
|
|
119
|
+
@click.pass_obj
|
|
120
|
+
def install(obj):
|
|
121
|
+
"""Clone the Git repositories of modules.yaml into the cache."""
|
|
122
|
+
workspace, _ = obj
|
|
123
|
+
|
|
124
|
+
for repo in read_manifest(workspace.manifest).repos:
|
|
125
|
+
if not repo.local:
|
|
126
|
+
installed = install_repo(repo, workspace.cache_dir)
|
|
127
|
+
log.log(OK, f"{repo.repo} {repo.version}: {'installed' if installed else 'in the cache'}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@main.command()
|
|
131
|
+
@click.pass_obj
|
|
132
|
+
def autoupdate(obj):
|
|
133
|
+
"""Move every version in modules.yaml to the newest tag of its repository."""
|
|
134
|
+
workspace, _ = obj
|
|
135
|
+
versions = {}
|
|
136
|
+
|
|
137
|
+
for repo in read_manifest(workspace.manifest).repos:
|
|
138
|
+
if repo.local:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if not (latest := get_latest_tag(repo.repo)):
|
|
142
|
+
log.warning(f'{repo.repo}: no tags, {repo.version} kept')
|
|
143
|
+
elif latest != repo.version:
|
|
144
|
+
log.info(f'{repo.repo}: {repo.version} -> {latest}')
|
|
145
|
+
versions[repo.repo] = latest
|
|
146
|
+
|
|
147
|
+
if versions:
|
|
148
|
+
workspace.manifest.write_text(set_versions(workspace.manifest.read_text(), versions))
|
|
149
|
+
log.warning('modules.yaml updated: run suil install')
|
|
150
|
+
else:
|
|
151
|
+
log.log(OK, 'modules.yaml is up to date')
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@main.command()
|
|
155
|
+
@click.pass_obj
|
|
156
|
+
def validate(obj):
|
|
157
|
+
"""Check modules.yaml and every module it declares, without connecting."""
|
|
158
|
+
workspace, _ = obj
|
|
159
|
+
log.log(OK, f"modules.yaml: {counted(len(_roots(workspace)), 'module')} valid")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@main.command('encrypt')
|
|
163
|
+
@click.argument('value')
|
|
164
|
+
@click.pass_obj
|
|
165
|
+
def encrypt_command(obj, value):
|
|
166
|
+
"""Encrypt a string to SUIL_AGE_RECIPIENT."""
|
|
167
|
+
_, settings = obj
|
|
168
|
+
print(f'!ENC[{encrypt(value, settings.age_recipient)}]')
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@main.command('decrypt')
|
|
172
|
+
@click.argument('value')
|
|
173
|
+
@click.pass_obj
|
|
174
|
+
def decrypt_command(obj, value):
|
|
175
|
+
"""Decrypt a string with SUIL_AGE_KEY."""
|
|
176
|
+
_, settings = obj
|
|
177
|
+
print(decrypt(value, settings.age_key))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _roots(workspace):
|
|
181
|
+
return check_manifest(workspace.manifest, workspace.modules_dir, workspace.cache_dir)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@errors_handler
|
|
185
|
+
def cli():
|
|
186
|
+
main()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Config(BaseSettings):
|
|
5
|
+
|
|
6
|
+
model_config = SettingsConfigDict(env_prefix='SUIL_', env_file='.env')
|
|
7
|
+
|
|
8
|
+
# age keys, X25519. The private half decrypts, the public half encrypts.
|
|
9
|
+
age_key: str | None = None
|
|
10
|
+
age_recipient: str | None = None
|
|
11
|
+
|
|
12
|
+
# Sudo on the targets. Set it and pyinfra never prompts, mid-run or at all.
|
|
13
|
+
sudo_password: str | None = None
|
|
14
|
+
|
|
15
|
+
# Terminal output
|
|
16
|
+
log_color: bool = True
|