pyinfra 0.11.dev3__py3-none-any.whl → 3.5.1__py3-none-any.whl

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 (203) hide show
  1. pyinfra/__init__.py +9 -12
  2. pyinfra/__main__.py +4 -0
  3. pyinfra/api/__init__.py +18 -3
  4. pyinfra/api/arguments.py +406 -0
  5. pyinfra/api/arguments_typed.py +79 -0
  6. pyinfra/api/command.py +274 -0
  7. pyinfra/api/config.py +222 -28
  8. pyinfra/api/connect.py +33 -13
  9. pyinfra/api/connectors.py +27 -0
  10. pyinfra/api/deploy.py +65 -66
  11. pyinfra/api/exceptions.py +67 -18
  12. pyinfra/api/facts.py +253 -202
  13. pyinfra/api/host.py +413 -50
  14. pyinfra/api/inventory.py +121 -160
  15. pyinfra/api/operation.py +432 -262
  16. pyinfra/api/operations.py +273 -260
  17. pyinfra/api/state.py +302 -248
  18. pyinfra/api/util.py +291 -368
  19. pyinfra/connectors/base.py +173 -0
  20. pyinfra/connectors/chroot.py +212 -0
  21. pyinfra/connectors/docker.py +381 -0
  22. pyinfra/connectors/dockerssh.py +297 -0
  23. pyinfra/connectors/local.py +238 -0
  24. pyinfra/connectors/scp/__init__.py +1 -0
  25. pyinfra/connectors/scp/client.py +204 -0
  26. pyinfra/connectors/ssh.py +670 -0
  27. pyinfra/connectors/ssh_util.py +114 -0
  28. pyinfra/connectors/sshuserclient/client.py +309 -0
  29. pyinfra/connectors/sshuserclient/config.py +102 -0
  30. pyinfra/connectors/terraform.py +135 -0
  31. pyinfra/connectors/util.py +410 -0
  32. pyinfra/connectors/vagrant.py +183 -0
  33. pyinfra/context.py +145 -0
  34. pyinfra/facts/__init__.py +7 -6
  35. pyinfra/facts/apk.py +22 -7
  36. pyinfra/facts/apt.py +117 -60
  37. pyinfra/facts/brew.py +100 -15
  38. pyinfra/facts/bsdinit.py +23 -0
  39. pyinfra/facts/cargo.py +37 -0
  40. pyinfra/facts/choco.py +47 -0
  41. pyinfra/facts/crontab.py +195 -0
  42. pyinfra/facts/deb.py +94 -0
  43. pyinfra/facts/dnf.py +48 -0
  44. pyinfra/facts/docker.py +96 -23
  45. pyinfra/facts/efibootmgr.py +113 -0
  46. pyinfra/facts/files.py +630 -58
  47. pyinfra/facts/flatpak.py +77 -0
  48. pyinfra/facts/freebsd.py +70 -0
  49. pyinfra/facts/gem.py +19 -6
  50. pyinfra/facts/git.py +59 -14
  51. pyinfra/facts/gpg.py +150 -0
  52. pyinfra/facts/hardware.py +313 -167
  53. pyinfra/facts/iptables.py +72 -62
  54. pyinfra/facts/launchd.py +44 -0
  55. pyinfra/facts/lxd.py +17 -4
  56. pyinfra/facts/mysql.py +122 -86
  57. pyinfra/facts/npm.py +17 -9
  58. pyinfra/facts/openrc.py +71 -0
  59. pyinfra/facts/opkg.py +246 -0
  60. pyinfra/facts/pacman.py +50 -7
  61. pyinfra/facts/pip.py +24 -7
  62. pyinfra/facts/pipx.py +82 -0
  63. pyinfra/facts/pkg.py +15 -6
  64. pyinfra/facts/pkgin.py +35 -0
  65. pyinfra/facts/podman.py +54 -0
  66. pyinfra/facts/postgres.py +178 -0
  67. pyinfra/facts/postgresql.py +6 -147
  68. pyinfra/facts/rpm.py +105 -0
  69. pyinfra/facts/runit.py +77 -0
  70. pyinfra/facts/selinux.py +161 -0
  71. pyinfra/facts/server.py +746 -285
  72. pyinfra/facts/snap.py +88 -0
  73. pyinfra/facts/systemd.py +139 -0
  74. pyinfra/facts/sysvinit.py +59 -0
  75. pyinfra/facts/upstart.py +35 -0
  76. pyinfra/facts/util/__init__.py +17 -0
  77. pyinfra/facts/util/databases.py +4 -6
  78. pyinfra/facts/util/packaging.py +37 -6
  79. pyinfra/facts/util/units.py +30 -0
  80. pyinfra/facts/util/win_files.py +99 -0
  81. pyinfra/facts/vzctl.py +20 -13
  82. pyinfra/facts/xbps.py +35 -0
  83. pyinfra/facts/yum.py +34 -40
  84. pyinfra/facts/zfs.py +77 -0
  85. pyinfra/facts/zypper.py +42 -0
  86. pyinfra/local.py +45 -83
  87. pyinfra/operations/__init__.py +12 -0
  88. pyinfra/operations/apk.py +98 -0
  89. pyinfra/operations/apt.py +488 -0
  90. pyinfra/operations/brew.py +231 -0
  91. pyinfra/operations/bsdinit.py +59 -0
  92. pyinfra/operations/cargo.py +45 -0
  93. pyinfra/operations/choco.py +61 -0
  94. pyinfra/operations/crontab.py +191 -0
  95. pyinfra/operations/dnf.py +210 -0
  96. pyinfra/operations/docker.py +446 -0
  97. pyinfra/operations/files.py +1939 -0
  98. pyinfra/operations/flatpak.py +94 -0
  99. pyinfra/operations/freebsd/__init__.py +12 -0
  100. pyinfra/operations/freebsd/freebsd_update.py +70 -0
  101. pyinfra/operations/freebsd/pkg.py +219 -0
  102. pyinfra/operations/freebsd/service.py +116 -0
  103. pyinfra/operations/freebsd/sysrc.py +92 -0
  104. pyinfra/operations/gem.py +47 -0
  105. pyinfra/operations/git.py +419 -0
  106. pyinfra/operations/iptables.py +311 -0
  107. pyinfra/operations/launchd.py +45 -0
  108. pyinfra/operations/lxd.py +68 -0
  109. pyinfra/operations/mysql.py +609 -0
  110. pyinfra/operations/npm.py +57 -0
  111. pyinfra/operations/openrc.py +63 -0
  112. pyinfra/operations/opkg.py +88 -0
  113. pyinfra/operations/pacman.py +81 -0
  114. pyinfra/operations/pip.py +205 -0
  115. pyinfra/operations/pipx.py +102 -0
  116. pyinfra/operations/pkg.py +70 -0
  117. pyinfra/operations/pkgin.py +91 -0
  118. pyinfra/operations/postgres.py +436 -0
  119. pyinfra/operations/postgresql.py +30 -0
  120. pyinfra/operations/puppet.py +40 -0
  121. pyinfra/operations/python.py +72 -0
  122. pyinfra/operations/runit.py +184 -0
  123. pyinfra/operations/selinux.py +189 -0
  124. pyinfra/operations/server.py +1099 -0
  125. pyinfra/operations/snap.py +117 -0
  126. pyinfra/operations/ssh.py +216 -0
  127. pyinfra/operations/systemd.py +149 -0
  128. pyinfra/operations/sysvinit.py +141 -0
  129. pyinfra/operations/upstart.py +68 -0
  130. pyinfra/operations/util/__init__.py +12 -0
  131. pyinfra/operations/util/docker.py +251 -0
  132. pyinfra/operations/util/files.py +247 -0
  133. pyinfra/operations/util/packaging.py +336 -0
  134. pyinfra/operations/util/service.py +46 -0
  135. pyinfra/operations/vzctl.py +137 -0
  136. pyinfra/operations/xbps.py +77 -0
  137. pyinfra/operations/yum.py +210 -0
  138. pyinfra/operations/zfs.py +175 -0
  139. pyinfra/operations/zypper.py +192 -0
  140. pyinfra/progress.py +44 -32
  141. pyinfra/py.typed +0 -0
  142. pyinfra/version.py +9 -1
  143. pyinfra-3.5.1.dist-info/METADATA +141 -0
  144. pyinfra-3.5.1.dist-info/RECORD +159 -0
  145. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info}/WHEEL +1 -2
  146. pyinfra-3.5.1.dist-info/entry_points.txt +12 -0
  147. {pyinfra-0.11.dev3.dist-info → pyinfra-3.5.1.dist-info/licenses}/LICENSE.md +1 -1
  148. pyinfra_cli/__init__.py +1 -0
  149. pyinfra_cli/cli.py +780 -0
  150. pyinfra_cli/commands.py +66 -0
  151. pyinfra_cli/exceptions.py +155 -65
  152. pyinfra_cli/inventory.py +233 -89
  153. pyinfra_cli/log.py +39 -43
  154. pyinfra_cli/main.py +26 -495
  155. pyinfra_cli/prints.py +215 -156
  156. pyinfra_cli/util.py +172 -105
  157. pyinfra_cli/virtualenv.py +25 -20
  158. pyinfra/api/connectors/__init__.py +0 -21
  159. pyinfra/api/connectors/ansible.py +0 -99
  160. pyinfra/api/connectors/docker.py +0 -178
  161. pyinfra/api/connectors/local.py +0 -169
  162. pyinfra/api/connectors/ssh.py +0 -402
  163. pyinfra/api/connectors/sshuserclient/client.py +0 -105
  164. pyinfra/api/connectors/sshuserclient/config.py +0 -90
  165. pyinfra/api/connectors/util.py +0 -63
  166. pyinfra/api/connectors/vagrant.py +0 -155
  167. pyinfra/facts/init.py +0 -176
  168. pyinfra/facts/util/files.py +0 -102
  169. pyinfra/hook.py +0 -41
  170. pyinfra/modules/__init__.py +0 -11
  171. pyinfra/modules/apk.py +0 -64
  172. pyinfra/modules/apt.py +0 -272
  173. pyinfra/modules/brew.py +0 -122
  174. pyinfra/modules/files.py +0 -711
  175. pyinfra/modules/gem.py +0 -30
  176. pyinfra/modules/git.py +0 -115
  177. pyinfra/modules/init.py +0 -344
  178. pyinfra/modules/iptables.py +0 -271
  179. pyinfra/modules/lxd.py +0 -45
  180. pyinfra/modules/mysql.py +0 -347
  181. pyinfra/modules/npm.py +0 -47
  182. pyinfra/modules/pacman.py +0 -60
  183. pyinfra/modules/pip.py +0 -99
  184. pyinfra/modules/pkg.py +0 -43
  185. pyinfra/modules/postgresql.py +0 -245
  186. pyinfra/modules/puppet.py +0 -20
  187. pyinfra/modules/python.py +0 -37
  188. pyinfra/modules/server.py +0 -524
  189. pyinfra/modules/ssh.py +0 -150
  190. pyinfra/modules/util/files.py +0 -52
  191. pyinfra/modules/util/packaging.py +0 -118
  192. pyinfra/modules/vzctl.py +0 -133
  193. pyinfra/modules/yum.py +0 -171
  194. pyinfra/pseudo_modules.py +0 -64
  195. pyinfra-0.11.dev3.dist-info/.DS_Store +0 -0
  196. pyinfra-0.11.dev3.dist-info/METADATA +0 -135
  197. pyinfra-0.11.dev3.dist-info/RECORD +0 -95
  198. pyinfra-0.11.dev3.dist-info/entry_points.txt +0 -3
  199. pyinfra-0.11.dev3.dist-info/top_level.txt +0 -2
  200. pyinfra_cli/__main__.py +0 -40
  201. pyinfra_cli/config.py +0 -92
  202. /pyinfra/{modules/util → connectors}/__init__.py +0 -0
  203. /pyinfra/{api/connectors → connectors}/sshuserclient/__init__.py +0 -0
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ from typing_extensions import override
4
+
5
+ from pyinfra.api import FactBase, MaskString, QuoteString, StringCommand
6
+ from pyinfra.api.util import try_int
7
+
8
+ from .util.databases import parse_columns_and_rows
9
+
10
+
11
+ def make_psql_command(
12
+ database: str | None = None,
13
+ user: str | None = None,
14
+ password: str | None = None,
15
+ host: str | None = None,
16
+ port: str | int | None = None,
17
+ executable="psql",
18
+ ) -> StringCommand:
19
+ target_bits: list[str] = []
20
+
21
+ if password:
22
+ target_bits.append(MaskString('PGPASSWORD="{0}"'.format(password)))
23
+
24
+ target_bits.append(executable)
25
+
26
+ if database:
27
+ target_bits.append("-d {0}".format(database))
28
+
29
+ if user:
30
+ target_bits.append("-U {0}".format(user))
31
+
32
+ if host:
33
+ target_bits.append("-h {0}".format(host))
34
+
35
+ if port:
36
+ target_bits.append("-p {0}".format(port))
37
+
38
+ return StringCommand(*target_bits)
39
+
40
+
41
+ def make_execute_psql_command(command, **psql_kwargs):
42
+ return StringCommand(
43
+ make_psql_command(**psql_kwargs),
44
+ "-Ac",
45
+ QuoteString(command), # quote this whole item as a single shell argument
46
+ )
47
+
48
+
49
+ class PostgresFactBase(FactBase):
50
+ abstract = True
51
+
52
+ psql_command: str
53
+
54
+ @override
55
+ def requires_command(self, *args, **kwargs):
56
+ return "psql"
57
+
58
+ @override
59
+ def command(
60
+ self,
61
+ psql_user=None,
62
+ psql_password=None,
63
+ psql_host=None,
64
+ psql_port=None,
65
+ psql_database=None,
66
+ ):
67
+ return make_execute_psql_command(
68
+ self.psql_command,
69
+ user=psql_user,
70
+ password=psql_password,
71
+ host=psql_host,
72
+ port=psql_port,
73
+ database=psql_database,
74
+ )
75
+
76
+
77
+ class PostgresRoles(PostgresFactBase):
78
+ """
79
+ Returns a dict of PostgreSQL roles and data:
80
+
81
+ .. code:: python
82
+
83
+ {
84
+ "pyinfra": {
85
+ "super": true,
86
+ "createrole": false,
87
+ "createdb": false,
88
+ ...
89
+ },
90
+ }
91
+ """
92
+
93
+ default = dict
94
+ psql_command = "SELECT * FROM pg_catalog.pg_roles"
95
+
96
+ @override
97
+ def process(self, output):
98
+ # Remove the last line of the output (row count)
99
+ output = output[:-1]
100
+ rows = parse_columns_and_rows(
101
+ output,
102
+ "|",
103
+ # Remove the "rol" prefix on column names
104
+ remove_column_prefix="rol",
105
+ )
106
+
107
+ users = {}
108
+
109
+ for details in rows:
110
+ for key, value in list(details.items()):
111
+ if key in ("oid", "connlimit"):
112
+ details[key] = try_int(value)
113
+
114
+ if key in (
115
+ "super",
116
+ "inherit",
117
+ "createrole",
118
+ "createdb",
119
+ "canlogin",
120
+ "replication",
121
+ "bypassrls",
122
+ ):
123
+ details[key] = value == "t"
124
+
125
+ users[details.pop("name")] = details
126
+
127
+ return users
128
+
129
+
130
+ class PostgresDatabases(PostgresFactBase):
131
+ """
132
+ Returns a dict of PostgreSQL databases and metadata:
133
+
134
+ .. code:: python
135
+
136
+ {
137
+ "pyinfra_stuff": {
138
+ "encoding": "UTF8",
139
+ "collate": "en_US.UTF-8",
140
+ "ctype": "en_US.UTF-8",
141
+ ...
142
+ },
143
+ }
144
+ """
145
+
146
+ default = dict
147
+ psql_command = "SELECT pg_catalog.pg_encoding_to_char(encoding), *, pg_catalog.pg_get_userbyid(datdba) AS owner FROM pg_catalog.pg_database" # noqa: E501
148
+
149
+ @override
150
+ def process(self, output):
151
+ # Remove the last line of the output (row count)
152
+ output = output[:-1]
153
+ rows = parse_columns_and_rows(
154
+ output,
155
+ "|",
156
+ # Remove the "dat" prefix on column names
157
+ remove_column_prefix="dat",
158
+ )
159
+
160
+ databases = {}
161
+
162
+ for details in rows:
163
+ details["encoding"] = details.pop("pg_encoding_to_char")
164
+ details["owner"] = details.pop("owner")
165
+ for key, value in list(details.items()):
166
+ if key.endswith("id") or key in (
167
+ "dba",
168
+ "tablespace",
169
+ "connlimit",
170
+ ):
171
+ details[key] = try_int(value)
172
+
173
+ if key in ("istemplate", "allowconn"):
174
+ details[key] = value == "t"
175
+
176
+ databases[details.pop("name")] = details
177
+
178
+ return databases
@@ -1,152 +1,11 @@
1
- from pyinfra.api import FactBase
2
- from pyinfra.api.util import try_int
1
+ from __future__ import annotations
3
2
 
4
- from .util.databases import parse_columns_and_rows
3
+ from .postgres import PostgresDatabases, PostgresRoles
5
4
 
6
5
 
7
- def make_psql_command(
8
- database=None,
9
- user=None,
10
- password=None,
11
- host=None,
12
- port=None,
13
- executable='psql',
14
- ):
15
- target_bits = []
6
+ class PostgresqlRoles(PostgresRoles):
7
+ deprecated = True
16
8
 
17
- if password:
18
- target_bits.append('PGPASSWORD="{0}"'.format(password))
19
9
 
20
- target_bits.append(executable)
21
-
22
- if database:
23
- target_bits.append('-d {0}'.format(database))
24
-
25
- if user:
26
- target_bits.append('-U {0}'.format(user))
27
-
28
- if host:
29
- target_bits.append('-h {0}'.format(host))
30
-
31
- if port:
32
- target_bits.append('-p {0}'.format(port))
33
-
34
- return ' '.join(target_bits)
35
-
36
-
37
- def make_execute_psql_command(command, **postgresql_kwargs):
38
- return '{0} -Ac "{1}"'.format(
39
- make_psql_command(**postgresql_kwargs),
40
- command.replace('"', '\\"'),
41
- )
42
-
43
-
44
- class PostgresqlFactBase(FactBase):
45
- abstract = True
46
-
47
- def command(
48
- self,
49
- postgresql_user=None, postgresql_password=None,
50
- postgresql_host=None, postgresql_port=None,
51
- ):
52
- return make_execute_psql_command(
53
- self.postgresql_command,
54
- user=postgresql_user,
55
- password=postgresql_password,
56
- host=postgresql_host,
57
- port=postgresql_port,
58
- )
59
-
60
-
61
- class PostgresqlRoles(PostgresqlFactBase):
62
- '''
63
- Returns a dict of PostgreSQL roles and data:
64
-
65
- .. code:: python
66
-
67
- 'pyinfra': {
68
- 'super': true,
69
- 'createrole': false,
70
- 'createdb': false,
71
- ...
72
- },
73
- ...
74
- '''
75
-
76
- default = dict
77
- postgresql_command = 'SELECT * FROM pg_catalog.pg_roles'
78
-
79
- def process(self, output):
80
- # Remove the last line of the output (row count)
81
- output = output[:-1]
82
- rows = parse_columns_and_rows(
83
- output, '|',
84
- # Remove the "rol" prefix on column names
85
- remove_column_prefix='rol',
86
- )
87
-
88
- users = {}
89
-
90
- for details in rows:
91
- for key, value in list(details.items()):
92
- if key in ('oid', 'connlimit'):
93
- details[key] = try_int(value)
94
-
95
- if key in (
96
- 'super', 'inherit', 'createrole', 'createdb',
97
- 'canlogin', 'replication', 'bypassrls',
98
- ):
99
- details[key] = value == 't'
100
-
101
- users[details.pop('name')] = details
102
-
103
- return users
104
-
105
-
106
- class PostgresqlDatabases(PostgresqlFactBase):
107
- '''
108
- Returns a dict of PostgreSQL databases and metadata:
109
-
110
- .. code:: python
111
-
112
- "pyinfra_stuff": {
113
- "encoding": "UTF8",
114
- "collate": "en_US.UTF-8",
115
- "ctype": "en_US.UTF-8",
116
- ...
117
- },
118
- ...
119
- '''
120
-
121
- default = dict
122
- postgresql_command = '''
123
- SELECT pg_catalog.pg_encoding_to_char(encoding), *
124
- FROM pg_catalog.pg_database
125
- '''
126
-
127
- def process(self, output):
128
- # Remove the last line of the output (row count)
129
- output = output[:-1]
130
- rows = parse_columns_and_rows(
131
- output, '|',
132
- # Remove the "dat" prefix on column names
133
- remove_column_prefix='dat',
134
- )
135
-
136
- databases = {}
137
-
138
- for details in rows:
139
- details['encoding'] = details.pop('pg_encoding_to_char')
140
-
141
- for key, value in list(details.items()):
142
- if key.endswith('id') or key in (
143
- 'dba', 'tablespace', 'connlimit',
144
- ):
145
- details[key] = try_int(value)
146
-
147
- if key in ('istemplate', 'allowconn'):
148
- details[key] = value == 't'
149
-
150
- databases[details.pop('name')] = details
151
-
152
- return databases
10
+ class PostgresqlDatabases(PostgresDatabases):
11
+ deprecated = True
pyinfra/facts/rpm.py ADDED
@@ -0,0 +1,105 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import shlex
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra.api import FactBase
9
+
10
+ from .util.packaging import parse_packages
11
+
12
+ rpm_regex = r"^(\S+)\ (\S+)$"
13
+ rpm_query_format = "%{NAME} %{VERSION}-%{RELEASE}\\n"
14
+
15
+
16
+ class RpmPackages(FactBase):
17
+ """
18
+ Returns a dict of installed rpm packages:
19
+
20
+ .. code:: python
21
+
22
+ {
23
+ "package_name": ["version"],
24
+ }
25
+ """
26
+
27
+ @override
28
+ def command(self) -> str:
29
+ return "rpm --queryformat {0} -qa".format(shlex.quote(rpm_query_format))
30
+
31
+ @override
32
+ def requires_command(self) -> str:
33
+ return "rpm"
34
+
35
+ default = dict
36
+
37
+ @override
38
+ def process(self, output):
39
+ return parse_packages(rpm_regex, output)
40
+
41
+
42
+ class RpmPackage(FactBase):
43
+ """
44
+ Returns information on a .rpm file:
45
+
46
+ .. code:: python
47
+
48
+ {
49
+ "name": "my_package",
50
+ "version": "1.0.0",
51
+ }
52
+ """
53
+
54
+ @override
55
+ def requires_command(self, package) -> str:
56
+ return "rpm"
57
+
58
+ @override
59
+ def command(self, package) -> str:
60
+ return (
61
+ "rpm --queryformat {0} -q {1} || "
62
+ "! test -e {1} || "
63
+ "rpm --queryformat {0} -qp {1} 2> /dev/null"
64
+ ).format(shlex.quote(rpm_query_format), shlex.quote(package))
65
+
66
+ @override
67
+ def process(self, output):
68
+ for line in output:
69
+ matches = re.match(rpm_regex, line)
70
+ if matches:
71
+ return {
72
+ "name": matches.group(1),
73
+ "version": matches.group(2),
74
+ }
75
+
76
+
77
+ class RpmPackageProvides(FactBase):
78
+ """
79
+ Returns a list of packages that provide the specified capability (command, file, etc).
80
+ """
81
+
82
+ default = list
83
+
84
+ @override
85
+ def requires_command(self, *args, **kwargs) -> str:
86
+ return "repoquery"
87
+
88
+ @override
89
+ def command(self, package):
90
+ # Accept failure here (|| true) for invalid/unknown packages
91
+ return "repoquery --queryformat {0} --whatprovides {1} || true".format(
92
+ shlex.quote(rpm_query_format),
93
+ shlex.quote(package),
94
+ )
95
+
96
+ @override
97
+ def process(self, output):
98
+ packages = []
99
+
100
+ for line in output:
101
+ matches = re.match(rpm_regex, line)
102
+ if matches:
103
+ packages.append(list(matches.groups()))
104
+
105
+ return packages
pyinfra/facts/runit.py ADDED
@@ -0,0 +1,77 @@
1
+ from typing_extensions import override
2
+
3
+ from pyinfra.api import FactBase
4
+
5
+
6
+ class RunitStatus(FactBase):
7
+ """
8
+ Returns a dict of name -> status for runit services.
9
+
10
+ + service: optionally check only for a single service
11
+ + svdir: alternative ``SVDIR``
12
+
13
+ .. code:: python
14
+
15
+ {
16
+ 'agetty-tty1': True, # service is running
17
+ 'dhcpcd': False, # service is down
18
+ 'wpa_supplicant': None, # service is managed, but not running or down,
19
+ # possibly in a fail state
20
+ }
21
+ """
22
+
23
+ default = dict
24
+
25
+ @override
26
+ def requires_command(self, *args, **kwargs) -> str:
27
+ return "sv"
28
+
29
+ @override
30
+ def command(self, service=None, svdir="/var/service") -> str:
31
+ if service is None:
32
+ return (
33
+ 'export SVDIR="{0}" && '
34
+ 'cd "$SVDIR" && find * -maxdepth 0 -exec sv status {{}} + 2>/dev/null'
35
+ ).format(svdir)
36
+ else:
37
+ return 'SVDIR="{0}" sv status "{1}"'.format(svdir, service)
38
+
39
+ @override
40
+ def process(self, output):
41
+ services = {}
42
+ for line in output:
43
+ statusstr, service, _ = line.split(sep=": ", maxsplit=2)
44
+ status = None
45
+
46
+ if statusstr == "run":
47
+ status = True
48
+ elif statusstr == "down":
49
+ status = False
50
+ # another observable state is "fail"
51
+ # report as ``None`` for now
52
+
53
+ services[service] = status
54
+
55
+ return services
56
+
57
+
58
+ class RunitManaged(FactBase):
59
+ """
60
+ Returns a set of all services managed by runit
61
+
62
+ + service: optionally check only for a single service
63
+ + svdir: alternative ``SVDIR``
64
+ """
65
+
66
+ default = set
67
+
68
+ @override
69
+ def command(self, service=None, svdir="/var/service"):
70
+ if service is None:
71
+ return 'cd "{0}" && find -mindepth 1 -maxdepth 1 -type l -printf "%f\n"'.format(svdir)
72
+ else:
73
+ return 'cd "{0}" && test -h "{1}" && echo "{1}" || true'.format(svdir, service)
74
+
75
+ @override
76
+ def process(self, output):
77
+ return set(output)
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import defaultdict
5
+
6
+ from typing_extensions import override
7
+
8
+ from pyinfra.api import FactBase
9
+
10
+ FIELDS = ["user", "role", "type", "level"] # order is significant, do not change
11
+
12
+
13
+ class SEBoolean(FactBase):
14
+ """
15
+ Returns the status of a SELinux Boolean as a string (``on`` or ``off``).
16
+ If ``boolean`` does not exist, ``SEBoolean`` returns the empty string.
17
+ """
18
+
19
+ @override
20
+ def requires_command(self, boolean) -> str:
21
+ return "getsebool"
22
+
23
+ default = str
24
+
25
+ @override
26
+ def command(self, boolean):
27
+ return "getsebool {0}".format(boolean)
28
+
29
+ @override
30
+ def process(self, output):
31
+ components = output[0].split(" --> ")
32
+ return components[1]
33
+
34
+
35
+ class FileContext(FactBase):
36
+ """
37
+ Returns structured SELinux file context data for a specified file
38
+ or ``None`` if the file does not exist.
39
+
40
+ .. code:: python
41
+
42
+ {
43
+ "user": "system_u",
44
+ "role": "object_r",
45
+ "type": "default_t",
46
+ "level": "s0",
47
+ }
48
+ """
49
+
50
+ @override
51
+ def command(self, path):
52
+ return "stat -c %C {0} || exit 0".format(path)
53
+
54
+ @override
55
+ def process(self, output):
56
+ context = {}
57
+ components = output[0].split(":")
58
+ context["user"] = components[0]
59
+ context["role"] = components[1]
60
+ context["type"] = components[2]
61
+ context["level"] = components[3]
62
+ return context
63
+
64
+
65
+ class FileContextMapping(FactBase):
66
+ """
67
+ Returns structured SELinux file context data for the specified target path prefix
68
+ using the same format as :ref:`facts:selinux.FileContext`.
69
+ If there is no mapping, it returns ``{}``
70
+ Note: This fact requires root privileges.
71
+ """
72
+
73
+ default = dict
74
+
75
+ @override
76
+ def requires_command(self, target) -> str:
77
+ return "semanage"
78
+
79
+ @override
80
+ def command(self, target):
81
+ return "set -o pipefail && semanage fcontext -n -l | (grep '^{0}' || true)".format(target)
82
+
83
+ @override
84
+ def process(self, output):
85
+ # example output: /etc all files system_u:object_r:etc_t:s0
86
+ # but lines at end that won't match: /etc/systemd/system = /usr/lib/systemd/system
87
+ if len(output) != 1:
88
+ return self.default()
89
+ m = re.match(r"^.*\s+(\w+):(\w+):(\w+):(\w+)", output[0])
90
+ return {k: m.group(i) for i, k in enumerate(FIELDS, 1)} if m is not None else self.default()
91
+
92
+
93
+ class SEPorts(FactBase):
94
+ """
95
+ Returns the SELinux 'type' definitions for ``(tcp|udp|dccp|sctp)`` ports.
96
+ Note: This fact requires root privileges.
97
+
98
+ .. code:: python
99
+
100
+ {
101
+ "tcp": { 22: "ssh_port_t", ...},
102
+ "udp": { ...}
103
+ }
104
+ """
105
+
106
+ default = dict
107
+ # example output: amqp_port_t tcp 15672, 5671-5672
108
+ _regex = re.compile(r"^([\w_]+)\s+(\w+)\s+([\w\-,\s]+)$")
109
+
110
+ @override
111
+ def requires_command(self) -> str:
112
+ return "semanage"
113
+
114
+ @override
115
+ def command(self):
116
+ return "semanage port -ln"
117
+
118
+ @override
119
+ def process(self, output):
120
+ labels: dict[str, dict] = defaultdict(dict)
121
+ for line in output:
122
+ m = SEPorts._regex.match(line)
123
+ if m is None: # something went wrong
124
+ continue
125
+ if m.group(1) == "unreserved_port_t": # these cover the entire space
126
+ continue
127
+ for item in m.group(3).split(","):
128
+ item = item.strip()
129
+ if "-" in item:
130
+ pieces = item.split("-")
131
+ start, stop = int(pieces[0]), int(pieces[1])
132
+ else:
133
+ start = stop = int(item)
134
+ labels[m.group(2)].update({port: m.group(1) for port in range(start, stop + 1)})
135
+
136
+ return labels
137
+
138
+
139
+ class SEPort(FactBase):
140
+ """
141
+ Returns the SELinux 'type' for the specified protocol ``(tcp|udp|dccp|sctp)`` and port number.
142
+ If no type has been set, ``SEPort`` returns the empty string.
143
+ Note: ``policycoreutils-dev`` must be installed for this to work.
144
+ """
145
+
146
+ default = str
147
+
148
+ @override
149
+ def requires_command(self, protocol, port) -> str:
150
+ return "sepolicy"
151
+
152
+ @override
153
+ def command(self, protocol, port):
154
+ return "(sepolicy network -p {0} 2>/dev/null || true) | grep {1}".format(port, protocol)
155
+
156
+ @override
157
+ def process(self, output):
158
+ # if type set, first line is specific and second is generic type for port range
159
+ # each rows in the format "22: tcp ssh_port_t 22"
160
+
161
+ return output[0].split(" ")[2] if len(output) > 1 else self.default()