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.
Files changed (104) hide show
  1. droch_shuil-0.2.0/LICENSE +21 -0
  2. droch_shuil-0.2.0/PKG-INFO +67 -0
  3. droch_shuil-0.2.0/README.md +43 -0
  4. droch_shuil-0.2.0/droch_shuil.egg-info/PKG-INFO +67 -0
  5. droch_shuil-0.2.0/droch_shuil.egg-info/SOURCES.txt +102 -0
  6. droch_shuil-0.2.0/droch_shuil.egg-info/dependency_links.txt +1 -0
  7. droch_shuil-0.2.0/droch_shuil.egg-info/entry_points.txt +6 -0
  8. droch_shuil-0.2.0/droch_shuil.egg-info/requires.txt +8 -0
  9. droch_shuil-0.2.0/droch_shuil.egg-info/top_level.txt +1 -0
  10. droch_shuil-0.2.0/pyproject.toml +47 -0
  11. droch_shuil-0.2.0/setup.cfg +4 -0
  12. droch_shuil-0.2.0/suil/__init__.py +1 -0
  13. droch_shuil-0.2.0/suil/__main__.py +4 -0
  14. droch_shuil-0.2.0/suil/cli.py +186 -0
  15. droch_shuil-0.2.0/suil/config.py +16 -0
  16. droch_shuil-0.2.0/suil/deployment.py +132 -0
  17. droch_shuil-0.2.0/suil/directory.py +209 -0
  18. droch_shuil-0.2.0/suil/errors.py +52 -0
  19. droch_shuil-0.2.0/suil/log.py +47 -0
  20. droch_shuil-0.2.0/suil/sdk/__init__.py +31 -0
  21. droch_shuil-0.2.0/suil/sdk/errors.py +47 -0
  22. droch_shuil-0.2.0/suil/sdk/facter.py +70 -0
  23. droch_shuil-0.2.0/suil/sdk/libs/__init__.py +6 -0
  24. droch_shuil-0.2.0/suil/sdk/libs/config/__init__.py +4 -0
  25. droch_shuil-0.2.0/suil/sdk/libs/config/expand_lookups.py +39 -0
  26. droch_shuil-0.2.0/suil/sdk/libs/config/strip_secrets.py +42 -0
  27. droch_shuil-0.2.0/suil/sdk/libs/host/__init__.py +5 -0
  28. droch_shuil-0.2.0/suil/sdk/libs/host/put_content.py +10 -0
  29. droch_shuil-0.2.0/suil/sdk/libs/host/run_command.py +12 -0
  30. droch_shuil-0.2.0/suil/sdk/libs/host/sudo_password.py +12 -0
  31. droch_shuil-0.2.0/suil/sdk/libs/inventory/__init__.py +3 -0
  32. droch_shuil-0.2.0/suil/sdk/libs/inventory/lookup.py +57 -0
  33. droch_shuil-0.2.0/suil/sdk/libs/manifest/__init__.py +21 -0
  34. droch_shuil-0.2.0/suil/sdk/libs/manifest/check_manifest.py +70 -0
  35. droch_shuil-0.2.0/suil/sdk/libs/manifest/get_checkout.py +8 -0
  36. droch_shuil-0.2.0/suil/sdk/libs/manifest/get_latest_tag.py +14 -0
  37. droch_shuil-0.2.0/suil/sdk/libs/manifest/get_revision.py +25 -0
  38. droch_shuil-0.2.0/suil/sdk/libs/manifest/get_roots.py +17 -0
  39. droch_shuil-0.2.0/suil/sdk/libs/manifest/install_repo.py +37 -0
  40. droch_shuil-0.2.0/suil/sdk/libs/manifest/read_manifest.py +18 -0
  41. droch_shuil-0.2.0/suil/sdk/libs/manifest/run_git.py +8 -0
  42. droch_shuil-0.2.0/suil/sdk/libs/manifest/set_versions.py +32 -0
  43. droch_shuil-0.2.0/suil/sdk/libs/merge/__init__.py +3 -0
  44. droch_shuil-0.2.0/suil/sdk/libs/merge/deep.py +98 -0
  45. droch_shuil-0.2.0/suil/sdk/libs/module/__init__.py +49 -0
  46. droch_shuil-0.2.0/suil/sdk/libs/module/check_facter.py +66 -0
  47. droch_shuil-0.2.0/suil/sdk/libs/module/collect_facts.py +76 -0
  48. droch_shuil-0.2.0/suil/sdk/libs/module/collect_files.py +38 -0
  49. droch_shuil-0.2.0/suil/sdk/libs/module/diff_configs.py +27 -0
  50. droch_shuil-0.2.0/suil/sdk/libs/module/diff_names.py +10 -0
  51. droch_shuil-0.2.0/suil/sdk/libs/module/diff_touches.py +9 -0
  52. droch_shuil-0.2.0/suil/sdk/libs/module/file_digest.py +9 -0
  53. droch_shuil-0.2.0/suil/sdk/libs/module/file_needs_write.py +14 -0
  54. droch_shuil-0.2.0/suil/sdk/libs/module/get_config.py +73 -0
  55. droch_shuil-0.2.0/suil/sdk/libs/module/get_defaults.py +13 -0
  56. droch_shuil-0.2.0/suil/sdk/libs/module/get_facts.py +7 -0
  57. droch_shuil-0.2.0/suil/sdk/libs/module/get_files.py +6 -0
  58. droch_shuil-0.2.0/suil/sdk/libs/module/get_names.py +8 -0
  59. droch_shuil-0.2.0/suil/sdk/libs/module/get_order.py +40 -0
  60. droch_shuil-0.2.0/suil/sdk/libs/module/get_public.py +6 -0
  61. droch_shuil-0.2.0/suil/sdk/libs/module/get_requires.py +14 -0
  62. droch_shuil-0.2.0/suil/sdk/libs/module/get_signature.py +36 -0
  63. droch_shuil-0.2.0/suil/sdk/libs/module/load_class.py +47 -0
  64. droch_shuil-0.2.0/suil/sdk/libs/module/load_code.py +58 -0
  65. droch_shuil-0.2.0/suil/sdk/libs/module/run_check.py +6 -0
  66. droch_shuil-0.2.0/suil/sdk/libs/module/run_code.py +17 -0
  67. droch_shuil-0.2.0/suil/sdk/libs/module/run_drift.py +10 -0
  68. droch_shuil-0.2.0/suil/sdk/libs/module/validate_file.py +29 -0
  69. droch_shuil-0.2.0/suil/sdk/libs/node/__init__.py +5 -0
  70. droch_shuil-0.2.0/suil/sdk/libs/node/collect.py +10 -0
  71. droch_shuil-0.2.0/suil/sdk/libs/node/get_role.py +9 -0
  72. droch_shuil-0.2.0/suil/sdk/libs/node/probe_os.py +47 -0
  73. droch_shuil-0.2.0/suil/sdk/libs/pyinfra/__init__.py +4 -0
  74. droch_shuil-0.2.0/suil/sdk/libs/pyinfra/make_inventory.py +28 -0
  75. droch_shuil-0.2.0/suil/sdk/libs/pyinfra/run_state.py +125 -0
  76. droch_shuil-0.2.0/suil/sdk/libs/role/__init__.py +4 -0
  77. droch_shuil-0.2.0/suil/sdk/libs/role/collect.py +11 -0
  78. droch_shuil-0.2.0/suil/sdk/libs/role/get_nodes.py +15 -0
  79. droch_shuil-0.2.0/suil/sdk/libs/run/__init__.py +3 -0
  80. droch_shuil-0.2.0/suil/sdk/libs/run/build_directory.py +63 -0
  81. droch_shuil-0.2.0/suil/sdk/libs/string/__init__.py +4 -0
  82. droch_shuil-0.2.0/suil/sdk/libs/string/decrypt.py +28 -0
  83. droch_shuil-0.2.0/suil/sdk/libs/string/encrypt.py +21 -0
  84. droch_shuil-0.2.0/suil/sdk/libs/yaml/__init__.py +4 -0
  85. droch_shuil-0.2.0/suil/sdk/libs/yaml/load.py +81 -0
  86. droch_shuil-0.2.0/suil/sdk/libs/yaml/load_data.py +35 -0
  87. droch_shuil-0.2.0/suil/sdk/models/__init__.py +23 -0
  88. droch_shuil-0.2.0/suil/sdk/models/config.py +11 -0
  89. droch_shuil-0.2.0/suil/sdk/models/context.py +13 -0
  90. droch_shuil-0.2.0/suil/sdk/models/encrypted.py +25 -0
  91. droch_shuil-0.2.0/suil/sdk/models/lookup.py +47 -0
  92. droch_shuil-0.2.0/suil/sdk/models/manifest.py +20 -0
  93. droch_shuil-0.2.0/suil/sdk/models/repo.py +30 -0
  94. droch_shuil-0.2.0/suil/sdk/models/root.py +16 -0
  95. droch_shuil-0.2.0/suil/sdk/models/secret.py +29 -0
  96. droch_shuil-0.2.0/suil/sdk/models/tagged.py +18 -0
  97. droch_shuil-0.2.0/suil/sdk/module.py +36 -0
  98. droch_shuil-0.2.0/suil/sdk/protocols.py +33 -0
  99. droch_shuil-0.2.0/suil/testing.py +44 -0
  100. droch_shuil-0.2.0/suil/tui.py +38 -0
  101. droch_shuil-0.2.0/suil/workspace.py +31 -0
  102. droch_shuil-0.2.0/tests/test_modules_import_sdk.py +24 -0
  103. droch_shuil-0.2.0/tests/test_modules_read_facts.py +24 -0
  104. 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,6 @@
1
+ [console_scripts]
2
+ droch-shuil = suil.cli:cli
3
+ suil = suil.cli:cli
4
+
5
+ [pytest11]
6
+ suil = suil.testing
@@ -0,0 +1,8 @@
1
+ click>=8.5
2
+ platformdirs>=4.11
3
+ pydantic>=2.13
4
+ pydantic-settings>=2.15
5
+ pyinfra<4,>=3.10
6
+ pyrage>=1.4
7
+ PyYAML>=6.0
8
+ questionary>=2.1
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ """Suil: declarative Linux host configuration on top of pyinfra."""
@@ -0,0 +1,4 @@
1
+ from suil.cli import cli
2
+
3
+ if __name__ == '__main__':
4
+ cli()
@@ -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