Compare commits

..

23 Commits
4.6.1 ... 4.7.0

Author SHA1 Message Date
Felix Fontein
22b72e6684 Release 4.7.0. 2022-04-05 14:02:29 +02:00
patchback[bot]
8e7bee4217 Fix small typo (#4452) (#4454)
(cherry picked from commit 380de2d0c1)

Co-authored-by: Wouter Schoot <wouter@schoot.org>
2022-04-05 14:00:35 +02:00
patchback[bot]
cef6b81e5b Bug fix: Warns user if incorrect SDK version is installed (#4422) (#4450)
* Add error handling to check correct SDK version installed

* Fix CI errors

* Added changelog fragment

* Changed exeption type

* Update changelogs fragment

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit e7ffa76db6)

Co-authored-by: Ricky White <rickywhite@outlook.com>
2022-04-05 07:49:30 +02:00
patchback[bot]
182c365d87 nmcli: suggest new routes4 and routes6 format (#4328) (#4447)
* suggest new routes4 and routes6 format

* make new options instead of modifying exiting one

* fix docs and some small errors

* fixing docs

(cherry picked from commit feb0fffd58)

Co-authored-by: Alex Groshev <38885591+haddystuff@users.noreply.github.com>
2022-04-05 07:12:38 +02:00
patchback[bot]
587cdc82e7 Keycloak client, Add always_display_in_console option (#4429) (#4448)
* Keycloak client, Add always_display_in_console option

* Add 4429-keycloak-client-add-always-display-in-console.yml fragment.

* Update changelogs/fragments/4429-keycloak-client-add-always-display-in-console.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Michal Vasko <mvasko@cloudwerkstatt.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 79256b2bd2)

Co-authored-by: whoamiUNIX <40315055+whoamiUNIX@users.noreply.github.com>
2022-04-05 07:09:20 +02:00
Felix Fontein
cb1a50a273 Prepare 4.7.0 release. 2022-04-05 07:05:07 +02:00
patchback[bot]
f0df50e665 Bugfix: zypper issue with specified package versions (#4421) (#4446)
* fixed issue with specified package versions

zypper.py was doing nothing on state=present, when ALL requestet/checked packages had a specific version stated. This was caused by get_installed_state() being called with an empty package list, which in this case returns information about all ALL installed packages. This lead to an exessive filter list prerun_state, essentially removing all packages that are installed in ANY version on the target system from the request list.

* Create 4421-zypper_package_version_handling_fix

added changelog fragment for https://github.com/ansible-collections/community.general/pull/4421

* Delete 4421-zypper_package_version_handling_fix

* Create 4421-zypper_package_version_handling_fix.yml

(cherry picked from commit bbe231e261)

Co-authored-by: tover99 <101673769+tover99@users.noreply.github.com>
2022-04-05 06:28:15 +02:00
patchback[bot]
47aa93d970 cronvar: ensure creation of /etc/cron.d in test (#4440) (#4444)
* ensure creation of /etc/cron.d in test

* fixed typo

(cherry picked from commit 9e0ff8ba4b)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-04-03 10:54:44 +02:00
patchback[bot]
e89648a114 Remove OpenSuSE Python 2 from devel CI. (#4442) (#4443)
(cherry picked from commit bd83490b45)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-04-02 18:31:40 +02:00
patchback[bot]
6f1bdb3e49 pids: re-enabled tests on Alpine Linux (#4405) (#4439)
* [WIP] pids: re-enabled tests on Alpine Linux

* trying to compile a simple-faked sleep command

* make FreeBSD happy

* remove the block testing for Alpine Linux

* simpler version of sleeper.c

* simpler version of sleeper.c, part II

* Update tests/integration/targets/pids/tasks/main.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update tests/integration/targets/pids/tasks/main.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* added license to sleeper.c file

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 21ee4c84b7)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-04-02 08:45:53 +02:00
patchback[bot]
fbf11668f4 CI: Remove 'warn:' that's removed in ansible-core 2.14 (#4434) (#4437)
* Remove 'warn:' that's removed in ansible-core 2.14.

* Install virtualenv when needed.

(cherry picked from commit 24ca69aa05)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-04-01 23:11:39 +02:00
patchback[bot]
3376442aa2 Proxmox Inventory: Add support for templating in inventory file (#4418) (#4435)
* added templating to the url, user, and password

* added changelog fragment

* typo in description for url, and password

* clarify in the changelog what can you change

* update documentation and added an example

* missing quote from examples

* Apply suggestions from code review

Changed to I for option names

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/inventory/proxmox.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 13d18c9aa8)

Co-authored-by: Ilija Matoski <ilijamt@gmail.com>
2022-04-01 23:07:42 +02:00
patchback[bot]
868edfa664 ipa_service: Add skip_host_check option (#4417) (#4436)
* ipa_service: Add `skip_host_check` option

* Update plugins/modules/identity/ipa/ipa_service.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/ipa/ipa_service.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/ipa/ipa_service.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* changelogs/fragments: Add 4417-ipa_service-add-skip_host_check.yml

Co-authored-by: sodd <4178855+sodd@users.noreply.github.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 1b357bade7)

Co-authored-by: sodd <sodd@users.noreply.github.com>
2022-04-01 23:07:23 +02:00
patchback[bot]
2fcb77f7fb Replace antsibull-lint collection-docs with antsibull-docs lint-collection-docs. (#4423) (#4426)
(cherry picked from commit 668bbed602)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-03-30 08:17:38 +02:00
patchback[bot]
17135dd082 Add stable-2.13 to CI, thin out older version matrix (#4413) (#4414)
* Add stable-2.13 to CI, thin out older version matrix.

* Thin out matrix more.

* And a bit more.

(cherry picked from commit caedcc3075)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-03-29 07:52:32 +02:00
patchback[bot]
7516018cfb keycloak: add missing validate_certs parameters for open_url calls (#4382) (#4410)
* fix: missing `validate_certs` parameters for `open_url` calls

As stated in the documentation, the `validate_certs` parameter can be
used to verify (or not) the TLS certificates. But, for some modules (at
least for the `keycloak_authentication` module), this parameter is not
used with the `open_url` function.

* add changelog fragment

* Update changelogs/fragments/4382-keycloak-add-missing-validate_certs-parameters.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Laurent Meunier <lme@atolcd.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 34420e143e)

Co-authored-by: Laurent Meunier <laurent@deltalima.net>
2022-03-28 22:25:14 +02:00
patchback[bot]
58df1df107 keycloak_client: add default_client_scopes and optional_client_scopes (#4385) (#4409)
* keycloak_client: add default_client_scopes and optional_client_scopes

* Changelog fragment for #4385

* Update changelogs/fragments/4385-keycloak-client-default-optional-scopes.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

* Update plugins/modules/identity/keycloak/keycloak_client.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 30c65cd84c)

Co-authored-by: Alex Lubbock <alex@lubbock.uk>
2022-03-28 22:25:00 +02:00
patchback[bot]
e9b3705809 feat: sudoers module supports runas parameter with default of root (#4380) (#4399)
* feat: sudoers module supports runas parameter with default of root

* fix: sudoers tests now pass

* chore: add changelog fragment for 4380

* fix: runas feature now a non-breaking change wh no def with no default

* fix: no trailing space in sudoers.py

* Update plugins/modules/system/sudoers.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 17fe813c18)

Co-authored-by: doubletwist13 <doubletwist@fearthepenguin.net>
2022-03-24 06:44:48 +00:00
patchback[bot]
743e9c851f ldap: added documentation as requested (#4389) (#4398)
* added documentation as requested

* Update plugins/doc_fragments/ldap.py

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 8515c03dc7)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2022-03-24 06:44:30 +00:00
patchback[bot]
a7883ee489 Fixed typo in keycloak_client_rolemapping examples (#4393) (#4401)
* Fixed typo in keycloak_client_rolemapping examples

* Add a changelog fragment.

* Removed changelogs fragment for docs-only change.

Co-authored-by: shnee <shnee@shnee.net>
(cherry picked from commit cb30eb2d30)

Co-authored-by: shnee <CurtyD13@gmail.com>
2022-03-24 06:44:18 +00:00
patchback[bot]
518af70b77 Proxmox inventory plugin - Fix tags parsing (#4378) (#4402)
* Proxmox inventory plugin - Fix tags parsing

  * In some cases the Proxmox API returns a tags string that consists in
    a single space. The Proxmox inventory plugin parsed that into a
    single, empty tag. Stripping the initial string then checking
    whether it actually contains something fixes that.
  * Do not call `_to_safe` on the concatenation of a known safe string
    and a string that was already made safe.

* Changelog fragment for Proxmox inventory plugin tags fix

* Proxmox inventory plugin - Include link to PR in fragment

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 622895fb55)

Co-authored-by: Emmanuel Benoît <tseeker@nocternity.net>
2022-03-24 06:44:02 +00:00
patchback[bot]
ce7d98aa6f Add collection links file. (#4384) (#4386)
(cherry picked from commit eb4495b716)

Co-authored-by: Felix Fontein <felix@fontein.de>
2022-03-22 07:21:28 +01:00
Felix Fontein
9f91f4b5cd Next expected release is 4.7.0. 2022-03-16 19:12:20 +01:00
31 changed files with 790 additions and 265 deletions

View File

@@ -69,6 +69,19 @@ stages:
- test: 3
- test: 4
- test: extra
- stage: Sanity_2_13
displayName: Sanity 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Test {0}
testFormat: 2.13/sanity/{0}
targets:
- test: 1
- test: 2
- test: 3
- test: 4
- stage: Sanity_2_12
displayName: Sanity 2.12
dependsOn: []
@@ -138,6 +151,19 @@ stages:
- test: 3.8
- test: 3.9
- test: '3.10'
- stage: Units_2_13
displayName: Units 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.13/units/{0}/1
targets:
- test: 2.7
- test: 3.6
- test: 3.8
- test: 3.9
- stage: Units_2_12
displayName: Units 2.12
dependsOn: []
@@ -148,12 +174,8 @@ stages:
testFormat: 2.12/units/{0}/1
targets:
- test: 2.6
- test: 2.7
- test: 3.5
- test: 3.6
- test: 3.7
- test: 3.8
- test: '3.10'
- stage: Units_2_11
displayName: Units 2.11
dependsOn: []
@@ -166,9 +188,6 @@ stages:
- test: 2.6
- test: 2.7
- test: 3.5
- test: 3.6
- test: 3.7
- test: 3.8
- test: 3.9
- stage: Units_2_10
displayName: Units 2.10
@@ -191,11 +210,7 @@ stages:
testFormat: 2.9/units/{0}/1
targets:
- test: 2.6
- test: 2.7
- test: 3.5
- test: 3.6
- test: 3.7
- test: 3.8
## Remote
- stage: Remote_devel
@@ -220,6 +235,22 @@ stages:
- 1
- 2
- 3
- stage: Remote_2_13
displayName: Remote 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/{0}
targets:
- name: macOS 12.0
test: macos/12.0
- name: RHEL 8.5
test: rhel/8.5
groups:
- 1
- 2
- 3
- stage: Remote_2_12
displayName: Remote 2.12
dependsOn: []
@@ -302,9 +333,7 @@ stages:
test: fedora34
- name: Fedora 35
test: fedora35
- name: openSUSE 15 py2
test: opensuse15py2
- name: openSUSE 15 py3
- name: openSUSE 15
test: opensuse15
- name: Ubuntu 18.04
test: ubuntu1804
@@ -316,6 +345,24 @@ stages:
- 1
- 2
- 3
- stage: Docker_2_13
displayName: Docker 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.13/linux/{0}
targets:
- name: Fedora 35
test: fedora35
- name: openSUSE 15 py2
test: opensuse15py2
- name: Alpine 3
test: alpine3
groups:
- 1
- 2
- 3
- stage: Docker_2_12
displayName: Docker 2.12
dependsOn: []
@@ -328,8 +375,6 @@ stages:
test: centos6
- name: Fedora 34
test: fedora34
- name: openSUSE 15 py3
test: opensuse15
- name: Ubuntu 20.04
test: ubuntu2004
groups:
@@ -344,12 +389,8 @@ stages:
parameters:
testFormat: 2.11/linux/{0}
targets:
- name: CentOS 7
test: centos7
- name: Fedora 33
test: fedora33
- name: openSUSE 15 py2
test: opensuse15py2
- name: Alpine 3
test: alpine3
groups:
@@ -380,8 +421,6 @@ stages:
targets:
- name: Fedora 31
test: fedora31
- name: openSUSE 15 py3
test: opensuse15
groups:
- 2
- 3
@@ -417,6 +456,16 @@ stages:
testFormat: devel/cloud/{0}/1
targets:
- test: 2.7
- test: '3.10'
- stage: Cloud_2_13
displayName: Cloud 2.13
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.13/cloud/{0}/1
targets:
- test: 3.9
- stage: Cloud_2_12
displayName: Cloud 2.12
@@ -466,26 +515,31 @@ stages:
- Sanity_2_10
- Sanity_2_11
- Sanity_2_12
- Sanity_2_13
- Units_devel
- Units_2_9
- Units_2_10
- Units_2_11
- Units_2_12
- Units_2_13
- Remote_devel
- Remote_2_9
- Remote_2_10
- Remote_2_11
- Remote_2_12
- Remote_2_13
- Docker_devel
- Docker_2_9
- Docker_2_10
- Docker_2_11
- Docker_2_12
- Docker_2_13
- Docker_community_devel
- Cloud_devel
- Cloud_2_9
- Cloud_2_10
- Cloud_2_11
- Cloud_2_12
- Cloud_2_13
jobs:
- template: templates/coverage.yml

View File

@@ -6,6 +6,32 @@ Community General Release Notes
This changelog describes changes after version 3.0.0.
v4.7.0
======
Release Summary
---------------
Regular bugfix and feature release.
Minor Changes
-------------
- ipa_service - add ``skip_host_check`` parameter. (https://github.com/ansible-collections/community.general/pull/4417).
- keycloak_client - add ``always_display_in_console`` parameter (https://github.com/ansible-collections/community.general/issues/4390).
- keycloak_client - add ``default_client_scopes`` and ``optional_client_scopes`` parameters. (https://github.com/ansible-collections/community.general/pull/4385).
- proxmox inventory plugin - add support for templating the ``url``, ``user``, and ``password`` options (https://github.com/ansible-collections/community.general/pull/4418).
- sudoers - add support for ``runas`` parameter (https://github.com/ansible-collections/community.general/issues/4379).
Bugfixes
--------
- dsv lookup plugin - raise an Ansible error if the wrong ``python-dsv-sdk`` version is installed (https://github.com/ansible-collections/community.general/pull/4422).
- keycloak_* - the documented ``validate_certs`` parameter was not taken into account when calling the ``open_url`` function in some cases, thus enforcing certificate validation even when ``validate_certs`` was set to ``false``. (https://github.com/ansible-collections/community.general/pull/4382)
- nmcli - fix returning "changed" when routes parameters set, also suggest new routes4 and routes6 format (https://github.com/ansible-collections/community.general/issues/4131).
- proxmox inventory plugin - fixed the ``tags_parsed`` field when Proxmox returns a single space for the ``tags`` entry (https://github.com/ansible-collections/community.general/pull/4378).
- zypper - fixed bug that caused zypper to always report [ok] and do nothing on ``state=present`` when all packages in ``name`` had a version specification (https://github.com/ansible-collections/community.general/issues/4371, https://github.com/ansible-collections/community.general/pull/4421).
v4.6.1
======

View File

@@ -17,7 +17,7 @@ If you encounter abusive behavior violating the [Ansible Code of Conduct](https:
## Tested with Ansible
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
Tested with the current Ansible 2.9, ansible-base 2.10, ansible-core 2.11, ansible-core 2.12, ansible-core 2.13 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
## External requirements

View File

@@ -1603,3 +1603,41 @@ releases:
- 4351-inventory-lxd-handling_metadata_wo_os_and_release.yml
- 4368-reverts-4281.yml
release_date: '2022-03-16'
4.7.0:
changes:
bugfixes:
- dsv lookup plugin - raise an Ansible error if the wrong ``python-dsv-sdk``
version is installed (https://github.com/ansible-collections/community.general/pull/4422).
- keycloak_* - the documented ``validate_certs`` parameter was not taken into
account when calling the ``open_url`` function in some cases, thus enforcing
certificate validation even when ``validate_certs`` was set to ``false``.
(https://github.com/ansible-collections/community.general/pull/4382)
- nmcli - fix returning "changed" when routes parameters set, also suggest new
routes4 and routes6 format (https://github.com/ansible-collections/community.general/issues/4131).
- proxmox inventory plugin - fixed the ``tags_parsed`` field when Proxmox returns
a single space for the ``tags`` entry (https://github.com/ansible-collections/community.general/pull/4378).
- zypper - fixed bug that caused zypper to always report [ok] and do nothing
on ``state=present`` when all packages in ``name`` had a version specification
(https://github.com/ansible-collections/community.general/issues/4371, https://github.com/ansible-collections/community.general/pull/4421).
minor_changes:
- ipa_service - add ``skip_host_check`` parameter. (https://github.com/ansible-collections/community.general/pull/4417).
- keycloak_client - add ``always_display_in_console`` parameter (https://github.com/ansible-collections/community.general/issues/4390).
- keycloak_client - add ``default_client_scopes`` and ``optional_client_scopes``
parameters. (https://github.com/ansible-collections/community.general/pull/4385).
- proxmox inventory plugin - add support for templating the ``url``, ``user``,
and ``password`` options (https://github.com/ansible-collections/community.general/pull/4418).
- sudoers - add support for ``runas`` parameter (https://github.com/ansible-collections/community.general/issues/4379).
release_summary: Regular bugfix and feature release.
fragments:
- 4.7.0.yml
- 4131-nmcli_fix_reports_changed_for_routes4_parameter.yml
- 4378-proxmox-inventory-tags.yml
- 4380-sudoers-runas-parameter.yml
- 4382-keycloak-add-missing-validate_certs-parameters.yml
- 4385-keycloak-client-default-optional-scopes.yml
- 4386-proxmox-support-templating-in-inventory-file.yml
- 4417-ipa_service-add-skip_host_check.yml
- 4421-zypper_package_version_handling_fix.yml
- 4422-warn-user-if-incorrect-SDK-version-is-installed.yaml
- 4429-keycloak-client-add-always-display-in-console.yml
release_date: '2022-04-05'

23
docs/docsite/links.yml Normal file
View File

@@ -0,0 +1,23 @@
---
edit_on_github:
repository: ansible-collections/community.general
branch: main
path_prefix: ''
extra_links:
- description: Submit a bug report
url: https://github.com/ansible-collections/community.general/issues/new?assignees=&labels=&template=bug_report.yml
- description: Request a feature
url: https://github.com/ansible-collections/community.general/issues/new?assignees=&labels=&template=feature_request.yml
communication:
matrix_rooms:
- topic: General usage and support questions
room: '#users:ansible.im'
irc_channels:
- topic: General usage and support questions
network: Libera
channel: '#ansible'
mailing_lists:
- topic: Ansible Project List
url: https://groups.google.com/g/ansible-project

View File

@@ -1,6 +1,6 @@
namespace: community
name: general
version: 4.6.1
version: 4.7.0
readme: README.md
authors:
- Ansible (https://github.com/ansible)

View File

@@ -38,8 +38,10 @@ options:
version_added: 2.0.0
server_uri:
description:
- A URI to the LDAP server.
- The I(server_uri) parameter may be a comma- or whitespace-separated list of URIs containing only the schema, the host, and the port fields.
- The default value lets the underlying LDAP client library look for a UNIX domain socket in its default location.
- Note that when using multiple URIs you cannot determine to which URI your client gets connected.
- For URIs containing additional fields, particularly when using commas, behavior is undefined.
type: str
default: ldapi:///
start_tls:

View File

@@ -31,6 +31,7 @@ DOCUMENTATION = '''
description:
- URL to Proxmox cluster.
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_URL) will be used instead.
- Since community.general 4.7.0 you can also use templating to specify the value of the I(url).
default: 'http://localhost:8006'
type: str
env:
@@ -40,6 +41,7 @@ DOCUMENTATION = '''
description:
- Proxmox authentication user.
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_USER) will be used instead.
- Since community.general 4.7.0 you can also use templating to specify the value of the I(user).
required: yes
type: str
env:
@@ -49,6 +51,7 @@ DOCUMENTATION = '''
description:
- Proxmox authentication password.
- If the value is not specified in the inventory configuration, the value of environment variable C(PROXMOX_PASSWORD) will be used instead.
- Since community.general 4.7.0 you can also use templating to specify the value of the I(password).
required: yes
type: str
env:
@@ -136,6 +139,14 @@ compose:
my_inv_var_1: "'my_var1_value'"
my_inv_var_2: >
"my_var_2_value"
# Specify the url, user and password using templating
# my.proxmox.yml
plugin: community.general.proxmox
url: "{{ lookup('ansible.builtin.ini', 'url', section='proxmox', file='file.ini') }}"
user: "{{ lookup('ansible.builtin.env','PM_USER') | default('ansible@pve') }}"
password: "{{ lookup('community.general.random_string', base64=True) }}"
'''
import itertools
@@ -148,6 +159,7 @@ from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cachea
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.utils.display import Display
from ansible.template import Templar
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
@@ -323,8 +335,10 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
# Additional field containing parsed tags as list
if config == 'tags':
parsed_key = self.to_safe('%s%s' % (key, "_parsed"))
properties[parsed_key] = [tag.strip() for tag in value.split(",")]
stripped_value = value.strip()
if stripped_value:
parsed_key = key + "_parsed"
properties[parsed_key] = [tag.strip() for tag in stripped_value.split(",")]
# The first field in the agent string tells you whether the agent is enabled
# the rest of the comma separated string is extra config for the agent
@@ -498,10 +512,24 @@ class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
# read config from file, this sets 'options'
self._read_config_data(path)
t = Templar(loader=loader)
# read options
self.proxmox_url = self.get_option('url').rstrip('/')
self.proxmox_user = self.get_option('user')
self.proxmox_password = self.get_option('password')
proxmox_url = self.get_option('url')
if t.is_template(proxmox_url):
proxmox_url = t.template(variable=proxmox_url, disable_lookups=False)
self.proxmox_url = proxmox_url.rstrip('/')
proxmox_user = self.get_option('user')
if t.is_template(proxmox_user):
proxmox_user = t.template(variable=proxmox_user, disable_lookups=False)
self.proxmox_user = proxmox_user
proxmox_password = self.get_option('password')
if t.is_template(proxmox_password):
proxmox_password = t.template(variable=proxmox_password, disable_lookups=False)
self.proxmox_password = proxmox_password
self.cache_key = self.get_cache_key(path)
self.use_cache = cache and self.get_option('cache')
self.host_filters = self.get_option('filters')

View File

@@ -105,11 +105,15 @@ display = Display()
class LookupModule(LookupBase):
@staticmethod
def Client(vault_parameters):
return SecretsVault(**vault_parameters)
try:
vault = SecretsVault(**vault_parameters)
return vault
except TypeError:
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
def run(self, terms, variables, **kwargs):
if sdk_is_missing:
raise AnsibleError("python-dsv-sdk must be installed to use this plugin")
raise AnsibleError("python-dsv-sdk==0.0.1 must be installed to use this plugin")
self.set_options(var_options=variables, direct=kwargs)

View File

@@ -1237,7 +1237,7 @@ class KeycloakAPI(object):
authentication_flow = {}
# Check if the authentication flow exists on the Keycloak serveraders
authentications = json.load(open_url(URL_AUTHENTICATION_FLOWS.format(url=self.baseurl, realm=realm), method='GET',
headers=self.restheaders, timeout=self.connection_timeout))
headers=self.restheaders, timeout=self.connection_timeout, validate_certs=self.validate_certs))
for authentication in authentications:
if authentication["alias"] == alias:
authentication_flow = authentication
@@ -1281,14 +1281,16 @@ class KeycloakAPI(object):
method='POST',
headers=self.restheaders,
data=json.dumps(new_name),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
flow_list = json.load(
open_url(
URL_AUTHENTICATION_FLOWS.format(url=self.baseurl,
realm=realm),
method='GET',
headers=self.restheaders,
timeout=self.connection_timeout))
timeout=self.connection_timeout,
validate_certs=self.validate_certs))
for flow in flow_list:
if flow["alias"] == config["alias"]:
return flow
@@ -1318,7 +1320,8 @@ class KeycloakAPI(object):
method='POST',
headers=self.restheaders,
data=json.dumps(new_flow),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
flow_list = json.load(
open_url(
URL_AUTHENTICATION_FLOWS.format(
@@ -1326,7 +1329,8 @@ class KeycloakAPI(object):
realm=realm),
method='GET',
headers=self.restheaders,
timeout=self.connection_timeout))
timeout=self.connection_timeout,
validate_certs=self.validate_certs))
for flow in flow_list:
if flow["alias"] == config["alias"]:
return flow
@@ -1351,7 +1355,8 @@ class KeycloakAPI(object):
method='PUT',
headers=self.restheaders,
data=json.dumps(updatedExec),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg="Unable to update executions %s: %s" % (updatedExec, str(e)))
@@ -1371,7 +1376,8 @@ class KeycloakAPI(object):
method='POST',
headers=self.restheaders,
data=json.dumps(authenticationConfig),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg="Unable to add authenticationConfig %s: %s" % (executionId, str(e)))
@@ -1395,7 +1401,8 @@ class KeycloakAPI(object):
method='POST',
headers=self.restheaders,
data=json.dumps(newSubFlow),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg="Unable to create new subflow %s: %s" % (subflowName, str(e)))
@@ -1418,7 +1425,8 @@ class KeycloakAPI(object):
method='POST',
headers=self.restheaders,
data=json.dumps(newExec),
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg="Unable to create new execution %s: %s" % (execution["provider"], str(e)))
@@ -1440,7 +1448,8 @@ class KeycloakAPI(object):
id=executionId),
method='POST',
headers=self.restheaders,
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
elif diff < 0:
for i in range(-diff):
open_url(
@@ -1450,7 +1459,8 @@ class KeycloakAPI(object):
id=executionId),
method='POST',
headers=self.restheaders,
timeout=self.connection_timeout)
timeout=self.connection_timeout,
validate_certs=self.validate_certs)
except Exception as e:
self.module.fail_json(msg="Unable to change execution priority %s: %s" % (executionId, str(e)))
@@ -1471,7 +1481,8 @@ class KeycloakAPI(object):
flowalias=quote(config["alias"])),
method='GET',
headers=self.restheaders,
timeout=self.connection_timeout))
timeout=self.connection_timeout,
validate_certs=self.validate_certs))
for execution in executions:
if "authenticationConfig" in execution:
execConfigId = execution["authenticationConfig"]
@@ -1483,7 +1494,8 @@ class KeycloakAPI(object):
id=execConfigId),
method='GET',
headers=self.restheaders,
timeout=self.connection_timeout))
timeout=self.connection_timeout,
validate_certs=self.validate_certs))
execution["authenticationConfig"] = execConfig
return executions
except Exception as e:

View File

@@ -32,6 +32,14 @@ options:
- Force principal name even if host is not in DNS.
required: false
type: bool
skip_host_check:
description:
- Force service to be created even when host object does not exist to manage it.
- This is only used on creation, not for updating existing services.
required: false
type: bool
default: false
version_added: 4.7.0
state:
description: State to ensure.
required: false
@@ -111,17 +119,19 @@ class ServiceIPAClient(IPAClient):
return self._post_json(method='service_remove_host', name=name, item={'host': item})
def get_service_dict(force=None, krbcanonicalname=None):
def get_service_dict(force=None, krbcanonicalname=None, skip_host_check=None):
data = {}
if force is not None:
data['force'] = force
if krbcanonicalname is not None:
data['krbcanonicalname'] = krbcanonicalname
if skip_host_check is not None:
data['skip_host_check'] = skip_host_check
return data
def get_service_diff(client, ipa_host, module_service):
non_updateable_keys = ['force', 'krbcanonicalname']
non_updateable_keys = ['force', 'krbcanonicalname', 'skip_host_check']
for key in non_updateable_keys:
if key in module_service:
del module_service[key]
@@ -135,7 +145,7 @@ def ensure(module, client):
hosts = module.params['hosts']
ipa_service = client.service_find(name=name)
module_service = get_service_dict(force=module.params['force'])
module_service = get_service_dict(force=module.params['force'], skip_host_check=module.params['skip_host_check'])
changed = False
if state in ['present', 'enabled', 'disabled']:
if not ipa_service:
@@ -183,6 +193,7 @@ def main():
argument_spec.update(
krbcanonicalname=dict(type='str', required=True, aliases=['name']),
force=dict(type='bool', required=False),
skip_host_check=dict(type='bool', default=False, required=False),
hosts=dict(type='list', required=False, elements='str'),
state=dict(type='str', required=False, default='present',
choices=['present', 'absent']))

View File

@@ -301,6 +301,15 @@ options:
- useTemplateMappers
type: bool
always_display_in_console:
description:
- Whether or not to display this client in account console, even if the
user does not have an active session.
aliases:
- alwaysDisplayInConsole
type: bool
version_added: 4.7.0
surrogate_auth_required:
description:
- Whether or not surrogate auth is required.
@@ -326,6 +335,24 @@ options:
- authenticationFlowBindingOverrides
version_added: 3.4.0
default_client_scopes:
description:
- List of default client scopes.
aliases:
- defaultClientScopes
type: list
elements: str
version_added: 4.7.0
optional_client_scopes:
description:
- List of optional client scopes.
aliases:
- optionalClientScopes
type: list
elements: str
version_added: 4.7.0
protocol_mappers:
description:
- a list of dicts defining protocol mappers for this client.
@@ -593,6 +620,7 @@ EXAMPLES = '''
use_template_config: False
use_template_scope: false
use_template_mappers: no
always_display_in_console: true
registered_nodes:
node01.example.com: 1507828202
registration_access_token: eyJWT_TOKEN
@@ -786,9 +814,12 @@ def main():
use_template_config=dict(type='bool', aliases=['useTemplateConfig']),
use_template_scope=dict(type='bool', aliases=['useTemplateScope']),
use_template_mappers=dict(type='bool', aliases=['useTemplateMappers']),
always_display_in_console=dict(type='bool', aliases=['alwaysDisplayInConsole']),
authentication_flow_binding_overrides=dict(type='dict', aliases=['authenticationFlowBindingOverrides']),
protocol_mappers=dict(type='list', elements='dict', options=protmapper_spec, aliases=['protocolMappers']),
authorization_settings=dict(type='dict', aliases=['authorizationSettings']),
default_client_scopes=dict(type='list', elements='str', aliases=['defaultClientScopes']),
optional_client_scopes=dict(type='list', elements='str', aliases=['optionalClientScopes']),
)
argument_spec.update(meta_args)

View File

@@ -104,7 +104,7 @@ author:
EXAMPLES = '''
- name: Map a client role to a group, authentication with credentials
community.general.keycloak_client_rolemappings:
community.general.keycloak_client_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
@@ -122,7 +122,7 @@ EXAMPLES = '''
delegate_to: localhost
- name: Map a client role to a group, authentication with token
community.general.keycloak_client_rolemappings:
community.general.keycloak_client_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth
@@ -138,7 +138,7 @@ EXAMPLES = '''
delegate_to: localhost
- name: Unmap client role from a group
community.general.keycloak_client_rolemappings:
community.general.keycloak_client_rolemapping:
realm: MyCustomRealm
auth_client_id: admin-cli
auth_keycloak_url: https://auth.example.com/auth

View File

@@ -294,7 +294,7 @@ class OnePasswordInfo(object):
except AnsibleModuleError as e:
module.fail_json(msg="Failed to perform initial sign in to 1Password: %s" % to_native(e))
else:
module.fail_json(msg="Unable to perform an initial sign in to 1Password. Please run '%s sigin' "
module.fail_json(msg="Unable to perform an initial sign in to 1Password. Please run '%s signin' "
"or define credentials in 'auto_login'. See the module documentation for details." % self.cli_path)
def get_token(self):

View File

@@ -90,11 +90,53 @@ options:
version_added: 3.2.0
routes4:
description:
- The list of ipv4 routes.
- Use the format '192.0.3.0/24 192.0.2.1'
- The list of IPv4 routes.
- Use the format C(192.0.3.0/24 192.0.2.1).
- To specify more complex routes, use the I(routes4_extended) option.
type: list
elements: str
version_added: 2.0.0
routes4_extended:
description:
- The list of IPv4 routes.
type: list
elements: dict
suboptions:
ip:
description:
- IP or prefix of route.
- Use the format C(192.0.3.0/24).
type: str
required: true
next_hop:
description:
- Use the format C(192.0.2.1).
type: str
metric:
description:
- Route metric.
type: int
table:
description:
- The table to add this route to.
- The default depends on C(ipv4.route-table).
type: int
cwnd:
description:
- The clamp for congestion window.
type: int
mtu:
description:
- If non-zero, only transmit packets of the specified size or smaller.
type: int
onlink:
description:
- Pretend that the nexthop is directly attached to this link, even if it does not match any interface prefix.
type: bool
tos:
description:
- The Type Of Service.
type: int
route_metric4:
description:
- Set metric level of ipv4 routes configured on interface.
@@ -165,9 +207,47 @@ options:
description:
- The list of IPv6 routes.
- Use the format C(fd12:3456:789a:1::/64 2001:dead:beef::1).
- To specify more complex routes, use the I(routes6_extended) option.
type: list
elements: str
version_added: 4.4.0
routes6_extended:
description:
- The list of IPv6 routes but with parameters.
type: list
elements: dict
suboptions:
ip:
description:
- IP or prefix of route.
- Use the format C(fd12:3456:789a:1::/64).
type: str
required: true
next_hop:
description:
- Use the format C(2001:dead:beef::1).
type: str
metric:
description:
- Route metric.
type: int
table:
description:
- The table to add this route to.
- The default depends on C(ipv6.route-table).
type: int
cwnd:
description:
- The clamp for congestion window.
type: int
mtu:
description:
- If non-zero, only transmit packets of the specified size or smaller.
type: int
onlink:
description:
- Pretend that the nexthop is directly attached to this link, even if it does not match any interface prefix.
type: bool
route_metric6:
description:
- Set metric level of IPv6 routes configured on interface.
@@ -1260,6 +1340,7 @@ class Nmcli(object):
self.gw4 = module.params['gw4']
self.gw4_ignore_auto = module.params['gw4_ignore_auto']
self.routes4 = module.params['routes4']
self.routes4_extended = module.params['routes4_extended']
self.route_metric4 = module.params['route_metric4']
self.routing_rules4 = module.params['routing_rules4']
self.never_default4 = module.params['never_default4']
@@ -1272,6 +1353,7 @@ class Nmcli(object):
self.gw6 = module.params['gw6']
self.gw6_ignore_auto = module.params['gw6_ignore_auto']
self.routes6 = module.params['routes6']
self.routes6_extended = module.params['routes6_extended']
self.route_metric6 = module.params['route_metric6']
self.dns6 = module.params['dns6']
self.dns6_search = module.params['dns6_search']
@@ -1371,7 +1453,7 @@ class Nmcli(object):
'ipv4.ignore-auto-dns': self.dns4_ignore_auto,
'ipv4.gateway': self.gw4,
'ipv4.ignore-auto-routes': self.gw4_ignore_auto,
'ipv4.routes': self.routes4,
'ipv4.routes': self.enforce_routes_format(self.routes4, self.routes4_extended),
'ipv4.route-metric': self.route_metric4,
'ipv4.routing-rules': self.routing_rules4,
'ipv4.never-default': self.never_default4,
@@ -1383,7 +1465,7 @@ class Nmcli(object):
'ipv6.ignore-auto-dns': self.dns6_ignore_auto,
'ipv6.gateway': self.gw6,
'ipv6.ignore-auto-routes': self.gw6_ignore_auto,
'ipv6.routes': self.routes6,
'ipv6.routes': self.enforce_routes_format(self.routes6, self.routes6_extended),
'ipv6.route-metric': self.route_metric6,
'ipv6.method': self.ipv6_method,
'ipv6.ip6-privacy': self.ip_privacy6,
@@ -1614,6 +1696,29 @@ class Nmcli(object):
return None
return [address if '/' in address else address + '/128' for address in ip6_addresses]
def enforce_routes_format(self, routes, routes_extended):
if routes is not None:
return routes
elif routes_extended is not None:
return [self.route_to_string(route) for route in routes_extended]
else:
return None
@staticmethod
def route_to_string(route):
result_str = ''
result_str += route['ip']
if route.get('next_hop') is not None:
result_str += ' ' + route['next_hop']
if route.get('metric') is not None:
result_str += ' ' + str(route['metric'])
for attribute, value in sorted(route.items()):
if attribute not in ('ip', 'next_hop', 'metric') and value is not None:
result_str += ' {0}={1}'.format(attribute, str(value).lower())
return result_str
@staticmethod
def bool_to_string(boolean):
if boolean:
@@ -1657,6 +1762,20 @@ class Nmcli(object):
return list
return str
def get_route_params(self, raw_values):
routes_params = []
for raw_value in raw_values:
route_params = {}
for parameter, value in re.findall(r'([\w-]*)\s?=\s?([^\s,}]*)', raw_value):
if parameter == 'nh':
route_params['next_hop'] = value
elif parameter == 'mt':
route_params['metric'] = value
else:
route_params[parameter] = value
routes_params.append(route_params)
return [self.route_to_string(route_params) for route_params in routes_params]
def list_connection_info(self):
cmd = [self.nmcli_bin, '--fields', 'name', '--terse', 'con', 'show']
(rc, out, err) = self.execute_command(cmd)
@@ -1852,13 +1971,7 @@ class Nmcli(object):
if key in conn_info:
current_value = conn_info[key]
if key in ('ipv4.routes', 'ipv6.routes') and current_value is not None:
# ipv4.routes and ipv6.routes do not have same options and show_connection() format
# options: ['10.11.0.0/24 10.10.0.2', '10.12.0.0/24 10.10.0.2 200']
# show_connection(): ['{ ip = 10.11.0.0/24, nh = 10.10.0.2 }', '{ ip = 10.12.0.0/24, nh = 10.10.0.2, mt = 200 }']
# Need to convert in order to compare both
current_value = [re.sub(r'^{\s*ip\s*=\s*([^, ]+),\s*nh\s*=\s*([^} ]+),\s*mt\s*=\s*([^} ]+)\s*}', r'\1 \2 \3',
route) for route in current_value]
current_value = [re.sub(r'^{\s*ip\s*=\s*([^, ]+),\s*nh\s*=\s*([^} ]+)\s*}', r'\1 \2', route) for route in current_value]
current_value = self.get_route_params(current_value)
if key == self.mac_setting:
# MAC addresses are case insensitive, nmcli always reports them in uppercase
value = value.upper()
@@ -1942,6 +2055,18 @@ def main():
gw4=dict(type='str'),
gw4_ignore_auto=dict(type='bool', default=False),
routes4=dict(type='list', elements='str'),
routes4_extended=dict(type='list',
elements='dict',
options=dict(
ip=dict(type='str', required=True),
next_hop=dict(type='str'),
metric=dict(type='int'),
table=dict(type='int'),
tos=dict(type='int'),
cwnd=dict(type='int'),
mtu=dict(type='int'),
onlink=dict(type='bool')
)),
route_metric4=dict(type='int'),
routing_rules4=dict(type='list', elements='str'),
never_default4=dict(type='bool', default=False),
@@ -1958,6 +2083,17 @@ def main():
dns6_search=dict(type='list', elements='str'),
dns6_ignore_auto=dict(type='bool', default=False),
routes6=dict(type='list', elements='str'),
routes6_extended=dict(type='list',
elements='dict',
options=dict(
ip=dict(type='str', required=True),
next_hop=dict(type='str'),
metric=dict(type='int'),
table=dict(type='int'),
cwnd=dict(type='int'),
mtu=dict(type='int'),
onlink=dict(type='bool')
)),
route_metric6=dict(type='int'),
method6=dict(type='str', choices=['ignore', 'auto', 'dhcp', 'link-local', 'manual', 'shared', 'disabled']),
ip_privacy6=dict(type='str', choices=['disabled', 'prefer-public-addr', 'prefer-temp-addr', 'unknown']),
@@ -2014,7 +2150,9 @@ def main():
gsm=dict(type='dict'),
wireguard=dict(type='dict'),
),
mutually_exclusive=[['never_default4', 'gw4']],
mutually_exclusive=[['never_default4', 'gw4'],
['routes4_extended', 'routes4'],
['routes6_extended', 'routes6']],
required_if=[("type", "wifi", [("ssid")])],
supports_check_mode=True,
)

View File

@@ -427,7 +427,9 @@ def package_present(m, name, want_latest):
# if a version is given leave the package in to let zypper handle the version
# resolution
packageswithoutversion = [p for p in packages if not p.version]
prerun_state = get_installed_state(m, packageswithoutversion)
prerun_state = {}
if packageswithoutversion:
prerun_state = get_installed_state(m, packageswithoutversion)
# generate lists of packages to install or remove
packages = [p for p in packages if p.shouldinstall != (p.name in prerun_state)]

View File

@@ -41,6 +41,11 @@ options:
- Whether a password will be required to run the sudo'd command.
default: true
type: bool
runas:
description:
- Specify the target user the command(s) will run as.
type: str
version_added: 4.7.0
sudoers_path:
description:
- The path which sudoers config files will be managed in.
@@ -69,6 +74,14 @@ EXAMPLES = '''
user: backup
commands: /usr/local/bin/backup
- name: Allow the bob user to run any commands as alice with sudo -u alice
community.general.sudoers:
name: bob-do-as-alice
state: present
user: bob
runas: alice
commands: ANY
- name: >-
Allow the monitoring group to run sudo /usr/local/bin/gather-app-metrics
without requiring a password
@@ -108,6 +121,7 @@ class Sudoers(object):
self.group = module.params['group']
self.state = module.params['state']
self.nopassword = module.params['nopassword']
self.runas = module.params['runas']
self.sudoers_path = module.params['sudoers_path']
self.file = os.path.join(self.sudoers_path, self.name)
self.commands = module.params['commands']
@@ -140,7 +154,8 @@ class Sudoers(object):
commands_str = ', '.join(self.commands)
nopasswd_str = 'NOPASSWD:' if self.nopassword else ''
return "{owner} ALL={nopasswd} {commands}\n".format(owner=owner, nopasswd=nopasswd_str, commands=commands_str)
runas_str = '({runas})'.format(runas=self.runas) if self.runas is not None else ''
return "{owner} ALL={runas}{nopasswd} {commands}\n".format(owner=owner, runas=runas_str, nopasswd=nopasswd_str, commands=commands_str)
def run(self):
if self.state == 'absent' and self.exists():
@@ -168,6 +183,10 @@ def main():
'type': 'bool',
'default': True,
},
'runas': {
'type': 'str',
'default': None,
},
'sudoers_path': {
'type': 'str',
'default': '/etc/sudoers.d',

View File

@@ -3,115 +3,117 @@
# and should not be used as examples of how to write Ansible roles #
####################################################################
- when:
- not (ansible_os_family == 'Alpine' and ansible_distribution_version is version('3.15', '<')) # TODO
block:
- name: Create EMAIL cron var
cronvar:
name: EMAIL
value: doug@ansibmod.con.com
register: create_cronvar1
- name: Ensure /etc/cron.d directory exists
file:
path: /etc/cron.d
state: directory
- name: Create EMAIL cron var again
cronvar:
name: EMAIL
value: doug@ansibmod.con.com
register: create_cronvar2
- name: Create EMAIL cron var
cronvar:
name: EMAIL
value: doug@ansibmod.con.com
register: create_cronvar1
- name: Check cron var value
shell: crontab -l -u root | grep -c EMAIL=doug@ansibmod.con.com
register: varcheck1
- name: Create EMAIL cron var again
cronvar:
name: EMAIL
value: doug@ansibmod.con.com
register: create_cronvar2
- name: Modify EMAIL cron var
cronvar:
name: EMAIL
value: jane@ansibmod.con.com
register: create_cronvar3
- name: Check cron var value
shell: crontab -l -u root | grep -c EMAIL=doug@ansibmod.con.com
register: varcheck1
- name: Check cron var value again
shell: crontab -l -u root | grep -c EMAIL=jane@ansibmod.con.com
register: varcheck2
- name: Modify EMAIL cron var
cronvar:
name: EMAIL
value: jane@ansibmod.con.com
register: create_cronvar3
- name: Remove EMAIL cron var
cronvar:
name: EMAIL
state: absent
register: remove_cronvar1
- name: Check cron var value again
shell: crontab -l -u root | grep -c EMAIL=jane@ansibmod.con.com
register: varcheck2
- name: Remove EMAIL cron var again
cronvar:
name: EMAIL
state: absent
register: remove_cronvar2
- name: Remove EMAIL cron var
cronvar:
name: EMAIL
state: absent
register: remove_cronvar1
- name: Check cron var value again
shell: crontab -l -u root | grep -c EMAIL
register: varcheck3
failed_when: varcheck3.rc == 0
- name: Remove EMAIL cron var again
cronvar:
name: EMAIL
state: absent
register: remove_cronvar2
- name: Add cron var to custom file
cronvar:
name: TESTVAR
value: somevalue
cron_file: cronvar_test
register: custom_cronfile1
- name: Check cron var value again
shell: crontab -l -u root | grep -c EMAIL
register: varcheck3
failed_when: varcheck3.rc == 0
- name: Add cron var to custom file again
cronvar:
name: TESTVAR
value: somevalue
cron_file: cronvar_test
register: custom_cronfile2
- name: Add cron var to custom file
cronvar:
name: TESTVAR
value: somevalue
cron_file: cronvar_test
register: custom_cronfile1
- name: Check cron var value in custom file
command: grep -c TESTVAR=somevalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck1
- name: Add cron var to custom file again
cronvar:
name: TESTVAR
value: somevalue
cron_file: cronvar_test
register: custom_cronfile2
- name: Change cron var in custom file
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
register: custom_cronfile3
- name: Check cron var value in custom file
command: grep -c TESTVAR=somevalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck1
- name: Check cron var value in custom file
command: grep -c TESTVAR=newvalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck2
- name: Change cron var in custom file
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
register: custom_cronfile3
- name: Remove cron var from custom file
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
state: absent
register: custom_remove_cronvar1
- name: Check cron var value in custom file
command: grep -c TESTVAR=newvalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck2
- name: Remove cron var from custom file again
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
state: absent
register: custom_remove_cronvar2
- name: Remove cron var from custom file
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
state: absent
register: custom_remove_cronvar1
- name: Check cron var value
command: grep -c TESTVAR=newvalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck3
failed_when: custom_varcheck3.rc == 0
- name: Remove cron var from custom file again
cronvar:
name: TESTVAR
value: newvalue
cron_file: cronvar_test
state: absent
register: custom_remove_cronvar2
- name: Esure cronvar tasks did the right thing
assert:
that:
- create_cronvar1 is changed
- create_cronvar2 is not changed
- create_cronvar3 is changed
- remove_cronvar1 is changed
- remove_cronvar2 is not changed
- varcheck1.stdout == '1'
- varcheck2.stdout == '1'
- varcheck3.stdout == '0'
- custom_remove_cronvar1 is changed
- custom_remove_cronvar2 is not changed
- custom_varcheck1.stdout == '1'
- custom_varcheck2.stdout == '1'
- custom_varcheck3.stdout == '0'
- name: Check cron var value
command: grep -c TESTVAR=newvalue {{ cron_config_path }}/cronvar_test
register: custom_varcheck3
failed_when: custom_varcheck3.rc == 0
- name: Ensure cronvar tasks did the right thing
assert:
that:
- create_cronvar1 is changed
- create_cronvar2 is not changed
- create_cronvar3 is changed
- remove_cronvar1 is changed
- remove_cronvar2 is not changed
- varcheck1.stdout == '1'
- varcheck2.stdout == '1'
- varcheck3.stdout == '0'
- custom_remove_cronvar1 is changed
- custom_remove_cronvar2 is not changed
- custom_varcheck1.stdout == '1'
- custom_varcheck2.stdout == '1'
- custom_varcheck3.stdout == '0'

View File

@@ -37,8 +37,6 @@
command: >-
dnf update -y
--setopt=obsoletes=0 {{ _packages | join(' ') }}
args:
warn: false
register: update_locked_packages
changed_when: '"Nothing to do" not in update_locked_packages.stdout'

View File

@@ -1,7 +1,5 @@
- name: "{{ reason }} ('up')"
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
when: service_state == 'up'
register: curl_result
until: not curl_result.failed
@@ -10,8 +8,6 @@
- name: "{{ reason }} ('down')"
command: "curl -sf http://localhost:8082/hello"
args:
warn: false
register: curl_result
failed_when: curl_result == 0
when: service_state == 'down'

View File

@@ -43,6 +43,18 @@
src: httpd_echo.py
dest: "{{ process_file }}"
- name: Install virtualenv
package:
name: virtualenv
state: present
when: ansible_distribution == 'CentOS' and ansible_distribution_major_version == '8'
- name: Install virtualenv
package:
name: python-virtualenv
state: present
when: ansible_os_family == 'Archlinux'
- name: install dependencies
pip:
name: "{{ item }}"

View File

@@ -1,3 +1,3 @@
#!/usr/bin/env bash
"$1" 100 &
"$1" 100 &
echo "$!" > "$2"

View File

@@ -0,0 +1,12 @@
/*
* (c) 2022, Alexei Znamensky <russoz@gmail.com>
* GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
*/
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
int delay = atoi(argv[1]);
sleep(delay);
}

View File

@@ -7,112 +7,105 @@
# Copyright: (c) 2019, Saranya Sridharan
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
- when:
- not (ansible_os_family == 'Alpine') # TODO
block:
- name: Attempt installation of latest 'psutil' version
pip:
name: psutil
ignore_errors: true
register: psutil_latest_install
- name: Attempt installation of latest 'psutil' version
pip:
name: psutil
ignore_errors: true
register: psutil_latest_install
- name: Install greatest 'psutil' version which will work with all pip versions
pip:
name: psutil < 5.7.0
when: psutil_latest_install is failed
- name: Install greatest 'psutil' version which will work with all pip versions
pip:
name: psutil < 5.7.0
when: psutil_latest_install is failed
- name: "Checking the empty result"
pids:
name: "blahblah"
register: emptypids
- name: "Checking the empty result"
pids:
name: "blahblah"
register: emptypids
- name: "Verify that the list of Process IDs (PIDs) returned is empty"
assert:
that:
- emptypids is not changed
- emptypids.pids == []
- name: "Verify that the list of Process IDs (PIDs) returned is empty"
assert:
that:
- emptypids is not changed
- emptypids.pids == []
- name: "Picking a random process name"
set_fact:
random_name: some-random-long-name-{{ 99999999 | random }}
- name: "Picking a random process name"
set_fact:
random_name: some-random-long-name-{{ 99999999 | random }}
- name: Copy the fake 'sleep' source code
copy:
src: sleeper.c
dest: "{{ remote_tmp_dir }}/sleeper.c"
mode: 0644
- name: "finding the 'sleep' binary"
command: which sleep
register: find_sleep
- name: Compile fake 'sleep' binary
command: cc {{ remote_tmp_dir }}/sleeper.c -o {{ remote_tmp_dir }}/{{ random_name }}
- name: "Copying 'sleep' binary"
command: cp {{ find_sleep.stdout }} {{ remote_tmp_dir }}/{{ random_name }}
# The following does not work on macOS 11.1 (it uses shutil.copystat, and that will die with a PermissionError):
# copy:
# src: "{{ find_sleep.stdout }}"
# dest: "{{ remote_tmp_dir }}/{{ random_name }}"
# mode: "0777"
# remote_src: true
- name: Copy helper script
copy:
src: obtainpid.sh
dest: "{{ remote_tmp_dir }}/obtainpid.sh"
mode: 0755
- name: Copy helper script
copy:
src: obtainpid.sh
dest: "{{ remote_tmp_dir }}/obtainpid.sh"
- name: "Run the fake 'sleep' binary"
command: "sh {{ remote_tmp_dir }}/obtainpid.sh '{{ remote_tmp_dir }}/{{ random_name }}' '{{ remote_tmp_dir }}/obtainpid.txt'"
- name: "Running the copy of 'sleep' binary"
command: "sh {{ remote_tmp_dir }}/obtainpid.sh '{{ remote_tmp_dir }}/{{ random_name }}' '{{ remote_tmp_dir }}/obtainpid.txt'"
async: 100
poll: 0
async: 100
poll: 0
- name: "Wait for one second to make sure that the fake 'sleep' binary has actually been started"
pause:
seconds: 1
- name: "Wait for one second to make sure that the sleep copy has actually been started"
pause:
seconds: 1
- name: "Checking the process IDs (PIDs) of fake 'sleep' binary"
pids:
name: "{{ random_name }}"
register: pids
- name: "Checking the process IDs (PIDs) of sleep binary"
pids:
name: "{{ random_name }}"
register: pids
- name: "Checking that exact non-substring matches are required"
pids:
name: "{{ random_name[0:5] }}"
register: exactpidmatch
- name: "Checking that exact non-substring matches are required"
pids:
name: "{{ random_name[0:5] }}"
register: exactpidmatch
- name: "Checking that patterns can be used with the pattern option"
pids:
pattern: "{{ random_name[0:5] }}"
register: pattern_pid_match
- name: "Checking that patterns can be used with the pattern option"
pids:
pattern: "{{ random_name[0:5] }}"
register: pattern_pid_match
- name: "Checking that case-insensitive patterns can be used with the pattern option"
pids:
pattern: "{{ random_name[0:5] | upper }}"
ignore_case: true
register: caseinsensitive_pattern_pid_match
- name: "Checking that case-insensitive patterns can be used with the pattern option"
pids:
pattern: "{{ random_name[0:5] | upper }}"
ignore_case: true
register: caseinsensitive_pattern_pid_match
- name: "Checking that .* includes test pid"
pids:
pattern: .*
register: match_all
- name: "Checking that .* includes test pid"
pids:
pattern: .*
register: match_all
- name: "Reading pid from the file"
slurp:
src: "{{ remote_tmp_dir }}/obtainpid.txt"
register: newpid
- name: "Reading pid from the file"
slurp:
src: "{{ remote_tmp_dir }}/obtainpid.txt"
register: newpid
- name: "Verify that the Process IDs (PIDs) returned is not empty and also equal to the PIDs obtained in console"
assert:
that:
- "pids.pids | join(' ') == newpid.content | b64decode | trim"
- "pids.pids | length > 0"
- "exactpidmatch.pids == []"
- "pattern_pid_match.pids | join(' ') == newpid.content | b64decode | trim"
- "caseinsensitive_pattern_pid_match.pids | join(' ') == newpid.content | b64decode | trim"
- newpid.content | b64decode | trim | int in match_all.pids
- name: "Verify that the Process IDs (PIDs) returned is not empty and also equal to the PIDs obtained in console"
assert:
that:
- "pids.pids | join(' ') == newpid.content | b64decode | trim"
- "pids.pids | length > 0"
- "exactpidmatch.pids == []"
- "pattern_pid_match.pids | join(' ') == newpid.content | b64decode | trim"
- "caseinsensitive_pattern_pid_match.pids | join(' ') == newpid.content | b64decode | trim"
- newpid.content | b64decode | trim | int in match_all.pids
- name: "Register output of bad input pattern"
pids:
pattern: (unterminated
register: bad_pattern_result
ignore_errors: true
- name: "Register output of bad input pattern"
pids:
pattern: (unterminated
register: bad_pattern_result
ignore_errors: true
- name: "Verify that bad input pattern result is failed"
assert:
that:
- bad_pattern_result is failed
- name: "Verify that bad input pattern result is failed"
assert:
that:
- bad_pattern_result is failed

View File

@@ -21,7 +21,6 @@
- '{{ pkgng_test_outofdate_pkg_tempdir.path }}'
- '--manifest'
- '{{ pkgng_test_outofdate_pkg_tempdir.path }}/MANIFEST'
warn: no
# pkg switched from .txz to .pkg in version 1.17.0
# Might as well look for all valid pkg extensions.

View File

@@ -32,8 +32,6 @@
until: faketime_package_installed is success
- name: Find libfaketime path
shell: '{{ list_pkg_files }} {{ faketime_pkg }} | grep -F libfaketime.so.1'
args:
warn: false
register: libfaketime_path
- when: ansible_service_mgr == 'systemd'
block:

View File

@@ -165,8 +165,6 @@
- name: Reinstall internationalization files
shell: yum -y reinstall glibc-common || yum -y install glibc-common
args:
warn: false
when: locale_present is failed
- name: Generate locale (RedHat)

View File

@@ -102,6 +102,21 @@
src: "{{ alt_sudoers_path }}/my-sudo-rule-5"
register: rule_5_contents
- name: Create rule to runas another user
community.general.sudoers:
name: my-sudo-rule-6
state: present
user: alice
commands: /usr/local/bin/command
runas: bob
sudoers_path: "{{ sudoers_path }}"
register: rule_6
- name: Grab contents of my-sudo-rule-6 (in alternative directory)
ansible.builtin.slurp:
src: "{{ sudoers_path }}/my-sudo-rule-6"
register: rule_6_contents
- name: Revoke rule 1
community.general.sudoers:
@@ -133,6 +148,7 @@
- "rule_3_contents['content'] | b64decode == 'alice ALL= /usr/local/bin/command\n'"
- "rule_4_contents['content'] | b64decode == '%students ALL=NOPASSWD: /usr/local/bin/command\n'"
- "rule_5_contents['content'] | b64decode == 'alice ALL=NOPASSWD: /usr/local/bin/command\n'"
- "rule_6_contents['content'] | b64decode == 'alice ALL=(bob)NOPASSWD: /usr/local/bin/command\n'"
- name: Check stats
ansible.builtin.assert:

View File

@@ -14,7 +14,7 @@ def main():
"""Main entry point."""
if not os.path.isdir(os.path.join('docs', 'docsite')):
return
p = subprocess.run(['antsibull-lint', 'collection-docs', '.'], check=False)
p = subprocess.run(['antsibull-docs', 'lint-collection-docs', '.'], check=False)
if p.returncode not in (0, 3):
print('{0}:0:0: unexpected return code {1}'.format(sys.argv[0], p.returncode))

View File

@@ -0,0 +1,50 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/modules/cloud/lxc/lxc_container.py use-argspec-type-path
plugins/modules/cloud/lxc/lxc_container.py validate-modules:use-run-command-not-popen
plugins/modules/cloud/misc/rhevm.py validate-modules:parameter-state-invalid-choice
plugins/modules/cloud/rackspace/rax.py use-argspec-type-path # fix needed
plugins/modules/cloud/rackspace/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/cloud/rackspace/rax_files_objects.py use-argspec-type-path
plugins/modules/cloud/rackspace/rax_scaling_group.py use-argspec-type-path # fix needed, expanduser() applied to dict values
plugins/modules/cloud/scaleway/scaleway_organization_info.py validate-modules:return-syntax-error
plugins/modules/cloud/smartos/vmadm.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/smartos/vmadm.py validate-modules:undocumented-parameter
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:undocumented-parameter
plugins/modules/cloud/univention/udm_share.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/univention/udm_user.py validate-modules:parameter-list-no-elements
plugins/modules/clustering/consul/consul.py validate-modules:doc-missing-type
plugins/modules/clustering/consul/consul.py validate-modules:undocumented-parameter
plugins/modules/clustering/consul/consul_session.py validate-modules:parameter-state-invalid-choice
plugins/modules/packaging/language/composer.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/language/yarn.py use-argspec-type-path
plugins/modules/packaging/os/apt_rpm.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/homebrew.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/homebrew_cask.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/opkg.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/pacman.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/redhat_subscription.py validate-modules:return-syntax-error
plugins/modules/packaging/os/slackpkg.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/urpmi.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/packaging/os/xbps.py validate-modules:parameter-invalid # invalid alias - removed in 5.0.0
plugins/modules/remote_management/hpilo/hpilo_boot.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/hpilo/hpilo_info.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/manageiq/manageiq_policies.py validate-modules:parameter-state-invalid-choice
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:doc-choices-do-not-match-spec # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:doc-missing-type # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:parameter-type-not-in-doc # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:undocumented-parameter # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/source_control/github/github_deploy_key.py validate-modules:parameter-invalid
plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/iptables_state.py validate-modules:undocumented-parameter
plugins/modules/system/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/puppet.py use-argspec-type-path
plugins/modules/system/puppet.py validate-modules:parameter-invalid # invalid alias - removed in 7.0.0
plugins/modules/system/ssh_config.py use-argspec-type-path # Required since module uses other methods to specify path
plugins/modules/system/xfconf.py validate-modules:parameter-state-invalid-choice # state get removed in 5.0.0
plugins/modules/system/xfconf.py validate-modules:return-syntax-error
plugins/modules/web_infrastructure/jenkins_plugin.py use-argspec-type-path

View File

@@ -169,6 +169,17 @@ TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_ROUTE = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'ifname': 'ethernet_non_existant',
'ip6': '2001:beef:cafe:10::1/64',
'routes6_extended': [{'ip': 'fd2e:446f:d85d:5::/64',
'next_hop': '2001:beef:cafe:10::2'}],
'method6': 'manual',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_ROUTE_SHOW_OUTPUT = """\
@@ -197,6 +208,14 @@ TESTCASE_ETHERNET_MOD_IPV4_INT_WITH_ROUTE_AND_METRIC = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'routes4_extended': [{'ip': '192.168.200.0/24', 'next_hop': '192.168.1.1'}],
'route_metric4': 10,
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_MOD_IPV4_INT_WITH_ROUTE_AND_METRIC_SHOW_OUTPUT = """\
@@ -218,6 +237,14 @@ TESTCASE_ETHERNET_MOD_IPV6_INT_WITH_ROUTE_AND_METRIC = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'routes6_extended': [{'ip': 'fd2e:446f:d85d:5::/64', 'next_hop': '2001:beef:cafe:10::2'}],
'route_metric6': 10,
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_MOD_IPV6_INT_WITH_ROUTE_AND_METRIC_SHOW_OUTPUT = """\
@@ -241,6 +268,17 @@ TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_MULTIPLE_ROUTES = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'ifname': 'ethernet_non_existant',
'ip6': '2001:beef:cafe:10::1/64',
'routes6_extended': [{'ip': 'fd2e:446f:d85d:5::/64', 'next_hop': '2001:beef:cafe:10::2'},
{'ip': 'fd2e:8890:abcd:25::/64', 'next_hop': '2001:beef:cafe:10::5'}],
'method6': 'manual',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_MULTIPLE_ROUTES_SHOW_OUTPUT = """\
@@ -273,6 +311,18 @@ TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_ROUTE_AND_METRIC = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'ifname': 'ethernet_non_existant',
'method4': 'disabled',
'ip6': '2001:beef:cafe:10::1/64',
'routes6_extended': [{'ip': 'fd2e:446f:d85d:5::/64', 'next_hop': '2001:beef:cafe:10::2'}],
'route_metric6': 5,
'method6': 'manual',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_ROUTE_AND_METRIC_SHOW_OUTPUT = """\
@@ -305,6 +355,19 @@ TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_MULTIPLE_ROUTES_AND_METRIC = [
'state': 'present',
'_ansible_check_mode': False,
},
{
'type': 'ethernet',
'conn_name': 'non_existent_nw_device',
'ifname': 'ethernet_non_existant',
'method4': 'disabled',
'ip6': '2001:beef:cafe:10::1/64',
'routes6_extended': [{'ip': 'fd2e:446f:d85d:5::/64', 'next_hop': '2001:beef:cafe:10::2'},
{'ip': 'fd2e:8890:abcd:25::/64', 'next_hop': '2001:beef:cafe:10::5'}],
'route_metric6': 5,
'method6': 'manual',
'state': 'present',
'_ansible_check_mode': False,
},
]
TESTCASE_ETHERNET_ADD_IPV6_INT_WITH_MULTIPLE_ROUTES_AND_METRIC_SHOW_OUTPUT = """\