ansible-core 2.19.2__py3-none-any.whl → 2.20.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 (202) hide show
  1. ansible/_internal/__init__.py +1 -4
  2. ansible/_internal/_ansiballz/_builder.py +1 -3
  3. ansible/_internal/_collection_proxy.py +7 -9
  4. ansible/_internal/_display_utils.py +145 -0
  5. ansible/_internal/_json/__init__.py +3 -4
  6. ansible/_internal/_templating/_engine.py +1 -1
  7. ansible/_internal/_templating/_jinja_plugins.py +1 -2
  8. ansible/_internal/_wrapt.py +105 -301
  9. ansible/cli/__init__.py +11 -10
  10. ansible/cli/adhoc.py +1 -2
  11. ansible/cli/arguments/option_helpers.py +1 -1
  12. ansible/cli/config.py +5 -6
  13. ansible/cli/doc.py +67 -67
  14. ansible/cli/galaxy.py +15 -24
  15. ansible/cli/inventory.py +0 -1
  16. ansible/cli/playbook.py +0 -1
  17. ansible/cli/pull.py +0 -1
  18. ansible/cli/scripts/ansible_connection_cli_stub.py +1 -1
  19. ansible/config/base.yml +1 -25
  20. ansible/config/manager.py +0 -2
  21. ansible/executor/play_iterator.py +42 -20
  22. ansible/executor/playbook_executor.py +0 -9
  23. ansible/executor/powershell/async_watchdog.ps1 +24 -4
  24. ansible/executor/task_executor.py +32 -22
  25. ansible/executor/task_queue_manager.py +1 -3
  26. ansible/galaxy/api.py +33 -80
  27. ansible/galaxy/collection/__init__.py +4 -17
  28. ansible/galaxy/dependency_resolution/dataclasses.py +0 -10
  29. ansible/galaxy/dependency_resolution/providers.py +1 -2
  30. ansible/galaxy/role.py +1 -33
  31. ansible/inventory/manager.py +2 -3
  32. ansible/keyword_desc.yml +0 -3
  33. ansible/module_utils/_internal/_datatag/__init__.py +2 -10
  34. ansible/module_utils/_internal/_no_six.py +86 -0
  35. ansible/module_utils/_text.py +28 -8
  36. ansible/module_utils/ansible_release.py +2 -2
  37. ansible/module_utils/basic.py +27 -24
  38. ansible/module_utils/common/_collections_compat.py +11 -2
  39. ansible/module_utils/common/collections.py +8 -3
  40. ansible/module_utils/common/dict_transformations.py +1 -2
  41. ansible/module_utils/common/network.py +4 -2
  42. ansible/module_utils/common/parameters.py +32 -41
  43. ansible/module_utils/common/text/converters.py +109 -23
  44. ansible/module_utils/common/text/formatters.py +6 -2
  45. ansible/module_utils/common/validation.py +11 -9
  46. ansible/module_utils/connection.py +8 -3
  47. ansible/module_utils/facts/hardware/linux.py +23 -7
  48. ansible/module_utils/facts/hardware/netbsd.py +1 -1
  49. ansible/module_utils/facts/hardware/sunos.py +2 -1
  50. ansible/module_utils/facts/packages.py +6 -2
  51. ansible/module_utils/facts/system/distribution.py +2 -1
  52. ansible/module_utils/facts/system/env.py +6 -3
  53. ansible/module_utils/facts/system/local.py +3 -1
  54. ansible/module_utils/parsing/convert_bool.py +6 -2
  55. ansible/module_utils/service.py +2 -3
  56. ansible/module_utils/six/__init__.py +11 -6
  57. ansible/module_utils/urls.py +6 -2
  58. ansible/module_utils/yumdnf.py +0 -5
  59. ansible/modules/apt.py +18 -13
  60. ansible/modules/apt_repository.py +1 -1
  61. ansible/modules/assemble.py +5 -9
  62. ansible/modules/blockinfile.py +39 -23
  63. ansible/modules/cron.py +26 -35
  64. ansible/modules/deb822_repository.py +83 -12
  65. ansible/modules/dnf.py +3 -7
  66. ansible/modules/dnf5.py +4 -6
  67. ansible/modules/expect.py +0 -3
  68. ansible/modules/find.py +1 -2
  69. ansible/modules/get_url.py +1 -1
  70. ansible/modules/git.py +4 -5
  71. ansible/modules/include_vars.py +1 -1
  72. ansible/modules/lineinfile.py +71 -63
  73. ansible/modules/package_facts.py +1 -1
  74. ansible/modules/pip.py +8 -2
  75. ansible/modules/replace.py +6 -6
  76. ansible/modules/service.py +3 -4
  77. ansible/modules/stat.py +20 -0
  78. ansible/modules/uri.py +9 -10
  79. ansible/modules/user.py +1 -2
  80. ansible/modules/wait_for.py +2 -2
  81. ansible/modules/wait_for_connection.py +2 -1
  82. ansible/modules/yum_repository.py +1 -16
  83. ansible/parsing/dataloader.py +24 -31
  84. ansible/parsing/mod_args.py +3 -0
  85. ansible/parsing/vault/__init__.py +1 -2
  86. ansible/playbook/base.py +8 -56
  87. ansible/playbook/block.py +0 -60
  88. ansible/playbook/collectionsearch.py +1 -2
  89. ansible/playbook/handler.py +1 -7
  90. ansible/playbook/helpers.py +0 -7
  91. ansible/playbook/included_file.py +1 -1
  92. ansible/playbook/play.py +103 -37
  93. ansible/playbook/play_context.py +4 -0
  94. ansible/playbook/role/__init__.py +10 -65
  95. ansible/playbook/role/definition.py +3 -4
  96. ansible/playbook/role/include.py +2 -3
  97. ansible/playbook/role/metadata.py +1 -12
  98. ansible/playbook/role/requirement.py +1 -2
  99. ansible/playbook/role_include.py +1 -2
  100. ansible/playbook/taggable.py +16 -5
  101. ansible/playbook/task.py +51 -55
  102. ansible/plugins/action/__init__.py +20 -19
  103. ansible/plugins/action/add_host.py +1 -2
  104. ansible/plugins/action/fetch.py +2 -4
  105. ansible/plugins/action/group_by.py +1 -2
  106. ansible/plugins/action/include_vars.py +20 -22
  107. ansible/plugins/action/script.py +1 -3
  108. ansible/plugins/action/template.py +1 -2
  109. ansible/plugins/action/uri.py +4 -2
  110. ansible/plugins/cache/__init__.py +1 -0
  111. ansible/plugins/callback/__init__.py +13 -6
  112. ansible/plugins/connection/__init__.py +3 -7
  113. ansible/plugins/connection/local.py +2 -3
  114. ansible/plugins/connection/psrp.py +0 -2
  115. ansible/plugins/connection/ssh.py +2 -7
  116. ansible/plugins/connection/winrm.py +0 -2
  117. ansible/plugins/doc_fragments/result_format_callback.py +15 -0
  118. ansible/plugins/filter/core.py +4 -5
  119. ansible/plugins/filter/encryption.py +3 -27
  120. ansible/plugins/filter/mathstuff.py +1 -2
  121. ansible/plugins/filter/to_nice_yaml.yml +31 -3
  122. ansible/plugins/filter/to_yaml.yml +29 -12
  123. ansible/plugins/inventory/__init__.py +1 -2
  124. ansible/plugins/inventory/script.py +2 -1
  125. ansible/plugins/inventory/toml.py +3 -6
  126. ansible/plugins/inventory/yaml.py +1 -2
  127. ansible/plugins/list.py +10 -3
  128. ansible/plugins/loader.py +6 -6
  129. ansible/plugins/lookup/password.py +1 -2
  130. ansible/plugins/lookup/subelements.py +2 -3
  131. ansible/plugins/lookup/url.py +1 -1
  132. ansible/plugins/lookup/varnames.py +1 -2
  133. ansible/plugins/shell/__init__.py +9 -4
  134. ansible/plugins/shell/powershell.py +8 -24
  135. ansible/plugins/strategy/__init__.py +6 -3
  136. ansible/plugins/test/core.py +4 -1
  137. ansible/plugins/test/regex.yml +18 -6
  138. ansible/release.py +2 -2
  139. ansible/template/__init__.py +3 -7
  140. ansible/utils/collection_loader/_collection_config.py +5 -0
  141. ansible/utils/collection_loader/_collection_finder.py +11 -14
  142. ansible/utils/context_objects.py +7 -4
  143. ansible/utils/display.py +28 -167
  144. ansible/utils/encrypt.py +0 -5
  145. ansible/utils/helpers.py +6 -2
  146. ansible/utils/jsonrpc.py +7 -3
  147. ansible/utils/plugin_docs.py +49 -38
  148. ansible/utils/ssh_functions.py +0 -19
  149. ansible/utils/unsafe_proxy.py +7 -7
  150. ansible/vars/clean.py +2 -3
  151. ansible/vars/manager.py +27 -20
  152. ansible/vars/plugins.py +1 -31
  153. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/METADATA +3 -3
  154. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/RECORD +200 -200
  155. ansible_test/_data/completion/docker.txt +7 -7
  156. ansible_test/_data/completion/network.txt +0 -1
  157. ansible_test/_data/completion/remote.txt +4 -4
  158. ansible_test/_data/requirements/ansible-test.txt +1 -1
  159. ansible_test/_data/requirements/sanity.changelog.txt +1 -1
  160. ansible_test/_data/requirements/sanity.pep8.txt +1 -1
  161. ansible_test/_data/requirements/sanity.pylint.txt +4 -4
  162. ansible_test/_internal/cache.py +2 -5
  163. ansible_test/_internal/cli/compat.py +1 -1
  164. ansible_test/_internal/commands/coverage/combine.py +1 -3
  165. ansible_test/_internal/commands/integration/__init__.py +3 -7
  166. ansible_test/_internal/commands/integration/cloud/httptester.py +1 -1
  167. ansible_test/_internal/commands/integration/coverage.py +1 -3
  168. ansible_test/_internal/commands/integration/filters.py +5 -10
  169. ansible_test/_internal/commands/sanity/validate_modules.py +1 -5
  170. ansible_test/_internal/commands/units/__init__.py +1 -13
  171. ansible_test/_internal/completion.py +2 -5
  172. ansible_test/_internal/config.py +2 -7
  173. ansible_test/_internal/coverage_util.py +1 -1
  174. ansible_test/_internal/delegation.py +2 -0
  175. ansible_test/_internal/docker_util.py +1 -1
  176. ansible_test/_internal/host_profiles.py +6 -11
  177. ansible_test/_internal/provider/__init__.py +2 -5
  178. ansible_test/_internal/provisioning.py +2 -5
  179. ansible_test/_internal/pypi_proxy.py +1 -1
  180. ansible_test/_internal/target.py +2 -6
  181. ansible_test/_internal/thread.py +1 -4
  182. ansible_test/_internal/util.py +9 -14
  183. ansible_test/_util/controller/sanity/code-smell/runtime-metadata.py +14 -19
  184. ansible_test/_util/controller/sanity/pylint/plugins/unwanted.py +30 -27
  185. ansible_test/_util/controller/sanity/validate-modules/validate_modules/main.py +31 -18
  186. ansible_test/_util/controller/sanity/validate-modules/validate_modules/module_args.py +1 -2
  187. ansible_test/_util/controller/sanity/validate-modules/validate_modules/schema.py +59 -71
  188. ansible_test/_util/controller/sanity/validate-modules/validate_modules/utils.py +1 -2
  189. ansible_test/_util/target/cli/ansible_test_cli_stub.py +4 -2
  190. ansible_test/_util/target/common/constants.py +2 -2
  191. ansible_test/_util/target/setup/bootstrap.sh +0 -6
  192. ansible/utils/py3compat.py +0 -27
  193. ansible_test/_data/pytest/config/legacy.ini +0 -4
  194. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/WHEEL +0 -0
  195. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/entry_points.txt +0 -0
  196. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/COPYING +0 -0
  197. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/Apache-License.txt +0 -0
  198. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/BSD-3-Clause.txt +0 -0
  199. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/MIT-license.txt +0 -0
  200. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/PSF-license.txt +0 -0
  201. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/licenses/licenses/simplified_bsd.txt +0 -0
  202. {ansible_core-2.19.2.dist-info → ansible_core-2.20.0b1.dist-info}/top_level.txt +0 -0
@@ -1358,7 +1358,8 @@ def _split_multiext(name, min=3, max=4, count=2):
1358
1358
 
1359
1359
  def fetch_file(module, url, data=None, headers=None, method=None,
1360
1360
  use_proxy=True, force=False, last_mod_time=None, timeout=10,
1361
- unredirected_headers=None, decompress=True, ciphers=None):
1361
+ unredirected_headers=None, decompress=True, ciphers=None,
1362
+ ca_path=None, cookies=None):
1362
1363
  """Download and save a file via HTTP(S) or FTP (needs the module as parameter).
1363
1364
  This is basically a wrapper around fetch_url().
1364
1365
 
@@ -1375,6 +1376,8 @@ def fetch_file(module, url, data=None, headers=None, method=None,
1375
1376
  :kwarg unredirected_headers: (optional) A list of headers to not attach on a redirected request
1376
1377
  :kwarg decompress: (optional) Whether to attempt to decompress gzip content-encoded responses
1377
1378
  :kwarg ciphers: (optional) List of ciphers to use
1379
+ :kwarg ca_path: (optional) Path to CA bundle
1380
+ :kwarg cookies: (optional) CookieJar object to send with the request
1378
1381
 
1379
1382
  :returns: A string, the path to the downloaded file.
1380
1383
  """
@@ -1386,7 +1389,8 @@ def fetch_file(module, url, data=None, headers=None, method=None,
1386
1389
  module.add_cleanup_file(fetch_temp_file.name)
1387
1390
  try:
1388
1391
  rsp, info = fetch_url(module, url, data, headers, method, use_proxy, force, last_mod_time, timeout,
1389
- unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers)
1392
+ unredirected_headers=unredirected_headers, decompress=decompress, ciphers=ciphers,
1393
+ ca_path=ca_path, cookies=cookies)
1390
1394
  if not rsp or (rsp.code and rsp.code >= 400):
1391
1395
  module.fail_json(msg="Failure downloading %s, %s" % (url, info['msg']))
1392
1396
  data = rsp.read(bufsize)
@@ -32,10 +32,6 @@ yumdnf_argument_spec = dict(
32
32
  enablerepo=dict(type='list', elements='str', default=[]),
33
33
  exclude=dict(type='list', elements='str', default=[]),
34
34
  installroot=dict(type='str', default="/"),
35
- install_repoquery=dict(
36
- type='bool', default=True,
37
- removed_in_version='2.20', removed_from_collection='ansible.builtin',
38
- ),
39
35
  install_weak_deps=dict(type='bool', default=True),
40
36
  list=dict(type='str'),
41
37
  name=dict(type='list', elements='str', aliases=['pkg'], default=[]),
@@ -85,7 +81,6 @@ class YumDnf(metaclass=ABCMeta):
85
81
  self.enablerepo = self.module.params.get('enablerepo', [])
86
82
  self.exclude = self.module.params['exclude']
87
83
  self.installroot = self.module.params['installroot']
88
- self.install_repoquery = self.module.params['install_repoquery']
89
84
  self.install_weak_deps = self.module.params['install_weak_deps']
90
85
  self.list = self.module.params['list']
91
86
  self.names = [p.strip() for p in self.module.params['name']]
ansible/modules/apt.py CHANGED
@@ -372,6 +372,7 @@ import locale as locale_module
372
372
  import os
373
373
  import re
374
374
  import secrets
375
+ import shlex
375
376
  import shutil
376
377
  import sys
377
378
  import tempfile
@@ -382,7 +383,6 @@ from ansible.module_utils.common.file import S_IRWXU_RXG_RXO
382
383
  from ansible.module_utils.common.locale import get_best_parsable_locale
383
384
  from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
384
385
  from ansible.module_utils.common.text.converters import to_native, to_text
385
- from ansible.module_utils.six import string_types
386
386
  from ansible.module_utils.urls import fetch_file
387
387
 
388
388
  DPKG_OPTIONS = 'force-confdef,force-confold'
@@ -390,8 +390,6 @@ APT_GET_ZERO = "\n0 upgraded, 0 newly installed, 0 to remove"
390
390
  APTITUDE_ZERO = "\n0 packages upgraded, 0 newly installed, 0 to remove"
391
391
  APT_LISTS_PATH = "/var/lib/apt/lists"
392
392
  APT_UPDATE_SUCCESS_STAMP_PATH = "/var/lib/apt/periodic/update-success-stamp"
393
- APT_MARK_INVALID_OP = 'Invalid operation'
394
- APT_MARK_INVALID_OP_DEB6 = 'Usage: apt-mark [options] {markauto|unmarkauto} packages'
395
393
 
396
394
  CLEAN_OP_CHANGED_STR = dict(
397
395
  autoremove='The following packages will be REMOVED',
@@ -634,7 +632,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache):
634
632
  if pkgspec:
635
633
  for pkgspec_pattern in pkgspec:
636
634
 
637
- if not isinstance(pkgspec_pattern, string_types):
635
+ if not isinstance(pkgspec_pattern, str):
638
636
  m.fail_json(msg="Invalid type for package name, expected string but got %s" % type(pkgspec_pattern))
639
637
 
640
638
  pkgname_pattern, version_cmp, version = package_split(pkgspec_pattern)
@@ -690,26 +688,30 @@ def parse_diff(output):
690
688
  return {'prepared': '\n'.join(diff[diff_start:diff_end])}
691
689
 
692
690
 
693
- def mark_installed_manually(m, packages):
691
+ def mark_installed(m: AnsibleModule, packages: list[str], manual: bool) -> None:
692
+ """Mark packages as manually or automatically installed."""
694
693
  if not packages:
695
694
  return
696
695
 
696
+ if manual:
697
+ mark_msg = "manually"
698
+ mark_op = "manual"
699
+ else:
700
+ mark_msg = "auto"
701
+ mark_op = "auto"
702
+
697
703
  apt_mark_cmd_path = m.get_bin_path("apt-mark")
698
704
 
699
705
  # https://github.com/ansible/ansible/issues/40531
700
706
  if apt_mark_cmd_path is None:
701
- m.warn("Could not find apt-mark binary, not marking package(s) as manually installed.")
707
+ m.warn(f"Could not find apt-mark binary, not marking package(s) as {mark_msg} installed.")
702
708
  return
703
709
 
704
- cmd = "%s manual %s" % (apt_mark_cmd_path, ' '.join(packages))
710
+ cmd = [apt_mark_cmd_path, mark_op] + packages
705
711
  rc, out, err = m.run_command(cmd)
706
712
 
707
- if APT_MARK_INVALID_OP in err or APT_MARK_INVALID_OP_DEB6 in err:
708
- cmd = "%s unmarkauto %s" % (apt_mark_cmd_path, ' '.join(packages))
709
- rc, out, err = m.run_command(cmd)
710
-
711
713
  if rc != 0:
712
- m.fail_json(msg="'%s' failed: %s" % (cmd, err), stdout=out, stderr=err, rc=rc)
714
+ m.fail_json(msg=f"Command {shlex.join(cmd)!r} failed.", stdout=out, stderr=err, rc=rc)
713
715
 
714
716
 
715
717
  def install(m, pkgspec, cache, upgrade=False, default_release=None,
@@ -835,7 +837,7 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None,
835
837
  data = dict(changed=False)
836
838
 
837
839
  if not build_dep and not m.check_mode:
838
- mark_installed_manually(m, package_names)
840
+ mark_installed(m, package_names, manual=True)
839
841
 
840
842
  return (status, data)
841
843
 
@@ -914,6 +916,9 @@ def install_deb(
914
916
  dpkg_options=install_dpkg_options)
915
917
  if not success:
916
918
  m.fail_json(**retvals)
919
+ # Mark the dependencies as auto installed
920
+ # https://github.com/ansible/ansible/issues/78123
921
+ mark_installed(m, deps_to_install, manual=False)
917
922
  changed = retvals.get('changed', False)
918
923
 
919
924
  if pkgs_to_install:
@@ -508,7 +508,7 @@ class UbuntuSourcesList(SourcesList):
508
508
  try:
509
509
  rc, out, err = self.module.run_command([self.gpg_bin, '--list-packets', key_file])
510
510
  except OSError as ex:
511
- self.debug(f"Could check key against file {key_file!r}: {ex}")
511
+ self.module.debug(f"Could check key against file {key_file!r}: {ex}")
512
512
  continue
513
513
 
514
514
  if key_fingerprint in out:
@@ -131,7 +131,6 @@ import re
131
131
  import tempfile
132
132
 
133
133
  from ansible.module_utils.basic import AnsibleModule
134
- from ansible.module_utils.six import b, indexbytes
135
134
  from ansible.module_utils.common.text.converters import to_native
136
135
 
137
136
 
@@ -141,6 +140,7 @@ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, igno
141
140
  tmp = os.fdopen(tmpfd, 'wb')
142
141
  delimit_me = False
143
142
  add_newline = False
143
+ b_linesep = os.linesep.encode()
144
144
 
145
145
  for f in sorted(os.listdir(src_path)):
146
146
  if compiled_regexp and not compiled_regexp.search(f):
@@ -153,7 +153,7 @@ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, igno
153
153
 
154
154
  # always put a newline between fragments if the previous fragment didn't end with a newline.
155
155
  if add_newline:
156
- tmp.write(b('\n'))
156
+ tmp.write(b_linesep)
157
157
 
158
158
  # delimiters should only appear between fragments
159
159
  if delimit_me:
@@ -163,16 +163,12 @@ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, igno
163
163
  tmp.write(delimiter)
164
164
  # always make sure there's a newline after the
165
165
  # delimiter, so lines don't run together
166
-
167
- # byte indexing differs on Python 2 and 3,
168
- # use indexbytes for compat
169
- # chr(10) == '\n'
170
- if indexbytes(delimiter, -1) != 10:
171
- tmp.write(b('\n'))
166
+ if not delimiter.endswith(b_linesep):
167
+ tmp.write(b_linesep)
172
168
 
173
169
  tmp.write(fragment_content)
174
170
  delimit_me = True
175
- if fragment_content.endswith(b('\n')):
171
+ if fragment_content.endswith(b_linesep):
176
172
  add_newline = False
177
173
  else:
178
174
  add_newline = True
@@ -102,6 +102,13 @@ options:
102
102
  type: bool
103
103
  default: no
104
104
  version_added: '2.16'
105
+ encoding:
106
+ description:
107
+ - The character set in which the target file is encoded.
108
+ - For a list of available built-in encodings, see U(https://docs.python.org/3/library/codecs.html#standard-encodings)
109
+ type: str
110
+ default: utf-8
111
+ version_added: '2.20'
105
112
  notes:
106
113
  - When using C(with_*) loops be aware that if you do not set a unique mark the block will be overwritten on each iteration.
107
114
  - As of Ansible 2.3, the O(dest) option has been changed to O(path) as default, but O(dest) still works as well.
@@ -192,15 +199,16 @@ EXAMPLES = r"""
192
199
  import re
193
200
  import os
194
201
  import tempfile
195
- from ansible.module_utils.six import b
202
+
196
203
  from ansible.module_utils.basic import AnsibleModule
197
- from ansible.module_utils.common.text.converters import to_bytes, to_native
204
+ from ansible.module_utils.common.text.converters import to_native
198
205
 
199
206
 
200
- def write_changes(module, contents, path):
207
+ def write_changes(module, contents, path, encoding=None):
201
208
 
202
209
  tmpfd, tmpfile = tempfile.mkstemp(dir=module.tmpdir)
203
- with os.fdopen(tmpfd, 'wb') as tf:
210
+ # newline param set to translate newline sequences with system default line separator
211
+ with os.fdopen(tmpfd, 'w', encoding=encoding, newline=None) as tf:
204
212
  tf.write(contents)
205
213
 
206
214
  validate = module.params.get('validate', None)
@@ -246,6 +254,7 @@ def main():
246
254
  marker_end=dict(type='str', default='END'),
247
255
  append_newline=dict(type='bool', default=False),
248
256
  prepend_newline=dict(type='bool', default=False),
257
+ encoding=dict(type='str', default='utf-8'),
249
258
  ),
250
259
  mutually_exclusive=[['insertbefore', 'insertafter']],
251
260
  add_file_common_args=True,
@@ -254,6 +263,8 @@ def main():
254
263
  params = module.params
255
264
  path = params['path']
256
265
 
266
+ encoding = module.params.get('encoding', None)
267
+
257
268
  if os.path.isdir(path):
258
269
  module.fail_json(rc=256,
259
270
  msg='Path %s is a directory !' % path)
@@ -274,7 +285,8 @@ def main():
274
285
  original = None
275
286
  lines = []
276
287
  else:
277
- with open(path, 'rb') as f:
288
+ # newline param set to preserve newline sequences read from file
289
+ with open(path, 'r', encoding=encoding, newline='') as f:
278
290
  original = f.read()
279
291
  lines = original.splitlines(True)
280
292
 
@@ -288,10 +300,12 @@ def main():
288
300
 
289
301
  insertbefore = params['insertbefore']
290
302
  insertafter = params['insertafter']
291
- block = to_bytes(params['block'])
292
- marker = to_bytes(params['marker'])
303
+ block = params['block']
304
+ marker = params['marker']
293
305
  present = params['state'] == 'present'
294
- blank_line = [b(os.linesep)]
306
+
307
+ line_separator = os.linesep
308
+ blank_line = [line_separator]
295
309
 
296
310
  if not present and not path_exists:
297
311
  module.exit_json(changed=False, msg="File %s not present" % path)
@@ -300,17 +314,19 @@ def main():
300
314
  insertafter = 'EOF'
301
315
 
302
316
  if insertafter not in (None, 'EOF'):
303
- insertre = re.compile(to_bytes(insertafter, errors='surrogate_or_strict'))
317
+ insertre = re.compile(insertafter)
304
318
  elif insertbefore not in (None, 'BOF'):
305
- insertre = re.compile(to_bytes(insertbefore, errors='surrogate_or_strict'))
319
+ insertre = re.compile(insertbefore)
306
320
  else:
307
321
  insertre = None
308
322
 
309
- marker0 = re.sub(b(r'{mark}'), b(params['marker_begin']), marker) + b(os.linesep)
310
- marker1 = re.sub(b(r'{mark}'), b(params['marker_end']), marker) + b(os.linesep)
323
+ marker0 = re.sub(r'{mark}', params['marker_begin'], marker) + line_separator
324
+ marker1 = re.sub(r'{mark}', params['marker_end'], marker) + line_separator
325
+
311
326
  if present and block:
312
- if not block.endswith(b(os.linesep)):
313
- block += b(os.linesep)
327
+ if not block.endswith(line_separator):
328
+ block += line_separator
329
+
314
330
  blocklines = [marker0] + block.splitlines(True) + [marker1]
315
331
  else:
316
332
  blocklines = []
@@ -329,9 +345,9 @@ def main():
329
345
  match = insertre.search(original)
330
346
  if match:
331
347
  if insertafter:
332
- n0 = to_native(original).count('\n', 0, match.end())
348
+ n0 = original.count('\n', 0, match.end())
333
349
  elif insertbefore:
334
- n0 = to_native(original).count('\n', 0, match.start())
350
+ n0 = original.count('\n', 0, match.start())
335
351
  else:
336
352
  for i, line in enumerate(lines):
337
353
  if insertre.search(line):
@@ -352,15 +368,15 @@ def main():
352
368
 
353
369
  # Ensure there is a line separator before the block of lines to be inserted
354
370
  if n0 > 0:
355
- if not lines[n0 - 1].endswith(b(os.linesep)):
356
- lines[n0 - 1] += b(os.linesep)
371
+ if not lines[n0 - 1].endswith(line_separator):
372
+ lines[n0 - 1] += line_separator
357
373
 
358
374
  # Before the block: check if we need to prepend a blank line
359
375
  # If yes, we need to add the blank line if we are not at the beginning of the file
360
376
  # and the previous line is not a blank line
361
377
  # In both cases, we need to shift by one on the right the inserting position of the block
362
378
  if params['prepend_newline'] and present:
363
- if n0 != 0 and lines[n0 - 1] != b(os.linesep):
379
+ if n0 != 0 and lines[n0 - 1] != line_separator:
364
380
  lines[n0:n0] = blank_line
365
381
  n0 += 1
366
382
 
@@ -372,13 +388,13 @@ def main():
372
388
  # and the line right after is not a blank line
373
389
  if params['append_newline'] and present:
374
390
  line_after_block = n0 + len(blocklines)
375
- if line_after_block < len(lines) and lines[line_after_block] != b(os.linesep):
391
+ if line_after_block < len(lines) and lines[line_after_block] != line_separator:
376
392
  lines[line_after_block:line_after_block] = blank_line
377
393
 
378
394
  if lines:
379
- result = b''.join(lines)
395
+ result = ''.join(lines)
380
396
  else:
381
- result = b''
397
+ result = ''
382
398
 
383
399
  if module._diff:
384
400
  diff['after'] = result
@@ -402,7 +418,7 @@ def main():
402
418
  backup_file = module.backup_local(path)
403
419
  # We should always follow symlinks so that we change the real file
404
420
  real_path = os.path.realpath(params['path'])
405
- write_changes(module, result, real_path)
421
+ write_changes(module, result, real_path, encoding)
406
422
 
407
423
  if module.check_mode and not path_exists:
408
424
  module.exit_json(changed=changed, msg=msg, diff=diff)
ansible/modules/cron.py CHANGED
@@ -219,20 +219,20 @@ import os
219
219
  import platform
220
220
  import pwd
221
221
  import re
222
+ import shlex
222
223
  import sys
223
224
  import tempfile
224
225
 
225
226
  from ansible.module_utils.basic import AnsibleModule
226
227
  from ansible.module_utils.common.file import S_IRWU_RWG_RWO
227
228
  from ansible.module_utils.common.text.converters import to_bytes, to_native
228
- from ansible.module_utils.six.moves import shlex_quote
229
229
 
230
230
 
231
231
  class CronTabError(Exception):
232
232
  pass
233
233
 
234
234
 
235
- class CronTab(object):
235
+ class CronTab:
236
236
  """
237
237
  CronTab object to write time based crontab file
238
238
 
@@ -243,8 +243,8 @@ class CronTab(object):
243
243
  def __init__(self, module, user=None, cron_file=None):
244
244
  self.module = module
245
245
  self.user = user
246
- self.root = (os.getuid() == 0)
247
- self.lines = None
246
+ self.root = os.getuid() == 0
247
+ self.lines = []
248
248
  self.ansible = "#Ansible: "
249
249
  self.n_existing = ''
250
250
  self.cron_cmd = self.module.get_bin_path('crontab', required=True)
@@ -264,7 +264,6 @@ class CronTab(object):
264
264
 
265
265
  def read(self):
266
266
  # Read in the crontab from the system
267
- self.lines = []
268
267
  if self.cron_file:
269
268
  # read the cronfile
270
269
  try:
@@ -280,7 +279,7 @@ class CronTab(object):
280
279
  # FIXME: using safely quoted shell for now, but this really should be two non-shell calls instead.
281
280
  (rc, out, err) = self.module.run_command(self._read_user_execute(), use_unsafe_shell=True)
282
281
 
283
- if rc != 0 and rc != 1: # 1 can mean that there are no jobs.
282
+ if rc not in (0, 1): # 1 can mean that there are no jobs.
284
283
  raise CronTabError("Unable to read crontab")
285
284
 
286
285
  self.n_existing = out
@@ -300,11 +299,10 @@ class CronTab(object):
300
299
  def is_empty(self):
301
300
  if len(self.lines) == 0:
302
301
  return True
303
- else:
304
- for line in self.lines:
305
- if line.strip():
306
- return False
307
- return True
302
+ for line in self.lines:
303
+ if line.strip():
304
+ return False
305
+ return True
308
306
 
309
307
  def write(self, backup_file=None):
310
308
  """
@@ -451,13 +449,10 @@ class CronTab(object):
451
449
  if special:
452
450
  if self.cron_file:
453
451
  return "%s@%s %s %s" % (disable_prefix, special, self.user, job)
454
- else:
455
- return "%s@%s %s" % (disable_prefix, special, job)
456
- else:
457
- if self.cron_file:
458
- return "%s%s %s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, self.user, job)
459
- else:
460
- return "%s%s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, job)
452
+ return "%s@%s %s" % (disable_prefix, special, job)
453
+ if self.cron_file:
454
+ return "%s%s %s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, self.user, job)
455
+ return "%s%s %s %s %s %s %s" % (disable_prefix, minute, hour, day, month, weekday, job)
461
456
 
462
457
  def get_jobnames(self):
463
458
  jobnames = []
@@ -495,8 +490,7 @@ class CronTab(object):
495
490
 
496
491
  if len(newlines) == 0:
497
492
  return True
498
- else:
499
- return False # TODO add some more error testing
493
+ return False # TODO add some more error testing
500
494
 
501
495
  def _update_env(self, name, decl, addenvfunction):
502
496
  newlines = []
@@ -529,13 +523,13 @@ class CronTab(object):
529
523
  user = ''
530
524
  if self.user:
531
525
  if platform.system() == 'SunOS':
532
- return "su %s -c '%s -l'" % (shlex_quote(self.user), shlex_quote(self.cron_cmd))
533
- elif platform.system() == 'AIX':
534
- return "%s -l %s" % (shlex_quote(self.cron_cmd), shlex_quote(self.user))
535
- elif platform.system() == 'HP-UX':
536
- return "%s %s %s" % (self.cron_cmd, '-l', shlex_quote(self.user))
537
- elif pwd.getpwuid(os.getuid())[0] != self.user:
538
- user = '-u %s' % shlex_quote(self.user)
526
+ return "su %s -c '%s -l'" % (shlex.quote(self.user), shlex.quote(self.cron_cmd))
527
+ if platform.system() == 'AIX':
528
+ return "%s -l %s" % (shlex.quote(self.cron_cmd), shlex.quote(self.user))
529
+ if platform.system() == 'HP-UX':
530
+ return "%s %s %s" % (self.cron_cmd, '-l', shlex.quote(self.user))
531
+ if pwd.getpwuid(os.getuid())[0] != self.user:
532
+ user = '-u %s' % shlex.quote(self.user)
539
533
  return "%s %s %s" % (self.cron_cmd, user, '-l')
540
534
 
541
535
  def _write_execute(self, path):
@@ -546,10 +540,10 @@ class CronTab(object):
546
540
  if self.user:
547
541
  if platform.system() in ['SunOS', 'HP-UX', 'AIX']:
548
542
  return "chown %s %s ; su '%s' -c '%s %s'" % (
549
- shlex_quote(self.user), shlex_quote(path), shlex_quote(self.user), self.cron_cmd, shlex_quote(path))
550
- elif pwd.getpwuid(os.getuid())[0] != self.user:
551
- user = '-u %s' % shlex_quote(self.user)
552
- return "%s %s %s" % (self.cron_cmd, user, shlex_quote(path))
543
+ shlex.quote(self.user), shlex.quote(path), shlex.quote(self.user), self.cron_cmd, shlex.quote(path))
544
+ if pwd.getpwuid(os.getuid())[0] != self.user:
545
+ user = '-u %s' % shlex.quote(self.user)
546
+ return "%s %s %s" % (self.cron_cmd, user, shlex.quote(path))
553
547
 
554
548
 
555
549
  def main():
@@ -668,7 +662,7 @@ def main():
668
662
 
669
663
  # if requested make a backup before making a change
670
664
  if backup and not module.check_mode:
671
- (backuph, backup_file) = tempfile.mkstemp(prefix='crontab')
665
+ (dummy, backup_file) = tempfile.mkstemp(prefix='crontab')
672
666
  crontab.write(backup_file)
673
667
 
674
668
  if env:
@@ -763,9 +757,6 @@ def main():
763
757
 
764
758
  module.exit_json(**res_args)
765
759
 
766
- # --- should never get here
767
- module.exit_json(msg="Unable to execute cron task.")
768
-
769
760
 
770
761
  if __name__ == '__main__':
771
762
  main()
@@ -67,6 +67,17 @@ options:
67
67
  - Determines the path to the C(InRelease) file, relative to the normal
68
68
  position of an C(InRelease) file.
69
69
  type: str
70
+ install_python_debian:
71
+ description:
72
+ - Whether to automatically try to install the Python C(debian) library or not, if it is not already installed.
73
+ Without this library, the module does not work.
74
+ - Runs C(apt install python3-debian).
75
+ - Only works with the system Python. If you are using a Python on the remote that is not
76
+ the system Python, set O(install_python_debian=false) and ensure that the Python C(debian) library
77
+ for your Python version is installed some other way.
78
+ type: bool
79
+ default: false
80
+ version_added: '2.20'
70
81
  languages:
71
82
  description:
72
83
  - Defines which languages information such as translated
@@ -228,6 +239,7 @@ key_filename:
228
239
 
229
240
  import os
230
241
  import re
242
+ import sys
231
243
  import tempfile
232
244
  import textwrap
233
245
 
@@ -235,9 +247,9 @@ from ansible.module_utils.basic import AnsibleModule
235
247
  from ansible.module_utils.basic import missing_required_lib
236
248
  from ansible.module_utils.common.collections import is_sequence
237
249
  from ansible.module_utils.common.file import S_IRWXU_RXG_RXO, S_IRWU_RG_RO
250
+ from ansible.module_utils.common.respawn import has_respawned, probe_interpreters_for_module, respawn_module
238
251
  from ansible.module_utils.common.text.converters import to_bytes
239
252
  from ansible.module_utils.common.text.converters import to_native
240
- from ansible.module_utils.six import raise_from # type: ignore[attr-defined]
241
253
  from ansible.module_utils.urls import generic_urlparse
242
254
  from ansible.module_utils.urls import open_url
243
255
  from ansible.module_utils.urls import get_user_agent
@@ -326,7 +338,7 @@ def write_signed_by_key(module, v, slug):
326
338
  try:
327
339
  r = open_url(v, http_agent=get_user_agent())
328
340
  except Exception as exc:
329
- raise_from(RuntimeError(to_native(exc)), exc)
341
+ raise RuntimeError('Could not fetch signed_by key.') from exc
330
342
  else:
331
343
  b_data = r.read()
332
344
  else:
@@ -357,6 +369,21 @@ def write_signed_by_key(module, v, slug):
357
369
  return changed, filename, None
358
370
 
359
371
 
372
+ def install_python_debian(module, deb_pkg_name):
373
+
374
+ if not module.check_mode:
375
+ apt_path = module.get_bin_path('apt', required=True)
376
+ if apt_path:
377
+ rc, so, se = module.run_command([apt_path, 'update'])
378
+ if rc != 0:
379
+ module.fail_json(msg=f"Failed update while auto installing {deb_pkg_name} due to '{se.strip()}'")
380
+ rc, so, se = module.run_command([apt_path, 'install', deb_pkg_name, '-y', '-q'])
381
+ if rc != 0:
382
+ module.fail_json(msg=f"Failed to auto-install {deb_pkg_name} due to : '{se.strip()}'")
383
+ else:
384
+ module.fail_json(msg=f"{deb_pkg_name} must be installed to use check mode")
385
+
386
+
360
387
  def main():
361
388
  module = AnsibleModule(
362
389
  argument_spec={
@@ -395,6 +422,10 @@ def main():
395
422
  'inrelease_path': {
396
423
  'type': 'str',
397
424
  },
425
+ 'install_python_debian': {
426
+ 'type': 'bool',
427
+ 'default': False,
428
+ },
398
429
  'languages': {
399
430
  'elements': 'str',
400
431
  'type': 'list',
@@ -453,8 +484,53 @@ def main():
453
484
  )
454
485
 
455
486
  if not HAS_DEBIAN:
456
- module.fail_json(msg=missing_required_lib("python3-debian"),
457
- exception=DEBIAN_IMP_ERR)
487
+ deb_pkg_name = 'python3-debian'
488
+ # This interpreter can't see the debian Python library- we'll do the following to try and fix that as per
489
+ # the apt_repository module:
490
+ # 1) look in common locations for system-owned interpreters that can see it; if we find one, respawn under it
491
+ # 2) finding none, try to install a matching python-debian package for the current interpreter version;
492
+ # we limit to the current interpreter version to try and avoid installing a whole other Python just
493
+ # for deb support
494
+ # 3) if we installed a support package, try to respawn under what we think is the right interpreter (could be
495
+ # the current interpreter again, but we'll let it respawn anyway for simplicity)
496
+ # 4) if still not working, return an error and give up (some corner cases not covered, but this shouldn't be
497
+ # made any more complex than it already is to try and cover more, eg, custom interpreters taking over
498
+ # system locations)
499
+
500
+ if has_respawned():
501
+ # this shouldn't be possible; short-circuit early if it happens...
502
+ module.fail_json(msg=f"{deb_pkg_name} must be installed and visible from {sys.executable}.")
503
+
504
+ interpreters = ['/usr/bin/python3', '/usr/bin/python']
505
+
506
+ interpreter = probe_interpreters_for_module(interpreters, 'debian')
507
+
508
+ if interpreter:
509
+ # found the Python bindings; respawn this module under the interpreter where we found them
510
+ respawn_module(interpreter)
511
+ # this is the end of the line for this process, it will exit here once the respawned module has completed
512
+
513
+ # don't make changes if we're in check_mode
514
+ if module.check_mode:
515
+ module.fail_json(msg=f"{deb_pkg_name} must be installed to use check mode. If run with install_python_debian, this module can auto-install it.")
516
+
517
+ if module.params['install_python_debian']:
518
+ install_python_debian(module, deb_pkg_name)
519
+ else:
520
+ module.fail_json(msg=f'{deb_pkg_name} is not installed, and install_python_debian is False')
521
+
522
+ # try again to find the bindings in common places
523
+ interpreter = probe_interpreters_for_module(interpreters, 'debian')
524
+
525
+ if interpreter:
526
+ # found the Python bindings; respawn this module under the interpreter where we found them
527
+ # NB: respawn is somewhat wasteful if it's this interpreter, but simplifies the code
528
+ respawn_module(interpreter)
529
+ # this is the end of the line for this process, it will exit here once the respawned module has completed
530
+ else:
531
+ # we've done all we can do; just tell the user it's busted and get out
532
+ module.fail_json(msg=missing_required_lib(deb_pkg_name),
533
+ exception=DEBIAN_IMP_ERR)
458
534
 
459
535
  check_mode = module.check_mode
460
536
 
@@ -510,14 +586,9 @@ def main():
510
586
  elif is_sequence(value):
511
587
  value = format_list(value)
512
588
  elif key == 'signed_by':
513
- try:
514
- key_changed, signed_by_filename, signed_by_data = write_signed_by_key(module, value, slug)
515
- value = signed_by_filename or signed_by_data
516
- changed |= key_changed
517
- except RuntimeError as exc:
518
- module.fail_json(
519
- msg='Could not fetch signed_by key: %s' % to_native(exc)
520
- )
589
+ key_changed, signed_by_filename, signed_by_data = write_signed_by_key(module, value, slug)
590
+ value = signed_by_filename or signed_by_data
591
+ changed |= key_changed
521
592
 
522
593
  if value.count('\n') > 0:
523
594
  value = format_multiline(value)