Compare commits

..

38 Commits
2.4.0 ... 2.5.0

Author SHA1 Message Date
Felix Fontein
72c1a17bd9 Release 2.5.0. 2021-04-13 12:53:56 +02:00
patchback[bot]
694584f907 Add Jira attach operation (#2192) (#2231)
* Add Jira attach operation

Adds the `attach` operation to the `web_infrastructure.jira` module,
which allows a user to attach a file to an issue. The user can supply
either the path to a file, which will be read from storage, or a file
name and content (as bytes).

* Apply suggestions from code review

* Update plugins/modules/web_infrastructure/jira.py

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
(cherry picked from commit 98af8161b2)

Co-authored-by: Brandon McNama <brandonmcnama@outlook.com>
2021-04-13 08:06:52 +02:00
patchback[bot]
73e2c2eb85 Proxmox_Inv: Adding agent network interaces fact (#2148) (#2228)
* Added agent network interaces fact

* Adding changelog fragment

* More concise looping over interfaces

* Adding unit test case for agent interfaces

* Correcting whitespace issue

* Commented new dummy json returns with corresponding method

(cherry picked from commit 8ab356520d)

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-04-12 20:58:36 +00:00
patchback[bot]
f3ddc8757d spectrum_model_attrs: Initial commit (#1802) (#2229)
* spectrum_model_attrs: Initial commit

* spectrum_model_attrs: sanity check fixes (1)

* Apply suggestions from code review

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

* Apply suggestions from code review:
 * Removed ANSIBLE_METADATA.
 * List all currently supported names in DOCUMENTATION block.
 * Don't escape declarations that are long enough to fit on one
 line.

* Apply suggestions from code review:
  * YAML bools in DOCUMENTATION block.
  * Various DOCUMENTATION block aesthetics.
  * RETURN block proper format.
  * 'yes' -> True declaration in argument spec.
  * import urlencode from python 2 and 3 changed to
    six.moves.urllib.quote.

* spectrum_model_attrs: integration test added.

* Update plugins/modules/monitoring/spectrum_model_attrs.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Update plugins/modules/monitoring/spectrum_model_attrs.py

Co-authored-by: Amin Vakil <info@aminvakil.com>

* spectrum_model_attrs: lint error fixes.

Co-authored-by: Tyler Gates <tgates@citco.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
Co-authored-by: Amin Vakil <info@aminvakil.com>
(cherry picked from commit 1f001cafd9)

Co-authored-by: tgates81 <31669870+tgates81@users.noreply.github.com>
2021-04-12 20:54:00 +00:00
patchback[bot]
9241b853c0 java_keystore: improve error handling and returned results (#2183) (#2227)
* java_keystore - improve error handling and returned results

* set check_rc=False to return results as documented when module fails
* set LANG, LC_ALL and LC_MESSAGES to C to rely keytool output parsing
* fix pylint's `no-else-return` and `unused-variable` hints
* update related unit tests accordingly
* add a changelog fragment

update unit test (remove stdout_lines from returned dict)

fix unit test: failure is now expected when alias does not exist

* Update changelogs/fragments/2183-java_keystore_improve_error_handling.yml

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

* fix integration test: overwrite keystore at the same location

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

Co-authored-by: quidame <quidame@poivron.org>
2021-04-12 22:13:36 +02:00
patchback[bot]
1053b3c658 Grant supershipit to new maintainers (#2214) (#2222)
* Grant supershipit to a new maintainer

* Add maintainer

(cherry picked from commit 7356451aa1)

Co-authored-by: Andrew Klychkov <aklychko@redhat.com>
2021-04-12 11:24:13 +02:00
Felix Fontein
d9daa6b851 Fix typo. 2021-04-12 10:35:27 +02:00
Felix Fontein
a876fa0262 Prepare 2.5.0 release. 2021-04-11 17:30:47 +02:00
patchback[bot]
f64ace97af Added modules ipa_otpconfig and ipa_otptoken (#2122) (#2219)
* Added module for ipa_otpconfig

* Make no_log=False explicit.

* Updated inputs to be int type instead of strings to align to expected inputs.  Updated output message

* Add changelog fragment

* Remove changelog fragment as this is a new module

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

Add version_added field to module description.

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

* Updated punctuation in examples

* Add unit test for ipa_otpconfig

* Add ipa_otptoken module with unit test

* Updated documentation in unit test

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Added some documentation updates to make it conform to ansible standards

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

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

* Address review comments

Co-authored-by: Chris Costa <chris.costa@compellingtech.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 31645ded11)

Co-authored-by: justchris1 <30219018+justchris1@users.noreply.github.com>
2021-04-11 15:53:33 +02:00
patchback[bot]
b701b5893f npm: Add no_bin_links option (#2146) (#2217)
* Add no-bin-links option to npm

* Add changelog

* Fix changelog format

* Add integration test

* Change node package from thelounge to ncp

(cherry picked from commit fa13826273)

Co-authored-by: Amin Vakil <info@aminvakil.com>
2021-04-11 06:25:22 +02:00
patchback[bot]
24667e12d0 Added fields to the ipa_config module (#2116) (#2216)
* Added fields to the ipa_config module: ipadefaultprimarygroup, ipagroupsearchfields, ipahomesrootdir, ipamaxusernamelength, ipapwdexpadvnotify, ipasearchrecordslimit, ipasearchtimelimit, ipauserauthtype, ipausersearchfields

* Fixed typos in documentation spec

* Updated a field that was missing the version_added decoration

* Add changelog fragment

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

Cleanup example to be consistent with others.

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

* Cleanup example to be consistent with others.

* Fixed changelog fragment

* Updated punctuation in examples

* Switched some elements to use int instead of str, and fixed duplicated example

* Change type of field for ipauserauthtype to list of str, add support for ipaconfigstring and ipakrbauthzdata

* Update fragment to represent adding support for ipaconfigstring and ipakrbauthzdata

* Update changelogs/fragments/2116-add-fields-to-ipa-config-module.yml

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

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

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

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

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

* Address review comments by making inputs into group search and user search fields a list of strings, even though IPA does not treat it as a multiselect field

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

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

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

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

Co-authored-by: Chris Costa <chris.costa@compellingtech.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 5502e4ec17)

Co-authored-by: justchris1 <30219018+justchris1@users.noreply.github.com>
2021-04-11 06:25:04 +02:00
patchback[bot]
9d93760564 Bugfix: PyGithub does not support explicit port in base_url (#2204) (#2215)
* Bugfix: PyGithub does not support explicit port in base_url

* Fix unit tests

* Fix unit tests

* Added changelog

* Update changelogs/fragments/2204-github_repo-fix-baseurl_port.yml

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
(cherry picked from commit 8eb2331aea)

Co-authored-by: Álvaro Torres Cogollo <atorrescogollo@gmail.com>
2021-04-09 12:16:29 +02:00
patchback[bot]
ec78558559 New module: Add Pritunl VPN organization module (net_tools/pritunl/) (#804) (#2212)
(cherry picked from commit f0b7c6351e)

Co-authored-by: Florian Dambrine <Lowess@users.noreply.github.com>
2021-04-09 05:35:07 +02:00
patchback[bot]
d5c8d7ddcc inventory/proxmox: added constructable and added keyed_groups, groups and compose (#2180) (#2211)
* added constructable and added keyed_groups, groups and compose

* Update changelogs/fragments/2162-proxmox-constructable.yml

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

* added constructed to extends_documentation_fragment and version_added to all the items

* renamed _apply_rules to _apply_constructable for more clarity

* Update changelogs/fragments/2162-proxmox-constructable.yml

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

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

Co-authored-by: Ilija Matoski <ilijamt@gmail.com>
2021-04-09 05:34:52 +02:00
patchback[bot]
6338048c73 Add path_join compatibility shim (#2172) (#2206)
* Add path_join compatibility shim.

* Add myself as maintainer.

(cherry picked from commit 4b6722d938)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-04-08 08:31:35 +02:00
patchback[bot]
92b388817f Add dict filter (#2171) (#2205)
* Add dict and list_to_dict filters.

* Remove list_to_dict filter.

* Add myself as maintainer.

(cherry picked from commit b6ae47c455)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-04-08 08:26:13 +02:00
patchback[bot]
c72b337327 module_helper - fixed decorator cause_changes (#2203) (#2207)
* fixed decorator cause_changes

* added changelog fragment

* typo

(cherry picked from commit 0cd0f0eaf6)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-08 08:26:04 +02:00
patchback[bot]
e5080b7847 Fix issue where multiselect field in userauthtype did not allow multiple values (#2174) (#2202)
* Fix issue where multiselect field in userauthtype did not allow multiple values

* Add changelogfragment for change

* Update changelogs/fragments/2174-ipa-user-userauthtype-multiselect.yml

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

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

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

* Update changelogs/fragments/2174-ipa-user-userauthtype-multiselect.yml

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

Co-authored-by: Chris Costa <chris.costa@compellingtech.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 595d590862)

Co-authored-by: justchris1 <30219018+justchris1@users.noreply.github.com>
2021-04-07 21:48:31 +02:00
patchback[bot]
079925fe66 ipa_user sshpubkey can now support multi word comments in the key (#2159) (#2201)
* ipa_user sshpubkey can now support multi word comments in the key

* Add documentation fragment for pull request

* Update changelogs/fragments/2159-ipa-user-sshpubkey-multi-word-comments.yaml

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

* Cleaner implementation of multi word comments

Co-authored-by: Chris Costa <chris.costa@compellingtech.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 7f91821bcc)

Co-authored-by: justchris1 <30219018+justchris1@users.noreply.github.com>
2021-04-07 20:55:33 +02:00
patchback[bot]
19a87874f7 Update java_cert module (#2008) (#2199)
* porting https://github.com/ansible/ansible/pull/56778 as requested in https://github.com/ansible-collections/community.general/issues/821

* fix imports, add back trust_cacerts option

* try to fix import, ansible-lint fixes

* modify import to use ansible.module_utils.six instead

* cleanup indentation for tests/integration/targets/java_cert/tasks/main.yml file

* remove external crypto dependency - switch to openssl, work on password obfuscation, using files compare to reduce logic

* java_cert - remove latest run_command using password in arguments

* fix sanity check

* rename changelog fragment file - wrong extension

* add openssl dependency

* fix openssl_bin parameter missing on _get_digest_from_x509_file function call

* remove useless close files, fix paragraph, fix changelog, clean import re

* fix missing dots at end-of-line in changelogs fragments

* fix reminder case

* fix changelog

* restore .gitignore

* fix indentation on integration test files, delete useless json file

* fix typo importing tasks in tests/integration/targets/java_cert/tasks/main.yml

* Update changelogs/fragments/2008-update-java-cert-replace-cert-when-changed.yml

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

* Update tests/integration/targets/java_cert/tasks/state_change.yml

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* Update plugins/modules/system/java_cert.py

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

* fix hardcoded executable keytool, use re.sub instead of import, add required cert_url or cert_alias parameter when absent, fix python script and cert_url test

* fix pylint issue with setupSSLServeR.py

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

Co-authored-by: absynth76 <58172580+absynth76@users.noreply.github.com>
2021-04-07 20:41:58 +02:00
patchback[bot]
809cdda9ef Fix HAProxy draining (#1993) (#2198)
* Fix HAProxy draining by manually entering the 'MAINT' state

Inspired by rldleblanc: https://github.com/ansible/ansible/issues/37591#issuecomment-610130611

Signed-off-by: Norman Ziegner <norman.ziegner@ufz.de>

* Add changelog fragment

Signed-off-by: Norman Ziegner <norman.ziegner@ufz.de>

* Fix drain function docstring

Signed-off-by: Norman Ziegner <norman.ziegner@ufz.de>

* Fix typos

Signed-off-by: Norman Ziegner <norman.ziegner@ufz.de>

* Update changelog fragment

Signed-off-by: Norman Ziegner <norman.ziegner@ufz.de>
(cherry picked from commit 7145204594)

Co-authored-by: Norman Ziegner <normo157@gmail.com>
2021-04-07 20:15:51 +02:00
patchback[bot]
bec6f732ad jira - changing the logic for transition (#1978) (#2195)
* attempt at fixing the issue

* Update plugins/modules/web_infrastructure/jira.py

* Fixed setting of "fields" element in the payload

* added changelog fragment

* added accountId parameter + minor fixes in docs

* added integration test for jira

* adjustments per PR

* Update plugins/modules/web_infrastructure/jira.py

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

* Update plugins/modules/web_infrastructure/jira.py

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

* Update plugins/modules/web_infrastructure/jira.py

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

* Update plugins/modules/web_infrastructure/jira.py

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

* Update plugins/modules/web_infrastructure/jira.py

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

* adjustments per PR

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-07 08:39:06 +02:00
patchback[bot]
d2cdca416c Applying ModuleHelper variable mgmt to xfconf -> improvements on MH (#2188) (#2191)
* applying MH variable mgmt to xfconf - improvements on MH

* added changelog fragment

(cherry picked from commit 9aec9b502e)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-06 21:51:12 +02:00
patchback[bot]
0f1ccc07c5 xfconf - state absent was not honoring check_mode (#2185) (#2186)
* state absent was not honoring check_mode

* added changelog fragment

(cherry picked from commit 9a5191d1f9)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-06 17:26:52 +02:00
patchback[bot]
deb1071666 [PR #2130/6bea8215 backport][stable-2] ansible/ansible's stable-2.11 branch has been created. (#2184)
* ansible/ansible's stable-2.11 branch has been created. (#2130)

(cherry picked from commit 6bea8215c9)

* Update ignore-2.12.txt.

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-04-06 08:31:47 +02:00
patchback[bot]
eb9c5eb796 replace inline clear password by environment variable (#2177) (#2182)
* replace inline clear password by environment variable on a per-command basis.

* add changelog fragment
* update related unit tests

* Update changelogs/fragments/2177-java_keystore_1668_dont_expose_secrets_on_cmdline.yml

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

* fix unit test: force result without lambda

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

Co-authored-by: quidame <quidame@poivron.org>
2021-04-05 18:45:06 +02:00
patchback[bot]
5c8504323e ModuleHelper variables management (#2162) (#2178)
* added metadata for variables in module helper

* adjustments

* added separate support for tracking changes

* rewrote the diff code

* added integration test for module_helper

* using ansible.module_utils.common.dict_transformations.dict_merge

* improved dependency management

* restore ModuleHelper to base classes of CmdStateModuleHelper

* added assertions to ensure the failing module name appears in the error messages

* added test code for state-based modules

* fixed test name

* renamed class to VarMeta

* small fixes

* fixes from the PR

* fixed VarDict.__set_attr__

* added VarDict.__getitem__()

* added changelog fragment

* adjustments per PR

* ModuleHelper.output is now aware of conflicting variable names

* Update plugins/module_utils/module_helper.py

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-05 15:39:08 +02:00
patchback[bot]
ab391c2cfa java_keystore/fix 1667 improve temp files storage (#2163) (#2176)
* improve temporary files storage (naming/removal)

* update unit tests

* Update changelogs/fragments/2163-java_keystore_1667_improve_temp_files_storage.yml

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

* add dedicated function to randomize PKCS#12 filename

fix unit tests (also mock the new function)

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

Co-authored-by: quidame <quidame@poivron.org>
2021-04-05 15:19:17 +02:00
patchback[bot]
a14b525bdc removed unreachable code (#2157) (#2170)
* removed unreachable code

* added changelog fragment

(cherry picked from commit b81a7cdd16)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-05 09:50:16 +02:00
patchback[bot]
996ef6ab49 rewritten as list literals (#2160) (#2168)
* rewritten as list literals

* added changelog fragment

(cherry picked from commit b97e31dd55)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-05 09:50:07 +02:00
patchback[bot]
055c8dac9c fixed calls to list.extend() (#2161) (#2166)
* fixed calls to list.extend()

* added changelog fragment

* Update changelogs/fragments/2161-pkgutil-list-extend.yml

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-04 23:59:59 +02:00
patchback[bot]
f4a9c7cc8b [WIP] Committer guidelines (#2077) (#2153)
* First idea for committer guidelines.

* Update commit-rights.md

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Update commit-rights.md

Co-authored-by: John R Barker <john@johnrbarker.com>

* Apply suggestions from code review

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>

* Improve 'do not' list.

* Add improvements from ansible/ansible#73782.

* Apply suggestions from code review

Co-authored-by: Amin Vakil <info@aminvakil.com>

* Apply suggestions from code review

* Update commit-rights.md

* Update commit-rights.md

Co-authored-by: Andrew Klychkov <aaklychkov@mail.ru>
Co-authored-by: John R Barker <john@johnrbarker.com>
Co-authored-by: Amin Vakil <info@aminvakil.com>
(cherry picked from commit 95156a11a1)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-04-03 11:09:28 +00:00
patchback[bot]
0c1f96290a using get_bin_path() on atomic modules (#2144) (#2151)
* using get_bin_path() on atomic modules

* added changelog fragment

* Update changelogs/fragments/2144-atomic_get_bin_path.yml

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-02 22:35:56 +02:00
patchback[bot]
d260f7ffda kibana_plugin: fixed remove call + run_command with list instead of str (#2143) (#2149)
* fixed remove call + run_command with list instead of str

* fixed the other calls to run_command()

* added changelog fragment

* adjustment on run_command params

* Update changelogs/fragments/2143-kibana_plugin-fixed-function-calls.yml

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-02 22:25:57 +02:00
patchback[bot]
35d81adabf apache2_mod_proxy - minor improvements/fixes (#2142) (#2145)
* minor improvements/fixes

- moved imports from the bottom of the code to the top (ansible-style).
- pythonified/simplified get_member_status()/set_member_status()
- reduced clutter in Balancer.__init__()

* added changelog fragment

(cherry picked from commit 1d1cbc4f56)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-04-01 09:01:38 +02:00
patchback[bot]
10a61c9dc3 fixed str formatting (#2139) (#2141)
(cherry picked from commit f1dbef4143)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-31 14:15:05 +02:00
patchback[bot]
6f47bcc399 fix type information for vmadm.resolvers (#2136) (#2138)
* fix type information for vmadm.resolvers

* Update changelogs/fragments/2135-vmadm-resolvers-type-fix.yml

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

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

Co-authored-by: Gaige B Paulsen <github@gbp.gaige.net>
2021-03-31 12:00:29 +02:00
Felix Fontein
7140b456ae Next release will be 2.5.0. 2021-03-30 13:39:08 +02:00
91 changed files with 6087 additions and 772 deletions

View File

@@ -56,6 +56,19 @@ stages:
- test: 3
- test: 4
- test: extra
- stage: Sanity_2_11
displayName: Sanity 2.11
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Test {0}
testFormat: 2.11/sanity/{0}
targets:
- test: 1
- test: 2
- test: 3
- test: 4
- stage: Sanity_2_10
displayName: Sanity 2.10
dependsOn: []
@@ -99,6 +112,22 @@ stages:
- test: 3.7
- test: 3.8
- test: 3.9
- stage: Units_2_11
displayName: Units 2.11
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.11/units/{0}/1
targets:
- test: 2.6
- test: 2.7
- test: 3.5
- test: 3.6
- test: 3.7
- test: 3.8
- test: 3.9
- stage: Units_2_10
displayName: Units 2.10
dependsOn: []
@@ -154,6 +183,25 @@ stages:
- 1
- 2
- 3
- stage: Remote_2_11
displayName: Remote 2.11
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.11/{0}
targets:
- name: macOS 11.1
test: macos/11.1
- name: RHEL 7.9
test: rhel/7.9
- name: RHEL 8.3
test: rhel/8.3
- name: FreeBSD 12.2
test: freebsd/12.2
groups:
- 1
- 2
- stage: Remote_2_10
displayName: Remote 2.10
dependsOn: []
@@ -224,6 +272,25 @@ stages:
- 1
- 2
- 3
- stage: Docker_2_11
displayName: Docker 2.11
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
testFormat: 2.11/linux/{0}
targets:
- name: CentOS 8
test: centos8
- name: Fedora 32
test: fedora33
- name: openSUSE 15 py3
test: opensuse15
- name: Ubuntu 20.04
test: ubuntu2004
groups:
- 2
- 3
- stage: Docker_2_10
displayName: Docker 2.10
dependsOn: []
@@ -270,6 +337,16 @@ stages:
parameters:
nameFormat: Python {0}
testFormat: devel/cloud/{0}/1
targets:
- test: 3.8
- stage: Cloud_2_11
displayName: Cloud 2.11
dependsOn: []
jobs:
- template: templates/matrix.yml
parameters:
nameFormat: Python {0}
testFormat: 2.11/cloud/{0}/1
targets:
- test: 2.7
- test: 3.6
@@ -299,17 +376,22 @@ stages:
- Sanity_devel
- Sanity_2_9
- Sanity_2_10
- Sanity_2_11
- Units_devel
- Units_2_9
- Units_2_10
- Units_2_11
- Remote_devel
- Remote_2_9
- Remote_2_10
- Remote_2_11
- Docker_devel
- Docker_2_9
- Docker_2_10
- Docker_2_11
- Cloud_devel
- Cloud_2_9
- Cloud_2_10
- Cloud_2_11
jobs:
- template: templates/coverage.yml

6
.github/BOTMETA.yml vendored
View File

@@ -1,5 +1,7 @@
automerge: true
files:
plugins/:
supershipit: aminvakil russoz
changelogs/fragments/:
support: community
$actions:
@@ -53,12 +55,16 @@ files:
$doc_fragments/xenserver.py:
maintainers: bvitnik
labels: xenserver
$filters/dict.py:
maintainers: felixfontein
$filters/dict_kv.py:
maintainers: giner
$filters/jc.py:
maintainers: kellyjonbrazil
$filters/list.py:
maintainers: vbotka
$filters/path_join_shim.py:
maintainers: felixfontein
$filters/time.py:
maintainers: resmo
$httpapis/:

View File

@@ -6,6 +6,105 @@ Community General Release Notes
This changelog describes changes after version 1.0.0.
v2.5.0
======
Release Summary
---------------
Regular feature release. Will be the last 2.x.0 minor release.
Minor Changes
-------------
- apache2_mod_proxy - refactored/cleaned-up part of the code (https://github.com/ansible-collections/community.general/pull/2142).
- atomic_container - using ``get_bin_path()`` before calling ``run_command()`` (https://github.com/ansible-collections/community.general/pull/2144).
- atomic_host - using ``get_bin_path()`` before calling ``run_command()`` (https://github.com/ansible-collections/community.general/pull/2144).
- atomic_image - using ``get_bin_path()`` before calling ``run_command()`` (https://github.com/ansible-collections/community.general/pull/2144).
- beadm - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- bitbucket_pipeline_variable - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- hiera lookup - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- ipa_config - add new options ``ipaconfigstring``, ``ipadefaultprimarygroup``, ``ipagroupsearchfields``, ``ipahomesrootdir``, ``ipabrkauthzdata``, ``ipamaxusernamelength``, ``ipapwdexpadvnotify``, ``ipasearchrecordslimit``, ``ipasearchtimelimit``, ``ipauserauthtype``, and ``ipausersearchfields`` (https://github.com/ansible-collections/community.general/pull/2116).
- ipa_user - fix ``userauthtype`` option to take in list of strings for the multi-select field instead of single string (https://github.com/ansible-collections/community.general/pull/2174).
- ipwcli_dns - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- java_cert - change ``state: present`` to check certificates by hash, not just alias name (https://github.com/ansible/ansible/issues/43249).
- jira - added ``attach`` operation, which allows a user to attach a file to an issue (https://github.com/ansible-collections/community.general/pull/2192).
- jira - added parameter ``account_id`` for compatibility with recent versions of JIRA (https://github.com/ansible-collections/community.general/issues/818, https://github.com/ansible-collections/community.general/pull/1978).
- known_hosts module utils - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- module_helper module utils - added management of facts and adhoc setting of the initial value for variables (https://github.com/ansible-collections/community.general/pull/2188).
- module_helper module utils - added mechanism to manage variables, providing automatic output of variables, change status and diff information (https://github.com/ansible-collections/community.general/pull/2162).
- nictagadm - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- npm - add ``no_bin_links`` option (https://github.com/ansible-collections/community.general/issues/2128).
- ovh_ip_failover - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- proxmox inventory plugin - added ``Constructable`` class to the inventory to provide options ``strict``, ``keyed_groups``, ``groups``, and ``compose`` (https://github.com/ansible-collections/community.general/pull/2180).
- proxmox inventory plugin - added ``proxmox_agent_interfaces`` fact describing network interfaces returned from a QEMU guest agent (https://github.com/ansible-collections/community.general/pull/2148).
- rhevm - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- smartos_image_info - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- svr4pkg - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- xattr - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- xfconf - changed implementation to use ``ModuleHelper`` new features (https://github.com/ansible-collections/community.general/pull/2188).
- zfs_facts - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- zpool_facts - minor refactor converting multiple statements to a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
Security Fixes
--------------
- java_cert - remove password from ``run_command`` arguments (https://github.com/ansible-collections/community.general/pull/2008).
- java_keystore - pass secret to keytool through an environment variable to not expose it as a commandline argument (https://github.com/ansible-collections/community.general/issues/1668).
Bugfixes
--------
- dimensiondata_network - bug when formatting message, instead of % a simple comma was used (https://github.com/ansible-collections/community.general/pull/2139).
- github_repo - PyGithub bug does not allow explicit port in ``base_url``. Specifying port is not required (https://github.com/PyGithub/PyGithub/issues/1913).
- haproxy - fix a bug preventing haproxy from properly entering ``DRAIN`` mode (https://github.com/ansible-collections/community.general/issues/1913).
- ipa_user - allow ``sshpubkey`` to permit multiple word comments (https://github.com/ansible-collections/community.general/pull/2159).
- java_cert - allow setting ``state: absent`` by providing just the ``cert_alias`` (https://github.com/ansible/ansible/issues/27982).
- java_cert - properly handle proxy arguments when the scheme is provided (https://github.com/ansible/ansible/issues/54481).
- java_keystore - improve error handling and return ``cmd`` as documented. Force ``LANG``, ``LC_ALL`` and ``LC_MESSAGES`` environment variables to ``C`` to rely on ``keytool`` output parsing. Fix pylint's ``unused-variable`` and ``no-else-return`` hints (https://github.com/ansible-collections/community.general/pull/2183).
- java_keystore - use tempfile lib to create temporary files with randomized names, and remove the temporary PKCS#12 keystore as well as other materials (https://github.com/ansible-collections/community.general/issues/1667).
- jira - fixed fields' update in ticket transitions (https://github.com/ansible-collections/community.general/issues/818).
- kibana_plugin - added missing parameters to ``remove_plugin`` when using ``state=present force=true``, and fix potential quoting errors when invoking ``kibana`` (https://github.com/ansible-collections/community.general/pull/2143).
- module_helper module utils - fixed decorator ``cause_changes`` (https://github.com/ansible-collections/community.general/pull/2203).
- pkgutil - fixed calls to ``list.extend()`` (https://github.com/ansible-collections/community.general/pull/2161).
- vmadm - correct type of list elements in ``resolvers`` parameter (https://github.com/ansible-collections/community.general/issues/2135).
- xfconf - module was not honoring check mode when ``state`` was ``absent`` (https://github.com/ansible-collections/community.general/pull/2185).
New Plugins
-----------
Filter
~~~~~~
- dict - The ``dict`` function as a filter: converts a list of tuples to a dictionary
- path_join - Redirects to ansible.builtin.path_join for ansible-base 2.10 or newer, and provides a compatible implementation for Ansible 2.9
New Modules
-----------
Identity
~~~~~~~~
ipa
^^^
- ipa_otpconfig - Manage FreeIPA OTP Configuration Settings
- ipa_otptoken - Manage FreeIPA OTPs
Monitoring
~~~~~~~~~~
- spectrum_model_attrs - Enforce a model's attributes in CA Spectrum.
Net Tools
~~~~~~~~~
pritunl
^^^^^^^
- pritunl_org - Manages Pritunl Organizations using the Pritunl API
- pritunl_org_info - List Pritunl Organizations using the Pritunl API
v2.4.0
======

View File

@@ -9,7 +9,7 @@ You can find [documentation for this collection on the Ansible docs site](https:
## Tested with Ansible
Tested with the current Ansible 2.9 and 2.10 releases and the current development version of Ansible. Ansible versions before 2.9.10 are not supported.
Tested with the current Ansible 2.9, ansible-base 2.10 and ansible-core 2.11 releases and the current development version of ansible-core. Ansible versions before 2.9.10 are not supported.
## External requirements

View File

@@ -1661,3 +1661,146 @@ releases:
name: loganalytics
namespace: null
release_date: '2021-03-30'
2.5.0:
changes:
bugfixes:
- dimensiondata_network - bug when formatting message, instead of % a simple
comma was used (https://github.com/ansible-collections/community.general/pull/2139).
- github_repo - PyGithub bug does not allow explicit port in ``base_url``. Specifying
port is not required (https://github.com/PyGithub/PyGithub/issues/1913).
- haproxy - fix a bug preventing haproxy from properly entering ``DRAIN`` mode
(https://github.com/ansible-collections/community.general/issues/1913).
- ipa_user - allow ``sshpubkey`` to permit multiple word comments (https://github.com/ansible-collections/community.general/pull/2159).
- 'java_cert - allow setting ``state: absent`` by providing just the ``cert_alias``
(https://github.com/ansible/ansible/issues/27982).'
- java_cert - properly handle proxy arguments when the scheme is provided (https://github.com/ansible/ansible/issues/54481).
- java_keystore - improve error handling and return ``cmd`` as documented. Force
``LANG``, ``LC_ALL`` and ``LC_MESSAGES`` environment variables to ``C`` to
rely on ``keytool`` output parsing. Fix pylint's ``unused-variable`` and ``no-else-return``
hints (https://github.com/ansible-collections/community.general/pull/2183).
- java_keystore - use tempfile lib to create temporary files with randomized
names, and remove the temporary PKCS#12 keystore as well as other materials
(https://github.com/ansible-collections/community.general/issues/1667).
- jira - fixed fields' update in ticket transitions (https://github.com/ansible-collections/community.general/issues/818).
- kibana_plugin - added missing parameters to ``remove_plugin`` when using ``state=present
force=true``, and fix potential quoting errors when invoking ``kibana`` (https://github.com/ansible-collections/community.general/pull/2143).
- module_helper module utils - fixed decorator ``cause_changes`` (https://github.com/ansible-collections/community.general/pull/2203).
- pkgutil - fixed calls to ``list.extend()`` (https://github.com/ansible-collections/community.general/pull/2161).
- vmadm - correct type of list elements in ``resolvers`` parameter (https://github.com/ansible-collections/community.general/issues/2135).
- xfconf - module was not honoring check mode when ``state`` was ``absent``
(https://github.com/ansible-collections/community.general/pull/2185).
minor_changes:
- apache2_mod_proxy - refactored/cleaned-up part of the code (https://github.com/ansible-collections/community.general/pull/2142).
- atomic_container - using ``get_bin_path()`` before calling ``run_command()``
(https://github.com/ansible-collections/community.general/pull/2144).
- atomic_host - using ``get_bin_path()`` before calling ``run_command()`` (https://github.com/ansible-collections/community.general/pull/2144).
- atomic_image - using ``get_bin_path()`` before calling ``run_command()`` (https://github.com/ansible-collections/community.general/pull/2144).
- beadm - minor refactor converting multiple statements to a single list literal
(https://github.com/ansible-collections/community.general/pull/2160).
- bitbucket_pipeline_variable - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- hiera lookup - minor refactor converting multiple statements to a single list
literal (https://github.com/ansible-collections/community.general/pull/2160).
- ipa_config - add new options ``ipaconfigstring``, ``ipadefaultprimarygroup``,
``ipagroupsearchfields``, ``ipahomesrootdir``, ``ipabrkauthzdata``, ``ipamaxusernamelength``,
``ipapwdexpadvnotify``, ``ipasearchrecordslimit``, ``ipasearchtimelimit``,
``ipauserauthtype``, and ``ipausersearchfields`` (https://github.com/ansible-collections/community.general/pull/2116).
- ipa_user - fix ``userauthtype`` option to take in list of strings for the
multi-select field instead of single string (https://github.com/ansible-collections/community.general/pull/2174).
- ipwcli_dns - minor refactor converting multiple statements to a single list
literal (https://github.com/ansible-collections/community.general/pull/2160).
- 'java_cert - change ``state: present`` to check certificates by hash, not
just alias name (https://github.com/ansible/ansible/issues/43249).'
- jira - added ``attach`` operation, which allows a user to attach a file to
an issue (https://github.com/ansible-collections/community.general/pull/2192).
- jira - added parameter ``account_id`` for compatibility with recent versions
of JIRA (https://github.com/ansible-collections/community.general/issues/818,
https://github.com/ansible-collections/community.general/pull/1978).
- known_hosts module utils - minor refactor converting multiple statements to
a single list literal (https://github.com/ansible-collections/community.general/pull/2160).
- module_helper module utils - added management of facts and adhoc setting of
the initial value for variables (https://github.com/ansible-collections/community.general/pull/2188).
- module_helper module utils - added mechanism to manage variables, providing
automatic output of variables, change status and diff information (https://github.com/ansible-collections/community.general/pull/2162).
- nictagadm - minor refactor converting multiple statements to a single list
literal (https://github.com/ansible-collections/community.general/pull/2160).
- npm - add ``no_bin_links`` option (https://github.com/ansible-collections/community.general/issues/2128).
- ovh_ip_failover - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- proxmox inventory plugin - added ``Constructable`` class to the inventory
to provide options ``strict``, ``keyed_groups``, ``groups``, and ``compose``
(https://github.com/ansible-collections/community.general/pull/2180).
- proxmox inventory plugin - added ``proxmox_agent_interfaces`` fact describing
network interfaces returned from a QEMU guest agent (https://github.com/ansible-collections/community.general/pull/2148).
- rhevm - removed unreachable code (https://github.com/ansible-collections/community.general/pull/2157).
- smartos_image_info - minor refactor converting multiple statements to a single
list literal (https://github.com/ansible-collections/community.general/pull/2160).
- svr4pkg - minor refactor converting multiple statements to a single list literal
(https://github.com/ansible-collections/community.general/pull/2160).
- xattr - minor refactor converting multiple statements to a single list literal
(https://github.com/ansible-collections/community.general/pull/2160).
- xfconf - changed implementation to use ``ModuleHelper`` new features (https://github.com/ansible-collections/community.general/pull/2188).
- zfs_facts - minor refactor converting multiple statements to a single list
literal (https://github.com/ansible-collections/community.general/pull/2160).
- zpool_facts - minor refactor converting multiple statements to a single list
literal (https://github.com/ansible-collections/community.general/pull/2160).
release_summary: Regular feature release. Will be the last 2.x.0 minor release.
security_fixes:
- java_cert - remove password from ``run_command`` arguments (https://github.com/ansible-collections/community.general/pull/2008).
- java_keystore - pass secret to keytool through an environment variable to
not expose it as a commandline argument (https://github.com/ansible-collections/community.general/issues/1668).
fragments:
- 1978-jira-transition-logic.yml
- 1993-haproxy-fix-draining.yml
- 2.5.0.yml
- 2008-update-java-cert-replace-cert-when-changed.yml
- 2116-add-fields-to-ipa-config-module.yml
- 2135-vmadm-resolvers-type-fix.yml
- 2139-dimensiondata_network-str-format.yml
- 2142-apache2_mod_proxy-cleanup.yml
- 2143-kibana_plugin-fixed-function-calls.yml
- 2144-atomic_get_bin_path.yml
- 2146-npm-add_no_bin_links_option.yaml
- 2148-proxmox-inventory-agent-interfaces.yml
- 2157-unreachable-code.yml
- 2159-ipa-user-sshpubkey-multi-word-comments.yaml
- 2160-list-literals.yml
- 2161-pkgutil-list-extend.yml
- 2162-modhelper-variables.yml
- 2162-proxmox-constructable.yml
- 2163-java_keystore_1667_improve_temp_files_storage.yml
- 2174-ipa-user-userauthtype-multiselect.yml
- 2177-java_keystore_1668_dont_expose_secrets_on_cmdline.yml
- 2183-java_keystore_improve_error_handling.yml
- 2185-xfconf-absent-check-mode.yml
- 2188-xfconf-modhelper-variables.yml
- 2192-add-jira-attach.yml
- 2203-modhelper-cause-changes-deco.yml
- 2204-github_repo-fix-baseurl_port.yml
- dict-filter.yml
- path_join-shim-filter.yml
modules:
- description: Manage FreeIPA OTP Configuration Settings
name: ipa_otpconfig
namespace: identity.ipa
- description: Manage FreeIPA OTPs
name: ipa_otptoken
namespace: identity.ipa
- description: Manages Pritunl Organizations using the Pritunl API
name: pritunl_org
namespace: net_tools.pritunl
- description: List Pritunl Organizations using the Pritunl API
name: pritunl_org_info
namespace: net_tools.pritunl
- description: Enforce a model's attributes in CA Spectrum.
name: spectrum_model_attrs
namespace: monitoring
plugins:
filter:
- description: 'The ``dict`` function as a filter: converts a list of tuples
to a dictionary'
name: dict
namespace: null
- description: Redirects to ansible.builtin.path_join for ansible-base 2.10
or newer, and provides a compatible implementation for Ansible 2.9
name: path_join
namespace: null
release_date: '2021-04-13'

72
commit-rights.md Normal file
View File

@@ -0,0 +1,72 @@
Committers Guidelines for community.general
===========================================
This document is based on the [Ansible committer guidelines](https://github.com/ansible/ansible/blob/b57444af14062ec96e0af75fdfc2098c74fe2d9a/docs/docsite/rst/community/committer_guidelines.rst) ([latest version](https://docs.ansible.com/ansible/devel/community/committer_guidelines.html)).
These are the guidelines for people with commit privileges on the Ansible Community General Collection GitHub repository. Please read the guidelines before you commit.
These guidelines apply to everyone. At the same time, this is NOT a process document. So just use good judgment. You have been given commit access because we trust your judgment.
That said, use the trust wisely.
If you abuse the trust and break components and builds, and so on, the trust level falls and you may be asked not to commit or you may lose your commit privileges.
Our workflow on GitHub
----------------------
As a committer, you may already know this, but our workflow forms a lot of our team policies. Please ensure you are aware of the following workflow steps:
* Fork the repository upon which you want to do some work to your own personal repository
* Work on the specific branch upon which you need to commit
* Create a Pull Request back to the collection repository and await reviews
* Adjust code as necessary based on the Comments provided
* Ask someone from the other committers to do a final review and merge
Sometimes, committers merge their own pull requests. This section is a set of guidelines. If you are changing a comma in a doc or making a very minor change, you can use your best judgement. This is another trust thing. The process is critical for any major change, but for little things or getting something done quickly, use your best judgement and make sure people on the team are aware of your work.
Roles
-----
* Release managers: Merge pull requests to `stable-X` branches, create tags to do releases.
* Committers: Fine to do PRs for most things, but we should have a timebox. Hanging PRs may merge on the judgement of these devs.
* Module maintainers: Module maintainers own specific modules and have indirect commit access through the current module PR mechanisms. This is primary [ansibullbot](https://github.com/ansibullbot)'s `shipit` mechanism.
General rules
-------------
Individuals with direct commit access to this collection repository are entrusted with powers that allow them to do a broad variety of things--probably more than we can write down. Rather than rules, treat these as general *guidelines*, individuals with this power are expected to use their best judgement.
* Do NOTs:
- Do not commit directly.
- Do not merge your own PRs. Someone else should have a chance to review and approve the PR merge. You have a small amount of leeway here for very minor changes.
- Do not forget about non-standard / alternate environments. Consider the alternatives. Yes, people have bad/unusual/strange environments (like binaries from multiple init systems installed), but they are the ones who need us the most.
- Do not drag your community team members down. Discuss the technical merits of any pull requests you review. Avoid negativity and personal comments. For more guidance on being a good community member, read the [Ansible Community Code of Conduct](https://docs.ansible.com/ansible/latest/community/code_of_conduct.html).
- Do not forget about the maintenance burden. High-maintenance features may not be worth adding.
- Do not break playbooks. Always keep backwards compatibility in mind.
- Do not forget to keep it simple. Complexity breeds all kinds of problems.
- Do not merge to branches other than `main`, especially not to `stable-X`, if you do not have explicit permission to do so.
- Do not create tags. Tags are used in the release process, and should only be created by the people responsible for managing the stable branches.
* Do:
- Squash, avoid merges whenever possible, use GitHub's squash commits or cherry pick if needed (bisect thanks you).
- Be active. Committers who have no activity on the project (through merges, triage, commits, and so on) will have their permissions suspended.
- Consider backwards compatibility (goes back to "do not break existing playbooks").
- Write tests. PRs with tests are looked at with more priority than PRs without tests that should have them included. While not all changes require tests, be sure to add them for bug fixes or functionality changes.
- Discuss with other committers, specially when you are unsure of something.
- Document! If your PR is a new feature or a change to behavior, make sure you've updated all associated documentation or have notified the right people to do so.
- Consider scope, sometimes a fix can be generalized.
- Keep it simple, then things are maintainable, debuggable and intelligible.
Committers are expected to continue to follow the same community and contribution guidelines followed by the rest of the Ansible community.
People
------
Individuals who have been asked to become a part of this group have generally been contributing in significant ways to the community.general collection for some time. Should they agree, they are requested to add their names and GitHub IDs to this file, in the section below, through a pull request. Doing so indicates that these individuals agree to act in the ways that their fellow committers trust that they will act.
| Name | GitHub ID | IRC Nick | Other |
| ------------------- | -------------------- | ------------------ | -------------------- |
| Andrew Klychkov | andersson007 | andersson007_ | |
| Felix Fontein | felixfontein | felixfontein | |
| John R Barker | gundalow | gundalow | |

View File

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

View File

@@ -601,3 +601,10 @@ plugin_routing:
redirect: community.docker.docker_swarm
kubevirt:
redirect: community.kubevirt.kubevirt
filter:
path_join:
# The ansible.builtin.path_join filter has been added in ansible-base 2.10.
# Since plugin routing is only available since ansible-base 2.10, this
# redirect will be used for ansible-base 2.10 or later, and the included
# path_join filter will be used for Ansible 2.9 or earlier.
redirect: ansible.builtin.path_join

24
plugins/filter/dict.py Normal file
View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
def dict_filter(sequence):
'''Convert a list of tuples to a dictionary.
Example: ``[[1, 2], ['a', 'b']] | community.general.dict`` results in ``{1: 2, 'a': 'b'}``
'''
return dict(sequence)
class FilterModule(object):
'''Ansible jinja2 filters'''
def filters(self):
return {
'dict': dict_filter,
}

View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020-2021, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import os.path
def path_join(list):
'''Join list of paths.
This is a minimal shim for ansible.builtin.path_join included in ansible-base 2.10.
This should only be called by Ansible 2.9 or earlier. See meta/runtime.yml for details.
'''
return os.path.join(*list)
class FilterModule(object):
'''Ansible jinja2 filters'''
def filters(self):
return {
'path_join': path_join,
}

View File

@@ -19,6 +19,7 @@ DOCUMENTATION = '''
- Will retrieve the first network interface with an IP for Proxmox nodes.
- Can retrieve LXC/QEMU configuration as facts.
extends_documentation_fragment:
- constructed
- inventory_cache
options:
plugin:
@@ -69,6 +70,14 @@ DOCUMENTATION = '''
description: Gather LXC/QEMU configuration facts.
default: no
type: bool
strict:
version_added: 2.5.0
compose:
version_added: 2.5.0
groups:
version_added: 2.5.0
keyed_groups:
version_added: 2.5.0
'''
EXAMPLES = '''
@@ -78,6 +87,15 @@ url: http://localhost:8006
user: ansible@pve
password: secure
validate_certs: no
keyed_groups:
- key: proxmox_tags_parsed
separator: ""
prefix: group
groups:
webservers: "'web' in (proxmox_tags_parsed|list)"
mailservers: "'mail' in (proxmox_tags_parsed|list)"
compose:
ansible_port: 2222
'''
import re
@@ -86,7 +104,7 @@ from ansible.module_utils.common._collections_compat import MutableMapping
from distutils.version import LooseVersion
from ansible.errors import AnsibleError
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
from ansible.module_utils.six.moves.urllib.parse import urlencode
# 3rd party imports
@@ -99,7 +117,7 @@ except ImportError:
HAS_REQUESTS = False
class InventoryModule(BaseInventoryPlugin, Cacheable):
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
''' Host inventory parser for ansible using Proxmox as source. '''
NAME = 'community.general.proxmox'
@@ -206,9 +224,36 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
except Exception:
return None
def _get_agent_network_interfaces(self, node, vmid, vmtype):
result = []
try:
ifaces = self._get_json(
"%s/api2/json/nodes/%s/%s/%s/agent/network-get-interfaces" % (
self.proxmox_url, node, vmtype, vmid
)
)['result']
for iface in ifaces:
result.append({
'name': iface['name'],
'mac-address': iface['hardware-address'],
'ip-addresses': [
"%s/%s" % (ip['ip-address'], ip['prefix']) for ip in iface['ip-addresses']
]
})
except requests.HTTPError:
pass
return result
def _get_vm_config(self, node, vmid, vmtype, name):
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/config" % (self.proxmox_url, node, vmtype, vmid))
node_key = 'node'
node_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), node_key.lower()))
self.inventory.set_variable(name, node_key, node)
vmid_key = 'vmid'
vmid_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), vmid_key.lower()))
self.inventory.set_variable(name, vmid_key, vmid)
@@ -236,6 +281,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
parsed_value = [tag.strip() for tag in value.split(",")]
self.inventory.set_variable(name, parsed_key, parsed_value)
if config == 'agent' and int(value):
agent_iface_key = self.to_safe('%s%s' % (key, "_interfaces"))
agent_iface_value = self._get_agent_network_interfaces(node, vmid, vmtype)
if agent_iface_value:
self.inventory.set_variable(name, agent_iface_key, agent_iface_value)
if not (isinstance(value, int) or ',' not in value):
# split off strings with commas to a dict
# skip over any keys that cannot be processed
@@ -264,6 +315,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
regex = r"[^A-Za-z0-9\_]"
return re.sub(regex, "_", word.replace(" ", ""))
def _apply_constructable(self, name, variables):
strict = self.get_option('strict')
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=strict)
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=strict)
self._set_composite_vars(self.get_option('compose'), variables, name, strict=strict)
def _populate(self):
self._get_auth()
@@ -318,6 +375,8 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
if self.get_option('want_facts'):
self._get_vm_config(node['node'], lxc['vmid'], 'lxc', lxc['name'])
self._apply_constructable(lxc["name"], self.inventory.get_host(lxc['name']).get_vars())
# get QEMU vm's for this node
node_qemu_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), ('%s_qemu' % node['node']).lower()))
self.inventory.add_group(node_qemu_group)
@@ -340,6 +399,8 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
if self.get_option('want_facts'):
self._get_vm_config(node['node'], qemu['vmid'], 'qemu', qemu['name'])
self._apply_constructable(qemu["name"], self.inventory.get_host(qemu['name']).get_vars())
# gather vm's in pools
for pool in self._get_pools():
if pool.get('poolid'):

View File

@@ -84,7 +84,5 @@ class Hiera(object):
class LookupModule(LookupBase):
def run(self, terms, variables=''):
hiera = Hiera()
ret = []
ret.append(hiera.get(terms))
ret = [hiera.get(terms)]
return ret

View File

@@ -119,9 +119,9 @@ class IPAClient(object):
data = dict(method=method)
# TODO: We should probably handle this a little better.
if method in ('ping', 'config_show'):
if method in ('ping', 'config_show', 'otpconfig_show'):
data['params'] = [[], {}]
elif method == 'config_mod':
elif method in ('config_mod', 'otpconfig_mod'):
data['params'] = [[], item]
else:
data['params'] = [[name], item]

View File

@@ -87,11 +87,12 @@ def not_in_host_file(self, host):
user_host_file = "~/.ssh/known_hosts"
user_host_file = os.path.expanduser(user_host_file)
host_file_list = []
host_file_list.append(user_host_file)
host_file_list.append("/etc/ssh/ssh_known_hosts")
host_file_list.append("/etc/ssh/ssh_known_hosts2")
host_file_list.append("/etc/openssh/ssh_known_hosts")
host_file_list = [
user_host_file,
"/etc/ssh/ssh_known_hosts",
"/etc/ssh/ssh_known_hosts2",
"/etc/openssh/ssh_known_hosts",
]
hfiles_not_found = 0
for hf in host_file_list:

View File

@@ -10,6 +10,7 @@ from functools import partial, wraps
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.dict_transformations import dict_merge
class ModuleHelperException(Exception):
@@ -24,12 +25,12 @@ class ModuleHelperException(Exception):
def __init__(self, *args, **kwargs):
self.msg = self._get_remove('msg', kwargs) or "Module failed with exception: {0}".format(self)
self.update_output = self._get_remove('update_output', kwargs) or {}
super(ModuleHelperException, self).__init__(*args, **kwargs)
super(ModuleHelperException, self).__init__(*args)
class ArgFormat(object):
"""
Argument formatter
Argument formatter for use as a command line parameter. Used in CmdMixin.
"""
BOOLEAN = 0
PRINTF = 1
@@ -50,7 +51,8 @@ class ArgFormat(object):
def __init__(self, name, fmt=None, style=FORMAT, stars=0):
"""
Creates a new formatter
Creates a CLI-formatter for one specific argument. The argument may be a module parameter or just a named parameter for
the CLI command execution.
:param name: Name of the argument to be formatted
:param fmt: Either a str to be formatted (using or not printf-style) or a callable that does that
:param style: Whether arg_format (as str) should use printf-style formatting.
@@ -99,18 +101,27 @@ class ArgFormat(object):
return [str(p) for p in func(value)]
def cause_changes(func, on_success=True, on_failure=False):
@wraps(func)
def wrapper(self, *args, **kwargs):
try:
func(*args, **kwargs)
if on_success:
self.changed = True
except Exception as e:
if on_failure:
self.changed = True
raise
return wrapper
def cause_changes(on_success=None, on_failure=None):
def deco(func):
if on_success is None and on_failure is None:
return func
@wraps(func)
def wrapper(*args, **kwargs):
try:
self = args[0]
func(*args, **kwargs)
if on_success is not None:
self.changed = on_success
except Exception:
if on_failure is not None:
self.changed = on_failure
raise
return wrapper
return deco
def module_fails_on_exception(func):
@@ -123,11 +134,12 @@ def module_fails_on_exception(func):
except ModuleHelperException as e:
if e.update_output:
self.update_output(e.update_output)
self.module.fail_json(changed=False, msg=e.msg, exception=traceback.format_exc(), output=self.output, vars=self.vars)
self.module.fail_json(msg=e.msg, exception=traceback.format_exc(),
output=self.output, vars=self.vars.output(), **self.output)
except Exception as e:
self.vars.msg = "Module failed with exception: {0}".format(str(e).strip())
self.vars.exception = traceback.format_exc()
self.module.fail_json(changed=False, msg=self.vars.msg, exception=self.vars.exception, output=self.output, vars=self.vars)
msg = "Module failed with exception: {0}".format(str(e).strip())
self.module.fail_json(msg=msg, exception=traceback.format_exc(),
output=self.output, vars=self.vars.output(), **self.output)
return wrapper
@@ -141,7 +153,7 @@ class DependencyCtxMgr(object):
self.exc_tb = None
def __enter__(self):
pass
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.has_it = exc_type is None
@@ -155,32 +167,157 @@ class DependencyCtxMgr(object):
return self.msg or str(self.exc_val)
class ModuleHelper(object):
_dependencies = []
module = {}
facts_name = None
class VarMeta(object):
NOTHING = object()
def __init__(self, diff=False, output=True, change=None, fact=False):
self.init = False
self.initial_value = None
self.value = None
self.diff = diff
self.change = diff if change is None else change
self.output = output
self.fact = fact
def set(self, diff=None, output=None, change=None, fact=None, initial_value=NOTHING):
if diff is not None:
self.diff = diff
if output is not None:
self.output = output
if change is not None:
self.change = change
if fact is not None:
self.fact = fact
if initial_value is not self.NOTHING:
self.initial_value = initial_value
def set_value(self, value):
if not self.init:
self.initial_value = value
self.init = True
self.value = value
return self
@property
def has_changed(self):
return self.change and (self.initial_value != self.value)
@property
def diff_result(self):
return None if not (self.diff and self.has_changed) else {
'before': self.initial_value,
'after': self.value,
}
def __str__(self):
return "<VarMeta: value={0}, initial={1}, diff={2}, output={3}, change={4}>".format(
self.value, self.initial_value, self.diff, self.output, self.change
)
class ModuleHelper(object):
_output_conflict_list = ('msg', 'exception', 'output', 'vars', 'changed')
_dependencies = []
module = None
facts_name = None
output_params = ()
diff_params = ()
change_params = ()
facts_params = ()
class VarDict(object):
def __init__(self):
self._data = dict()
self._meta = dict()
def __getitem__(self, item):
return self._data[item]
def __setitem__(self, key, value):
self.set(key, value)
class AttrDict(dict):
def __getattr__(self, item):
return self[item]
try:
return self._data[item]
except KeyError:
return getattr(self._data, item)
def __setattr__(self, key, value):
if key in ('_data', '_meta'):
super(ModuleHelper.VarDict, self).__setattr__(key, value)
else:
self.set(key, value)
def meta(self, name):
return self._meta[name]
def set_meta(self, name, **kwargs):
self.meta(name).set(**kwargs)
def set(self, name, value, **kwargs):
if name in ('_data', '_meta'):
raise ValueError("Names _data and _meta are reserved for use by ModuleHelper")
self._data[name] = value
if name in self._meta:
meta = self.meta(name)
else:
meta = VarMeta(**kwargs)
meta.set_value(value)
self._meta[name] = meta
def output(self):
return dict((k, v) for k, v in self._data.items() if self.meta(k).output)
def diff(self):
diff_results = [(k, self.meta(k).diff_result) for k in self._data]
diff_results = [dr for dr in diff_results if dr[1] is not None]
if diff_results:
before = dict((dr[0], dr[1]['before']) for dr in diff_results)
after = dict((dr[0], dr[1]['after']) for dr in diff_results)
return {'before': before, 'after': after}
return None
def facts(self):
facts_result = dict((k, v) for k, v in self._data.items() if self._meta[k].fact)
return facts_result if facts_result else None
def change_vars(self):
return [v for v in self._data if self.meta(v).change]
def has_changed(self, v):
return self._meta[v].has_changed
def __init__(self, module=None):
self.vars = ModuleHelper.AttrDict()
self.output_dict = dict()
self.facts_dict = dict()
self.vars = ModuleHelper.VarDict()
self._changed = False
if module:
self.module = module
if isinstance(self.module, dict):
if not isinstance(self.module, AnsibleModule):
self.module = AnsibleModule(**self.module)
for name, value in self.module.params.items():
self.vars.set(
name, value,
diff=name in self.diff_params,
output=name in self.output_params,
change=None if not self.change_params else name in self.change_params,
fact=name in self.facts_params,
)
def update_vars(self, meta=None, **kwargs):
if meta is None:
meta = {}
for k, v in kwargs.items():
self.vars.set(k, v, **meta)
def update_output(self, **kwargs):
self.output_dict.update(kwargs)
self.update_vars(meta={"output": True}, **kwargs)
def update_facts(self, **kwargs):
self.facts_dict.update(kwargs)
self.update_vars(meta={"fact": True}, **kwargs)
def __init_module__(self):
pass
@@ -191,6 +328,9 @@ class ModuleHelper(object):
def __quit_module__(self):
pass
def _vars_changed(self):
return any(self.vars.has_changed(v) for v in self.vars.change_vars())
@property
def changed(self):
return self._changed
@@ -199,12 +339,25 @@ class ModuleHelper(object):
def changed(self, value):
self._changed = value
def has_changed(self):
return self.changed or self._vars_changed()
@property
def output(self):
result = dict(self.vars)
result.update(self.output_dict)
result = dict(self.vars.output())
if self.facts_name:
result['ansible_facts'] = {self.facts_name: self.facts_dict}
facts = self.vars.facts()
if facts is not None:
result['ansible_facts'] = {self.facts_name: facts}
if self.module._diff:
diff = result.get('diff', {})
vars_diff = self.vars.diff() or {}
result['diff'] = dict_merge(dict(diff), vars_diff)
for varname in result:
if varname in self._output_conflict_list:
result["_" + varname] = result[varname]
del result[varname]
return result
@module_fails_on_exception
@@ -213,7 +366,7 @@ class ModuleHelper(object):
self.__init_module__()
self.__run__()
self.__quit_module__()
self.module.exit_json(changed=self.changed, **self.output_dict)
self.module.exit_json(changed=self.has_changed(), **self.output)
@classmethod
def dependency(cls, name, msg):
@@ -224,9 +377,9 @@ class ModuleHelper(object):
for d in self._dependencies:
if not d.has_it:
self.module.fail_json(changed=False,
exception=d.exc_val.__traceback__.format_exc(),
exception="\n".join(traceback.format_exception(d.exc_type, d.exc_val, d.exc_tb)),
msg=d.text,
**self.output_dict)
**self.output)
class StateMixin(object):
@@ -332,7 +485,7 @@ class CmdMixin(object):
return rc, out, err
def run_command(self, extra_params=None, params=None, *args, **kwargs):
self.vars['cmd_args'] = self._calculate_args(extra_params, params)
self.vars.cmd_args = self._calculate_args(extra_params, params)
options = dict(self.run_command_fixed_options)
env_update = dict(options.get('environ_update', {}))
options['check_rc'] = options.get('check_rc', self.check_rc)
@@ -341,7 +494,7 @@ class CmdMixin(object):
self.update_output(force_lang=self.force_lang)
options['environ_update'] = env_update
options.update(kwargs)
rc, out, err = self.module.run_command(self.vars['cmd_args'], *args, **options)
rc, out, err = self.module.run_command(self.vars.cmd_args, *args, **options)
self.update_output(rc=rc, stdout=out, stderr=err)
return self.process_command_output(rc, out, err)

View File

@@ -57,6 +57,34 @@ def _get_pritunl_organizations(api_token, api_secret, base_url, validate_certs=T
)
def _delete_pritunl_organization(
api_token, api_secret, base_url, organization_id, validate_certs=True
):
return pritunl_auth_request(
base_url=base_url,
api_token=api_token,
api_secret=api_secret,
method="DELETE",
path="/organization/%s" % (organization_id),
validate_certs=validate_certs,
)
def _post_pritunl_organization(
api_token, api_secret, base_url, organization_data, validate_certs=True
):
return pritunl_auth_request(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
method="POST",
path="/organization/%s",
headers={"Content-Type": "application/json"},
data=json.dumps(organization_data),
validate_certs=validate_certs,
)
def _get_pritunl_users(
api_token, api_secret, base_url, organization_id, validate_certs=True
):
@@ -179,6 +207,29 @@ def list_pritunl_users(
return users
def post_pritunl_organization(
api_token,
api_secret,
base_url,
organization_name,
validate_certs=True,
):
response = _post_pritunl_organization(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
organization_data={"name": organization_name},
validate_certs=True,
)
if response.getcode() != 200:
raise PritunlException(
"Could not add organization %s to Pritunl" % (organization_name)
)
# The user PUT request returns the updated user object
return json.loads(response.read())
def post_pritunl_user(
api_token,
api_secret,
@@ -227,6 +278,25 @@ def post_pritunl_user(
return json.loads(response.read())
def delete_pritunl_organization(
api_token, api_secret, base_url, organization_id, validate_certs=True
):
response = _delete_pritunl_organization(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
organization_id=organization_id,
validate_certs=True,
)
if response.getcode() != 200:
raise PritunlException(
"Could not remove organization %s from Pritunl" % (organization_id)
)
return json.loads(response.read())
def delete_pritunl_user(
api_token, api_secret, base_url, organization_id, user_id, validate_certs=True
):

View File

@@ -102,7 +102,8 @@ def do_install(module, mode, rootfs, container, image, values_list, backend):
system_list = ["--system"] if mode == 'system' else []
user_list = ["--user"] if mode == 'user' else []
rootfs_list = ["--rootfs=%s" % rootfs] if rootfs else []
args = ['atomic', 'install', "--storage=%s" % backend, '--name=%s' % container] + system_list + user_list + rootfs_list + values_list + [image]
atomic_bin = module.get_bin_path('atomic')
args = [atomic_bin, 'install', "--storage=%s" % backend, '--name=%s' % container] + system_list + user_list + rootfs_list + values_list + [image]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
@@ -112,7 +113,8 @@ def do_install(module, mode, rootfs, container, image, values_list, backend):
def do_update(module, container, image, values_list):
args = ['atomic', 'containers', 'update', "--rebase=%s" % image] + values_list + [container]
atomic_bin = module.get_bin_path('atomic')
args = [atomic_bin, 'containers', 'update', "--rebase=%s" % image] + values_list + [container]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
@@ -122,7 +124,8 @@ def do_update(module, container, image, values_list):
def do_uninstall(module, name, backend):
args = ['atomic', 'uninstall', "--storage=%s" % backend, name]
atomic_bin = module.get_bin_path('atomic')
args = [atomic_bin, 'uninstall', "--storage=%s" % backend, name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
@@ -130,7 +133,8 @@ def do_uninstall(module, name, backend):
def do_rollback(module, name):
args = ['atomic', 'containers', 'rollback', name]
atomic_bin = module.get_bin_path('atomic')
args = [atomic_bin, 'containers', 'rollback', name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
@@ -148,14 +152,12 @@ def core(module):
backend = module.params['backend']
state = module.params['state']
atomic_bin = module.get_bin_path('atomic')
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
out = {}
err = {}
rc = 0
values_list = ["--set=%s" % x for x in values] if values else []
args = ['atomic', 'containers', 'list', '--no-trunc', '-n', '--all', '-f', 'backend=%s' % backend, '-f', 'container=%s' % name]
args = [atomic_bin, 'containers', 'list', '--no-trunc', '-n', '--all', '-f', 'backend=%s' % backend, '-f', 'container=%s' % name]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0:
module.fail_json(rc=rc, msg=err)
@@ -194,9 +196,7 @@ def main():
module.fail_json(msg="values is supported only with user or system mode")
# Verify that the platform supports atomic command
rc, out, err = module.run_command('atomic -v', check_rc=False)
if rc != 0:
module.fail_json(msg="Error in running atomic command", err=err)
dummy = module.get_bin_path('atomic', required=True)
try:
core(module)

View File

@@ -57,18 +57,14 @@ from ansible.module_utils._text import to_native
def core(module):
revision = module.params['revision']
args = []
atomic_bin = module.get_bin_path('atomic', required=True)
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
if revision == 'latest':
args = ['atomic', 'host', 'upgrade']
args = [atomic_bin, 'host', 'upgrade']
else:
args = ['atomic', 'host', 'deploy', revision]
out = {}
err = {}
rc = 0
args = [atomic_bin, 'host', 'deploy', revision]
rc, out, err = module.run_command(args, check_rc=False)

View File

@@ -73,7 +73,8 @@ from ansible.module_utils._text import to_native
def do_upgrade(module, image):
args = ['atomic', 'update', '--force', image]
atomic_bin = module.get_bin_path('atomic')
args = [atomic_bin, 'update', '--force', image]
rc, out, err = module.run_command(args, check_rc=False)
if rc != 0: # something went wrong emit the msg
module.fail_json(rc=rc, msg=err)
@@ -91,20 +92,21 @@ def core(module):
is_upgraded = False
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
atomic_bin = module.get_bin_path('atomic')
out = {}
err = {}
rc = 0
if backend:
if state == 'present' or state == 'latest':
args = ['atomic', 'pull', "--storage=%s" % backend, image]
args = [atomic_bin, 'pull', "--storage=%s" % backend, image]
rc, out, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
else:
out_run = ""
if started:
args = ['atomic', 'run', "--storage=%s" % backend, image]
args = [atomic_bin, 'run', "--storage=%s" % backend, image]
rc, out_run, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
@@ -112,7 +114,7 @@ def core(module):
changed = "Extracting" in out or "Copying blob" in out
module.exit_json(msg=(out + out_run), changed=changed)
elif state == 'absent':
args = ['atomic', 'images', 'delete', "--storage=%s" % backend, image]
args = [atomic_bin, 'images', 'delete', "--storage=%s" % backend, image]
rc, out, err = module.run_command(args, check_rc=False)
if rc < 0:
module.fail_json(rc=rc, msg=err)
@@ -126,11 +128,11 @@ def core(module):
is_upgraded = do_upgrade(module, image)
if started:
args = ['atomic', 'run', image]
args = [atomic_bin, 'run', image]
else:
args = ['atomic', 'install', image]
args = [atomic_bin, 'install', image]
elif state == 'absent':
args = ['atomic', 'uninstall', image]
args = [atomic_bin, 'uninstall', image]
rc, out, err = module.run_command(args, check_rc=False)
@@ -155,9 +157,7 @@ def main():
)
# Verify that the platform supports atomic command
rc, out, err = module.run_command('atomic -v', check_rc=False)
if rc != 0:
module.fail_json(msg="Error in running atomic command", err=err)
dummy = module.get_bin_path('atomic', required=True)
try:
core(module)

View File

@@ -260,7 +260,7 @@ class DimensionDataNetworkModule(DimensionDataModule):
)
self.module.fail_json(
"Unexpected failure deleting network with id %s", network.id
"Unexpected failure deleting network with id %s" % network.id
)
except DimensionDataAPIException as e:

View File

@@ -1229,24 +1229,6 @@ class RHEV(object):
self.__get_conn()
return self.conn.set_VM_Host(vmname, vmhost)
# pylint: disable=unreachable
VM = self.conn.get_VM(vmname)
HOST = self.conn.get_Host(vmhost)
if VM.placement_policy.host is None:
self.conn.set_VM_Host(vmname, vmhost)
elif str(VM.placement_policy.host.id) != str(HOST.id):
self.conn.set_VM_Host(vmname, vmhost)
else:
setMsg("VM's startup host was already set to " + vmhost)
checkFail()
if str(VM.status.state) == "up":
self.conn.migrate_VM(vmname, vmhost)
checkFail()
return True
def setHost(self, hostname, cluster, ifaces):
self.__get_conn()
return self.conn.set_Host(hostname, cluster, ifaces)

View File

@@ -162,7 +162,6 @@ def waitForTaskDone(client, name, taskId, timeout):
currentTimeout -= 5
if currentTimeout < 0:
return False
return True
def main():

View File

@@ -119,20 +119,13 @@ class NicTag(object):
return is_mac(self.mac.lower())
def nictag_exists(self):
cmd = [self.nictagadm_bin]
cmd.append('exists')
cmd.append(self.name)
cmd = [self.nictagadm_bin, 'exists', self.name]
(rc, dummy, dummy) = self.module.run_command(cmd)
return rc == 0
def add_nictag(self):
cmd = [self.nictagadm_bin]
cmd.append('-v')
cmd.append('add')
cmd = [self.nictagadm_bin, '-v', 'add']
if self.etherstub:
cmd.append('-l')
@@ -150,10 +143,7 @@ class NicTag(object):
return self.module.run_command(cmd)
def delete_nictag(self):
cmd = [self.nictagadm_bin]
cmd.append('-v')
cmd.append('delete')
cmd = [self.nictagadm_bin, '-v', 'delete']
if self.force:
cmd.append('-f')

View File

@@ -72,10 +72,7 @@ class ImageFacts(object):
self.filters = module.params['filters']
def return_all_installed_images(self):
cmd = [self.module.get_bin_path('imgadm')]
cmd.append('list')
cmd.append('-j')
cmd = [self.module.get_bin_path('imgadm'), 'list', '-j']
if self.filters:
cmd.append(self.filters)

View File

@@ -233,7 +233,7 @@ options:
description:
- List of resolvers to be put into C(/etc/resolv.conf).
type: list
elements: dict
elements: str
routes:
required: false
description:
@@ -702,7 +702,7 @@ def main():
vnc_password=dict(type='str', no_log=True),
disks=dict(type='list', elements='dict'),
nics=dict(type='list', elements='dict'),
resolvers=dict(type='list', elements='dict'),
resolvers=dict(type='list', elements='str'),
filesystems=dict(type='list', elements='dict'),
)

View File

@@ -170,25 +170,23 @@ def install_plugin(module, plugin_bin, plugin_name, url, timeout, allow_root, ki
cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["present"], plugin_name]
if url:
cmd_args.append("--url %s" % url)
cmd_args.extend(["--url", url])
if timeout:
cmd_args.append("--timeout %s" % timeout)
cmd_args.extend(["--timeout", timeout])
if allow_root:
cmd_args.append('--allow-root')
cmd = " ".join(cmd_args)
if module.check_mode:
return True, cmd, "check mode", ""
return True, " ".join(cmd_args), "check mode", ""
rc, out, err = module.run_command(cmd)
rc, out, err = module.run_command(cmd_args)
if rc != 0:
reason = parse_error(out)
module.fail_json(msg=reason)
return True, cmd, out, err
return True, " ".join(cmd_args), out, err
def remove_plugin(module, plugin_bin, plugin_name, allow_root, kibana_version='4.6'):
@@ -201,17 +199,15 @@ def remove_plugin(module, plugin_bin, plugin_name, allow_root, kibana_version='4
if allow_root:
cmd_args.append('--allow-root')
cmd = " ".join(cmd_args)
if module.check_mode:
return True, cmd, "check mode", ""
return True, " ".join(cmd_args), "check mode", ""
rc, out, err = module.run_command(cmd)
rc, out, err = module.run_command(cmd_args)
if rc != 0:
reason = parse_error(out)
module.fail_json(msg=reason)
return True, cmd, out, err
return True, " ".join(cmd_args), out, err
def get_kibana_version(module, plugin_bin, allow_root):
@@ -220,8 +216,7 @@ def get_kibana_version(module, plugin_bin, allow_root):
if allow_root:
cmd_args.append('--allow-root')
cmd = " ".join(cmd_args)
rc, out, err = module.run_command(cmd)
rc, out, err = module.run_command(cmd_args)
if rc != 0:
module.fail_json(msg="Failed to get Kibana version : %s" % err)
@@ -269,7 +264,7 @@ def main():
if state == "present":
if force:
remove_plugin(module, plugin_bin, name)
remove_plugin(module, plugin_bin, name, allow_root, kibana_version)
changed, cmd, out, err = install_plugin(module, plugin_bin, name, url, timeout, allow_root, kibana_version)
elif state == "absent":

View File

@@ -98,9 +98,8 @@ from ansible.module_utils._text import to_native
def get_xattr_keys(module, path, follow):
cmd = [module.get_bin_path('getfattr', True)]
# prevents warning and not sure why it's not default
cmd.append('--absolute-names')
cmd = [module.get_bin_path('getfattr', True), '--absolute-names']
if not follow:
cmd.append('-h')
cmd.append(path)
@@ -109,10 +108,8 @@ def get_xattr_keys(module, path, follow):
def get_xattr(module, path, key, follow):
cmd = [module.get_bin_path('getfattr', True), '--absolute-names']
cmd = [module.get_bin_path('getfattr', True)]
# prevents warning and not sure why it's not default
cmd.append('--absolute-names')
if not follow:
cmd.append('-h')
if key is None:

View File

@@ -14,6 +14,13 @@ short_description: Manage Global FreeIPA Configuration Settings
description:
- Modify global configuration settings of a FreeIPA Server.
options:
ipaconfigstring:
description: Extra hashes to generate in password plug-in.
aliases: ["configstring"]
type: list
elements: str
choices: ["AllowNThash", "KDC:Disable Last Success", "KDC:Disable Lockout", "KDC:Disable Default Preauth for SPNs"]
version_added: '2.5.0'
ipadefaultloginshell:
description: Default shell for new users.
aliases: ["loginshell"]
@@ -22,25 +29,158 @@ options:
description: Default e-mail domain for new users.
aliases: ["emaildomain"]
type: str
ipadefaultprimarygroup:
description: Default group for new users.
aliases: ["primarygroup"]
type: str
version_added: '2.5.0'
ipagroupsearchfields:
description: A list of fields to search in when searching for groups.
aliases: ["groupsearchfields"]
type: list
elements: str
version_added: '2.5.0'
ipahomesrootdir:
description: Default location of home directories.
aliases: ["homesrootdir"]
type: str
version_added: '2.5.0'
ipakrbauthzdata:
description: Default types of PAC supported for services.
aliases: ["krbauthzdata"]
type: list
elements: str
choices: ["MS-PAC", "PAD", "nfs:NONE"]
version_added: '2.5.0'
ipamaxusernamelength:
description: Maximum length of usernames.
aliases: ["maxusernamelength"]
type: int
version_added: '2.5.0'
ipapwdexpadvnotify:
description: Notice of impending password expiration, in days.
aliases: ["pwdexpadvnotify"]
type: int
version_added: '2.5.0'
ipasearchrecordslimit:
description: Maximum number of records to search (-1 or 0 is unlimited).
aliases: ["searchrecordslimit"]
type: int
version_added: '2.5.0'
ipasearchtimelimit:
description: Maximum amount of time (seconds) for a search (-1 or 0 is unlimited).
aliases: ["searchtimelimit"]
type: int
version_added: '2.5.0'
ipauserauthtype:
description: The authentication type to use by default.
aliases: ["userauthtype"]
choices: ["password", "radius", "otp", "pkinit", "hardened", "disabled"]
type: list
elements: str
version_added: '2.5.0'
ipausersearchfields:
description: A list of fields to search in when searching for users.
aliases: ["usersearchfields"]
type: list
elements: str
version_added: '2.5.0'
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure the default login shell is bash.
- name: Ensure password plugin features DC:Disable Last Success and KDC:Disable Lockout are enabled
community.general.ipa_config:
ipaconfigstring: ["KDC:Disable Last Success", "KDC:Disable Lockout"]
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default login shell is bash
community.general.ipa_config:
ipadefaultloginshell: /bin/bash
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default e-mail domain is ansible.com.
- name: Ensure the default e-mail domain is ansible.com
community.general.ipa_config:
ipadefaultemaildomain: ansible.com
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default primary group is set to ipausers
community.general.ipa_config:
ipadefaultprimarygroup: ipausers
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the group search fields are set to 'cn,description'
community.general.ipa_config:
ipagroupsearchfields: ['cn', 'description']
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the home directory location is set to /home
community.general.ipa_config:
ipahomesrootdir: /home
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default types of PAC supported for services is set to MS-PAC and PAD
community.general.ipa_config:
ipakrbauthzdata: ["MS-PAC", "PAD"]
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the maximum user name length is set to 32
community.general.ipa_config:
ipamaxusernamelength: 32
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the password expiration notice is set to 4 days
community.general.ipa_config:
ipapwdexpadvnotify: 4
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the search record limit is set to 100
community.general.ipa_config:
ipasearchrecordslimit: 100
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the search time limit is set to 2 seconds
community.general.ipa_config:
ipasearchtimelimit: 2
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the default user auth type is password
community.general.ipa_config:
ipauserauthtype: ['password']
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the user search fields is set to 'uid,givenname,sn,ou,title'
community.general.ipa_config:
ipausersearchfields: ['uid', 'givenname', 'sn', 'ou', 'title']
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
'''
RETURN = r'''
@@ -68,12 +208,40 @@ class ConfigIPAClient(IPAClient):
return self._post_json(method='config_mod', name=name, item=item)
def get_config_dict(ipadefaultloginshell=None, ipadefaultemaildomain=None):
def get_config_dict(ipaconfigstring=None, ipadefaultloginshell=None,
ipadefaultemaildomain=None, ipadefaultprimarygroup=None,
ipagroupsearchfields=None, ipahomesrootdir=None,
ipakrbauthzdata=None, ipamaxusernamelength=None,
ipapwdexpadvnotify=None, ipasearchrecordslimit=None,
ipasearchtimelimit=None, ipauserauthtype=None,
ipausersearchfields=None):
config = {}
if ipaconfigstring is not None:
config['ipaconfigstring'] = ipaconfigstring
if ipadefaultloginshell is not None:
config['ipadefaultloginshell'] = ipadefaultloginshell
if ipadefaultemaildomain is not None:
config['ipadefaultemaildomain'] = ipadefaultemaildomain
if ipadefaultprimarygroup is not None:
config['ipadefaultprimarygroup'] = ipadefaultprimarygroup
if ipagroupsearchfields is not None:
config['ipagroupsearchfields'] = ','.join(ipagroupsearchfields)
if ipahomesrootdir is not None:
config['ipahomesrootdir'] = ipahomesrootdir
if ipakrbauthzdata is not None:
config['ipakrbauthzdata'] = ipakrbauthzdata
if ipamaxusernamelength is not None:
config['ipamaxusernamelength'] = str(ipamaxusernamelength)
if ipapwdexpadvnotify is not None:
config['ipapwdexpadvnotify'] = str(ipapwdexpadvnotify)
if ipasearchrecordslimit is not None:
config['ipasearchrecordslimit'] = str(ipasearchrecordslimit)
if ipasearchtimelimit is not None:
config['ipasearchtimelimit'] = str(ipasearchtimelimit)
if ipauserauthtype is not None:
config['ipauserauthtype'] = ipauserauthtype
if ipausersearchfields is not None:
config['ipausersearchfields'] = ','.join(ipausersearchfields)
return config
@@ -84,8 +252,19 @@ def get_config_diff(client, ipa_config, module_config):
def ensure(module, client):
module_config = get_config_dict(
ipaconfigstring=module.params.get('ipaconfigstring'),
ipadefaultloginshell=module.params.get('ipadefaultloginshell'),
ipadefaultemaildomain=module.params.get('ipadefaultemaildomain'),
ipadefaultprimarygroup=module.params.get('ipadefaultprimarygroup'),
ipagroupsearchfields=module.params.get('ipagroupsearchfields'),
ipahomesrootdir=module.params.get('ipahomesrootdir'),
ipakrbauthzdata=module.params.get('ipakrbauthzdata'),
ipamaxusernamelength=module.params.get('ipamaxusernamelength'),
ipapwdexpadvnotify=module.params.get('ipapwdexpadvnotify'),
ipasearchrecordslimit=module.params.get('ipasearchrecordslimit'),
ipasearchtimelimit=module.params.get('ipasearchtimelimit'),
ipauserauthtype=module.params.get('ipauserauthtype'),
ipausersearchfields=module.params.get('ipausersearchfields'),
)
ipa_config = client.config_show()
diff = get_config_diff(client, ipa_config, module_config)
@@ -106,8 +285,31 @@ def ensure(module, client):
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(
ipaconfigstring=dict(type='list', elements='str',
choices=['AllowNThash',
'KDC:Disable Last Success',
'KDC:Disable Lockout',
'KDC:Disable Default Preauth for SPNs'],
aliases=['configstring']),
ipadefaultloginshell=dict(type='str', aliases=['loginshell']),
ipadefaultemaildomain=dict(type='str', aliases=['emaildomain']),
ipadefaultprimarygroup=dict(type='str', aliases=['primarygroup']),
ipagroupsearchfields=dict(type='list', elements='str',
aliases=['groupsearchfields']),
ipahomesrootdir=dict(type='str', aliases=['homesrootdir']),
ipakrbauthzdata=dict(type='list', elements='str',
choices=['MS-PAC', 'PAD', 'nfs:NONE'],
aliases=['krbauthzdata']),
ipamaxusernamelength=dict(type='int', aliases=['maxusernamelength']),
ipapwdexpadvnotify=dict(type='int', aliases=['pwdexpadvnotify']),
ipasearchrecordslimit=dict(type='int', aliases=['searchrecordslimit']),
ipasearchtimelimit=dict(type='int', aliases=['searchtimelimit']),
ipauserauthtype=dict(type='list', elements='str',
aliases=['userauthtype'],
choices=["password", "radius", "otp", "pkinit",
"hardened", "disabled"]),
ipausersearchfields=dict(type='list', elements='str',
aliases=['usersearchfields']),
)
module = AnsibleModule(

View File

@@ -0,0 +1,172 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Ansible Project
# Heavily influenced from Fran Fitzpatrick <francis.x.fitzpatrick@gmail.com> ipa_config module
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: ipa_otpconfig
author: justchris1 (@justchris1)
short_description: Manage FreeIPA OTP Configuration Settings
version_added: 2.5.0
description:
- Modify global configuration settings of a FreeIPA Server with respect to OTP (One Time Passwords).
options:
ipatokentotpauthwindow:
description: TOTP authentication window in seconds.
aliases: ["totpauthwindow"]
type: int
ipatokentotpsyncwindow:
description: TOTP synchronization window in seconds.
aliases: ["totpsyncwindow"]
type: int
ipatokenhotpauthwindow:
description: HOTP authentication window in number of hops.
aliases: ["hotpauthwindow"]
type: int
ipatokenhotpsyncwindow:
description: HOTP synchronization window in hops.
aliases: ["hotpsyncwindow"]
type: int
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Ensure the TOTP authentication window is set to 300 seconds
community.general.ipa_otpconfig:
ipatokentotpauthwindow: '300'
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the TOTP syncronization window is set to 86400 seconds
community.general.ipa_otpconfig:
ipatokentotpsyncwindow: '86400'
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the HOTP authentication window is set to 10 hops
community.general.ipa_otpconfig:
ipatokenhotpauthwindow: '10'
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
- name: Ensure the HOTP syncronization window is set to 100 hops
community.general.ipa_otpconfig:
ipatokenhotpsyncwindow: '100'
ipa_host: localhost
ipa_user: admin
ipa_pass: supersecret
'''
RETURN = r'''
otpconfig:
description: OTP configuration as returned by IPA API.
returned: always
type: dict
'''
import traceback
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class OTPConfigIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(OTPConfigIPAClient, self).__init__(module, host, port, protocol)
def otpconfig_show(self):
return self._post_json(method='otpconfig_show', name=None)
def otpconfig_mod(self, name, item):
return self._post_json(method='otpconfig_mod', name=name, item=item)
def get_otpconfig_dict(ipatokentotpauthwindow=None, ipatokentotpsyncwindow=None,
ipatokenhotpauthwindow=None, ipatokenhotpsyncwindow=None):
config = {}
if ipatokentotpauthwindow is not None:
config['ipatokentotpauthwindow'] = str(ipatokentotpauthwindow)
if ipatokentotpsyncwindow is not None:
config['ipatokentotpsyncwindow'] = str(ipatokentotpsyncwindow)
if ipatokenhotpauthwindow is not None:
config['ipatokenhotpauthwindow'] = str(ipatokenhotpauthwindow)
if ipatokenhotpsyncwindow is not None:
config['ipatokenhotpsyncwindow'] = str(ipatokenhotpsyncwindow)
return config
def get_otpconfig_diff(client, ipa_config, module_config):
return client.get_diff(ipa_data=ipa_config, module_data=module_config)
def ensure(module, client):
module_otpconfig = get_otpconfig_dict(
ipatokentotpauthwindow=module.params.get('ipatokentotpauthwindow'),
ipatokentotpsyncwindow=module.params.get('ipatokentotpsyncwindow'),
ipatokenhotpauthwindow=module.params.get('ipatokenhotpauthwindow'),
ipatokenhotpsyncwindow=module.params.get('ipatokenhotpsyncwindow'),
)
ipa_otpconfig = client.otpconfig_show()
diff = get_otpconfig_diff(client, ipa_otpconfig, module_otpconfig)
changed = False
new_otpconfig = {}
for module_key in diff:
if module_otpconfig.get(module_key) != ipa_otpconfig.get(module_key, None):
changed = True
new_otpconfig.update({module_key: module_otpconfig.get(module_key)})
if changed and not module.check_mode:
client.otpconfig_mod(name=None, item=new_otpconfig)
return changed, client.otpconfig_show()
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(
ipatokentotpauthwindow=dict(type='int', aliases=['totpauthwindow'], no_log=False),
ipatokentotpsyncwindow=dict(type='int', aliases=['totpsyncwindow'], no_log=False),
ipatokenhotpauthwindow=dict(type='int', aliases=['hotpauthwindow'], no_log=False),
ipatokenhotpsyncwindow=dict(type='int', aliases=['hotpsyncwindow'], no_log=False),
)
module = AnsibleModule(
argument_spec=argument_spec,
supports_check_mode=True
)
client = OTPConfigIPAClient(
module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot']
)
try:
client.login(
username=module.params['ipa_user'],
password=module.params['ipa_pass']
)
changed, otpconfig = ensure(module, client)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
module.exit_json(changed=changed, otpconfig=otpconfig)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,527 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2017, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = r'''
---
module: ipa_otptoken
author: justchris1 (@justchris1)
short_description: Manage FreeIPA OTPs
version_added: 2.5.0
description:
- Add, modify, and delete One Time Passwords in IPA.
options:
uniqueid:
description: Unique ID of the token in IPA.
required: true
aliases: ["name"]
type: str
newuniqueid:
description: If specified, the unique id specified will be changed to this.
type: str
otptype:
description:
- Type of OTP.
- "B(Note:) Cannot be modified after OTP is created."
type: str
choices: [ totp, hotp ]
secretkey:
description:
- Token secret (Base64).
- If OTP is created and this is not specified, a random secret will be generated by IPA.
- "B(Note:) Cannot be modified after OTP is created."
type: str
description:
description: Description of the token (informational only).
type: str
owner:
description: Assigned user of the token.
type: str
enabled:
description: Mark the token as enabled (default C(true)).
default: true
type: bool
notbefore:
description:
- First date/time the token can be used.
- In the format C(YYYYMMddHHmmss).
- For example, C(20180121182022) will allow the token to be used starting on 21 January 2018 at 18:20:22.
type: str
notafter:
description:
- Last date/time the token can be used.
- In the format C(YYYYMMddHHmmss).
- For example, C(20200121182022) will allow the token to be used until 21 January 2020 at 18:20:22.
type: str
vendor:
description: Token vendor name (informational only).
type: str
model:
description: Token model (informational only).
type: str
serial:
description: Token serial (informational only).
type: str
state:
description: State to ensure.
choices: ['present', 'absent']
default: 'present'
type: str
algorithm:
description:
- Token hash algorithm.
- "B(Note:) Cannot be modified after OTP is created."
choices: ['sha1', 'sha256', 'sha384', 'sha512']
type: str
digits:
description:
- Number of digits each token code will have.
- "B(Note:) Cannot be modified after OTP is created."
choices: [ 6, 8 ]
type: int
offset:
description:
- TOTP token / IPA server time difference.
- "B(Note:) Cannot be modified after OTP is created."
type: int
interval:
description:
- Length of TOTP token code validity in seconds.
- "B(Note:) Cannot be modified after OTP is created."
type: int
counter:
description:
- Initial counter for the HOTP token.
- "B(Note:) Cannot be modified after OTP is created."
type: int
extends_documentation_fragment:
- community.general.ipa.documentation
'''
EXAMPLES = r'''
- name: Create a totp for pinky, allowing the IPA server to generate using defaults
community.general.ipa_otptoken:
uniqueid: Token123
otptype: totp
owner: pinky
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Create a 8 digit hotp for pinky with sha256 with specified validity times
community.general.ipa_otptoken:
uniqueid: Token123
enabled: true
otptype: hotp
digits: 8
secretkey: UMKSIER00zT2T2tWMUlTRmNlekRCbFQvWFBVZUh2dElHWGR6T3VUR3IzK2xjaFk9
algorithm: sha256
notbefore: 20180121182123
notafter: 20220121182123
owner: pinky
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Update Token123 to indicate a vendor, model, serial number (info only), and description
community.general.ipa_otptoken:
uniqueid: Token123
vendor: Acme
model: acme101
serial: SerialNumber1
description: Acme OTP device
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Disable Token123
community.general.ipa_otptoken:
uniqueid: Token123
enabled: false
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
- name: Rename Token123 to TokenABC and enable it
community.general.ipa_otptoken:
uniqueid: Token123
newuniqueid: TokenABC
enabled: true
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
'''
RETURN = r'''
otptoken:
description: OTP Token as returned by IPA API
returned: always
type: dict
'''
import base64
import traceback
from ansible.module_utils.basic import AnsibleModule, sanitize_keys
from ansible_collections.community.general.plugins.module_utils.ipa import IPAClient, ipa_argument_spec
from ansible.module_utils._text import to_native
class OTPTokenIPAClient(IPAClient):
def __init__(self, module, host, port, protocol):
super(OTPTokenIPAClient, self).__init__(module, host, port, protocol)
def otptoken_find(self, name):
return self._post_json(method='otptoken_find', name=None, item={'all': True,
'ipatokenuniqueid': name,
'timelimit': '0',
'sizelimit': '0'})
def otptoken_add(self, name, item):
return self._post_json(method='otptoken_add', name=name, item=item)
def otptoken_mod(self, name, item):
return self._post_json(method='otptoken_mod', name=name, item=item)
def otptoken_del(self, name):
return self._post_json(method='otptoken_del', name=name)
def base64_to_base32(base64_string):
"""Converts base64 string to base32 string"""
b32_string = base64.b32encode(base64.b64decode(base64_string)).decode('ascii')
return b32_string
def base32_to_base64(base32_string):
"""Converts base32 string to base64 string"""
b64_string = base64.b64encode(base64.b32decode(base32_string)).decode('ascii')
return b64_string
def get_otptoken_dict(ansible_to_ipa, uniqueid=None, newuniqueid=None, otptype=None, secretkey=None, description=None, owner=None,
enabled=None, notbefore=None, notafter=None, vendor=None,
model=None, serial=None, algorithm=None, digits=None, offset=None,
interval=None, counter=None):
"""Create the dictionary of settings passed in"""
otptoken = {}
if uniqueid is not None:
otptoken[ansible_to_ipa['uniqueid']] = uniqueid
if newuniqueid is not None:
otptoken[ansible_to_ipa['newuniqueid']] = newuniqueid
if otptype is not None:
otptoken[ansible_to_ipa['otptype']] = otptype.upper()
if secretkey is not None:
# For some unknown reason, while IPA returns the secret in base64,
# it wants the secret passed in as base32. This makes it more difficult
# for comparison (does 'current' equal to 'new'). Moreover, this may
# cause some subtle issue in a playbook as the output is encoded
# in a different way than if it was passed in as a parameter. For
# these reasons, have the module standardize on base64 input (as parameter)
# and output (from IPA).
otptoken[ansible_to_ipa['secretkey']] = base64_to_base32(secretkey)
if description is not None:
otptoken[ansible_to_ipa['description']] = description
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'
if notbefore is not None:
otptoken[ansible_to_ipa['notbefore']] = notbefore + 'Z'
if notafter is not None:
otptoken[ansible_to_ipa['notafter']] = notafter + 'Z'
if vendor is not None:
otptoken[ansible_to_ipa['vendor']] = vendor
if model is not None:
otptoken[ansible_to_ipa['model']] = model
if serial is not None:
otptoken[ansible_to_ipa['serial']] = serial
if algorithm is not None:
otptoken[ansible_to_ipa['algorithm']] = algorithm
if digits is not None:
otptoken[ansible_to_ipa['digits']] = str(digits)
if offset is not None:
otptoken[ansible_to_ipa['offset']] = str(offset)
if interval is not None:
otptoken[ansible_to_ipa['interval']] = str(interval)
if counter is not None:
otptoken[ansible_to_ipa['counter']] = str(counter)
return otptoken
def transform_output(ipa_otptoken, ansible_to_ipa, ipa_to_ansible):
"""Transform the output received by IPA to a format more friendly
before it is returned to the user. IPA returns even simple
strings as a list of strings. It also returns bools and
int as string. This function cleans that up before return.
"""
updated_otptoken = ipa_otptoken
# Used to hold values that will be sanitized from output as no_log.
# For the case where secretkey is not specified at the module, but
# is passed back from IPA.
sanitize_strings = set()
# Rename the IPA parameters to the more friendly ansible module names for them
for ipa_parameter in ipa_to_ansible:
if ipa_parameter in ipa_otptoken:
updated_otptoken[ipa_to_ansible[ipa_parameter]] = ipa_otptoken[ipa_parameter]
updated_otptoken.pop(ipa_parameter)
# Change the type from IPA's list of string to the appropriate return value type
# based on field. By default, assume they should be strings.
for ansible_parameter in ansible_to_ipa:
if ansible_parameter in updated_otptoken:
if isinstance(updated_otptoken[ansible_parameter], list) and len(updated_otptoken[ansible_parameter]) == 1:
if ansible_parameter in ['digits', 'offset', 'interval', 'counter']:
updated_otptoken[ansible_parameter] = int(updated_otptoken[ansible_parameter][0])
elif ansible_parameter == 'enabled':
updated_otptoken[ansible_parameter] = bool(updated_otptoken[ansible_parameter][0])
else:
updated_otptoken[ansible_parameter] = updated_otptoken[ansible_parameter][0]
if 'secretkey' in updated_otptoken:
if isinstance(updated_otptoken['secretkey'], dict):
if '__base64__' in updated_otptoken['secretkey']:
sanitize_strings.add(updated_otptoken['secretkey']['__base64__'])
b64key = updated_otptoken['secretkey']['__base64__']
updated_otptoken.pop('secretkey')
updated_otptoken['secretkey'] = b64key
sanitize_strings.add(b64key)
elif '__base32__' in updated_otptoken['secretkey']:
sanitize_strings.add(updated_otptoken['secretkey']['__base32__'])
b32key = updated_otptoken['secretkey']['__base32__']
b64key = base32_to_base64(b32key)
updated_otptoken.pop('secretkey')
updated_otptoken['secretkey'] = b64key
sanitize_strings.add(b32key)
sanitize_strings.add(b64key)
return updated_otptoken, sanitize_strings
def validate_modifications(ansible_to_ipa, module, ipa_otptoken,
module_otptoken, unmodifiable_after_creation):
"""Checks to see if the requested modifications are valid. Some elements
cannot be modified after initial creation. However, we still want to
validate arguments that are specified, but are not different than what
is currently set on the server.
"""
modifications_valid = True
for parameter in unmodifiable_after_creation:
if ansible_to_ipa[parameter] in module_otptoken and ansible_to_ipa[parameter] in ipa_otptoken:
mod_value = module_otptoken[ansible_to_ipa[parameter]]
# For someone unknown reason, the returns from IPA put almost all
# values in a list, even though passing them in a list (even of
# length 1) will be rejected. The module values for all elements
# other than type (totp or hotp) have this happen.
if parameter == 'otptype':
ipa_value = ipa_otptoken[ansible_to_ipa[parameter]]
else:
if len(ipa_otptoken[ansible_to_ipa[parameter]]) != 1:
module.fail_json(msg=("Invariant fail: Return value from IPA is not a list " +
"of length 1. Please open a bug report for the module."))
if parameter == 'secretkey':
# We stored the secret key in base32 since we had assumed that would need to
# be the format if we were contacting IPA to create it. However, we are
# now comparing it against what is already set in the IPA server, so convert
# back to base64 for comparison.
mod_value = base32_to_base64(mod_value)
# For the secret key, it is even more specific in that the key is returned
# in a dict, in the list, as the __base64__ entry for the IPA response.
ipa_value = ipa_otptoken[ansible_to_ipa[parameter]][0]['__base64__']
if '__base64__' in ipa_otptoken[ansible_to_ipa[parameter]][0]:
ipa_value = ipa_otptoken[ansible_to_ipa[parameter]][0]['__base64__']
elif '__base32__' in ipa_otptoken[ansible_to_ipa[parameter]][0]:
b32key = ipa_otptoken[ansible_to_ipa[parameter]][0]['__base32__']
b64key = base32_to_base64(b32key)
ipa_value = b64key
else:
ipa_value = None
else:
ipa_value = ipa_otptoken[ansible_to_ipa[parameter]][0]
if mod_value != ipa_value:
modifications_valid = False
fail_message = ("Parameter '" + parameter + "' cannot be changed once " +
"the OTP is created and the requested value specified here (" +
str(mod_value) +
") differs from what is set in the IPA server ("
+ str(ipa_value) + ")")
module.fail_json(msg=fail_message)
return modifications_valid
def ensure(module, client):
# dict to map from ansible parameter names to attribute names
# used by IPA (which are not so friendly).
ansible_to_ipa = {'uniqueid': 'ipatokenuniqueid',
'newuniqueid': 'rename',
'otptype': 'type',
'secretkey': 'ipatokenotpkey',
'description': 'description',
'owner': 'ipatokenowner',
'enabled': 'ipatokendisabled',
'notbefore': 'ipatokennotbefore',
'notafter': 'ipatokennotafter',
'vendor': 'ipatokenvendor',
'model': 'ipatokenmodel',
'serial': 'ipatokenserial',
'algorithm': 'ipatokenotpalgorithm',
'digits': 'ipatokenotpdigits',
'offset': 'ipatokentotpclockoffset',
'interval': 'ipatokentotptimestep',
'counter': 'ipatokenhotpcounter'}
# Create inverse dictionary for mapping return values
ipa_to_ansible = {}
for (k, v) in ansible_to_ipa.items():
ipa_to_ansible[v] = k
unmodifiable_after_creation = ['otptype', 'secretkey', 'algorithm',
'digits', 'offset', 'interval', 'counter']
state = module.params['state']
uniqueid = module.params['uniqueid']
module_otptoken = get_otptoken_dict(ansible_to_ipa=ansible_to_ipa,
uniqueid=module.params.get('uniqueid'),
newuniqueid=module.params.get('newuniqueid'),
otptype=module.params.get('otptype'),
secretkey=module.params.get('secretkey'),
description=module.params.get('description'),
owner=module.params.get('owner'),
enabled=module.params.get('enabled'),
notbefore=module.params.get('notbefore'),
notafter=module.params.get('notafter'),
vendor=module.params.get('vendor'),
model=module.params.get('model'),
serial=module.params.get('serial'),
algorithm=module.params.get('algorithm'),
digits=module.params.get('digits'),
offset=module.params.get('offset'),
interval=module.params.get('interval'),
counter=module.params.get('counter'))
ipa_otptoken = client.otptoken_find(name=uniqueid)
if ansible_to_ipa['newuniqueid'] in module_otptoken:
# Check to see if the new unique id is already taken in use
ipa_otptoken_new = client.otptoken_find(name=module_otptoken[ansible_to_ipa['newuniqueid']])
if ipa_otptoken_new:
module.fail_json(msg=("Requested rename through newuniqueid to " +
module_otptoken[ansible_to_ipa['newuniqueid']] +
" failed because the new unique id is already in use"))
changed = False
if state == 'present':
if not ipa_otptoken:
changed = True
if not module.check_mode:
# It would not make sense to have a rename after creation, so if the user
# specified a newuniqueid, just replace the uniqueid with the updated one
# before creation
if ansible_to_ipa['newuniqueid'] in module_otptoken:
module_otptoken[ansible_to_ipa['uniqueid']] = module_otptoken[ansible_to_ipa['newuniqueid']]
uniqueid = module_otptoken[ansible_to_ipa['newuniqueid']]
module_otptoken.pop(ansible_to_ipa['newuniqueid'])
# IPA wants the unique id in the first position and not as a key/value pair.
# Get rid of it from the otptoken dict and just specify it in the name field
# for otptoken_add.
if ansible_to_ipa['uniqueid'] in module_otptoken:
module_otptoken.pop(ansible_to_ipa['uniqueid'])
module_otptoken['all'] = True
ipa_otptoken = client.otptoken_add(name=uniqueid, item=module_otptoken)
else:
if not(validate_modifications(ansible_to_ipa, module, ipa_otptoken,
module_otptoken, unmodifiable_after_creation)):
module.fail_json(msg="Modifications requested in module are not valid")
# IPA will reject 'modifications' that do not actually modify anything
# if any of the unmodifiable elements are specified. Explicitly
# get rid of them here. They were not different or else the
# we would have failed out in validate_modifications.
for x in unmodifiable_after_creation:
if ansible_to_ipa[x] in module_otptoken:
module_otptoken.pop(ansible_to_ipa[x])
diff = client.get_diff(ipa_data=ipa_otptoken, module_data=module_otptoken)
if len(diff) > 0:
changed = True
if not module.check_mode:
# IPA wants the unique id in the first position and not as a key/value pair.
# Get rid of it from the otptoken dict and just specify it in the name field
# for otptoken_mod.
if ansible_to_ipa['uniqueid'] in module_otptoken:
module_otptoken.pop(ansible_to_ipa['uniqueid'])
module_otptoken['all'] = True
ipa_otptoken = client.otptoken_mod(name=uniqueid, item=module_otptoken)
else:
if ipa_otptoken:
changed = True
if not module.check_mode:
client.otptoken_del(name=uniqueid)
# Transform the output to use ansible keywords (not the IPA keywords) and
# sanitize any key values in the output.
ipa_otptoken, sanitize_strings = transform_output(ipa_otptoken, ansible_to_ipa, ipa_to_ansible)
module.no_log_values = module.no_log_values.union(sanitize_strings)
sanitized_otptoken = sanitize_keys(obj=ipa_otptoken, no_log_strings=module.no_log_values)
return changed, sanitized_otptoken
def main():
argument_spec = ipa_argument_spec()
argument_spec.update(uniqueid=dict(type='str', aliases=['name'], required=True),
newuniqueid=dict(type='str'),
otptype=dict(type='str', choices=['totp', 'hotp']),
secretkey=dict(type='str', no_log=True),
description=dict(type='str'),
owner=dict(type='str'),
enabled=dict(type='bool', default=True),
notbefore=dict(type='str'),
notafter=dict(type='str'),
vendor=dict(type='str'),
model=dict(type='str'),
serial=dict(type='str'),
state=dict(type='str', choices=['present', 'absent'], default='present'),
algorithm=dict(type='str', choices=['sha1', 'sha256', 'sha384', 'sha512']),
digits=dict(type='int', choices=[6, 8]),
offset=dict(type='int'),
interval=dict(type='int'),
counter=dict(type='int'))
module = AnsibleModule(argument_spec=argument_spec,
supports_check_mode=True)
client = OTPTokenIPAClient(module=module,
host=module.params['ipa_host'],
port=module.params['ipa_port'],
protocol=module.params['ipa_prot'])
try:
client.login(username=module.params['ipa_user'],
password=module.params['ipa_pass'])
changed, otptoken = ensure(module, client)
except Exception as e:
module.fail_json(msg=to_native(e), exception=traceback.format_exc())
module.exit_json(changed=changed, otptoken=otptoken)
if __name__ == '__main__':
main()

View File

@@ -94,7 +94,8 @@ options:
description:
- The authentication type to use for the user.
choices: ["password", "radius", "otp", "pkinit", "hardened"]
type: str
type: list
elements: str
version_added: '1.2.0'
extends_documentation_fragment:
- community.general.ipa.documentation
@@ -146,11 +147,13 @@ EXAMPLES = r'''
ipa_pass: topsecret
update_password: on_create
- name: Ensure pinky is present and using one time password authentication
- name: Ensure pinky is present and using one time password and RADIUS authentication
community.general.ipa_user:
name: pinky
state: present
userauthtype: otp
userauthtype:
- otp
- radius
ipa_host: ipa.example.com
ipa_user: admin
ipa_pass: topsecret
@@ -269,16 +272,18 @@ def get_user_diff(client, ipa_user, module_user):
def get_ssh_key_fingerprint(ssh_key, hash_algo='sha256'):
"""
Return the public key fingerprint of a given public SSH key
in format "[fp] [user@host] (ssh-rsa)" where fp is of the format:
in format "[fp] [comment] (ssh-rsa)" where fp is of the format:
FB:0C:AC:0A:07:94:5B:CE:75:6E:63:32:13:AD:AD:D7
for md5 or
SHA256:[base64]
for sha256
Comments are assumed to be all characters past the second
whitespace character in the sshpubkey string.
:param ssh_key:
:param hash_algo:
:return:
"""
parts = ssh_key.strip().split()
parts = ssh_key.strip().split(None, 2)
if len(parts) == 0:
return None
key_type = parts[0]
@@ -293,8 +298,8 @@ def get_ssh_key_fingerprint(ssh_key, hash_algo='sha256'):
if len(parts) < 3:
return "%s (%s)" % (key_fp, key_type)
else:
user_host = parts[2]
return "%s %s (%s)" % (key_fp, user_host, key_type)
comment = parts[2]
return "%s %s (%s)" % (key_fp, comment, key_type)
def ensure(module, client):
@@ -361,7 +366,7 @@ def main():
telephonenumber=dict(type='list', elements='str'),
title=dict(type='str'),
homedirectory=dict(type='str'),
userauthtype=dict(type='str',
userauthtype=dict(type='list', elements='str',
choices=['password', 'radius', 'otp', 'pkinit', 'hardened']))
module = AnsibleModule(argument_spec=argument_spec,

View File

@@ -0,0 +1 @@
./identity/ipa/ipa_otpconfig.py

View File

@@ -0,0 +1 @@
./identity/ipa/ipa_otptoken.py

View File

@@ -0,0 +1,528 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2021, Tyler Gates <tgates81@gmail.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: spectrum_model_attrs
short_description: Enforce a model's attributes in CA Spectrum.
description:
- This module can be used to enforce a model's attributes in CA Spectrum.
version_added: 2.5.0
author:
- Tyler Gates (@tgates81)
notes:
- Tested on CA Spectrum version 10.4.2.0.189.
- Model creation and deletion are not possible with this module. For that use M(community.general.spectrum_device) instead.
requirements:
- 'python >= 2.7'
options:
url:
description:
- URL of OneClick server.
type: str
required: true
url_username:
description:
- OneClick username.
type: str
required: true
aliases: [username]
url_password:
description:
- OneClick password.
type: str
required: true
aliases: [password]
use_proxy:
description:
- if C(no), it will not use a proxy, even if one is defined in
an environment variable on the target hosts.
default: yes
required: false
type: bool
name:
description:
- Model name.
type: str
required: true
type:
description:
- Model type.
type: str
required: true
validate_certs:
description:
- Validate SSL certificates. Only change this to C(false) if you can guarantee that you are talking to the correct endpoint and there is no
man-in-the-middle attack happening.
type: bool
default: yes
required: false
attributes:
description:
- A list of attribute names and values to enforce.
- All values and parameters are case sensitive and must be provided as strings only.
required: true
type: list
elements: dict
suboptions:
name:
description:
- Attribute name OR hex ID.
- 'Currently defined names are:'
- ' C(App_Manufacturer) (C(0x230683))'
- ' C(CollectionsModelNameString) (C(0x12adb))'
- ' C(Condition) (C(0x1000a))'
- ' C(Criticality) (C(0x1290c))'
- ' C(DeviceType) (C(0x23000e))'
- ' C(isManaged) (C(0x1295d))'
- ' C(Model_Class) (C(0x11ee8))'
- ' C(Model_Handle) (C(0x129fa))'
- ' C(Model_Name) (C(0x1006e))'
- ' C(Modeltype_Handle) (C(0x10001))'
- ' C(Modeltype_Name) (C(0x10000))'
- ' C(Network_Address) (C(0x12d7f))'
- ' C(Notes) (C(0x11564))'
- ' C(ServiceDesk_Asset_ID) (C(0x12db9))'
- ' C(TopologyModelNameString) (C(0x129e7))'
- ' C(sysDescr) (C(0x10052))'
- ' C(sysName) (C(0x10b5b))'
- ' C(Vendor_Name) (C(0x11570))'
- ' C(Description) (C(0x230017))'
- Hex IDs are the direct identifiers in Spectrum and will always work.
- 'To lookup hex IDs go to the UI: Locator -> Devices -> By Model Name -> <enter any model> -> Attributes tab.'
type: str
required: true
value:
description:
- Attribute value. Empty strings should be C("") or C(null).
type: str
required: true
'''
EXAMPLES = r'''
- name: Enforce maintenance mode for modelxyz01 with a note about why
community.general.spectrum_model_attrs:
url: "http://oneclick.url.com"
username: "{{ oneclick_username }}"
password: "{{ oneclick_password }}"
name: "modelxyz01"
type: "Host_Device"
validate_certs: true
attributes:
- name: "isManaged"
value: "false"
- name: "Notes"
value: "MM set on {{ ansible_date_time.iso8601 }} via CO {{ CO }} by {{ tower_user_name | default(ansible_user_id) }}"
delegate_to: localhost
register: spectrum_model_attrs_status
'''
RETURN = r'''
msg:
description: Informational message on the job result.
type: str
returned: always
sample: 'Success'
changed_attrs:
description: Dictionary of changed name or hex IDs (whichever was specified) to their new corresponding values.
type: dict
returned: always
sample: {
"Notes": "MM set on 2021-02-03T22:04:02Z via CO CO9999 by tgates",
"isManaged": "true"
}
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six.moves.urllib.parse import quote
import json
import re
import xml.etree.ElementTree as ET
class spectrum_model_attrs:
def __init__(self, module):
self.module = module
self.url = module.params['url']
# If the user did not define a full path to the restul space in url:
# params, add what we believe it to be.
if not re.search('\\/.+', self.url.split('://')[1]):
self.url = "%s/spectrum/restful" % self.url.rstrip('/')
# Align these with what is defined in OneClick's UI under:
# Locator -> Devices -> By Model Name -> <enter any model> ->
# Attributes tab.
self.attr_map = dict(App_Manufacturer=hex(0x230683),
CollectionsModelNameString=hex(0x12adb),
Condition=hex(0x1000a),
Criticality=hex(0x1290c),
DeviceType=hex(0x23000e),
isManaged=hex(0x1295d),
Model_Class=hex(0x11ee8),
Model_Handle=hex(0x129fa),
Model_Name=hex(0x1006e),
Modeltype_Handle=hex(0x10001),
Modeltype_Name=hex(0x10000),
Network_Address=hex(0x12d7f),
Notes=hex(0x11564),
ServiceDesk_Asset_ID=hex(0x12db9),
TopologyModelNameString=hex(0x129e7),
sysDescr=hex(0x10052),
sysName=hex(0x10b5b),
Vendor_Name=hex(0x11570),
Description=hex(0x230017))
self.search_qualifiers = [
"and", "or", "not", "greater-than", "greater-than-or-equals",
"less-than", "less-than-or-equals", "equals", "equals-ignore-case",
"does-not-equal", "does-not-equal-ignore-case", "has-prefix",
"does-not-have-prefix", "has-prefix-ignore-case",
"does-not-have-prefix-ignore-case", "has-substring",
"does-not-have-substring", "has-substring-ignore-case",
"does-not-have-substring-ignore-case", "has-suffix",
"does-not-have-suffix", "has-suffix-ignore-case",
"does-not-have-suffix-ignore-case", "has-pcre",
"has-pcre-ignore-case", "has-wildcard", "has-wildcard-ignore-case",
"is-derived-from", "not-is-derived-from"]
self.resp_namespace = dict(ca="http://www.ca.com/spectrum/restful/schema/response")
self.result = dict(msg="", changed_attrs=dict())
self.success_msg = "Success"
def build_url(self, path):
"""
Build a sane Spectrum restful API URL
:param path: The path to append to the restful base
:type path: str
:returns: Complete restful API URL
:rtype: str
"""
return "%s/%s" % (self.url.rstrip('/'), path.lstrip('/'))
def attr_id(self, name):
"""
Get attribute hex ID
:param name: The name of the attribute to retrieve the hex ID for
:type name: str
:returns: Translated hex ID of name, or None if no translation found
:rtype: str or None
"""
try:
return self.attr_map[name]
except KeyError:
return None
def attr_name(self, _id):
"""
Get attribute name from hex ID
:param _id: The hex ID to lookup a name for
:type _id: str
:returns: Translated name of hex ID, or None if no translation found
:rtype: str or None
"""
for name, m_id in list(self.attr_map.items()):
if _id == m_id:
return name
return None
def urlencode(self, string):
"""
URL Encode a string
:param: string: The string to URL encode
:type string: str
:returns: URL encode version of supplied string
:rtype: str
"""
return quote(string, "<>%-_.!*'():?#/@&+,;=")
def update_model(self, model_handle, attrs):
"""
Update a model's attributes
:param model_handle: The model's handle ID
:type model_handle: str
:param attrs: Model's attributes to update. {'<name/id>': '<attr>'}
:type attrs: dict
:returns: Nothing; exits on error or updates self.results
:rtype: None
"""
# Build the update URL
update_url = self.build_url("/model/%s?" % model_handle)
for name, val in list(attrs.items()):
if val is None:
# None values should be converted to empty strings
val = ""
val = self.urlencode(str(val))
if not update_url.endswith('?'):
update_url += "&"
update_url += "attr=%s&val=%s" % (self.attr_id(name) or name, val)
# POST to /model to update the attributes, or fail.
resp, info = fetch_url(self.module, update_url, method="PUT",
headers={"Content-Type": "application/json",
"Accept": "application/json"},
use_proxy=self.module.params['use_proxy'])
status_code = info["status"]
if status_code >= 400:
body = info['body']
else:
body = "" if resp is None else resp.read()
if status_code != 200:
self.result['msg'] = "HTTP PUT error %s: %s: %s" % (status_code, update_url, body)
self.module.fail_json(**self.result)
# Load and parse the JSON response and either fail or set results.
json_resp = json.loads(body)
"""
Example success response:
{'model-update-response-list':{'model-responses':{'model':{'@error':'Success','@mh':'0x1010e76','attribute':{'@error':'Success','@id':'0x1295d'}}}}}"
Example failure response:
{'model-update-response-list': {'model-responses': {'model': {'@error': 'PartialFailure', '@mh': '0x1010e76', 'attribute': {'@error-message': 'brn0vlappua001: You do not have permission to set attribute Network_Address for this model.', '@error': 'Error', '@id': '0x12d7f'}}}}}
""" # noqa
model_resp = json_resp['model-update-response-list']['model-responses']['model']
if model_resp['@error'] != "Success":
# I'm not 100% confident on the expected failure structure so just
# dump all of ['attribute'].
self.result['msg'] = str(model_resp['attribute'])
self.module.fail_json(**self.result)
# Should be OK if we get to here, set results.
self.result['msg'] = self.success_msg
self.result['changed_attrs'].update(attrs)
self.result['changed'] = True
def find_model(self, search_criteria, ret_attrs=None):
"""
Search for a model in /models
:param search_criteria: The XML <rs:search-criteria>
:type search_criteria: str
:param ret_attrs: List of attributes by name or ID to return back
(default is Model_Handle)
:type ret_attrs: list
returns: Dictionary mapping of ret_attrs to values: {ret_attr: ret_val}
rtype: dict
"""
# If no return attributes were asked for, return Model_Handle.
if ret_attrs is None:
ret_attrs = ['Model_Handle']
# Set the XML <rs:requested-attribute id=<id>> tags. If no hex ID
# is found for the name, assume it is already in hex. {name: hex ID}
rqstd_attrs = ""
for ra in ret_attrs:
_id = self.attr_id(ra) or ra
rqstd_attrs += '<rs:requested-attribute id="%s" />' % (self.attr_id(ra) or ra)
# Build the complete XML search query for HTTP POST.
xml = """<?xml version="1.0" encoding="UTF-8"?>
<rs:model-request throttlesize="5"
xmlns:rs="http://www.ca.com/spectrum/restful/schema/request"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.ca.com/spectrum/restful/schema/request ../../../xsd/Request.xsd">
<rs:target-models>
<rs:models-search>
<rs:search-criteria xmlns="http://www.ca.com/spectrum/restful/schema/filter">
{0}
</rs:search-criteria>
</rs:models-search>
</rs:target-models>
{1}
</rs:model-request>
""".format(search_criteria, rqstd_attrs)
# POST to /models and fail on errors.
url = self.build_url("/models")
resp, info = fetch_url(self.module, url, data=xml, method="POST",
use_proxy=self.module.params['use_proxy'],
headers={"Content-Type": "application/xml",
"Accept": "application/xml"})
status_code = info["status"]
if status_code >= 400:
body = info['body']
else:
body = "" if resp is None else resp.read()
if status_code != 200:
self.result['msg'] = "HTTP POST error %s: %s: %s" % (status_code, url, body)
self.module.fail_json(**self.result)
# Parse through the XML response and fail on any detected errors.
root = ET.fromstring(body)
total_models = int(root.attrib['total-models'])
error = root.attrib['error']
model_responses = root.find('ca:model-responses', self.resp_namespace)
if total_models < 1:
self.result['msg'] = "No models found matching search criteria `%s'" % search_criteria
self.module.fail_json(**self.result)
elif total_models > 1:
self.result['msg'] = "More than one model found (%s): `%s'" % (total_models, ET.tostring(model_responses,
encoding='unicode'))
self.module.fail_json(**self.result)
if error != "EndOfResults":
self.result['msg'] = "Unexpected search response `%s': %s" % (error, ET.tostring(model_responses,
encoding='unicode'))
self.module.fail_json(**self.result)
model = model_responses.find('ca:model', self.resp_namespace)
attrs = model.findall('ca:attribute', self.resp_namespace)
if not attrs:
self.result['msg'] = "No attributes returned."
self.module.fail_json(**self.result)
# XML response should be successful. Iterate and set each returned
# attribute ID/name and value for return.
ret = dict()
for attr in attrs:
attr_id = attr.get('id')
attr_name = self.attr_name(attr_id)
# Note: all values except empty strings (None) are strings only!
attr_val = attr.text
key = attr_name if attr_name in ret_attrs else attr_id
ret[key] = attr_val
ret_attrs.remove(key)
return ret
def find_model_by_name_type(self, mname, mtype, ret_attrs=None):
"""
Find a model by name and type
:param mname: Model name
:type mname: str
:param mtype: Model type
:type mtype: str
:param ret_attrs: List of attributes by name or ID to return back
(default is Model_Handle)
:type ret_attrs: list
returns: find_model(): Dictionary mapping of ret_attrs to values:
{ret_attr: ret_val}
rtype: dict
"""
# If no return attributes were asked for, return Model_Handle.
if ret_attrs is None:
ret_attrs = ['Model_Handle']
"""This is basically as follows:
<filtered-models>
<and>
<equals>
<attribute id=...>
<value>...</value>
</attribute>
</equals>
<equals>
<attribute...>
</equals>
</and>
</filtered-models>
"""
# Parent filter tag
filtered_models = ET.Element('filtered-models')
# Logically and
_and = ET.SubElement(filtered_models, 'and')
# Model Name
MN_equals = ET.SubElement(_and, 'equals')
Model_Name = ET.SubElement(MN_equals, 'attribute',
{'id': self.attr_map['Model_Name']})
MN_value = ET.SubElement(Model_Name, 'value')
MN_value.text = mname
# Model Type Name
MTN_equals = ET.SubElement(_and, 'equals')
Modeltype_Name = ET.SubElement(MTN_equals, 'attribute',
{'id': self.attr_map['Modeltype_Name']})
MTN_value = ET.SubElement(Modeltype_Name, 'value')
MTN_value.text = mtype
return self.find_model(ET.tostring(filtered_models,
encoding='unicode'),
ret_attrs)
def ensure_model_attrs(self):
# Get a list of all requested attribute names/IDs plus Model_Handle and
# use them to query the values currently set. Store finding in a
# dictionary.
req_attrs = []
for attr in self.module.params['attributes']:
req_attrs.append(attr['name'])
if 'Model_Handle' not in req_attrs:
req_attrs.append('Model_Handle')
# Survey attributes currently set and store in a dict.
cur_attrs = self.find_model_by_name_type(self.module.params['name'],
self.module.params['type'],
req_attrs)
# Iterate through the requested attributes names/IDs values pair and
# compare with those currently set. If different, attempt to change.
Model_Handle = cur_attrs.pop("Model_Handle")
for attr in self.module.params['attributes']:
req_name = attr['name']
req_val = attr['value']
if req_val == "":
# The API will return None on empty string
req_val = None
if cur_attrs[req_name] != req_val:
if self.module.check_mode:
self.result['changed_attrs'][req_name] = req_val
self.result['msg'] = self.success_msg
self.result['changed'] = True
continue
resp = self.update_model(Model_Handle, {req_name: req_val})
self.module.exit_json(**self.result)
def run_module():
argument_spec = dict(
url=dict(type='str', required=True),
url_username=dict(type='str', required=True, aliases=['username']),
url_password=dict(type='str', required=True, aliases=['password'],
no_log=True),
validate_certs=dict(type='bool', default=True),
use_proxy=dict(type='bool', default=True),
name=dict(type='str', required=True),
type=dict(type='str', required=True),
attributes=dict(type='list',
required=True,
elements='dict',
options=dict(
name=dict(type='str', required=True),
value=dict(type='str', required=True)
)),
)
module = AnsibleModule(
supports_check_mode=True,
argument_spec=argument_spec,
)
try:
sm = spectrum_model_attrs(module)
sm.ensure_model_attrs()
except Exception as e:
module.fail_json(msg="Failed to ensure attribute(s) on `%s' with "
"exception: %s" % (module.params['name'],
to_native(e)))
def main():
run_module()
if __name__ == "__main__":
main()

View File

@@ -367,10 +367,9 @@ class HAProxy(object):
# We can assume there will only be 1 element in state because both svname and pxname are always set when we get here
# When using track we get a status like this: MAINT (via pxname/svname) so we need to do substring matching
if status in state[0]['status']:
if not self._drain or (state[0]['scur'] == '0' and 'MAINT' in state):
if not self._drain or state[0]['scur'] == '0':
return True
else:
time.sleep(self.wait_interval)
time.sleep(self.wait_interval)
self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." %
(pxname, svname, status, self.wait_retries))
@@ -409,15 +408,17 @@ class HAProxy(object):
def drain(self, host, backend, status='DRAIN'):
"""
Drain action, sets the server to DRAIN mode.
In this mode mode, the server will not accept any new connections
In this mode, the server will not accept any new connections
other than those that are accepted via persistence.
"""
haproxy_version = self.discover_version()
# check if haproxy version suppots DRAIN state (starting with 1.5)
# check if haproxy version supports DRAIN state (starting with 1.5)
if haproxy_version and (1, 5) <= haproxy_version:
cmd = "set server $pxname/$svname state drain"
self.execute_for_backends(cmd, backend, host, status)
self.execute_for_backends(cmd, backend, host, "DRAIN")
if status == "MAINT":
self.disabled(host, backend, self.shutdown_sessions)
def act(self):
"""
@@ -426,7 +427,7 @@ class HAProxy(object):
# Get the state before the run
self.command_results['state_before'] = self.get_state_for(self.backend, self.host)
# toggle enable/disbale server
# toggle enable/disable server
if self.state == 'enabled':
self.enabled(self.host, self.backend, self.weight)
elif self.state == 'disabled' and self._drain:

View File

@@ -205,9 +205,11 @@ class ResourceRecord(object):
def list_record(self, record):
# check if the record exists via list on ipwcli
search = 'list %s' % (record.replace(';', '&&').replace('set', 'where'))
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
cmd = [
self.module.get_bin_path('ipwcli', True),
'-user=%s' % self.user,
'-password=%s' % self.password,
]
rc, out, err = self.module.run_command(cmd, data=search)
if 'Invalid username or password' in out:
@@ -222,9 +224,11 @@ class ResourceRecord(object):
def deploy_record(self, record):
# check what happens if create fails on ipworks
stdin = 'create %s' % (record)
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
cmd = [
self.module.get_bin_path('ipwcli', True),
'-user=%s' % self.user,
'-password=%s' % self.password,
]
rc, out, err = self.module.run_command(cmd, data=stdin)
if 'Invalid username or password' in out:
@@ -238,9 +242,11 @@ class ResourceRecord(object):
def delete_record(self, record):
# check what happens if create fails on ipworks
stdin = 'delete %s' % (record.replace(';', '&&').replace('set', 'where'))
cmd = [self.module.get_bin_path('ipwcli', True)]
cmd.append('-user=%s' % (self.user))
cmd.append('-password=%s' % (self.password))
cmd = [
self.module.get_bin_path('ipwcli', True),
'-user=%s' % self.user,
'-password=%s' % self.password,
]
rc, out, err = self.module.run_command(cmd, data=stdin)
if 'Invalid username or password' in out:

View File

@@ -0,0 +1,199 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Florian Dambrine <android.florian@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
---
module: pritunl_org
author: Florian Dambrine (@Lowess)
version_added: 2.5.0
short_description: Manages Pritunl Organizations using the Pritunl API
description:
- A module to manage Pritunl organizations using the Pritunl API.
extends_documentation_fragment:
- community.general.pritunl
options:
name:
type: str
required: true
aliases:
- org
description:
- The name of the organization to manage in Pritunl.
force:
type: bool
default: false
description:
- If I(force) is C(true) and I(state) is C(absent), the module
will delete the organization, no matter if it contains users
or not. By default I(force) is C(false), which will cause the
module to fail the deletion of the organization when it contains
users.
state:
type: str
default: 'present'
choices:
- present
- absent
description:
- If C(present), the module adds organization I(name) to
Pritunl. If C(absent), attempt to delete the organization
from Pritunl (please read about I(force) usage).
"""
EXAMPLES = """
- name: Ensure the organization named MyOrg exists
community.general.pritunl_org:
state: present
name: MyOrg
- name: Ensure the organization named MyOrg does not exist
community.general.pritunl_org:
state: absent
name: MyOrg
"""
RETURN = """
response:
description: JSON representation of a Pritunl Organization.
returned: success
type: dict
sample:
{
"auth_api": False,
"name": "Foo",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "csftwlu6uhralzi2dpmhekz3",
}
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api import (
PritunlException,
delete_pritunl_organization,
post_pritunl_organization,
list_pritunl_organizations,
get_pritunl_settings,
pritunl_argument_spec,
)
def add_pritunl_organization(module):
result = {}
org_name = module.params.get("name")
org_obj_list = list_pritunl_organizations(
**dict_merge(
get_pritunl_settings(module),
{"filters": {"name": org_name}},
)
)
# If the organization already exists
if len(org_obj_list) > 0:
result["changed"] = False
result["response"] = org_obj_list[0]
else:
# Otherwise create it
response = post_pritunl_organization(
**dict_merge(
get_pritunl_settings(module),
{"organization_name": org_name},
)
)
result["changed"] = True
result["response"] = response
module.exit_json(**result)
def remove_pritunl_organization(module):
result = {}
org_name = module.params.get("name")
force = module.params.get("force")
org_obj_list = []
org_obj_list = list_pritunl_organizations(
**dict_merge(
get_pritunl_settings(module),
{
"filters": {"name": org_name},
},
)
)
# No organization found
if len(org_obj_list) == 0:
result["changed"] = False
result["response"] = {}
else:
# Otherwise attempt to delete it
org = org_obj_list[0]
# Only accept deletion under specific conditions
if force or org["user_count"] == 0:
response = delete_pritunl_organization(
**dict_merge(
get_pritunl_settings(module),
{"organization_id": org["id"]},
)
)
result["changed"] = True
result["response"] = response
else:
module.fail_json(
msg=(
"Can not remove organization '%s' with %d attached users. "
"Either set 'force' option to true or remove active users "
"from the organization"
)
% (org_name, org["user_count"])
)
module.exit_json(**result)
def main():
argument_spec = pritunl_argument_spec()
argument_spec.update(
dict(
name=dict(required=True, type="str", aliases=["org"]),
force=dict(required=False, type="bool", default=False),
state=dict(
required=False, choices=["present", "absent"], default="present"
),
)
),
module = AnsibleModule(argument_spec=argument_spec)
state = module.params.get("state")
try:
if state == "present":
add_pritunl_organization(module)
elif state == "absent":
remove_pritunl_organization(module)
except PritunlException as e:
module.fail_json(msg=to_native(e))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,129 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Florian Dambrine <android.florian@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
---
module: pritunl_org_info
author: Florian Dambrine (@Lowess)
version_added: 2.5.0
short_description: List Pritunl Organizations using the Pritunl API
description:
- A module to list Pritunl organizations using the Pritunl API.
extends_documentation_fragment:
- community.general.pritunl
options:
organization:
type: str
required: false
aliases:
- org
default: null
description:
- Name of the Pritunl organization to search for.
If none provided, the module will return all Pritunl
organizations.
"""
EXAMPLES = """
- name: List all existing Pritunl organizations
community.general.pritunl_org_info:
- name: Search for an organization named MyOrg
community.general.pritunl_user_info:
organization: MyOrg
"""
RETURN = """
organizations:
description: List of Pritunl organizations.
returned: success
type: list
elements: dict
sample:
[
{
"auth_api": False,
"name": "FooOrg",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "csftwlu6uhralzi2dpmhekz3",
},
{
"auth_api": False,
"name": "MyOrg",
"auth_token": None,
"user_count": 3,
"auth_secret": None,
"id": "58070daee63f3b2e6e472c36",
},
{
"auth_api": False,
"name": "BarOrg",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "v1sncsxxybnsylc8gpqg85pg",
}
]
"""
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api import (
PritunlException,
get_pritunl_settings,
list_pritunl_organizations,
pritunl_argument_spec,
)
def get_pritunl_organizations(module):
org_name = module.params.get("organization")
organizations = []
organizations = list_pritunl_organizations(
**dict_merge(
get_pritunl_settings(module),
{"filters": {"name": org_name} if org_name else None},
)
)
if org_name and len(organizations) == 0:
# When an org_name is provided but no organization match return an error
module.fail_json(msg="Organization '%s' does not exist" % org_name)
result = {}
result["changed"] = False
result["organizations"] = organizations
module.exit_json(**result)
def main():
argument_spec = pritunl_argument_spec()
argument_spec.update(
dict(
organization=dict(required=False, type="str", default=None, aliases=["org"])
)
),
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
try:
get_pritunl_organizations(module)
except PritunlException as e:
module.fail_json(msg=to_native(e))
if __name__ == "__main__":
main()

View File

@@ -82,6 +82,12 @@ options:
type: bool
default: no
version_added: 2.0.0
no_bin_links:
description:
- Use the C(--no-bin-links) flag when installing.
type: bool
default: no
version_added: 2.5.0
requirements:
- npm installed in bin path (recommended /usr/local/bin)
'''
@@ -151,6 +157,7 @@ class Npm(object):
self.unsafe_perm = kwargs['unsafe_perm']
self.state = kwargs['state']
self.no_optional = kwargs['no_optional']
self.no_bin_links = kwargs['no_bin_links']
if kwargs['executable']:
self.executable = kwargs['executable'].split(' ')
@@ -181,6 +188,8 @@ class Npm(object):
cmd.append(self.registry)
if self.no_optional:
cmd.append('--no-optional')
if self.no_bin_links:
cmd.append('--no-bin-links')
# If path is specified, cd into that path and run the command.
cwd = None
@@ -259,6 +268,7 @@ def main():
unsafe_perm=dict(default=False, type='bool'),
ci=dict(default=False, type='bool'),
no_optional=dict(default=False, type='bool'),
no_bin_links=dict(default=False, type='bool'),
)
arg_spec['global'] = dict(default=False, type='bool')
module = AnsibleModule(
@@ -278,6 +288,7 @@ def main():
unsafe_perm = module.params['unsafe_perm']
ci = module.params['ci']
no_optional = module.params['no_optional']
no_bin_links = module.params['no_bin_links']
if not path and not glbl:
module.fail_json(msg='path must be specified when not using global')
@@ -286,7 +297,7 @@ def main():
npm = Npm(module, name=name, path=path, version=version, glbl=glbl, production=production,
executable=executable, registry=registry, ignore_scripts=ignore_scripts,
unsafe_perm=unsafe_perm, state=state, no_optional=no_optional)
unsafe_perm=unsafe_perm, state=state, no_optional=no_optional, no_bin_links=no_bin_links)
changed = False
if ci:

View File

@@ -130,7 +130,7 @@ def packages_not_latest(module, names, site, update_catalog):
cmd.append('-U')
cmd.append('-c')
if site is not None:
cmd.extend('-t', site)
cmd.extend(['-t', site])
if names != ['*']:
cmd.extend(names)
rc, out, err = run_command(module, cmd)
@@ -159,7 +159,7 @@ def package_install(module, state, pkgs, site, update_catalog, force):
if update_catalog:
cmd.append('-U')
if site is not None:
cmd.extend('-t', site)
cmd.extend(['-t', site])
if force:
cmd.append('-f')
cmd.extend(pkgs)
@@ -174,7 +174,7 @@ def package_upgrade(module, pkgs, site, update_catalog, force):
if update_catalog:
cmd.append('-U')
if site is not None:
cmd.extend('-t', site)
cmd.extend(['-t', site])
if force:
cmd.append('-f')
cmd += pkgs

View File

@@ -108,8 +108,7 @@ from ansible.module_utils.basic import AnsibleModule
def package_installed(module, name, category):
cmd = [module.get_bin_path('pkginfo', True)]
cmd.append('-q')
cmd = [module.get_bin_path('pkginfo', True), '-q']
if category:
cmd.append('-c')
cmd.append(name)

View File

@@ -0,0 +1 @@
./net_tools/pritunl/pritunl_org.py

View File

@@ -0,0 +1 @@
./net_tools/pritunl/pritunl_org_info.py

View File

@@ -149,8 +149,6 @@ def get_existing_pipeline_variable(module, bitbucket):
var['name'] = var.pop('key')
return var
return None
def create_pipeline_variable(module, bitbucket):
info, content = bitbucket.request(

View File

@@ -121,9 +121,9 @@ except Exception:
def authenticate(username=None, password=None, access_token=None):
if access_token:
return Github(base_url="https://api.github.com:443", login_or_token=access_token)
return Github(base_url="https://api.github.com", login_or_token=access_token)
else:
return Github(base_url="https://api.github.com:443", login_or_token=username, password=password)
return Github(base_url="https://api.github.com", login_or_token=username, password=password)
def create_repo(gh, name, organization=None, private=False, description='', check_mode=False):

View File

@@ -0,0 +1 @@
./monitoring/spectrum_model_attrs.py

View File

@@ -175,10 +175,7 @@ class ZFSFacts(object):
self.facts = []
def dataset_exists(self):
cmd = [self.module.get_bin_path('zfs')]
cmd.append('list')
cmd.append(self.name)
cmd = [self.module.get_bin_path('zfs'), 'list', self.name]
(rc, out, err) = self.module.run_command(cmd)
@@ -188,10 +185,7 @@ class ZFSFacts(object):
return False
def get_facts(self):
cmd = [self.module.get_bin_path('zfs')]
cmd.append('get')
cmd.append('-H')
cmd = [self.module.get_bin_path('zfs'), 'get', '-H']
if self.parsable:
cmd.append('-p')
if self.recurse:
@@ -202,10 +196,7 @@ class ZFSFacts(object):
if self.type:
cmd.append('-t')
cmd.append(self.type)
cmd.append('-o')
cmd.append('name,property,value')
cmd.append(self.properties)
cmd.append(self.name)
cmd.extend(['-o', 'name,property,value', self.properties, self.name])
(rc, out, err) = self.module.run_command(cmd)

View File

@@ -134,10 +134,7 @@ class ZPoolFacts(object):
self.facts = []
def pool_exists(self):
cmd = [self.module.get_bin_path('zpool')]
cmd.append('list')
cmd.append(self.name)
cmd = [self.module.get_bin_path('zpool'), 'list', self.name]
(rc, out, err) = self.module.run_command(cmd)
@@ -147,10 +144,7 @@ class ZPoolFacts(object):
return False
def get_facts(self):
cmd = [self.module.get_bin_path('zpool')]
cmd.append('get')
cmd.append('-H')
cmd = [self.module.get_bin_path('zpool'), 'get', '-H']
if self.parsable:
cmd.append('-p')
cmd.append('-o')

View File

@@ -154,9 +154,7 @@ class BE(object):
self.is_freebsd = os.uname()[0] == 'FreeBSD'
def _beadm_list(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('list')
cmd.append('-H')
cmd = [self.module.get_bin_path('beadm'), 'list', '-H']
if '@' in self.name:
cmd.append('-s')
return self.module.run_command(cmd)
@@ -218,42 +216,26 @@ class BE(object):
return False
def activate_be(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('activate')
cmd.append(self.name)
cmd = [self.module.get_bin_path('beadm'), 'activate', self.name]
return self.module.run_command(cmd)
def create_be(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('create')
cmd = [self.module.get_bin_path('beadm'), 'create']
if self.snapshot:
cmd.append('-e')
cmd.append(self.snapshot)
cmd.extend(['-e', self.snapshot])
if not self.is_freebsd:
if self.description:
cmd.append('-d')
cmd.append(self.description)
cmd.extend(['-d', self.description])
if self.options:
cmd.append('-o')
cmd.append(self.options)
cmd.extend(['-o', self.options])
cmd.append(self.name)
return self.module.run_command(cmd)
def destroy_be(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('destroy')
cmd.append('-F')
cmd.append(self.name)
cmd = [self.module.get_bin_path('beadm'), 'destroy', '-F', self.name]
return self.module.run_command(cmd)
def is_mounted(self):
@@ -276,10 +258,7 @@ class BE(object):
return False
def mount_be(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('mount')
cmd.append(self.name)
cmd = [self.module.get_bin_path('beadm'), 'mount', self.name]
if self.mountpoint:
cmd.append(self.mountpoint)
@@ -287,9 +266,7 @@ class BE(object):
return self.module.run_command(cmd)
def unmount_be(self):
cmd = [self.module.get_bin_path('beadm')]
cmd.append('unmount')
cmd = [self.module.get_bin_path('beadm'), 'unmount']
if self.force:
cmd.append('-f')
cmd.append(self.name)

View File

@@ -10,6 +10,7 @@ __metaclass__ = type
DOCUMENTATION = r'''
---
module: java_cert
short_description: Uses keytool to import/remove key from java keystore (cacerts)
description:
- This is a wrapper module around keytool, which can be used to import/remove
@@ -81,9 +82,12 @@ options:
state:
description:
- Defines action which can be either certificate import or removal.
- When state is present, the certificate will always idempotently be inserted
into the keystore, even if there already exists a cert alias that is different.
type: str
choices: [ absent, present ]
default: present
requirements: [openssl, keytool]
author:
- Adam Hamsik (@haad)
'''
@@ -166,41 +170,143 @@ cmd:
'''
import os
import tempfile
import random
import string
import re
# import module snippets
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.six.moves.urllib.parse import urlparse
from ansible.module_utils.six.moves.urllib.request import getproxies
def get_keystore_type(keystore_type):
def _get_keystore_type_keytool_parameters(keystore_type):
''' Check that custom keystore is presented in parameters '''
if keystore_type:
return " -storetype '%s'" % keystore_type
return ''
return ["-storetype", keystore_type]
return []
def check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
def _check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
''' Check if certificate with alias is present in keystore
located at keystore_path '''
test_cmd = ("%s -noprompt -list -keystore '%s' -storepass '%s' "
"-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
test_cmd = [
executable,
"-list",
"-keystore",
keystore_path,
"-alias",
alias,
"-rfc"
]
test_cmd += _get_keystore_type_keytool_parameters(keystore_type)
check_rc, dummy, dummy = module.run_command(test_cmd)
(check_rc, stdout, dummy) = module.run_command(test_cmd, data=keystore_pass, check_rc=False)
if check_rc == 0:
return True
return False
return (True, stdout)
return (False, '')
def import_cert_url(module, executable, url, port, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
''' Import certificate from URL into keystore located at keystore_path '''
def _get_certificate_from_url(module, executable, url, port, pem_certificate_output):
remote_cert_pem_chain = _download_cert_url(module, executable, url, port)
with open(pem_certificate_output, 'w') as f:
f.write(remote_cert_pem_chain)
https_proxy = os.getenv("https_proxy")
def _get_first_certificate_from_x509_file(module, pem_certificate_file, pem_certificate_output, openssl_bin):
""" Read a X509 certificate chain file and output the first certificate in the list """
extract_cmd = [
openssl_bin,
"x509",
"-in",
pem_certificate_file,
"-out",
pem_certificate_output
]
(extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
if extract_rc != 0:
# trying der encoded file
extract_cmd += ["-inform", "der"]
(extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
if extract_rc != 0:
# this time it's a real failure
module.fail_json(msg="Internal module failure, cannot extract certificate, error: %s" % extract_stderr,
rc=extract_rc, cmd=extract_cmd)
return extract_rc
def _get_digest_from_x509_file(module, pem_certificate_file, openssl_bin):
""" Read a X509 certificate file and output sha256 digest using openssl """
# cleanup file before to compare
(dummy, tmp_certificate) = tempfile.mkstemp()
module.add_cleanup_file(tmp_certificate)
_get_first_certificate_from_x509_file(module, pem_certificate_file, tmp_certificate, openssl_bin)
dgst_cmd = [
openssl_bin,
"dgst",
"-r",
"-sha256",
tmp_certificate
]
(dgst_rc, dgst_stdout, dgst_stderr) = module.run_command(dgst_cmd, check_rc=False)
if dgst_rc != 0:
module.fail_json(msg="Internal module failure, cannot compute digest for certificate, error: %s" % dgst_stderr,
rc=dgst_rc, cmd=dgst_cmd)
return dgst_stdout.split(' ')[0]
def _export_public_cert_from_pkcs12(module, executable, pkcs_file, alias, password, dest):
""" Runs keytools to extract the public cert from a PKCS12 archive and write it to a file. """
export_cmd = [
executable,
"-list",
"-keystore",
pkcs_file,
"-alias",
alias,
"-storetype",
"pkcs12",
"-rfc"
]
(export_rc, export_stdout, export_err) = module.run_command(export_cmd, data=password, check_rc=False)
if export_rc != 0:
module.fail_json(msg="Internal module failure, cannot extract public certificate from pkcs12, error: %s" % export_err,
rc=export_rc)
with open(dest, 'w') as f:
f.write(export_stdout)
def get_proxy_settings(scheme='https'):
""" Returns a tuple containing (proxy_host, proxy_port). (False, False) if no proxy is found """
proxy_url = getproxies().get(scheme, '')
if not proxy_url:
return (False, False)
else:
parsed_url = urlparse(proxy_url)
if parsed_url.scheme:
(proxy_host, proxy_port) = parsed_url.netloc.split(':')
else:
(proxy_host, proxy_port) = parsed_url.path.split(':')
return (proxy_host, proxy_port)
def build_proxy_options():
""" Returns list of valid proxy options for keytool """
(proxy_host, proxy_port) = get_proxy_settings()
no_proxy = os.getenv("no_proxy")
proxy_opts = ''
if https_proxy is not None:
(proxy_host, proxy_port) = https_proxy.split(':')
proxy_opts = "-J-Dhttps.proxyHost=%s -J-Dhttps.proxyPort=%s" % (proxy_host, proxy_port)
proxy_opts = []
if proxy_host:
proxy_opts.extend(["-J-Dhttps.proxyHost=%s" % proxy_host, "-J-Dhttps.proxyPort=%s" % proxy_port])
if no_proxy is not None:
# For Java's nonProxyHosts property, items are separated by '|',
@@ -210,46 +316,48 @@ def import_cert_url(module, executable, url, port, keystore_path, keystore_pass,
# The property name is http.nonProxyHosts, there is no
# separate setting for HTTPS.
proxy_opts += " -J-Dhttp.nonProxyHosts='%s'" % non_proxy_hosts
proxy_opts.extend(["-J-Dhttp.nonProxyHosts=%s" % non_proxy_hosts])
return proxy_opts
fetch_cmd = "%s -printcert -rfc -sslserver %s %s:%d" % (executable, proxy_opts, url, port)
import_cmd = ("%s -importcert -noprompt -keystore '%s' "
"-storepass '%s' -alias '%s' %s") % (executable, keystore_path,
keystore_pass, alias,
get_keystore_type(keystore_type))
if trust_cacert:
import_cmd = import_cmd + " -trustcacerts"
def _download_cert_url(module, executable, url, port):
""" Fetches the certificate from the remote URL using `keytool -printcert...`
The PEM formatted string is returned """
proxy_opts = build_proxy_options()
fetch_cmd = [executable, "-printcert", "-rfc", "-sslserver"] + proxy_opts + ["%s:%d" % (url, port)]
# Fetch SSL certificate from remote host.
dummy, fetch_out, dummy = module.run_command(fetch_cmd, check_rc=True)
(fetch_rc, fetch_out, fetch_err) = module.run_command(fetch_cmd, check_rc=False)
# Use remote certificate from remote host and import it to a java keystore
(import_rc, import_out, import_err) = module.run_command(import_cmd,
data=fetch_out,
check_rc=False)
diff = {'before': '\n', 'after': '%s\n' % alias}
if import_rc == 0:
module.exit_json(changed=True, msg=import_out,
rc=import_rc, cmd=import_cmd, stdout=import_out,
diff=diff)
else:
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd,
error=import_err)
if fetch_rc != 0:
module.fail_json(msg="Internal module failure, cannot download certificate, error: %s" % fetch_err,
rc=fetch_rc, cmd=fetch_cmd)
return fetch_out
def import_cert_path(module, executable, path, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
''' Import certificate from path into keystore located on
keystore_path as alias '''
import_cmd = ("%s -importcert -noprompt -keystore '%s' "
"-storepass '%s' -file '%s' -alias '%s' %s") % (executable, keystore_path,
keystore_pass, path, alias,
get_keystore_type(keystore_type))
import_cmd = [
executable,
"-importcert",
"-noprompt",
"-keystore",
keystore_path,
"-file",
path,
"-alias",
alias
]
import_cmd += _get_keystore_type_keytool_parameters(keystore_type)
if trust_cacert:
import_cmd = import_cmd + " -trustcacerts"
import_cmd.extend(["-trustcacerts"])
# Use local certificate from local path and import it to a java keystore
(import_rc, import_out, import_err) = module.run_command(import_cmd,
data="%s\n%s" % (keystore_pass, keystore_pass),
check_rc=False)
diff = {'before': '\n', 'after': '%s\n' % alias}
@@ -261,41 +369,29 @@ def import_cert_path(module, executable, path, keystore_path, keystore_pass, ali
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
def import_pkcs12_path(module, executable, path, keystore_path, keystore_pass, pkcs12_pass, pkcs12_alias, alias, keystore_type):
''' Import pkcs12 from path into keystore located on
keystore_path as alias '''
import_cmd = ("%s -importkeystore -noprompt -destkeystore '%s' -srcstoretype PKCS12 "
"-deststorepass '%s' -destkeypass '%s' -srckeystore '%s' -srcstorepass '%s' "
"-srcalias '%s' -destalias '%s' %s") % (executable, keystore_path, keystore_pass,
keystore_pass, path, pkcs12_pass, pkcs12_alias,
alias, get_keystore_type(keystore_type))
# Use local certificate from local path and import it to a java keystore
(import_rc, import_out, import_err) = module.run_command(import_cmd,
check_rc=False)
diff = {'before': '\n', 'after': '%s\n' % alias}
if import_rc == 0:
module.exit_json(changed=True, msg=import_out,
rc=import_rc, cmd=import_cmd, stdout=import_out,
error=import_err, diff=diff)
else:
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type):
def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type, exit_after=True):
''' Delete certificate identified with alias from keystore on keystore_path '''
del_cmd = ("%s -delete -keystore '%s' -storepass '%s' "
"-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
del_cmd = [
executable,
"-delete",
"-noprompt",
"-keystore",
keystore_path,
"-alias",
alias
]
del_cmd += _get_keystore_type_keytool_parameters(keystore_type)
# Delete SSL certificate from keystore
(del_rc, del_out, del_err) = module.run_command(del_cmd, check_rc=True)
(del_rc, del_out, del_err) = module.run_command(del_cmd, data=keystore_pass, check_rc=True)
diff = {'before': '%s\n' % alias, 'after': None}
if exit_after:
diff = {'before': '%s\n' % alias, 'after': None}
module.exit_json(changed=True, msg=del_out,
rc=del_rc, cmd=del_cmd, stdout=del_out,
error=del_err, diff=diff)
module.exit_json(changed=True, msg=del_out,
rc=del_rc, cmd=del_cmd, stdout=del_out,
error=del_err, diff=diff)
def test_keytool(module, executable):
@@ -333,7 +429,8 @@ def main():
module = AnsibleModule(
argument_spec=argument_spec,
required_one_of=[['cert_path', 'cert_url', 'pkcs12_path']],
required_if=[['state', 'present', ('cert_path', 'cert_url', 'pkcs12_path'), True],
['state', 'absent', ('cert_url', 'cert_alias'), True]],
required_together=[['keystore_path', 'keystore_pass']],
mutually_exclusive=[
['cert_url', 'cert_path', 'pkcs12_path']
@@ -359,6 +456,9 @@ def main():
executable = module.params.get('executable')
state = module.params.get('state')
# openssl dependency resolution
openssl_bin = module.get_bin_path('openssl', True)
if path and not cert_alias:
module.fail_json(changed=False,
msg="Using local path import from %s requires alias argument."
@@ -369,31 +469,62 @@ def main():
if not keystore_create:
test_keystore(module, keystore_path)
cert_present = check_cert_present(module, executable, keystore_path,
keystore_pass, cert_alias, keystore_type)
alias_exists, alias_exists_output = _check_cert_present(
module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
if state == 'absent' and cert_present:
(dummy, new_certificate) = tempfile.mkstemp()
(dummy, old_certificate) = tempfile.mkstemp()
module.add_cleanup_file(new_certificate)
module.add_cleanup_file(old_certificate)
if state == 'absent' and alias_exists:
if module.check_mode:
module.exit_json(changed=True)
# delete and exit
delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
elif state == 'present' and not cert_present:
if module.check_mode:
module.exit_json(changed=True)
# dump certificate to enroll in the keystore on disk and compute digest
if state == 'present':
# The alias exists in the keystore so we must now compare the SHA256 hash of the
# public certificate already in the keystore, and the certificate we are wanting to add
if alias_exists:
with open(old_certificate, "w") as f:
f.write(alias_exists_output)
keystore_cert_digest = _get_digest_from_x509_file(module, old_certificate, openssl_bin)
else:
keystore_cert_digest = ''
if pkcs12_path:
import_pkcs12_path(module, executable, pkcs12_path, keystore_path,
keystore_pass, pkcs12_pass, pkcs12_alias, cert_alias, keystore_type)
# Extracting certificate with openssl
_export_public_cert_from_pkcs12(module, executable, pkcs12_path, cert_alias, pkcs12_pass, new_certificate)
if path:
import_cert_path(module, executable, path, keystore_path,
elif path:
# Extracting the X509 digest is a bit easier. Keytool will print the PEM
# certificate to stdout so we don't need to do any transformations.
new_certificate = path
elif url:
# Getting the X509 digest from a URL is the same as from a path, we just have
# to download the cert first
_get_certificate_from_url(module, executable, url, port, new_certificate)
new_cert_digest = _get_digest_from_x509_file(module, new_certificate, openssl_bin)
if keystore_cert_digest != new_cert_digest:
if module.check_mode:
module.exit_json(changed=True)
if alias_exists:
# The certificate in the keystore does not match with the one we want to be present
# The existing certificate must first be deleted before we insert the correct one
delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type, exit_after=False)
import_cert_path(module, executable, new_certificate, keystore_path,
keystore_pass, cert_alias, keystore_type, trust_cacert)
if url:
import_cert_url(module, executable, url, port, keystore_path,
keystore_pass, cert_alias, keystore_type, trust_cacert)
module.exit_json(changed=False)

View File

@@ -114,13 +114,15 @@ cmd:
description: Executed command to get action done
returned: changed and failure
type: str
sample: "openssl x509 -noout -in /tmp/cert.crt -fingerprint -sha256"
sample: "/usr/bin/openssl x509 -noout -in /tmp/user/1000/tmp8jd_lh23 -fingerprint -sha256"
'''
from ansible.module_utils.basic import AnsibleModule
import os
import re
import tempfile
from ansible.module_utils.basic import AnsibleModule
def read_certificate_fingerprint(module, openssl_bin, certificate_path):
@@ -129,59 +131,70 @@ def read_certificate_fingerprint(module, openssl_bin, certificate_path):
if rc != 0:
return module.fail_json(msg=current_certificate_fingerprint_out,
err=current_certificate_fingerprint_err,
rc=rc,
cmd=current_certificate_fingerprint_cmd)
cmd=current_certificate_fingerprint_cmd,
rc=rc)
current_certificate_match = re.search(r"=([\w:]+)", current_certificate_fingerprint_out)
if not current_certificate_match:
return module.fail_json(
msg="Unable to find the current certificate fingerprint in %s" % current_certificate_fingerprint_out,
rc=rc,
cmd=current_certificate_fingerprint_err
)
return module.fail_json(msg="Unable to find the current certificate fingerprint in %s" % current_certificate_fingerprint_out,
cmd=current_certificate_fingerprint_cmd,
rc=rc)
return current_certificate_match.group(1)
def read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_path, keystore_password):
stored_certificate_fingerprint_cmd = [keytool_bin, "-list", "-alias", alias, "-keystore", keystore_path, "-storepass", keystore_password, "-v"]
(rc, stored_certificate_fingerprint_out, stored_certificate_fingerprint_err) = run_commands(module, stored_certificate_fingerprint_cmd)
stored_certificate_fingerprint_cmd = [keytool_bin, "-list", "-alias", alias, "-keystore", keystore_path, "-storepass:env", "STOREPASS", "-v"]
(rc, stored_certificate_fingerprint_out, stored_certificate_fingerprint_err) = run_commands(
module, stored_certificate_fingerprint_cmd, environ_update=dict(STOREPASS=keystore_password))
if rc != 0:
if "keytool error: java.lang.Exception: Alias <%s> does not exist" % alias not in stored_certificate_fingerprint_out:
return module.fail_json(msg=stored_certificate_fingerprint_out,
err=stored_certificate_fingerprint_err,
rc=rc,
cmd=stored_certificate_fingerprint_cmd)
else:
return None
else:
stored_certificate_match = re.search(r"SHA256: ([\w:]+)", stored_certificate_fingerprint_out)
if not stored_certificate_match:
return module.fail_json(
msg="Unable to find the stored certificate fingerprint in %s" % stored_certificate_fingerprint_out,
rc=rc,
cmd=stored_certificate_fingerprint_cmd
)
# First intention was to not fail, and overwrite the keystore instead,
# in case of alias mismatch; but an issue in error handling caused the
# module to fail anyway.
# See: https://github.com/ansible-collections/community.general/issues/1671
# And: https://github.com/ansible-collections/community.general/pull/2183
# if "keytool error: java.lang.Exception: Alias <%s> does not exist" % alias in stored_certificate_fingerprint_out:
# return "alias mismatch"
# if re.match(r'keytool error: java\.io\.IOException: [Kk]eystore( was tampered with, or)? password was incorrect',
# stored_certificate_fingerprint_out):
# return "password mismatch"
return module.fail_json(msg=stored_certificate_fingerprint_out,
err=stored_certificate_fingerprint_err,
cmd=stored_certificate_fingerprint_cmd,
rc=rc)
return stored_certificate_match.group(1)
stored_certificate_match = re.search(r"SHA256: ([\w:]+)", stored_certificate_fingerprint_out)
if not stored_certificate_match:
return module.fail_json(msg="Unable to find the stored certificate fingerprint in %s" % stored_certificate_fingerprint_out,
cmd=stored_certificate_fingerprint_cmd,
rc=rc)
return stored_certificate_match.group(1)
def run_commands(module, cmd, data=None, check_rc=True):
return module.run_command(cmd, check_rc=check_rc, data=data)
def run_commands(module, cmd, data=None, environ_update=None, check_rc=False):
return module.run_command(cmd, check_rc=check_rc, data=data, environ_update=environ_update)
def create_file(path, content):
with open(path, 'w') as f:
def create_path():
dummy, tmpfile = tempfile.mkstemp()
os.remove(tmpfile)
return tmpfile
def create_file(content):
tmpfd, tmpfile = tempfile.mkstemp()
with os.fdopen(tmpfd, 'w') as f:
f.write(content)
return path
return tmpfile
def create_tmp_certificate(module):
return create_file("/tmp/%s.crt" % module.params['name'], module.params['certificate'])
return create_file(module.params['certificate'])
def create_tmp_private_key(module):
return create_file("/tmp/%s.key" % module.params['name'], module.params['private_key'])
return create_file(module.params['private_key'])
def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass, alias):
@@ -196,59 +209,57 @@ def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass,
def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass):
if module.check_mode:
module.exit_json(changed=True)
else:
certificate_path = create_tmp_certificate(module)
private_key_path = create_tmp_private_key(module)
try:
if os.path.exists(keystore_path):
os.remove(keystore_path)
return module.exit_json(changed=True)
keystore_p12_path = "/tmp/keystore.p12"
if os.path.exists(keystore_p12_path):
os.remove(keystore_p12_path)
certificate_path = create_tmp_certificate(module)
private_key_path = create_tmp_private_key(module)
keystore_p12_path = create_path()
try:
if os.path.exists(keystore_path):
os.remove(keystore_path)
export_p12_cmd = [openssl_bin, "pkcs12", "-export", "-name", name, "-in", certificate_path,
"-inkey", private_key_path, "-out",
keystore_p12_path, "-passout", "stdin"]
export_p12_cmd = [openssl_bin, "pkcs12", "-export", "-name", name, "-in", certificate_path,
"-inkey", private_key_path, "-out", keystore_p12_path, "-passout", "stdin"]
# when keypass is provided, add -passin
cmd_stdin = ""
if keypass:
export_p12_cmd.append("-passin")
export_p12_cmd.append("stdin")
cmd_stdin = "%s\n" % keypass
# when keypass is provided, add -passin
cmd_stdin = ""
if keypass:
export_p12_cmd.append("-passin")
export_p12_cmd.append("stdin")
cmd_stdin = "%s\n" % keypass
cmd_stdin += "%s\n%s" % (password, password)
cmd_stdin += "%s\n%s" % (password, password)
(rc, export_p12_out, export_p12_err) = run_commands(module, export_p12_cmd, data=cmd_stdin)
if rc != 0:
return module.fail_json(msg=export_p12_out,
rc=rc,
cmd=export_p12_cmd)
(rc, export_p12_out, dummy) = run_commands(module, export_p12_cmd, data=cmd_stdin)
if rc != 0:
return module.fail_json(msg=export_p12_out,
cmd=export_p12_cmd,
rc=rc)
import_keystore_cmd = [keytool_bin, "-importkeystore",
"-destkeystore", keystore_path,
"-srckeystore", keystore_p12_path,
"-srcstoretype", "pkcs12",
"-alias", name,
"-deststorepass", password,
"-srcstorepass", password,
"-noprompt"]
(rc, import_keystore_out, import_keystore_err) = run_commands(module, import_keystore_cmd, data=None)
if rc == 0:
update_jks_perm(module, keystore_path)
return module.exit_json(changed=True,
msg=import_keystore_out,
rc=rc,
cmd=import_keystore_cmd,
stdout_lines=import_keystore_out)
else:
return module.fail_json(msg=import_keystore_out,
rc=rc,
cmd=import_keystore_cmd)
finally:
os.remove(certificate_path)
os.remove(private_key_path)
import_keystore_cmd = [keytool_bin, "-importkeystore",
"-destkeystore", keystore_path,
"-srckeystore", keystore_p12_path,
"-srcstoretype", "pkcs12",
"-alias", name,
"-deststorepass:env", "STOREPASS",
"-srcstorepass:env", "STOREPASS",
"-noprompt"]
(rc, import_keystore_out, dummy) = run_commands(module, import_keystore_cmd, data=None,
environ_update=dict(STOREPASS=password))
if rc != 0:
return module.fail_json(msg=import_keystore_out,
cmd=import_keystore_cmd,
rc=rc)
update_jks_perm(module, keystore_path)
return module.exit_json(changed=True,
msg=import_keystore_out,
cmd=import_keystore_cmd,
rc=rc)
finally:
os.remove(certificate_path)
os.remove(private_key_path)
os.remove(keystore_p12_path)
def update_jks_perm(module, keystore_path):
@@ -280,7 +291,7 @@ def process_jks(module):
else:
if not module.check_mode:
update_jks_perm(module, keystore_path)
return module.exit_json(changed=False)
module.exit_json(changed=False)
else:
create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass)
@@ -308,6 +319,7 @@ def main():
add_file_common_args=spec.add_file_common_args,
supports_check_mode=spec.supports_check_mode
)
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
process_jks(module)

View File

@@ -143,10 +143,7 @@ def values_fmt(values, value_types):
for value, value_type in zip(values, value_types):
if value_type == 'bool':
value = fix_bool(value)
result.append('--type')
result.append('{0}'.format(value_type))
result.append('--set')
result.append('{0}'.format(value))
result.extend(['--type', '{0}'.format(value_type), '--set', '{0}'.format(value)])
return result
@@ -155,6 +152,10 @@ class XFConfException(Exception):
class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
change_params = 'value',
diff_params = 'value',
output_params = ('property', 'channel', 'value')
facts_params = ('property', 'channel', 'value')
module = dict(
argument_spec=dict(
state=dict(default="present",
@@ -185,17 +186,15 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
)
def update_xfconf_output(self, **kwargs):
self.update_output(**kwargs)
if not self.module.params['disable_facts']:
self.update_facts(**kwargs)
self.update_vars(meta={"output": True, "fact": True}, **kwargs)
def __init_module__(self):
self.does_not = 'Property "{0}" does not exist on channel "{1}".'.format(self.module.params['property'],
self.module.params['channel'])
self.vars.previous_value = self._get()
self.update_xfconf_output(property=self.module.params['property'],
channel=self.module.params['channel'],
previous_value=None)
self.vars.set('previous_value', self._get(), fact=True)
self.vars.set('type', self.vars.value_type, fact=True)
self.vars.meta('value').set(initial_value=self.vars.previous_value)
if not self.module.params['disable_facts']:
self.facts_name = "xfconf"
self.module.deprecate(
@@ -220,33 +219,23 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
return result
@property
def changed(self):
if self.vars.previous_value is None:
return self.vars.value is not None
elif self.vars.value is None:
return self.vars.previous_value is not None
else:
return set(self.vars.previous_value) != set(self.vars.value)
def _get(self):
return self.run_command(params=('channel', 'property'))
def state_get(self):
self.vars.value = self.vars.previous_value
self.update_xfconf_output(value=self.vars.value)
self.vars.previous_value = None
def state_absent(self):
if not self.module.check_mode:
self.run_command(params=('channel', 'property', {'reset': True}))
self.vars.value = None
self.run_command(params=('channel', 'property', {'reset': True}))
self.update_xfconf_output(previous_value=self.vars.previous_value,
value=None)
def state_present(self):
# stringify all values - in the CLI they will all be happy strings anyway
# and by doing this here the rest of the code can be agnostic to it
self.vars.value = [str(v) for v in self.module.params['value']]
value_type = self.module.params['value_type']
self.vars.value = [str(v) for v in self.vars.value]
value_type = self.vars.value_type
values_len = len(self.vars.value)
types_len = len(value_type)
@@ -263,7 +252,7 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
# calculates if it is an array
self.vars.is_array = \
bool(self.module.params['force_array']) or \
bool(self.vars.force_array) or \
isinstance(self.vars.previous_value, list) or \
values_len > 1
@@ -277,11 +266,9 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
if not self.vars.is_array:
self.vars.value = self.vars.value[0]
value_type = value_type[0]
self.update_xfconf_output(previous_value=self.vars.previous_value,
value=self.vars.value,
type=value_type)
self.vars.type = value_type[0]
else:
self.vars.type = value_type
def main():

View File

@@ -198,6 +198,10 @@ members:
import re
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.urls import fetch_url
from ansible.module_utils.six import iteritems
BEAUTIFUL_SOUP_IMP_ERR = None
try:
from BeautifulSoup import BeautifulSoup
@@ -273,13 +277,8 @@ class BalancerMember(object):
'drained': 'Drn',
'hot_standby': 'Stby',
'ignore_errors': 'Ign'}
status = {}
actual_status = str(self.attributes['Status'])
for mode in status_mapping.keys():
if re.search(pattern=status_mapping[mode], string=actual_status):
status[mode] = True
else:
status[mode] = False
status = dict((mode, patt in actual_status) for mode, patt in iteritems(status_mapping))
return status
def set_member_status(self, values):
@@ -290,13 +289,10 @@ class BalancerMember(object):
'ignore_errors': '&w_status_I'}
request_body = regexp_extraction(self.management_url, EXPRESSION, 1)
for k in values_mapping.keys():
if values[str(k)]:
request_body = request_body + str(values_mapping[k]) + '=1'
else:
request_body = request_body + str(values_mapping[k]) + '=0'
values_url = "".join("{0}={1}".format(url_param, 1 if values[mode] else 0) for mode, url_param in iteritems(values_mapping))
request_body = "{0}{1}".format(request_body, values_url)
response = fetch_url(self.module, self.management_url, data=str(request_body))
response = fetch_url(self.module, self.management_url, data=request_body)
if response[1]['status'] != 200:
self.module.fail_json(msg="Could not set the member status! " + self.host + " " + response[1]['status'])
@@ -309,11 +305,11 @@ class Balancer(object):
def __init__(self, host, suffix, module, members=None, tls=False):
if tls:
self.base_url = str(str('https://') + str(host))
self.url = str(str('https://') + str(host) + str(suffix))
self.base_url = 'https://' + str(host)
self.url = 'https://' + str(host) + str(suffix)
else:
self.base_url = str(str('http://') + str(host))
self.url = str(str('http://') + str(host) + str(suffix))
self.base_url = 'http://' + str(host)
self.url = 'http://' + str(host) + str(suffix)
self.module = module
self.page = self.fetch_balancer_page()
if members is None:
@@ -444,7 +440,5 @@ def main():
module.fail_json(msg=str(module.params['member_host']) + ' is not a member of the balancer ' + str(module.params['balancer_vhost']) + '!')
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.urls import fetch_url
if __name__ == '__main__':
main()

View File

@@ -5,6 +5,7 @@
# Atlassian open-source approval reference OSR-76.
#
# (c) 2020, Per Abildgaard Toft <per@minfejl.dk> Search and update function
# (c) 2021, Brandon McNama <brandonmcnama@outlook.com> Issue attachment functionality
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@@ -29,7 +30,7 @@ options:
type: str
required: true
aliases: [ command ]
choices: [ comment, create, edit, fetch, link, search, transition, update ]
choices: [ attach, comment, create, edit, fetch, link, search, transition, update ]
description:
- The operation to perform.
@@ -56,12 +57,14 @@ options:
required: false
description:
- The issue summary, where appropriate.
- Note that JIRA may not allow changing field values on specific transitions or states.
description:
type: str
required: false
description:
- The issue description, where appropriate.
- Note that JIRA may not allow changing field values on specific transitions or states.
issuetype:
type: str
@@ -81,18 +84,28 @@ options:
required: false
description:
- The comment text to add.
- Note that JIRA may not allow changing field values on specific transitions or states.
status:
type: str
required: false
description:
- The desired status; only relevant for the transition operation.
- Only used when I(operation) is C(transition), and a bit of a misnomer, it actually refers to the transition name.
assignee:
type: str
required: false
description:
- Sets the assignee on create or transition operations. Note not all transitions will allow this.
- Sets the the assignee when I(operation) is C(create), C(transition) or C(edit).
- Recent versions of JIRA no longer accept a user name as a user identifier. In that case, use I(account_id) instead.
- Note that JIRA may not allow changing field values on specific transitions or states.
account_id:
type: str
description:
- Sets the account identifier for the assignee when I(operation) is C(create), C(transition) or C(edit).
- Note that JIRA may not allow changing field values on specific transitions or states.
version_added: 2.5.0
linktype:
type: str
@@ -119,6 +132,7 @@ options:
- This is a free-form data structure that can contain arbitrary data. This is passed directly to the JIRA REST API
(possibly after merging with other required data, as when passed to create). See examples for more information,
and the JIRA REST API for the structure required for various fields.
- Note that JIRA may not allow changing field values on specific transitions or states.
jql:
required: false
@@ -149,12 +163,37 @@ options:
default: true
type: bool
attachment:
type: dict
version_added: 2.5.0
description:
- Information about the attachment being uploaded.
suboptions:
filename:
required: true
type: path
description:
- The path to the file to upload (from the remote node) or, if I(content) is specified,
the filename to use for the attachment.
content:
type: str
description:
- The Base64 encoded contents of the file to attach. If not specified, the contents of I(filename) will be
used instead.
mimetype:
type: str
description:
- The MIME type to supply for the upload. If not specified, best-effort detection will be
done.
notes:
- "Currently this only works with basic-auth."
- "To use with JIRA Cloud, pass the login e-mail as the I(username) and the API token as I(password)."
author:
- "Steve Smith (@tarka)"
- "Per Abildgaard Toft (@pertoft)"
- "Brandon McNama (@DWSR)"
"""
EXAMPLES = r"""
@@ -172,7 +211,7 @@ EXAMPLES = r"""
args:
fields:
customfield_13225: "test"
customfield_12931: '{"value": "Test"}'
customfield_12931: {"value": "Test"}
register: issue
- name: Comment on issue
@@ -282,24 +321,40 @@ EXAMPLES = r"""
inwardissue: HSP-1
outwardissue: MKY-1
# Transition an issue by target status
- name: Close the issue
# Transition an issue
- name: Resolve the issue
community.general.jira:
uri: '{{ server }}'
username: '{{ user }}'
password: '{{ pass }}'
issue: '{{ issue.meta.key }}'
operation: transition
status: Done
args:
status: Resolve Issue
account_id: 112233445566778899aabbcc
fields:
customfield_14321: [ {'set': {'value': 'Value of Select' }} ]
comment: [ { 'add': { 'body' : 'Test' } }]
resolution:
name: Done
description: I am done! This is the last description I will ever give you.
# Attach a file to an issue
- name: Attach a file
community.general.jira:
uri: '{{ server }}'
username: '{{ user }}'
password: '{{ pass }}'
issue: HSP-1
operation: attach
attachment:
filename: topsecretreport.xlsx
"""
import base64
import binascii
import json
import mimetypes
import os
import random
import string
import sys
import traceback
@@ -311,8 +366,17 @@ from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.urls import fetch_url
def request(url, user, passwd, timeout, data=None, method=None):
if data:
def request(
url,
user,
passwd,
timeout,
data=None,
method=None,
content_type='application/json',
additional_headers=None
):
if data and content_type == 'application/json':
data = json.dumps(data)
# NOTE: fetch_url uses a password manager, which follows the
@@ -323,9 +387,18 @@ def request(url, user, passwd, timeout, data=None, method=None):
# inject the basic-auth header up-front to ensure that JIRA treats
# the requests as authorized for this user.
auth = to_text(base64.b64encode(to_bytes('{0}:{1}'.format(user, passwd), errors='surrogate_or_strict')))
response, info = fetch_url(module, url, data=data, method=method, timeout=timeout,
headers={'Content-Type': 'application/json',
'Authorization': "Basic %s" % auth})
headers = {}
if isinstance(additional_headers) == dict:
headers = additional_headers.copy()
headers.update({
"Content-Type": content_type,
"Authorization": "Basic %s" % auth,
})
response, info = fetch_url(
module, url, data=data, method=method, timeout=timeout, headers=headers
)
if info['status'] not in (200, 201, 204):
error = None
@@ -351,8 +424,8 @@ def request(url, user, passwd, timeout, data=None, method=None):
return {}
def post(url, user, passwd, timeout, data):
return request(url, user, passwd, timeout, data=data, method='POST')
def post(url, user, passwd, timeout, data, content_type='application/json', additional_headers=None):
return request(url, user, passwd, timeout, data=data, method='POST', content_type=content_type, additional_headers=additional_headers)
def put(url, user, passwd, timeout, data):
@@ -440,10 +513,22 @@ def transition(restbase, user, passwd, params):
if not tid:
raise ValueError("Failed find valid transition for '%s'" % target)
fields = dict(params['fields'])
if params['summary'] is not None:
fields.update({'summary': params['summary']})
if params['description'] is not None:
fields.update({'description': params['description']})
# Perform it
url = restbase + '/issue/' + params['issue'] + "/transitions"
data = {'transition': {"id": tid},
'update': params['fields']}
'fields': fields}
if params['comment'] is not None:
data.update({"update": {
"comment": [{
"add": {"body": params['comment']}
}],
}})
return True, post(url, user, passwd, params['timeout'], data)
@@ -460,13 +545,89 @@ def link(restbase, user, passwd, params):
return True, post(url, user, passwd, params['timeout'], data)
def attach(restbase, user, passwd, params):
filename = params['attachment'].get('filename')
content = params['attachment'].get('content')
if not any((filename, content)):
raise ValueError('at least one of filename or content must be provided')
mime = params['attachment'].get('mimetype')
if not os.path.isfile(filename):
raise ValueError('The provided filename does not exist: %s' % filename)
content_type, data = _prepare_attachment(filename, content, mime)
url = restbase + '/issue/' + params['issue'] + '/attachments'
return True, post(
url, user, passwd, params['timeout'], data, content_type=content_type,
additional_headers={"X-Atlassian-Token": "no-check"}
)
# Ideally we'd just use prepare_multipart from ansible.module_utils.urls, but
# unfortunately it does not support specifying the encoding and also defaults to
# base64. Jira doesn't support base64 encoded attachments (and is therefore not
# spec compliant. Go figure). I originally wrote this function as an almost
# exact copypasta of prepare_multipart, but ran into some encoding issues when
# using the noop encoder. Hand rolling the entire message body seemed to work
# out much better.
#
# https://community.atlassian.com/t5/Jira-questions/Jira-dosen-t-decode-base64-attachment-request-REST-API/qaq-p/916427
#
# content is expected to be a base64 encoded string since Ansible doesn't
# support passing raw bytes objects.
def _prepare_attachment(filename, content=None, mime_type=None):
def escape_quotes(s):
return s.replace('"', '\\"')
boundary = "".join(random.choice(string.digits + string.ascii_letters) for i in range(30))
name = to_native(os.path.basename(filename))
if not mime_type:
try:
mime_type = mimetypes.guess_type(filename or '', strict=False)[0] or 'application/octet-stream'
except Exception:
mime_type = 'application/octet-stream'
main_type, sep, sub_type = mime_type.partition('/')
if not content and filename:
with open(to_bytes(filename, errors='surrogate_or_strict'), 'rb') as f:
content = f.read()
else:
try:
content = base64.decode(content)
except binascii.Error as e:
raise Exception("Unable to base64 decode file content: %s" % e)
lines = [
"--{0}".format(boundary),
'Content-Disposition: form-data; name="file"; filename={0}'.format(escape_quotes(name)),
"Content-Type: {0}".format("{0}/{1}".format(main_type, sub_type)),
'',
to_text(content),
"--{0}--".format(boundary),
""
]
return (
"multipart/form-data; boundary={0}".format(boundary),
"\r\n".join(lines)
)
def main():
global module
module = AnsibleModule(
argument_spec=dict(
attachment=dict(type='dict', options=dict(
content=dict(type='str'),
filename=dict(type='path', required=True),
mimetype=dict(type='str')
)),
uri=dict(type='str', required=True),
operation=dict(type='str', choices=['create', 'comment', 'edit', 'update', 'fetch', 'transition', 'link', 'search'],
operation=dict(type='str', choices=['attach', 'create', 'comment', 'edit', 'update', 'fetch', 'transition', 'link', 'search'],
aliases=['command'], required=True),
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
@@ -486,8 +647,10 @@ def main():
maxresults=dict(type='int'),
timeout=dict(type='float', default=10),
validate_certs=dict(default=True, type='bool'),
account_id=dict(type='str'),
),
required_if=(
('operation', 'attach', ['issue', 'attachment']),
('operation', 'create', ['project', 'issuetype', 'summary']),
('operation', 'comment', ['issue', 'comment']),
('operation', 'fetch', ['issue']),
@@ -495,6 +658,7 @@ def main():
('operation', 'link', ['linktype', 'inwardissue', 'outwardissue']),
('operation', 'search', ['jql']),
),
mutually_exclusive=[('assignee', 'account_id')],
supports_check_mode=False
)
@@ -506,6 +670,8 @@ def main():
passwd = module.params['password']
if module.params['assignee']:
module.params['fields']['assignee'] = {'name': module.params['assignee']}
if module.params['account_id']:
module.params['fields']['assignee'] = {'accountId': module.params['account_id']}
if not uri.endswith('/'):
uri = uri + '/'

View File

@@ -13,6 +13,11 @@ matrix:
- env: T=devel/sanity/3
- env: T=devel/sanity/4
- env: T=2.11/sanity/1
- env: T=2.11/sanity/2
- env: T=2.11/sanity/3
- env: T=2.11/sanity/4
- env: T=2.10/sanity/1
- env: T=2.10/sanity/2
- env: T=2.10/sanity/3

View File

@@ -0,0 +1,2 @@
shippable/posix/group3
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller

View File

@@ -0,0 +1,7 @@
---
- name: "Test dict filter"
assert:
that:
- "[['a', 'b']] | community.general.dict == dict([['a', 'b']])"
- "[['a', 'b'], [1, 2]] | community.general.dict == dict([['a', 'b'], [1, 2]])"
- "[] | community.general.dict == dict([])"

View File

@@ -0,0 +1,2 @@
shippable/posix/group1
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller

View File

@@ -0,0 +1,7 @@
---
- name: "Test path_join filter"
assert:
that:
- "['a', 'b'] | community.general.path_join == 'a/b'"
- "['a', '/b'] | community.general.path_join == '/b'"
- "[''] | community.general.path_join == ''"

View File

@@ -1,3 +1,13 @@
---
test_pkcs12_path: testpkcs.p12
test_keystore_path: keystore.jks
test_keystore_path: keystore.jks
test_keystore2_path: "{{ output_dir }}/keystore2.jks"
test_keystore2_password: changeit
test_cert_path: "{{ output_dir }}/cert.pem"
test_key_path: "{{ output_dir }}/key.pem"
test_cert2_path: "{{ output_dir }}/cert2.pem"
test_key2_path: "{{ output_dir }}/key2.pem"
test_pkcs_path: "{{ output_dir }}/cert.p12"
test_pkcs2_path: "{{ output_dir }}/cert2.p12"
test_ssl: setupSSLServer.py
test_ssl_port: 21500

View File

@@ -0,0 +1,20 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import ssl
import os
import sys
root_dir = sys.argv[1]
port = int(sys.argv[2])
try:
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
except ModuleNotFoundError:
from http.server import HTTPServer, SimpleHTTPRequestHandler
httpd = HTTPServer(('localhost', port), SimpleHTTPRequestHandler)
httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True,
certfile=os.path.join(root_dir, 'cert.pem'),
keyfile=os.path.join(root_dir, 'key.pem'))
httpd.handle_request()

View File

@@ -1,2 +1,3 @@
dependencies:
- setup_java_keytool
- setup_openssl

View File

@@ -11,15 +11,16 @@
- name: import pkcs12
java_cert:
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
pkcs12_password: changeit
pkcs12_alias: default
cert_alias: default
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
pkcs12_password: changeit
pkcs12_alias: default
cert_alias: default
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
register: result_success
- name: verify success
assert:
that:
@@ -27,14 +28,14 @@
- name: import pkcs12 with wrong password
java_cert:
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
pkcs12_password: wrong_pass
pkcs12_alias: default
cert_alias: default_new
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
pkcs12_password: wrong_pass
pkcs12_alias: default
cert_alias: default_new
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
ignore_errors: true
register: result_wrong_pass
@@ -45,16 +46,62 @@
- name: test fail on mutually exclusive params
java_cert:
cert_path: ca.crt
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
cert_alias: default
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
cert_path: ca.crt
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
cert_alias: default
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
keystore_create: yes
state: present
ignore_errors: true
register: result_excl_params
- name: verify failed exclusive params
assert:
that:
- result_excl_params is failed
- name: test fail on missing required params
java_cert:
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
state: absent
ignore_errors: true
register: result_missing_required_param
- name: verify failed missing required params
assert:
that:
- result_missing_required_param is failed
- name: delete object based on cert_alias parameter
java_cert:
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
keystore_pass: changeme_keystore
cert_alias: default
state: absent
ignore_errors: true
register: result_alias_deleted
- name: verify object successfully deleted
assert:
that:
- result_alias_deleted is successful
- name: include extended test suite
import_tasks: state_change.yml
- name: cleanup environment
file:
path: "{{ item }}"
state: absent
loop:
- "{{ output_dir }}/{{ test_pkcs12_path }}"
- "{{ output_dir }}/{{ test_keystore_path }}"
- "{{ test_keystore2_path }}"
- "{{ test_cert_path }}"
- "{{ test_key_path }}"
- "{{ test_cert2_path }}"
- "{{ test_key2_path }}"
- "{{ test_pkcs_path }}"
- "{{ test_pkcs2_path }}"

View File

@@ -0,0 +1,169 @@
---
- name: Generate the self signed cert used as a place holder to create the java keystore
command: openssl req -x509 -newkey rsa:4096 -keyout {{ test_key_path }} -out {{ test_cert_path }} -days 365 -nodes -subj '/CN=localhost'
args:
creates: "{{ test_key_path }}"
- name: Create the test keystore
java_keystore:
name: placeholder
dest: "{{ test_keystore2_path }}"
password: "{{ test_keystore2_password }}"
private_key: "{{ lookup('file', '{{ test_key_path }}') }}"
certificate: "{{ lookup('file', '{{ test_cert_path }}') }}"
- name: Generate the self signed cert we will use for testing
command: openssl req -x509 -newkey rsa:4096 -keyout '{{ test_key2_path }}' -out '{{ test_cert2_path }}' -days 365 -nodes -subj '/CN=localhost'
args:
creates: "{{ test_key2_path }}"
- name: |
Import the newly created certificate. This is our main test.
If the java_cert has been updated properly, then this task will report changed each time
since the module will be comparing the hash of the certificate instead of validating that the alias
simply exists
java_cert:
cert_alias: test_cert
cert_path: "{{ test_cert2_path }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: present
register: result_x509_changed
- name: Verify the x509 status has changed
assert:
that:
- result_x509_changed is changed
- name: |
We also want to make sure that the status doesnt change if we import the same cert
java_cert:
cert_alias: test_cert
cert_path: "{{ test_cert2_path }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: present
register: result_x509_succeeded
- name: Verify the x509 status is ok
assert:
that:
- result_x509_succeeded is succeeded
- name: Create the pkcs12 archive from the test x509 cert
command: >
openssl pkcs12
-in {{ test_cert_path }}
-inkey {{ test_key_path }}
-export
-name test_pkcs12_cert
-out {{ test_pkcs_path }}
-passout pass:"{{ test_keystore2_password }}"
- name: Create the pkcs12 archive from the certificate we will be trying to add to the keystore
command: >
openssl pkcs12
-in {{ test_cert2_path }}
-inkey {{ test_key2_path }}
-export
-name test_pkcs12_cert
-out {{ test_pkcs2_path }}
-passout pass:"{{ test_keystore2_password }}"
- name: >
Ensure the original pkcs12 cert is in the keystore
java_cert:
cert_alias: test_pkcs12_cert
pkcs12_alias: test_pkcs12_cert
pkcs12_path: "{{ test_pkcs_path }}"
pkcs12_password: "{{ test_keystore2_password }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: present
- name: |
Perform the same test, but we will now be testing the pkcs12 functionality
If we add a different pkcs12 cert with the same alias, we should have a chnaged result, NOT the same
java_cert:
cert_alias: test_pkcs12_cert
pkcs12_alias: test_pkcs12_cert
pkcs12_path: "{{ test_pkcs2_path }}"
pkcs12_password: "{{ test_keystore2_password }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: present
register: result_pkcs12_changed
- name: Verify the pkcs12 status has changed
assert:
that:
- result_pkcs12_changed is changed
- name: |
We are requesting the same cert now, so the status should show OK
java_cert:
cert_alias: test_pkcs12_cert
pkcs12_alias: test_pkcs12_cert
pkcs12_path: "{{ test_pkcs2_path }}"
pkcs12_password: "{{ test_keystore2_password }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
register: result_pkcs12_succeeded
- name: Verify the pkcs12 status is ok
assert:
that:
- result_pkcs12_succeeded is succeeded
- name: Copy the ssl server script
copy:
src: "setupSSLServer.py"
dest: "{{ output_dir }}"
- name: Create an SSL server that we will use for testing URL imports
command: python {{ output_dir }}/setupSSLServer.py {{ output_dir }} {{ test_ssl_port }}
async: 10
poll: 0
- name: |
Download the original cert.pem from our temporary server. The current cert should contain
cert2.pem. Importing this cert should return a status of changed
java_cert:
cert_alias: test_cert_localhost
cert_url: localhost
cert_port: "{{ test_ssl_port }}"
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: present
register: result_url_changed
- name: Verify that the url status is changed
assert:
that:
- result_url_changed is changed
- name: Ensure we can remove the x509 cert
java_cert:
cert_alias: test_cert
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: absent
register: result_x509_absent
- name: Verify the x509 cert is absent
assert:
that:
- result_x509_absent is changed
- name: Ensure we can remove the pkcs12 archive
java_cert:
cert_alias: test_pkcs12_cert
keystore_path: "{{ test_keystore2_path }}"
keystore_pass: "{{ test_keystore2_password }}"
state: absent
register: result_pkcs12_absent
- name: Verify the pkcs12 archive is absent
assert:
that:
- result_pkcs12_absent is changed

View File

@@ -63,11 +63,11 @@
- name: Create a Java key store for the given certificates (check mode)
community.general.java_keystore: &create_key_store_data
name: example
certificate: "{{lookup('file', output_dir ~ '/' ~ item.name ~ '.pem') }}"
private_key: "{{lookup('file', output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key') }}"
certificate: "{{ lookup('file', output_dir ~ '/' ~ item.name ~ '.pem') }}"
private_key: "{{ lookup('file', output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.key') }}"
private_key_passphrase: "{{ item.passphrase | default(omit) }}"
password: changeit
dest: "{{ output_dir ~ '/' ~ item.name ~ '.jks' }}"
dest: "{{ output_dir ~ '/' ~ (item.keyname | default(item.name)) ~ '.jks' }}"
loop: &create_key_store_loop
- name: cert
- name: cert-pw

View File

@@ -0,0 +1,2 @@
unsupported
shippable/posix/group3

View File

@@ -0,0 +1,58 @@
---
- community.general.jira:
uri: "{{ uri }}"
username: "{{ user }}"
password: "{{ pasw }}"
project: "{{ proj }}"
operation: create
summary: test ticket
description: bla bla bla
issuetype: Task
register: issue
- debug:
msg: Issue={{ issue }}
- name: Add comment bleep bleep
community.general.jira:
uri: "{{ uri }}"
username: "{{ user }}"
password: "{{ pasw }}"
issue: "{{ issue.meta.key }}"
operation: comment
comment: bleep bleep!
- name: Transition -> In Progress with comment
community.general.jira:
uri: "{{ uri }}"
username: "{{ user }}"
password: "{{ pasw }}"
issue: "{{ issue.meta.key }}"
operation: transition
status: Start Progress
comment: -> in progress
- name: Change assignee
community.general.jira:
uri: "{{ uri }}"
username: "{{ user }}"
password: "{{ pasw }}"
issue: "{{ issue.meta.key }}"
operation: edit
accountId: "{{ user2 }}"
- name: Transition -> Resolved with comment
community.general.jira:
uri: "{{ uri }}"
username: "{{ user }}"
password: "{{ pasw }}"
issue: "{{ issue.meta.key }}"
operation: transition
status: Resolve Issue
comment: -> resolved
accountId: "{{ user1 }}"
fields:
resolution:
name: Done
description: wakawakawakawaka
- debug:
msg:
- Issue = {{ issue.meta.key }}
- URL = {{ issue.meta.self }}

View File

@@ -0,0 +1,7 @@
---
uri: https://xxxx.atlassian.net/
user: xxx@xxxx.xxx
pasw: supersecret
proj: ABC
user1: 6574474636373822y7338
user2: 6574474636373822y73959696

View File

@@ -0,0 +1 @@
shippable/posix/group4

View File

@@ -0,0 +1,69 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2021, Alexei Znamensky <russoz@gmail.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: mdepfail
author: "Alexei Znamensky (@russoz)"
short_description: Simple module for testing
description:
- Simple module test description.
options:
a:
description: aaaa
type: int
b:
description: bbbb
type: str
c:
description: cccc
type: str
'''
EXAMPLES = ""
RETURN = ""
from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper
from ansible.module_utils.basic import missing_required_lib
with ModuleHelper.dependency("nopackagewiththisname", missing_required_lib("nopackagewiththisname")):
import nopackagewiththisname
class MSimple(ModuleHelper):
output_params = ('a', 'b', 'c')
module = dict(
argument_spec=dict(
a=dict(type='int'),
b=dict(type='str'),
c=dict(type='str'),
),
)
def __init_module__(self):
self.vars.set('value', None)
self.vars.set('abc', "abc", diff=True)
def __run__(self):
if (0 if self.vars.a is None else self.vars.a) >= 100:
raise Exception("a >= 100")
if self.vars.c == "abc change":
self.vars['abc'] = "changed abc"
if self.vars.get('a', 0) == 2:
self.vars['b'] = str(self.vars.b) * 2
self.vars['c'] = str(self.vars.c) * 2
def main():
msimple = MSimple()
msimple.run()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2021, Alexei Znamensky <russoz@gmail.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: msimple
author: "Alexei Znamensky (@russoz)"
short_description: Simple module for testing
description:
- Simple module test description.
options:
a:
description: aaaa
type: int
b:
description: bbbb
type: str
c:
description: cccc
type: str
'''
EXAMPLES = ""
RETURN = ""
from ansible_collections.community.general.plugins.module_utils.module_helper import ModuleHelper
class MSimple(ModuleHelper):
output_params = ('a', 'b', 'c')
module = dict(
argument_spec=dict(
a=dict(type='int'),
b=dict(type='str'),
c=dict(type='str'),
),
)
def __init_module__(self):
self.vars.set('value', None)
self.vars.set('abc', "abc", diff=True)
def __run__(self):
if (0 if self.vars.a is None else self.vars.a) >= 100:
raise Exception("a >= 100")
if self.vars.c == "abc change":
self.vars['abc'] = "changed abc"
if self.vars.get('a', 0) == 2:
self.vars['b'] = str(self.vars.b) * 2
self.vars['c'] = str(self.vars.c) * 2
def main():
msimple = MSimple()
msimple.run()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2021, Alexei Znamensky <russoz@gmail.com>
#
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
module: mstate
author: "Alexei Znamensky (@russoz)"
short_description: State-based module for testing
description:
- State-based module test description.
options:
a:
description: aaaa
type: int
required: yes
b:
description: bbbb
type: str
c:
description: cccc
type: str
state:
description: test states
type: str
choices: [join, b_x_a, c_x_a, both_x_a]
default: join
'''
EXAMPLES = ""
RETURN = ""
from ansible_collections.community.general.plugins.module_utils.module_helper import StateModuleHelper
class MState(StateModuleHelper):
output_params = ('a', 'b', 'c', 'state')
module = dict(
argument_spec=dict(
a=dict(type='int', required=True),
b=dict(type='str'),
c=dict(type='str'),
state=dict(type='str', choices=['join', 'b_x_a', 'c_x_a', 'both_x_a', 'nop'], default='join'),
),
)
def __init_module__(self):
self.vars.set('result', "abc", diff=True)
def state_join(self):
self.vars['result'] = "".join([str(self.vars.a), str(self.vars.b), str(self.vars.c)])
def state_b_x_a(self):
self.vars['result'] = str(self.vars.b) * self.vars.a
def state_c_x_a(self):
self.vars['result'] = str(self.vars.c) * self.vars.a
def state_both_x_a(self):
self.vars['result'] = (str(self.vars.b) + str(self.vars.c)) * self.vars.a
def state_nop(self):
pass
def main():
mstate = MState()
mstate.run()
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,3 @@
- include_tasks: msimple.yml
- include_tasks: mdepfail.yml
- include_tasks: mstate.yml

View File

@@ -0,0 +1,14 @@
- name: test failing dependency
mdepfail:
a: 123
ignore_errors: yes
register: result
- name: assert failing dependency
assert:
that:
- result.failed is true
- '"Failed to import" in result.msg'
- '"nopackagewiththisname" in result.msg'
- '"ModuleNotFoundError:" in result.exception'
- '"nopackagewiththisname" in result.exception'

View File

@@ -0,0 +1,54 @@
- name: test msimple 1
msimple:
a: 80
register: simple1
- name: assert simple1
assert:
that:
- simple1.a == 80
- simple1.abc == "abc"
- simple1.changed is false
- simple1.value is none
- name: test msimple 2
msimple:
a: 101
ignore_errors: yes
register: simple2
- name: assert simple2
assert:
that:
- simple2.a == 101
- 'simple2.msg == "Module failed with exception: a >= 100"'
- simple2.abc == "abc"
- simple2.failed is true
- simple2.changed is false
- simple2.value is none
- name: test msimple 3
msimple:
a: 2
b: potatoes
register: simple3
- name: assert simple3
assert:
that:
- simple3.a == 2
- simple3.b == "potatoespotatoes"
- simple3.c == "NoneNone"
- simple3.changed is false
- name: test msimple 4
msimple:
c: abc change
register: simple4
- name: assert simple4
assert:
that:
- simple4.c == "abc change"
- simple4.abc == "changed abc"
- simple4.changed is true

View File

@@ -0,0 +1,79 @@
- name: test mstate 1
mstate:
a: 80
b: banana
c: cashew
state: nop
register: state1
- name: assert state1
assert:
that:
- state1.a == 80
- state1.b == "banana"
- state1.c == "cashew"
- state1.result == "abc"
- state1.changed is false
- name: test mstate 2
mstate:
a: 80
b: banana
c: cashew
register: state2
- name: assert state2
assert:
that:
- state2.a == 80
- state2.b == "banana"
- state2.c == "cashew"
- state2.result == "80bananacashew"
- state2.changed is true
- name: test mstate 3
mstate:
a: 3
b: banana
state: b_x_a
register: state3
- name: assert state3
assert:
that:
- state3.a == 3
- state3.b == "banana"
- state3.result == "bananabananabanana"
- state3.changed is true
- name: test mstate 4
mstate:
a: 4
c: cashew
state: c_x_a
register: state4
- name: assert state4
assert:
that:
- state4.a == 4
- state4.c == "cashew"
- state4.result == "cashewcashewcashewcashew"
- state4.changed is true
- name: test mstate 5
mstate:
a: 5
b: foo
c: bar
state: both_x_a
register: state5
- name: assert state5
assert:
that:
- state5.a == 5
- state5.b == "foo"
- state5.c == "bar"
- state5.result == "foobarfoobarfoobarfoobarfoobar"
- state5.changed is true

View File

@@ -0,0 +1,64 @@
---
- name: 'Remove any node modules'
file:
path: '{{ remote_dir }}/node_modules'
state: absent
- vars:
# sample: node-v8.2.0-linux-x64.tar.xz
node_path: '{{ remote_dir }}/{{ nodejs_path }}/bin'
package: 'ncp'
block:
- shell: npm --version
environment:
PATH: '{{ node_path }}:{{ ansible_env.PATH }}'
register: npm_version
- debug:
var: npm_version.stdout
- name: 'Install simple package with no_bin_links disabled'
npm:
path: '{{ remote_dir }}'
executable: '{{ node_path }}/npm'
state: present
name: '{{ package }}'
no_bin_links: false
environment:
PATH: '{{ node_path }}:{{ ansible_env.PATH }}'
register: npm_install_no_bin_links_disabled
- name: 'Make sure .bin folder has been created'
stat:
path: "{{ remote_dir }}/node_modules/.bin"
register: npm_dotbin_folder_disabled
- name: 'Remove any node modules'
file:
path: '{{ remote_dir }}/node_modules'
state: absent
- name: 'Install simple package with no_bin_links enabled'
npm:
path: '{{ remote_dir }}'
executable: '{{ node_path }}/npm'
state: present
name: '{{ package }}'
no_bin_links: true
environment:
PATH: '{{ node_path }}:{{ ansible_env.PATH }}'
register: npm_install_no_bin_links_enabled
- name: 'Make sure .bin folder has not been created'
stat:
path: "{{ remote_dir }}/node_modules/.bin"
register: npm_dotbin_folder_enabled
- assert:
that:
- npm_install_no_bin_links_disabled is success
- npm_install_no_bin_links_disabled is changed
- npm_install_no_bin_links_enabled is success
- npm_install_no_bin_links_enabled is changed
- npm_dotbin_folder_disabled.stat.exists
- not npm_dotbin_folder_enabled.stat.exists

View File

@@ -1,2 +1,3 @@
- include_tasks: setup.yml
- include_tasks: test.yml
- include_tasks: no_bin_links.yml

View File

@@ -0,0 +1 @@
unsupported

View File

@@ -0,0 +1,73 @@
- name: "Verify required variables: model_name, model_type, oneclick_username, oneclick_password, oneclick_url"
fail:
msg: "One or more of the following variables are not set: model_name, model_type, oneclick_username, oneclick_password, oneclick_url"
when: >
model_name is not defined
or model_type is not defined
or oneclick_username is not defined
or oneclick_password is not defined
or oneclick_url is not defined
- block:
- name: "001: Enforce maintenance mode for {{ model_name }} with a note about why [check_mode test]"
spectrum_model_attrs: &mm_enabled_args
url: "{{ oneclick_url }}"
username: "{{ oneclick_username }}"
password: "{{ oneclick_password }}"
name: "{{ model_name }}"
type: "{{ model_type }}"
validate_certs: false
attributes:
- name: "isManaged"
value: "false"
- name: "Notes"
value: "{{ note_mm_enabled }}"
check_mode: true
register: mm_enabled_check_mode
- name: "001: assert that changes were made"
assert:
that:
- mm_enabled_check_mode is changed
- name: "001: assert that changed_attrs is properly set"
assert:
that:
- mm_enabled_check_mode.changed_attrs.Notes == note_mm_enabled
- mm_enabled_check_mode.changed_attrs.isManaged == "false"
- name: "002: Enforce maintenance mode for {{ model_name }} with a note about why"
spectrum_model_attrs:
<<: *mm_enabled_args
register: mm_enabled
check_mode: false
- name: "002: assert that changes were made"
assert:
that:
- mm_enabled is changed
- name: "002: assert that changed_attrs is properly set"
assert:
that:
- mm_enabled.changed_attrs.Notes == note_mm_enabled
- mm_enabled.changed_attrs.isManaged == "false"
- name: "003: Enforce maintenance mode for {{ model_name }} with a note about why [idempontence test]"
spectrum_model_attrs:
<<: *mm_enabled_args
register: mm_enabled_idp
check_mode: false
- name: "003: assert that changes were not made"
assert:
that:
- mm_enabled_idp is not changed
- name: "003: assert that changed_attrs is not set"
assert:
that:
- mm_enabled_idp.changed_attrs == {}
vars:
note_mm_enabled: "MM set via CO #1234 by OJ Simpson"

View File

@@ -0,0 +1,166 @@
plugins/module_utils/compat/ipaddress.py no-assert
plugins/module_utils/compat/ipaddress.py no-unicode-literals
plugins/module_utils/_mount.py future-import-boilerplate
plugins/module_utils/_mount.py metaclass-boilerplate
plugins/modules/cloud/linode/linode.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/linode/linode.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/linode/linode.py validate-modules:undocumented-parameter
plugins/modules/cloud/lxc/lxc_container.py use-argspec-type-path
plugins/modules/cloud/lxc/lxc_container.py validate-modules:use-run-command-not-popen
plugins/modules/cloud/misc/rhevm.py validate-modules:parameter-state-invalid-choice
plugins/modules/cloud/online/online_server_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/online/online_server_info.py validate-modules:return-syntax-error
plugins/modules/cloud/online/online_user_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/online/online_user_info.py validate-modules:return-syntax-error
plugins/modules/cloud/ovirt/ovirt_affinity_label_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_affinity_label_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_api_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_cluster_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_cluster_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_datacenter_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_datacenter_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_disk_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_disk_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_event_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_external_provider_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_external_provider_facts.py validate-modules:undocumented-parameter
plugins/modules/cloud/ovirt/ovirt_group_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_group_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_host_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_host_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_host_storage_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_host_storage_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_host_storage_facts.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/ovirt/ovirt_network_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_network_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_nic_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_nic_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_permission_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_permission_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_quota_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_quota_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_scheduling_policy_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_scheduling_policy_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_snapshot_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_snapshot_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_storage_domain_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_storage_domain_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_storage_template_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_storage_template_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_storage_template_facts.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/ovirt/ovirt_storage_vm_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_storage_vm_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_storage_vm_facts.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/ovirt/ovirt_tag_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_tag_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_template_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_template_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_user_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_user_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_vm_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_vm_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/ovirt/ovirt_vm_facts.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/ovirt/ovirt_vmpool_facts.py validate-modules:doc-missing-type
plugins/modules/cloud/ovirt/ovirt_vmpool_facts.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/rackspace/rax.py use-argspec-type-path # fix needed
plugins/modules/cloud/rackspace/rax.py validate-modules:doc-missing-type
plugins/modules/cloud/rackspace/rax.py validate-modules:undocumented-parameter
plugins/modules/cloud/rackspace/rax_files.py validate-modules:parameter-state-invalid-choice
plugins/modules/cloud/rackspace/rax_files_objects.py use-argspec-type-path
plugins/modules/cloud/rackspace/rax_mon_notification_plan.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/rackspace/rax_scaling_group.py use-argspec-type-path # fix needed, expanduser() applied to dict values
plugins/modules/cloud/scaleway/scaleway_image_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_image_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_ip_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_ip_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_organization_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_organization_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_security_group_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_security_group_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_server_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_server_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_snapshot_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_snapshot_info.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_volume_facts.py validate-modules:return-syntax-error
plugins/modules/cloud/scaleway/scaleway_volume_info.py validate-modules:return-syntax-error
plugins/modules/cloud/smartos/vmadm.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/smartos/vmadm.py validate-modules:undocumented-parameter
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/spotinst/spotinst_aws_elastigroup.py validate-modules:undocumented-parameter
plugins/modules/cloud/univention/udm_dns_record.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/univention/udm_dns_zone.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/univention/udm_dns_zone.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/univention/udm_dns_zone.py validate-modules:undocumented-parameter
plugins/modules/cloud/univention/udm_share.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/univention/udm_user.py validate-modules:parameter-list-no-elements
plugins/modules/cloud/xenserver/xenserver_guest.py validate-modules:doc-choices-do-not-match-spec
plugins/modules/cloud/xenserver/xenserver_guest.py validate-modules:doc-required-mismatch
plugins/modules/cloud/xenserver/xenserver_guest.py validate-modules:missing-suboption-docs
plugins/modules/cloud/xenserver/xenserver_guest.py validate-modules:parameter-type-not-in-doc
plugins/modules/cloud/xenserver/xenserver_guest.py validate-modules:undocumented-parameter
plugins/modules/clustering/consul/consul.py validate-modules:doc-missing-type
plugins/modules/clustering/consul/consul.py validate-modules:undocumented-parameter
plugins/modules/clustering/consul/consul_session.py validate-modules:parameter-state-invalid-choice
plugins/modules/monitoring/bigpanda.py validate-modules:invalid-argument-name
plugins/modules/monitoring/datadog/datadog_monitor.py validate-modules:invalid-argument-name
plugins/modules/net_tools/ldap/ldap_attr.py validate-modules:parameter-type-not-in-doc # This triggers when a parameter is undocumented
plugins/modules/net_tools/ldap/ldap_attr.py validate-modules:undocumented-parameter # Parameter removed but reason for removal is shown by custom code
plugins/modules/net_tools/ldap/ldap_entry.py validate-modules:doc-missing-type
plugins/modules/net_tools/ldap/ldap_entry.py validate-modules:parameter-type-not-in-doc
plugins/modules/net_tools/ldap/ldap_entry.py validate-modules:undocumented-parameter # Parameter removed but reason for removal is shown by custom code
plugins/modules/notification/cisco_webex.py validate-modules:invalid-argument-name
plugins/modules/notification/grove.py validate-modules:invalid-argument-name
plugins/modules/packaging/language/composer.py validate-modules:parameter-invalid
plugins/modules/packaging/os/apt_rpm.py validate-modules:parameter-invalid
plugins/modules/packaging/os/homebrew.py validate-modules:parameter-invalid
plugins/modules/packaging/os/homebrew_cask.py validate-modules:parameter-invalid
plugins/modules/packaging/os/opkg.py validate-modules:parameter-invalid
plugins/modules/packaging/os/pacman.py validate-modules:parameter-invalid
plugins/modules/packaging/os/redhat_subscription.py validate-modules:return-syntax-error
plugins/modules/packaging/os/slackpkg.py validate-modules:parameter-invalid
plugins/modules/packaging/os/urpmi.py validate-modules:parameter-invalid
plugins/modules/packaging/os/xbps.py validate-modules:parameter-invalid
plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:doc-missing-type
plugins/modules/remote_management/dellemc/idrac_server_config_profile.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/dellemc/ome_device_info.py validate-modules:parameter-list-no-elements
plugins/modules/remote_management/hpilo/hpilo_boot.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/hpilo/hpilo_info.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/hpilo/hponcfg.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/manageiq/manageiq_policies.py validate-modules:parameter-state-invalid-choice
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:doc-choices-do-not-match-spec # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:doc-missing-type # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:parameter-type-not-in-doc # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_provider.py validate-modules:undocumented-parameter # missing docs on suboptions
plugins/modules/remote_management/manageiq/manageiq_tags.py validate-modules:parameter-state-invalid-choice
plugins/modules/remote_management/stacki/stacki_host.py validate-modules:doc-default-does-not-match-spec
plugins/modules/remote_management/stacki/stacki_host.py validate-modules:parameter-type-not-in-doc
plugins/modules/remote_management/stacki/stacki_host.py validate-modules:undocumented-parameter
plugins/modules/source_control/github/github_deploy_key.py validate-modules:parameter-invalid
plugins/modules/storage/glusterfs/gluster_heal_info.py validate-modules:parameter-type-not-in-doc
plugins/modules/storage/glusterfs/gluster_peer.py validate-modules:parameter-list-no-elements
plugins/modules/storage/glusterfs/gluster_volume.py validate-modules:parameter-list-no-elements
plugins/modules/storage/glusterfs/gluster_volume.py validate-modules:parameter-type-not-in-doc
plugins/modules/storage/netapp/na_ontap_gather_facts.py validate-modules:parameter-state-invalid-choice
plugins/modules/storage/purestorage/purefa_facts.py validate-modules:doc-required-mismatch
plugins/modules/storage/purestorage/purefa_facts.py validate-modules:parameter-list-no-elements
plugins/modules/storage/purestorage/purefa_facts.py validate-modules:return-syntax-error
plugins/modules/storage/purestorage/purefb_facts.py validate-modules:parameter-list-no-elements
plugins/modules/storage/purestorage/purefb_facts.py validate-modules:return-syntax-error
plugins/modules/system/gconftool2.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/iptables_state.py validate-modules:undocumented-parameter
plugins/modules/system/launchd.py use-argspec-type-path # False positive
plugins/modules/system/osx_defaults.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/parted.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/puppet.py use-argspec-type-path
plugins/modules/system/puppet.py validate-modules:doc-default-does-not-match-spec # show_diff is not documented
plugins/modules/system/puppet.py validate-modules:parameter-type-not-in-doc
plugins/modules/system/runit.py validate-modules:parameter-type-not-in-doc
plugins/modules/system/ssh_config.py use-argspec-type-path # Required since module uses other methods to specify path
plugins/modules/system/xfconf.py validate-modules:parameter-state-invalid-choice
plugins/modules/system/xfconf.py validate-modules:return-syntax-error
plugins/modules/web_infrastructure/jenkins_plugin.py use-argspec-type-path
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/utils/shippable/check_matrix.py replace-urlopen
tests/utils/shippable/timing.py shebang

View File

@@ -71,8 +71,7 @@ def get_json(url):
"status": "running",
"vmid": "100",
"disk": "1000",
"uptime": 1000,
"tags": "test, tags, here"}]
"uptime": 1000}]
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu":
# _get_qemu_per_node
return [{"name": "test-qemu",
@@ -106,8 +105,7 @@ def get_json(url):
"vmid": "9001",
"uptime": 0,
"disk": 0,
"status": "stopped",
"tags": "test, tags, here"}]
"status": "stopped"}]
elif url == "https://localhost:8006/api2/json/pools/test":
# _get_members_per_pool
return {"members": [{"uptime": 1000,
@@ -164,6 +162,125 @@ def get_json(url):
"method6": "manual",
"autostart": 1,
"active": 1}]
elif url == "https://localhost:8006/api2/json/nodes/testnode/lxc/100/config":
# _get_vm_config (lxc)
return {
"console": 1,
"rootfs": "local-lvm:vm-100-disk-0,size=4G",
"cmode": "tty",
"description": "A testnode",
"cores": 1,
"hostname": "test-lxc",
"arch": "amd64",
"tty": 2,
"swap": 0,
"cpulimit": "0",
"net0": "name=eth0,bridge=vmbr0,gw=10.1.1.1,hwaddr=FF:FF:FF:FF:FF:FF,ip=10.1.1.3/24,type=veth",
"ostype": "ubuntu",
"digest": "123456789abcdef0123456789abcdef01234567890",
"protection": 0,
"memory": 1000,
"onboot": 0,
"cpuunits": 1024,
"tags": "one, two, three",
}
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/config":
# _get_vm_config (qemu)
return {
"tags": "one, two, three",
"cores": 1,
"ide2": "none,media=cdrom",
"memory": 1000,
"kvm": 1,
"digest": "0123456789abcdef0123456789abcdef0123456789",
"description": "A test qemu",
"sockets": 1,
"onboot": 1,
"vmgenid": "ffffffff-ffff-ffff-ffff-ffffffffffff",
"numa": 0,
"bootdisk": "scsi0",
"cpu": "host",
"name": "test-qemu",
"ostype": "l26",
"hotplug": "network,disk,usb",
"scsi0": "local-lvm:vm-101-disk-0,size=8G",
"net0": "virtio=ff:ff:ff:ff:ff:ff,bridge=vmbr0,firewall=1",
"agent": "1",
"bios": "seabios",
"ide0": "local-lvm:vm-101-cloudinit,media=cdrom,size=4M",
"boot": "cdn",
"scsihw": "virtio-scsi-pci",
"smbios1": "uuid=ffffffff-ffff-ffff-ffff-ffffffffffff"
}
elif url == "https://localhost:8006/api2/json/nodes/testnode/qemu/101/agent/network-get-interfaces":
# _get_agent_network_interfaces
return {"result": [
{
"hardware-address": "00:00:00:00:00:00",
"ip-addresses": [
{
"prefix": 8,
"ip-address-type": "ipv4",
"ip-address": "127.0.0.1"
},
{
"ip-address-type": "ipv6",
"ip-address": "::1",
"prefix": 128
}],
"statistics": {
"rx-errs": 0,
"rx-bytes": 163244,
"rx-packets": 1623,
"rx-dropped": 0,
"tx-dropped": 0,
"tx-packets": 1623,
"tx-bytes": 163244,
"tx-errs": 0},
"name": "lo"},
{
"statistics": {
"rx-packets": 4025,
"rx-dropped": 12,
"rx-bytes": 324105,
"rx-errs": 0,
"tx-errs": 0,
"tx-bytes": 368860,
"tx-packets": 3479,
"tx-dropped": 0},
"name": "eth0",
"ip-addresses": [
{
"prefix": 24,
"ip-address-type": "ipv4",
"ip-address": "10.1.2.3"
},
{
"prefix": 64,
"ip-address": "fd8c:4687:e88d:1be3:5b70:7b88:c79c:293",
"ip-address-type": "ipv6"
}],
"hardware-address": "ff:ff:ff:ff:ff:ff"
},
{
"hardware-address": "ff:ff:ff:ff:ff:ff",
"ip-addresses": [
{
"prefix": 16,
"ip-address": "10.10.2.3",
"ip-address-type": "ipv4"
}],
"name": "docker0",
"statistics": {
"rx-bytes": 0,
"rx-errs": 0,
"rx-dropped": 0,
"rx-packets": 0,
"tx-packets": 0,
"tx-dropped": 0,
"tx-errs": 0,
"tx-bytes": 0
}}]}
def get_vm_status(node, vmtype, vmid, name):
@@ -173,6 +290,10 @@ def get_vm_status(node, vmtype, vmid, name):
def get_option(option):
if option == 'group_prefix':
return 'proxmox_'
if option == 'facts_prefix':
return 'proxmox_'
elif option == 'want_facts':
return True
else:
return False
@@ -201,6 +322,9 @@ def test_populate(inventory, mocker):
group_qemu = inventory.inventory.groups['proxmox_pool_test']
assert group_qemu.hosts == [host_qemu]
# check if qemu-test has eth0 interface in agent_interfaces fact
assert 'eth0' in [d['name'] for d in host_qemu.get_vars()['proxmox_agent_interfaces']]
# check if lxc-test has been discovered correctly
group_lxc = inventory.inventory.groups['proxmox_all_lxc']
assert group_lxc.hosts == [host_lxc]

View File

@@ -9,7 +9,9 @@ import json
import pytest
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.module_utils.net_tools.pritunl import api
from ansible_collections.community.general.plugins.module_utils.net_tools.pritunl import (
api,
)
from mock import MagicMock
__metaclass__ = type
@@ -17,6 +19,237 @@ __metaclass__ = type
# Pritunl Mocks
PRITUNL_ORGS = [
{
"auth_api": False,
"name": "Foo",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "csftwlu6uhralzi2dpmhekz3",
},
{
"auth_api": False,
"name": "GumGum",
"auth_token": None,
"user_count": 3,
"auth_secret": None,
"id": "58070daee63f3b2e6e472c36",
},
{
"auth_api": False,
"name": "Bar",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "v1sncsxxybnsylc8gpqg85pg",
},
]
NEW_PRITUNL_ORG = {
"auth_api": False,
"name": "NewOrg",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "604a140ae63f3b36bc34c7bd",
}
PRITUNL_USERS = [
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "server",
"email": "bot@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["admin", "multiregion"],
"audit": False,
"name": "bot",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "client",
"email": "florian@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["web", "database"],
"audit": False,
"name": "florian",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "server",
"email": "ops@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["web", "database"],
"audit": False,
"name": "ops",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
]
NEW_PRITUNL_USER = {
"auth_type": "local",
"disabled": False,
"dns_servers": None,
"otp_secret": "6M4UWP2BCJBSYZAT",
"name": "alice",
"pin": False,
"dns_suffix": None,
"client_to_client": False,
"email": "alice@company.com",
"organization_name": "GumGum",
"bypass_secondary": False,
"groups": ["a", "b"],
"organization": "58070daee63f3b2e6e472c36",
"port_forwarding": [],
"type": "client",
"id": "590add71e63f3b72d8bb951a",
}
NEW_PRITUNL_USER_UPDATED = dict_merge(
NEW_PRITUNL_USER,
{
"disabled": True,
"name": "bob",
"email": "bob@company.com",
"groups": ["c", "d"],
},
)
class PritunlEmptyOrganizationMock(MagicMock):
"""Pritunl API Mock for organization GET API calls."""
def getcode(self):
return 200
def read(self):
return json.dumps([])
class PritunlListOrganizationMock(MagicMock):
"""Pritunl API Mock for organization GET API calls."""
@@ -25,34 +258,7 @@ class PritunlListOrganizationMock(MagicMock):
return 200
def read(self):
return json.dumps(
[
{
"auth_api": False,
"name": "Foo",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "csftwlu6uhralzi2dpmhekz3",
},
{
"auth_api": False,
"name": "GumGum",
"auth_token": None,
"user_count": 3,
"auth_secret": None,
"id": "58070daee63f3b2e6e472c36",
},
{
"auth_api": False,
"name": "Bar",
"auth_token": None,
"user_count": 0,
"auth_secret": None,
"id": "v1sncsxxybnsylc8gpqg85pg",
},
]
)
return json.dumps(PRITUNL_ORGS)
class PritunlListUserMock(MagicMock):
@@ -62,163 +268,7 @@ class PritunlListUserMock(MagicMock):
return 200
def read(self):
return json.dumps(
[
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "server",
"email": "bot@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["admin", "multiregion"],
"audit": False,
"name": "bot",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "client",
"email": "florian@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["web", "database"],
"audit": False,
"name": "florian",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
{
"auth_type": "google",
"dns_servers": None,
"pin": True,
"dns_suffix": None,
"servers": [
{
"status": False,
"platform": None,
"server_id": "580711322bb66c1d59b9568f",
"virt_address6": "fd00:c0a8: 9700: 0: 192: 168: 101: 27",
"virt_address": "192.168.101.27",
"name": "vpn-A",
"real_address": None,
"connected_since": None,
"id": "580711322bb66c1d59b9568f",
"device_name": None,
},
{
"status": False,
"platform": None,
"server_id": "5dad2cc6e63f3b3f4a6dfea5",
"virt_address6": "fd00:c0a8:f200: 0: 192: 168: 201: 37",
"virt_address": "192.168.201.37",
"name": "vpn-B",
"real_address": None,
"connected_since": None,
"id": "5dad2cc6e63f3b3f4a6dfea5",
"device_name": None,
},
],
"disabled": False,
"network_links": [],
"port_forwarding": [],
"id": "58070dafe63f3b2e6e472c3b",
"organization_name": "GumGum",
"type": "server",
"email": "ops@company.com",
"status": True,
"dns_mapping": None,
"otp_secret": "123456789ABCDEFG",
"client_to_client": False,
"sso": "google",
"bypass_secondary": False,
"groups": ["web", "database"],
"audit": False,
"name": "ops",
"gravatar": True,
"otp_auth": True,
"organization": "58070daee63f3b2e6e472c36",
},
]
)
return json.dumps(PRITUNL_USERS)
class PritunlErrorMock(MagicMock):
@@ -231,6 +281,22 @@ class PritunlErrorMock(MagicMock):
return "{}"
class PritunlPostOrganizationMock(MagicMock):
def getcode(self):
return 200
def read(self):
return json.dumps(NEW_PRITUNL_ORG)
class PritunlListOrganizationAfterPostMock(MagicMock):
def getcode(self):
return 200
def read(self):
return json.dumps(PRITUNL_ORGS + [NEW_PRITUNL_ORG])
class PritunlPostUserMock(MagicMock):
"""Pritunl API Mock for POST API calls."""
@@ -238,28 +304,7 @@ class PritunlPostUserMock(MagicMock):
return 200
def read(self):
return json.dumps(
[
{
"auth_type": "local",
"disabled": False,
"dns_servers": None,
"otp_secret": "6M4UWP2BCJBSYZAT",
"name": "alice",
"pin": False,
"dns_suffix": None,
"client_to_client": False,
"email": "alice@company.com",
"organization_name": "GumGum",
"bypass_secondary": False,
"groups": ["a", "b"],
"organization": "58070daee63f3b2e6e472c36",
"port_forwarding": [],
"type": "client",
"id": "590add71e63f3b72d8bb951a",
}
]
)
return json.dumps([NEW_PRITUNL_USER])
class PritunlPutUserMock(MagicMock):
@@ -269,26 +314,17 @@ class PritunlPutUserMock(MagicMock):
return 200
def read(self):
return json.dumps(
{
"auth_type": "local",
"disabled": True,
"dns_servers": None,
"otp_secret": "WEJANJYMF3Q2QSLG",
"name": "bob",
"pin": False,
"dns_suffix": False,
"client_to_client": False,
"email": "bob@company.com",
"organization_name": "GumGum",
"bypass_secondary": False,
"groups": ["c", "d"],
"organization": "58070daee63f3b2e6e472c36",
"port_forwarding": [],
"type": "client",
"id": "590add71e63f3b72d8bb951a",
}
)
return json.dumps(NEW_PRITUNL_USER_UPDATED)
class PritunlDeleteOrganizationMock(MagicMock):
"""Pritunl API Mock for DELETE API calls."""
def getcode(self):
return 200
def read(self):
return "{}"
class PritunlDeleteUserMock(MagicMock):
@@ -321,14 +357,21 @@ def pritunl_settings():
}
@pytest.fixture
def pritunl_organization_data():
return {
"name": NEW_PRITUNL_ORG["name"],
}
@pytest.fixture
def pritunl_user_data():
return {
"name": "alice",
"email": "alice@company.com",
"groups": ["a", "b"],
"disabled": False,
"type": "client",
"name": NEW_PRITUNL_USER["name"],
"email": NEW_PRITUNL_USER["email"],
"groups": NEW_PRITUNL_USER["groups"],
"disabled": NEW_PRITUNL_USER["disabled"],
"type": NEW_PRITUNL_USER["type"],
}
@@ -347,6 +390,11 @@ def get_pritunl_error_mock():
return PritunlErrorMock()
@pytest.fixture
def post_pritunl_organization_mock():
return PritunlPostOrganizationMock()
@pytest.fixture
def post_pritunl_user_mock():
return PritunlPostUserMock()
@@ -357,6 +405,11 @@ def put_pritunl_user_mock():
return PritunlPutUserMock()
@pytest.fixture
def delete_pritunl_organization_mock():
return PritunlDeleteOrganizationMock()
@pytest.fixture
def delete_pritunl_user_mock():
return PritunlDeleteUserMock()
@@ -460,6 +513,25 @@ class TestPritunlApi:
assert user["name"] == user_expected
# Test for POST operation on Pritunl API
def test_add_pritunl_organization(
self,
pritunl_settings,
pritunl_organization_data,
post_pritunl_organization_mock,
):
api._post_pritunl_organization = post_pritunl_organization_mock()
create_response = api.post_pritunl_organization(
**dict_merge(
pritunl_settings,
{"organization_name": pritunl_organization_data["name"]},
)
)
# Ensure provided settings match with the ones returned by Pritunl
for k, v in iteritems(pritunl_organization_data):
assert create_response[k] == v
@pytest.mark.parametrize("org_id", [("58070daee63f3b2e6e472c36")])
def test_add_and_update_pritunl_user(
self,
@@ -513,6 +585,24 @@ class TestPritunlApi:
assert update_response[k] == create_response[k]
# Test for DELETE operation on Pritunl API
@pytest.mark.parametrize("org_id", [("58070daee63f3b2e6e472c36")])
def test_delete_pritunl_organization(
self, pritunl_settings, org_id, delete_pritunl_organization_mock
):
api._delete_pritunl_organization = delete_pritunl_organization_mock()
response = api.delete_pritunl_organization(
**dict_merge(
pritunl_settings,
{
"organization_id": org_id,
},
)
)
assert response == {}
@pytest.mark.parametrize(
"org_id,user_id", [("58070daee63f3b2e6e472c36", "590add71e63f3b72d8bb951a")]
)

View File

@@ -6,10 +6,12 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
from collections import namedtuple
import pytest
from ansible_collections.community.general.plugins.module_utils.module_helper import (
ArgFormat, DependencyCtxMgr, ModuleHelper
ArgFormat, DependencyCtxMgr, ModuleHelper, VarMeta, cause_changes
)
@@ -105,3 +107,100 @@ def test_dependency_ctxmgr():
with ctx:
import sys
assert ctx.has_it
def test_variable_meta():
meta = VarMeta()
assert meta.output is True
assert meta.diff is False
assert meta.value is None
meta.set_value("abc")
assert meta.initial_value == "abc"
assert meta.value == "abc"
assert meta.diff_result is None
meta.set_value("def")
assert meta.initial_value == "abc"
assert meta.value == "def"
assert meta.diff_result is None
def test_variable_meta_diff():
meta = VarMeta(diff=True)
assert meta.output is True
assert meta.diff is True
assert meta.value is None
meta.set_value("abc")
assert meta.initial_value == "abc"
assert meta.value == "abc"
assert meta.diff_result is None
meta.set_value("def")
assert meta.initial_value == "abc"
assert meta.value == "def"
assert meta.diff_result == {"before": "abc", "after": "def"}
meta.set_value("ghi")
assert meta.initial_value == "abc"
assert meta.value == "ghi"
assert meta.diff_result == {"before": "abc", "after": "ghi"}
def test_vardict():
vd = ModuleHelper.VarDict()
vd.set('a', 123)
assert vd['a'] == 123
assert vd.a == 123
assert 'a' in vd._meta
assert vd.meta('a').output is True
assert vd.meta('a').diff is False
assert vd.meta('a').change is False
vd['b'] = 456
vd.set_meta('a', diff=True, change=True)
vd.set_meta('b', diff=True, output=False)
vd['c'] = 789
vd['a'] = 'new_a'
vd['c'] = 'new_c'
assert vd.a == 'new_a'
assert vd.c == 'new_c'
assert vd.output() == {'a': 'new_a', 'c': 'new_c'}
assert vd.diff() == {'before': {'a': 123}, 'after': {'a': 'new_a'}}, "diff={0}".format(vd.diff())
class MockMH(object):
changed = None
def _div(self, x, y):
return x / y
func_none = cause_changes()(_div)
func_onsucc = cause_changes(on_success=True)(_div)
func_onfail = cause_changes(on_failure=True)(_div)
func_onboth = cause_changes(on_success=True, on_failure=True)(_div)
CAUSE_CHG_DECO_PARAMS = ['method', 'expect_exception', 'expect_changed']
CAUSE_CHG_DECO = dict(
none_succ=dict(method='func_none', expect_exception=False, expect_changed=None),
none_fail=dict(method='func_none', expect_exception=True, expect_changed=None),
onsucc_succ=dict(method='func_onsucc', expect_exception=False, expect_changed=True),
onsucc_fail=dict(method='func_onsucc', expect_exception=True, expect_changed=None),
onfail_succ=dict(method='func_onfail', expect_exception=False, expect_changed=None),
onfail_fail=dict(method='func_onfail', expect_exception=True, expect_changed=True),
onboth_succ=dict(method='func_onboth', expect_exception=False, expect_changed=True),
onboth_fail=dict(method='func_onboth', expect_exception=True, expect_changed=True),
)
CAUSE_CHG_DECO_IDS = sorted(CAUSE_CHG_DECO.keys())
@pytest.mark.parametrize(CAUSE_CHG_DECO_PARAMS,
[[CAUSE_CHG_DECO[tc][param]
for param in CAUSE_CHG_DECO_PARAMS]
for tc in CAUSE_CHG_DECO_IDS],
ids=CAUSE_CHG_DECO_IDS)
def test_cause_changes_deco(method, expect_exception, expect_changed):
mh = MockMH()
if expect_exception:
with pytest.raises(Exception):
getattr(mh, method)(1, 0)
else:
getattr(mh, method)(9, 3)
assert mh.changed == expect_changed

View File

@@ -0,0 +1,406 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from contextlib import contextmanager
from ansible_collections.community.general.tests.unit.compat import unittest
from ansible_collections.community.general.tests.unit.compat.mock import call, patch
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
from ansible_collections.community.general.plugins.modules.identity.ipa import ipa_otpconfig
@contextmanager
def patch_ipa(**kwargs):
"""Mock context manager for patching the methods in OTPConfigIPAClient that contact the IPA server
Patches the `login` and `_post_json` methods
Keyword arguments are passed to the mock object that patches `_post_json`
No arguments are passed to the mock object that patches `login` because no tests require it
Example::
with patch_ipa(return_value={}) as (mock_login, mock_post):
...
"""
obj = ipa_otpconfig.OTPConfigIPAClient
with patch.object(obj, 'login') as mock_login:
with patch.object(obj, '_post_json', **kwargs) as mock_post:
yield mock_login, mock_post
class TestIPAOTPConfig(ModuleTestCase):
def setUp(self):
super(TestIPAOTPConfig, self).setUp()
self.module = ipa_otpconfig
def _test_base(self, module_args, return_value, mock_calls, changed):
"""Base function that's called by all the other test functions
module_args (dict):
Arguments passed to the module
return_value (dict):
Mocked return value of OTPConfigIPAClient.otpconfig_show, as returned by the IPA API.
This should be set to the current state. It will be changed to the desired state using the above arguments.
(Technically, this is the return value of _post_json, but it's only checked by otpconfig_show).
mock_calls (list/tuple of dicts):
List of calls made to OTPConfigIPAClient._post_json, in order.
_post_json is called by all of the otpconfig_* methods of the class.
Pass an empty list if no calls are expected.
changed (bool):
Whether or not the module is supposed to be marked as changed
"""
set_module_args(module_args)
# Run the module
with patch_ipa(return_value=return_value) as (mock_login, mock_post):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
# Verify that the calls to _post_json match what is expected
expected_call_count = len(mock_calls)
if expected_call_count > 1:
# Convert the call dicts to unittest.mock.call instances because `assert_has_calls` only accepts them
converted_calls = []
for call_dict in mock_calls:
converted_calls.append(call(**call_dict))
mock_post.assert_has_calls(converted_calls)
self.assertEqual(len(mock_post.mock_calls), expected_call_count)
elif expected_call_count == 1:
mock_post.assert_called_once_with(**mock_calls[0])
else: # expected_call_count is 0
mock_post.assert_not_called()
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_set_all_no_adjustment(self):
"""Set values requiring no adjustment"""
module_args = {
'ipatokentotpauthwindow': 11,
'ipatokentotpsyncwindow': 12,
'ipatokenhotpauthwindow': 13,
'ipatokenhotpsyncwindow': 14
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_all_aliases_no_adjustment(self):
"""Set values requiring no adjustment on all using aliases values"""
module_args = {
'totpauthwindow': 11,
'totpsyncwindow': 12,
'hotpauthwindow': 13,
'hotpsyncwindow': 14
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_totp_auth_window_no_adjustment(self):
"""Set values requiring no adjustment on totpauthwindow"""
module_args = {
'totpauthwindow': 11
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_totp_sync_window_no_adjustment(self):
"""Set values requiring no adjustment on totpsyncwindow"""
module_args = {
'totpsyncwindow': 12
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_hotp_auth_window_no_adjustment(self):
"""Set values requiring no adjustment on hotpauthwindow"""
module_args = {
'hotpauthwindow': 13
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_hotp_sync_window_no_adjustment(self):
"""Set values requiring no adjustment on hotpsyncwindow"""
module_args = {
'hotpsyncwindow': 14
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_totp_auth_window(self):
"""Set values requiring adjustment on totpauthwindow"""
module_args = {
'totpauthwindow': 10
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_mod',
'name': None,
'item': {'ipatokentotpauthwindow': '10'}
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_totp_sync_window(self):
"""Set values requiring adjustment on totpsyncwindow"""
module_args = {
'totpsyncwindow': 10
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_mod',
'name': None,
'item': {'ipatokentotpsyncwindow': '10'}
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_hotp_auth_window(self):
"""Set values requiring adjustment on hotpauthwindow"""
module_args = {
'hotpauthwindow': 10
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_mod',
'name': None,
'item': {'ipatokenhotpauthwindow': '10'}
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_hotp_sync_window(self):
"""Set values requiring adjustment on hotpsyncwindow"""
module_args = {
'hotpsyncwindow': 10
}
return_value = {
'ipatokentotpauthwindow': ['11'],
'ipatokentotpsyncwindow': ['12'],
'ipatokenhotpauthwindow': ['13'],
'ipatokenhotpsyncwindow': ['14']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_mod',
'name': None,
'item': {'ipatokenhotpsyncwindow': '10'}
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_set_all(self):
"""Set values requiring adjustment on all"""
module_args = {
'ipatokentotpauthwindow': 11,
'ipatokentotpsyncwindow': 12,
'ipatokenhotpauthwindow': 13,
'ipatokenhotpsyncwindow': 14
}
return_value = {
'ipatokentotpauthwindow': ['1'],
'ipatokentotpsyncwindow': ['2'],
'ipatokenhotpauthwindow': ['3'],
'ipatokenhotpsyncwindow': ['4']}
mock_calls = (
{
'method': 'otpconfig_show',
'name': None
},
{
'method': 'otpconfig_mod',
'name': None,
'item': {'ipatokentotpauthwindow': '11',
'ipatokentotpsyncwindow': '12',
'ipatokenhotpauthwindow': '13',
'ipatokenhotpsyncwindow': '14'}
},
{
'method': 'otpconfig_show',
'name': None
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_fail_post(self):
"""Fail due to an exception raised from _post_json"""
set_module_args({
'ipatokentotpauthwindow': 11,
'ipatokentotpsyncwindow': 12,
'ipatokenhotpauthwindow': 13,
'ipatokenhotpsyncwindow': 14
})
with patch_ipa(side_effect=Exception('ERROR MESSAGE')) as (mock_login, mock_post):
with self.assertRaises(AnsibleFailJson) as exec_info:
self.module.main()
self.assertEqual(exec_info.exception.args[0]['msg'], 'ERROR MESSAGE')
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,495 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from contextlib import contextmanager
from ansible_collections.community.general.tests.unit.compat import unittest
from ansible_collections.community.general.tests.unit.compat.mock import call, patch
from ansible_collections.community.general.tests.unit.plugins.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args
from ansible_collections.community.general.plugins.modules.identity.ipa import ipa_otptoken
@contextmanager
def patch_ipa(**kwargs):
"""Mock context manager for patching the methods in OTPTokenIPAClient that contact the IPA server
Patches the `login` and `_post_json` methods
Keyword arguments are passed to the mock object that patches `_post_json`
No arguments are passed to the mock object that patches `login` because no tests require it
Example::
with patch_ipa(return_value={}) as (mock_login, mock_post):
...
"""
obj = ipa_otptoken.OTPTokenIPAClient
with patch.object(obj, 'login') as mock_login:
with patch.object(obj, '_post_json', **kwargs) as mock_post:
yield mock_login, mock_post
class TestIPAOTPToken(ModuleTestCase):
def setUp(self):
super(TestIPAOTPToken, self).setUp()
self.module = ipa_otptoken
def _test_base(self, module_args, return_value, mock_calls, changed):
"""Base function that's called by all the other test functions
module_args (dict):
Arguments passed to the module
return_value (dict):
Mocked return value of OTPTokenIPAClient.otptoken_show, as returned by the IPA API.
This should be set to the current state. It will be changed to the desired state using the above arguments.
(Technically, this is the return value of _post_json, but it's only checked by otptoken_show).
mock_calls (list/tuple of dicts):
List of calls made to OTPTokenIPAClient._post_json, in order.
_post_json is called by all of the otptoken_* methods of the class.
Pass an empty list if no calls are expected.
changed (bool):
Whether or not the module is supposed to be marked as changed
"""
set_module_args(module_args)
# Run the module
with patch_ipa(return_value=return_value) as (mock_login, mock_post):
with self.assertRaises(AnsibleExitJson) as exec_info:
self.module.main()
# Verify that the calls to _post_json match what is expected
expected_call_count = len(mock_calls)
if expected_call_count > 1:
# Convert the call dicts to unittest.mock.call instances because `assert_has_calls` only accepts them
converted_calls = []
for call_dict in mock_calls:
converted_calls.append(call(**call_dict))
mock_post.assert_has_calls(converted_calls)
self.assertEqual(len(mock_post.mock_calls), expected_call_count)
elif expected_call_count == 1:
mock_post.assert_called_once_with(**mock_calls[0])
else: # expected_call_count is 0
mock_post.assert_not_called()
# Verify that the module's changed status matches what is expected
self.assertIs(exec_info.exception.args[0]['changed'], changed)
def test_add_new_all_default(self):
"""Add a new OTP with all default values"""
module_args = {
'uniqueid': 'NewToken1'
}
return_value = {}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_add',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'FALSE',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_add_new_all_default_with_aliases(self):
"""Add a new OTP with all default values using alias values"""
module_args = {
'name': 'NewToken1'
}
return_value = {}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_add',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'FALSE',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_add_new_all_specified(self):
"""Add a new OTP with all default values"""
module_args = {
'uniqueid': 'NewToken1',
'otptype': 'hotp',
'secretkey': 'VGVzdFNlY3JldDE=',
'description': 'Test description',
'owner': 'pinky',
'enabled': True,
'notbefore': '20200101010101',
'notafter': '20900101010101',
'vendor': 'Acme',
'model': 'ModelT',
'serial': 'Number1',
'state': 'present',
'algorithm': 'sha256',
'digits': 6,
'offset': 10,
'interval': 30,
'counter': 30,
}
return_value = {}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_add',
'name': 'NewToken1',
'item': {'type': 'HOTP',
'ipatokenotpkey': 'KRSXG5CTMVRXEZLUGE======',
'description': 'Test description',
'ipatokenowner': 'pinky',
'ipatokendisabled': 'FALSE',
'ipatokennotbefore': '20200101010101Z',
'ipatokennotafter': '20900101010101Z',
'ipatokenvendor': 'Acme',
'ipatokenmodel': 'ModelT',
'ipatokenserial': 'Number1',
'ipatokenotpalgorithm': 'sha256',
'ipatokenotpdigits': '6',
'ipatokentotpclockoffset': '10',
'ipatokentotptimestep': '30',
'ipatokenhotpcounter': '30',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_already_existing_no_change_all_specified(self):
"""Add a new OTP with all values specified but needing no change"""
module_args = {
'uniqueid': 'NewToken1',
'otptype': 'hotp',
'secretkey': 'VGVzdFNlY3JldDE=',
'description': 'Test description',
'owner': 'pinky',
'enabled': True,
'notbefore': '20200101010101',
'notafter': '20900101010101',
'vendor': 'Acme',
'model': 'ModelT',
'serial': 'Number1',
'state': 'present',
'algorithm': 'sha256',
'digits': 6,
'offset': 10,
'interval': 30,
'counter': 30,
}
return_value = {'ipatokenuniqueid': 'NewToken1',
'type': 'HOTP',
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
'ipatokenmodel': ['ModelT'],
'ipatokenserial': ['Number1'],
'ipatokenotpalgorithm': ['sha256'],
'ipatokenotpdigits': ['6'],
'ipatokentotpclockoffset': ['10'],
'ipatokentotptimestep': ['30'],
'ipatokenhotpcounter': ['30']}
mock_calls = [
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
}
]
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_already_existing_one_change_all_specified(self):
"""Modify an existing OTP with one value specified needing change"""
module_args = {
'uniqueid': 'NewToken1',
'otptype': 'hotp',
'secretkey': 'VGVzdFNlY3JldDE=',
'description': 'Test description',
'owner': 'brain',
'enabled': True,
'notbefore': '20200101010101',
'notafter': '20900101010101',
'vendor': 'Acme',
'model': 'ModelT',
'serial': 'Number1',
'state': 'present',
'algorithm': 'sha256',
'digits': 6,
'offset': 10,
'interval': 30,
'counter': 30,
}
return_value = {'ipatokenuniqueid': 'NewToken1',
'type': 'HOTP',
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
'ipatokenmodel': ['ModelT'],
'ipatokenserial': ['Number1'],
'ipatokenotpalgorithm': ['sha256'],
'ipatokenotpdigits': ['6'],
'ipatokentotpclockoffset': ['10'],
'ipatokentotptimestep': ['30'],
'ipatokenhotpcounter': ['30']}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_mod',
'name': 'NewToken1',
'item': {'description': 'Test description',
'ipatokenowner': 'brain',
'ipatokendisabled': 'FALSE',
'ipatokennotbefore': '20200101010101Z',
'ipatokennotafter': '20900101010101Z',
'ipatokenvendor': 'Acme',
'ipatokenmodel': 'ModelT',
'ipatokenserial': 'Number1',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_already_existing_all_valid_change_all_specified(self):
"""Modify an existing OTP with all valid values specified needing change"""
module_args = {
'uniqueid': 'NewToken1',
'otptype': 'hotp',
'secretkey': 'VGVzdFNlY3JldDE=',
'description': 'New Test description',
'owner': 'pinky',
'enabled': False,
'notbefore': '20200101010102',
'notafter': '20900101010102',
'vendor': 'NewAcme',
'model': 'NewModelT',
'serial': 'Number2',
'state': 'present',
'algorithm': 'sha256',
'digits': 6,
'offset': 10,
'interval': 30,
'counter': 30,
}
return_value = {'ipatokenuniqueid': 'NewToken1',
'type': 'HOTP',
'ipatokenotpkey': [{'__base64__': 'VGVzdFNlY3JldDE='}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
'ipatokenmodel': ['ModelT'],
'ipatokenserial': ['Number1'],
'ipatokenotpalgorithm': ['sha256'],
'ipatokenotpdigits': ['6'],
'ipatokentotpclockoffset': ['10'],
'ipatokentotptimestep': ['30'],
'ipatokenhotpcounter': ['30']}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_mod',
'name': 'NewToken1',
'item': {'description': 'New Test description',
'ipatokenowner': 'pinky',
'ipatokendisabled': 'TRUE',
'ipatokennotbefore': '20200101010102Z',
'ipatokennotafter': '20900101010102Z',
'ipatokenvendor': 'NewAcme',
'ipatokenmodel': 'NewModelT',
'ipatokenserial': 'Number2',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_delete_existing_token(self):
"""Delete an existing OTP"""
module_args = {
'uniqueid': 'NewToken1',
'state': 'absent'
}
return_value = {'ipatokenuniqueid': 'NewToken1',
'type': 'HOTP',
'ipatokenotpkey': [{'__base64__': 'KRSXG5CTMVRXEZLUGE======'}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
'ipatokenmodel': ['ModelT'],
'ipatokenserial': ['Number1'],
'ipatokenotpalgorithm': ['sha256'],
'ipatokenotpdigits': ['6'],
'ipatokentotpclockoffset': ['10'],
'ipatokentotptimestep': ['30'],
'ipatokenhotpcounter': ['30']}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_del',
'name': 'NewToken1'
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_disable_existing_token(self):
"""Disable an existing OTP"""
module_args = {
'uniqueid': 'NewToken1',
'otptype': 'hotp',
'enabled': False
}
return_value = {'ipatokenuniqueid': 'NewToken1',
'type': 'HOTP',
'ipatokenotpkey': [{'__base64__': 'KRSXG5CTMVRXEZLUGE======'}],
'description': ['Test description'],
'ipatokenowner': ['pinky'],
'ipatokendisabled': ['FALSE'],
'ipatokennotbefore': ['20200101010101Z'],
'ipatokennotafter': ['20900101010101Z'],
'ipatokenvendor': ['Acme'],
'ipatokenmodel': ['ModelT'],
'ipatokenserial': ['Number1'],
'ipatokenotpalgorithm': ['sha256'],
'ipatokenotpdigits': ['6'],
'ipatokentotpclockoffset': ['10'],
'ipatokentotptimestep': ['30'],
'ipatokenhotpcounter': ['30']}
mock_calls = (
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
},
{
'method': 'otptoken_mod',
'name': 'NewToken1',
'item': {'ipatokendisabled': 'TRUE',
'all': True}
}
)
changed = True
self._test_base(module_args, return_value, mock_calls, changed)
def test_delete_not_existing_token(self):
"""Delete a OTP that does not exist"""
module_args = {
'uniqueid': 'NewToken1',
'state': 'absent'
}
return_value = {}
mock_calls = [
{
'method': 'otptoken_find',
'name': None,
'item': {'all': True,
'ipatokenuniqueid': 'NewToken1',
'timelimit': '0',
'sizelimit': '0'}
}
]
changed = False
self._test_base(module_args, return_value, mock_calls, changed)
def test_fail_post(self):
"""Fail due to an exception raised from _post_json"""
set_module_args({
'uniqueid': 'NewToken1'
})
with patch_ipa(side_effect=Exception('ERROR MESSAGE')) as (mock_login, mock_post):
with self.assertRaises(AnsibleFailJson) as exec_info:
self.module.main()
self.assertEqual(exec_info.exception.args[0]['msg'], 'ERROR MESSAGE')
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,204 @@
# -*- coding: utf-8 -*-
# (c) 2021 Florian Dambrine <android.florian@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
import sys
from ansible.module_utils.common.dict_transformations import dict_merge
from ansible.module_utils.six import iteritems
from ansible_collections.community.general.plugins.modules.net_tools.pritunl import (
pritunl_org,
)
from ansible_collections.community.general.tests.unit.compat.mock import patch
from ansible_collections.community.general.tests.unit.plugins.module_utils.net_tools.pritunl.test_api import (
PritunlDeleteOrganizationMock,
PritunlListOrganizationMock,
PritunlListOrganizationAfterPostMock,
PritunlPostOrganizationMock,
)
from ansible_collections.community.general.tests.unit.plugins.modules.utils import (
AnsibleExitJson,
AnsibleFailJson,
ModuleTestCase,
set_module_args,
)
__metaclass__ = type
class TestPritunlOrg(ModuleTestCase):
def setUp(self):
super(TestPritunlOrg, self).setUp()
self.module = pritunl_org
# Add backward compatibility
if sys.version_info < (3, 2):
self.assertRegex = self.assertRegexpMatches
def tearDown(self):
super(TestPritunlOrg, self).tearDown()
def patch_add_pritunl_organization(self, **kwds):
return patch(
"ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api._post_pritunl_organization",
autospec=True,
**kwds
)
def patch_delete_pritunl_organization(self, **kwds):
return patch(
"ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api._delete_pritunl_organization",
autospec=True,
**kwds
)
def patch_get_pritunl_organizations(self, **kwds):
return patch(
"ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api._get_pritunl_organizations",
autospec=True,
**kwds
)
def test_without_parameters(self):
"""Test without parameters"""
set_module_args({})
with self.assertRaises(AnsibleFailJson):
self.module.main()
def test_present(self):
"""Test Pritunl organization creation."""
org_params = {"name": "NewOrg"}
set_module_args(
dict_merge(
{
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
},
org_params,
)
)
# Test creation
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as mock_get:
with self.patch_add_pritunl_organization(
side_effect=PritunlPostOrganizationMock
) as mock_add:
with self.assertRaises(AnsibleExitJson) as create_result:
self.module.main()
create_exc = create_result.exception.args[0]
self.assertTrue(create_exc["changed"])
self.assertEqual(create_exc["response"]["name"], org_params["name"])
self.assertEqual(create_exc["response"]["user_count"], 0)
# Test module idempotency
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationAfterPostMock
) as mock_get:
with self.patch_add_pritunl_organization(
side_effect=PritunlPostOrganizationMock
) as mock_add:
with self.assertRaises(AnsibleExitJson) as idempotent_result:
self.module.main()
idempotent_exc = idempotent_result.exception.args[0]
# Ensure both calls resulted in the same returned value
# except for changed which sould be false the second time
for k, v in iteritems(idempotent_exc):
if k == "changed":
self.assertFalse(idempotent_exc[k])
else:
self.assertEqual(create_exc[k], idempotent_exc[k])
def test_absent(self):
"""Test organization removal from Pritunl."""
org_params = {"name": "NewOrg"}
set_module_args(
dict_merge(
{
"state": "absent",
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
},
org_params,
)
)
# Test deletion
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationAfterPostMock
) as mock_get:
with self.patch_delete_pritunl_organization(
side_effect=PritunlDeleteOrganizationMock
) as mock_delete:
with self.assertRaises(AnsibleExitJson) as delete_result:
self.module.main()
delete_exc = delete_result.exception.args[0]
self.assertTrue(delete_exc["changed"])
self.assertEqual(delete_exc["response"], {})
# Test module idempotency
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as mock_get:
with self.patch_delete_pritunl_organization(
side_effect=PritunlDeleteOrganizationMock
) as mock_add:
with self.assertRaises(AnsibleExitJson) as idempotent_result:
self.module.main()
idempotent_exc = idempotent_result.exception.args[0]
# Ensure both calls resulted in the same returned value
# except for changed which sould be false the second time
self.assertFalse(idempotent_exc["changed"])
self.assertEqual(idempotent_exc["response"], delete_exc["response"])
def test_absent_with_existing_users(self):
"""Test organization removal with attached users should fail except if force is true."""
module_args = {
"state": "absent",
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
"name": "GumGum",
}
set_module_args(module_args)
# Test deletion
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as mock_get:
with self.patch_delete_pritunl_organization(
side_effect=PritunlDeleteOrganizationMock
) as mock_delete:
with self.assertRaises(AnsibleFailJson) as failure_result:
self.module.main()
failure_exc = failure_result.exception.args[0]
self.assertRegex(failure_exc["msg"], "Can not remove organization")
# Switch force=True which should run successfully
set_module_args(dict_merge(module_args, {"force": True}))
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as mock_get:
with self.patch_delete_pritunl_organization(
side_effect=PritunlDeleteOrganizationMock
) as mock_delete:
with self.assertRaises(AnsibleExitJson) as delete_result:
self.module.main()
delete_exc = delete_result.exception.args[0]
self.assertTrue(delete_exc["changed"])

View File

@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Florian Dambrine <android.florian@gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
import sys
from ansible_collections.community.general.plugins.modules.net_tools.pritunl import (
pritunl_org_info,
)
from ansible_collections.community.general.tests.unit.compat.mock import patch
from ansible_collections.community.general.tests.unit.plugins.module_utils.net_tools.pritunl.test_api import (
PritunlListOrganizationMock,
PritunlEmptyOrganizationMock,
)
from ansible_collections.community.general.tests.unit.plugins.modules.utils import (
AnsibleExitJson,
AnsibleFailJson,
ModuleTestCase,
set_module_args,
)
__metaclass__ = type
class TestPritunlOrgInfo(ModuleTestCase):
def setUp(self):
super(TestPritunlOrgInfo, self).setUp()
self.module = pritunl_org_info
# Add backward compatibility
if sys.version_info < (3, 2):
self.assertRegex = self.assertRegexpMatches
def tearDown(self):
super(TestPritunlOrgInfo, self).tearDown()
def patch_get_pritunl_organizations(self, **kwds):
return patch(
"ansible_collections.community.general.plugins.module_utils.net_tools.pritunl.api._get_pritunl_organizations",
autospec=True,
**kwds
)
def test_without_parameters(self):
"""Test without parameters"""
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as org_mock:
set_module_args({})
with self.assertRaises(AnsibleFailJson):
self.module.main()
self.assertEqual(org_mock.call_count, 0)
def test_list_empty_organizations(self):
"""Listing all organizations even when no org exists should be valid."""
with self.patch_get_pritunl_organizations(
side_effect=PritunlEmptyOrganizationMock
) as org_mock:
with self.assertRaises(AnsibleExitJson) as result:
set_module_args(
{
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
}
)
self.module.main()
self.assertEqual(org_mock.call_count, 1)
exc = result.exception.args[0]
self.assertEqual(len(exc["organizations"]), 0)
def test_list_specific_organization(self):
"""Listing a specific organization should be valid."""
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as org_mock:
with self.assertRaises(AnsibleExitJson) as result:
set_module_args(
{
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
"org": "GumGum",
}
)
self.module.main()
self.assertEqual(org_mock.call_count, 1)
exc = result.exception.args[0]
self.assertEqual(len(exc["organizations"]), 1)
def test_list_unknown_organization(self):
"""Listing an unknown organization should result in a failure."""
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as org_mock:
with self.assertRaises(AnsibleFailJson) as result:
set_module_args(
{
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
"org": "Unknown",
}
)
self.module.main()
self.assertEqual(org_mock.call_count, 1)
exc = result.exception.args[0]
self.assertRegex(exc["msg"], "does not exist")
def test_list_all_organizations(self):
"""Listing all organizations should be valid."""
with self.patch_get_pritunl_organizations(
side_effect=PritunlListOrganizationMock
) as org_mock:
with self.assertRaises(AnsibleExitJson) as result:
set_module_args(
{
"pritunl_api_token": "token",
"pritunl_api_secret": "secret",
"pritunl_url": "https://pritunl.domain.com",
}
)
self.module.main()
self.assertEqual(org_mock.call_count, 1)
exc = result.exception.args[0]
self.assertEqual(len(exc["organizations"]), 3)

View File

@@ -17,42 +17,42 @@ def debug_mock(url, request):
print(request.original.__dict__)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/orgs/.*', method="get")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/orgs/.*', method="get")
def get_orgs_mock(url, request):
match = re.search(r"api\.github\.com:443/orgs/(?P<org>[^/]+)", request.url)
match = re.search(r"api\.github\.com(:[0-9]+)?/orgs/(?P<org>[^/]+)", request.url)
org = match.group("org")
# https://docs.github.com/en/rest/reference/orgs#get-an-organization
headers = {'content-type': 'application/json'}
content = {
"login": org,
"url": "https://api.github.com:443/orgs/{0}".format(org)
"url": "https://api.github.com/orgs/{0}".format(org)
}
content = json.dumps(content).encode("utf-8")
return response(200, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/user', method="get")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user', method="get")
def get_user_mock(url, request):
# https://docs.github.com/en/rest/reference/users#get-the-authenticated-user
headers = {'content-type': 'application/json'}
content = {
"login": "octocat",
"url": "https://api.github.com:443/users/octocat"
"url": "https://api.github.com/users/octocat"
}
content = json.dumps(content).encode("utf-8")
return response(200, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/repos/.*/.*', method="get")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/repos/.*/.*', method="get")
def get_repo_notfound_mock(url, request):
return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/repos/.*/.*', method="get")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/repos/.*/.*', method="get")
def get_repo_mock(url, request):
match = re.search(
r"api\.github\.com:443/repos/(?P<org>[^/]+)/(?P<repo>[^/]+)", request.url)
r"api\.github\.com(:[0-9]+)?/repos/(?P<org>[^/]+)/(?P<repo>[^/]+)", request.url)
org = match.group("org")
repo = match.group("repo")
@@ -61,7 +61,7 @@ def get_repo_mock(url, request):
content = {
"name": repo,
"full_name": "{0}/{1}".format(org, repo),
"url": "https://api.github.com:443/repos/{0}/{1}".format(org, repo),
"url": "https://api.github.com/repos/{0}/{1}".format(org, repo),
"private": False,
"description": "This your first repo!",
"default_branch": "master",
@@ -71,10 +71,10 @@ def get_repo_mock(url, request):
return response(200, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/orgs/.*/repos', method="post")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/orgs/.*/repos', method="post")
def create_new_org_repo_mock(url, request):
match = re.search(
r"api\.github\.com:443/orgs/(?P<org>[^/]+)/repos", request.url)
r"api\.github\.com(:[0-9]+)?/orgs/(?P<org>[^/]+)/repos", request.url)
org = match.group("org")
repo = json.loads(request.body)
@@ -90,7 +90,7 @@ def create_new_org_repo_mock(url, request):
return response(201, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/user/repos', method="post")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/user/repos', method="post")
def create_new_user_repo_mock(url, request):
repo = json.loads(request.body)
@@ -106,10 +106,10 @@ def create_new_user_repo_mock(url, request):
return response(201, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/repos/.*/.*', method="patch")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/repos/.*/.*', method="patch")
def patch_repo_mock(url, request):
match = re.search(
r"api\.github\.com:443/repos/(?P<org>[^/]+)/(?P<repo>[^/]+)", request.url)
r"api\.github\.com(:[0-9]+)?/repos/(?P<org>[^/]+)/(?P<repo>[^/]+)", request.url)
org = match.group("org")
repo = match.group("repo")
@@ -119,7 +119,7 @@ def patch_repo_mock(url, request):
content = {
"name": repo,
"full_name": "{0}/{1}".format(org, repo),
"url": "https://api.github.com:443/repos/{0}/{1}".format(org, repo),
"url": "https://api.github.com/repos/{0}/{1}".format(org, repo),
"private": body['private'],
"description": body['description'],
"default_branch": "master",
@@ -129,13 +129,13 @@ def patch_repo_mock(url, request):
return response(200, content, headers, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/repos/.*/.*', method="delete")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/repos/.*/.*', method="delete")
def delete_repo_mock(url, request):
# https://docs.github.com/en/rest/reference/repos#delete-a-repository
return response(204, None, None, None, 5, request)
@urlmatch(netloc=r'api\.github\.com:443$', path=r'/repos/.*/.*', method="delete")
@urlmatch(netloc=r'api\.github\.com(:[0-9]+)?$', path=r'/repos/.*/.*', method="delete")
def delete_repo_notfound_mock(url, request):
# https://docs.github.com/en/rest/reference/repos#delete-a-repository
return response(404, "{\"message\": \"Not Found\"}", "", "Not Found", 5, request)

View File

@@ -26,8 +26,8 @@ class TestCreateJavaKeystore(ModuleTestCase):
orig_exists = os.path.exists
self.spec = ArgumentSpec()
self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file',
side_effect=lambda path, content: path)
self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file')
self.mock_create_path = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_path')
self.mock_run_commands = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.run_commands')
self.mock_os_path_exists = patch('os.path.exists',
side_effect=lambda path: True if path == '/path/to/keystore.jks' else orig_exists(path))
@@ -37,6 +37,7 @@ class TestCreateJavaKeystore(ModuleTestCase):
side_effect=lambda path: (False, None))
self.run_commands = self.mock_run_commands.start()
self.create_file = self.mock_create_file.start()
self.create_path = self.mock_create_path.start()
self.selinux_context = self.mock_selinux_context.start()
self.is_special_selinux_path = self.mock_is_special_selinux_path.start()
self.os_path_exists = self.mock_os_path_exists.start()
@@ -45,6 +46,7 @@ class TestCreateJavaKeystore(ModuleTestCase):
"""Teardown."""
super(TestCreateJavaKeystore, self).tearDown()
self.mock_create_file.stop()
self.mock_create_path.stop()
self.mock_run_commands.stop()
self.mock_selinux_context.stop()
self.mock_is_special_selinux_path.stop()
@@ -67,17 +69,18 @@ class TestCreateJavaKeystore(ModuleTestCase):
module.exit_json = Mock()
with patch('os.remove', return_value=True):
self.run_commands.side_effect = lambda module, cmd, data: (0, '', '')
self.create_path.side_effect = ['/tmp/tmpgrzm2ah7']
self.create_file.side_effect = ['/tmp/etacifitrec', '/tmp/yek_etavirp']
self.run_commands.side_effect = [(0, '', ''), (0, '', '')]
create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
module.exit_json.assert_called_once_with(
changed=True,
cmd=["keytool", "-importkeystore",
"-destkeystore", "/path/to/keystore.jks",
"-srckeystore", "/tmp/keystore.p12", "-srcstoretype", "pkcs12", "-alias", "test",
"-deststorepass", "changeit", "-srcstorepass", "changeit", "-noprompt"],
"-srckeystore", "/tmp/tmpgrzm2ah7", "-srcstoretype", "pkcs12", "-alias", "test",
"-deststorepass:env", "STOREPASS", "-srcstorepass:env", "STOREPASS", "-noprompt"],
msg='',
rc=0,
stdout_lines=''
rc=0
)
def test_create_jks_keypass_fail_export_pkcs12(self):
@@ -98,12 +101,15 @@ class TestCreateJavaKeystore(ModuleTestCase):
module.fail_json = Mock()
with patch('os.remove', return_value=True):
self.create_path.side_effect = ['/tmp/tmp1cyp12xa']
self.create_file.side_effect = ['/tmp/tmpvalcrt32', '/tmp/tmpwh4key0c']
self.run_commands.side_effect = [(1, '', ''), (0, '', '')]
create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "passphrase-foo")
module.fail_json.assert_called_once_with(
cmd=["openssl", "pkcs12", "-export", "-name", "test",
"-in", "/tmp/foo.crt", "-inkey", "/tmp/foo.key",
"-out", "/tmp/keystore.p12",
"-in", "/tmp/tmpvalcrt32",
"-inkey", "/tmp/tmpwh4key0c",
"-out", "/tmp/tmp1cyp12xa",
"-passout", "stdin",
"-passin", "stdin"],
msg='',
@@ -127,12 +133,15 @@ class TestCreateJavaKeystore(ModuleTestCase):
module.fail_json = Mock()
with patch('os.remove', return_value=True):
self.create_path.side_effect = ['/tmp/tmp1cyp12xa']
self.create_file.side_effect = ['/tmp/tmpvalcrt32', '/tmp/tmpwh4key0c']
self.run_commands.side_effect = [(1, '', ''), (0, '', '')]
create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
module.fail_json.assert_called_once_with(
cmd=["openssl", "pkcs12", "-export", "-name", "test",
"-in", "/tmp/foo.crt", "-inkey", "/tmp/foo.key",
"-out", "/tmp/keystore.p12",
"-in", "/tmp/tmpvalcrt32",
"-inkey", "/tmp/tmpwh4key0c",
"-out", "/tmp/tmp1cyp12xa",
"-passout", "stdin"],
msg='',
rc=1
@@ -155,13 +164,15 @@ class TestCreateJavaKeystore(ModuleTestCase):
module.fail_json = Mock()
with patch('os.remove', return_value=True):
self.create_path.side_effect = ['/tmp/tmpgrzm2ah7']
self.create_file.side_effect = ['/tmp/etacifitrec', '/tmp/yek_etavirp']
self.run_commands.side_effect = [(0, '', ''), (1, '', '')]
create_jks(module, "test", "openssl", "keytool", "/path/to/keystore.jks", "changeit", "")
module.fail_json.assert_called_once_with(
cmd=["keytool", "-importkeystore",
"-destkeystore", "/path/to/keystore.jks",
"-srckeystore", "/tmp/keystore.p12", "-srcstoretype", "pkcs12", "-alias", "test",
"-deststorepass", "changeit", "-srcstorepass", "changeit", "-noprompt"],
"-srckeystore", "/tmp/tmpgrzm2ah7", "-srcstoretype", "pkcs12", "-alias", "test",
"-deststorepass:env", "STOREPASS", "-srcstorepass:env", "STOREPASS", "-noprompt"],
msg='',
rc=1
)
@@ -174,8 +185,7 @@ class TestCertChanged(ModuleTestCase):
"""Setup."""
super(TestCertChanged, self).setUp()
self.spec = ArgumentSpec()
self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file',
side_effect=lambda path, content: path)
self.mock_create_file = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.create_file')
self.mock_run_commands = patch('ansible_collections.community.general.plugins.modules.system.java_keystore.run_commands')
self.run_commands = self.mock_run_commands.start()
self.create_file = self.mock_create_file.start()
@@ -201,6 +211,7 @@ class TestCertChanged(ModuleTestCase):
)
with patch('os.remove', return_value=True):
self.create_file.side_effect = ['/tmp/placeholder']
self.run_commands.side_effect = [(0, 'foo=abcd:1234:efgh', ''), (0, 'SHA256: abcd:1234:efgh', '')]
result = cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
self.assertFalse(result, 'Fingerprint is identical')
@@ -220,11 +231,12 @@ class TestCertChanged(ModuleTestCase):
)
with patch('os.remove', return_value=True):
self.create_file.side_effect = ['/tmp/placeholder']
self.run_commands.side_effect = [(0, 'foo=abcd:1234:efgh', ''), (0, 'SHA256: wxyz:9876:stuv', '')]
result = cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
self.assertTrue(result, 'Fingerprint mismatch')
def test_cert_changed_alias_does_not_exist(self):
def test_cert_changed_fail_alias_does_not_exist(self):
set_module_args(dict(
certificate='cert-foo',
private_key='private-foo',
@@ -238,11 +250,19 @@ class TestCertChanged(ModuleTestCase):
supports_check_mode=self.spec.supports_check_mode
)
module.fail_json = Mock()
with patch('os.remove', return_value=True):
self.create_file.side_effect = ['/tmp/placeholder']
self.run_commands.side_effect = [(0, 'foo=abcd:1234:efgh', ''),
(1, 'keytool error: java.lang.Exception: Alias <foo> does not exist', '')]
result = cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
self.assertTrue(result, 'Certificate does not exist')
cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
module.fail_json.assert_called_once_with(
cmd=["keytool", "-list", "-alias", "foo", "-keystore", "/path/to/keystore.jks", "-storepass:env", "STOREPASS", "-v"],
msg='keytool error: java.lang.Exception: Alias <foo> does not exist',
err='',
rc=1
)
def test_cert_changed_fail_read_cert(self):
set_module_args(dict(
@@ -261,10 +281,11 @@ class TestCertChanged(ModuleTestCase):
module.fail_json = Mock()
with patch('os.remove', return_value=True):
self.create_file.side_effect = ['/tmp/tmpdj6bvvme']
self.run_commands.side_effect = [(1, '', 'Oops'), (0, 'SHA256: wxyz:9876:stuv', '')]
cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
module.fail_json.assert_called_once_with(
cmd=["openssl", "x509", "-noout", "-in", "/tmp/foo.crt", "-fingerprint", "-sha256"],
cmd=["openssl", "x509", "-noout", "-in", "/tmp/tmpdj6bvvme", "-fingerprint", "-sha256"],
msg='',
err='Oops',
rc=1
@@ -287,10 +308,11 @@ class TestCertChanged(ModuleTestCase):
module.fail_json = Mock(return_value=True)
with patch('os.remove', return_value=True):
self.create_file.side_effect = ['/tmp/placeholder']
self.run_commands.side_effect = [(0, 'foo: wxyz:9876:stuv', ''), (1, '', 'Oops')]
cert_changed(module, "openssl", "keytool", "/path/to/keystore.jks", "changeit", 'foo')
module.fail_json.assert_called_with(
cmd=["keytool", "-list", "-alias", "foo", "-keystore", "/path/to/keystore.jks", "-storepass", "changeit", "-v"],
cmd=["keytool", "-list", "-alias", "foo", "-keystore", "/path/to/keystore.jks", "-storepass:env", "STOREPASS", "-v"],
msg='',
err='Oops',
rc=1