ansible-core 2.17.4__py3-none-any.whl → 2.18.0b1__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.

Potentially problematic release.


This version of ansible-core might be problematic. Click here for more details.

Files changed (320) hide show
  1. ansible/__main__.py +2 -17
  2. ansible/cli/__init__.py +3 -15
  3. ansible/cli/config.py +187 -24
  4. ansible/cli/console.py +1 -1
  5. ansible/cli/doc.py +38 -16
  6. ansible/cli/galaxy.py +30 -53
  7. ansible/cli/inventory.py +2 -2
  8. ansible/cli/pull.py +2 -2
  9. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -10
  10. ansible/config/base.yml +127 -57
  11. ansible/config/manager.py +89 -11
  12. ansible/constants.py +32 -9
  13. ansible/errors/__init__.py +5 -0
  14. ansible/executor/interpreter_discovery.py +1 -1
  15. ansible/executor/play_iterator.py +16 -0
  16. ansible/executor/playbook_executor.py +1 -4
  17. ansible/executor/powershell/become_wrapper.ps1 +4 -5
  18. ansible/executor/powershell/bootstrap_wrapper.ps1 +2 -3
  19. ansible/executor/powershell/exec_wrapper.ps1 +1 -1
  20. ansible/executor/powershell/module_manifest.py +2 -2
  21. ansible/executor/task_executor.py +50 -39
  22. ansible/executor/task_queue_manager.py +1 -1
  23. ansible/executor/task_result.py +1 -1
  24. ansible/galaxy/api.py +3 -4
  25. ansible/galaxy/collection/__init__.py +21 -10
  26. ansible/galaxy/collection/concrete_artifact_manager.py +2 -2
  27. ansible/galaxy/collection/galaxy_api_proxy.py +10 -16
  28. ansible/galaxy/collection/gpg.py +17 -23
  29. ansible/galaxy/data/COPYING +7 -0
  30. ansible/galaxy/data/apb/Dockerfile.j2 +1 -0
  31. ansible/galaxy/data/apb/Makefile.j2 +1 -0
  32. ansible/galaxy/data/apb/README.md +7 -3
  33. ansible/galaxy/data/apb/apb.yml.j2 +1 -0
  34. ansible/galaxy/data/apb/defaults/main.yml.j2 +1 -0
  35. ansible/galaxy/data/apb/handlers/main.yml.j2 +1 -0
  36. ansible/galaxy/data/apb/meta/main.yml.j2 +1 -0
  37. ansible/galaxy/data/apb/playbooks/deprovision.yml.j2 +1 -0
  38. ansible/galaxy/data/apb/playbooks/provision.yml.j2 +1 -0
  39. ansible/galaxy/data/apb/tasks/main.yml.j2 +1 -0
  40. ansible/galaxy/data/apb/tests/ansible.cfg +1 -0
  41. ansible/galaxy/data/apb/tests/inventory +1 -0
  42. ansible/galaxy/data/apb/tests/test.yml.j2 +1 -0
  43. ansible/galaxy/data/apb/vars/main.yml.j2 +1 -0
  44. ansible/galaxy/data/collections_galaxy_meta.yml +1 -0
  45. ansible/galaxy/data/container/defaults/main.yml.j2 +1 -0
  46. ansible/galaxy/data/container/handlers/main.yml.j2 +1 -0
  47. ansible/galaxy/data/container/meta/container.yml.j2 +1 -0
  48. ansible/galaxy/data/container/meta/main.yml.j2 +1 -0
  49. ansible/galaxy/data/container/tasks/main.yml.j2 +1 -0
  50. ansible/galaxy/data/container/tests/ansible.cfg +1 -0
  51. ansible/galaxy/data/container/tests/inventory +1 -0
  52. ansible/galaxy/data/container/tests/test.yml.j2 +1 -0
  53. ansible/galaxy/data/container/vars/main.yml.j2 +1 -0
  54. ansible/galaxy/data/default/collection/README.md.j2 +1 -0
  55. ansible/galaxy/data/default/collection/galaxy.yml.j2 +1 -0
  56. ansible/galaxy/data/default/collection/meta/runtime.yml +1 -0
  57. ansible/galaxy/data/default/collection/plugins/README.md.j2 +1 -0
  58. ansible/galaxy/data/default/role/defaults/main.yml.j2 +1 -0
  59. ansible/galaxy/data/default/role/handlers/main.yml.j2 +1 -0
  60. ansible/galaxy/data/default/role/meta/main.yml.j2 +1 -0
  61. ansible/galaxy/data/default/role/tasks/main.yml.j2 +1 -0
  62. ansible/galaxy/data/default/role/tests/inventory +1 -0
  63. ansible/galaxy/data/default/role/tests/test.yml.j2 +1 -0
  64. ansible/galaxy/data/default/role/vars/main.yml.j2 +1 -0
  65. ansible/galaxy/data/network/cliconf_plugins/example.py.j2 +1 -0
  66. ansible/galaxy/data/network/defaults/main.yml.j2 +1 -0
  67. ansible/galaxy/data/network/library/example_command.py.j2 +1 -0
  68. ansible/galaxy/data/network/library/example_config.py.j2 +1 -0
  69. ansible/galaxy/data/network/library/example_facts.py.j2 +1 -0
  70. ansible/galaxy/data/network/meta/main.yml.j2 +1 -0
  71. ansible/galaxy/data/network/module_utils/example.py.j2 +1 -0
  72. ansible/galaxy/data/network/netconf_plugins/example.py.j2 +1 -0
  73. ansible/galaxy/data/network/tasks/main.yml.j2 +1 -0
  74. ansible/galaxy/data/network/terminal_plugins/example.py.j2 +1 -0
  75. ansible/galaxy/data/network/tests/inventory +1 -0
  76. ansible/galaxy/data/network/tests/test.yml.j2 +1 -0
  77. ansible/galaxy/data/network/vars/main.yml.j2 +1 -0
  78. ansible/galaxy/dependency_resolution/providers.py +3 -3
  79. ansible/galaxy/role.py +1 -1
  80. ansible/galaxy/token.py +20 -8
  81. ansible/keyword_desc.yml +1 -1
  82. ansible/module_utils/_internal/__init__.py +0 -0
  83. ansible/module_utils/_internal/_concurrent/__init__.py +0 -0
  84. ansible/module_utils/_internal/_concurrent/_daemon_threading.py +28 -0
  85. ansible/module_utils/_internal/_concurrent/_futures.py +21 -0
  86. ansible/module_utils/ansible_release.py +2 -2
  87. ansible/module_utils/api.py +2 -2
  88. ansible/module_utils/basic.py +14 -11
  89. ansible/module_utils/common/collections.py +1 -1
  90. ansible/module_utils/common/file.py +0 -6
  91. ansible/module_utils/common/process.py +22 -9
  92. ansible/module_utils/common/text/converters.py +5 -8
  93. ansible/module_utils/common/text/formatters.py +20 -4
  94. ansible/module_utils/common/validation.py +33 -25
  95. ansible/module_utils/compat/paramiko.py +6 -1
  96. ansible/module_utils/compat/selinux.py +2 -2
  97. ansible/module_utils/connection.py +8 -24
  98. ansible/module_utils/csharp/Ansible.Become.cs +14 -25
  99. ansible/module_utils/csharp/Ansible.Process.cs +1 -1
  100. ansible/module_utils/distro/__init__.py +1 -1
  101. ansible/module_utils/distro/_distro.py +8 -4
  102. ansible/module_utils/facts/collector.py +2 -0
  103. ansible/module_utils/facts/default_collectors.py +3 -1
  104. ansible/module_utils/facts/hardware/aix.py +54 -52
  105. ansible/module_utils/facts/hardware/darwin.py +37 -34
  106. ansible/module_utils/facts/hardware/freebsd.py +55 -15
  107. ansible/module_utils/facts/hardware/hpux.py +3 -0
  108. ansible/module_utils/facts/hardware/linux.py +101 -57
  109. ansible/module_utils/facts/hardware/netbsd.py +3 -0
  110. ansible/module_utils/facts/hardware/openbsd.py +4 -1
  111. ansible/module_utils/facts/hardware/sunos.py +7 -1
  112. ansible/module_utils/facts/network/aix.py +16 -17
  113. ansible/module_utils/facts/network/fc_wwn.py +4 -1
  114. ansible/module_utils/facts/network/hpux.py +21 -4
  115. ansible/module_utils/facts/network/iscsi.py +7 -8
  116. ansible/module_utils/facts/network/linux.py +0 -2
  117. ansible/module_utils/facts/other/facter.py +9 -4
  118. ansible/module_utils/facts/other/ohai.py +5 -5
  119. ansible/module_utils/facts/packages.py +49 -7
  120. ansible/module_utils/facts/sysctl.py +33 -31
  121. ansible/module_utils/facts/system/distribution.py +1 -1
  122. ansible/module_utils/facts/system/local.py +12 -22
  123. ansible/module_utils/facts/system/service_mgr.py +3 -1
  124. ansible/module_utils/facts/system/systemd.py +47 -0
  125. ansible/module_utils/powershell/Ansible.ModuleUtils.AddType.psm1 +1 -1
  126. ansible/module_utils/powershell/Ansible.ModuleUtils.CamelConversion.psm1 +1 -1
  127. ansible/module_utils/splitter.py +1 -1
  128. ansible/modules/add_host.py +1 -1
  129. ansible/modules/apt.py +43 -32
  130. ansible/modules/apt_key.py +6 -6
  131. ansible/modules/apt_repository.py +23 -14
  132. ansible/modules/assemble.py +7 -2
  133. ansible/modules/assert.py +4 -4
  134. ansible/modules/blockinfile.py +3 -6
  135. ansible/modules/command.py +1 -1
  136. ansible/modules/copy.py +4 -4
  137. ansible/modules/cron.py +13 -10
  138. ansible/modules/deb822_repository.py +16 -17
  139. ansible/modules/debconf.py +9 -9
  140. ansible/modules/debug.py +1 -1
  141. ansible/modules/dnf.py +79 -164
  142. ansible/modules/dnf5.py +48 -31
  143. ansible/modules/dpkg_selections.py +2 -2
  144. ansible/modules/expect.py +2 -2
  145. ansible/modules/fetch.py +2 -2
  146. ansible/modules/file.py +5 -3
  147. ansible/modules/find.py +40 -12
  148. ansible/modules/gather_facts.py +4 -2
  149. ansible/modules/get_url.py +29 -24
  150. ansible/modules/git.py +35 -35
  151. ansible/modules/group.py +71 -1
  152. ansible/modules/hostname.py +2 -4
  153. ansible/modules/include_vars.py +5 -5
  154. ansible/modules/iptables.py +13 -16
  155. ansible/modules/known_hosts.py +16 -13
  156. ansible/modules/lineinfile.py +1 -4
  157. ansible/modules/meta.py +6 -1
  158. ansible/modules/mount_facts.py +651 -0
  159. ansible/modules/package_facts.py +63 -80
  160. ansible/modules/pause.py +4 -3
  161. ansible/modules/pip.py +14 -14
  162. ansible/modules/replace.py +1 -4
  163. ansible/modules/rpm_key.py +31 -11
  164. ansible/modules/service.py +8 -8
  165. ansible/modules/service_facts.py +20 -5
  166. ansible/modules/set_stats.py +1 -1
  167. ansible/modules/setup.py +3 -3
  168. ansible/modules/stat.py +3 -3
  169. ansible/modules/subversion.py +1 -1
  170. ansible/modules/systemd.py +16 -10
  171. ansible/modules/systemd_service.py +16 -10
  172. ansible/modules/sysvinit.py +4 -4
  173. ansible/modules/unarchive.py +35 -22
  174. ansible/modules/uri.py +24 -18
  175. ansible/modules/user.py +145 -12
  176. ansible/modules/validate_argument_spec.py +3 -3
  177. ansible/modules/wait_for_connection.py +2 -1
  178. ansible/modules/yum_repository.py +136 -179
  179. ansible/parsing/dataloader.py +2 -2
  180. ansible/parsing/mod_args.py +11 -10
  181. ansible/parsing/vault/__init__.py +8 -3
  182. ansible/parsing/yaml/constructor.py +10 -8
  183. ansible/parsing/yaml/objects.py +1 -1
  184. ansible/playbook/base.py +12 -23
  185. ansible/playbook/helpers.py +4 -0
  186. ansible/playbook/loop_control.py +8 -0
  187. ansible/playbook/play.py +4 -22
  188. ansible/playbook/play_context.py +0 -16
  189. ansible/playbook/playbook_include.py +2 -2
  190. ansible/playbook/role/__init__.py +2 -2
  191. ansible/playbook/task.py +1 -1
  192. ansible/plugins/__init__.py +2 -0
  193. ansible/plugins/action/__init__.py +7 -9
  194. ansible/plugins/action/reboot.py +2 -2
  195. ansible/plugins/become/__init__.py +1 -1
  196. ansible/plugins/callback/__init__.py +44 -3
  197. ansible/plugins/callback/default.py +1 -1
  198. ansible/plugins/cliconf/__init__.py +1 -1
  199. ansible/plugins/connection/paramiko_ssh.py +2 -80
  200. ansible/plugins/connection/psrp.py +33 -82
  201. ansible/plugins/connection/ssh.py +0 -8
  202. ansible/plugins/connection/winrm.py +46 -1
  203. ansible/plugins/doc_fragments/connection_pipelining.py +2 -2
  204. ansible/plugins/doc_fragments/constructed.py +10 -10
  205. ansible/plugins/doc_fragments/default_callback.py +8 -8
  206. ansible/plugins/doc_fragments/files.py +5 -5
  207. ansible/plugins/doc_fragments/inventory_cache.py +2 -2
  208. ansible/plugins/doc_fragments/result_format_callback.py +6 -6
  209. ansible/plugins/doc_fragments/return_common.py +1 -1
  210. ansible/plugins/doc_fragments/shell_common.py +2 -10
  211. ansible/plugins/doc_fragments/shell_windows.py +0 -9
  212. ansible/plugins/doc_fragments/url.py +2 -2
  213. ansible/plugins/doc_fragments/url_windows.py +4 -5
  214. ansible/plugins/doc_fragments/validate.py +1 -1
  215. ansible/plugins/filter/core.py +2 -0
  216. ansible/plugins/filter/human_to_bytes.yml +9 -0
  217. ansible/plugins/filter/password_hash.yml +1 -1
  218. ansible/plugins/filter/strftime.yml +1 -1
  219. ansible/plugins/filter/to_nice_json.yml +7 -3
  220. ansible/plugins/filter/to_uuid.yml +1 -1
  221. ansible/plugins/inventory/script.py +1 -1
  222. ansible/plugins/list.py +1 -1
  223. ansible/plugins/loader.py +0 -11
  224. ansible/plugins/lookup/config.py +1 -1
  225. ansible/plugins/lookup/csvfile.py +21 -9
  226. ansible/plugins/lookup/env.py +8 -9
  227. ansible/plugins/lookup/ini.py +10 -1
  228. ansible/plugins/lookup/random_choice.py +2 -2
  229. ansible/plugins/lookup/url.py +7 -2
  230. ansible/plugins/shell/__init__.py +15 -20
  231. ansible/plugins/shell/powershell.py +9 -6
  232. ansible/plugins/strategy/__init__.py +16 -7
  233. ansible/plugins/test/core.py +23 -1
  234. ansible/plugins/test/issubset.yml +1 -1
  235. ansible/plugins/test/subset.yml +1 -1
  236. ansible/plugins/test/timedout.yml +20 -0
  237. ansible/plugins/test/vault_encrypted.yml +6 -6
  238. ansible/plugins/test/vaulted_file.yml +19 -0
  239. ansible/release.py +2 -2
  240. ansible/template/__init__.py +3 -8
  241. ansible/utils/collection_loader/_collection_finder.py +23 -55
  242. ansible/utils/display.py +44 -31
  243. ansible/utils/jsonrpc.py +1 -1
  244. ansible/utils/listify.py +1 -5
  245. ansible/utils/path.py +3 -0
  246. ansible/utils/vars.py +18 -27
  247. ansible/vars/manager.py +7 -150
  248. ansible/vars/plugins.py +1 -1
  249. ansible_core-2.18.0b1.dist-info/Apache-License.txt +202 -0
  250. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/METADATA +36 -23
  251. ansible_core-2.18.0b1.dist-info/MIT-license.txt +14 -0
  252. ansible_core-2.18.0b1.dist-info/PSF-license.txt +48 -0
  253. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/RECORD +311 -306
  254. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/WHEEL +1 -1
  255. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/entry_points.txt +1 -1
  256. ansible_core-2.18.0b1.dist-info/simplified_bsd.txt +8 -0
  257. ansible_test/_data/completion/docker.txt +7 -7
  258. ansible_test/_data/completion/remote.txt +5 -4
  259. ansible_test/_data/completion/windows.txt +4 -4
  260. ansible_test/_data/requirements/ansible-test.txt +1 -2
  261. ansible_test/_data/requirements/constraints.txt +1 -2
  262. ansible_test/_data/requirements/sanity.ansible-doc.txt +3 -3
  263. ansible_test/_data/requirements/sanity.changelog.in +1 -1
  264. ansible_test/_data/requirements/sanity.changelog.txt +4 -4
  265. ansible_test/_data/requirements/sanity.import.plugin.txt +2 -2
  266. ansible_test/_data/requirements/sanity.import.txt +1 -1
  267. ansible_test/_data/requirements/sanity.integration-aliases.txt +1 -1
  268. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  269. ansible_test/_data/requirements/sanity.pylint.txt +5 -7
  270. ansible_test/_data/requirements/sanity.runtime-metadata.txt +2 -2
  271. ansible_test/_data/requirements/sanity.validate-modules.txt +3 -3
  272. ansible_test/_data/requirements/sanity.yamllint.in +1 -0
  273. ansible_test/_data/requirements/sanity.yamllint.txt +1 -1
  274. ansible_test/_internal/ansible_util.py +8 -35
  275. ansible_test/_internal/ci/azp.py +1 -1
  276. ansible_test/_internal/classification/__init__.py +0 -2
  277. ansible_test/_internal/cli/parsers/key_value_parsers.py +3 -0
  278. ansible_test/_internal/commands/integration/cloud/hcloud.py +1 -1
  279. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  280. ansible_test/_internal/commands/integration/cloud/nios.py +1 -1
  281. ansible_test/_internal/commands/sanity/__init__.py +96 -19
  282. ansible_test/_internal/commands/sanity/pylint.py +20 -24
  283. ansible_test/_internal/completion.py +2 -0
  284. ansible_test/_internal/constants.py +0 -1
  285. ansible_test/_internal/coverage_util.py +1 -2
  286. ansible_test/_internal/docker_util.py +1 -1
  287. ansible_test/_internal/host_configs.py +10 -0
  288. ansible_test/_internal/host_profiles.py +9 -13
  289. ansible_test/_internal/pypi_proxy.py +1 -1
  290. ansible_test/_internal/python_requirements.py +5 -14
  291. ansible_test/_internal/timeout.py +1 -1
  292. ansible_test/_internal/util.py +40 -0
  293. ansible_test/_internal/util_common.py +5 -1
  294. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.json +3 -1
  295. ansible_test/_util/controller/sanity/code-smell/action-plugin-docs.py +6 -3
  296. ansible_test/_util/controller/sanity/code-smell/empty-init.json +0 -2
  297. ansible_test/_util/controller/sanity/pylint/config/collection.cfg +1 -0
  298. ansible_test/_util/controller/sanity/pylint/config/default.cfg +1 -0
  299. ansible_test/_util/controller/sanity/pylint/plugins/deprecated.py +1 -19
  300. ansible_test/_util/controller/sanity/shellcheck/exclude.txt +1 -0
  301. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +67 -2
  302. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +27 -5
  303. ansible_test/_util/target/cli/ansible_test_cli_stub.py +0 -0
  304. ansible_test/_util/target/common/constants.py +2 -2
  305. ansible_test/_util/target/injector/python.py +5 -0
  306. ansible_test/_util/target/pytest/plugins/ansible_pytest_coverage.py +6 -0
  307. ansible_test/_util/target/sanity/import/importer.py +1 -1
  308. ansible_test/_util/target/setup/bootstrap.sh +6 -17
  309. ansible_test/_util/target/setup/requirements.py +14 -20
  310. ansible_test/config/config.yml +1 -1
  311. ansible_core-2.17.4.data/scripts/ansible-test +0 -44
  312. ansible_test/_data/requirements/sanity.mypy.in +0 -10
  313. ansible_test/_data/requirements/sanity.mypy.txt +0 -18
  314. ansible_test/_internal/commands/sanity/mypy.py +0 -274
  315. ansible_test/_util/controller/sanity/mypy/ansible-core.ini +0 -116
  316. ansible_test/_util/controller/sanity/mypy/ansible-test.ini +0 -27
  317. ansible_test/_util/controller/sanity/mypy/modules.ini +0 -92
  318. ansible_test/_util/controller/sanity/mypy/packaging.ini +0 -20
  319. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/COPYING +0 -0
  320. {ansible_core-2.17.4.dist-info → ansible_core-2.18.0b1.dist-info}/top_level.txt +0 -0
@@ -28,7 +28,7 @@ from __future__ import annotations
28
28
  import copy
29
29
  import functools
30
30
  import itertools
31
- import random
31
+ import secrets
32
32
  import sys
33
33
  import time
34
34
 
@@ -131,7 +131,7 @@ def generate_jittered_backoff(retries=10, delay_base=3, delay_threshold=60):
131
131
  :param delay_threshold: The maximum time in seconds for any delay.
132
132
  """
133
133
  for retry in range(0, retries):
134
- yield random.randint(0, min(delay_threshold, delay_base * 2 ** retry))
134
+ yield secrets.randbelow(min(delay_threshold, delay_base * 2 ** retry))
135
135
 
136
136
 
137
137
  def retry_never(exception_or_result):
@@ -9,7 +9,7 @@ import sys
9
9
 
10
10
  # Used for determining if the system is running a new enough python version
11
11
  # and should only restrict on our documented minimum versions
12
- _PY_MIN = (3, 7)
12
+ _PY_MIN = (3, 8)
13
13
 
14
14
  if sys.version_info < _PY_MIN:
15
15
  print(json.dumps(dict(
@@ -458,7 +458,7 @@ class AnsibleModule(object):
458
458
  self._selinux_mls_enabled = None
459
459
  self._selinux_initial_context = None
460
460
 
461
- # finally, make sure we're in a sane working dir
461
+ # finally, make sure we're in a logical working dir
462
462
  self._set_cwd()
463
463
 
464
464
  @property
@@ -1202,6 +1202,7 @@ class AnsibleModule(object):
1202
1202
  setattr(self, PASS_VARS[k][0], PASS_VARS[k][1])
1203
1203
 
1204
1204
  def safe_eval(self, value, locals=None, include_exceptions=False):
1205
+ # deprecated: description='no longer used in the codebase' core_version='2.21'
1205
1206
  return safe_eval(value, locals, include_exceptions)
1206
1207
 
1207
1208
  def _load_params(self):
@@ -1353,9 +1354,10 @@ class AnsibleModule(object):
1353
1354
  Find system executable in PATH.
1354
1355
 
1355
1356
  :param arg: The executable to find.
1356
- :param required: if executable is not found and required is ``True``, fail_json
1357
+ :param required: if the executable is not found and required is ``True``, fail_json
1357
1358
  :param opt_dirs: optional list of directories to search in addition to ``PATH``
1358
- :returns: if found return full path; otherwise return None
1359
+ :returns: if found return full path; otherwise return original arg, unless 'warning' then return None
1360
+ :raises: Sysexit: if arg is not found and required=True (via fail_json)
1359
1361
  '''
1360
1362
 
1361
1363
  bin_path = None
@@ -1364,8 +1366,6 @@ class AnsibleModule(object):
1364
1366
  except ValueError as e:
1365
1367
  if required:
1366
1368
  self.fail_json(msg=to_text(e))
1367
- else:
1368
- return bin_path
1369
1369
 
1370
1370
  return bin_path
1371
1371
 
@@ -1432,7 +1432,7 @@ class AnsibleModule(object):
1432
1432
  kwargs['deprecations'] = deprecations
1433
1433
 
1434
1434
  # preserve bools/none from no_log
1435
- # TODO: once python version on target high enough, dict comprh
1435
+ # TODO: once python version on target high enough, dict comprehensions
1436
1436
  preserved = {}
1437
1437
  for k, v in kwargs.items():
1438
1438
  if v is None or isinstance(v, bool):
@@ -1557,7 +1557,7 @@ class AnsibleModule(object):
1557
1557
  # Similar to shutil.copy(), but metadata is copied as well - in fact,
1558
1558
  # this is just shutil.copy() followed by copystat(). This is similar
1559
1559
  # to the Unix command cp -p.
1560
- #
1560
+
1561
1561
  # shutil.copystat(src, dst)
1562
1562
  # Copy the permission bits, last access time, last modification time,
1563
1563
  # and flags from src to dst. The file contents, owner, and group are
@@ -1598,6 +1598,7 @@ class AnsibleModule(object):
1598
1598
  dest_stat = os.stat(b_dest)
1599
1599
  os.chown(b_src, dest_stat.st_uid, dest_stat.st_gid)
1600
1600
  shutil.copystat(b_dest, b_src)
1601
+ os.utime(b_src, times=(time.time(), time.time()))
1601
1602
  except OSError as e:
1602
1603
  if e.errno != errno.EPERM:
1603
1604
  raise
@@ -1659,8 +1660,10 @@ class AnsibleModule(object):
1659
1660
  b_tmp_dest_name, context, False)
1660
1661
  try:
1661
1662
  tmp_stat = os.stat(b_tmp_dest_name)
1662
- if keep_dest_attrs and dest_stat and (tmp_stat.st_uid != dest_stat.st_uid or tmp_stat.st_gid != dest_stat.st_gid):
1663
- os.chown(b_tmp_dest_name, dest_stat.st_uid, dest_stat.st_gid)
1663
+ if keep_dest_attrs:
1664
+ if dest_stat and (tmp_stat.st_uid != dest_stat.st_uid or tmp_stat.st_gid != dest_stat.st_gid):
1665
+ os.chown(b_tmp_dest_name, dest_stat.st_uid, dest_stat.st_gid)
1666
+ os.utime(b_tmp_dest_name, times=(time.time(), time.time()))
1664
1667
  except OSError as e:
1665
1668
  if e.errno != errno.EPERM:
1666
1669
  raise
@@ -2058,7 +2061,7 @@ class AnsibleModule(object):
2058
2061
  # not as exact as above, but should be good enough for most platforms that fail the previous call
2059
2062
  buffer_size = select.PIPE_BUF
2060
2063
  except Exception:
2061
- buffer_size = 9000 # use sane default JIC
2064
+ buffer_size = 9000 # use logical default JIC
2062
2065
 
2063
2066
  return buffer_size
2064
2067
 
@@ -65,7 +65,7 @@ class ImmutableDict(Hashable, Mapping):
65
65
 
66
66
 
67
67
  def is_string(seq):
68
- """Identify whether the input has a string-like type (inclding bytes)."""
68
+ """Identify whether the input has a string-like type (including bytes)."""
69
69
  # AnsibleVaultEncryptedUnicode inherits from Sequence, but is expected to be a string like object
70
70
  return isinstance(seq, (text_type, binary_type)) or getattr(seq, '__ENCRYPTED__', False)
71
71
 
@@ -7,12 +7,6 @@ import os
7
7
  import stat
8
8
  import re
9
9
 
10
- try:
11
- import selinux # pylint: disable=unused-import
12
- HAVE_SELINUX = True
13
- except ImportError:
14
- HAVE_SELINUX = False
15
-
16
10
 
17
11
  FILE_ATTRIBUTES = {
18
12
  'A': 'noatime',
@@ -12,13 +12,18 @@ from ansible.module_utils.common.warnings import deprecate
12
12
  def get_bin_path(arg, opt_dirs=None, required=None):
13
13
  '''
14
14
  Find system executable in PATH. Raises ValueError if the executable is not found.
15
- Optional arguments:
16
- - required: [Deprecated] Before 2.10, if executable is not found and required is true it raises an Exception.
17
- In 2.10 and later, an Exception is always raised. This parameter will be removed in 2.21.
18
- - opt_dirs: optional list of directories to search in addition to PATH
15
+
16
+ :param arg: the executable to find
17
+ :type arg: string
18
+ :param opt_dirs: optional list of directories to search in addition to PATH
19
+ :type opt_dirs: list of strings
20
+ :param required: DEPRECATED. This parameter will be removed in 2.21
21
+ :type required: boolean
22
+ :returns: path to arg (should be abs path unless PATH or opt_dirs are relative paths)
23
+ :raises: ValueError: if arg is not found
24
+
19
25
  In addition to PATH and opt_dirs, this function also looks through /sbin, /usr/sbin and /usr/local/sbin. A lot of
20
26
  modules, especially for gathering facts, depend on this behaviour.
21
- If found return full path, otherwise raise ValueError.
22
27
  '''
23
28
  if required is not None:
24
29
  deprecate(
@@ -27,26 +32,34 @@ def get_bin_path(arg, opt_dirs=None, required=None):
27
32
  collection_name="ansible.builtin",
28
33
  )
29
34
 
35
+ paths = []
36
+ sbin_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
30
37
  opt_dirs = [] if opt_dirs is None else opt_dirs
31
38
 
32
- sbin_paths = ['/sbin', '/usr/sbin', '/usr/local/sbin']
33
- paths = []
39
+ # Construct possible paths with precedence
40
+ # passed in paths
34
41
  for d in opt_dirs:
35
42
  if d is not None and os.path.exists(d):
36
43
  paths.append(d)
44
+ # system configured paths
37
45
  paths += os.environ.get('PATH', '').split(os.pathsep)
38
- bin_path = None
39
- # mangle PATH to include /sbin dirs
46
+
47
+ # existing /sbin dirs, if not there already
40
48
  for p in sbin_paths:
41
49
  if p not in paths and os.path.exists(p):
42
50
  paths.append(p)
51
+
52
+ # Search for binary
53
+ bin_path = None
43
54
  for d in paths:
44
55
  if not d:
45
56
  continue
46
57
  path = os.path.join(d, arg)
47
58
  if os.path.exists(path) and not os.path.isdir(path) and is_executable(path):
59
+ # fist found wins
48
60
  bin_path = path
49
61
  break
62
+
50
63
  if bin_path is None:
51
64
  raise ValueError('Failed to find required executable "%s" in paths: %s' % (arg, os.pathsep.join(paths)))
52
65
 
@@ -267,14 +267,11 @@ def _json_encode_fallback(obj):
267
267
 
268
268
 
269
269
  def jsonify(data, **kwargs):
270
- # After 2.18, we should remove this loop, and hardcode to utf-8 in alignment with requiring utf-8 module responses
271
- for encoding in ("utf-8", "latin-1"):
272
- try:
273
- new_data = container_to_text(data, encoding=encoding)
274
- except UnicodeDecodeError:
275
- continue
276
- return json.dumps(new_data, default=_json_encode_fallback, **kwargs)
277
- raise UnicodeError('Invalid unicode encoding encountered')
270
+ try:
271
+ new_data = container_to_text(data, encoding='utf-8')
272
+ except UnicodeDecodeError:
273
+ raise UnicodeError('Invalid unicode encoding encountered')
274
+ return json.dumps(new_data, default=_json_encode_fallback, **kwargs)
278
275
 
279
276
 
280
277
  def container_to_bytes(d, encoding='utf-8', errors='surrogate_or_strict'):
@@ -20,6 +20,18 @@ SIZE_RANGES = {
20
20
  'B': 1,
21
21
  }
22
22
 
23
+ VALID_UNITS = {
24
+ 'B': (('byte', 'B'), ('bit', 'b')),
25
+ 'K': (('kilobyte', 'KB'), ('kilobit', 'Kb')),
26
+ 'M': (('megabyte', 'MB'), ('megabit', 'Mb')),
27
+ 'G': (('gigabyte', 'GB'), ('gigabit', 'Gb')),
28
+ 'T': (('terabyte', 'TB'), ('terabit', 'Tb')),
29
+ 'P': (('petabyte', 'PB'), ('petabit', 'Pb')),
30
+ 'E': (('exabyte', 'EB'), ('exabit', 'Eb')),
31
+ 'Z': (('zetabyte', 'ZB'), ('zetabit', 'Zb')),
32
+ 'Y': (('yottabyte', 'YB'), ('yottabit', 'Yb')),
33
+ }
34
+
23
35
 
24
36
  def lenient_lowercase(lst):
25
37
  """Lowercase elements of a list.
@@ -53,7 +65,8 @@ def human_to_bytes(number, default_unit=None, isbits=False):
53
65
  The function expects 'b' (lowercase) as a bit identifier, e.g. 'Mb'/'Kb'/etc.
54
66
  if 'MB'/'KB'/... is passed, the ValueError will be rased.
55
67
  """
56
- m = re.search(r'^\s*(\d*\.?\d*)\s*([A-Za-z]+)?', str(number), flags=re.IGNORECASE)
68
+ m = re.search(r'^([0-9]*\.?[0-9]+)(?:\s*([A-Za-z]+))?\s*$', str(number))
69
+
57
70
  if m is None:
58
71
  raise ValueError("human_to_bytes() can't interpret following string: %s" % str(number))
59
72
  try:
@@ -86,10 +99,13 @@ def human_to_bytes(number, default_unit=None, isbits=False):
86
99
  expect_message = 'expect %s%s or %s' % (range_key, unit_class, range_key)
87
100
  if range_key == 'B':
88
101
  expect_message = 'expect %s or %s' % (unit_class, unit_class_name)
89
-
90
- if unit_class_name in unit.lower():
102
+ unit_group = VALID_UNITS.get(range_key, None)
103
+ if unit_group is None:
104
+ raise ValueError(f"human_to_bytes() can't interpret a valid unit for {range_key}")
105
+ isbits_flag = 1 if isbits else 0
106
+ if unit.lower() == unit_group[isbits_flag][0]:
91
107
  pass
92
- elif unit[1] != unit_class:
108
+ elif unit != unit_group[isbits_flag][1]:
93
109
  raise ValueError("human_to_bytes() failed to convert %s. Value is not a valid string (%s)" % (number, expect_message))
94
110
 
95
111
  return int(round(num * limit))
@@ -4,6 +4,7 @@
4
4
 
5
5
  from __future__ import annotations
6
6
 
7
+ import decimal
7
8
  import json
8
9
  import os
9
10
  import re
@@ -13,10 +14,10 @@ from ansible.module_utils.common.text.converters import to_native
13
14
  from ansible.module_utils.common.collections import is_iterable
14
15
  from ansible.module_utils.common.text.converters import jsonify
15
16
  from ansible.module_utils.common.text.formatters import human_to_bytes
17
+ from ansible.module_utils.common.warnings import deprecate
16
18
  from ansible.module_utils.parsing.convert_bool import boolean
17
19
  from ansible.module_utils.six import (
18
20
  binary_type,
19
- integer_types,
20
21
  string_types,
21
22
  text_type,
22
23
  )
@@ -39,6 +40,10 @@ def count_terms(terms, parameters):
39
40
 
40
41
 
41
42
  def safe_eval(value, locals=None, include_exceptions=False):
43
+ deprecate(
44
+ "The safe_eval function should not be used.",
45
+ version="2.21",
46
+ )
42
47
  # do not allow method calls to modules
43
48
  if not isinstance(value, string_types):
44
49
  # already templated to a datavaluestructure, perhaps?
@@ -415,7 +420,7 @@ def check_type_dict(value):
415
420
 
416
421
  Raises :class:`TypeError` if unable to convert to a dict
417
422
 
418
- :arg value: Dict or string to convert to a dict. Accepts ``k1=v2, k2=v2``.
423
+ :arg value: Dict or string to convert to a dict. Accepts ``k1=v2, k2=v2`` or ``k1=v2 k2=v2``.
419
424
 
420
425
  :returns: value converted to a dictionary
421
426
  """
@@ -427,10 +432,14 @@ def check_type_dict(value):
427
432
  try:
428
433
  return json.loads(value)
429
434
  except Exception:
430
- (result, exc) = safe_eval(value, dict(), include_exceptions=True)
431
- if exc is not None:
432
- raise TypeError('unable to evaluate string as dictionary')
433
- return result
435
+ try:
436
+ result = literal_eval(value)
437
+ except Exception:
438
+ pass
439
+ else:
440
+ if isinstance(result, dict):
441
+ return result
442
+ raise TypeError('unable to evaluate string as dictionary')
434
443
  elif '=' in value:
435
444
  fields = []
436
445
  field_buffer = []
@@ -457,7 +466,11 @@ def check_type_dict(value):
457
466
  field = ''.join(field_buffer)
458
467
  if field:
459
468
  fields.append(field)
460
- return dict(x.split("=", 1) for x in fields)
469
+ try:
470
+ return dict(x.split("=", 1) for x in fields)
471
+ except ValueError:
472
+ # no "=" to split on: "k1=v1, k2"
473
+ raise TypeError('unable to evaluate string in the "key=value" format as dictionary')
461
474
  else:
462
475
  raise TypeError("dictionary requested, could not parse JSON or key=value")
463
476
 
@@ -493,16 +506,15 @@ def check_type_int(value):
493
506
 
494
507
  :return: int of given value
495
508
  """
496
- if isinstance(value, integer_types):
497
- return value
498
-
499
- if isinstance(value, string_types):
509
+ if not isinstance(value, int):
500
510
  try:
501
- return int(value)
502
- except ValueError:
503
- pass
504
-
505
- raise TypeError('%s cannot be converted to an int' % type(value))
511
+ if (decimal_value := decimal.Decimal(value)) != (int_value := int(decimal_value)):
512
+ raise ValueError("Significant decimal part found")
513
+ else:
514
+ value = int_value
515
+ except (decimal.DecimalException, TypeError, ValueError) as e:
516
+ raise TypeError(f'"{value!r}" cannot be converted to an int') from e
517
+ return value
506
518
 
507
519
 
508
520
  def check_type_float(value):
@@ -514,16 +526,12 @@ def check_type_float(value):
514
526
 
515
527
  :returns: float of given value.
516
528
  """
517
- if isinstance(value, float):
518
- return value
519
-
520
- if isinstance(value, (binary_type, text_type, int)):
529
+ if not isinstance(value, float):
521
530
  try:
522
- return float(value)
523
- except ValueError:
524
- pass
525
-
526
- raise TypeError('%s cannot be converted to a float' % type(value))
531
+ value = float(value)
532
+ except (TypeError, ValueError) as e:
533
+ raise TypeError(f'{type(value)} cannot be converted to a float')
534
+ return value
527
535
 
528
536
 
529
537
  def check_type_path(value,):
@@ -11,7 +11,12 @@ PARAMIKO_IMPORT_ERR = None
11
11
 
12
12
  try:
13
13
  with warnings.catch_warnings():
14
- warnings.filterwarnings('ignore', message='Blowfish has been deprecated', category=UserWarning)
14
+ # Blowfish has been moved, but the deprecated import is used by paramiko versions older than 2.9.5.
15
+ # See: https://github.com/paramiko/paramiko/pull/2039
16
+ warnings.filterwarnings('ignore', message='Blowfish has been ', category=UserWarning)
17
+ # TripleDES has been moved, but the deprecated import is used by paramiko versions older than 3.3.2 and 3.4.1.
18
+ # See: https://github.com/paramiko/paramiko/pull/2421
19
+ warnings.filterwarnings('ignore', message='TripleDES has been ', category=UserWarning)
15
20
  import paramiko # pylint: disable=unused-import
16
21
  # paramiko and gssapi are incompatible and raise AttributeError not ImportError
17
22
  # When running in FIPS mode, cryptography raises InternalError
@@ -11,8 +11,8 @@ from ctypes import CDLL, c_char_p, c_int, byref, POINTER, get_errno
11
11
 
12
12
  try:
13
13
  _selinux_lib = CDLL('libselinux.so.1', use_errno=True)
14
- except OSError:
15
- raise ImportError('unable to load libselinux.so')
14
+ except OSError as ex:
15
+ raise ImportError('unable to load libselinux.so') from ex
16
16
 
17
17
 
18
18
  def _module_setup():
@@ -29,8 +29,8 @@
29
29
  from __future__ import annotations
30
30
 
31
31
  import os
32
- import hashlib
33
32
  import json
33
+ import pickle
34
34
  import socket
35
35
  import struct
36
36
  import traceback
@@ -40,30 +40,14 @@ from functools import partial
40
40
  from ansible.module_utils.common.text.converters import to_bytes, to_text
41
41
  from ansible.module_utils.common.json import AnsibleJSONEncoder
42
42
  from ansible.module_utils.six import iteritems
43
- from ansible.module_utils.six.moves import cPickle
44
43
 
45
44
 
46
- def write_to_file_descriptor(fd, obj):
47
- """Handles making sure all data is properly written to file descriptor fd.
45
+ def write_to_stream(stream, obj):
46
+ """Write a length+newline-prefixed pickled object to a stream."""
47
+ src = pickle.dumps(obj)
48
48
 
49
- In particular, that data is encoded in a character stream-friendly way and
50
- that all data gets written before returning.
51
- """
52
- # Need to force a protocol that is compatible with both py2 and py3.
53
- # That would be protocol=2 or less.
54
- # Also need to force a protocol that excludes certain control chars as
55
- # stdin in this case is a pty and control chars will cause problems.
56
- # that means only protocol=0 will work.
57
- src = cPickle.dumps(obj, protocol=0)
58
-
59
- # raw \r characters will not survive pty round-trip
60
- # They should be rehydrated on the receiving end
61
- src = src.replace(b'\r', br'\r')
62
- data_hash = to_bytes(hashlib.sha1(src).hexdigest())
63
-
64
- os.write(fd, b'%d\n' % len(src))
65
- os.write(fd, src)
66
- os.write(fd, b'%s\n' % data_hash)
49
+ stream.write(b'%d\n' % len(src))
50
+ stream.write(src)
67
51
 
68
52
 
69
53
  def send_data(s, data):
@@ -146,7 +130,7 @@ class Connection(object):
146
130
  data = json.dumps(req, cls=AnsibleJSONEncoder, vault_to_text=True)
147
131
  except TypeError as exc:
148
132
  raise ConnectionError(
149
- "Failed to encode some variables as JSON for communication with ansible-connection. "
133
+ "Failed to encode some variables as JSON for communication with the persistent connection helper. "
150
134
  "The original exception was: %s" % to_text(exc)
151
135
  )
152
136
 
@@ -176,7 +160,7 @@ class Connection(object):
176
160
  if response['id'] != reqid:
177
161
  raise ConnectionError('invalid json-rpc id received')
178
162
  if "result_type" in response:
179
- response["result"] = cPickle.loads(to_bytes(response["result"]))
163
+ response["result"] = pickle.loads(to_bytes(response["result"], errors="surrogateescape"))
180
164
 
181
165
  return response
182
166
 
@@ -333,13 +333,12 @@ namespace Ansible.Become
333
333
  // Grant access to the current Windows Station and Desktop to the become user
334
334
  GrantAccessToWindowStationAndDesktop(account);
335
335
 
336
- // Try and impersonate a SYSTEM token, we need a SYSTEM token to either become a well known service
337
- // account or have administrative rights on the become access token.
338
- // If we ultimately are becoming the SYSTEM account we want the token with the most privileges available.
339
- // https://github.com/ansible/ansible/issues/71453
340
- bool mostPrivileges = becomeSid == "S-1-5-18";
336
+ // Try and impersonate a SYSTEM token. We need the SeTcbPrivilege for
337
+ // - LogonUser for a service SID
338
+ // - S4U logon
339
+ // - Token elevation
341
340
  systemToken = GetPrimaryTokenForUser(new SecurityIdentifier("S-1-5-18"),
342
- new List<string>() { "SeTcbPrivilege" }, mostPrivileges);
341
+ new List<string>() { "SeTcbPrivilege" });
343
342
  if (systemToken != null)
344
343
  {
345
344
  try
@@ -357,11 +356,9 @@ namespace Ansible.Become
357
356
 
358
357
  try
359
358
  {
360
- if (becomeSid == "S-1-5-18")
361
- userTokens.Add(systemToken);
362
359
  // Cannot use String.IsEmptyOrNull() as an empty string is an account that doesn't have a pass.
363
360
  // We only use S4U if no password was defined or it was null
364
- else if (!SERVICE_SIDS.Contains(becomeSid) && password == null && logonType != LogonType.NewCredentials)
361
+ if (!SERVICE_SIDS.Contains(becomeSid) && password == null && logonType != LogonType.NewCredentials)
365
362
  {
366
363
  // If no password was specified, try and duplicate an existing token for that user or use S4U to
367
364
  // generate one without network credentials
@@ -384,6 +381,11 @@ namespace Ansible.Become
384
381
  string domain = null;
385
382
  switch (becomeSid)
386
383
  {
384
+ case "S-1-5-18":
385
+ logonType = LogonType.Service;
386
+ domain = "NT AUTHORITY";
387
+ username = "SYSTEM";
388
+ break;
387
389
  case "S-1-5-19":
388
390
  logonType = LogonType.Service;
389
391
  domain = "NT AUTHORITY";
@@ -426,7 +428,7 @@ namespace Ansible.Become
426
428
  }
427
429
 
428
430
  private static SafeNativeHandle GetPrimaryTokenForUser(SecurityIdentifier sid,
429
- List<string> requiredPrivileges = null, bool mostPrivileges = false)
431
+ List<string> requiredPrivileges = null)
430
432
  {
431
433
  // According to CreateProcessWithTokenW we require a token with
432
434
  // TOKEN_QUERY, TOKEN_DUPLICATE and TOKEN_ASSIGN_PRIMARY
@@ -436,9 +438,6 @@ namespace Ansible.Become
436
438
  TokenAccessLevels.AssignPrimary |
437
439
  TokenAccessLevels.Impersonate;
438
440
 
439
- SafeNativeHandle userToken = null;
440
- int privilegeCount = 0;
441
-
442
441
  foreach (SafeNativeHandle hToken in TokenUtil.EnumerateUserTokens(sid, dwAccess))
443
442
  {
444
443
  // Filter out any Network logon tokens, using become with that is useless when S4U
@@ -449,10 +448,6 @@ namespace Ansible.Become
449
448
 
450
449
  List<string> actualPrivileges = TokenUtil.GetTokenPrivileges(hToken).Select(x => x.Name).ToList();
451
450
 
452
- // If the token has less or the same number of privileges than the current token, skip it.
453
- if (mostPrivileges && privilegeCount >= actualPrivileges.Count)
454
- continue;
455
-
456
451
  // Check that the required privileges are on the token
457
452
  if (requiredPrivileges != null)
458
453
  {
@@ -464,22 +459,16 @@ namespace Ansible.Become
464
459
  // Duplicate the token to convert it to a primary token with the access level required.
465
460
  try
466
461
  {
467
- userToken = TokenUtil.DuplicateToken(hToken, TokenAccessLevels.MaximumAllowed,
462
+ return TokenUtil.DuplicateToken(hToken, TokenAccessLevels.MaximumAllowed,
468
463
  SecurityImpersonationLevel.Anonymous, TokenType.Primary);
469
- privilegeCount = actualPrivileges.Count;
470
464
  }
471
465
  catch (Process.Win32Exception)
472
466
  {
473
467
  continue;
474
468
  }
475
-
476
- // If we don't care about getting the token with the most privileges, escape the loop as we already
477
- // have a token.
478
- if (!mostPrivileges)
479
- break;
480
469
  }
481
470
 
482
- return userToken;
471
+ return null;
483
472
  }
484
473
 
485
474
  private static SafeNativeHandle GetS4UTokenForUser(SecurityIdentifier sid, LogonType logonType)
@@ -397,7 +397,7 @@ namespace Ansible.Process
397
397
  internal static Result WaitProcess(SafeFileHandle stdoutRead, SafeFileHandle stdoutWrite, SafeFileHandle stderrRead,
398
398
  SafeFileHandle stderrWrite, FileStream stdinStream, byte[] stdin, IntPtr hProcess, string outputEncoding = null)
399
399
  {
400
- // Default to using UTF-8 as the output encoding, this should be a sane default for most scenarios.
400
+ // Default to using UTF-8 as the output encoding, this should be a logical default for most scenarios.
401
401
  outputEncoding = String.IsNullOrEmpty(outputEncoding) ? "utf-8" : outputEncoding;
402
402
  Encoding encodingInstance = Encoding.GetEncoding(outputEncoding);
403
403
 
@@ -22,7 +22,7 @@ Compat distro library.
22
22
  from __future__ import annotations
23
23
 
24
24
  # The following makes it easier for us to script updates of the bundled code
25
- _BUNDLED_METADATA = {"pypi_name": "distro", "version": "1.8.0"}
25
+ _BUNDLED_METADATA = {"pypi_name": "distro", "version": "1.9.0"}
26
26
 
27
27
  # The following additional changes have been made:
28
28
  # * Remove optparse since it is not needed for our use.
@@ -1,4 +1,4 @@
1
- # Copyright 2015,2016,2017 Nir Cohen
1
+ # Copyright 2015-2021 Nir Cohen
2
2
  #
3
3
  # Licensed under the Apache License, Version 2.0 (the "License");
4
4
  # you may not use this file except in compliance with the License.
@@ -59,7 +59,7 @@ except ImportError:
59
59
  # Python 3.7
60
60
  TypedDict = dict
61
61
 
62
- __version__ = "1.8.0"
62
+ __version__ = "1.9.0"
63
63
 
64
64
 
65
65
  class VersionDict(TypedDict):
@@ -129,6 +129,7 @@ _DISTRO_RELEASE_BASENAME_PATTERN = re.compile(r"(\w+)[-_](release|version)$")
129
129
  # Base file names to be looked up for if _UNIXCONFDIR is not readable.
130
130
  _DISTRO_RELEASE_BASENAMES = [
131
131
  "SuSE-release",
132
+ "altlinux-release",
132
133
  "arch-release",
133
134
  "base-release",
134
135
  "centos-release",
@@ -155,6 +156,8 @@ _DISTRO_RELEASE_IGNORE_BASENAMES = (
155
156
  "system-release",
156
157
  "plesk-release",
157
158
  "iredmail-release",
159
+ "board-release",
160
+ "ec2_version",
158
161
  )
159
162
 
160
163
 
@@ -247,6 +250,7 @@ def id() -> str:
247
250
  "rocky" Rocky Linux
248
251
  "aix" AIX
249
252
  "guix" Guix System
253
+ "altlinux" ALT Linux
250
254
  ============== =========================================
251
255
 
252
256
  If you have a need to get distros for reliable IDs added into this set,
@@ -995,10 +999,10 @@ class LinuxDistribution:
995
999
 
996
1000
  For details, see :func:`distro.info`.
997
1001
  """
998
- return dict(
1002
+ return InfoDict(
999
1003
  id=self.id(),
1000
1004
  version=self.version(pretty, best),
1001
- version_parts=dict(
1005
+ version_parts=VersionDict(
1002
1006
  major=self.major_version(best),
1003
1007
  minor=self.minor_version(best),
1004
1008
  build_number=self.build_number(best),
@@ -90,6 +90,8 @@ class BaseFactCollector:
90
90
  def _transform_dict_keys(self, fact_dict):
91
91
  '''update a dicts keys to use new names as transformed by self._transform_name'''
92
92
 
93
+ if fact_dict is None:
94
+ return {}
93
95
  for old_key in list(fact_dict.keys()):
94
96
  new_key = self._transform_name(old_key)
95
97
  # pop the item by old_key and replace it using new_key