Compare commits

...

31 Commits
8.2.0 ... 8.3.0

Author SHA1 Message Date
Felix Fontein
983b4d70e0 Release 8.3.0. 2024-01-29 20:16:51 +01:00
Felix Fontein
821ae9bc41 Prepare 8.3.0 release. 2024-01-29 20:16:02 +01:00
Felix Fontein
d5efd56dae [stable-8] terraform: support diff for resource_changes (#7896) (#7914)
terraform: support diff for resource_changes (#7896)

(cherry picked from commit 0dc891bf37)

Co-authored-by: Parsa Yousefi <p.yousefi97@gmail.com>
2024-01-29 19:30:16 +01:00
patchback[bot]
88ef840750 [PR #7697/a5cd4ebe backport][stable-8] Simplify regex for identifying order number in DN (#7646) (#7915)
Simplify regex for identifying order number in DN (#7646) (#7697)

Assume that if a string of digits occurs between curly braces anywhere
in the first component of the DN, that this is an order number. The
sequence does not necessarily have to occur after an equals sign.

(cherry picked from commit a5cd4ebea2)

Co-authored-by: Aaron Sowry <aeneby@users.noreply.github.com>
2024-01-29 19:30:08 +01:00
patchback[bot]
08d04a7923 [PR #7695/997e6345 backport][stable-8] Fixes #7389 - NMCLI issue with creating a wifi bridge-slave (#7912)
Fixes #7389 - NMCLI issue with creating a wifi bridge-slave (#7695)

* working mod

* added changelog fragment

* added link on fragment

* Update changelogs/fragments/7389-nmcli-issue-with-creating-a-wifi-bridge-slave.yml

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

* last fix

---------

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

Co-authored-by: Gianmarco Mameli <57061995+gianmarco-mameli@users.noreply.github.com>
2024-01-28 13:39:43 +01:00
patchback[bot]
9d0b14b239 [PR #7907/2580da97 backport][stable-8] Zuul third-party-check: disable ansible-doc part of galaxy-importer (#7910)
Zuul third-party-check: disable ansible-doc part of galaxy-importer (#7907)

Zuul third-party-check: disable ansible-doc part of galaxy-importer.

(cherry picked from commit 2580da9796)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-27 16:05:38 +01:00
patchback[bot]
9e83a4cb34 [PR #7901/84147081 backport][stable-8] Consul acl deprecation (#7906)
Consul acl deprecation (#7901)

Start deprecation of consul_acl.

(cherry picked from commit 84147081d4)

Co-authored-by: Florian Apolloner <florian@apolloner.eu>
2024-01-27 13:54:05 +01:00
patchback[bot]
dc5f012e52 [PR #7897/afd19888 backport][stable-8] Consul action group (#7905)
Consul action group (#7897)

Added action group for new style consul modules.

(cherry picked from commit afd1988810)

Co-authored-by: Florian Apolloner <florian@apolloner.eu>
2024-01-27 12:21:34 +01:00
Felix Fontein
210adc196e Fix changelog fragment filenames.
(cherry picked from commit f8465c692b)
2024-01-27 11:07:35 +01:00
patchback[bot]
f30fcec398 [PR #7870/be3bfd6f backport][stable-8] Detection of already installed homebrew cask (#7904)
Detection of already installed homebrew cask (#7870)

* fix: detect already installed cask

Use json output v2 to check if formulae and casks are installed

chore: add changelog fragment

* test: add homebrew cask specific tests

* refactor: change cask used in tests

* chore: apply suggestions to changelog fragment

(cherry picked from commit be3bfd6fa5)

Co-authored-by: João Victor Silva <160127815@aluno.unb.br>
2024-01-27 10:37:13 +01:00
patchback[bot]
0a904d60cd [PR #7878/29f98654 backport][stable-8] Add new consul modules and reuse code between them. (#7902)
Add new consul modules and reuse code between them. (#7878)

Refactored consul modules and added new roles.

(cherry picked from commit 29f9865497)

Co-authored-by: Florian Apolloner <florian@apolloner.eu>
2024-01-27 10:33:33 +01:00
patchback[bot]
1ee2bba140 [PR #7824/4298f2dd backport][stable-8] New module: gitlab_milestone (#7899)
New module: gitlab_milestone (#7824)

* new module gitlab_milestone

* change BOTMETA

* remove blank line

* version_added field

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

* Update plugins/modules/gitlab_milestone.py

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

* Update description with reference

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

* Dates as string type

* Removed python 2.7 requirement

* Fixes from recent PR comments.

* milestones_obj returned on success

---------

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

Co-authored-by: Gabriele Pongelli <gpongelli@users.noreply.github.com>
2024-01-27 10:23:38 +01:00
patchback[bot]
0aa9cd0e30 [PR #7872/2d3f99ec backport][stable-8] fix proxmox update when setting does not already exist (#7898)
fix proxmox update when setting does not already exist (#7872)

* fix proxmox update when setting does not already exist

* add changelog fragment

---------

Co-authored-by: Eric Trombly <etrombly@iomaxis.com>
(cherry picked from commit 2d3f99ec3a)

Co-authored-by: Eric Trombly <etrombly@yahoo.com>
2024-01-27 10:23:30 +01:00
Felix Fontein
7009a768a4 [stable-8] New module: gitlab_label (#7657) (#7900)
New module: gitlab_label (#7657)

* gitlab project label first commit

* fixes from CI run

* fixing some sanity test

* sanity checks, removing typing

* remove default for required field

* fix indentation

* improving test set

* fixes to pass test set

* reuse compliancy

* fix sanity checks

* fix: method returns group, not project

* refactor: start adding group, test still pass

* updated module and tests to handle group labels

* update name to remove 'project'

* removing default

* typo

* generic name for returned dict

* returns also label object from library invocation

* remove unused var, updated doc

* fix output object name

* version_added

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

* Remove python 2.7

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

* Missing dot

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

* Remove version_added

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

* Remove useless doc

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

* Color is a string

* Fixes from recent PR comments.

---------

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

Co-authored-by: Gabriele Pongelli <gpongelli@users.noreply.github.com>
2024-01-27 10:23:19 +01:00
patchback[bot]
2f5552da04 [PR #7873/13e3161f backport][stable-8] Refer to LXD containers/VMs as instances (#7891)
Refer to LXD containers/VMs as instances (#7873)

* plugins/connection/lxd: rename container to instance

Signed-off-by: Simon Deziel <simon.deziel@canonical.com>

* plugins/inventory/lxd: rename container to instance

It seems that a previous search and replace was done but it
missed those `containe_name` due to missing `r` in `container`.

Signed-off-by: Simon Deziel <simon.deziel@canonical.com>

---------

Signed-off-by: Simon Deziel <simon.deziel@canonical.com>
(cherry picked from commit 13e3161f2a)

Co-authored-by: Simon Deziel <simon.deziel@canonical.com>
2024-01-24 19:49:01 +01:00
Felix Fontein
145686cfe0 [stable-8] Add redfish_info command to get service identification (#7883) (#7887)
Add redfish_info command to get service identification (#7883)

* Update redfish_info.py

* Create 7882-add-redfish-get-service-identification.yml

* add get_service_identification

* Update changelogs/fragments/7882-add-redfish-get-service-identification.yml

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

* Update plugins/modules/redfish_info.py

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

---------

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

Co-authored-by: D Honig <namssa@gmail.com>
2024-01-23 07:59:06 +01:00
patchback[bot]
08239919de [PR #7875/44028060 backport][stable-8] Fix: incus connection plugin treats inventory_hostname incorrectly in remote config (#7886)
Fix: incus connection plugin treats inventory_hostname incorrectly in remote config (#7875)

* Fixes inventory_hostname treatment as a litteral instead of inventory_hostname variable. Similar problem fixed in LXD: https://github.com/ansible-collections/community.general/pull/4912

* changelog for upsream

* Update changelogs/fragments/7874-incus_connection_treats_inventory_hostname_as_literal_in_remotes.yml

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

---------

Co-authored-by: travis <travis@cypressMini.local>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 44028060c3)

Co-authored-by: Travis McCollum <travis.mccollum+github@gmail.com>
2024-01-23 07:29:23 +01:00
patchback[bot]
1ab1f8f62b [PR #7826/44679e71 backport][stable-8] Refactor of consul modules (#7877)
Refactor of consul modules (#7826)

* Extract common functionality.

* Refactor duplicated code into module_utils.

* Fixed ansible-test issues.

* Address review comments.

* Revert changes to consul_acl.

It uses deprecated APIs disabled since Consul 1.11 (which is EOL), don't
bother updating the module anymore.

* Remove unused code.

* Merge token into default doc fragment.

* JSON all the way down.

* extract validation tests into custom file and prep for requests removal.

* Removed dependency on requests.

* Initial test for consul_kv.

* fixup license headers.

* Revert changes to consul.py since it utilizes python-consul.

* Disable the lookup test for now.

* Fix python 2.7 support.

* Address review comments.

* Address review comments.

* Addec changelog fragment.

* Mark ConsulModule as private.

(cherry picked from commit 44679e71a2)

Co-authored-by: Florian Apolloner <florian@apolloner.eu>
2024-01-21 17:51:45 +00:00
patchback[bot]
13c25154b5 [PR #7855/cd77d67e backport][stable-8] Add missing id parameter into pacman_key documentation examples. (#7868)
Add missing `id` parameter into pacman_key documentation examples. (#7855)

Key ID is a mandatory parameter, and the examples which miss it are
incorrect.

(cherry picked from commit cd77d67efb)

Co-authored-by: Danila Kiver <forumdan@mail.ru>
2024-01-18 08:12:58 +01:00
patchback[bot]
d22199f82e [PR #7821/92f8bf7b backport][stable-8] mssql_script: make module Python 2 compatible (#7866)
mssql_script: make module Python 2 compatible (#7821)

Make module Python 2 compatible.

(cherry picked from commit 92f8bf7b6f)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-18 07:33:49 +01:00
patchback[bot]
82a07de870 [PR #7857/069b485b backport][stable-8] Use shared workflow for Galaxy import test (#7864)
Use shared workflow for Galaxy import test (#7857)

Simplifiy workflows.

(cherry picked from commit 069b485b7e)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-18 07:33:35 +01:00
patchback[bot]
895c1fe2a0 [PR #7858/002208f4 backport][stable-8] Make compatible with newer reuse versions (#7860)
Make compatible with newer reuse versions (#7858)

Make compatible with newer reuse versions.

(cherry picked from commit 002208f425)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-18 07:10:56 +01:00
patchback[bot]
3e972990cb [PR #7795/31de16ce backport][stable-8] ipa_otptoken: fix wrong return value string to bool (#7852)
ipa_otptoken: fix wrong return value string to bool (#7795)

ipa_data is return ipatokendisable in boolean format and the module expects it as a string
this behavior causes a lack of idempotency and the get_diff module will fail in the second run.

(cherry picked from commit 31de16cee3)

Co-authored-by: Parsa Yousefi <p.yousefi97@gmail.com>
2024-01-16 22:31:47 +01:00
Felix Fontein
0ac6e44566 [stable-8] Use import galaxy workflow from ansible-collections/community.docker#754 (#7842)
Use import galaxy workflow from ansible-collections/community.docker#754 (#7839)

Use import galaxy workflow from https://github.com/ansible-collections/community.docker/pull/754.

(cherry picked from commit 32ec751996)
2024-01-13 19:25:40 +01:00
Felix Fontein
1308198eae [stable-8] CI: remove ignore files for ansible-core 2.11 and 2.12 (#7838)
CI: remove ignore files for ansible-core 2.11 and 2.12 (#7837)

Remove ignore files for ansible-core 2.11 and 2.12.

(cherry picked from commit 76fde43fca)
2024-01-13 16:26:24 +01:00
Felix Fontein
d887985b46 Rewrite with PyYAML (except comments). 2024-01-13 11:20:17 +01:00
patchback[bot]
7d6e7fa5fa [PR #7831/8891f559 backport][stable-8] Disable timezone tests on Arch Linux (#7835)
Disable timezone tests on Arch Linux (#7831)

Disable timezone tests on Arch Linux.

(cherry picked from commit 8891f559ef)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-13 11:00:23 +01:00
patchback[bot]
b61b988b2e [PR #7827/87866477 backport][stable-8] CI: fix xml tests on RHEL 8 (#7830)
CI: fix xml tests on RHEL 8 (#7827)

* Try to fix xml installation on RHEL.

* Install python-lxml on RHEL 8. Should speed up tests considerably.

(cherry picked from commit 878664778e)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-13 10:11:39 +01:00
Felix Fontein
549c9e69ae [stable-8] CI: for some reason async-timeout doesn't seem to get installed on Python 3.11 (#7812)
CI: for some reason async-timeout doesn't seem to get installed on Python 3.11 (#7811)

For some reason async-timeout doesn't seem to get installed on Python 3.11.

(cherry picked from commit 9946f758af)
2024-01-05 08:53:06 +01:00
patchback[bot]
fe35cff7a4 [PR #7807/ee8b1570 backport][stable-8] Fix failing sanity and integration tests (#7809)
Fix failing sanity and integration tests (#7807)

* Remove some Shippable specific code that trips latest shellcheck.

* Rename templated shell script to .sh.j2 to avoid shellcheck disliking the templating.

* Copy on the remote, not from controller to remote.

(cherry picked from commit ee8b15708f)

Co-authored-by: Felix Fontein <felix@fontein.de>
2024-01-04 23:24:38 +01:00
Felix Fontein
cef57b044b Next expected release will be 8.3.0 2024-01-01 18:22:57 +01:00
76 changed files with 4295 additions and 1199 deletions

6
.github/BOTMETA.yml vendored
View File

@@ -562,8 +562,12 @@ files:
maintainers: paytroff
$modules/gitlab_issue.py:
maintainers: zvaraondrej
$modules/gitlab_label.py:
maintainers: gpongelli
$modules/gitlab_merge_request.py:
maintainers: zvaraondrej
$modules/gitlab_milestone.py:
maintainers: gpongelli
$modules/gitlab_project_variable.py:
maintainers: markuman
$modules/gitlab_instance_variable.py:
@@ -1490,7 +1494,7 @@ macros:
team_ansible_core:
team_aix: MorrisA bcoca d-little flynn1973 gforster kairoaraujo marvin-sinister mator molekuul ramooncamacho wtcross
team_bsd: JoergFiedler MacLemon bcoca dch jasperla mekanix opoplawski overhacked tuxillo
team_consul: sgargan
team_consul: sgargan apollo13
team_cyberark_conjur: jvanderhoof ryanprior
team_e_spirit: MatrixCrawler getjack
team_flatpak: JayKayy oolongbrothers

20
.github/workflows/import-galaxy.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
name: import-galaxy
'on':
# Run CI against all pushes (direct commits, also merged PRs) to main, and all Pull Requests
push:
branches:
- main
- stable-*
pull_request:
jobs:
import-galaxy:
permissions:
contents: read
name: Test to import built collection artifact with Galaxy importer
uses: ansible-community/github-action-test-galaxy-import/.github/workflows/test-galaxy-import.yml@main

View File

@@ -26,10 +26,5 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha || '' }}
- name: Install dependencies
run: |
pip install reuse
- name: Check REUSE compliance
run: |
reuse lint
- name: REUSE Compliance Check
uses: fsfe/reuse-action@v2

View File

@@ -6,6 +6,53 @@ Community General Release Notes
This changelog describes changes after version 7.0.0.
v8.3.0
======
Release Summary
---------------
Regular bugfix and feature release.
Minor Changes
-------------
- consul_auth_method, consul_binding_rule, consul_policy, consul_role, consul_session, consul_token - added action group ``community.general.consul`` (https://github.com/ansible-collections/community.general/pull/7897).
- consul_policy - added support for diff and check mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_policy, consul_role, consul_session - removed dependency on ``requests`` and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826, https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``node_identities`` now expects a ``node_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``service_identities`` now expects a ``service_name`` option to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for diff mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for templated policies (https://github.com/ansible-collections/community.general/pull/7878).
- redfish_info - add command ``GetServiceIdentification`` to get service identification (https://github.com/ansible-collections/community.general/issues/7882).
- terraform - add support for ``diff_mode`` for terraform resource_changes (https://github.com/ansible-collections/community.general/pull/7896).
Deprecated Features
-------------------
- consul_acl - the module has been deprecated and will be removed in community.general 10.0.0. ``consul_token`` and ``consul_policy`` can be used instead (https://github.com/ansible-collections/community.general/pull/7901).
Bugfixes
--------
- homebrew - detect already installed formulae and casks using JSON output from ``brew info`` (https://github.com/ansible-collections/community.general/issues/864).
- incus connection plugin - treats ``inventory_hostname`` as a variable instead of a literal in remote connections (https://github.com/ansible-collections/community.general/issues/7874).
- ipa_otptoken - the module expect ``ipatokendisabled`` as string but the ``ipatokendisabled`` value is returned as a boolean (https://github.com/ansible-collections/community.general/pull/7795).
- ldap - previously the order number (if present) was expected to follow an equals sign in the DN. This makes it so the order number string is identified correctly anywhere within the DN (https://github.com/ansible-collections/community.general/issues/7646).
- mssql_script - make the module work with Python 2 (https://github.com/ansible-collections/community.general/issues/7818, https://github.com/ansible-collections/community.general/pull/7821).
- nmcli - fix ``connection.slave-type`` wired to ``bond`` and not with parameter ``slave_type`` in case of connection type ``wifi`` (https://github.com/ansible-collections/community.general/issues/7389).
- proxmox - fix updating a container config if the setting does not already exist (https://github.com/ansible-collections/community.general/pull/7872).
New Modules
-----------
- consul_acl_bootstrap - Bootstrap ACLs in Consul
- consul_auth_method - Manipulate Consul auth methods
- consul_binding_rule - Manipulate Consul binding rules
- consul_token - Manipulate Consul tokens
- gitlab_label - Creates/updates/deletes GitLab Labels belonging to project or group.
- gitlab_milestone - Creates/updates/deletes GitLab Milestones belonging to project or group
v8.2.0
======

View File

@@ -1072,3 +1072,76 @@ releases:
name: github_app_access_token
namespace: null
release_date: '2024-01-01'
8.3.0:
changes:
bugfixes:
- homebrew - detect already installed formulae and casks using JSON output from
``brew info`` (https://github.com/ansible-collections/community.general/issues/864).
- incus connection plugin - treats ``inventory_hostname`` as a variable instead
of a literal in remote connections (https://github.com/ansible-collections/community.general/issues/7874).
- ipa_otptoken - the module expect ``ipatokendisabled`` as string but the ``ipatokendisabled``
value is returned as a boolean (https://github.com/ansible-collections/community.general/pull/7795).
- ldap - previously the order number (if present) was expected to follow an
equals sign in the DN. This makes it so the order number string is identified
correctly anywhere within the DN (https://github.com/ansible-collections/community.general/issues/7646).
- mssql_script - make the module work with Python 2 (https://github.com/ansible-collections/community.general/issues/7818,
https://github.com/ansible-collections/community.general/pull/7821).
- nmcli - fix ``connection.slave-type`` wired to ``bond`` and not with parameter
``slave_type`` in case of connection type ``wifi`` (https://github.com/ansible-collections/community.general/issues/7389).
- proxmox - fix updating a container config if the setting does not already
exist (https://github.com/ansible-collections/community.general/pull/7872).
deprecated_features:
- consul_acl - the module has been deprecated and will be removed in community.general
10.0.0. ``consul_token`` and ``consul_policy`` can be used instead (https://github.com/ansible-collections/community.general/pull/7901).
minor_changes:
- consul_auth_method, consul_binding_rule, consul_policy, consul_role, consul_session,
consul_token - added action group ``community.general.consul`` (https://github.com/ansible-collections/community.general/pull/7897).
- consul_policy - added support for diff and check mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_policy, consul_role, consul_session - removed dependency on ``requests``
and factored out common parts (https://github.com/ansible-collections/community.general/pull/7826,
https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``node_identities`` now expects a ``node_name`` option to match
the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - ``service_identities`` now expects a ``service_name`` option
to match the Consul API, the old ``name`` is still supported as alias (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for diff mode (https://github.com/ansible-collections/community.general/pull/7878).
- consul_role - added support for templated policies (https://github.com/ansible-collections/community.general/pull/7878).
- redfish_info - add command ``GetServiceIdentification`` to get service identification
(https://github.com/ansible-collections/community.general/issues/7882).
- terraform - add support for ``diff_mode`` for terraform resource_changes (https://github.com/ansible-collections/community.general/pull/7896).
release_summary: Regular bugfix and feature release.
fragments:
- 7389-nmcli-issue-with-creating-a-wifi-bridge-slave.yml
- 7646-fix-order-number-detection-in-dn.yml
- 7797-ipa-fix-otp-idempotency.yml
- 7821-mssql_script-py2.yml
- 7826-consul-modules-refactoring.yaml
- 7870-homebrew-cask-installed-detection.yml
- 7872-proxmox_fix-update-if-setting-doesnt-exist.yaml
- 7874-incus_connection_treats_inventory_hostname_as_literal_in_remotes.yml
- 7882-add-redfish-get-service-identification.yml
- 7896-add-terraform-diff-mode.yml
- 7897-consul-action-group.yaml
- 7901-consul-acl-deprecation.yaml
- 8.3.0.yml
modules:
- description: Bootstrap ACLs in Consul
name: consul_acl_bootstrap
namespace: ''
- description: Manipulate Consul auth methods
name: consul_auth_method
namespace: ''
- description: Manipulate Consul binding rules
name: consul_binding_rule
namespace: ''
- description: Manipulate Consul tokens
name: consul_token
namespace: ''
- description: Creates/updates/deletes GitLab Labels belonging to project or group.
name: gitlab_label
namespace: ''
- description: Creates/updates/deletes GitLab Milestones belonging to project
or group
name: gitlab_milestone
namespace: ''
release_date: '2024-01-29'

View File

@@ -5,7 +5,7 @@
namespace: community
name: general
version: 8.2.0
version: 8.3.0
readme: README.md
authors:
- Ansible (https://github.com/ansible)

View File

@@ -4,6 +4,14 @@
# SPDX-License-Identifier: GPL-3.0-or-later
requires_ansible: '>=2.13.0'
action_groups:
consul:
- consul_auth_method
- consul_binding_rule
- consul_policy
- consul_role
- consul_session
- consul_token
plugin_routing:
connection:
docker:
@@ -22,6 +30,10 @@ plugin_routing:
nios_next_network:
redirect: infoblox.nios_modules.nios_next_network
modules:
consul_acl:
deprecation:
removal_version: 10.0.0
warning_text: Use community.general.consul_token and/or community.general.consul_policy instead.
rax_cbs_attachments:
deprecation:
removal_version: 9.0.0
@@ -153,9 +165,9 @@ plugin_routing:
stackdriver:
deprecation:
removal_version: 9.0.0
warning_text: >
This module relies on HTTPS APIs that do not exist anymore, and any new development in the
direction of providing an alternative should happen in the context of the google.cloud collection.
warning_text: This module relies on HTTPS APIs that do not exist anymore,
and any new development in the direction of providing an alternative should
happen in the context of the google.cloud collection.
system.aix_devices:
redirect: community.general.aix_devices
deprecation:
@@ -807,7 +819,8 @@ plugin_routing:
flowdock:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
notification.flowdock:
redirect: community.general.flowdock
deprecation:
@@ -4446,7 +4459,8 @@ plugin_routing:
webfaction_app:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
cloud.webfaction.webfaction_app:
redirect: community.general.webfaction_app
deprecation:
@@ -4457,7 +4471,8 @@ plugin_routing:
webfaction_db:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
cloud.webfaction.webfaction_db:
redirect: community.general.webfaction_db
deprecation:
@@ -4468,7 +4483,8 @@ plugin_routing:
webfaction_domain:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
cloud.webfaction.webfaction_domain:
redirect: community.general.webfaction_domain
deprecation:
@@ -4479,7 +4495,8 @@ plugin_routing:
webfaction_mailbox:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
cloud.webfaction.webfaction_mailbox:
redirect: community.general.webfaction_mailbox
deprecation:
@@ -4490,7 +4507,8 @@ plugin_routing:
webfaction_site:
deprecation:
removal_version: 9.0.0
warning_text: This module relies on HTTPS APIs that do not exist anymore and there is no clear path to update.
warning_text: This module relies on HTTPS APIs that do not exist anymore and
there is no clear path to update.
cloud.webfaction.webfaction_site:
redirect: community.general.webfaction_site
deprecation:
@@ -4646,7 +4664,8 @@ plugin_routing:
rackspace:
deprecation:
removal_version: 9.0.0
warning_text: This doc fragment is used by rax modules, that rely on the deprecated package pyrax.
warning_text: This doc fragment is used by rax modules, that rely on the deprecated
package pyrax.
_gcp:
redirect: community.google._gcp
docker:

View File

@@ -21,6 +21,7 @@ DOCUMENTATION = """
- The instance identifier.
default: inventory_hostname
vars:
- name: inventory_hostname
- name: ansible_host
- name: ansible_incus_host
executable:

View File

@@ -10,9 +10,9 @@ __metaclass__ = type
DOCUMENTATION = '''
author: Matt Clay (@mattclay) <matt@mystile.com>
name: lxd
short_description: Run tasks in lxc containers via lxc CLI
short_description: Run tasks in LXD instances via C(lxc) CLI
description:
- Run commands or put/fetch files to an existing lxc container using lxc CLI
- Run commands or put/fetch files to an existing instance using C(lxc) CLI.
options:
remote_addr:
description:
@@ -26,7 +26,7 @@ DOCUMENTATION = '''
- name: ansible_lxd_host
executable:
description:
- shell to use for execution inside container
- Shell to use for execution inside instance.
default: /bin/sh
vars:
- name: ansible_executable
@@ -71,7 +71,7 @@ class Connection(ConnectionBase):
raise AnsibleError("lxc command not found in PATH")
if self._play_context.remote_user is not None and self._play_context.remote_user != 'root':
self._display.warning('lxd does not support remote_user, using container default: root')
self._display.warning('lxd does not support remote_user, using default: root')
def _host(self):
""" translate remote_addr to lxd (short) hostname """

View File

@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright (c) Ansible project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
class ModuleDocFragment:
# Common parameters for Consul modules
DOCUMENTATION = r"""
options:
host:
description:
- Host of the consul agent, defaults to V(localhost).
default: localhost
type: str
port:
type: int
description:
- The port on which the consul agent is running.
default: 8500
scheme:
description:
- The protocol scheme on which the consul agent is running.
Defaults to V(http) and can be set to V(https) for secure connections.
default: http
type: str
validate_certs:
type: bool
description:
- Whether to verify the TLS certificate of the consul agent.
default: true
ca_path:
description:
- The CA bundle to use for https connections
type: str
"""
TOKEN = r"""
options:
token:
description:
- The token to use for authorization.
type: str
"""
ACTIONGROUP_CONSUL = r"""
options: {}
attributes:
action_group:
description: Use C(group/community.general.consul) in C(module_defaults) to set defaults for this module.
support: full
membership:
- community.general.consul
version_added: 8.3.0
"""

View File

@@ -470,7 +470,7 @@ class InventoryModule(BaseInventoryPlugin):
Helper to get the preferred interface provide by neme pattern from 'prefered_instance_network_interface'.
Args:
str(containe_name): name of instance
str(instance_name): name of instance
Kwargs:
None
Raises:
@@ -495,7 +495,7 @@ class InventoryModule(BaseInventoryPlugin):
Helper to get the VLAN_ID from the instance
Args:
str(containe_name): name of instance
str(instance_name): name of instance
Kwargs:
None
Raises:

View File

@@ -5,25 +5,317 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import copy
import json
from ansible.module_utils.six.moves.urllib import error as urllib_error
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import open_url
def get_consul_url(configuration):
return '%s://%s:%s/v1' % (configuration.scheme,
configuration.host, configuration.port)
return "%s://%s:%s/v1" % (
configuration.scheme,
configuration.host,
configuration.port,
)
def get_auth_headers(configuration):
if configuration.token is None:
return {}
else:
return {'X-Consul-Token': configuration.token}
return {"X-Consul-Token": configuration.token}
class RequestError(Exception):
pass
def __init__(self, status, response_data=None):
self.status = status
self.response_data = response_data
def __str__(self):
if self.response_data is None:
# self.status is already the message (backwards compat)
return self.status
return "HTTP %d: %s" % (self.status, self.response_data)
def handle_consul_response_error(response):
if 400 <= response.status_code < 600:
raise RequestError('%d %s' % (response.status_code, response.content))
raise RequestError("%d %s" % (response.status_code, response.content))
AUTH_ARGUMENTS_SPEC = dict(
host=dict(default="localhost"),
port=dict(type="int", default=8500),
scheme=dict(default="http"),
validate_certs=dict(type="bool", default=True),
token=dict(no_log=True),
ca_path=dict(),
)
def camel_case_key(key):
parts = []
for part in key.split("_"):
if part in {"id", "ttl", "jwks", "jwt", "oidc", "iam", "sts"}:
parts.append(part.upper())
else:
parts.append(part.capitalize())
return "".join(parts)
STATE_PARAMETER = "state"
STATE_PRESENT = "present"
STATE_ABSENT = "absent"
OPERATION_READ = "read"
OPERATION_CREATE = "create"
OPERATION_UPDATE = "update"
OPERATION_DELETE = "remove"
def _normalize_params(params, arg_spec):
final_params = {}
for k, v in params.items():
if k not in arg_spec: # Alias
continue
spec = arg_spec[k]
if (
spec.get("type") == "list"
and spec.get("elements") == "dict"
and spec.get("options")
and v
):
v = [_normalize_params(d, spec["options"]) for d in v]
elif spec.get("type") == "dict" and spec.get("options") and v:
v = _normalize_params(v, spec["options"])
final_params[k] = v
return final_params
class _ConsulModule:
"""Base class for Consul modules.
This class is considered private, till the API is fully fleshed out.
As such backwards incompatible changes can occur even in bugfix releases.
"""
api_endpoint = None # type: str
unique_identifier = None # type: str
result_key = None # type: str
create_only_fields = set()
params = {}
def __init__(self, module):
self._module = module
self.params = _normalize_params(module.params, module.argument_spec)
self.api_params = {
k: camel_case_key(k)
for k in self.params
if k not in STATE_PARAMETER and k not in AUTH_ARGUMENTS_SPEC
}
def execute(self):
obj = self.read_object()
changed = False
diff = {}
if self.params[STATE_PARAMETER] == STATE_PRESENT:
obj_from_module = self.module_to_obj(obj is not None)
if obj is None:
operation = OPERATION_CREATE
new_obj = self.create_object(obj_from_module)
diff = {"before": {}, "after": new_obj}
changed = True
else:
operation = OPERATION_UPDATE
if self._needs_update(obj, obj_from_module):
new_obj = self.update_object(obj, obj_from_module)
diff = {"before": obj, "after": new_obj}
changed = True
else:
new_obj = obj
elif self.params[STATE_PARAMETER] == STATE_ABSENT:
operation = OPERATION_DELETE
if obj is not None:
self.delete_object(obj)
changed = True
diff = {"before": obj, "after": {}}
else:
diff = {"before": {}, "after": {}}
new_obj = None
else:
raise RuntimeError("Unknown state supplied.")
result = {"changed": changed}
if changed:
result["operation"] = operation
if self._module._diff:
result["diff"] = diff
if self.result_key:
result[self.result_key] = new_obj
self._module.exit_json(**result)
def module_to_obj(self, is_update):
obj = {}
for k, v in self.params.items():
result = self.map_param(k, v, is_update)
if result:
obj[result[0]] = result[1]
return obj
def map_param(self, k, v, is_update):
def helper(item):
return {camel_case_key(k): v for k, v in item.items()}
def needs_camel_case(k):
spec = self._module.argument_spec[k]
return (
spec.get("type") == "list"
and spec.get("elements") == "dict"
and spec.get("options")
) or (spec.get("type") == "dict" and spec.get("options"))
if k in self.api_params and v is not None:
if isinstance(v, dict) and needs_camel_case(k):
v = helper(v)
elif isinstance(v, (list, tuple)) and needs_camel_case(k):
v = [helper(i) for i in v]
if is_update and k in self.create_only_fields:
return
return camel_case_key(k), v
def _needs_update(self, api_obj, module_obj):
api_obj = copy.deepcopy(api_obj)
module_obj = copy.deepcopy(module_obj)
return self.needs_update(api_obj, module_obj)
def needs_update(self, api_obj, module_obj):
for k, v in module_obj.items():
if k not in api_obj:
return True
if api_obj[k] != v:
return True
return False
def prepare_object(self, existing, obj):
operational_attributes = {"CreateIndex", "CreateTime", "Hash", "ModifyIndex"}
existing = {
k: v for k, v in existing.items() if k not in operational_attributes
}
for k, v in obj.items():
existing[k] = v
return existing
def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_CREATE:
return self.api_endpoint
elif identifier:
return "/".join([self.api_endpoint, identifier])
raise RuntimeError("invalid arguments passed")
def read_object(self):
url = self.endpoint_url(OPERATION_READ, self.params.get(self.unique_identifier))
try:
return self.get(url)
except RequestError as e:
if e.status == 404:
return
elif e.status == 403 and b"ACL not found" in e.response_data:
return
raise
def create_object(self, obj):
if self._module.check_mode:
return obj
else:
return self.put(self.api_endpoint, data=self.prepare_object({}, obj))
def update_object(self, existing, obj):
url = self.endpoint_url(
OPERATION_UPDATE, existing.get(camel_case_key(self.unique_identifier))
)
merged_object = self.prepare_object(existing, obj)
if self._module.check_mode:
return merged_object
else:
return self.put(url, data=merged_object)
def delete_object(self, obj):
if self._module.check_mode:
return {}
else:
url = self.endpoint_url(
OPERATION_DELETE, obj.get(camel_case_key(self.unique_identifier))
)
return self.delete(url)
def _request(self, method, url_parts, data=None, params=None):
module_params = self.params
if not isinstance(url_parts, (tuple, list)):
url_parts = [url_parts]
if params:
# Remove values that are None
params = {k: v for k, v in params.items() if v is not None}
ca_path = module_params.get("ca_path")
base_url = "%s://%s:%s/v1" % (
module_params["scheme"],
module_params["host"],
module_params["port"],
)
url = "/".join([base_url] + list(url_parts))
headers = {}
token = self.params.get("token")
if token:
headers["X-Consul-Token"] = token
try:
if data:
data = json.dumps(data)
headers["Content-Type"] = "application/json"
if params:
url = "%s?%s" % (url, urlencode(params))
response = open_url(
url,
method=method,
data=data,
headers=headers,
validate_certs=module_params["validate_certs"],
ca_path=ca_path,
)
response_data = response.read()
status = (
response.status if hasattr(response, "status") else response.getcode()
)
except urllib_error.URLError as e:
if isinstance(e, urllib_error.HTTPError):
status = e.code
response_data = e.fp.read()
else:
self._module.fail_json(
msg="Could not connect to consul agent at %s:%s, error was %s"
% (module_params["host"], module_params["port"], str(e))
)
raise
if 400 <= status < 600:
raise RequestError(status, response_data)
return json.loads(response_data)
def get(self, url_parts, **kwargs):
return self._request("GET", url_parts, **kwargs)
def put(self, url_parts, **kwargs):
return self._request("PUT", url_parts, **kwargs)
def delete(self, url_parts, **kwargs):
return self._request("DELETE", url_parts, **kwargs)

View File

@@ -59,11 +59,11 @@ def find_project(gitlab_instance, identifier):
def find_group(gitlab_instance, identifier):
try:
project = gitlab_instance.groups.get(identifier)
group = gitlab_instance.groups.get(identifier)
except Exception as e:
return None
return project
return group
def ensure_gitlab_package(module):

View File

@@ -139,5 +139,7 @@ class LdapGeneric(object):
def _xorder_dn(self):
# match X_ORDERed DNs
regex = r"\w+=\{\d+\}.+"
return re.match(regex, self.module.params['dn']) is not None
regex = r".+\{\d+\}.+"
explode_dn = ldap.dn.explode_dn(self.module.params['dn'])
return re.match(regex, explode_dn[0]) is not None

View File

@@ -3365,7 +3365,8 @@ class RedfishUtils(object):
inventory = {}
# Get these entries, but does not fail if not found
properties = ['Id', 'FirmwareVersion', 'ManagerType', 'Manufacturer', 'Model',
'PartNumber', 'PowerState', 'SerialNumber', 'Status', 'UUID']
'PartNumber', 'PowerState', 'SerialNumber', 'ServiceIdentification',
'Status', 'UUID']
response = self.get_request(self.root_uri + manager_uri)
if response['ret'] is False:
@@ -3383,6 +3384,30 @@ class RedfishUtils(object):
def get_multi_manager_inventory(self):
return self.aggregate_managers(self.get_manager_inventory)
def get_service_identification(self, manager):
result = {}
if manager is None:
if len(self.manager_uris) == 1:
manager = self.manager_uris[0].split('/')[-1]
elif len(self.manager_uris) > 1:
entries = self.get_multi_manager_inventory()['entries']
managers = [m[0]['manager_uri'] for m in entries if m[1].get('ServiceIdentification')]
if len(managers) == 1:
manager = managers[0].split('/')[-1]
else:
self.module.fail_json(msg=[
"Multiple managers with ServiceIdentification were found: %s" % str(managers),
"Please specify by using the 'manager' parameter in your playbook"])
elif len(self.manager_uris) == 0:
self.module.fail_json(msg="No manager identities were found")
response = self.get_request(self.root_uri + '/redfish/v1/Managers/' + manager, override_headers=None)
try:
result['service_identification'] = response['data']['ServiceIdentification']
except Exception as e:
self.module.fail_json(msg="Service ID not found for manager %s" % manager)
result['ret'] = True
return result
def set_session_service(self, sessions_config):
if sessions_config is None:
return {'ret': False, 'msg':

View File

@@ -26,6 +26,10 @@ attributes:
support: none
diff_mode:
support: none
deprecated:
removed_in: 10.0.0
why: The legacy ACL system was removed from Consul.
alternative: Use M(community.general.consul_token) and/or M(community.general.consul_policy) instead.
options:
mgmt_token:
description:

View File

@@ -0,0 +1,108 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_acl_bootstrap
short_description: Bootstrap ACLs in Consul
version_added: 8.3.0
description:
- Allows bootstrapping of ACLs in a Consul cluster, see
U(https://developer.hashicorp.com/consul/api-docs/acl#bootstrap-acls) for details.
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.consul
- community.general.attributes
attributes:
check_mode:
support: none
diff_mode:
support: none
options:
state:
description:
- Whether the token should be present or absent.
choices: ['present', 'bootstrapped']
default: present
type: str
bootstrap_secret:
description:
- The secret to be used as secret ID for the initial token.
- Needs to be an UUID.
type: str
"""
EXAMPLES = """
- name: Bootstrap the ACL system
community.general.consul_acl_bootstrap:
bootstrap_secret: 22eaeed1-bdbd-4651-724e-42ae6c43e387
"""
RETURN = """
result:
description:
- The bootstrap result as returned by the consul HTTP API.
- "B(Note:) If O(bootstrap_secret) has been specified the C(SecretID) and
C(ID) will not contain the secret but C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER).
If you pass O(bootstrap_secret), make sure your playbook/role does not depend
on this return value!"
returned: changed
type: dict
sample:
AccessorID: 834a5881-10a9-a45b-f63c-490e28743557
CreateIndex: 25
CreateTime: '2024-01-21T20:26:27.114612038+01:00'
Description: Bootstrap Token (Global Management)
Hash: X2AgaFhnQGRhSSF/h0m6qpX1wj/HJWbyXcxkEM/5GrY=
ID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
Local: false
ModifyIndex: 25
Policies:
- ID: 00000000-0000-0000-0000-000000000001
Name: global-management
SecretID: VALUE_SPECIFIED_IN_NO_LOG_PARAMETER
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
RequestError,
_ConsulModule,
)
_ARGUMENT_SPEC = {
"state": dict(type="str", choices=["present", "bootstrapped"], default="present"),
"bootstrap_secret": dict(type="str", no_log=True),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
_ARGUMENT_SPEC.pop("token")
def main():
module = AnsibleModule(_ARGUMENT_SPEC)
consul_module = _ConsulModule(module)
data = {}
if "bootstrap_secret" in module.params:
data["BootstrapSecret"] = module.params["bootstrap_secret"]
try:
response = consul_module.put("acl/bootstrap", data=data)
except RequestError as e:
if e.status == 403 and b"ACL bootstrap no longer allowed" in e.response_data:
return module.exit_json(changed=False)
raise
else:
return module.exit_json(changed=True, result=response)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,207 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_auth_method
short_description: Manipulate Consul auth methods
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of auth methods in a consul
cluster via the agent. For more details on using and configuring ACLs,
see U(https://www.consul.io/docs/guides/acl.html).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.actiongroup_consul
- community.general.consul.token
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: partial
details:
- In check mode the diff will miss operational attributes.
options:
state:
description:
- Whether the token should be present or absent.
choices: ['present', 'absent']
default: present
type: str
name:
description:
- Specifies a name for the ACL auth method.
- The name can contain alphanumeric characters, dashes C(-), and underscores C(_).
type: str
required: true
type:
description:
- The type of auth method being configured.
- This field is immutable.
- Required when the auth method is created.
type: str
choices: ['kubernetes', 'jwt', 'oidc', 'aws-iam']
description:
description:
- Free form human readable description of the auth method.
type: str
display_name:
description:
- An optional name to use instead of O(name) when displaying information about this auth method.
type: str
max_token_ttl:
description:
- This specifies the maximum life of any token created by this auth method.
- Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes, respectively).
type: str
token_locality:
description:
- Defines the kind of token that this auth method should produce.
type: str
choices: ['local', 'global']
config:
description:
- The raw configuration to use for the chosen auth method.
- Contents will vary depending upon the type chosen.
- Required when the auth method is created.
type: dict
"""
EXAMPLES = """
- name: Create an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
token: "{{ consul_management_token }}"
- name: Delete auth method
community.general.consul_auth_method:
name: test
state: absent
token: "{{ consul_management_token }}"
"""
RETURN = """
auth_method:
description: The auth method as returned by the consul HTTP API.
returned: always
type: dict
sample:
Config:
JWTValidationPubkeys:
- |-
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
CreateIndex: 416
ModifyIndex: 487
Name: test
Type: jwt
operation:
description: The operation performed.
returned: changed
type: str
sample: update
"""
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
_ConsulModule,
camel_case_key,
)
def normalize_ttl(ttl):
matches = re.findall(r"(\d+)(:h|m|s)", ttl)
ttl = 0
for value, unit in matches:
value = int(value)
if unit == "m":
value *= 60
elif unit == "h":
value *= 60 * 60
ttl += value
new_ttl = ""
hours, remainder = divmod(ttl, 3600)
if hours:
new_ttl += "{0}h".format(hours)
minutes, seconds = divmod(remainder, 60)
if minutes:
new_ttl += "{0}m".format(minutes)
if seconds:
new_ttl += "{0}s".format(seconds)
return new_ttl
class ConsulAuthMethodModule(_ConsulModule):
api_endpoint = "acl/auth-method"
result_key = "auth_method"
unique_identifier = "name"
def map_param(self, k, v, is_update):
if k == "config" and v:
v = {camel_case_key(k2): v2 for k2, v2 in v.items()}
return super(ConsulAuthMethodModule, self).map_param(k, v, is_update)
def needs_update(self, api_obj, module_obj):
if "MaxTokenTTL" in module_obj:
module_obj["MaxTokenTTL"] = normalize_ttl(module_obj["MaxTokenTTL"])
return super(ConsulAuthMethodModule, self).needs_update(api_obj, module_obj)
_ARGUMENT_SPEC = {
"name": dict(type="str", required=True),
"type": dict(type="str", choices=["kubernetes", "jwt", "oidc", "aws-iam"]),
"description": dict(type="str"),
"display_name": dict(type="str"),
"max_token_ttl": dict(type="str", no_log=False),
"token_locality": dict(type="str", choices=["local", "global"]),
"config": dict(type="dict"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
consul_module = ConsulAuthMethodModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,183 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_binding_rule
short_description: Manipulate Consul binding rules
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of binding rules in a consul
cluster via the agent. For more details on using and configuring binding rules,
see U(https://developer.hashicorp.com/consul/api-docs/acl/binding-rules).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.actiongroup_consul
- community.general.consul.token
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: partial
details:
- In check mode the diff will miss operational attributes.
options:
state:
description:
- Whether the binding rule should be present or absent.
choices: ['present', 'absent']
default: present
type: str
name:
description:
- Specifies a name for the binding rule.
- 'Note: This is used to identify the binding rule. But since the API does not support a name, it is prefixed to the description.'
type: str
required: true
description:
description:
- Free form human readable description of the binding rule.
type: str
auth_method:
description:
- The name of the auth method that this rule applies to.
type: str
required: true
selector:
description:
- Specifies the expression used to match this rule against valid identities returned from an auth method validation.
- If empty this binding rule matches all valid identities returned from the auth method.
type: str
bind_type:
description:
- Specifies the way the binding rule affects a token created at login.
type: str
choices: [service, node, role, templated-policy]
bind_name:
description:
- The name to bind to a token at login-time.
- What it binds to can be adjusted with different values of the O(bind_type) parameter.
type: str
bind_vars:
description:
- Specifies the templated policy variables when O(bind_type) is set to V(templated-policy).
type: dict
"""
EXAMPLES = """
- name: Create a binding rule
community.general.consul_binding_rule:
name: my_name
description: example rule
auth_method: minikube
bind_type: service
bind_name: "{{ serviceaccount.name }}"
token: "{{ consul_management_token }}"
- name: Remove a binding rule
community.general.consul_binding_rule:
name: my_name
auth_method: minikube
state: absent
"""
RETURN = """
binding_rule:
description: The binding rule as returned by the consul HTTP API.
returned: always
type: dict
sample:
Description: "my_name: example rule"
AuthMethod: minikube
Selector: serviceaccount.namespace==default
BindType: service
BindName: "{{ serviceaccount.name }}"
CreateIndex: 30
ID: 59c8a237-e481-4239-9202-45f117950c5f
ModifyIndex: 33
operation:
description: The operation performed.
returned: changed
type: str
sample: update
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
RequestError,
_ConsulModule,
)
class ConsulBindingRuleModule(_ConsulModule):
api_endpoint = "acl/binding-rule"
result_key = "binding_rule"
unique_identifier = "id"
def read_object(self):
url = "acl/binding-rules?authmethod={0}".format(self.params["auth_method"])
try:
results = self.get(url)
for result in results:
if result.get("Description").startswith(
"{0}: ".format(self.params["name"])
):
return result
except RequestError as e:
if e.status == 404:
return
elif e.status == 403 and b"ACL not found" in e.response_data:
return
raise
def module_to_obj(self, is_update):
obj = super(ConsulBindingRuleModule, self).module_to_obj(is_update)
del obj["Name"]
return obj
def prepare_object(self, existing, obj):
final = super(ConsulBindingRuleModule, self).prepare_object(existing, obj)
name = self.params["name"]
description = final.pop("Description", "").split(": ", 1)[-1]
final["Description"] = "{0}: {1}".format(name, description)
return final
_ARGUMENT_SPEC = {
"name": dict(type="str", required=True),
"description": dict(type="str"),
"auth_method": dict(type="str", required=True),
"selector": dict(type="str"),
"bind_type": dict(
type="str", choices=["service", "node", "role", "templated-policy"]
),
"bind_name": dict(type="str"),
"bind_vars": dict(type="dict"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
consul_module = ConsulBindingRuleModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View File

@@ -6,9 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: consul_policy
short_description: Manipulate Consul policies
version_added: 7.2.0
@@ -19,24 +20,29 @@ description:
author:
- Håkon Lerring (@Hakon)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.actiongroup_consul
- community.general.consul.token
- community.general.attributes
attributes:
check_mode:
support: none
support: full
version_added: 8.3.0
diff_mode:
support: none
support: partial
version_added: 8.3.0
details:
- In check mode the diff will miss operational attributes.
options:
state:
description:
- Whether the policy should be present or absent.
required: false
choices: ['present', 'absent']
default: present
type: str
valid_datacenters:
description:
- Valid datacenters for the policy. All if list is empty.
default: []
type: list
elements: str
name:
@@ -48,45 +54,12 @@ options:
description:
description:
- Description of the policy.
required: false
type: str
default: ''
rules:
type: str
description:
- Rule document that should be associated with the current policy.
required: false
host:
description:
- Host of the consul agent, defaults to localhost.
required: false
default: localhost
type: str
port:
type: int
description:
- The port on which the consul agent is running.
required: false
default: 8500
scheme:
description:
- The protocol scheme on which the consul agent is running.
required: false
default: http
type: str
token:
description:
- A management token is required to manipulate the policies.
type: str
validate_certs:
type: bool
description:
- Whether to verify the TLS certificate of the consul agent or not.
required: false
default: true
requirements:
- requests
'''
"""
EXAMPLES = """
- name: Create a policy with rules
@@ -127,246 +100,64 @@ EXAMPLES = """
"""
RETURN = """
policy:
description: The policy as returned by the consul HTTP API.
returned: always
type: dict
sample:
CreateIndex: 632
Description: Testing
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
Name: foo-access
Rules: |-
key "foo" {
policy = "read"
}
key "private/foo" {
policy = "deny"
}
operation:
description: The operation performed on the policy.
description: The operation performed.
returned: changed
type: str
sample: update
"""
from ansible.module_utils.basic import AnsibleModule
try:
from requests.exceptions import ConnectionError
import requests
has_requests = True
except ImportError:
has_requests = False
TOKEN_PARAMETER_NAME = "token"
HOST_PARAMETER_NAME = "host"
SCHEME_PARAMETER_NAME = "scheme"
VALIDATE_CERTS_PARAMETER_NAME = "validate_certs"
NAME_PARAMETER_NAME = "name"
DESCRIPTION_PARAMETER_NAME = "description"
PORT_PARAMETER_NAME = "port"
RULES_PARAMETER_NAME = "rules"
VALID_DATACENTERS_PARAMETER_NAME = "valid_datacenters"
STATE_PARAMETER_NAME = "state"
PRESENT_STATE_VALUE = "present"
ABSENT_STATE_VALUE = "absent"
REMOVE_OPERATION = "remove"
UPDATE_OPERATION = "update"
CREATE_OPERATION = "create"
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
OPERATION_READ,
_ConsulModule,
)
_ARGUMENT_SPEC = {
NAME_PARAMETER_NAME: dict(required=True),
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=''),
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
RULES_PARAMETER_NAME: dict(type='str'),
VALID_DATACENTERS_PARAMETER_NAME: dict(type='list', elements='str', default=[]),
HOST_PARAMETER_NAME: dict(default='localhost'),
SCHEME_PARAMETER_NAME: dict(default='http'),
TOKEN_PARAMETER_NAME: dict(no_log=True),
VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True),
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
"name": dict(required=True),
"description": dict(required=False, type="str"),
"rules": dict(type="str"),
"valid_datacenters": dict(type="list", elements="str"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def get_consul_url(configuration):
return '%s://%s:%s/v1' % (configuration.scheme,
configuration.host, configuration.port)
class ConsulPolicyModule(_ConsulModule):
api_endpoint = "acl/policy"
result_key = "policy"
unique_identifier = "id"
def get_auth_headers(configuration):
if configuration.token is None:
return {}
else:
return {'X-Consul-Token': configuration.token}
class RequestError(Exception):
pass
def handle_consul_response_error(response):
if 400 <= response.status_code < 600:
raise RequestError('%d %s' % (response.status_code, response.content))
def update_policy(policy, configuration):
url = '%s/acl/policy/%s' % (get_consul_url(configuration), policy['ID'])
headers = get_auth_headers(configuration)
response = requests.put(url, headers=headers, json={
'Name': configuration.name, # should be equal at this point.
'Description': configuration.description,
'Rules': configuration.rules,
'Datacenters': configuration.valid_datacenters
}, verify=configuration.validate_certs)
handle_consul_response_error(response)
updated_policy = response.json()
changed = (
policy.get('Rules', "") != updated_policy.get('Rules', "") or
policy.get('Description', "") != updated_policy.get('Description', "") or
policy.get('Datacenters', []) != updated_policy.get('Datacenters', [])
)
return Output(changed=changed, operation=UPDATE_OPERATION, policy=updated_policy)
def create_policy(configuration):
url = '%s/acl/policy' % get_consul_url(configuration)
headers = get_auth_headers(configuration)
response = requests.put(url, headers=headers, json={
'Name': configuration.name,
'Description': configuration.description,
'Rules': configuration.rules,
'Datacenters': configuration.valid_datacenters
}, verify=configuration.validate_certs)
handle_consul_response_error(response)
created_policy = response.json()
return Output(changed=True, operation=CREATE_OPERATION, policy=created_policy)
def remove_policy(configuration):
policies = get_policies(configuration)
if configuration.name in policies:
policy_id = policies[configuration.name]['ID']
policy = get_policy(policy_id, configuration)
url = '%s/acl/policy/%s' % (get_consul_url(configuration),
policy['ID'])
headers = get_auth_headers(configuration)
response = requests.delete(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
changed = True
else:
changed = False
return Output(changed=changed, operation=REMOVE_OPERATION)
def get_policies(configuration):
url = '%s/acl/policies' % get_consul_url(configuration)
headers = get_auth_headers(configuration)
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
policies = response.json()
existing_policies_mapped_by_name = dict(
(policy['Name'], policy) for policy in policies if policy['Name'] is not None)
return existing_policies_mapped_by_name
def get_policy(id, configuration):
url = '%s/acl/policy/%s' % (get_consul_url(configuration), id)
headers = get_auth_headers(configuration)
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
return response.json()
def set_policy(configuration):
policies = get_policies(configuration)
if configuration.name in policies:
index_policy_object = policies[configuration.name]
policy_id = policies[configuration.name]['ID']
rest_policy_object = get_policy(policy_id, configuration)
# merge dicts as some keys are only available in the partial policy
policy = index_policy_object.copy()
policy.update(rest_policy_object)
return update_policy(policy, configuration)
else:
return create_policy(configuration)
class Configuration:
"""
Configuration for this module.
"""
def __init__(self, token=None, host=None, scheme=None, validate_certs=None, name=None, description=None, port=None,
rules=None, valid_datacenters=None, state=None):
self.token = token # type: str
self.host = host # type: str
self.scheme = scheme # type: str
self.validate_certs = validate_certs # type: bool
self.name = name # type: str
self.description = description # type: str
self.port = port # type: int
self.rules = rules # type: str
self.valid_datacenters = valid_datacenters # type: str
self.state = state # type: str
class Output:
"""
Output of an action of this module.
"""
def __init__(self, changed=None, operation=None, policy=None):
self.changed = changed # type: bool
self.operation = operation # type: str
self.policy = policy # type: dict
def check_dependencies():
"""
Checks that the required dependencies have been imported.
:exception ImportError: if it is detected that any of the required dependencies have not been imported
"""
if not has_requests:
raise ImportError(
"requests required for this module. See https://pypi.org/project/requests/")
def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_READ:
return [self.api_endpoint, "name", self.params["name"]]
return super(ConsulPolicyModule, self).endpoint_url(operation, identifier)
def main():
"""
Main method.
"""
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=False)
try:
check_dependencies()
except ImportError as e:
module.fail_json(msg=str(e))
configuration = Configuration(
token=module.params.get(TOKEN_PARAMETER_NAME),
host=module.params.get(HOST_PARAMETER_NAME),
scheme=module.params.get(SCHEME_PARAMETER_NAME),
validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME),
name=module.params.get(NAME_PARAMETER_NAME),
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
port=module.params.get(PORT_PARAMETER_NAME),
rules=module.params.get(RULES_PARAMETER_NAME),
valid_datacenters=module.params.get(VALID_DATACENTERS_PARAMETER_NAME),
state=module.params.get(STATE_PARAMETER_NAME),
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
try:
if configuration.state == PRESENT_STATE_VALUE:
output = set_policy(configuration)
else:
output = remove_policy(configuration)
except ConnectionError as e:
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
configuration.host, configuration.port, str(e)))
raise
return_values = dict(changed=output.changed, operation=output.operation, policy=output.policy)
module.exit_json(**return_values)
consul_module = ConsulPolicyModule(module)
consul_module.execute()
if __name__ == "__main__":

View File

@@ -6,9 +6,10 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: consul_role
short_description: Manipulate Consul roles
version_added: 7.5.0
@@ -19,12 +20,18 @@ description:
author:
- Håkon Lerring (@Hakon)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.token
- community.general.consul.actiongroup_consul
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
support: partial
details:
- In check mode the diff will miss operational attributes.
version_added: 8.3.0
options:
name:
description:
@@ -34,7 +41,6 @@ options:
state:
description:
- whether the role should be present or absent.
required: false
choices: ['present', 'absent']
default: present
type: str
@@ -42,7 +48,6 @@ options:
description:
- Description of the role.
- If not specified, the assigned description will not be changed.
required: false
type: str
policies:
type: list
@@ -51,7 +56,6 @@ options:
- List of policies to attach to the role. Each policy is a dict.
- If the parameter is left blank, any policies currently assigned will not be changed.
- Any empty array (V([])) will clear any policies previously set.
required: false
suboptions:
name:
description:
@@ -63,6 +67,23 @@ options:
- The ID of the policy to attach to this role; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].name) must be specified.
type: str
templated_policies:
description:
- The list of templated policies that should be applied to the role.
type: list
elements: dict
version_added: 8.3.0
suboptions:
template_name:
description:
- The templated policy name.
type: str
required: true
template_variables:
description:
- The templated policy variables.
- Not all templated policies require variables.
type: dict
service_identities:
type: list
elements: dict
@@ -70,15 +91,18 @@ options:
- List of service identities to attach to the role.
- If not specified, any service identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
required: false
suboptions:
name:
service_name:
description:
- The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as - and _.
- This suboption has been renamed from O(service_identities[].name) to O(service_identities[].service_name)
in community.general 8.3.0. The old name can still be used.
type: str
required: true
aliases:
- name
datacenters:
description:
- The datacenters the policies will be effective.
@@ -87,7 +111,6 @@ options:
- including those which do not yet exist but may in the future.
type: list
elements: str
required: true
node_identities:
type: list
elements: dict
@@ -95,52 +118,25 @@ options:
- List of node identities to attach to the role.
- If not specified, any node identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
required: false
suboptions:
name:
node_name:
description:
- The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as - and _.
- This suboption has been renamed from O(node_identities[].name) to O(node_identities[].node_name)
in community.general 8.3.0. The old name can still be used.
type: str
required: true
aliases:
- name
datacenter:
description:
- The nodes datacenter.
- This will result in effective policy only being valid in this datacenter.
type: str
required: true
host:
description:
- Host of the consul agent, defaults to V(localhost).
required: false
default: localhost
type: str
port:
type: int
description:
- The port on which the consul agent is running.
required: false
default: 8500
scheme:
description:
- The protocol scheme on which the consul agent is running.
required: false
default: http
type: str
token:
description:
- A management token is required to manipulate the roles.
type: str
validate_certs:
type: bool
description:
- Whether to verify the TLS certificate of the consul agent.
required: false
default: true
requirements:
- requests
'''
"""
EXAMPLES = """
- name: Create a role with 2 policies
@@ -204,440 +200,81 @@ operation:
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import missing_required_lib
from ansible_collections.community.general.plugins.module_utils.consul import (
get_consul_url, get_auth_headers, handle_consul_response_error)
import traceback
REQUESTS_IMP_ERR = None
try:
from requests.exceptions import ConnectionError
import requests
HAS_REQUESTS = True
except ImportError:
HAS_REQUESTS = False
REQUESTS_IMP_ERR = traceback.format_exc()
TOKEN_PARAMETER_NAME = "token"
HOST_PARAMETER_NAME = "host"
SCHEME_PARAMETER_NAME = "scheme"
VALIDATE_CERTS_PARAMETER_NAME = "validate_certs"
NAME_PARAMETER_NAME = "name"
DESCRIPTION_PARAMETER_NAME = "description"
PORT_PARAMETER_NAME = "port"
POLICIES_PARAMETER_NAME = "policies"
SERVICE_IDENTITIES_PARAMETER_NAME = "service_identities"
NODE_IDENTITIES_PARAMETER_NAME = "node_identities"
STATE_PARAMETER_NAME = "state"
PRESENT_STATE_VALUE = "present"
ABSENT_STATE_VALUE = "absent"
REMOVE_OPERATION = "remove"
UPDATE_OPERATION = "update"
CREATE_OPERATION = "create"
POLICY_RULE_SPEC = dict(
name=dict(type='str'),
id=dict(type='str'),
AUTH_ARGUMENTS_SPEC,
OPERATION_READ,
_ConsulModule,
)
NODE_ID_RULE_SPEC = dict(
name=dict(type='str', required=True),
datacenter=dict(type='str', required=True),
class ConsulRoleModule(_ConsulModule):
api_endpoint = "acl/role"
result_key = "role"
unique_identifier = "id"
def endpoint_url(self, operation, identifier=None):
if operation == OPERATION_READ:
return [self.api_endpoint, "name", self.params["name"]]
return super(ConsulRoleModule, self).endpoint_url(operation, identifier)
NAME_ID_SPEC = dict(
name=dict(type="str"),
id=dict(type="str"),
)
SERVICE_ID_RULE_SPEC = dict(
name=dict(type='str', required=True),
datacenters=dict(type='list', elements='str', required=True),
NODE_ID_SPEC = dict(
node_name=dict(type="str", required=True, aliases=["name"]),
datacenter=dict(type="str", required=True),
)
SERVICE_ID_SPEC = dict(
service_name=dict(type="str", required=True, aliases=["name"]),
datacenters=dict(type="list", elements="str"),
)
TEMPLATE_POLICY_SPEC = dict(
template_name=dict(type="str", required=True),
template_variables=dict(type="dict"),
)
_ARGUMENT_SPEC = {
TOKEN_PARAMETER_NAME: dict(no_log=True),
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
HOST_PARAMETER_NAME: dict(default='localhost'),
SCHEME_PARAMETER_NAME: dict(default='http'),
VALIDATE_CERTS_PARAMETER_NAME: dict(type='bool', default=True),
NAME_PARAMETER_NAME: dict(required=True),
DESCRIPTION_PARAMETER_NAME: dict(required=False, type='str', default=None),
POLICIES_PARAMETER_NAME: dict(type='list', elements='dict', options=POLICY_RULE_SPEC,
mutually_exclusive=[('name', 'id')], required_one_of=[('name', 'id')], default=None),
SERVICE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=SERVICE_ID_RULE_SPEC, default=None),
NODE_IDENTITIES_PARAMETER_NAME: dict(type='list', elements='dict', options=NODE_ID_RULE_SPEC, default=None),
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
"name": dict(type="str", required=True),
"description": dict(type="str"),
"policies": dict(
type="list",
elements="dict",
options=NAME_ID_SPEC,
mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"templated_policies": dict(
type="list",
elements="dict",
options=TEMPLATE_POLICY_SPEC,
),
"node_identities": dict(
type="list",
elements="dict",
options=NODE_ID_SPEC,
),
"service_identities": dict(
type="list",
elements="dict",
options=SERVICE_ID_SPEC,
),
"state": dict(default="present", choices=["present", "absent"]),
}
def compare_consul_api_role_policy_objects(first, second):
# compare two lists of dictionaries, ignoring the ID element
for x in first:
x.pop('ID', None)
for x in second:
x.pop('ID', None)
return first == second
def update_role(role, configuration):
url = '%s/acl/role/%s' % (get_consul_url(configuration),
role['ID'])
headers = get_auth_headers(configuration)
update_role_data = {
'Name': configuration.name,
'Description': configuration.description,
}
# check if the user omitted the description, policies, service identities, or node identities
description_specified = configuration.description is not None
policy_specified = True
if len(configuration.policies) == 1 and configuration.policies[0] is None:
policy_specified = False
service_id_specified = True
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
service_id_specified = False
node_id_specified = True
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
node_id_specified = False
if description_specified:
update_role_data["Description"] = configuration.description
if policy_specified:
update_role_data["Policies"] = [x.to_dict() for x in configuration.policies]
if configuration.version >= ConsulVersion("1.5.0") and service_id_specified:
update_role_data["ServiceIdentities"] = [
x.to_dict() for x in configuration.service_identities]
if configuration.version >= ConsulVersion("1.8.0") and node_id_specified:
update_role_data["NodeIdentities"] = [
x.to_dict() for x in configuration.node_identities]
if configuration.check_mode:
description_changed = False
if description_specified:
description_changed = role.get('Description') != update_role_data["Description"]
else:
update_role_data["Description"] = role.get("Description")
policies_changed = False
if policy_specified:
policies_changed = not (
compare_consul_api_role_policy_objects(role.get('Policies', []), update_role_data.get('Policies', [])))
else:
if role.get('Policies') is not None:
update_role_data["Policies"] = role.get('Policies')
service_ids_changed = False
if service_id_specified:
service_ids_changed = role.get('ServiceIdentities') != update_role_data.get('ServiceIdentities')
else:
if role.get('ServiceIdentities') is not None:
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
node_ids_changed = False
if node_id_specified:
node_ids_changed = role.get('NodeIdentities') != update_role_data.get('NodeIdentities')
else:
if role.get('NodeIdentities'):
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
changed = (
description_changed or
policies_changed or
service_ids_changed or
node_ids_changed
)
return Output(changed=changed, operation=UPDATE_OPERATION, role=update_role_data)
else:
# if description, policies, service or node id are not specified; we need to get the existing value and apply it
if not description_specified and role.get('Description') is not None:
update_role_data["Description"] = role.get('Description')
if not policy_specified and role.get('Policies') is not None:
update_role_data["Policies"] = role.get('Policies')
if not service_id_specified and role.get('ServiceIdentities') is not None:
update_role_data["ServiceIdentities"] = role.get('ServiceIdentities')
if not node_id_specified and role.get('NodeIdentities') is not None:
update_role_data["NodeIdentities"] = role.get('NodeIdentities')
response = requests.put(url, headers=headers, json=update_role_data, verify=configuration.validate_certs)
handle_consul_response_error(response)
resulting_role = response.json()
changed = (
role['Description'] != resulting_role['Description'] or
role.get('Policies', None) != resulting_role.get('Policies', None) or
role.get('ServiceIdentities', None) != resulting_role.get('ServiceIdentities', None) or
role.get('NodeIdentities', None) != resulting_role.get('NodeIdentities', None)
)
return Output(changed=changed, operation=UPDATE_OPERATION, role=resulting_role)
def create_role(configuration):
url = '%s/acl/role' % get_consul_url(configuration)
headers = get_auth_headers(configuration)
# check if the user omitted policies, service identities, or node identities
policy_specified = True
if len(configuration.policies) == 1 and configuration.policies[0] is None:
policy_specified = False
service_id_specified = True
if len(configuration.service_identities) == 1 and configuration.service_identities[0] is None:
service_id_specified = False
node_id_specified = True
if len(configuration.node_identities) == 1 and configuration.node_identities[0] is None:
node_id_specified = False
# get rid of None item so we can set an empty list for policies, service identities and node identities
if not policy_specified:
configuration.policies.pop()
if not service_id_specified:
configuration.service_identities.pop()
if not node_id_specified:
configuration.node_identities.pop()
create_role_data = {
'Name': configuration.name,
'Description': configuration.description,
'Policies': [x.to_dict() for x in configuration.policies],
}
if configuration.version >= ConsulVersion("1.5.0"):
create_role_data["ServiceIdentities"] = [x.to_dict() for x in configuration.service_identities]
if configuration.version >= ConsulVersion("1.8.0"):
create_role_data["NodeIdentities"] = [x.to_dict() for x in configuration.node_identities]
if not configuration.check_mode:
response = requests.put(url, headers=headers, json=create_role_data, verify=configuration.validate_certs)
handle_consul_response_error(response)
resulting_role = response.json()
return Output(changed=True, operation=CREATE_OPERATION, role=resulting_role)
else:
return Output(changed=True, operation=CREATE_OPERATION)
def remove_role(configuration):
roles = get_roles(configuration)
if configuration.name in roles:
role_id = roles[configuration.name]['ID']
if not configuration.check_mode:
url = '%s/acl/role/%s' % (get_consul_url(configuration), role_id)
headers = get_auth_headers(configuration)
response = requests.delete(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
changed = True
else:
changed = False
return Output(changed=changed, operation=REMOVE_OPERATION)
def get_roles(configuration):
url = '%s/acl/roles' % get_consul_url(configuration)
headers = get_auth_headers(configuration)
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
roles = response.json()
existing_roles_mapped_by_id = dict((role['Name'], role) for role in roles if role['Name'] is not None)
return existing_roles_mapped_by_id
def get_consul_version(configuration):
url = '%s/agent/self' % get_consul_url(configuration)
headers = get_auth_headers(configuration)
response = requests.get(url, headers=headers, verify=configuration.validate_certs)
handle_consul_response_error(response)
config = response.json()["Config"]
return ConsulVersion(config["Version"])
def set_role(configuration):
roles = get_roles(configuration)
if configuration.name in roles:
role = roles[configuration.name]
return update_role(role, configuration)
else:
return create_role(configuration)
class ConsulVersion:
def __init__(self, version_string):
split = version_string.split('.')
self.major = split[0]
self.minor = split[1]
self.patch = split[2]
def __ge__(self, other):
return int(self.major + self.minor +
self.patch) >= int(other.major + other.minor + other.patch)
def __le__(self, other):
return int(self.major + self.minor +
self.patch) <= int(other.major + other.minor + other.patch)
class ServiceIdentity:
def __init__(self, input):
if not isinstance(input, dict) or 'name' not in input:
raise ValueError(
"Each element of service_identities must be a dict with the keys name and optionally datacenters")
self.name = input["name"]
self.datacenters = input["datacenters"] if "datacenters" in input else None
def to_dict(self):
return {
"ServiceName": self.name,
"Datacenters": self.datacenters
}
class NodeIdentity:
def __init__(self, input):
if not isinstance(input, dict) or 'name' not in input:
raise ValueError(
"Each element of node_identities must be a dict with the keys name and optionally datacenter")
self.name = input["name"]
self.datacenter = input["datacenter"] if "datacenter" in input else None
def to_dict(self):
return {
"NodeName": self.name,
"Datacenter": self.datacenter
}
class RoleLink:
def __init__(self, dict):
self.id = dict.get("id", None)
self.name = dict.get("name", None)
def to_dict(self):
return {
"ID": self.id,
"Name": self.name
}
class PolicyLink:
def __init__(self, dict):
self.id = dict.get("id", None)
self.name = dict.get("name", None)
def to_dict(self):
return {
"ID": self.id,
"Name": self.name
}
class Configuration:
"""
Configuration for this module.
"""
def __init__(self, token=None, host=None, scheme=None, validate_certs=None, name=None, description=None, port=None,
policies=None, service_identities=None, node_identities=None, state=None, check_mode=None):
self.token = token # type: str
self.host = host # type: str
self.port = port # type: int
self.scheme = scheme # type: str
self.validate_certs = validate_certs # type: bool
self.name = name # type: str
self.description = description # type: str
if policies is not None:
self.policies = [PolicyLink(p) for p in policies] # type: list(PolicyLink)
else:
self.policies = [None]
if service_identities is not None:
self.service_identities = [ServiceIdentity(s) for s in service_identities] # type: list(ServiceIdentity)
else:
self.service_identities = [None]
if node_identities is not None:
self.node_identities = [NodeIdentity(n) for n in node_identities] # type: list(NodeIdentity)
else:
self.node_identities = [None]
self.state = state # type: str
self.check_mode = check_mode # type: bool
class Output:
"""
Output of an action of this module.
"""
def __init__(self, changed=None, operation=None, role=None):
self.changed = changed # type: bool
self.operation = operation # type: str
self.role = role # type: dict
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
"""
Main method.
"""
module = AnsibleModule(_ARGUMENT_SPEC, supports_check_mode=True)
if not HAS_REQUESTS:
module.fail_json(msg=missing_required_lib("requests"),
exception=REQUESTS_IMP_ERR)
try:
configuration = Configuration(
token=module.params.get(TOKEN_PARAMETER_NAME),
host=module.params.get(HOST_PARAMETER_NAME),
port=module.params.get(PORT_PARAMETER_NAME),
scheme=module.params.get(SCHEME_PARAMETER_NAME),
validate_certs=module.params.get(VALIDATE_CERTS_PARAMETER_NAME),
name=module.params.get(NAME_PARAMETER_NAME),
description=module.params.get(DESCRIPTION_PARAMETER_NAME),
policies=module.params.get(POLICIES_PARAMETER_NAME),
service_identities=module.params.get(SERVICE_IDENTITIES_PARAMETER_NAME),
node_identities=module.params.get(NODE_IDENTITIES_PARAMETER_NAME),
state=module.params.get(STATE_PARAMETER_NAME),
check_mode=module.check_mode
)
except ValueError as err:
module.fail_json(msg='Configuration error: %s' % str(err))
return
try:
version = get_consul_version(configuration)
configuration.version = version
if configuration.state == PRESENT_STATE_VALUE:
output = set_role(configuration)
else:
output = remove_role(configuration)
except ConnectionError as e:
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
configuration.host, configuration.port, str(e)))
raise
return_values = dict(changed=output.changed, operation=output.operation, role=output.role)
module.exit_json(**return_values)
module = AnsibleModule(
_ARGUMENT_SPEC,
supports_check_mode=True,
)
consul_module = ConsulRoleModule(module)
consul_module.execute()
if __name__ == "__main__":

View File

@@ -16,12 +16,13 @@ description:
cluster. These sessions can then be used in conjunction with key value pairs
to implement distributed locks. In depth documentation for working with
sessions can be found at http://www.consul.io/docs/internals/sessions.html
requirements:
- requests
author:
- Steve Gargan (@sgargan)
- Håkon Lerring (@Hakon)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.actiongroup_consul
- community.general.consul.token
- community.general.attributes
attributes:
check_mode:
@@ -76,26 +77,6 @@ options:
the associated lock delay has expired.
type: list
elements: str
host:
description:
- The host of the consul agent defaults to localhost.
type: str
default: localhost
port:
description:
- The port on which the consul agent is running.
type: int
default: 8500
scheme:
description:
- The protocol scheme on which the consul agent is running.
type: str
default: http
validate_certs:
description:
- Whether to verify the TLS certificate of the consul agent.
type: bool
default: true
behavior:
description:
- The optional behavior that can be attached to the session when it
@@ -109,10 +90,6 @@ options:
type: int
version_added: 5.4.0
token:
description:
- The token key identifying an ACL rule set that controls access to
the key value pair.
type: str
version_added: 5.6.0
'''
@@ -148,95 +125,49 @@ EXAMPLES = '''
'''
from ansible.module_utils.basic import AnsibleModule
try:
import requests
from requests.exceptions import ConnectionError
has_requests = True
except ImportError:
has_requests = False
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC, _ConsulModule
)
def execute(module):
def execute(module, consul_module):
state = module.params.get('state')
if state in ['info', 'list', 'node']:
lookup_sessions(module)
lookup_sessions(module, consul_module)
elif state == 'present':
update_session(module)
update_session(module, consul_module)
else:
remove_session(module)
remove_session(module, consul_module)
class RequestError(Exception):
pass
def list_sessions(consul_module, datacenter):
return consul_module.get(
'session/list',
params={'dc': datacenter})
def handle_consul_response_error(response):
if 400 <= response.status_code < 600:
raise RequestError('%d %s' % (response.status_code, response.content))
def list_sessions_for_node(consul_module, node, datacenter):
return consul_module.get(
('session', 'node', node),
params={'dc': datacenter})
def get_consul_url(module):
return '%s://%s:%s/v1' % (module.params.get('scheme'),
module.params.get('host'), module.params.get('port'))
def get_session_info(consul_module, session_id, datacenter):
return consul_module.get(
('session', 'info', session_id),
params={'dc': datacenter})
def get_auth_headers(module):
if 'token' in module.params and module.params.get('token') is not None:
return {'X-Consul-Token': module.params.get('token')}
else:
return {}
def list_sessions(module, datacenter):
url = '%s/session/list' % get_consul_url(module)
headers = get_auth_headers(module)
response = requests.get(
url,
headers=headers,
params={
'dc': datacenter},
verify=module.params.get('validate_certs'))
handle_consul_response_error(response)
return response.json()
def list_sessions_for_node(module, node, datacenter):
url = '%s/session/node/%s' % (get_consul_url(module), node)
headers = get_auth_headers(module)
response = requests.get(
url,
headers=headers,
params={
'dc': datacenter},
verify=module.params.get('validate_certs'))
handle_consul_response_error(response)
return response.json()
def get_session_info(module, session_id, datacenter):
url = '%s/session/info/%s' % (get_consul_url(module), session_id)
headers = get_auth_headers(module)
response = requests.get(
url,
headers=headers,
params={
'dc': datacenter},
verify=module.params.get('validate_certs'))
handle_consul_response_error(response)
return response.json()
def lookup_sessions(module):
def lookup_sessions(module, consul_module):
datacenter = module.params.get('datacenter')
state = module.params.get('state')
try:
if state == 'list':
sessions_list = list_sessions(module, datacenter)
sessions_list = list_sessions(consul_module, datacenter)
# Ditch the index, this can be grabbed from the results
if sessions_list and len(sessions_list) >= 2:
sessions_list = sessions_list[1]
@@ -244,14 +175,14 @@ def lookup_sessions(module):
sessions=sessions_list)
elif state == 'node':
node = module.params.get('node')
sessions = list_sessions_for_node(module, node, datacenter)
sessions = list_sessions_for_node(consul_module, node, datacenter)
module.exit_json(changed=True,
node=node,
sessions=sessions)
elif state == 'info':
session_id = module.params.get('id')
session_by_id = get_session_info(module, session_id, datacenter)
session_by_id = get_session_info(consul_module, session_id, datacenter)
module.exit_json(changed=True,
session_id=session_id,
sessions=session_by_id)
@@ -260,10 +191,8 @@ def lookup_sessions(module):
module.fail_json(msg="Could not retrieve session info %s" % e)
def create_session(module, name, behavior, ttl, node,
def create_session(consul_module, name, behavior, ttl, node,
lock_delay, datacenter, checks):
url = '%s/session/create' % get_consul_url(module)
headers = get_auth_headers(module)
create_data = {
"LockDelay": lock_delay,
"Node": node,
@@ -273,19 +202,15 @@ def create_session(module, name, behavior, ttl, node,
}
if ttl is not None:
create_data["TTL"] = "%ss" % str(ttl) # TTL is in seconds
response = requests.put(
url,
headers=headers,
create_session_response_dict = consul_module.put(
'session/create',
params={
'dc': datacenter},
json=create_data,
verify=module.params.get('validate_certs'))
handle_consul_response_error(response)
create_session_response_dict = response.json()
data=create_data)
return create_session_response_dict["ID"]
def update_session(module):
def update_session(module, consul_module):
name = module.params.get('name')
delay = module.params.get('delay')
@@ -296,7 +221,7 @@ def update_session(module):
ttl = module.params.get('ttl')
try:
session = create_session(module,
session = create_session(consul_module,
name=name,
behavior=behavior,
ttl=ttl,
@@ -317,22 +242,15 @@ def update_session(module):
module.fail_json(msg="Could not create/update session %s" % e)
def destroy_session(module, session_id):
url = '%s/session/destroy/%s' % (get_consul_url(module), session_id)
headers = get_auth_headers(module)
response = requests.put(
url,
headers=headers,
verify=module.params.get('validate_certs'))
handle_consul_response_error(response)
return response.content == "true"
def destroy_session(consul_module, session_id):
return consul_module.put(('session', 'destroy', session_id))
def remove_session(module):
def remove_session(module, consul_module):
session_id = module.params.get('id')
try:
destroy_session(module, session_id)
destroy_session(consul_module, session_id)
module.exit_json(changed=True,
session_id=session_id)
@@ -341,12 +259,6 @@ def remove_session(module):
session_id, e))
def test_dependencies(module):
if not has_requests:
raise ImportError(
"requests required for this module. See https://pypi.org/project/requests/")
def main():
argument_spec = dict(
checks=dict(type='list', elements='str'),
@@ -358,10 +270,6 @@ def main():
'release',
'delete']),
ttl=dict(type='int'),
host=dict(type='str', default='localhost'),
port=dict(type='int', default=8500),
scheme=dict(type='str', default='http'),
validate_certs=dict(type='bool', default=True),
id=dict(type='str'),
name=dict(type='str'),
node=dict(type='str'),
@@ -375,7 +283,7 @@ def main():
'node',
'present']),
datacenter=dict(type='str'),
token=dict(type='str', no_log=True),
**AUTH_ARGUMENTS_SPEC
)
module = AnsibleModule(
@@ -387,14 +295,10 @@ def main():
],
supports_check_mode=False
)
test_dependencies(module)
consul_module = _ConsulModule(module)
try:
execute(module)
except ConnectionError as e:
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
module.params.get('host'), module.params.get('port'), e))
execute(module, consul_module)
except Exception as e:
module.fail_json(msg=str(e))

View File

@@ -0,0 +1,325 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: consul_token
short_description: Manipulate Consul tokens
version_added: 8.3.0
description:
- Allows the addition, modification and deletion of tokens in a consul
cluster via the agent. For more details on using and configuring ACLs,
see U(https://www.consul.io/docs/guides/acl.html).
author:
- Florian Apolloner (@apollo13)
extends_documentation_fragment:
- community.general.consul
- community.general.consul.token
- community.general.consul.actiongroup_consul
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: partial
details:
- In check mode the diff will miss operational attributes.
options:
state:
description:
- Whether the token should be present or absent.
choices: ['present', 'absent']
default: present
type: str
accessor_id:
description:
- Specifies a UUID to use as the token's Accessor ID.
If not specified a UUID will be generated for this field.
type: str
secret_id:
description:
- Specifies a UUID to use as the token's Secret ID.
If not specified a UUID will be generated for this field.
type: str
description:
description:
- Free form human readable description of the token.
type: str
policies:
type: list
elements: dict
description:
- List of policies to attach to the token. Each policy is a dict.
- If the parameter is left blank, any policies currently assigned will not be changed.
- Any empty array (V([])) will clear any policies previously set.
suboptions:
name:
description:
- The name of the policy to attach to this token; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].id) must be specified.
type: str
id:
description:
- The ID of the policy to attach to this token; see M(community.general.consul_policy) for more info.
- Either this or O(policies[].name) must be specified.
type: str
roles:
type: list
elements: dict
description:
- List of roles to attach to the token. Each role is a dict.
- If the parameter is left blank, any roles currently assigned will not be changed.
- Any empty array (V([])) will clear any roles previously set.
suboptions:
name:
description:
- The name of the role to attach to this token; see M(community.general.consul_role) for more info.
- Either this or O(roles[].id) must be specified.
type: str
id:
description:
- The ID of the role to attach to this token; see M(community.general.consul_role) for more info.
- Either this or O(roles[].name) must be specified.
type: str
templated_policies:
description:
- The list of templated policies that should be applied to the role.
type: list
elements: dict
suboptions:
template_name:
description:
- The templated policy name.
type: str
required: true
template_variables:
description:
- The templated policy variables.
- Not all templated policies require variables.
type: dict
service_identities:
type: list
elements: dict
description:
- List of service identities to attach to the token.
- If not specified, any service identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions:
service_name:
description:
- The name of the service.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
type: str
required: true
datacenters:
description:
- The datacenters the token will be effective.
- If an empty array (V([])) is specified, the token will valid in all datacenters.
- including those which do not yet exist but may in the future.
type: list
elements: str
node_identities:
type: list
elements: dict
description:
- List of node identities to attach to the token.
- If not specified, any node identities currently assigned will not be changed.
- If the parameter is an empty array (V([])), any node identities assigned will be unassigned.
suboptions:
node_name:
description:
- The name of the node.
- Must not be longer than 256 characters, must start and end with a lowercase alphanumeric character.
- May only contain lowercase alphanumeric characters as well as V(-) and V(_).
type: str
required: true
datacenter:
description:
- The nodes datacenter.
- This will result in effective token only being valid in this datacenter.
type: str
required: true
local:
description:
- If true, indicates that the token should not be replicated globally
and instead be local to the current datacenter.
type: bool
expiration_ttl:
description:
- This is a convenience field and if set will initialize the C(expiration_time).
Can be specified in the form of V(60s) or V(5m) (that is, 60 seconds or 5 minutes,
respectively). Ingored when the token is updated!
type: str
"""
EXAMPLES = """
- name: Create / Update a token by accessor_id
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
roles:
- name: role1
- name: role2
service_identities:
- service_name: service1
datacenters: [dc1, dc2]
node_identities:
- node_name: node1
datacenter: dc1
expiration_ttl: 50m
- name: Delete a token
community.general.consul_token:
state: absent
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
token: 8adddd91-0bd6-d41d-ae1a-3b49cfa9a0e8
"""
RETURN = """
token:
description: The token as returned by the consul HTTP API.
returned: always
type: dict
sample:
AccessorID: 07a7de84-c9c7-448a-99cc-beaf682efd21
CreateIndex: 632
CreateTime: "2024-01-14T21:53:01.402749174+01:00"
Description: Testing
Hash: rj5PeDHddHslkpW7Ij4OD6N4bbSXiecXFmiw2SYXg2A=
Local: false
ModifyIndex: 633
SecretID: bd380fba-da17-7cee-8576-8d6427c6c930
ServiceIdentities: [{"ServiceName": "test"}]
operation:
description: The operation performed.
returned: changed
type: str
sample: update
"""
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.consul import (
AUTH_ARGUMENTS_SPEC,
_ConsulModule,
)
def normalize_link_obj(api_obj, module_obj, key):
api_objs = api_obj.get(key)
module_objs = module_obj.get(key)
if api_objs is None or module_objs is None:
return
name_to_id = {i["Name"]: i["ID"] for i in api_objs}
id_to_name = {i["ID"]: i["Name"] for i in api_objs}
for obj in module_objs:
identifier = obj.get("ID")
name = obj.get("Name)")
if identifier and not name and identifier in id_to_name:
obj["Name"] = id_to_name[identifier]
if not identifier and name and name in name_to_id:
obj["ID"] = name_to_id[name]
class ConsulTokenModule(_ConsulModule):
api_endpoint = "acl/token"
result_key = "token"
unique_identifier = "accessor_id"
create_only_fields = {"expiration_ttl"}
def needs_update(self, api_obj, module_obj):
# SecretID is usually not supplied
if "SecretID" not in module_obj and "SecretID" in api_obj:
del api_obj["SecretID"]
normalize_link_obj(api_obj, module_obj, "Roles")
normalize_link_obj(api_obj, module_obj, "Policies")
# ExpirationTTL is only supported on create, not for update
# it writes to ExpirationTime, so we need to remove that as well
if "ExpirationTTL" in module_obj:
del module_obj["ExpirationTTL"]
return super(ConsulTokenModule, self).needs_update(api_obj, module_obj)
NAME_ID_SPEC = dict(
name=dict(type="str"),
id=dict(type="str"),
)
NODE_ID_SPEC = dict(
node_name=dict(type="str", required=True),
datacenter=dict(type="str", required=True),
)
SERVICE_ID_SPEC = dict(
service_name=dict(type="str", required=True),
datacenters=dict(type="list", elements="str"),
)
TEMPLATE_POLICY_SPEC = dict(
template_name=dict(type="str", required=True),
template_variables=dict(type="dict"),
)
_ARGUMENT_SPEC = {
"description": dict(),
"accessor_id": dict(),
"secret_id": dict(no_log=True),
"roles": dict(
type="list",
elements="dict",
options=NAME_ID_SPEC,
mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"policies": dict(
type="list",
elements="dict",
options=NAME_ID_SPEC,
mutually_exclusive=[("name", "id")],
required_one_of=[("name", "id")],
),
"templated_policies": dict(
type="list",
elements="dict",
options=TEMPLATE_POLICY_SPEC,
),
"node_identities": dict(
type="list",
elements="dict",
options=NODE_ID_SPEC,
),
"service_identities": dict(
type="list",
elements="dict",
options=SERVICE_ID_SPEC,
),
"local": dict(type="bool"),
"expiration_ttl": dict(type="str"),
"state": dict(default="present", choices=["present", "absent"]),
}
_ARGUMENT_SPEC.update(AUTH_ARGUMENTS_SPEC)
def main():
module = AnsibleModule(
_ARGUMENT_SPEC,
required_if=[("state", "absent", ["accessor_id"])],
supports_check_mode=True,
)
consul_module = ConsulTokenModule(module)
consul_module.execute()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,500 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@gmail.com)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: gitlab_label
short_description: Creates/updates/deletes GitLab Labels belonging to project or group.
version_added: 8.3.0
description:
- When a label does not exist, it will be created.
- When a label does exist, its value will be updated when the values are different.
- Labels can be purged.
author:
- "Gabriele Pongelli (@gpongelli)"
requirements:
- python-gitlab python module
extends_documentation_fragment:
- community.general.auth_basic
- community.general.gitlab
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
state:
description:
- Create or delete project or group label.
default: present
type: str
choices: ["present", "absent"]
purge:
description:
- When set to V(true), delete all labels which are not mentioned in the task.
default: false
type: bool
required: false
project:
description:
- The path and name of the project. Either this or O(group) is required.
required: false
type: str
group:
description:
- The path of the group. Either this or O(project) is required.
required: false
type: str
labels:
description:
- A list of dictionaries that represents gitlab project's or group's labels.
type: list
elements: dict
required: false
default: []
suboptions:
name:
description:
- The name of the label.
type: str
required: true
color:
description:
- The color of the label.
- Required when O(state=present).
type: str
priority:
description:
- Integer value to give priority to the label.
type: int
required: false
default: null
description:
description:
- Label's description.
type: str
default: null
new_name:
description:
- Optional field to change label's name.
type: str
default: null
'''
EXAMPLES = '''
# same project's task can be executed for group
- name: Create one Label
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
color: "#123456"
state: present
- name: Create many group labels
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
group: "group1"
labels:
- name: label_one
color: "#123456"
description: this is a label
priority: 20
- name: label_two
color: "#554422"
state: present
- name: Create many project labels
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
color: "#123456"
description: this is a label
priority: 20
- name: label_two
color: "#554422"
state: present
- name: Set or update some labels
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
color: "#224488"
state: present
- name: Add label in check mode
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
color: "#224488"
check_mode: true
- name: Delete Label
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
state: absent
- name: Change Label name
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
labels:
- name: label_one
new_name: label_two
state: absent
- name: Purge all labels
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
purge: true
- name: Delete many labels
community.general.gitlab_label:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
state: absent
labels:
- name: label-abc123
- name: label-two
'''
RETURN = '''
labels:
description: Four lists of the labels which were added, updated, removed or exist.
returned: success
type: dict
contains:
added:
description: A list of labels which were created.
returned: always
type: list
sample: ['abcd', 'label-one']
untouched:
description: A list of labels which exist.
returned: always
type: list
sample: ['defg', 'new-label']
removed:
description: A list of labels which were deleted.
returned: always
type: list
sample: ['defg', 'new-label']
updated:
description: A list pre-existing labels whose values have been set.
returned: always
type: list
sample: ['defg', 'new-label']
labels_obj:
description: API object.
returned: success
type: dict
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
)
class GitlabLabels(object):
def __init__(self, module, gitlab_instance, group_id, project_id):
self._gitlab = gitlab_instance
self.gitlab_object = group_id if group_id else project_id
self.is_group_label = True if group_id else False
self._module = module
def list_all_labels(self):
page_nb = 1
labels = []
vars_page = self.gitlab_object.labels.list(page=page_nb)
while len(vars_page) > 0:
labels += vars_page
page_nb += 1
vars_page = self.gitlab_object.labels.list(page=page_nb)
return labels
def create_label(self, var_obj):
if self._module.check_mode:
return True, True
var = {
"name": var_obj.get('name'),
"color": var_obj.get('color'),
}
if var_obj.get('description') is not None:
var["description"] = var_obj.get('description')
if var_obj.get('priority') is not None:
var["priority"] = var_obj.get('priority')
_obj = self.gitlab_object.labels.create(var)
return True, _obj.asdict()
def update_label(self, var_obj):
if self._module.check_mode:
return True, True
_label = self.gitlab_object.labels.get(var_obj.get('name'))
if var_obj.get('new_name') is not None:
_label.new_name = var_obj.get('new_name')
if var_obj.get('description') is not None:
_label.description = var_obj.get('description')
if var_obj.get('priority') is not None:
_label.priority = var_obj.get('priority')
# save returns None
_label.save()
return True, _label.asdict()
def delete_label(self, var_obj):
if self._module.check_mode:
return True, True
_label = self.gitlab_object.labels.get(var_obj.get('name'))
# delete returns None
_label.delete()
return True, _label.asdict()
def compare(requested_labels, existing_labels, state):
# we need to do this, because it was determined in a previous version - more or less buggy
# basically it is not necessary and might result in more/other bugs!
# but it is required and only relevant for check mode!!
# logic represents state 'present' when not purge. all other can be derived from that
# untouched => equal in both
# updated => name and scope are equal
# added => name and scope does not exist
untouched = list()
updated = list()
added = list()
if state == 'present':
_existing_labels = list()
for item in existing_labels:
_existing_labels.append({'name': item.get('name')})
for var in requested_labels:
if var in existing_labels:
untouched.append(var)
else:
compare_item = {'name': var.get('name')}
if compare_item in _existing_labels:
updated.append(var)
else:
added.append(var)
return untouched, updated, added
def native_python_main(this_gitlab, purge, requested_labels, state, module):
change = False
return_value = dict(added=[], updated=[], removed=[], untouched=[])
return_obj = dict(added=[], updated=[], removed=[])
labels_before = [x.asdict() for x in this_gitlab.list_all_labels()]
# filter out and enrich before compare
for item in requested_labels:
# add defaults when not present
if item.get('description') is None:
item['description'] = ""
if item.get('new_name') is None:
item['new_name'] = None
if item.get('priority') is None:
item['priority'] = None
# group label does not have priority, removing for comparison
if this_gitlab.is_group_label:
item.pop('priority')
for item in labels_before:
# remove field only from server
item.pop('id')
item.pop('description_html')
item.pop('text_color')
item.pop('subscribed')
# field present only when it's a project's label
if 'is_project_label' in item:
item.pop('is_project_label')
item['new_name'] = None
if state == 'present':
add_or_update = [x for x in requested_labels if x not in labels_before]
for item in add_or_update:
try:
_rv, _obj = this_gitlab.create_label(item)
if _rv:
return_value['added'].append(item)
return_obj['added'].append(_obj)
except Exception:
# create raises exception with following error message when label already exists
_rv, _obj = this_gitlab.update_label(item)
if _rv:
return_value['updated'].append(item)
return_obj['updated'].append(_obj)
if purge:
# re-fetch
_labels = this_gitlab.list_all_labels()
for item in labels_before:
_rv, _obj = this_gitlab.delete_label(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
elif state == 'absent':
if not purge:
_label_names_requested = [x['name'] for x in requested_labels]
remove_requested = [x for x in labels_before if x['name'] in _label_names_requested]
for item in remove_requested:
_rv, _obj = this_gitlab.delete_label(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
else:
for item in labels_before:
_rv, _obj = this_gitlab.delete_label(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
if module.check_mode:
_untouched, _updated, _added = compare(requested_labels, labels_before, state)
return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched)
if any(return_value[x] for x in ['added', 'removed', 'updated']):
change = True
labels_after = [x.asdict() for x in this_gitlab.list_all_labels()]
return change, return_value, labels_before, labels_after, return_obj
def main():
argument_spec = basic_auth_argument_spec()
argument_spec.update(auth_argument_spec())
argument_spec.update(
project=dict(type='str', required=False, default=None),
group=dict(type='str', required=False, default=None),
purge=dict(type='bool', required=False, default=False),
labels=dict(type='list', elements='dict', required=False, default=list(),
options=dict(
name=dict(type='str', required=True),
color=dict(type='str', required=False),
description=dict(type='str', required=False),
priority=dict(type='int', required=False),
new_name=dict(type='str', required=False),)
),
state=dict(type='str', default="present", choices=["absent", "present"]),
)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
['api_username', 'api_token'],
['api_username', 'api_oauth_token'],
['api_username', 'api_job_token'],
['api_token', 'api_oauth_token'],
['api_token', 'api_job_token'],
['project', 'group'],
],
required_together=[
['api_username', 'api_password'],
],
required_one_of=[
['api_username', 'api_token', 'api_oauth_token', 'api_job_token'],
['project', 'group']
],
supports_check_mode=True
)
ensure_gitlab_package(module)
gitlab_project = module.params['project']
gitlab_group = module.params['group']
purge = module.params['purge']
label_list = module.params['labels']
state = module.params['state']
gitlab_version = gitlab.__version__
_min_gitlab = '3.2.0'
if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
module.fail_json(msg="community.general.gitlab_label requires python-gitlab Python module >= %s "
"(installed version: [%s]). Please upgrade "
"python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
gitlab_instance = gitlab_authentication(module)
# find_project can return None, but the other must exist
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
# find_group can return None, but the other must exist
gitlab_group_id = find_group(gitlab_instance, gitlab_group)
# if both not found, module must exist
if not gitlab_project_id and not gitlab_group_id:
if gitlab_project and not gitlab_project_id:
module.fail_json(msg="project '%s' not found." % gitlab_project)
if gitlab_group and not gitlab_group_id:
module.fail_json(msg="group '%s' not found." % gitlab_group)
this_gitlab = GitlabLabels(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id,
project_id=gitlab_project_id)
if state == 'present':
_existing_labels = [x.asdict()['name'] for x in this_gitlab.list_all_labels()]
# color is mandatory when creating label, but it's optional when changing name or updating other fields
if any(x['color'] is None and x['new_name'] is None and x['name'] not in _existing_labels for x in label_list):
module.fail_json(msg='color parameter is required for new labels')
change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, label_list, state, module)
if not module.check_mode:
raw_return_value['untouched'] = [x for x in before if x in after]
added = [x.get('name') for x in raw_return_value['added']]
updated = [x.get('name') for x in raw_return_value['updated']]
removed = [x.get('name') for x in raw_return_value['removed']]
untouched = [x.get('name') for x in raw_return_value['untouched']]
return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched)
module.exit_json(changed=change, labels=return_value, labels_obj=_obj)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,496 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2023, Gabriele Pongelli (gabriele.pongelli@gmail.com)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: gitlab_milestone
short_description: Creates/updates/deletes GitLab Milestones belonging to project or group
version_added: 8.3.0
description:
- When a milestone does not exist, it will be created.
- When a milestone does exist, its value will be updated when the values are different.
- Milestones can be purged.
author:
- "Gabriele Pongelli (@gpongelli)"
requirements:
- python-gitlab python module
extends_documentation_fragment:
- community.general.auth_basic
- community.general.gitlab
- community.general.attributes
attributes:
check_mode:
support: full
diff_mode:
support: none
options:
state:
description:
- Create or delete milestone.
default: present
type: str
choices: ["present", "absent"]
purge:
description:
- When set to V(true), delete all milestone which are not mentioned in the task.
default: false
type: bool
required: false
project:
description:
- The path and name of the project. Either this or O(group) is required.
required: false
type: str
group:
description:
- The path of the group. Either this or O(project) is required.
required: false
type: str
milestones:
description:
- A list of dictionaries that represents gitlab project's or group's milestones.
type: list
elements: dict
required: false
default: []
suboptions:
title:
description:
- The name of the milestone.
type: str
required: true
due_date:
description:
- Milestone due date in YYYY-MM-DD format.
type: str
required: false
default: null
start_date:
description:
- Milestone start date in YYYY-MM-DD format.
type: str
required: false
default: null
description:
description:
- Milestone's description.
type: str
default: null
'''
EXAMPLES = '''
# same project's task can be executed for group
- name: Create one milestone
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
milestones:
- title: milestone_one
start_date: "2024-01-04"
state: present
- name: Create many group milestones
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
group: "group1"
milestones:
- title: milestone_one
start_date: "2024-01-04"
description: this is a milestone
due_date: "2024-02-04"
- title: milestone_two
state: present
- name: Create many project milestones
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
milestones:
- title: milestone_one
start_date: "2024-01-04"
description: this is a milestone
due_date: "2024-02-04"
- title: milestone_two
state: present
- name: Set or update some milestones
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
milestones:
- title: milestone_one
start_date: "2024-05-04"
state: present
- name: Add milestone in check mode
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
milestones:
- title: milestone_one
start_date: "2024-05-04"
check_mode: true
- name: Delete milestone
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
milestones:
- title: milestone_one
state: absent
- name: Purge all milestones
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
purge: true
- name: Delete many milestones
community.general.gitlab_milestone:
api_url: https://gitlab.com
api_token: secret_access_token
project: "group1/project1"
state: absent
milestones:
- title: milestone-abc123
- title: milestone-two
'''
RETURN = '''
milestones:
description: Four lists of the milestones which were added, updated, removed or exist.
returned: success
type: dict
contains:
added:
description: A list of milestones which were created.
returned: always
type: list
sample: ['abcd', 'milestone-one']
untouched:
description: A list of milestones which exist.
returned: always
type: list
sample: ['defg', 'new-milestone']
removed:
description: A list of milestones which were deleted.
returned: always
type: list
sample: ['defg', 'new-milestone']
updated:
description: A list pre-existing milestones whose values have been set.
returned: always
type: list
sample: ['defg', 'new-milestone']
milestones_obj:
description: API object.
returned: success
type: dict
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.api import basic_auth_argument_spec
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
from ansible_collections.community.general.plugins.module_utils.gitlab import (
auth_argument_spec, gitlab_authentication, ensure_gitlab_package, find_group, find_project, gitlab
)
from datetime import datetime
class GitlabMilestones(object):
def __init__(self, module, gitlab_instance, group_id, project_id):
self._gitlab = gitlab_instance
self.gitlab_object = group_id if group_id else project_id
self.is_group_milestone = True if group_id else False
self._module = module
def list_all_milestones(self):
page_nb = 1
milestones = []
vars_page = self.gitlab_object.milestones.list(page=page_nb)
while len(vars_page) > 0:
milestones += vars_page
page_nb += 1
vars_page = self.gitlab_object.milestones.list(page=page_nb)
return milestones
def create_milestone(self, var_obj):
if self._module.check_mode:
return True, True
var = {
"title": var_obj.get('title'),
}
if var_obj.get('description') is not None:
var["description"] = var_obj.get('description')
if var_obj.get('start_date') is not None:
var["start_date"] = self.check_date(var_obj.get('start_date'))
if var_obj.get('due_date') is not None:
var["due_date"] = self.check_date(var_obj.get('due_date'))
_obj = self.gitlab_object.milestones.create(var)
return True, _obj.asdict()
def update_milestone(self, var_obj):
if self._module.check_mode:
return True, True
_milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title')))
if var_obj.get('description') is not None:
_milestone.description = var_obj.get('description')
if var_obj.get('start_date') is not None:
_milestone.start_date = var_obj.get('start_date')
if var_obj.get('due_date') is not None:
_milestone.due_date = var_obj.get('due_date')
# save returns None
_milestone.save()
return True, _milestone.asdict()
def get_milestone_id(self, _title):
_milestone_list = self.gitlab_object.milestones.list()
_found = list(filter(lambda x: x.title == _title, _milestone_list))
if _found:
return _found[0].id
else:
self._module.fail_json(msg="milestone '%s' not found." % _title)
def check_date(self, _date):
try:
datetime.strptime(_date, '%Y-%m-%d')
except ValueError:
self._module.fail_json(msg="milestone's date '%s' not in correct format." % _date)
return _date
def delete_milestone(self, var_obj):
if self._module.check_mode:
return True, True
_milestone = self.gitlab_object.milestones.get(self.get_milestone_id(var_obj.get('title')))
# delete returns None
_milestone.delete()
return True, _milestone.asdict()
def compare(requested_milestones, existing_milestones, state):
# we need to do this, because it was determined in a previous version - more or less buggy
# basically it is not necessary and might result in more/other bugs!
# but it is required and only relevant for check mode!!
# logic represents state 'present' when not purge. all other can be derived from that
# untouched => equal in both
# updated => title are equal
# added => title does not exist
untouched = list()
updated = list()
added = list()
if state == 'present':
_existing_milestones = list()
for item in existing_milestones:
_existing_milestones.append({'title': item.get('title')})
for var in requested_milestones:
if var in existing_milestones:
untouched.append(var)
else:
compare_item = {'title': var.get('title')}
if compare_item in _existing_milestones:
updated.append(var)
else:
added.append(var)
return untouched, updated, added
def native_python_main(this_gitlab, purge, requested_milestones, state, module):
change = False
return_value = dict(added=[], updated=[], removed=[], untouched=[])
return_obj = dict(added=[], updated=[], removed=[])
milestones_before = [x.asdict() for x in this_gitlab.list_all_milestones()]
# filter out and enrich before compare
for item in requested_milestones:
# add defaults when not present
if item.get('description') is None:
item['description'] = ""
if item.get('due_date') is None:
item['due_date'] = None
if item.get('start_date') is None:
item['start_date'] = None
for item in milestones_before:
# remove field only from server
item.pop('id')
item.pop('iid')
item.pop('created_at')
item.pop('expired')
item.pop('state')
item.pop('updated_at')
item.pop('web_url')
# group milestone has group_id, while project has project_id
if 'group_id' in item:
item.pop('group_id')
if 'project_id' in item:
item.pop('project_id')
if state == 'present':
add_or_update = [x for x in requested_milestones if x not in milestones_before]
for item in add_or_update:
try:
_rv, _obj = this_gitlab.create_milestone(item)
if _rv:
return_value['added'].append(item)
return_obj['added'].append(_obj)
except Exception:
# create raises exception with following error message when milestone already exists
_rv, _obj = this_gitlab.update_milestone(item)
if _rv:
return_value['updated'].append(item)
return_obj['updated'].append(_obj)
if purge:
# re-fetch
_milestones = this_gitlab.list_all_milestones()
for item in milestones_before:
_rv, _obj = this_gitlab.delete_milestone(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
elif state == 'absent':
if not purge:
_milestone_titles_requested = [x['title'] for x in requested_milestones]
remove_requested = [x for x in milestones_before if x['title'] in _milestone_titles_requested]
for item in remove_requested:
_rv, _obj = this_gitlab.delete_milestone(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
else:
for item in milestones_before:
_rv, _obj = this_gitlab.delete_milestone(item)
if _rv:
return_value['removed'].append(item)
return_obj['removed'].append(_obj)
if module.check_mode:
_untouched, _updated, _added = compare(requested_milestones, milestones_before, state)
return_value = dict(added=_added, updated=_updated, removed=return_value['removed'], untouched=_untouched)
if any(return_value[x] for x in ['added', 'removed', 'updated']):
change = True
milestones_after = [x.asdict() for x in this_gitlab.list_all_milestones()]
return change, return_value, milestones_before, milestones_after, return_obj
def main():
argument_spec = basic_auth_argument_spec()
argument_spec.update(auth_argument_spec())
argument_spec.update(
project=dict(type='str', required=False, default=None),
group=dict(type='str', required=False, default=None),
purge=dict(type='bool', required=False, default=False),
milestones=dict(type='list', elements='dict', required=False, default=list(),
options=dict(
title=dict(type='str', required=True),
description=dict(type='str', required=False),
due_date=dict(type='str', required=False),
start_date=dict(type='str', required=False),)
),
state=dict(type='str', default="present", choices=["absent", "present"]),
)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
['api_username', 'api_token'],
['api_username', 'api_oauth_token'],
['api_username', 'api_job_token'],
['api_token', 'api_oauth_token'],
['api_token', 'api_job_token'],
['project', 'group'],
],
required_together=[
['api_username', 'api_password'],
],
required_one_of=[
['api_username', 'api_token', 'api_oauth_token', 'api_job_token'],
['project', 'group']
],
supports_check_mode=True
)
ensure_gitlab_package(module)
gitlab_project = module.params['project']
gitlab_group = module.params['group']
purge = module.params['purge']
milestone_list = module.params['milestones']
state = module.params['state']
gitlab_version = gitlab.__version__
_min_gitlab = '3.2.0'
if LooseVersion(gitlab_version) < LooseVersion(_min_gitlab):
module.fail_json(msg="community.general.gitlab_milestone requires python-gitlab Python module >= %s "
"(installed version: [%s]). Please upgrade "
"python-gitlab to version %s or above." % (_min_gitlab, gitlab_version, _min_gitlab))
gitlab_instance = gitlab_authentication(module)
# find_project can return None, but the other must exist
gitlab_project_id = find_project(gitlab_instance, gitlab_project)
# find_group can return None, but the other must exist
gitlab_group_id = find_group(gitlab_instance, gitlab_group)
# if both not found, module must exist
if not gitlab_project_id and not gitlab_group_id:
if gitlab_project and not gitlab_project_id:
module.fail_json(msg="project '%s' not found." % gitlab_project)
if gitlab_group and not gitlab_group_id:
module.fail_json(msg="group '%s' not found." % gitlab_group)
this_gitlab = GitlabMilestones(module=module, gitlab_instance=gitlab_instance, group_id=gitlab_group_id,
project_id=gitlab_project_id)
change, raw_return_value, before, after, _obj = native_python_main(this_gitlab, purge, milestone_list, state,
module)
if not module.check_mode:
raw_return_value['untouched'] = [x for x in before if x in after]
added = [x.get('title') for x in raw_return_value['added']]
updated = [x.get('title') for x in raw_return_value['updated']]
removed = [x.get('title') for x in raw_return_value['removed']]
untouched = [x.get('title') for x in raw_return_value['untouched']]
return_value = dict(added=added, updated=updated, removed=removed, untouched=untouched)
module.exit_json(changed=change, milestones=return_value, milestones_obj=_obj)
if __name__ == '__main__':
main()

View File

@@ -165,6 +165,7 @@ changed_pkgs:
version_added: '0.2.0'
'''
import json
import os.path
import re
@@ -184,6 +185,10 @@ def _create_regex_group_complement(s):
chars = filter(None, (line.split('#')[0].strip() for line in lines))
group = r'[^' + r''.join(chars) + r']'
return re.compile(group)
def _check_package_in_json(json_output, package_type):
return bool(json_output.get(package_type, []) and json_output[package_type][0].get("installed"))
# /utils ------------------------------------------------------------------ }}}
@@ -479,17 +484,13 @@ class Homebrew(object):
cmd = [
"{brew_path}".format(brew_path=self.brew_path),
"info",
"--json=v2",
self.current_package,
]
rc, out, err = self.module.run_command(cmd)
for line in out.split('\n'):
if (
re.search(r'Built from source', line)
or re.search(r'Poured from bottle', line)
):
return True
data = json.loads(out)
return False
return _check_package_in_json(data, "formulae") or _check_package_in_json(data, "casks")
def _current_package_is_outdated(self):
if not self.valid_package(self.current_package):

View File

@@ -237,7 +237,7 @@ def get_otptoken_dict(ansible_to_ipa, uniqueid=None, newuniqueid=None, otptype=N
if owner is not None:
otptoken[ansible_to_ipa['owner']] = owner
if enabled is not None:
otptoken[ansible_to_ipa['enabled']] = 'FALSE' if enabled else 'TRUE'
otptoken[ansible_to_ipa['enabled']] = False if enabled else True
if notbefore is not None:
otptoken[ansible_to_ipa['notbefore']] = notbefore + 'Z'
if notafter is not None:

View File

@@ -283,7 +283,7 @@ def run_module():
# Process the script into batches
queries = []
current_batch = []
for statement in script.splitlines(keepends=True):
for statement in script.splitlines(True):
# Ignore the Byte Order Mark, if found
if statement.strip() == '\uFEFF':
continue

View File

@@ -1832,7 +1832,7 @@ class Nmcli(object):
elif self.type == 'wifi':
options.update({
'802-11-wireless.ssid': self.ssid,
'connection.slave-type': 'bond' if self.master else None,
'connection.slave-type': ('bond' if self.slave_type is None else self.slave_type) if self.master else None,
})
if self.wifi:
for name, value in self.wifi.items():

View File

@@ -88,11 +88,13 @@ options:
EXAMPLES = '''
- name: Import a key via local file
community.general.pacman_key:
id: 01234567890ABCDE01234567890ABCDE12345678
data: "{{ lookup('file', 'keyfile.asc') }}"
state: present
- name: Import a key via remote file
community.general.pacman_key:
id: 01234567890ABCDE01234567890ABCDE12345678
file: /tmp/keyfile.asc
state: present

View File

@@ -562,6 +562,10 @@ class ProxmoxLxcAnsible(ProxmoxAnsible):
# compare the requested config against the current
update_config = False
for (arg, value) in kwargs.items():
# if the arg isn't in the current config, it needs to be updated
if arg not in current_config:
update_config = True
break
# some values are lists, the order isn't always the same, so split them and compare by key
if isinstance(value, str):
current_values = current_config[arg].split(",")

View File

@@ -55,6 +55,11 @@ options:
- Security token for authenticating to OOB controller.
type: str
version_added: 2.3.0
manager:
description:
- Name of manager on OOB controller to target.
type: str
version_added: '8.3.0'
timeout:
description:
- Timeout in seconds for HTTP requests to OOB controller.
@@ -248,6 +253,15 @@ EXAMPLES = '''
username: "{{ username }}"
password: "{{ password }}"
- name: Get service identification
community.general.redfish_info:
category: Manager
command: GetServiceIdentification
manager: "{{ manager }}"
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
- name: Get software inventory
community.general.redfish_info:
category: Update
@@ -369,7 +383,7 @@ CATEGORY_COMMANDS_ALL = {
"Update": ["GetFirmwareInventory", "GetFirmwareUpdateCapabilities", "GetSoftwareInventory",
"GetUpdateStatus"],
"Manager": ["GetManagerNicInventory", "GetVirtualMedia", "GetLogs", "GetNetworkProtocols",
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory"],
"GetHealthReport", "GetHostInterfaces", "GetManagerInventory", "GetServiceIdentification"],
}
CATEGORY_COMMANDS_DEFAULT = {
@@ -395,6 +409,7 @@ def main():
auth_token=dict(no_log=True),
timeout=dict(type='int'),
update_handle=dict(),
manager=dict(),
),
required_together=[
('username', 'password'),
@@ -429,6 +444,9 @@ def main():
# update handle
update_handle = module.params['update_handle']
# manager
manager = module.params['manager']
# Build root URI
root_uri = "https://" + module.params['baseuri']
rf_utils = RedfishUtils(creds, root_uri, timeout, module)
@@ -579,6 +597,8 @@ def main():
result["host_interfaces"] = rf_utils.get_hostinterfaces()
elif command == "GetManagerInventory":
result["manager"] = rf_utils.get_multi_manager_inventory()
elif command == "GetServiceIdentification":
result["service_id"] = rf_utils.get_service_identification(manager)
# Return data back
module.exit_json(redfish_facts=result)

View File

@@ -21,7 +21,8 @@ attributes:
check_mode:
support: full
diff_mode:
support: none
support: full
version_added: 8.3.0
options:
state:
choices: ['planned', 'present', 'absent']
@@ -428,6 +429,42 @@ def build_plan(command, project_path, variables_args, state_file, targets, state
))
def get_diff(diff_output):
def get_tf_resource_address(e):
return e['resource']
diff_json_output = json.loads(diff_output)
tf_reosource_changes = diff_json_output['resource_changes']
diff_after = []
diff_before = []
changed = False
for item in tf_reosource_changes:
item_change = item['change']
tf_before_state = {'resource': item['address'], 'change': item['change']['before']}
tf_after_state = {'resource': item['address'], 'change': item['change']['after']}
if item_change['actions'] == ['update'] or item_change['actions'] == ['delete', 'create']:
diff_before.append(tf_before_state)
diff_after.append(tf_after_state)
changed = True
if item_change['actions'] == ['delete']:
diff_before.append(tf_before_state)
changed = True
if item_change['actions'] == ['create']:
diff_after.append(tf_after_state)
changed = True
diff_before.sort(key=get_tf_resource_address)
diff_after.sort(key=get_tf_resource_address)
return changed, dict(
before=({'data': diff_before}),
after=({'data': diff_after}),
)
def main():
global module
module = AnsibleModule(
@@ -619,6 +656,19 @@ def main():
"Consider switching the 'check_destroy' to false to suppress this error")
command.append(plan_file)
result_diff = dict()
if module._diff or module.check_mode:
diff_command = [command[0], 'show', '-json', plan_file]
rc, diff_output, err = module.run_command(diff_command, check_rc=False, cwd=project_path)
changed, result_diff = get_diff(diff_output)
if rc != 0:
if workspace_ctx["current"] != workspace:
select_workspace(command[0], project_path, workspace_ctx["current"])
module.fail_json(msg=err.rstrip(), rc=rc, stdout=out,
stdout_lines=out.splitlines(), stderr=err,
stderr_lines=err.splitlines(),
cmd=' '.join(command))
if needs_application and not module.check_mode and state != 'planned':
rc, out, err = module.run_command(command, check_rc=False, cwd=project_path)
if rc != 0:
@@ -651,7 +701,18 @@ def main():
if state == 'absent' and workspace != 'default' and purge_workspace is True:
remove_workspace(command[0], project_path, workspace)
module.exit_json(changed=changed, state=state, workspace=workspace, outputs=outputs, stdout=out, stderr=err, command=' '.join(command))
result = {
'state': state,
'workspace': workspace,
'outputs': outputs,
'stdout': out,
'stderr': err,
'command': ' '.join(command),
'changed': changed,
'diff': result_diff,
}
module.exit_json(**result)
if __name__ == '__main__':

View File

@@ -0,0 +1,8 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
[galaxy-importer]
# This is only needed to make Zuul's third-party-check happy.
# It is not needed by anything else.
run_ansible_doc=false

View File

@@ -7,7 +7,8 @@
ansible.builtin.copy:
src: /bin/echo
dest: "{{ item.copy_to }}/echo"
mode: "755"
mode: "0755"
remote_src: true
when: item.copy_to is defined
- name: test cmd_echo module ({{ item.name }})

View File

@@ -0,0 +1,74 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
register: result
- assert:
that:
- result is changed
- result.auth_method.Type == 'jwt'
- result.operation == 'create'
- name: Update auth method
community.general.consul_auth_method:
name: test
max_token_ttl: 30m80s
register: result
- assert:
that:
- result is changed
- result.auth_method.Type == 'jwt'
- result.operation == 'update'
- name: Update auth method (noop)
community.general.consul_auth_method:
name: test
max_token_ttl: 30m80s
register: result
- assert:
that:
- result is not changed
- result.auth_method.Type == 'jwt'
- result.operation is not defined
- name: Delete auth method
community.general.consul_auth_method:
name: test
state: absent
register: result
- assert:
that:
- result is changed
- result.operation == 'remove'
- name: Delete auth method (noop)
community.general.consul_auth_method:
name: test
state: absent
register: result
- assert:
that:
- result is not changed
- result.operation is not defined

View File

@@ -0,0 +1,73 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create an auth method
community.general.consul_auth_method:
name: test
type: jwt
config:
jwt_validation_pubkeys:
- |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----
- name: Create a binding rule
community.general.consul_binding_rule:
name: test-binding
description: my description
auth_method: test
bind_type: service
bind_name: yolo
register: result
- assert:
that:
- result is changed
- result.binding_rule.AuthMethod == 'test'
- result.binding.Description == 'test-binding: my description'
- result.operation == 'create'
- name: Update a binding rule
community.general.consul_binding_rule:
name: test-binding
auth_method: test
bind_name: yolo2
register: result
- assert:
that:
- result is changed
- result.binding.Description == 'test-binding: my description'
- result.operation == 'update'
- name: Update a binding rule (noop)
community.general.consul_binding_rule:
name: test-binding
auth_method: test
register: result
- assert:
that:
- result is not changed
- result.binding.Description == 'test-binding: my description'
- result.operation is not defined
- name: Delete a binding rule
community.general.consul_binding_rule:
name: test-binding
auth_method: test
state: absent
register: result
- assert:
that:
- result is changed
- result.operation == 'remove'

View File

@@ -0,0 +1,76 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: ensure unknown scheme fails
consul_session:
state: info
id: dummy
scheme: non_existent
token: "{{ consul_management_token }}"
register: result
ignore_errors: true
- assert:
that:
- result is failed
- name: ensure SSL certificate is checked
consul_session:
state: info
id: dummy
port: 8501
scheme: https
token: "{{ consul_management_token }}"
register: result
ignore_errors: true
- name: previous task should fail since certificate is not known
assert:
that:
- result is failed
- "'certificate verify failed' in result.msg"
- name: ensure SSL certificate isn't checked when validate_certs is disabled
consul_session:
state: info
id: dummy
port: 8501
scheme: https
token: "{{ consul_management_token }}"
validate_certs: false
register: result
- name: previous task should succeed since certificate isn't checked
assert:
that:
- result is changed
- name: ensure a secure connection is possible
consul_session:
state: info
id: dummy
port: 8501
scheme: https
token: "{{ consul_management_token }}"
ca_path: '{{ remote_dir }}/cert.pem'
register: result
- assert:
that:
- result is changed
- name: ensure connection errors are handled properly
consul_session:
state: info
id: dummy
token: "{{ consul_management_token }}"
port: 1234
register: result
ignore_errors: true
- assert:
that:
- result is failed
- result.msg.startswith('Could not connect to consul agent at localhost:1234, error was')

View File

@@ -0,0 +1,57 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create a key
consul_kv:
key: somekey
value: somevalue
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.data.Value == 'somevalue'
#- name: Test the lookup
# assert:
# that:
# - lookup('community.general.consul_kv', 'somekey', token=consul_management_token) == 'somevalue'
- name: Update a key with the same data
consul_kv:
key: somekey
value: somevalue
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- result.data.Value == 'somevalue'
- name: Remove a key from the store
consul_kv:
key: somekey
state: absent
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.data.Value == 'somevalue'
- name: Remove a non-existant key from the store
consul_kv:
key: somekey
state: absent
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- not result.data

View File

@@ -13,13 +13,14 @@
key "private/foo" {
policy = "deny"
}
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result['policy']['Name'] == 'foo-access'
- result.policy.Name == 'foo-access'
- result.operation == 'create'
- name: Update the rules associated to a policy
consul_policy:
name: foo-access
@@ -33,11 +34,13 @@
event "bbq" {
policy = "write"
}
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result.operation == 'update'
- name: Update reports not changed when updating again without changes
consul_policy:
name: foo-access
@@ -51,17 +54,19 @@
event "bbq" {
policy = "write"
}
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is not changed
- result.operation is not defined
- name: Remove a policy
consul_policy:
name: foo-access
token: "{{ consul_management_token }}"
state: absent
register: result
- assert:
that:
- result is changed
- result is changed
- result.operation == 'remove'

View File

@@ -13,7 +13,6 @@
key "private/foo" {
policy = "deny"
}
token: "{{ consul_management_token }}"
register: policy_result
- name: Create another policy with rules
@@ -26,7 +25,6 @@
key "private/bar" {
policy = "deny"
}
token: "{{ consul_management_token }}"
register: policy_result
- name: Create a role with policy
@@ -34,40 +32,40 @@
name: foo-role-with-policy
policies:
- name: "foo-access-for-role"
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result['role']['Name'] == 'foo-role-with-policy'
- result.role.Name == 'foo-role-with-policy'
- result.operation == 'create'
- name: Update policy description, in check mode
consul_role:
name: foo-role-with-policy
description: "Testing updating description"
token: "{{ consul_management_token }}"
check_mode: yes
register: result
- assert:
that:
- result is changed
- result['role']['Description'] == "Testing updating description"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
- result.role.Description == "Testing updating description"
- result.role.Policies.0.Name == 'foo-access-for-role'
- result.operation == 'update'
- name: Update policy to add the description
consul_role:
name: foo-role-with-policy
description: "Role for testing policies"
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result['role']['Description'] == "Role for testing policies"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
- result.role.Description == "Role for testing policies"
- result.role.Policies.0.Name == 'foo-access-for-role'
- result.operation == 'update'
- name: Update the role with another policy, also testing leaving description blank
consul_role:
@@ -75,19 +73,18 @@
policies:
- name: "foo-access-for-role"
- name: "bar-access-for-role"
token: "{{ consul_management_token }}"
register: result
- assert:
that:
- result is changed
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
- result['role']['Policies'][1]['Name'] == 'bar-access-for-role'
- result['role']['Description'] == "Role for testing policies"
- result.role.Policies.0.Name == 'foo-access-for-role'
- result.role.Policies.1.Name == 'bar-access-for-role'
- result.role.Description == "Role for testing policies"
- result.operation == 'update'
- name: Create a role with service identity
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
service_identities:
- name: web
@@ -98,12 +95,11 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
- result.role.ServiceIdentities.0.ServiceName == "web"
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- name: Update the role with service identity in check mode
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
service_identities:
- name: web
@@ -115,12 +111,11 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc2"
- result.role.ServiceIdentities.0.ServiceName == "web"
- result.role.ServiceIdentities.0.Datacenters.0 == "dc2"
- name: Update the role with service identity to add a policy, leaving the service id unchanged
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
policies:
- name: "foo-access-for-role"
@@ -129,13 +124,12 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
- result['role']['Policies'][0]['Name'] == 'foo-access-for-role'
- result.role.ServiceIdentities.0.ServiceName == "web"
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result.role.Policies.0.Name == 'foo-access-for-role'
- name: Update the role with service identity to remove the policies
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
policies: []
register: result
@@ -143,13 +137,12 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
- result['role']['Policies'] is not defined
- result.role.ServiceIdentities.0.ServiceName == "web"
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result.role.Policies is not defined
- name: Update the role with service identity to remove the node identities, in check mode
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
node_identities: []
register: result
@@ -158,14 +151,13 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'][0]['ServiceName'] == "web"
- result['role']['ServiceIdentities'][0]['Datacenters'][0] == "dc1"
- result['role']['Policies'] is not defined
- result['role']['NodeIdentities'] == [] # in check mode the cleared field is returned as an empty array
- result.role.ServiceIdentities.0.ServiceName == "web"
- result.role.ServiceIdentities.0.Datacenters.0 == "dc1"
- result.role.Policies is not defined
- result.role.NodeIdentities == [] # in check mode the cleared field is returned as an empty array
- name: Update the role with service identity to remove the service identities
consul_role:
token: "{{ consul_management_token }}"
name: role-with-service-identity
service_identities: []
register: result
@@ -173,12 +165,11 @@
- assert:
that:
- result is changed
- result['role']['ServiceIdentities'] is not defined # in normal mode the dictionary is removed from the result
- result['role']['Policies'] is not defined
- result.role.ServiceIdentities is not defined # in normal mode the dictionary is removed from the result
- result.role.Policies is not defined
- name: Create a role with node identity
consul_role:
token: "{{ consul_management_token }}"
name: role-with-node-identity
node_identities:
- name: node-1
@@ -188,14 +179,16 @@
- assert:
that:
- result is changed
- result['role']['NodeIdentities'][0]['NodeName'] == "node-1"
- result['role']['NodeIdentities'][0]['Datacenter'] == "dc2"
- result.role.NodeIdentities.0.NodeName == "node-1"
- result.role.NodeIdentities.0.Datacenter == "dc2"
- name: Remove the last role
consul_role:
token: "{{ consul_management_token }}"
name: role-with-node-identity
state: absent
register: result
- assert:
that:
- result is changed
- result is changed
- result.operation == 'remove'

View File

@@ -6,7 +6,6 @@
- name: list sessions
consul_session:
state: list
token: "{{ consul_management_token }}"
register: result
- assert:
@@ -18,7 +17,6 @@
consul_session:
state: present
name: testsession
token: "{{ consul_management_token }}"
register: result
- assert:
@@ -33,7 +31,6 @@
- name: list sessions after creation
consul_session:
state: list
token: "{{ consul_management_token }}"
register: result
- set_fact:
@@ -61,7 +58,6 @@
consul_session:
state: info
id: '{{ session_id }}'
token: "{{ consul_management_token }}"
register: result
- assert:
@@ -72,7 +68,6 @@
consul_session:
state: info
name: test
token: "{{ consul_management_token }}"
register: result
ignore_errors: true
@@ -80,70 +75,10 @@
that:
- result is failed
- name: ensure unknown scheme fails
consul_session:
state: info
id: '{{ session_id }}'
scheme: non_existent
token: "{{ consul_management_token }}"
register: result
ignore_errors: true
- assert:
that:
- result is failed
- name: ensure SSL certificate is checked
consul_session:
state: info
id: '{{ session_id }}'
port: 8501
scheme: https
token: "{{ consul_management_token }}"
register: result
ignore_errors: true
- name: previous task should fail since certificate is not known
assert:
that:
- result is failed
- "'certificate verify failed' in result.msg"
- name: ensure SSL certificate isn't checked when validate_certs is disabled
consul_session:
state: info
id: '{{ session_id }}'
port: 8501
scheme: https
token: "{{ consul_management_token }}"
validate_certs: false
register: result
- name: previous task should succeed since certificate isn't checked
assert:
that:
- result is changed
- name: ensure a secure connection is possible
consul_session:
state: info
id: '{{ session_id }}'
port: 8501
scheme: https
token: "{{ consul_management_token }}"
environment:
REQUESTS_CA_BUNDLE: '{{ remote_dir }}/cert.pem'
register: result
- assert:
that:
- result is changed
- name: delete a session
consul_session:
state: absent
id: '{{ session_id }}'
token: "{{ consul_management_token }}"
register: result
- assert:
@@ -153,7 +88,6 @@
- name: list sessions after deletion
consul_session:
state: list
token: "{{ consul_management_token }}"
register: result
- assert:
@@ -180,7 +114,6 @@
state: present
name: session-with-ttl
ttl: 180 # sec
token: "{{ consul_management_token }}"
register: result
- assert:

View File

@@ -0,0 +1,77 @@
---
# Copyright (c) 2024, Florian Apolloner (@apollo13)
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Create a policy with rules
community.general.consul_policy:
name: "{{ item }}"
rules: |
key "foo" {
policy = "read"
}
loop:
- foo-access
- foo-access2
- name: Create token
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
service_identities:
- service_name: test
datacenters: [test1, test2]
node_identities:
- node_name: test
datacenter: test
policies:
- name: foo-access
- name: foo-access2
expiration_ttl: 1h
register: create_result
- assert:
that:
- create_result is changed
- create_result.token.AccessorID == "07a7de84-c9c7-448a-99cc-beaf682efd21"
- create_result.operation == 'create'
- name: Update token
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
description: Testing
policies:
- id: "{{ create_result.token.Policies[-1].ID }}"
service_identities: []
register: result
- assert:
that:
- result is changed
- result.operation == 'update'
- name: Update token (noop)
community.general.consul_token:
state: present
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
policies:
- id: "{{ create_result.token.Policies[-1].ID }}"
register: result
- assert:
that:
- result is not changed
- result.operation is not defined
- name: Remove token
community.general.consul_token:
state: absent
accessor_id: 07a7de84-c9c7-448a-99cc-beaf682efd21
register: result
- assert:
that:
- result is changed
- not result.token
- result.operation == 'remove'

View File

@@ -77,21 +77,30 @@
- name: Start Consul (dev mode enabled)
shell: nohup {{ consul_cmd }} agent -dev -config-file {{ remote_tmp_dir }}/consul_config.hcl </dev/null >/dev/null 2>&1 &
- name: Bootstrap ACL
command: '{{ consul_cmd }} acl bootstrap --format=json'
register: consul_bootstrap_result_string
consul_acl_bootstrap:
register: consul_bootstrap_result
- set_fact:
consul_management_token: '{{ consul_bootstrap_json_result["SecretID"] }}'
vars:
consul_bootstrap_json_result: '{{ consul_bootstrap_result_string.stdout | from_json }}'
consul_management_token: '{{ consul_bootstrap_result.result.SecretID }}'
- name: Create some data
command: '{{ consul_cmd }} kv put -token={{consul_management_token}} data/value{{ item }} foo{{ item }}'
loop:
- 1
- 2
- 3
- import_tasks: consul_session.yml
- import_tasks: consul_policy.yml
- import_tasks: consul_role.yml
- import_tasks: consul_general.yml
- import_tasks: consul_kv.yml
- block:
- import_tasks: consul_session.yml
- import_tasks: consul_policy.yml
- import_tasks: consul_role.yml
- import_tasks: consul_token.yml
- import_tasks: consul_auth_method.yml
- import_tasks: consul_binding_rule.yml
module_defaults:
group/community.general.consul:
token: "{{ consul_management_token }}"
always:
- name: Kill consul process
shell: kill $(cat {{ remote_tmp_dir }}/consul.pid)

View File

@@ -0,0 +1,19 @@
<!--
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
-->
# Gitlab integration tests
1. to run integration tests locally, I've setup a podman pod with both gitlab-ee image and the testing image
2. gitlab's related information were taken from [here](https://docs.gitlab.com/ee/install/docker.html), about the variable it needs (hostname, ports, volumes); volumes were pre-made before launching the image
3. image that run integration tests is started with `podman run --rm -it --pod <pod_name> --name <image_name> --network=host --volume <path_to_git_repo>/ansible_community/community.general:<container_path_to>/workspace/ansible_collections/community/general quay.io/ansible/azure-pipelines-test-container:4.0.1`
4. into the testing image, run
```sh
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
cd <container_path_to>/workspace/ansible_collections/community/general
ansible-test integration gitlab_label -vvv
```
While debugging with `q` package, open a second terminal and run `podman exec -it <image_name> /bin/bash` and inside it do `tail -f /tmp/q` .

View File

@@ -0,0 +1,6 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
gitlab/ci
disabled

View File

@@ -0,0 +1,17 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
gitlab_project_name: ansible_test_project
gitlab_host: ansible_test_host
gitlab_api_token: ansible_test_api_token
gitlab_project_group: ansible_test_group
gitlab_branch: ansible_test_branch
gitlab_first_label: ansible_test_label
gitlab_first_label_color: "#112233"
gitlab_first_label_description: "label description"
gitlab_first_label_priority: 10
gitlab_second_label: ansible_test_second_label
gitlab_second_label_color: "#445566"
gitlab_second_label_new_name: ansible_test_second_label_new_name

View File

@@ -0,0 +1,463 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Install required libs
pip:
name: python-gitlab
state: present
- block:
###
### Group label
###
- name: Create {{ gitlab_project_group }}
gitlab_group:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_group }}"
state: present
- name: Purge all group labels for check_mode test
gitlab_label:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
group: "{{ gitlab_project_group }}"
purge: true
- name: Group label - Add a label in check_mode
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
check_mode: true
register: gitlab_group_label_state
- name: Group label - Check_mode state must be changed
assert:
that:
- gitlab_group_label_state is changed
- name: Group label - Create label {{ gitlab_first_label }} and {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_first_label_color }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_group_label_create
- name: Group label - Test Label Created
assert:
that:
- gitlab_group_label_create is changed
- gitlab_group_label_create.labels.added|length == 2
- gitlab_group_label_create.labels.untouched|length == 0
- gitlab_group_label_create.labels.removed|length == 0
- gitlab_group_label_create.labels.updated|length == 0
- gitlab_group_label_create.labels.added[0] == "{{ gitlab_first_label }}"
- gitlab_group_label_create.labels.added[1] == "{{ gitlab_second_label }}"
- name: Group label - Create Label ( Idempotency test )
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_first_label_color }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
state: present
register: gitlab_group_label_create_idempotence
- name: Group label - Test Create Label is Idempotent
assert:
that:
- gitlab_group_label_create_idempotence is not changed
- name: Group label - Update Label {{ gitlab_first_label }} changing color
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_group_label_update
- name: Group label - Test Label Updated
assert:
that:
- gitlab_group_label_update.labels.added|length == 0
- gitlab_group_label_update.labels.untouched|length == 0
- gitlab_group_label_update.labels.removed|length == 0
- gitlab_group_label_update.labels.updated|length == 1
- gitlab_group_label_update.labels.updated[0] == "{{ gitlab_first_label }}"
- name: Group label - Change label {{ gitlab_second_label }} name to {{ gitlab_second_label_new_name }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_second_label }}"
new_name: "{{ gitlab_second_label_new_name }}"
state: present
register: gitlab_group_label_new_name
- name: Group label - Test Label name changed
assert:
that:
- gitlab_group_label_new_name.labels.added|length == 0
- gitlab_group_label_new_name.labels.untouched|length == 0
- gitlab_group_label_new_name.labels.removed|length == 0
- gitlab_group_label_new_name.labels.updated|length == 1
- gitlab_group_label_new_name.labels.updated[0] == "{{ gitlab_second_label }}"
- name: Group label - Change label name back to {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_second_label_new_name }}"
new_name: "{{ gitlab_second_label }}"
state: present
register: gitlab_group_label_orig_name
- name: Group label - Test Label name changed back
assert:
that:
- gitlab_group_label_orig_name.labels.added|length == 0
- gitlab_group_label_orig_name.labels.untouched|length == 0
- gitlab_group_label_orig_name.labels.removed|length == 0
- gitlab_group_label_orig_name.labels.updated|length == 1
- gitlab_group_label_orig_name.labels.updated[0] == "{{ gitlab_second_label_new_name }}"
- name: Group label - Update Label Test ( Additions )
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_second_label }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
state: present
register: gitlab_group_label_update_additions
- name: Group label - Test Label Updated ( Additions )
assert:
that:
- gitlab_group_label_update_additions.labels.added|length == 0
- gitlab_group_label_update_additions.labels.untouched|length == 0
- gitlab_group_label_update_additions.labels.removed|length == 0
- gitlab_group_label_update_additions.labels.updated|length == 1
- gitlab_group_label_update_additions.labels.updated[0] == "{{ gitlab_second_label }}"
- name: Group label - Delete Label {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
labels:
- name: "{{ gitlab_second_label }}"
state: absent
register: gitlab_group_label_delete
- name: Group label - Test label is deleted
assert:
that:
- gitlab_group_label_delete is changed
- gitlab_group_label_delete.labels.added|length == 0
- gitlab_group_label_delete.labels.untouched|length == 0
- gitlab_group_label_delete.labels.removed|length == 1
- gitlab_group_label_delete.labels.updated|length == 0
- gitlab_group_label_delete.labels.removed[0] == "{{ gitlab_second_label }}"
- name: Group label - Create label {{ gitlab_second_label }} again purging the other
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
purge: true
labels:
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_group_label_create_purging
- name: Group label - Test Label Created again
assert:
that:
- gitlab_group_label_create_purging is changed
- gitlab_group_label_create_purging.labels.added|length == 1
- gitlab_group_label_create_purging.labels.untouched|length == 0
- gitlab_group_label_create_purging.labels.removed|length == 1
- gitlab_group_label_create_purging.labels.updated|length == 0
- gitlab_group_label_create_purging.labels.added[0] == "{{ gitlab_second_label }}"
- gitlab_group_label_create_purging.labels.removed[0] == "{{ gitlab_first_label }}"
###
### Project label
###
- name: Create {{ gitlab_project_name }}
gitlab_project:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_name }}"
group: "{{ gitlab_project_group }}"
default_branch: "{{ gitlab_branch }}"
initialize_with_readme: true
state: present
- name: Purge all labels for check_mode test
gitlab_label:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
- name: Add a label in check_mode
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
check_mode: true
register: gitlab_first_label_state
- name: Check_mode state must be changed
assert:
that:
- gitlab_first_label_state is changed
- name: Create label {{ gitlab_first_label }} and {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_first_label_color }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_first_label_create
- name: Test Label Created
assert:
that:
- gitlab_first_label_create is changed
- gitlab_first_label_create.labels.added|length == 2
- gitlab_first_label_create.labels.untouched|length == 0
- gitlab_first_label_create.labels.removed|length == 0
- gitlab_first_label_create.labels.updated|length == 0
- gitlab_first_label_create.labels.added[0] == "{{ gitlab_first_label }}"
- gitlab_first_label_create.labels.added[1] == "{{ gitlab_second_label }}"
- name: Create Label ( Idempotency test )
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_first_label_color }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
state: present
register: gitlab_first_label_create_idempotence
- name: Test Create Label is Idempotent
assert:
that:
- gitlab_first_label_create_idempotence is not changed
- name: Update Label {{ gitlab_first_label }} changing color
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_first_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_first_label_update
- name: Test Label Updated
assert:
that:
- gitlab_first_label_update.labels.added|length == 0
- gitlab_first_label_update.labels.untouched|length == 0
- gitlab_first_label_update.labels.removed|length == 0
- gitlab_first_label_update.labels.updated|length == 1
- gitlab_first_label_update.labels.updated[0] == "{{ gitlab_first_label }}"
- name: Change label {{ gitlab_second_label }} name to {{ gitlab_second_label_new_name }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_second_label }}"
new_name: "{{ gitlab_second_label_new_name }}"
state: present
register: gitlab_first_label_new_name
- name: Test Label name changed
assert:
that:
- gitlab_first_label_new_name.labels.added|length == 0
- gitlab_first_label_new_name.labels.untouched|length == 0
- gitlab_first_label_new_name.labels.removed|length == 0
- gitlab_first_label_new_name.labels.updated|length == 1
- gitlab_first_label_new_name.labels.updated[0] == "{{ gitlab_second_label }}"
- name: Change label name back to {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_second_label_new_name }}"
new_name: "{{ gitlab_second_label }}"
state: present
register: gitlab_first_label_orig_name
- name: Test Label name changed back
assert:
that:
- gitlab_first_label_orig_name.labels.added|length == 0
- gitlab_first_label_orig_name.labels.untouched|length == 0
- gitlab_first_label_orig_name.labels.removed|length == 0
- gitlab_first_label_orig_name.labels.updated|length == 1
- gitlab_first_label_orig_name.labels.updated[0] == "{{ gitlab_second_label_new_name }}"
- name: Update Label Test ( Additions )
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_second_label }}"
description: "{{ gitlab_first_label_description }}"
priority: "{{ gitlab_first_label_priority }}"
state: present
register: gitlab_first_label_update_additions
- name: Test Label Updated ( Additions )
assert:
that:
- gitlab_first_label_update_additions.labels.added|length == 0
- gitlab_first_label_update_additions.labels.untouched|length == 0
- gitlab_first_label_update_additions.labels.removed|length == 0
- gitlab_first_label_update_additions.labels.updated|length == 1
- gitlab_first_label_update_additions.labels.updated[0] == "{{ gitlab_second_label }}"
- name: Delete Label {{ gitlab_second_label }}
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
labels:
- name: "{{ gitlab_second_label }}"
state: absent
register: gitlab_first_label_delete
- name: Test label is deleted
assert:
that:
- gitlab_first_label_delete is changed
- gitlab_first_label_delete.labels.added|length == 0
- gitlab_first_label_delete.labels.untouched|length == 0
- gitlab_first_label_delete.labels.removed|length == 1
- gitlab_first_label_delete.labels.updated|length == 0
- gitlab_first_label_delete.labels.removed[0] == "{{ gitlab_second_label }}"
- name: Create label {{ gitlab_second_label }} again purging the other
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
labels:
- name: "{{ gitlab_second_label }}"
color: "{{ gitlab_second_label_color }}"
state: present
register: gitlab_first_label_create_purging
- name: Test Label Created again
assert:
that:
- gitlab_first_label_create_purging is changed
- gitlab_first_label_create_purging.labels.added|length == 1
- gitlab_first_label_create_purging.labels.untouched|length == 0
- gitlab_first_label_create_purging.labels.removed|length == 1
- gitlab_first_label_create_purging.labels.updated|length == 0
- gitlab_first_label_create_purging.labels.added[0] == "{{ gitlab_second_label }}"
- gitlab_first_label_create_purging.labels.removed[0] == "{{ gitlab_first_label }}"
always:
- name: Delete Labels
gitlab_label:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
labels:
- name: "{{ gitlab_first_label }}"
- name: "{{ gitlab_second_label }}"
state: absent
register: gitlab_first_label_always_delete
- name: Test label are deleted
assert:
that:
- gitlab_first_label_always_delete is changed
- gitlab_first_label_always_delete.labels.added|length == 0
- gitlab_first_label_always_delete.labels.untouched|length == 0
- gitlab_first_label_always_delete.labels.removed|length > 0
- gitlab_first_label_always_delete.labels.updated|length == 0
- name: Clean up {{ gitlab_project_name }}
gitlab_project:
api_url: "{{ gitlab_host }}"
validate_certs: false
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_name }}"
group: "{{ gitlab_project_group }}"
state: absent
- name: Clean up {{ gitlab_project_group }}
gitlab_group:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_group }}"
state: absent

View File

@@ -0,0 +1,19 @@
<!--
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
-->
# Gitlab integration tests
1. to run integration tests locally, I've setup a podman pod with both gitlab-ee image and the testing image
2. gitlab's related information were taken from [here](https://docs.gitlab.com/ee/install/docker.html), about the variable it needs (hostname, ports, volumes); volumes were pre-made before launching the image
3. image that run integration tests is started with `podman run --rm -it --pod <pod_name> --name <image_name> --network=host --volume <path_to_git_repo>/ansible_community/community.general:<container_path_to>/workspace/ansible_collections/community/general quay.io/ansible/azure-pipelines-test-container:4.0.1`
4. into the testing image, run
```sh
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
cd <container_path_to>/workspace/ansible_collections/community/general
ansible-test integration gitlab_milestone -vvv
```
While debugging with `q` package, open a second terminal and run `podman exec -it <image_name> /bin/bash` and inside it do `tail -f /tmp/q` .

View File

@@ -0,0 +1,6 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
gitlab/ci
disabled

View File

@@ -0,0 +1,18 @@
---
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
gitlab_project_name: ansible_test_project
gitlab_host: ansible_test_host
gitlab_api_token: ansible_test_api_token
gitlab_project_group: ansible_test_group
gitlab_branch: ansible_test_branch
gitlab_first_milestone: ansible_test_milestone
gitlab_first_milestone_description: "milestone description"
gitlab_first_milestone_start_date: "2024-01-01"
gitlab_first_milestone_due_date: "2024-12-31"
gitlab_first_milestone_new_start_date: "2024-05-01"
gitlab_first_milestone_new_due_date: "2024-10-31"
gitlab_second_milestone: ansible_test_second_milestone
gitlab_second_milestone_description: "new milestone"

View File

@@ -0,0 +1,388 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Install required libs
pip:
name: python-gitlab
state: present
- block:
###
### Group milestone
###
- name: Create {{ gitlab_project_group }}
gitlab_group:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_group }}"
state: present
- name: Purge all group milestones for check_mode test
gitlab_milestone:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
group: "{{ gitlab_project_group }}"
purge: true
- name: Group milestone - Add a milestone in check_mode
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
check_mode: true
register: gitlab_group_milestone_state
- name: Group milestone - Check_mode state must be changed
assert:
that:
- gitlab_group_milestone_state is changed
- name: Purge all group milestones for project milestone test - cannot exist with same name
gitlab_milestone:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
group: "{{ gitlab_project_group }}"
purge: true
register: gitlab_group_milestone_purged
- name: Group milestone - Create milestone {{ gitlab_first_milestone }} and {{ gitlab_second_milestone }}
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_start_date }}"
due_date: "{{ gitlab_first_milestone_due_date }}"
- title: "{{ gitlab_second_milestone }}"
state: present
register: gitlab_group_milestone_create
- name: Group milestone - Test milestone Created
assert:
that:
- gitlab_group_milestone_create is changed
- gitlab_group_milestone_create.milestones.added|length == 2
- gitlab_group_milestone_create.milestones.untouched|length == 0
- gitlab_group_milestone_create.milestones.removed|length == 0
- gitlab_group_milestone_create.milestones.updated|length == 0
- gitlab_group_milestone_create.milestones.added[0] == "{{ gitlab_first_milestone }}"
- gitlab_group_milestone_create.milestones.added[1] == "{{ gitlab_second_milestone }}"
- name: Group milestone - Create milestone ( Idempotency test )
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_start_date }}"
due_date: "{{ gitlab_first_milestone_due_date }}"
state: present
register: gitlab_group_milestone_create_idempotence
- name: Group milestone - Test Create milestone is Idempotent
assert:
that:
- gitlab_group_milestone_create_idempotence is not changed
- name: Group milestone - Update milestone {{ gitlab_first_milestone }} changing dates
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_new_start_date }}"
due_date: "{{ gitlab_first_milestone_new_due_date }}"
state: present
register: gitlab_group_milestone_update
- name: Group milestone - Test milestone Updated
assert:
that:
- gitlab_group_milestone_update.milestones.added|length == 0
- gitlab_group_milestone_update.milestones.untouched|length == 0
- gitlab_group_milestone_update.milestones.removed|length == 0
- gitlab_group_milestone_update.milestones.updated|length == 1
- gitlab_group_milestone_update.milestones.updated[0] == "{{ gitlab_first_milestone }}"
- name: Group milestone - Update milestone Test ( Additions )
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
description: "{{ gitlab_first_milestone_description }}"
state: present
register: gitlab_group_milestone_update_additions
- name: Group milestone - Test milestone Updated ( Additions )
assert:
that:
- gitlab_group_milestone_update_additions.milestones.added|length == 0
- gitlab_group_milestone_update_additions.milestones.untouched|length == 0
- gitlab_group_milestone_update_additions.milestones.removed|length == 0
- gitlab_group_milestone_update_additions.milestones.updated|length == 1
- gitlab_group_milestone_update_additions.milestones.updated[0] == "{{ gitlab_second_milestone }}"
- name: Group milestone - Delete milestone {{ gitlab_second_milestone }}
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
state: absent
register: gitlab_group_milestone_delete
- name: Group milestone - Test milestone is deleted
assert:
that:
- gitlab_group_milestone_delete is changed
- gitlab_group_milestone_delete.milestones.added|length == 0
- gitlab_group_milestone_delete.milestones.untouched|length == 0
- gitlab_group_milestone_delete.milestones.removed|length == 1
- gitlab_group_milestone_delete.milestones.updated|length == 0
- gitlab_group_milestone_delete.milestones.removed[0] == "{{ gitlab_second_milestone }}"
- name: Group milestone - Create group milestone {{ gitlab_second_milestone }} again purging the other
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
group: "{{ gitlab_project_group }}"
purge: true
milestones:
- title: "{{ gitlab_second_milestone }}"
state: present
register: gitlab_group_milestone_create_purging
- name: Group milestone - Test milestone Created again
assert:
that:
- gitlab_group_milestone_create_purging is changed
- gitlab_group_milestone_create_purging.milestones.added|length == 1
- gitlab_group_milestone_create_purging.milestones.untouched|length == 0
- gitlab_group_milestone_create_purging.milestones.removed|length == 1
- gitlab_group_milestone_create_purging.milestones.updated|length == 0
- gitlab_group_milestone_create_purging.milestones.added[0] == "{{ gitlab_second_milestone }}"
- gitlab_group_milestone_create_purging.milestones.removed[0] == "{{ gitlab_first_milestone }}"
###
### Project milestone
###
- name: Purge all group milestones for project milestone test - cannot exist with same name
gitlab_milestone:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
group: "{{ gitlab_project_group }}"
purge: true
register: gitlab_group_milestone_purged
- name: Create {{ gitlab_project_name }}
gitlab_project:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_name }}"
group: "{{ gitlab_project_group }}"
default_branch: "{{ gitlab_branch }}"
initialize_with_readme: true
state: present
- name: Purge all milestones for check_mode test
gitlab_milestone:
api_url: "{{ gitlab_host }}"
api_token: "{{ gitlab_api_token }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
- name: Add a milestone in check_mode
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
description: "{{ gitlab_second_milestone_description }}"
check_mode: true
register: gitlab_first_milestone_state
- name: Check_mode state must be changed
assert:
that:
- gitlab_first_milestone_state is changed
- name: Create milestone {{ gitlab_first_milestone }} and {{ gitlab_second_milestone }}
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_start_date }}"
due_date: "{{ gitlab_first_milestone_due_date }}"
- title: "{{ gitlab_second_milestone }}"
state: present
register: gitlab_milestones_create
- name: Test milestone Created
assert:
that:
- gitlab_milestones_create is changed
- gitlab_milestones_create.milestones.added|length == 2
- gitlab_milestones_create.milestones.untouched|length == 0
- gitlab_milestones_create.milestones.removed|length == 0
- gitlab_milestones_create.milestones.updated|length == 0
- gitlab_milestones_create.milestones.added[0] == "{{ gitlab_first_milestone }}"
- gitlab_milestones_create.milestones.added[1] == "{{ gitlab_second_milestone }}"
- name: Create milestone ( Idempotency test )
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_start_date }}"
due_date: "{{ gitlab_first_milestone_due_date }}"
state: present
register: gitlab_first_milestone_create_idempotence
- name: Test Create milestone is Idempotent
assert:
that:
- gitlab_first_milestone_create_idempotence is not changed
- name: Update milestone {{ gitlab_first_milestone }} changing dates
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_first_milestone }}"
start_date: "{{ gitlab_first_milestone_new_start_date }}"
due_date: "{{ gitlab_first_milestone_new_due_date }}"
state: present
register: gitlab_first_milestone_update
- name: Test milestone Updated
assert:
that:
- gitlab_first_milestone_update.milestones.added|length == 0
- gitlab_first_milestone_update.milestones.untouched|length == 0
- gitlab_first_milestone_update.milestones.removed|length == 0
- gitlab_first_milestone_update.milestones.updated|length == 1
- gitlab_first_milestone_update.milestones.updated[0] == "{{ gitlab_first_milestone }}"
- name: Update milestone Test ( Additions )
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
description: "{{ gitlab_second_milestone_description }}"
state: present
register: gitlab_first_milestone_update_additions
- name: Test milestone Updated ( Additions )
assert:
that:
- gitlab_first_milestone_update_additions.milestones.added|length == 0
- gitlab_first_milestone_update_additions.milestones.untouched|length == 0
- gitlab_first_milestone_update_additions.milestones.removed|length == 0
- gitlab_first_milestone_update_additions.milestones.updated|length == 1
- gitlab_first_milestone_update_additions.milestones.updated[0] == "{{ gitlab_second_milestone }}"
- name: Delete milestone {{ gitlab_second_milestone }}
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
milestones:
- title: "{{ gitlab_second_milestone }}"
state: absent
register: gitlab_first_milestone_delete
- name: Test milestone is deleted
assert:
that:
- gitlab_first_milestone_delete is changed
- gitlab_first_milestone_delete.milestones.added|length == 0
- gitlab_first_milestone_delete.milestones.untouched|length == 0
- gitlab_first_milestone_delete.milestones.removed|length == 1
- gitlab_first_milestone_delete.milestones.updated|length == 0
- gitlab_first_milestone_delete.milestones.removed[0] == "{{ gitlab_second_milestone }}"
- name: Create milestone {{ gitlab_second_milestone }} again purging the other
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
milestones:
- title: "{{ gitlab_second_milestone }}"
state: present
register: gitlab_first_milestone_create_purging
- name: Test milestone Created again
assert:
that:
- gitlab_first_milestone_create_purging is changed
- gitlab_first_milestone_create_purging.milestones.added|length == 1
- gitlab_first_milestone_create_purging.milestones.untouched|length == 0
- gitlab_first_milestone_create_purging.milestones.removed|length == 1
- gitlab_first_milestone_create_purging.milestones.updated|length == 0
- gitlab_first_milestone_create_purging.milestones.added[0] == "{{ gitlab_second_milestone }}"
- gitlab_first_milestone_create_purging.milestones.removed[0] == "{{ gitlab_first_milestone }}"
always:
- name: Delete milestones
gitlab_milestone:
api_token: "{{ gitlab_api_token }}"
api_url: "{{ gitlab_host }}"
project: "{{ gitlab_project_group }}/{{ gitlab_project_name }}"
purge: true
milestones:
- title: "{{ gitlab_first_milestone }}"
- title: "{{ gitlab_second_milestone }}"
state: absent
register: gitlab_first_milestone_always_delete
- name: Test milestone are deleted
assert:
that:
- gitlab_first_milestone_always_delete is changed
- gitlab_first_milestone_always_delete.milestones.added|length == 0
- gitlab_first_milestone_always_delete.milestones.untouched|length == 0
- gitlab_first_milestone_always_delete.milestones.removed|length > 0
- gitlab_first_milestone_always_delete.milestones.updated|length == 0
- name: Clean up {{ gitlab_project_name }}
gitlab_project:
api_url: "{{ gitlab_host }}"
validate_certs: false
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_name }}"
group: "{{ gitlab_project_group }}"
state: absent
- name: Clean up {{ gitlab_project_group }}
gitlab_group:
api_url: "{{ gitlab_host }}"
validate_certs: true
api_token: "{{ gitlab_api_token }}"
name: "{{ gitlab_project_group }}"
state: absent

View File

@@ -0,0 +1,99 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Test code for the homebrew module.
# Copyright (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Find brew binary
command: which brew
register: brew_which
when: ansible_distribution in ['MacOSX']
- name: Get owner of brew binary
stat:
path: "{{ brew_which.stdout }}"
register: brew_stat
when: ansible_distribution in ['MacOSX']
#- name: Use ignored-pinned option while upgrading all
# homebrew:
# upgrade_all: true
# upgrade_options: ignore-pinned
# become: true
# become_user: "{{ brew_stat.stat.pw_name }}"
# register: upgrade_option_result
# environment:
# HOMEBREW_NO_AUTO_UPDATE: True
#- assert:
# that:
# - upgrade_option_result.changed
- vars:
package_name: kitty
block:
- name: Make sure {{ package_name }} package is not installed
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
- name: Install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed
- name: Uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed

View File

@@ -0,0 +1,99 @@
---
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
# Test code for the homebrew module.
# Copyright (c) 2020, Abhijeet Kasurde <akasurde@redhat.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Find brew binary
command: which brew
register: brew_which
when: ansible_distribution in ['MacOSX']
- name: Get owner of brew binary
stat:
path: "{{ brew_which.stdout }}"
register: brew_stat
when: ansible_distribution in ['MacOSX']
#- name: Use ignored-pinned option while upgrading all
# homebrew:
# upgrade_all: true
# upgrade_options: ignore-pinned
# become: true
# become_user: "{{ brew_stat.stat.pw_name }}"
# register: upgrade_option_result
# environment:
# HOMEBREW_NO_AUTO_UPDATE: True
#- assert:
# that:
# - upgrade_option_result.changed
- vars:
package_name: gnu-tar
block:
- name: Make sure {{ package_name }} package is not installed
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
- name: Install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed
- name: Uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed

View File

@@ -9,91 +9,9 @@
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
- name: Find brew binary
command: which brew
register: brew_which
when: ansible_distribution in ['MacOSX']
- name: Get owner of brew binary
stat:
path: "{{ brew_which.stdout }}"
register: brew_stat
when: ansible_distribution in ['MacOSX']
#- name: Use ignored-pinned option while upgrading all
# homebrew:
# upgrade_all: true
# upgrade_options: ignore-pinned
# become: true
# become_user: "{{ brew_stat.stat.pw_name }}"
# register: upgrade_option_result
# environment:
# HOMEBREW_NO_AUTO_UPDATE: True
#- assert:
# that:
# - upgrade_option_result.changed
- vars:
package_name: gnu-tar
- block:
- include_tasks: 'formulae.yml'
- when: ansible_distribution in ['MacOSX']
block:
- name: Make sure {{ package_name }} package is not installed
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
- name: Install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again install {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: present
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed
- name: Uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- package_result.changed
- name: Again uninstall {{ package_name }} package using homebrew
homebrew:
name: "{{ package_name }}"
state: absent
update_homebrew: false
become: true
become_user: "{{ brew_stat.stat.pw_name }}"
register: package_result
- assert:
that:
- not package_result.changed
- include_tasks: 'casks.yml'

View File

@@ -45,7 +45,7 @@
- name: Copy templated helper script
template:
src: obtainpid.sh
src: obtainpid.sh.j2
dest: "{{ remote_tmp_dir }}/obtainpid.sh"
mode: 0755

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN CERTIFICATE-----
MIIDAjCCAeqgAwIBAgIJANguFROhaWocMA0GCSqGSIb3DQEBCwUAMDExIDAeBgNV
BAMMF1RMU0dlblNlbGZTaWduZWR0Um9vdENBMQ0wCwYDVQQHDAQkJCQkMB4XDTE5

View File

@@ -1,3 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: Ansible Project

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDqVt84czSxWnWW
4Ng6hmKE3NarbLsycwtjrYBokV7Kk7Mp7PrBbYF05FOgSdJLvL6grlRSQK2VPsXd

View File

@@ -1,3 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later
SPDX-FileCopyrightText: Ansible Project

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN CERTIFICATE-----
MIIDRjCCAi6gAwIBAgIBAjANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH
ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAeFw0xOTAxMTEwODMz

View File

@@ -0,0 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAqDPjkNxwpwlAAM/Shhk8FgfYUG1HwGV5v7LZW9v7jgKd6zcM
QJQrP4IspgRxOiLupqytNOlZ/mfYm6iKw9i7gjsXLtucvIKKhutk4HT+bGvcEfuf

View File

@@ -0,0 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN CERTIFICATE-----
MIIDRjCCAi6gAwIBAgIBATANBgkqhkiG9w0BAQsFADAxMSAwHgYDVQQDDBdUTFNH
ZW5TZWxmU2lnbmVkdFJvb3RDQTENMAsGA1UEBwwEJCQkJDAeFw0xOTAxMTEwODMz

View File

@@ -0,0 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -1,7 +1,3 @@
# Copyright (c) Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAyMBKx8AHrEQX3fR4mZJgd1WIdvHNUJBPSPJ2MhySl9mQVIQM
yvofNAZHEySfeNuualsgAh/8JeeF3v6HxVBaxmuL89Ks+FJC/yiNDhsNvGOKpyna

View File

@@ -0,0 +1,3 @@
Copyright (c) Ansible Project
GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
SPDX-License-Identifier: GPL-3.0-or-later

View File

@@ -77,6 +77,7 @@
when:
- ansible_facts.distribution ~ ansible_facts.distribution_major_version not in ['Fedora31', 'Fedora32']
- not (ansible_os_family == 'Alpine') # TODO
- not (ansible_distribution == 'Archlinux') # TODO
block:
- name: set timezone to Etc/UTC
timezone:

View File

@@ -14,6 +14,15 @@
state: present
when: ansible_os_family == "FreeBSD"
- name: Install requirements (RHEL 8)
package:
name:
- libxml2-devel
- libxslt-devel
- python3-lxml
state: present
when: ansible_distribution == "RedHat" and ansible_distribution_major_version == "8"
# Needed for MacOSX !
- name: Install lxml
pip:

View File

@@ -1,20 +0,0 @@
.azure-pipelines/scripts/publish-codecov.py compile-2.6!skip # Uses Python 3.6+ syntax
.azure-pipelines/scripts/publish-codecov.py compile-2.7!skip # Uses Python 3.6+ syntax
.azure-pipelines/scripts/publish-codecov.py compile-3.5!skip # Uses Python 3.6+ syntax
.azure-pipelines/scripts/publish-codecov.py future-import-boilerplate
.azure-pipelines/scripts/publish-codecov.py metaclass-boilerplate
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice
plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin
plugins/modules/lxc_container.py validate-modules:use-run-command-not-popen
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax_files_objects.py use-argspec-type-path # module deprecated - removed in 9.0.0
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice # module deprecated - removed in 9.0.0
plugins/modules/rax.py use-argspec-type-path # module deprecated - removed in 9.0.0
plugins/modules/read_csv.py validate-modules:invalid-documentation
plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice
plugins/modules/xfconf.py validate-modules:return-syntax-error
tests/integration/targets/django_manage/files/base_test/simple_project/p1/manage.py compile-2.6 # django generated code
tests/integration/targets/django_manage/files/base_test/simple_project/p1/manage.py compile-2.7 # django generated code
tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes

View File

@@ -1,13 +0,0 @@
.azure-pipelines/scripts/publish-codecov.py replace-urlopen
plugins/modules/consul_session.py validate-modules:parameter-state-invalid-choice
plugins/modules/iptables_state.py validate-modules:undocumented-parameter # params _back and _timeout used by action plugin
plugins/modules/lxc_container.py validate-modules:use-run-command-not-popen
plugins/modules/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/rax_files_objects.py use-argspec-type-path # module deprecated - removed in 9.0.0
plugins/modules/rax_files.py validate-modules:parameter-state-invalid-choice # module deprecated - removed in 9.0.0
plugins/modules/rax.py use-argspec-type-path # module deprecated - removed in 9.0.0
plugins/modules/read_csv.py validate-modules:invalid-documentation
plugins/modules/rhevm.py validate-modules:parameter-state-invalid-choice
plugins/modules/xfconf.py validate-modules:return-syntax-error
tests/unit/plugins/modules/test_gio_mime.yaml no-smart-quotes

View File

@@ -104,7 +104,7 @@ class TestIPAOTPToken(ModuleTestCase):
{
'method': 'otptoken_add',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'FALSE',
'item': {'ipatokendisabled': False,
'all': True}
}
)
@@ -130,7 +130,7 @@ class TestIPAOTPToken(ModuleTestCase):
{
'method': 'otptoken_add',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'FALSE',
'item': {'ipatokendisabled': False,
'all': True}
}
)
@@ -176,7 +176,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': 'KRSXG5CTMVRXEZLUGE======',
'description': 'Test description',
'ipatokenowner': 'pinky',
'ipatokendisabled': 'FALSE',
'ipatokendisabled': False,
'ipatokennotbefore': '20200101010101Z',
'ipatokennotafter': '20900101010101Z',
'ipatokenvendor': 'Acme',
@@ -220,7 +220,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokendisabled': [False],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
@@ -271,7 +271,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokendisabled': [False],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
@@ -296,7 +296,7 @@ class TestIPAOTPToken(ModuleTestCase):
'name': 'NewToken1',
'item': {'description': 'Test description',
'ipatokenowner': 'brain',
'ipatokendisabled': 'FALSE',
'ipatokendisabled': False,
'ipatokennotbefore': '20200101010101Z',
'ipatokennotafter': '20900101010101Z',
'ipatokenvendor': 'Acme',
@@ -335,7 +335,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokendisabled': [False],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
@@ -360,7 +360,7 @@ class TestIPAOTPToken(ModuleTestCase):
'name': 'NewToken1',
'item': {'description': 'New Test description',
'ipatokenowner': 'pinky',
'ipatokendisabled': 'TRUE',
'ipatokendisabled': True,
'ipatokennotbefore': '20200101010102Z',
'ipatokennotafter': '20900101010102Z',
'ipatokenvendor': 'NewAcme',
@@ -384,7 +384,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': [{'__base64__': 'KRSXG5CTMVRXEZLUGE======'}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokendisabled': [False],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
@@ -425,7 +425,7 @@ class TestIPAOTPToken(ModuleTestCase):
'ipatokenotpkey': [{'__base64__': 'KRSXG5CTMVRXEZLUGE======'}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokendisabled': [False],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
@@ -448,7 +448,7 @@ class TestIPAOTPToken(ModuleTestCase):
{
'method': 'otptoken_mod',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'TRUE',
'item': {'ipatokendisabled': True,
'all': True}
}
)

View File

@@ -11,6 +11,7 @@ python-memcached ; python_version >= '3.6'
# requirement for the redis cache plugin
redis
async-timeout ; python_version == '3.11'
# requirement for the linode module
linode-python # APIv3

View File

@@ -65,16 +65,7 @@ else
retry pip install "https://github.com/ansible/ansible/archive/stable-${ansible_version}.tar.gz" --disable-pip-version-check
fi
if [ "${SHIPPABLE_BUILD_ID:-}" ]; then
export ANSIBLE_COLLECTIONS_PATHS="${HOME}/.ansible"
SHIPPABLE_RESULT_DIR="$(pwd)/shippable"
TEST_DIR="${ANSIBLE_COLLECTIONS_PATHS}/ansible_collections/community/general"
mkdir -p "${TEST_DIR}"
cp -aT "${SHIPPABLE_BUILD_DIR}" "${TEST_DIR}"
cd "${TEST_DIR}"
else
export ANSIBLE_COLLECTIONS_PATHS="${PWD}/../../../"
fi
export ANSIBLE_COLLECTIONS_PATHS="${PWD}/../../../"
if [ "${test}" == "sanity/extra" ]; then
retry pip install junit-xml --disable-pip-version-check