Compare commits

...

87 Commits
2.2.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
Felix Fontein
8c67a5bda9 Release 2.4.0. 2021-03-30 12:39:01 +02:00
patchback[bot]
4ae436a8cc Callback plugin: Azure Log Analytics (#2091) (#2133)
* adding plugins/callback/loganalytics.py

* * fixed sanity check issues
* adjusted documentation and license sections

* added changelogs fragment

* * added unit test
* documentation updated

* updated changelogs

* further docuement update

* minor fixes

* updated unittest

* suggested updates from community

* remove AnsibleError section

(cherry picked from commit 19db6f24f7)

Co-authored-by: zhcli <49675498+zhcli@users.noreply.github.com>
2021-03-30 12:37:52 +02:00
Felix Fontein
5f5c07a942 Add release summary. 2021-03-30 12:14:33 +02:00
patchback[bot]
1cef1359d0 git_config - fixed bug with scope file (#2125) (#2132)
* fixed bug

- scope file was not working
- added test to guarantee that behaviour
- marked integration test as destructive, because it overwrites ~/.gitconfig

* added changelog fragment

* Update tests/integration/targets/git_config/tasks/setup_no_value.yml

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

* Update tests/integration/targets/git_config/tasks/get_set_state_present_file.yml

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

* Update tests/integration/targets/git_config/tasks/get_set_state_present_file.yml

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

* Update tests/integration/targets/git_config/aliases

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

* Update changelogs/fragments/2125-git-config-scope-file.yml

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-30 09:20:04 +02:00
patchback[bot]
0d28bfb67e vdo: add force option (#2110) (#2123)
* vdo: add force option

* Add changelog

* Improve the diff the next time something is added :)

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

* Add warning text placeholder by felixfontein

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

* Add warning text

* Apply suggestion for warning text from rhawalsh

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

Co-authored-by: Amin Vakil <info@aminvakil.com>
2021-03-27 15:41:58 +01:00
patchback[bot]
ef304ed824 remove billdodd from team_redfish (#2118) (#2120)
(cherry picked from commit 0de196413f)

Co-authored-by: Bill Dodd <billdodd@gmail.com>
2021-03-26 20:08:05 +01:00
patchback[bot]
bf17f289b3 AZP: update default container version (#2112) (#2114)
(cherry picked from commit 0bc76c98b0)

Co-authored-by: Andrew Klychkov <aklychko@redhat.com>
2021-03-26 13:09:43 +01:00
patchback[bot]
0eff87d0be opennebula: add one_template module (#2046) (#2111)
* opennebula: add one_template module

A basic module for maintaining VM templates which should be flexible enough
for most needs ...

* fixup! opennebula: add one_template module

* fixup! fixup! opennebula: add one_template module

(cherry picked from commit cdc415ea1f)

Co-authored-by: Georg Gadinger <nilsding@nilsding.org>
2021-03-26 07:47:38 +01:00
Abhijeet Kasurde
f00fabfa48 Typo fix in changelog (#2030)
Signed-off-by: Abhijeet Kasurde <akasurde@redhat.com>
2021-03-26 07:24:37 +01:00
patchback[bot]
426cbafa06 ipa_service - Correct pluralisation of "hosts" in example (#2103) (#2105)
(cherry picked from commit 2558cd3f01)

Co-authored-by: Alex Willmer <al.willmer@cgi.com>
2021-03-26 07:09:53 +01:00
patchback[bot]
93fe1f9a3e Bugfix: Respect PATH env variable in zypper modules (#2094) (#2109)
* Bugfix: Respect PATH env variable in zypper modules

* Improve changelogs/fragments/2094-bugfix-respect-PATH-env-variable-in-zypper-modules.yaml

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

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

Co-authored-by: Stefan Richter <sealor@users.noreply.github.com>
2021-03-25 22:57:53 +01:00
patchback[bot]
4e944772d5 Updated vdo maintainer to rhawalsh. (#2102) (#2107)
bgurney-rh does not work with VDO projects anymore.  This change re-points
maintainer pings to rhawalsh instead.

(cherry picked from commit 62cd38a9a0)

Co-authored-by: Andy Walsh <33293922+rhawalsh@users.noreply.github.com>
2021-03-25 22:46:04 +01:00
patchback[bot]
50abeee579 Add a Pulp 2 related note to pulp_repo (#2096) (#2100)
(cherry picked from commit de8e2a83e2)

Co-authored-by: Matthias Dellweg <2500@gmx.de>
2021-03-24 18:13:04 +01:00
patchback[bot]
eccc8d88b6 Add support for sudo su - using password auth (#2054) (#2097)
* Add support for `sudo su -` using password auth

Allow users to run Ansible tasks through `sudo su -` using password auth

- Feature Pull Request

sudosu

So I have been using this at various customers for bootstrapping Ansible mostly.

Often you have an existing setup where there is a user that has root-access enabled through sudo, but only to run `su` to log using the user's password.
In these specific cases the root password is unique to the system and therefore not an easy way to automate bootstrapping.

Having a `sudo su -` become option **with password prompt** is not possible with the existing become methods (neither sudo nor su can be used) by abusing `become_exe` or `become_flags`.

This fixes ansible/ansible#12686

* Fix all reported issues

* Add unit tests

* Apply suggestions from code review

* Update plugins/become/sudosu.py

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

* Update tests/unit/plugins/become/test_sudosu.py

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

* Update tests/unit/plugins/become/test_sudosu.py

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

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

Co-authored-by: Dag Wieers <dag@wieers.com>
2021-03-24 17:48:53 +01:00
patchback[bot]
6d2d364a00 add new module xcc_redfish_command to manage Lenovo servers using Redfish APIs (#2007) (#2095)
* add new module xcc_redfish_command to manage Lenovo servers using Redfish APIs

* Update plugins/modules/remote_management/lenovoxcc/xcc_redfish_command.py

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

* fix some errors detected by ansible-test sanity

* end all descriptions (except short_description) with a period

* fix return definition problem and other errors detected by ansible-test sanity

* Always use true/false for booleans in YAML

* It is usually a good idea to leave away required: false

* fix errors detected by ansible-test sanity

* fix elements of command is not defined

* check whether resource_uri is specified for Raw commands

* if no Members property, return false; if empty array, return true

* get @odata.etag from patch body instead of getting again

* add request_body checking

* add unit test for the module

* fix errors detected by ansible-test sanity --test pep8

* update class name xcc_RedfishUtils to XCCRedfishUtils to follow convention; import AnsibleExitJson, AnsibleFailJson, ModuleTestCase, set_module_args, exit_json, fail_json from ansible_collections.community.general.tests.unit.plugins.modules.utils instead of inline them

* support using security token for auth

* fix line too long error

* As 2.3.0 got released yesterday, move to 2.4.0

* add maintainers for lenovoxcc

* update to make sure that it's sorted alphabetically

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

Co-authored-by: panyy3 <panyy3@lenovo.com>
2021-03-24 16:43:11 +01:00
patchback[bot]
e781dd3c9b fixed documentation for oneview modules (#2092) (#2093)
(cherry picked from commit 94cf07efbf)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-24 16:41:03 +01:00
Felix Fontein
362f899a99 Next expected release is 2.4.0. 2021-03-23 13:30:16 +01:00
Felix Fontein
b44f6b8114 Release 2.3.0. 2021-03-23 12:21:35 +01:00
patchback[bot]
53a145ecb0 Install collections in CI directly with git to work around the Galaxy CloudFlare PITA. (#2082) (#2086)
(cherry picked from commit 7fe9dd7a60)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-03-23 07:42:21 +01:00
patchback[bot]
b22b44088f Temporarily disable copr integration tests due to failures with remote repository. (#2083) (#2085)
(cherry picked from commit 09351d9010)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-03-23 07:17:20 +01:00
patchback[bot]
e0a1aa2f46 Fixed documentation (#2062) (#2081)
(cherry picked from commit 88994ef2b7)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-22 20:55:53 +01:00
patchback[bot]
53e7e48834 improve force_archive parameter documentation of archive module (#2052) (#2079)
* improve documentation for force_archive parameter

* add link to unarchive module

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

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

Co-authored-by: Triantafyllos <ttsak@hotmail.com>
2021-03-22 20:55:37 +01:00
Bill Dodd
62e3a2ed2f Add support for Redfish session create, delete, and authenticate (#2027) (#2053)
* Add support for Redfish session create, delete, and authenticate (#2027)

* Add support for Redfish session create and delete

* add changelog fragment

* Apply suggestions from code review

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

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

* fix inadvertant spaces around equals
2021-03-22 18:27:25 +01:00
Felix Fontein
ecede6ca99 Prepare 2.3.0 release. 2021-03-22 07:58:24 +01:00
patchback[bot]
e1ac1fa6db stacki_host - configured params to use fallback instead of default (#2072) (#2076)
* configuredd params to use fallback instead of default

* added changelog fragment

(cherry picked from commit 5fc56676c2)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-21 15:43:12 +01:00
patchback[bot]
81cef0bd05 New Filter plugin from_csv (#2037) (#2074)
* Added from_csv filter and integration tests

* Cleaning up whitespace

* Adding changelog fragment

* Updated changelog fragment name

* Removed temp fragment

* Refactoring csv functions Part 1

* Syncing refactored csv modules/filters

* Adding unit tests for csv Module_Util

* Updating changelog fragment

* Correcting whitespace in unit test

* Improving changelog fragment

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

* Update changelogs/fragments/2037-add-from-csv-filter.yml

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

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-03-21 13:56:32 +01:00
patchback[bot]
a2bb118e95 Add gandi_livedns module (#328) (#2070)
* Add gandi_livedns module

This module uses REST API to register, update and delete domain name
entries in Gandi DNS service (https://www.gandi.net/en/domain).

* Apply suggestions from code review

* Update plugins/module_utils/gandi_livedns_api.py

Co-authored-by: Gregory Thiemonge <greg@thiemonge.org>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 81f3ad45c9)

Co-authored-by: Gregory Thiemonge <44313235+gthiemonge@users.noreply.github.com>
2021-03-21 13:22:14 +01:00
patchback[bot]
bf9bcd9bb4 snmp_facts - added timeout and retries params to module (#2065) (#2073)
* added timeout and retries params to module

* added changelog fragment

* Update plugins/modules/net_tools/snmp_facts.py

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

* Update plugins/modules/net_tools/snmp_facts.py

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

* removed default for retries per suggestion in PR

* Update plugins/modules/net_tools/snmp_facts.py

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-21 11:52:40 +01:00
patchback[bot]
9bfd61e117 New module: Add Pritunl VPN user module (net_tools/pritunl/) (#803) (#2071)
(cherry picked from commit 68fc48cd1f)

Co-authored-by: Florian Dambrine <Lowess@users.noreply.github.com>
2021-03-21 11:46:33 +01:00
patchback[bot]
ca81a5cf2f ipa_sudorule add support for setting runasextusers (#2031) (#2068)
* Add support for setting runasextusers

* fix formatting

* add changelog fragment

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

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

* Update changelogs/fragments/2031-ipa_sudorule_add_runasextusers.yml

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

Co-authored-by: quasd <qquasd@gmail.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit ff9f98795e)

Co-authored-by: quasd <quasd@users.noreply.github.com>
2021-03-21 11:24:07 +01:00
patchback[bot]
853dd21eab archive - a first refactoring (#2061) (#2069)
* a first refactoring on archive

* added changelog fragment

* suggestion from PR

(cherry picked from commit 606eb0df15)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-21 11:23:55 +01:00
patchback[bot]
6f267d8f35 archive - created an integration test that archives broken links (#2063) (#2066)
* created an integration test that archives broken links

* sanity fix

(cherry picked from commit f5a9584ae6)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-21 10:37:43 +01:00
patchback[bot]
1f975eff56 Fix nios modules to work with ansible-core 2.11 (#2057) (#2059)
* Fix nios modules to work with ansible-core 2.11.

* Adjust tests.

(cherry picked from commit 24f8be834a)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-03-20 14:10:24 +01:00
patchback[bot]
0ca922248f Adding xmadsen and renxulei as Redfish maintainers (#2047) (#2056)
(cherry picked from commit a23fc67f1f)

Co-authored-by: Mike Raineri <mraineri@gmail.com>
2021-03-20 10:43:29 +01:00
patchback[bot]
ef7ade6a56 Adding purge parameter to proxmox for use with lxc delete requests (#2013) (#2050)
* added purge as optional module parameter

* Adding changelog fragment

* Adding version to documentation for purge

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

* Updating changelog

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

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

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-03-19 19:58:44 +01:00
patchback[bot]
d721283846 Fix IndexError in SetManagerNic (#2040) (#2049)
* fix IndexError in SetManagerNic

* add changelog fragment

(cherry picked from commit 0b2ebabd29)

Co-authored-by: Bill Dodd <billdodd@gmail.com>
2021-03-19 19:58:27 +01:00
patchback[bot]
af410f5572 update linode team (#2039) (#2043)
(cherry picked from commit 8225b745f3)

Co-authored-by: Charlie Kenney <Charlesc.kenney@gmail.com>
2021-03-19 07:43:52 +01:00
patchback[bot]
442dabbcc6 fix: scaleway inventory pagination (#2036) (#2042)
* fix: scaleway inventory pagination

* add changelog

* Update changelogs/fragments/2036-scaleway-inventory.yml

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

Co-authored-by: Antoine Barbare <abarbare@online.net>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit fe61be3e11)

Co-authored-by: abarbare <antoinebarbare@gmail.com>
2021-03-18 23:32:01 +01:00
patchback[bot]
bbb155409e Improvements and fixes to ModuleHelper, with (some) tests. (#2024) (#2034)
* Improvements and fixes to ModuleHelper, with (some) tests.

* added changelog fragment

* adjusted changelog frag - get_bin_path() handling is actually a bugfix

(cherry picked from commit 4fbef900e1)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-17 14:15:48 +01:00
patchback[bot]
a83556af80 allow passing the --allow-root flag to kibana_plugin module (#2014) (#2022)
* kibana_plugin module parameter force is a boolean

* allow passing the --allow-root flag to kibana_plugin module

* add changelog fragment for kibana_plugin --allow-root

Co-authored-by: Amin Vakil <info@aminvakil.com>
Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Amin Vakil <info@aminvakil.com>
Co-authored-by: Felix Fontein <felix@fontein.de>
(cherry picked from commit 3162ed6795)

Co-authored-by: dacodas <dacoda.strack@gmail.com>
2021-03-15 14:05:10 +01:00
patchback[bot]
13a5e5a1ba Adding tags as module parameter to proxmox_kvm (#2000) (#2023)
* Adding tags as module parameter

* Added changelog fragment

* Correcting typo in changelog fragment

* Correcting punctuation in docs

* Including version to tags parameter description

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

* Correct tag validation and parsing logic condition

Original test was for key and not value

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

* Improving usability with default null behavior

* Removing default case and related unneccessary complexity

* Display regex in tags description as code

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

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

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-03-15 14:05:02 +01:00
patchback[bot]
466bd89bd4 Tidy up sanity checks ignore lines modules (batch 8) (#2006) (#2019)
* fixed validation-modules for plugins/modules/cloud/smartos/smartos_image_info.py

* fixed validation-modules for plugins/modules/cloud/rackspace/rax_scaling_group.py

* fixed validation-modules for plugins/modules/cloud/rackspace/rax_cdb_user.py

* fixed validation-modules for plugins/modules/cloud/rackspace/rax.py

* Tidy up sanity checks ignore lines modules (batch 8)

* added changelog fragment

* rolled back removal of parameter from rax.py

(cherry picked from commit f8859af377)

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-14 11:55:07 +01:00
patchback[bot]
bd4d5fe9db More false-positives (not flagged by sanity tests yet). (#2010) (#2016)
(cherry picked from commit 49d9a257ef)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-03-13 13:48:18 +01:00
patchback[bot]
cf889faf42 Remove password requirement when creating lxc containers (#1999) (#2011)
* Removed requirement for password

* Updated documentation for password

* Adding changelog fragment

* Update changelogs/fragments/1999-proxmox-fix-issue-1955.yml

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

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

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-03-12 20:35:11 +01:00
patchback[bot]
ea313503dd Mark non-secret leaking module options with no_log=False (#2001) (#2005)
* Mark non-secret leaking module options with no_log=False.

* Add changelog fragment.

(cherry picked from commit 1ea080762b)

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-03-12 09:17:05 +01:00
patchback[bot]
57fa6526c4 Excluded qemu templates in pools (#1991) (#2003)
* Excluded qemu templates in pools

* Added changelog fragment

* Made check more robust

(cherry picked from commit 178209be27)

Co-authored-by: Jeffrey van Pelt <jeff@vanpelt.one>
2021-03-12 08:24:24 +01:00
patchback[bot]
ae4bee2627 jenkins_job - added validate_certs parameter, setting the PYTHONHTTPSVERIFY env var (#1977) (#1996)
* added validate_certs parameter, setting the PYTHONHTTPSVERIFY env var

* added changelog fragment

* Update plugins/modules/web_infrastructure/jenkins_job.py

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

* Update plugins/modules/web_infrastructure/jenkins_job.py

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

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

Co-authored-by: Alexei Znamensky <103110+russoz@users.noreply.github.com>
2021-03-12 07:25:35 +01:00
patchback[bot]
87000ae491 Allow tags strings containing commas in proxmox inventory plug-in (#1949) (#1998)
* Included explicit parsing for proxmox guest tags and updated corresponding unit test with tags key

* Including changelog fragment for PR 1949

* Removed ellipsis from test

Proxmox only permits periods when surrounded by alphanumeric characters

* Corrected punctuation for changelog entry

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

* Allowing tags string to contain commas

* Incorporated new parsed tags fact with bugfix

* Correcting whitespace issues

* Update changelogs/fragments/1949-proxmox-inventory-tags.yml

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

* Update plugins/inventory/proxmox.py

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

* Update changelogs/fragments/1949-proxmox-inventory-tags.yml

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

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

Co-authored-by: Ajpantuso <ajpantuso@gmail.com>
2021-03-12 07:25:18 +01:00
Felix Fontein
46e221cbc6 Next expected release is 2.3.0. 2021-03-08 13:23:12 +01:00
219 changed files with 11884 additions and 945 deletions

View File

@@ -36,7 +36,7 @@ variables:
resources:
containers:
- container: default
image: quay.io/ansible/azure-pipelines-test-container:1.8.0
image: quay.io/ansible/azure-pipelines-test-container:1.9.0
pool: Standard
@@ -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

16
.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/:
@@ -708,6 +714,8 @@ files:
labels: cisco
$modules/remote_management/ipmi/:
maintainers: bgaifullin cloudnull
$modules/remote_management/lenovoxcc/:
maintainers: panyy3 renxulei
$modules/remote_management/lxca/:
maintainers: navalkp prabhosa
$modules/remote_management/manageiq/:
@@ -728,7 +736,7 @@ files:
$modules/remote_management/oneview/oneview_fcoe_network.py:
maintainers: fgbulsoni
$modules/remote_management/redfish/:
maintainers: $team_redfish billdodd
maintainers: $team_redfish
ignore: jose-delarosa
$modules/remote_management/stacki/stacki_host.py:
maintainers: bsanders bbyhuy
@@ -919,7 +927,7 @@ files:
maintainers: ahtik ovcharenko pyykkis
labels: ufw
$modules/system/vdo.py:
maintainers: bgurney-rh
maintainers: rhawalsh
$modules/system/xfconf.py:
maintainers: russoz jbenden
labels: xfconf
@@ -1014,7 +1022,7 @@ macros:
team_ipa: Akasurde Nosmoht fxfitz
team_jboss: Wolfant jairojunior wbrefvem
team_keycloak: eikef ndclt
team_linode: InTheCloudDan decentral1se displague rmcintosh
team_linode: InTheCloudDan decentral1se displague rmcintosh Charliekenney23 LBGarber
team_macos: Akasurde kyleabenson martinm82 danieljaouen indrajitr
team_manageiq: abellotti cben gtanzillo yaacov zgalor dkorn evertmulder
team_netapp: amit0701 carchi8py hulquest lmprice lonico ndswartz schmots1
@@ -1022,7 +1030,7 @@ macros:
team_opennebula: ilicmilan meerkampdvv rsmontero xorel
team_oracle: manojmeda mross22 nalsaber
team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16
team_redfish: billdodd mraineri tomasg2012
team_redfish: mraineri tomasg2012 xmadsen renxulei
team_rhn: FlossWare alikins barnabycourt vritant
team_scaleway: QuentinBrosse abarbare jerome-quere kindermoumoute remyleone sieben
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l

View File

@@ -6,6 +6,219 @@ 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
======
Release Summary
---------------
Regular feature and bugfix release.
Minor Changes
-------------
- vdo - add ``force`` option (https://github.com/ansible-collections/community.general/issues/2101).
Bugfixes
--------
- git_config - fixed scope ``file`` behaviour and added integraton test for it (https://github.com/ansible-collections/community.general/issues/2117).
- zypper, zypper_repository - respect ``PATH`` environment variable when resolving zypper executable path (https://github.com/ansible-collections/community.general/pull/2094).
New Plugins
-----------
Become
~~~~~~
- sudosu - Run tasks using sudo su -
Callback
~~~~~~~~
- loganalytics - Posts task results to Azure Log Analytics
New Modules
-----------
Cloud
~~~~~
opennebula
^^^^^^^^^^
- one_template - Manages OpenNebula templates
Remote Management
~~~~~~~~~~~~~~~~~
lenovoxcc
^^^^^^^^^
- xcc_redfish_command - Manages Lenovo Out-Of-Band controllers using Redfish APIs
v2.3.0
======
Release Summary
---------------
Fixes compatibility issues with the latest ansible-core 2.11 beta, some more bugs, and contains several new features, modules and plugins.
Minor Changes
-------------
- archive - refactored some reused code out into a couple of functions (https://github.com/ansible-collections/community.general/pull/2061).
- csv module utils - new module_utils for shared functions between ``from_csv`` filter and ``read_csv`` module (https://github.com/ansible-collections/community.general/pull/2037).
- ipa_sudorule - add support for setting sudo runasuser (https://github.com/ansible-collections/community.general/pull/2031).
- jenkins_job - add a ``validate_certs`` parameter that allows disabling TLS/SSL certificate validation (https://github.com/ansible-collections/community.general/issues/255).
- kibana_plugin - add parameter for passing ``--allow-root`` flag to kibana and kibana-plugin commands (https://github.com/ansible-collections/community.general/pull/2014).
- proxmox - added ``purge`` module parameter for use when deleting lxc's with HA options (https://github.com/ansible-collections/community.general/pull/2013).
- proxmox inventory plugin - added ``tags_parsed`` fact containing tags parsed as a list (https://github.com/ansible-collections/community.general/pull/1949).
- proxmox_kvm - added new module parameter ``tags`` for use with PVE 6+ (https://github.com/ansible-collections/community.general/pull/2000).
- rax - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- rax_cdb_user - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- rax_scaling_group - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- read_csv - refactored read_csv module to use shared csv functions from csv module_utils (https://github.com/ansible-collections/community.general/pull/2037).
- redfish_* modules, redfish_utils module utils - add support for Redfish session create, delete, and authenticate (https://github.com/ansible-collections/community.general/issues/1975).
- snmp_facts - added parameters ``timeout`` and ``retries`` to module (https://github.com/ansible-collections/community.general/issues/980).
Bugfixes
--------
- Mark various module options with ``no_log=False`` which have a name that potentially could leak secrets, but which do not (https://github.com/ansible-collections/community.general/pull/2001).
- module_helper module utils - actually ignoring formatting of parameters with value ``None`` (https://github.com/ansible-collections/community.general/pull/2024).
- module_helper module utils - handling ``ModuleHelperException`` now properly calls ``fail_json()`` (https://github.com/ansible-collections/community.general/pull/2024).
- module_helper module utils - use the command name as-is in ``CmdMixin`` if it fails ``get_bin_path()`` - allowing full path names to be passed (https://github.com/ansible-collections/community.general/pull/2024).
- nios* modules - fix modules to work with ansible-core 2.11 (https://github.com/ansible-collections/community.general/pull/2057).
- proxmox - removed requirement that root password is provided when containter state is ``present`` (https://github.com/ansible-collections/community.general/pull/1999).
- proxmox inventory - exclude qemu templates from inclusion to the inventory via pools (https://github.com/ansible-collections/community.general/issues/1986, https://github.com/ansible-collections/community.general/pull/1991).
- proxmox inventory plugin - allowed proxomox tag string to contain commas when returned as fact (https://github.com/ansible-collections/community.general/pull/1949).
- redfish_config module, redfish_utils module utils - fix IndexError in ``SetManagerNic`` command (https://github.com/ansible-collections/community.general/issues/1692).
- scaleway inventory plugin - fix pagination on scaleway inventory plugin (https://github.com/ansible-collections/community.general/pull/2036).
- stacki_host - replaced ``default`` to environment variables with ``fallback`` to them (https://github.com/ansible-collections/community.general/pull/2072).
New Plugins
-----------
Filter
~~~~~~
- from_csv - Converts CSV text input into list of dicts
New Modules
-----------
Net Tools
~~~~~~~~~
- gandi_livedns - Manage Gandi LiveDNS records
pritunl
^^^^^^^
- pritunl_user - Manage Pritunl Users using the Pritunl API
- pritunl_user_info - List Pritunl Users using the Pritunl API
v2.2.0
======
@@ -369,7 +582,7 @@ Minor Changes
- The collection is now actively tested in CI with the latest Ansible 2.9 release.
- airbrake_deployment - add ``version`` param; clarified docs on ``revision`` param (https://github.com/ansible-collections/community.general/pull/583).
- apk - added ``no_cache`` option (https://github.com/ansible-collections/community.general/pull/548).
- archive - fix paramater types (https://github.com/ansible-collections/community.general/pull/1039).
- archive - fix parameter types (https://github.com/ansible-collections/community.general/pull/1039).
- cloudflare_dns - add support for environment variable ``CLOUDFLARE_TOKEN`` (https://github.com/ansible-collections/community.general/pull/1238).
- consul - added support for tcp checks (https://github.com/ansible-collections/community.general/issues/1128).
- datadog - mark ``notification_message`` as ``no_log`` (https://github.com/ansible-collections/community.general/pull/1338).
@@ -516,7 +729,7 @@ Breaking Changes / Porting Guide
If you use ansible-base 2.10 or newer and did not install Ansible 3.0.0, but installed (and/or upgraded) community.general manually, you need to make sure to also install ``community.postgresql`` if you are using any of the ``postgresql`` modules.
While ansible-base 2.10 or newer can use the redirects that community.general 2.0.0 adds, the collection they point to (community.postgresql) must be installed for them to work.
- The Google cloud inventory script ``gce.py`` has been migrated to the ``community.google`` collection. Install the ``community.google`` collection in order to continue using it.
- archive - remove path folder itself when ``remove`` paramater is true (https://github.com/ansible-collections/community.general/issues/1041).
- archive - remove path folder itself when ``remove`` parameter is true (https://github.com/ansible-collections/community.general/issues/1041).
- log_plays callback - add missing information to the logs generated by the callback plugin. This changes the log message format (https://github.com/ansible-collections/community.general/pull/442).
- passwordstore lookup plugin - now parsing a password store entry as YAML if possible, skipping the first line (which by convention only contains the password and nothing else). If it cannot be parsed as YAML, the old ``key: value`` parser will be used to process the entry. Can break backwards compatibility if YAML formatted code was parsed in a non-YAML interpreted way, e.g. ``foo: [bar, baz]`` will become a list with two elements in the new version, but a string ``'[bar, baz]'`` in the old (https://github.com/ansible-collections/community.general/issues/1673).
- pkgng - passing ``name: *`` with ``state: absent`` will no longer remove every installed package from the system. It is now a noop. (https://github.com/ansible-collections/community.general/pull/569).

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

@@ -145,7 +145,7 @@ releases:
- The Google cloud inventory script ``gce.py`` has been migrated to the ``community.google``
collection. Install the ``community.google`` collection in order to continue
using it.
- archive - remove path folder itself when ``remove`` paramater is true (https://github.com/ansible-collections/community.general/issues/1041).
- archive - remove path folder itself when ``remove`` parameter is true (https://github.com/ansible-collections/community.general/issues/1041).
- log_plays callback - add missing information to the logs generated by the
callback plugin. This changes the log message format (https://github.com/ansible-collections/community.general/pull/442).
- 'passwordstore lookup plugin - now parsing a password store entry as YAML
@@ -414,7 +414,7 @@ releases:
- airbrake_deployment - add ``version`` param; clarified docs on ``revision``
param (https://github.com/ansible-collections/community.general/pull/583).
- apk - added ``no_cache`` option (https://github.com/ansible-collections/community.general/pull/548).
- archive - fix paramater types (https://github.com/ansible-collections/community.general/pull/1039).
- archive - fix parameter types (https://github.com/ansible-collections/community.general/pull/1039).
- cloudflare_dns - add support for environment variable ``CLOUDFLARE_TOKEN``
(https://github.com/ansible-collections/community.general/pull/1238).
- consul - added support for tcp checks (https://github.com/ansible-collections/community.general/issues/1128).
@@ -1544,3 +1544,263 @@ releases:
name: version_sort
namespace: null
release_date: '2021-03-08'
2.3.0:
changes:
bugfixes:
- Mark various module options with ``no_log=False`` which have a name that potentially
could leak secrets, but which do not (https://github.com/ansible-collections/community.general/pull/2001).
- module_helper module utils - actually ignoring formatting of parameters with
value ``None`` (https://github.com/ansible-collections/community.general/pull/2024).
- module_helper module utils - handling ``ModuleHelperException`` now properly
calls ``fail_json()`` (https://github.com/ansible-collections/community.general/pull/2024).
- module_helper module utils - use the command name as-is in ``CmdMixin`` if
it fails ``get_bin_path()`` - allowing full path names to be passed (https://github.com/ansible-collections/community.general/pull/2024).
- nios* modules - fix modules to work with ansible-core 2.11 (https://github.com/ansible-collections/community.general/pull/2057).
- proxmox - removed requirement that root password is provided when containter
state is ``present`` (https://github.com/ansible-collections/community.general/pull/1999).
- proxmox inventory - exclude qemu templates from inclusion to the inventory
via pools (https://github.com/ansible-collections/community.general/issues/1986,
https://github.com/ansible-collections/community.general/pull/1991).
- proxmox inventory plugin - allowed proxomox tag string to contain commas when
returned as fact (https://github.com/ansible-collections/community.general/pull/1949).
- redfish_config module, redfish_utils module utils - fix IndexError in ``SetManagerNic``
command (https://github.com/ansible-collections/community.general/issues/1692).
- scaleway inventory plugin - fix pagination on scaleway inventory plugin (https://github.com/ansible-collections/community.general/pull/2036).
- stacki_host - replaced ``default`` to environment variables with ``fallback``
to them (https://github.com/ansible-collections/community.general/pull/2072).
minor_changes:
- archive - refactored some reused code out into a couple of functions (https://github.com/ansible-collections/community.general/pull/2061).
- csv module utils - new module_utils for shared functions between ``from_csv``
filter and ``read_csv`` module (https://github.com/ansible-collections/community.general/pull/2037).
- ipa_sudorule - add support for setting sudo runasuser (https://github.com/ansible-collections/community.general/pull/2031).
- jenkins_job - add a ``validate_certs`` parameter that allows disabling TLS/SSL
certificate validation (https://github.com/ansible-collections/community.general/issues/255).
- kibana_plugin - add parameter for passing ``--allow-root`` flag to kibana
and kibana-plugin commands (https://github.com/ansible-collections/community.general/pull/2014).
- proxmox - added ``purge`` module parameter for use when deleting lxc's with
HA options (https://github.com/ansible-collections/community.general/pull/2013).
- proxmox inventory plugin - added ``tags_parsed`` fact containing tags parsed
as a list (https://github.com/ansible-collections/community.general/pull/1949).
- proxmox_kvm - added new module parameter ``tags`` for use with PVE 6+ (https://github.com/ansible-collections/community.general/pull/2000).
- rax - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- rax_cdb_user - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- rax_scaling_group - elements of list parameters are now validated (https://github.com/ansible-collections/community.general/pull/2006).
- read_csv - refactored read_csv module to use shared csv functions from csv
module_utils (https://github.com/ansible-collections/community.general/pull/2037).
- redfish_* modules, redfish_utils module utils - add support for Redfish session
create, delete, and authenticate (https://github.com/ansible-collections/community.general/issues/1975).
- snmp_facts - added parameters ``timeout`` and ``retries`` to module (https://github.com/ansible-collections/community.general/issues/980).
release_summary: Fixes compatibility issues with the latest ansible-core 2.11
beta, some more bugs, and contains several new features, modules and plugins.
fragments:
- 1949-proxmox-inventory-tags.yml
- 1977-jenkinsjob-validate-certs.yml
- 1991-proxmox-inventory-fix-template-in-pool.yml
- 1999-proxmox-fix-issue-1955.yml
- 2.3.0.yml
- 2000-proxmox_kvm-tag-support.yml
- 2001-no_log-false.yml
- 2006-valmod-batch8.yml
- 2013-proxmox-purge-parameter.yml
- 2014-allow-root-for-kibana-plugin.yaml
- 2024-module-helper-fixes.yml
- 2027-add-redfish-session-create-delete-authenticate.yml
- 2031-ipa_sudorule_add_runasextusers.yml
- 2036-scaleway-inventory.yml
- 2037-add-from-csv-filter.yml
- 2040-fix-index-error-in-redfish-set-manager-nic.yml
- 2057-nios-devel.yml
- 2061-archive-refactor1.yml
- 2065-snmp-facts-timeout.yml
- 2072-stacki-host-params-fallback.yml
modules:
- description: Manage Gandi LiveDNS records
name: gandi_livedns
namespace: net_tools
- description: Manage Pritunl Users using the Pritunl API
name: pritunl_user
namespace: net_tools.pritunl
- description: List Pritunl Users using the Pritunl API
name: pritunl_user_info
namespace: net_tools.pritunl
plugins:
filter:
- description: Converts CSV text input into list of dicts
name: from_csv
namespace: null
release_date: '2021-03-23'
2.4.0:
changes:
bugfixes:
- git_config - fixed scope ``file`` behaviour and added integraton test for
it (https://github.com/ansible-collections/community.general/issues/2117).
- zypper, zypper_repository - respect ``PATH`` environment variable when resolving
zypper executable path (https://github.com/ansible-collections/community.general/pull/2094).
minor_changes:
- vdo - add ``force`` option (https://github.com/ansible-collections/community.general/issues/2101).
release_summary: Regular feature and bugfix release.
fragments:
- 2.4.0.yml
- 2094-bugfix-respect-PATH-env-variable-in-zypper-modules.yaml
- 2110-vdo-add_force_option.yaml
- 2125-git-config-scope-file.yml
modules:
- description: Manages OpenNebula templates
name: one_template
namespace: cloud.opennebula
- description: Manages Lenovo Out-Of-Band controllers using Redfish APIs
name: xcc_redfish_command
namespace: remote_management.lenovoxcc
plugins:
become:
- description: Run tasks using sudo su -
name: sudosu
namespace: null
callback:
- description: Posts task results to Azure Log Analytics
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.2.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

91
plugins/become/sudosu.py Normal file
View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, 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 = """
become: sudosu
short_description: Run tasks using sudo su -
description:
- This become plugins allows your remote/login user to execute commands as another user via the C(sudo) and C(su) utilities combined.
author:
- Dag Wieers (@dagwieers)
version_added: 2.4.0
options:
become_user:
description: User you 'become' to execute the task.
default: root
ini:
- section: privilege_escalation
key: become_user
- section: sudo_become_plugin
key: user
vars:
- name: ansible_become_user
- name: ansible_sudo_user
env:
- name: ANSIBLE_BECOME_USER
- name: ANSIBLE_SUDO_USER
become_flags:
description: Options to pass to C(sudo).
default: -H -S -n
ini:
- section: privilege_escalation
key: become_flags
- section: sudo_become_plugin
key: flags
vars:
- name: ansible_become_flags
- name: ansible_sudo_flags
env:
- name: ANSIBLE_BECOME_FLAGS
- name: ANSIBLE_SUDO_FLAGS
become_pass:
description: Password to pass to C(sudo).
required: false
vars:
- name: ansible_become_password
- name: ansible_become_pass
- name: ansible_sudo_pass
env:
- name: ANSIBLE_BECOME_PASS
- name: ANSIBLE_SUDO_PASS
ini:
- section: sudo_become_plugin
key: password
"""
from ansible.plugins.become import BecomeBase
class BecomeModule(BecomeBase):
name = 'community.general.sudosu'
# messages for detecting prompted password issues
fail = ('Sorry, try again.',)
missing = ('Sorry, a password is required to run sudo', 'sudo: a password is required')
def build_become_command(self, cmd, shell):
super(BecomeModule, self).build_become_command(cmd, shell)
if not cmd:
return cmd
becomecmd = 'sudo'
flags = self.get_option('become_flags') or ''
prompt = ''
if self.get_option('become_pass'):
self.prompt = '[sudo via ansible, key=%s] password:' % self._id
if flags: # this could be simplified, but kept as is for now for backwards string matching
flags = flags.replace('-n', '')
prompt = '-p "%s"' % (self.prompt)
user = self.get_option('become_user') or ''
if user:
user = '%s' % (user)
return ' '.join([becomecmd, flags, prompt, 'su -l', user, self._build_success_command(cmd, shell)])

View File

@@ -0,0 +1,234 @@
# 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 = '''
callback: loganalytics
type: aggregate
short_description: Posts task results to Azure Log Analytics
author: "Cyrus Li (@zhcli) <cyrus1006@gmail.com>"
description:
- This callback plugin will post task results in JSON formatted to an Azure Log Analytics workspace.
- Credits to authors of splunk callback plugin.
version_added: "2.4.0"
requirements:
- Whitelisting this callback plugin.
- An Azure log analytics work space has been established.
options:
workspace_id:
description: Workspace ID of the Azure log analytics workspace.
required: true
env:
- name: WORKSPACE_ID
ini:
- section: callback_loganalytics
key: workspace_id
shared_key:
description: Shared key to connect to Azure log analytics workspace.
required: true
env:
- name: WORKSPACE_SHARED_KEY
ini:
- section: callback_loganalytics
key: shared_key
'''
EXAMPLES = '''
examples: |
Whitelist the plugin in ansible.cfg:
[defaults]
callback_whitelist = community.general.loganalytics
Set the environment variable:
export WORKSPACE_ID=01234567-0123-0123-0123-01234567890a
export WORKSPACE_SHARED_KEY=dZD0kCbKl3ehZG6LHFMuhtE0yHiFCmetzFMc2u+roXIUQuatqU924SsAAAAPemhjbGlAemhjbGktTUJQAQIDBA==
Or configure the plugin in ansible.cfg in the callback_loganalytics block:
[callback_loganalytics]
workspace_id = 01234567-0123-0123-0123-01234567890a
shared_key = dZD0kCbKl3ehZG6LHFMuhtE0yHiFCmetzFMc2u+roXIUQuatqU924SsAAAAPemhjbGlAemhjbGktTUJQAQIDBA==
'''
import hashlib
import hmac
import base64
import logging
import json
import uuid
import socket
import getpass
from datetime import datetime
from os.path import basename
from ansible.module_utils.urls import open_url
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.plugins.callback import CallbackBase
class AzureLogAnalyticsSource(object):
def __init__(self):
self.ansible_check_mode = False
self.ansible_playbook = ""
self.ansible_version = ""
self.session = str(uuid.uuid4())
self.host = socket.gethostname()
self.user = getpass.getuser()
self.extra_vars = ""
def __build_signature(self, date, workspace_id, shared_key, content_length):
# Build authorisation signature for Azure log analytics API call
sigs = "POST\n{0}\napplication/json\nx-ms-date:{1}\n/api/logs".format(
str(content_length), date)
utf8_sigs = sigs.encode('utf-8')
decoded_shared_key = base64.b64decode(shared_key)
hmac_sha256_sigs = hmac.new(
decoded_shared_key, utf8_sigs, digestmod=hashlib.sha256).digest()
encoded_hash = base64.b64encode(hmac_sha256_sigs).decode('utf-8')
signature = "SharedKey {0}:{1}".format(workspace_id, encoded_hash)
return signature
def __build_workspace_url(self, workspace_id):
return "https://{0}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01".format(workspace_id)
def __rfc1123date(self):
return datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
def send_event(self, workspace_id, shared_key, state, result, runtime):
if result._task_fields['args'].get('_ansible_check_mode') is True:
self.ansible_check_mode = True
if result._task_fields['args'].get('_ansible_version'):
self.ansible_version = \
result._task_fields['args'].get('_ansible_version')
if result._task._role:
ansible_role = str(result._task._role)
else:
ansible_role = None
data = {}
data['uuid'] = result._task._uuid
data['session'] = self.session
data['status'] = state
data['timestamp'] = self.__rfc1123date()
data['host'] = self.host
data['user'] = self.user
data['runtime'] = runtime
data['ansible_version'] = self.ansible_version
data['ansible_check_mode'] = self.ansible_check_mode
data['ansible_host'] = result._host.name
data['ansible_playbook'] = self.ansible_playbook
data['ansible_role'] = ansible_role
data['ansible_task'] = result._task_fields
# Removing args since it can contain sensitive data
if 'args' in data['ansible_task']:
data['ansible_task'].pop('args')
data['ansible_result'] = result._result
if 'content' in data['ansible_result']:
data['ansible_result'].pop('content')
# Adding extra vars info
data['extra_vars'] = self.extra_vars
# Preparing the playbook logs as JSON format and send to Azure log analytics
jsondata = json.dumps({'event': data}, cls=AnsibleJSONEncoder, sort_keys=True)
content_length = len(jsondata)
rfc1123date = self.__rfc1123date()
signature = self.__build_signature(rfc1123date, workspace_id, shared_key, content_length)
workspace_url = self.__build_workspace_url(workspace_id)
open_url(
workspace_url,
jsondata,
headers={
'content-type': 'application/json',
'Authorization': signature,
'Log-Type': 'ansible_playbook',
'x-ms-date': rfc1123date
},
method='POST'
)
class CallbackModule(CallbackBase):
CALLBACK_VERSION = 2.0
CALLBACK_TYPE = 'aggregate'
CALLBACK_NAME = 'loganalytics'
CALLBACK_NEEDS_WHITELIST = True
def __init__(self, display=None):
super(CallbackModule, self).__init__(display=display)
self.start_datetimes = {} # Collect task start times
self.workspace_id = None
self.shared_key = None
self.loganalytics = AzureLogAnalyticsSource()
def _seconds_since_start(self, result):
return (
datetime.utcnow() -
self.start_datetimes[result._task._uuid]
).total_seconds()
def set_options(self, task_keys=None, var_options=None, direct=None):
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
self.workspace_id = self.get_option('workspace_id')
self.shared_key = self.get_option('shared_key')
def v2_playbook_on_play_start(self, play):
vm = play.get_variable_manager()
extra_vars = vm.extra_vars
self.loganalytics.extra_vars = extra_vars
def v2_playbook_on_start(self, playbook):
self.loganalytics.ansible_playbook = basename(playbook._file_name)
def v2_playbook_on_task_start(self, task, is_conditional):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_playbook_on_handler_task_start(self, task):
self.start_datetimes[task._uuid] = datetime.utcnow()
def v2_runner_on_ok(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'OK',
result,
self._seconds_since_start(result)
)
def v2_runner_on_skipped(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'SKIPPED',
result,
self._seconds_since_start(result)
)
def v2_runner_on_failed(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'FAILED',
result,
self._seconds_since_start(result)
)
def runner_on_async_failed(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'FAILED',
result,
self._seconds_since_start(result)
)
def v2_runner_on_unreachable(self, result, **kwargs):
self.loganalytics.send_event(
self.workspace_id,
self.shared_key,
'UNREACHABLE',
result,
self._seconds_since_start(result)
)

View File

@@ -13,12 +13,32 @@ class ModuleDocFragment(object):
DOCUMENTATION = r'''
options:
config:
description:
description:
- Path to a .json configuration file containing the OneView client configuration.
The configuration file is optional and when used should be present in the host running the ansible commands.
If the file path is not provided, the configuration will be loaded from environment variables.
For links to example configuration files or how to use the environment variables verify the notes section.
type: path
type: path
api_version:
description:
- OneView API Version.
type: int
image_streamer_hostname:
description:
- IP address or hostname for the HPE Image Streamer REST API.
type: str
hostname:
description:
- IP address or hostname for the appliance.
type: str
username:
description:
- Username for API authentication.
type: str
password:
description:
- Password for API authentication.
type: str
requirements:
- python >= 2.7.9

View File

@@ -0,0 +1,43 @@
# -*- 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
class ModuleDocFragment(object):
DOCUMENTATION = r"""
options:
pritunl_url:
type: str
required: true
description:
- URL and port of the Pritunl server on which the API is enabled.
pritunl_api_token:
type: str
required: true
description:
- API Token of a Pritunl admin user.
- It needs to be enabled in Administrators > USERNAME > Enable Token Authentication.
pritunl_api_secret:
type: str
required: true
description:
- API Secret found in Administrators > USERNAME > API Secret.
validate_certs:
type: bool
required: false
default: true
description:
- If certificates should be validated or not.
- This should never be set to C(false), except if you are very sure that
your connection to the server can not be subject to a Man In The Middle
attack.
"""

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,49 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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
from ansible.errors import AnsibleFilterError
from ansible.module_utils._text import to_native
from ansible_collections.community.general.plugins.module_utils.csv import (initialize_dialect, read_csv, CSVError,
DialectNotAvailableError,
CustomDialectFailureError)
def from_csv(data, dialect='excel', fieldnames=None, delimiter=None, skipinitialspace=None, strict=None):
dialect_params = {
"delimiter": delimiter,
"skipinitialspace": skipinitialspace,
"strict": strict,
}
try:
dialect = initialize_dialect(dialect, **dialect_params)
except (CustomDialectFailureError, DialectNotAvailableError) as e:
raise AnsibleFilterError(to_native(e))
reader = read_csv(data, dialect, fieldnames)
data_list = []
try:
for row in reader:
data_list.append(row)
except CSVError as e:
raise AnsibleFilterError("Unable to process file: %s" % to_native(e))
return data_list
class FilterModule(object):
def filters(self):
return {
'from_csv': from_csv
}

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)
@@ -217,6 +262,10 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
vmtype_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), vmtype_key.lower()))
self.inventory.set_variable(name, vmtype_key, vmtype)
plaintext_configs = [
'tags',
]
for config in ret:
key = config
key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), key.lower()))
@@ -226,6 +275,18 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
if config == 'rootfs' or config.startswith(('virtio', 'sata', 'ide', 'scsi')):
value = ('disk_image=' + value)
# Additional field containing parsed tags as list
if config == 'tags':
parsed_key = self.to_safe('%s%s' % (key, "_parsed"))
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
@@ -254,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()
@@ -308,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)
@@ -330,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'):
@@ -339,7 +410,8 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
for member in self._get_members_per_pool(pool['poolid']):
if member.get('name'):
self.inventory.add_child(pool_group, member['name'])
if not member.get('template'):
self.inventory.add_child(pool_group, member['name'])
def parse(self, inventory, loader, path, cache=True):
if not HAS_REQUESTS:

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

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
# Copyright: (c) 2018, Dag Wieers (@dagwieers) <dag@wieers.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
import csv
from io import BytesIO, StringIO
from ansible.module_utils._text import to_native
from ansible.module_utils.six import PY3
class CustomDialectFailureError(Exception):
pass
class DialectNotAvailableError(Exception):
pass
CSVError = csv.Error
def initialize_dialect(dialect, **kwargs):
# Add Unix dialect from Python 3
class unix_dialect(csv.Dialect):
"""Describe the usual properties of Unix-generated CSV files."""
delimiter = ','
quotechar = '"'
doublequote = True
skipinitialspace = False
lineterminator = '\n'
quoting = csv.QUOTE_ALL
csv.register_dialect("unix", unix_dialect)
if dialect not in csv.list_dialects():
raise DialectNotAvailableError("Dialect '%s' is not supported by your version of python." % dialect)
# Create a dictionary from only set options
dialect_params = dict((k, v) for k, v in kwargs.items() if v is not None)
if dialect_params:
try:
csv.register_dialect('custom', dialect, **dialect_params)
except TypeError as e:
raise CustomDialectFailureError("Unable to create custom dialect: %s" % to_native(e))
dialect = 'custom'
return dialect
def read_csv(data, dialect, fieldnames=None):
data = to_native(data, errors='surrogate_or_strict')
if PY3:
fake_fh = StringIO(data)
else:
fake_fh = BytesIO(data)
reader = csv.DictReader(fake_fh, fieldnames=fieldnames, dialect=dialect)
return reader

View File

@@ -0,0 +1,234 @@
# -*- coding: utf-8 -*-
# Copyright: (c) 2019 Gregory Thiemonge <gregory.thiemonge@gmail.com>
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import json
from ansible.module_utils._text import to_native, to_text
from ansible.module_utils.urls import fetch_url
class GandiLiveDNSAPI(object):
api_endpoint = 'https://api.gandi.net/v5/livedns'
changed = False
error_strings = {
400: 'Bad request',
401: 'Permission denied',
404: 'Resource not found',
}
attribute_map = {
'record': 'rrset_name',
'type': 'rrset_type',
'ttl': 'rrset_ttl',
'values': 'rrset_values'
}
def __init__(self, module):
self.module = module
self.api_key = module.params['api_key']
def _build_error_message(self, module, info):
s = ''
body = info.get('body')
if body:
errors = module.from_json(body).get('errors')
if errors:
error = errors[0]
name = error.get('name')
if name:
s += '{0} :'.format(name)
description = error.get('description')
if description:
s += description
return s
def _gandi_api_call(self, api_call, method='GET', payload=None, error_on_404=True):
headers = {'Authorization': 'Apikey {0}'.format(self.api_key),
'Content-Type': 'application/json'}
data = None
if payload:
try:
data = json.dumps(payload)
except Exception as e:
self.module.fail_json(msg="Failed to encode payload as JSON: %s " % to_native(e))
resp, info = fetch_url(self.module,
self.api_endpoint + api_call,
headers=headers,
data=data,
method=method)
error_msg = ''
if info['status'] >= 400 and (info['status'] != 404 or error_on_404):
err_s = self.error_strings.get(info['status'], '')
error_msg = "API Error {0}: {1}".format(err_s, self._build_error_message(self.module, info))
result = None
try:
content = resp.read()
except AttributeError:
content = None
if content:
try:
result = json.loads(to_text(content, errors='surrogate_or_strict'))
except (getattr(json, 'JSONDecodeError', ValueError)) as e:
error_msg += "; Failed to parse API response with error {0}: {1}".format(to_native(e), content)
if error_msg:
self.module.fail_json(msg=error_msg)
return result, info['status']
def build_result(self, result, domain):
if result is None:
return None
res = {}
for k in self.attribute_map:
v = result.get(self.attribute_map[k], None)
if v is not None:
if k == 'record' and v == '@':
v = ''
res[k] = v
res['domain'] = domain
return res
def build_results(self, results, domain):
if results is None:
return []
return [self.build_result(r, domain) for r in results]
def get_records(self, record, type, domain):
url = '/domains/%s/records' % (domain)
if record:
url += '/%s' % (record)
if type:
url += '/%s' % (type)
records, status = self._gandi_api_call(url, error_on_404=False)
if status == 404:
return []
if not isinstance(records, list):
records = [records]
# filter by type if record is not set
if not record and type:
records = [r
for r in records
if r['rrset_type'] == type]
return records
def create_record(self, record, type, values, ttl, domain):
url = '/domains/%s/records' % (domain)
new_record = {
'rrset_name': record,
'rrset_type': type,
'rrset_values': values,
'rrset_ttl': ttl,
}
record, status = self._gandi_api_call(url, method='POST', payload=new_record)
if status in (200, 201,):
return new_record
return None
def update_record(self, record, type, values, ttl, domain):
url = '/domains/%s/records/%s/%s' % (domain, record, type)
new_record = {
'rrset_values': values,
'rrset_ttl': ttl,
}
record = self._gandi_api_call(url, method='PUT', payload=new_record)[0]
return record
def delete_record(self, record, type, domain):
url = '/domains/%s/records/%s/%s' % (domain, record, type)
self._gandi_api_call(url, method='DELETE')
def delete_dns_record(self, record, type, values, domain):
if record == '':
record = '@'
records = self.get_records(record, type, domain)
if records:
cur_record = records[0]
self.changed = True
if values is not None and set(cur_record['rrset_values']) != set(values):
new_values = set(cur_record['rrset_values']) - set(values)
if new_values:
# Removing one or more values from a record, we update the record with the remaining values
self.update_record(record, type, list(new_values), cur_record['rrset_ttl'], domain)
records = self.get_records(record, type, domain)
return records[0], self.changed
if not self.module.check_mode:
self.delete_record(record, type, domain)
else:
cur_record = None
return None, self.changed
def ensure_dns_record(self, record, type, ttl, values, domain):
if record == '':
record = '@'
records = self.get_records(record, type, domain)
if records:
cur_record = records[0]
do_update = False
if ttl is not None and cur_record['rrset_ttl'] != ttl:
do_update = True
if values is not None and set(cur_record['rrset_values']) != set(values):
do_update = True
if do_update:
if self.module.check_mode:
result = dict(
rrset_type=type,
rrset_name=record,
rrset_values=values,
rrset_ttl=ttl
)
else:
self.update_record(record, type, values, ttl, domain)
records = self.get_records(record, type, domain)
result = records[0]
self.changed = True
return result, self.changed
else:
return cur_record, self.changed
if self.module.check_mode:
new_record = dict(
rrset_type=type,
rrset_name=record,
rrset_values=values,
rrset_ttl=ttl
)
result = new_record
else:
result = self.create_record(record, type, values, ttl, domain)
self.changed = True
return result, self.changed

View File

@@ -55,7 +55,7 @@ def keycloak_argument_spec():
:return: argument_spec dict
"""
return dict(
auth_keycloak_url=dict(type='str', aliases=['url'], required=True),
auth_keycloak_url=dict(type='str', aliases=['url'], required=True, no_log=False),
auth_client_id=dict(type='str', default='admin-cli'),
auth_realm=dict(type='str', required=True),
auth_client_secret=dict(type='str', default=None, no_log=True),

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.
@@ -93,22 +95,33 @@ class ArgFormat(object):
self.arg_format = (self.stars_deco(stars))(self.arg_format)
def to_text(self, value):
if value is None:
return []
func = self.arg_format
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):
@@ -121,10 +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(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
@@ -138,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
@@ -152,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
@@ -188,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
@@ -196,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
@@ -210,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):
@@ -221,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):
@@ -292,7 +448,10 @@ class CmdMixin(object):
extra_params = extra_params or dict()
cmd_args = list([self.command]) if isinstance(self.command, str) else list(self.command)
cmd_args[0] = self.module.get_bin_path(cmd_args[0])
try:
cmd_args[0] = self.module.get_bin_path(cmd_args[0], required=True)
except ValueError:
pass
param_list = params if params else self.module.params.keys()
for param in param_list:
@@ -326,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)
@@ -335,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

@@ -18,6 +18,7 @@ from ansible.module_utils._text import to_native
from ansible.module_utils.six import iteritems
from ansible.module_utils._text import to_text
from ansible.module_utils.basic import env_fallback
from ansible.module_utils.common.validation import check_type_dict
try:
from infoblox_client.connector import Connector
@@ -399,11 +400,11 @@ class WapiModule(WapiBase):
if 'ipv4addrs' in proposed_object:
if 'nios_next_ip' in proposed_object['ipv4addrs'][0]['ipv4addr']:
ip_range = self.module._check_type_dict(proposed_object['ipv4addrs'][0]['ipv4addr'])['nios_next_ip']
ip_range = check_type_dict(proposed_object['ipv4addrs'][0]['ipv4addr'])['nios_next_ip']
proposed_object['ipv4addrs'][0]['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
elif 'ipv4addr' in proposed_object:
if 'nios_next_ip' in proposed_object['ipv4addr']:
ip_range = self.module._check_type_dict(proposed_object['ipv4addr'])['nios_next_ip']
ip_range = check_type_dict(proposed_object['ipv4addr'])['nios_next_ip']
proposed_object['ipv4addr'] = NIOS_NEXT_AVAILABLE_IP + ':' + ip_range
return proposed_object
@@ -485,7 +486,7 @@ class WapiModule(WapiBase):
if ('name' in obj_filter):
# gets and returns the current object based on name/old_name passed
try:
name_obj = self.module._check_type_dict(obj_filter['name'])
name_obj = check_type_dict(obj_filter['name'])
old_name = name_obj['old_name']
new_name = name_obj['new_name']
except TypeError:
@@ -521,7 +522,7 @@ class WapiModule(WapiBase):
test_obj_filter['name'] = test_obj_filter['name'].lower()
# resolves issue where multiple a_records with same name and different IP address
try:
ipaddr_obj = self.module._check_type_dict(obj_filter['ipv4addr'])
ipaddr_obj = check_type_dict(obj_filter['ipv4addr'])
ipaddr = ipaddr_obj['old_ipv4addr']
except TypeError:
ipaddr = obj_filter['ipv4addr']
@@ -530,7 +531,7 @@ class WapiModule(WapiBase):
# resolves issue where multiple txt_records with same name and different text
test_obj_filter = obj_filter
try:
text_obj = self.module._check_type_dict(obj_filter['text'])
text_obj = check_type_dict(obj_filter['text'])
txt = text_obj['old_text']
except TypeError:
txt = obj_filter['text']
@@ -543,7 +544,7 @@ class WapiModule(WapiBase):
# resolves issue where multiple a_records with same name and different IP address
test_obj_filter = obj_filter
try:
ipaddr_obj = self.module._check_type_dict(obj_filter['ipv4addr'])
ipaddr_obj = check_type_dict(obj_filter['ipv4addr'])
ipaddr = ipaddr_obj['old_ipv4addr']
except TypeError:
ipaddr = obj_filter['ipv4addr']
@@ -553,7 +554,7 @@ class WapiModule(WapiBase):
# resolves issue where multiple txt_records with same name and different text
test_obj_filter = obj_filter
try:
text_obj = self.module._check_type_dict(obj_filter['text'])
text_obj = check_type_dict(obj_filter['text'])
txt = text_obj['old_text']
except TypeError:
txt = obj_filter['text']

View File

@@ -0,0 +1,370 @@
# -*- 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)
"""
Pritunl API that offers CRUD operations on Pritunl Organizations and Users
"""
from __future__ import absolute_import, division, print_function
import base64
import hashlib
import hmac
import json
import time
import uuid
from ansible.module_utils.six import iteritems
from ansible.module_utils.urls import open_url
__metaclass__ = type
class PritunlException(Exception):
pass
def pritunl_argument_spec():
return dict(
pritunl_url=dict(required=True, type="str"),
pritunl_api_token=dict(required=True, type="str", no_log=False),
pritunl_api_secret=dict(required=True, type="str", no_log=True),
validate_certs=dict(required=False, type="bool", default=True),
)
def get_pritunl_settings(module):
"""
Helper function to set required Pritunl request params from module arguments.
"""
return {
"api_token": module.params.get("pritunl_api_token"),
"api_secret": module.params.get("pritunl_api_secret"),
"base_url": module.params.get("pritunl_url"),
"validate_certs": module.params.get("validate_certs"),
}
def _get_pritunl_organizations(api_token, api_secret, base_url, validate_certs=True):
return pritunl_auth_request(
base_url=base_url,
api_token=api_token,
api_secret=api_secret,
method="GET",
path="/organization",
validate_certs=validate_certs,
)
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
):
return pritunl_auth_request(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
method="GET",
path="/user/%s" % organization_id,
validate_certs=validate_certs,
)
def _delete_pritunl_user(
api_token, api_secret, base_url, organization_id, user_id, validate_certs=True
):
return pritunl_auth_request(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
method="DELETE",
path="/user/%s/%s" % (organization_id, user_id),
validate_certs=validate_certs,
)
def _post_pritunl_user(
api_token, api_secret, base_url, organization_id, user_data, validate_certs=True
):
return pritunl_auth_request(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
method="POST",
path="/user/%s" % organization_id,
headers={"Content-Type": "application/json"},
data=json.dumps(user_data),
validate_certs=validate_certs,
)
def _put_pritunl_user(
api_token,
api_secret,
base_url,
organization_id,
user_id,
user_data,
validate_certs=True,
):
return pritunl_auth_request(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
method="PUT",
path="/user/%s/%s" % (organization_id, user_id),
headers={"Content-Type": "application/json"},
data=json.dumps(user_data),
validate_certs=validate_certs,
)
def list_pritunl_organizations(
api_token, api_secret, base_url, validate_certs=True, filters=None
):
orgs = []
response = _get_pritunl_organizations(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
validate_certs=validate_certs,
)
if response.getcode() != 200:
raise PritunlException("Could not retrieve organizations from Pritunl")
else:
for org in json.loads(response.read()):
# No filtering
if filters is None:
orgs.append(org)
else:
if not any(
filter_val != org[filter_key]
for filter_key, filter_val in iteritems(filters)
):
orgs.append(org)
return orgs
def list_pritunl_users(
api_token, api_secret, base_url, organization_id, validate_certs=True, filters=None
):
users = []
response = _get_pritunl_users(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
validate_certs=validate_certs,
organization_id=organization_id,
)
if response.getcode() != 200:
raise PritunlException("Could not retrieve users from Pritunl")
else:
for user in json.loads(response.read()):
# No filtering
if filters is None:
users.append(user)
else:
if not any(
filter_val != user[filter_key]
for filter_key, filter_val in iteritems(filters)
):
users.append(user)
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,
base_url,
organization_id,
user_data,
user_id=None,
validate_certs=True,
):
# If user_id is provided will do PUT otherwise will do POST
if user_id is None:
response = _post_pritunl_user(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
organization_id=organization_id,
user_data=user_data,
validate_certs=True,
)
if response.getcode() != 200:
raise PritunlException(
"Could not remove user %s from organization %s from Pritunl"
% (user_id, organization_id)
)
# user POST request returns an array of a single item,
# so return this item instead of the list
return json.loads(response.read())[0]
else:
response = _put_pritunl_user(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
organization_id=organization_id,
user_data=user_data,
user_id=user_id,
validate_certs=True,
)
if response.getcode() != 200:
raise PritunlException(
"Could not update user %s from organization %s from Pritunl"
% (user_id, organization_id)
)
# The user PUT request returns the updated user object
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
):
response = _delete_pritunl_user(
api_token=api_token,
api_secret=api_secret,
base_url=base_url,
organization_id=organization_id,
user_id=user_id,
validate_certs=True,
)
if response.getcode() != 200:
raise PritunlException(
"Could not remove user %s from organization %s from Pritunl"
% (user_id, organization_id)
)
return json.loads(response.read())
def pritunl_auth_request(
api_token,
api_secret,
base_url,
method,
path,
validate_certs=True,
headers=None,
data=None,
):
"""
Send an API call to a Pritunl server.
Taken from https://pritunl.com/api and adaped work with Ansible open_url
"""
auth_timestamp = str(int(time.time()))
auth_nonce = uuid.uuid4().hex
auth_string = "&".join(
[api_token, auth_timestamp, auth_nonce, method.upper(), path]
+ ([data] if data else [])
)
auth_signature = base64.b64encode(
hmac.new(
api_secret.encode("utf-8"), auth_string.encode("utf-8"), hashlib.sha256
).digest()
)
auth_headers = {
"Auth-Token": api_token,
"Auth-Timestamp": auth_timestamp,
"Auth-Nonce": auth_nonce,
"Auth-Signature": auth_signature,
}
if headers:
auth_headers.update(headers)
try:
uri = "%s%s" % (base_url, path)
return open_url(
uri,
method=method.upper(),
headers=auth_headers,
data=data,
validate_certs=validate_certs,
)
except Exception as e:
raise PritunlException(e)

View File

@@ -39,14 +39,16 @@ class OpenNebulaModule:
wait_timeout=dict(type='int', default=300),
)
def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None):
def __init__(self, argument_spec, supports_check_mode=False, mutually_exclusive=None, required_one_of=None, required_if=None):
module_args = OpenNebulaModule.common_args
module_args = OpenNebulaModule.common_args.copy()
module_args.update(argument_spec)
self.module = AnsibleModule(argument_spec=module_args,
supports_check_mode=supports_check_mode,
mutually_exclusive=mutually_exclusive)
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of,
required_if=required_if)
self.result = dict(changed=False,
original_message='',
message='')

View File

@@ -104,7 +104,7 @@ def get_common_arg_spec(supports_create=False, supports_wait=False):
if supports_create:
common_args.update(
key_by=dict(type="list", elements="str"),
key_by=dict(type="list", elements="str", no_log=False),
force_create=dict(type="bool", default=False),
)

View File

@@ -39,13 +39,34 @@ class RedfishUtils(object):
self.data_modification = data_modification
self._init_session()
def _auth_params(self, headers):
"""
Return tuple of required authentication params based on the presence
of a token in the self.creds dict. If using a token, set the
X-Auth-Token header in the `headers` param.
:param headers: dict containing headers to send in request
:return: tuple of username, password and force_basic_auth
"""
if self.creds.get('token'):
username = None
password = None
force_basic_auth = False
headers['X-Auth-Token'] = self.creds['token']
else:
username = self.creds['user']
password = self.creds['pswd']
force_basic_auth = True
return username, password, force_basic_auth
# The following functions are to send GET/POST/PATCH/DELETE requests
def get_request(self, uri):
req_headers = dict(GET_HEADERS)
username, password, basic_auth = self._auth_params(req_headers)
try:
resp = open_url(uri, method="GET", headers=GET_HEADERS,
url_username=self.creds['user'],
url_password=self.creds['pswd'],
force_basic_auth=True, validate_certs=False,
resp = open_url(uri, method="GET", headers=req_headers,
url_username=username, url_password=password,
force_basic_auth=basic_auth, validate_certs=False,
follow_redirects='all',
use_proxy=True, timeout=self.timeout)
data = json.loads(to_native(resp.read()))
@@ -66,14 +87,16 @@ class RedfishUtils(object):
return {'ret': True, 'data': data, 'headers': headers}
def post_request(self, uri, pyld):
req_headers = dict(POST_HEADERS)
username, password, basic_auth = self._auth_params(req_headers)
try:
resp = open_url(uri, data=json.dumps(pyld),
headers=POST_HEADERS, method="POST",
url_username=self.creds['user'],
url_password=self.creds['pswd'],
force_basic_auth=True, validate_certs=False,
headers=req_headers, method="POST",
url_username=username, url_password=password,
force_basic_auth=basic_auth, validate_certs=False,
follow_redirects='all',
use_proxy=True, timeout=self.timeout)
headers = dict((k.lower(), v) for (k, v) in resp.info().items())
except HTTPError as e:
msg = self._get_extended_message(e)
return {'ret': False,
@@ -87,10 +110,10 @@ class RedfishUtils(object):
except Exception as e:
return {'ret': False,
'msg': "Failed POST request to '%s': '%s'" % (uri, to_text(e))}
return {'ret': True, 'resp': resp}
return {'ret': True, 'headers': headers, 'resp': resp}
def patch_request(self, uri, pyld):
headers = PATCH_HEADERS
req_headers = dict(PATCH_HEADERS)
r = self.get_request(uri)
if r['ret']:
# Get etag from etag header or @odata.etag property
@@ -98,15 +121,13 @@ class RedfishUtils(object):
if not etag:
etag = r['data'].get('@odata.etag')
if etag:
# Make copy of headers and add If-Match header
headers = dict(headers)
headers['If-Match'] = etag
req_headers['If-Match'] = etag
username, password, basic_auth = self._auth_params(req_headers)
try:
resp = open_url(uri, data=json.dumps(pyld),
headers=headers, method="PATCH",
url_username=self.creds['user'],
url_password=self.creds['pswd'],
force_basic_auth=True, validate_certs=False,
headers=req_headers, method="PATCH",
url_username=username, url_password=password,
force_basic_auth=basic_auth, validate_certs=False,
follow_redirects='all',
use_proxy=True, timeout=self.timeout)
except HTTPError as e:
@@ -125,13 +146,14 @@ class RedfishUtils(object):
return {'ret': True, 'resp': resp}
def delete_request(self, uri, pyld=None):
req_headers = dict(DELETE_HEADERS)
username, password, basic_auth = self._auth_params(req_headers)
try:
data = json.dumps(pyld) if pyld else None
resp = open_url(uri, data=data,
headers=DELETE_HEADERS, method="DELETE",
url_username=self.creds['user'],
url_password=self.creds['pswd'],
force_basic_auth=True, validate_certs=False,
headers=req_headers, method="DELETE",
url_username=username, url_password=password,
force_basic_auth=basic_auth, validate_certs=False,
follow_redirects='all',
use_proxy=True, timeout=self.timeout)
except HTTPError as e:
@@ -1196,6 +1218,54 @@ class RedfishUtils(object):
return {'ret': True, 'changed': True, 'msg': "Clear all sessions successfully"}
def create_session(self):
if not self.creds.get('user') or not self.creds.get('pswd'):
return {'ret': False, 'msg':
'Must provide the username and password parameters for '
'the CreateSession command'}
payload = {
'UserName': self.creds['user'],
'Password': self.creds['pswd']
}
response = self.post_request(self.root_uri + self.sessions_uri, payload)
if response['ret'] is False:
return response
headers = response['headers']
if 'x-auth-token' not in headers:
return {'ret': False, 'msg':
'The service did not return the X-Auth-Token header in '
'the response from the Sessions collection POST'}
if 'location' not in headers:
self.module.warn(
'The service did not return the Location header for the '
'session URL in the response from the Sessions collection '
'POST')
session_uri = None
else:
session_uri = urlparse(headers.get('location')).path
session = dict()
session['token'] = headers.get('x-auth-token')
session['uri'] = session_uri
return {'ret': True, 'changed': True, 'session': session,
'msg': 'Session created successfully'}
def delete_session(self, session_uri):
if not session_uri:
return {'ret': False, 'msg':
'Must provide the session_uri parameter for the '
'DeleteSession command'}
response = self.delete_request(self.root_uri + session_uri)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True,
'msg': 'Session deleted successfully'}
def get_firmware_update_capabilities(self):
result = {}
response = self.get_request(self.root_uri + self.update_uri)
@@ -2676,6 +2746,10 @@ class RedfishUtils(object):
need_change = True
# type is list
if isinstance(set_value, list):
if len(set_value) != len(cur_value):
# if arrays are not the same len, no need to check each element
need_change = True
continue
for i in range(len(set_value)):
for subprop in payload[property][i].keys():
if subprop not in target_ethernet_current_setting[property][i]:

View File

@@ -39,7 +39,7 @@ class ScalewayException(Exception):
R_LINK_HEADER = r'''<[^>]+>;\srel="(first|previous|next|last)"
(,<[^>]+>;\srel="(first|previous|next|last)")*'''
# Specify a single relation, for iteration and string extraction purposes
R_RELATION = r'<(?P<target_IRI>[^>]+)>; rel="(?P<relation>first|previous|next|last)"'
R_RELATION = r'</?(?P<target_IRI>[^>]+)>; rel="(?P<relation>first|previous|next|last)"'
def parse_pagination_link(header):

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

@@ -242,7 +242,7 @@ def initialise_module():
no_log=True,
fallback=(env_fallback, ['LINODE_ACCESS_TOKEN']),
),
authorized_keys=dict(type='list', elements='str', required=False),
authorized_keys=dict(type='list', elements='str', required=False, no_log=False),
group=dict(type='str', required=False),
image=dict(type='str', required=False),
region=dict(type='str', required=False),

View File

@@ -17,7 +17,6 @@ options:
password:
description:
- the instance root password
- required only for C(state=present)
type: str
hostname:
description:
@@ -124,6 +123,15 @@ options:
- with states C(stopped) , C(restarted) allow to force stop instance
type: bool
default: 'no'
purge:
description:
- Remove container from all related configurations.
- For example backup jobs, replication jobs, or HA.
- Related ACLs and Firewall entries will always be removed.
- Used with state C(absent).
type: bool
default: false
version_added: 2.3.0
state:
description:
- Indicate desired state of the instance
@@ -507,6 +515,7 @@ def main():
searchdomain=dict(),
timeout=dict(type='int', default=30),
force=dict(type='bool', default=False),
purge=dict(type='bool', default=False),
state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted']),
pubkey=dict(type='str', default=None),
unprivileged=dict(type='bool', default=False),
@@ -514,7 +523,7 @@ def main():
hookscript=dict(type='str'),
proxmox_default_behavior=dict(type='str', choices=['compatibility', 'no_defaults']),
),
required_if=[('state', 'present', ['node', 'hostname', 'password', 'ostemplate'])],
required_if=[('state', 'present', ['node', 'hostname', 'ostemplate'])],
required_together=[('api_token_id', 'api_token_secret')],
required_one_of=[('api_password', 'api_token_id')],
)
@@ -687,7 +696,13 @@ def main():
if getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE)(vmid).status.current.get()['status'] == 'mounted':
module.exit_json(changed=False, msg="VM %s is mounted. Stop it with force option before deletion." % vmid)
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid)
delete_params = {}
if module.params['purge']:
delete_params['purge'] = 1
taskid = getattr(proxmox.nodes(vm[0]['node']), VZ_TYPE).delete(vmid, **delete_params)
while timeout:
if (proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['status'] == 'stopped' and
proxmox.nodes(vm[0]['node']).tasks(taskid).status.get()['exitstatus'] == 'OK'):

View File

@@ -425,6 +425,14 @@ options:
option has a default of C(no). Note that the default value of I(proxmox_default_behavior)
changes in community.general 4.0.0.
type: bool
tags:
description:
- List of tags to apply to the VM instance.
- Tags must start with C([a-z0-9_]) followed by zero or more of the following characters C([a-z0-9_-+.]).
- Tags are only available in Proxmox 6+.
type: list
elements: str
version_added: 2.3.0
target:
description:
- Target node. Only allowed if the original VM is on shared storage.
@@ -858,7 +866,7 @@ def wait_for_task(module, proxmox, node, taskid):
def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sockets, update, **kwargs):
# Available only in PVE 4
only_v4 = ['force', 'protection', 'skiplock']
only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig']
only_v6 = ['ciuser', 'cipassword', 'sshkeys', 'ipconfig', 'tags']
# valide clone parameters
valid_clone_params = ['format', 'full', 'pool', 'snapname', 'storage', 'target']
@@ -928,6 +936,13 @@ def create_vm(module, proxmox, vmid, newid, node, name, memory, cpu, cores, sock
if searchdomains:
kwargs['searchdomain'] = ' '.join(searchdomains)
# VM tags are expected to be valid and presented as a comma/semi-colon delimited string
if 'tags' in kwargs:
for tag in kwargs['tags']:
if not re.match(r'^[a-z0-9_][a-z0-9_\-\+\.]*$', tag):
module.fail_json(msg='%s is not a valid tag' % tag)
kwargs['tags'] = ",".join(kwargs['tags'])
# -args and skiplock require root@pam user - but can not use api tokens
if module.params['api_user'] == "root@pam" and module.params['args'] is None:
if not update and module.params['proxmox_default_behavior'] == 'compatibility':
@@ -1057,12 +1072,13 @@ def main():
smbios=dict(type='str'),
snapname=dict(type='str'),
sockets=dict(type='int'),
sshkeys=dict(type='str'),
sshkeys=dict(type='str', no_log=False),
startdate=dict(type='str'),
startup=dict(),
state=dict(default='present', choices=['present', 'absent', 'stopped', 'started', 'restarted', 'current']),
storage=dict(type='str'),
tablet=dict(type='bool'),
tags=dict(type='list', elements='str'),
target=dict(type='str'),
tdf=dict(type='bool'),
template=dict(type='bool'),
@@ -1267,6 +1283,7 @@ def main():
startdate=module.params['startdate'],
startup=module.params['startup'],
tablet=module.params['tablet'],
tags=module.params['tags'],
target=module.params['target'],
tdf=module.params['tdf'],
template=module.params['template'],

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

@@ -630,7 +630,7 @@ def main():
ram=dict(type='float'),
hdds=dict(type='list', elements='dict'),
count=dict(type='int', default=1),
ssh_key=dict(type='raw'),
ssh_key=dict(type='raw', no_log=False),
auto_increment=dict(type='bool', default=True),
server=dict(type='str'),
datacenter=dict(

View File

@@ -0,0 +1,276 @@
#!/usr/bin/python
#
# Copyright: (c) 2021, Georg Gadinger <nilsding@nilsding.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = '''
---
module: one_template
short_description: Manages OpenNebula templates
version_added: 2.4.0
requirements:
- pyone
description:
- "Manages OpenNebula templates."
options:
id:
description:
- A I(id) of the template you would like to manage. If not set then a
- new template will be created with the given I(name).
type: int
name:
description:
- A I(name) of the template you would like to manage. If a template with
- the given name does not exist it will be created, otherwise it will be
- managed by this module.
type: str
template:
description:
- A string containing the template contents.
type: str
state:
description:
- C(present) - state that is used to manage the template.
- C(absent) - delete the template.
choices: ["present", "absent"]
default: present
type: str
notes:
- Supports C(check_mode). Note that check mode always returns C(changed=true) for existing templates, even if the template would not actually change.
extends_documentation_fragment:
- community.general.opennebula
author:
- "Georg Gadinger (@nilsding)"
'''
EXAMPLES = '''
- name: Fetch the TEMPLATE by id
community.general.one_template:
id: 6459
register: result
- name: Print the TEMPLATE properties
ansible.builtin.debug:
var: result
- name: Fetch the TEMPLATE by name
community.general.one_template:
name: tf-prd-users-workerredis-p6379a
register: result
- name: Create a new or update an existing TEMPLATE
community.general.one_template:
name: generic-opensuse
template: |
CONTEXT = [
HOSTNAME = "generic-opensuse"
]
CPU = "1"
CUSTOM_ATTRIBUTE = ""
DISK = [
CACHE = "writeback",
DEV_PREFIX = "sd",
DISCARD = "unmap",
IMAGE = "opensuse-leap-15.2",
IMAGE_UNAME = "oneadmin",
IO = "threads",
SIZE = "" ]
MEMORY = "2048"
NIC = [
MODEL = "virtio",
NETWORK = "testnet",
NETWORK_UNAME = "oneadmin" ]
OS = [
ARCH = "x86_64",
BOOT = "disk0" ]
SCHED_REQUIREMENTS = "CLUSTER_ID=\\"100\\""
VCPU = "2"
- name: Delete the TEMPLATE by id
community.general.one_template:
id: 6459
state: absent
'''
RETURN = '''
id:
description: template id
type: int
returned: when I(state=present)
sample: 153
name:
description: template name
type: str
returned: when I(state=present)
sample: app1
template:
description: the parsed template
type: dict
returned: when I(state=present)
group_id:
description: template's group id
type: int
returned: when I(state=present)
sample: 1
group_name:
description: template's group name
type: str
returned: when I(state=present)
sample: one-users
owner_id:
description: template's owner id
type: int
returned: when I(state=present)
sample: 143
owner_name:
description: template's owner name
type: str
returned: when I(state=present)
sample: ansible-test
'''
from ansible_collections.community.general.plugins.module_utils.opennebula import OpenNebulaModule
class TemplateModule(OpenNebulaModule):
def __init__(self):
argument_spec = dict(
id=dict(type='int', required=False),
name=dict(type='str', required=False),
state=dict(type='str', choices=['present', 'absent'], default='present'),
template=dict(type='str', required=False),
)
mutually_exclusive = [
['id', 'name']
]
required_one_of = [('id', 'name')]
required_if = [
['state', 'present', ['template']]
]
OpenNebulaModule.__init__(self,
argument_spec,
supports_check_mode=True,
mutually_exclusive=mutually_exclusive,
required_one_of=required_one_of,
required_if=required_if)
def run(self, one, module, result):
params = module.params
id = params.get('id')
name = params.get('name')
desired_state = params.get('state')
template_data = params.get('template')
self.result = {}
template = self.get_template_instance(id, name)
needs_creation = False
if not template and desired_state != 'absent':
if id:
module.fail_json(msg="There is no template with id=" + str(id))
else:
needs_creation = True
if desired_state == 'absent':
self.result = self.delete_template(template)
else:
if needs_creation:
self.result = self.create_template(name, template_data)
else:
self.result = self.update_template(template, template_data)
self.exit()
def get_template(self, predicate):
# -3 means "Resources belonging to the user"
# the other two parameters are used for pagination, -1 for both essentially means "return all"
pool = self.one.templatepool.info(-3, -1, -1)
for template in pool.VMTEMPLATE:
if predicate(template):
return template
return None
def get_template_by_id(self, template_id):
return self.get_template(lambda template: (template.ID == template_id))
def get_template_by_name(self, template_name):
return self.get_template(lambda template: (template.NAME == template_name))
def get_template_instance(self, requested_id, requested_name):
if requested_id:
return self.get_template_by_id(requested_id)
else:
return self.get_template_by_name(requested_name)
def get_template_info(self, template):
info = {
'id': template.ID,
'name': template.NAME,
'template': template.TEMPLATE,
'user_name': template.UNAME,
'user_id': template.UID,
'group_name': template.GNAME,
'group_id': template.GID,
}
return info
def create_template(self, name, template_data):
if not self.module.check_mode:
self.one.template.allocate("NAME = \"" + name + "\"\n" + template_data)
result = self.get_template_info(self.get_template_by_name(name))
result['changed'] = True
return result
def update_template(self, template, template_data):
if not self.module.check_mode:
# 0 = replace the whole template
self.one.template.update(template.ID, template_data, 0)
result = self.get_template_info(self.get_template_by_id(template.ID))
if self.module.check_mode:
# Unfortunately it is not easy to detect if the template would have changed, therefore always report a change here.
result['changed'] = True
else:
# if the previous parsed template data is not equal to the updated one, this has changed
result['changed'] = template.TEMPLATE != result['template']
return result
def delete_template(self, template):
if not template:
return {'changed': False}
if not self.module.check_mode:
self.one.template.delete(template.ID)
return {'changed': True}
def main():
TemplateModule().run_module()
if __name__ == '__main__':
main()

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

@@ -583,7 +583,7 @@ def main():
volume_size=dict(type='int', default=10),
disk_type=dict(choices=['HDD', 'SSD'], default='HDD'),
image_password=dict(default=None, no_log=True),
ssh_keys=dict(type='list', elements='str', default=[]),
ssh_keys=dict(type='list', elements='str', default=[], no_log=False),
bus=dict(choices=['VIRTIO', 'IDE'], default='VIRTIO'),
lan=dict(type='int', default=1),
count=dict(type='int', default=1),

View File

@@ -376,7 +376,7 @@ def main():
bus=dict(choices=['VIRTIO', 'IDE'], default='VIRTIO'),
image=dict(),
image_password=dict(no_log=True),
ssh_keys=dict(type='list', elements='str', default=[]),
ssh_keys=dict(type='list', elements='str', default=[], no_log=False),
disk_type=dict(choices=['HDD', 'SSD'], default='HDD'),
licence_type=dict(default='UNKNOWN'),
count=dict(type='int', default=1),

View File

@@ -549,7 +549,7 @@ def main():
password=dict(default='', required=False, type='str', no_log=True),
account=dict(default='', required=False, type='str'),
application=dict(required=True, type='str'),
keyset=dict(required=True, type='str'),
keyset=dict(required=True, type='str', no_log=False),
state=dict(default='present', type='str',
choices=['started', 'stopped', 'present', 'absent']),
name=dict(required=True, type='str'), description=dict(type='str'),

View File

@@ -110,6 +110,7 @@ options:
with this image
instance_ids:
type: list
elements: str
description:
- list of instance ids, currently only used when state='absent' to
remove instances
@@ -129,6 +130,7 @@ options:
- Name to give the instance
networks:
type: list
elements: str
description:
- The network to attach to the instances. If specified, you must include
ALL networks including the public and private interfaces. Can be C(id)
@@ -810,11 +812,11 @@ def main():
flavor=dict(),
group=dict(),
image=dict(),
instance_ids=dict(type='list'),
instance_ids=dict(type='list', elements='str'),
key_name=dict(aliases=['keypair']),
meta=dict(type='dict', default={}),
name=dict(),
networks=dict(type='list', default=['public', 'private']),
networks=dict(type='list', elements='str', default=['public', 'private']),
service=dict(),
state=dict(default='present', choices=['present', 'absent']),
user_data=dict(no_log=True),

View File

@@ -30,6 +30,7 @@ options:
required: yes
databases:
type: list
elements: str
description:
- Name of the databases that the user can access
default: []
@@ -189,7 +190,7 @@ def main():
cdb_id=dict(type='str', required=True),
db_username=dict(type='str', required=True),
db_password=dict(type='str', required=True, no_log=True),
databases=dict(type='list', default=[]),
databases=dict(type='list', elements='str', default=[]),
host=dict(type='str', default='%'),
state=dict(default='present', choices=['present', 'absent'])
)

View File

@@ -53,6 +53,7 @@ options:
- key pair to use on the instance
loadbalancers:
type: list
elements: dict
description:
- List of load balancer C(id) and C(port) hashes
max_entities:
@@ -78,6 +79,7 @@ options:
required: true
networks:
type: list
elements: str
description:
- The network to attach to the instances. If specified, you must include
ALL networks including the public and private interfaces. Can be C(id)
@@ -376,12 +378,12 @@ def main():
flavor=dict(required=True),
image=dict(required=True),
key_name=dict(),
loadbalancers=dict(type='list'),
loadbalancers=dict(type='list', elements='dict'),
meta=dict(type='dict', default={}),
min_entities=dict(type='int', required=True),
max_entities=dict(type='int', required=True),
name=dict(required=True),
networks=dict(type='list', default=['public', 'private']),
networks=dict(type='list', elements='str', default=['public', 'private']),
server_name=dict(required=True),
state=dict(default='present', choices=['present', 'absent']),
user_data=dict(no_log=True),

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

@@ -24,6 +24,7 @@ options:
manifest and 'published_date', 'published', 'source', 'clones',
and 'size'. More information can be found at U(https://smartos.org/man/1m/imgadm)
under 'imgadm list'.
type: str
'''
EXAMPLES = '''
@@ -71,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

@@ -404,7 +404,7 @@ def main():
nic_speed=dict(type='int', choices=NIC_SPEEDS),
public_vlan=dict(type='str'),
private_vlan=dict(type='str'),
ssh_keys=dict(type='list', elements='str', default=[]),
ssh_keys=dict(type='list', elements='str', default=[], no_log=False),
post_uri=dict(type='str'),
state=dict(type='str', default='present', choices=STATES),
wait=dict(type='bool', default=True),

View File

@@ -1448,7 +1448,7 @@ def main():
iam_role_arn=dict(type='str'),
iam_role_name=dict(type='str'),
image_id=dict(type='str', required=True),
key_pair=dict(type='str'),
key_pair=dict(type='str', no_log=False),
kubernetes=dict(type='dict'),
lifetime_period=dict(type='int'),
load_balancers=dict(type='list'),

View File

@@ -1839,7 +1839,7 @@ def main():
type='list',
elements='dict',
options=dict(
key=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=False),
value=dict(type='raw', required=True),
),
),

View File

@@ -229,7 +229,7 @@ _ARGUMENT_SPEC = {
PORT_PARAMETER_NAME: dict(default=8500, type='int'),
RULES_PARAMETER_NAME: dict(type='list', elements='dict'),
STATE_PARAMETER_NAME: dict(default=PRESENT_STATE_VALUE, choices=[PRESENT_STATE_VALUE, ABSENT_STATE_VALUE]),
TOKEN_PARAMETER_NAME: dict(),
TOKEN_PARAMETER_NAME: dict(no_log=False),
TOKEN_TYPE_PARAMETER_NAME: dict(choices=[CLIENT_TOKEN_TYPE_VALUE, MANAGEMENT_TOKEN_TYPE_VALUE],
default=CLIENT_TOKEN_TYPE_VALUE)
}

View File

@@ -297,7 +297,7 @@ def main():
argument_spec=dict(
cas=dict(type='str'),
flags=dict(type='str'),
key=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=False),
host=dict(type='str', default='localhost'),
scheme=dict(type='str', default='http'),
validate_certs=dict(type='bool', default=True),

View File

@@ -134,7 +134,7 @@ def run_module():
# define the available arguments/parameters that a user can pass to
# the module
module_args = dict(
key=dict(type='str', required=True),
key=dict(type='str', required=True, no_log=False),
value=dict(type='str', required=True),
host=dict(type='str', default='localhost'),
port=dict(type='int', default=2379),

View File

@@ -190,9 +190,9 @@ def run_module():
min_cluster_size=dict(type='int', required=False, default=1),
target_cluster_size=dict(type='int', required=False, default=None),
fail_on_cluster_change=dict(type='bool', required=False, default=True),
migrate_tx_key=dict(type='str', required=False,
migrate_tx_key=dict(type='str', required=False, no_log=False,
default="migrate_tx_partitions_remaining"),
migrate_rx_key=dict(type='str', required=False,
migrate_rx_key=dict(type='str', required=False, no_log=False,
default="migrate_rx_partitions_remaining")
)

View File

@@ -58,7 +58,13 @@ options:
description:
- Delete and re-install the plugin. Can be useful for plugins update.
type: bool
default: 'no'
default: false
allow_root:
description:
- Whether to allow C(kibana) and C(kibana-plugin) to be run as root. Passes the C(--allow-root) flag to these commands.
type: bool
default: false
version_added: 2.3.0
'''
EXAMPLES = '''
@@ -152,7 +158,7 @@ def parse_error(string):
return string
def install_plugin(module, plugin_bin, plugin_name, url, timeout, kibana_version='4.6'):
def install_plugin(module, plugin_bin, plugin_name, url, timeout, allow_root, kibana_version='4.6'):
if LooseVersion(kibana_version) > LooseVersion('4.6'):
kibana_plugin_bin = os.path.join(os.path.dirname(plugin_bin), 'kibana-plugin')
cmd_args = [kibana_plugin_bin, "install"]
@@ -164,48 +170,53 @@ def install_plugin(module, plugin_bin, plugin_name, url, timeout, kibana_version
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])
cmd = " ".join(cmd_args)
if allow_root:
cmd_args.append('--allow-root')
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, kibana_version='4.6'):
def remove_plugin(module, plugin_bin, plugin_name, allow_root, kibana_version='4.6'):
if LooseVersion(kibana_version) > LooseVersion('4.6'):
kibana_plugin_bin = os.path.join(os.path.dirname(plugin_bin), 'kibana-plugin')
cmd_args = [kibana_plugin_bin, "remove", plugin_name]
else:
cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["absent"], plugin_name]
cmd = " ".join(cmd_args)
if allow_root:
cmd_args.append('--allow-root')
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):
def get_kibana_version(module, plugin_bin, allow_root):
cmd_args = [plugin_bin, '--version']
cmd = " ".join(cmd_args)
rc, out, err = module.run_command(cmd)
if allow_root:
cmd_args.append('--allow-root')
rc, out, err = module.run_command(cmd_args)
if rc != 0:
module.fail_json(msg="Failed to get Kibana version : %s" % err)
@@ -222,7 +233,8 @@ def main():
plugin_bin=dict(default="/opt/kibana/bin/kibana", type="path"),
plugin_dir=dict(default="/opt/kibana/installedPlugins/", type="path"),
version=dict(default=None),
force=dict(default="no", type="bool")
force=dict(default=False, type="bool"),
allow_root=dict(default=False, type="bool"),
),
supports_check_mode=True,
)
@@ -235,10 +247,11 @@ def main():
plugin_dir = module.params["plugin_dir"]
version = module.params["version"]
force = module.params["force"]
allow_root = module.params["allow_root"]
changed, cmd, out, err = False, '', '', ''
kibana_version = get_kibana_version(module, plugin_bin)
kibana_version = get_kibana_version(module, plugin_bin, allow_root)
present = is_plugin_present(parse_plugin_repo(name), plugin_dir)
@@ -251,11 +264,11 @@ def main():
if state == "present":
if force:
remove_plugin(module, plugin_bin, name)
changed, cmd, out, err = install_plugin(module, plugin_bin, name, url, timeout, kibana_version)
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":
changed, cmd, out, err = remove_plugin(module, plugin_bin, name, kibana_version)
changed, cmd, out, err = remove_plugin(module, plugin_bin, name, allow_root, kibana_version)
module.exit_json(changed=changed, cmd=cmd, name=name, state=state, url=url, timeout=timeout, stdout=out, stderr=err)

View File

@@ -36,6 +36,7 @@ options:
description:
- The file name of the destination archive. The parent directory must exists on the remote host.
- This is required when C(path) refers to multiple files by either specifying a glob, a directory or multiple paths in a list.
- If the destination archive already exists, it will be truncated and overwritten.
type: path
exclude_path:
description:
@@ -44,8 +45,9 @@ options:
elements: path
force_archive:
description:
- Allow you to force the module to treat this as an archive even if only a single file is specified.
- By default behaviour is maintained. i.e A when a single file is specified it is compressed only (not archived).
- Allows you to force the module to treat this as an archive even if only a single file is specified.
- By default when a single file is specified it is compressed only (not archived).
- Enable this if you want to use M(ansible.builtin.unarchive) on an archive of a single file created with this module.
type: bool
default: false
remove:
@@ -153,7 +155,6 @@ expanded_exclude_paths:
'''
import bz2
import filecmp
import glob
import gzip
import io
@@ -186,6 +187,33 @@ else:
HAS_LZMA = False
def to_b(s):
return to_bytes(s, errors='surrogate_or_strict')
def to_n(s):
return to_native(s, errors='surrogate_or_strict')
def to_na(s):
return to_native(s, errors='surrogate_or_strict', encoding='ascii')
def expand_paths(paths):
expanded_path = []
is_globby = False
for path in paths:
b_path = to_b(path)
if b'*' in b_path or b'?' in b_path:
e_paths = glob.glob(b_path)
is_globby = True
else:
e_paths = [b_path]
expanded_path.extend(e_paths)
return expanded_path, is_globby
def main():
module = AnsibleModule(
argument_spec=dict(
@@ -204,21 +232,17 @@ def main():
check_mode = module.check_mode
paths = params['path']
dest = params['dest']
b_dest = None if not dest else to_bytes(dest, errors='surrogate_or_strict')
b_dest = None if not dest else to_b(dest)
exclude_paths = params['exclude_path']
remove = params['remove']
b_expanded_paths = []
b_expanded_exclude_paths = []
fmt = params['format']
b_fmt = to_bytes(fmt, errors='surrogate_or_strict')
b_fmt = to_b(fmt)
force_archive = params['force_archive']
globby = False
changed = False
state = 'absent'
# Simple or archive file compression (inapplicable with 'zip' since it's always an archive)
archive = False
b_successes = []
# Fail early
@@ -227,35 +251,7 @@ def main():
exception=LZMA_IMP_ERR)
module.fail_json(msg="lzma or backports.lzma is required when using xz format.")
for path in paths:
b_path = to_bytes(path, errors='surrogate_or_strict')
# Expand any glob characters. If found, add the expanded glob to the
# list of expanded_paths, which might be empty.
if (b'*' in b_path or b'?' in b_path):
b_expanded_paths.extend(glob.glob(b_path))
globby = True
# If there are no glob characters the path is added to the expanded paths
# whether the path exists or not
else:
b_expanded_paths.append(b_path)
# Only attempt to expand the exclude paths if it exists
if exclude_paths:
for exclude_path in exclude_paths:
b_exclude_path = to_bytes(exclude_path, errors='surrogate_or_strict')
# Expand any glob characters. If found, add the expanded glob to the
# list of expanded_paths, which might be empty.
if (b'*' in b_exclude_path or b'?' in b_exclude_path):
b_expanded_exclude_paths.extend(glob.glob(b_exclude_path))
# If there are no glob character the exclude path is added to the expanded
# exclude paths whether the path exists or not.
else:
b_expanded_exclude_paths.append(b_exclude_path)
b_expanded_paths, globby = expand_paths(paths)
if not b_expanded_paths:
return module.fail_json(
path=', '.join(paths),
@@ -263,6 +259,9 @@ def main():
msg='Error, no source paths were found'
)
# Only attempt to expand the exclude paths if it exists
b_expanded_exclude_paths = expand_paths(exclude_paths)[0] if exclude_paths else []
# Only try to determine if we are working with an archive or not if we haven't set archive to true
if not force_archive:
# If we actually matched multiple files or TRIED to, then
@@ -280,7 +279,7 @@ def main():
if archive and not b_dest:
module.fail_json(dest=dest, path=', '.join(paths), msg='Error, must specify "dest" when archiving multiple files or trees')
b_sep = to_bytes(os.sep, errors='surrogate_or_strict')
b_sep = to_b(os.sep)
b_archive_paths = []
b_missing = []
@@ -321,7 +320,7 @@ def main():
# No source files were found but the named archive exists: are we 'compress' or 'archive' now?
if len(b_missing) == len(b_expanded_paths) and b_dest and os.path.exists(b_dest):
# Just check the filename to know if it's an archive or simple compressed file
if re.search(br'(\.tar|\.tar\.gz|\.tgz|\.tbz2|\.tar\.bz2|\.tar\.xz|\.zip)$', os.path.basename(b_dest), re.IGNORECASE):
if re.search(br'\.(tar|tar\.(gz|bz2|xz)|tgz|tbz2|zip)$', os.path.basename(b_dest), re.IGNORECASE):
state = 'archive'
else:
state = 'compress'
@@ -352,7 +351,7 @@ def main():
# Slightly more difficult (and less efficient!) compression using zipfile module
if fmt == 'zip':
arcfile = zipfile.ZipFile(
to_native(b_dest, errors='surrogate_or_strict', encoding='ascii'),
to_na(b_dest),
'w',
zipfile.ZIP_DEFLATED,
True
@@ -360,7 +359,7 @@ def main():
# Easier compression using tarfile module
elif fmt == 'gz' or fmt == 'bz2':
arcfile = tarfile.open(to_native(b_dest, errors='surrogate_or_strict', encoding='ascii'), 'w|' + fmt)
arcfile = tarfile.open(to_na(b_dest), 'w|' + fmt)
# python3 tarfile module allows xz format but for python2 we have to create the tarfile
# in memory and then compress it with lzma.
@@ -370,7 +369,7 @@ def main():
# Or plain tar archiving
elif fmt == 'tar':
arcfile = tarfile.open(to_native(b_dest, errors='surrogate_or_strict', encoding='ascii'), 'w')
arcfile = tarfile.open(to_na(b_dest), 'w')
b_match_root = re.compile(br'^%s' % re.escape(b_arcroot))
for b_path in b_archive_paths:
@@ -382,7 +381,7 @@ def main():
for b_dirname in b_dirnames:
b_fullpath = b_dirpath + b_dirname
n_fullpath = to_native(b_fullpath, errors='surrogate_or_strict', encoding='ascii')
n_fullpath = to_na(b_fullpath)
n_arcname = to_native(b_match_root.sub(b'', b_fullpath), errors='surrogate_or_strict')
try:
@@ -396,8 +395,8 @@ def main():
for b_filename in b_filenames:
b_fullpath = b_dirpath + b_filename
n_fullpath = to_native(b_fullpath, errors='surrogate_or_strict', encoding='ascii')
n_arcname = to_native(b_match_root.sub(b'', b_fullpath), errors='surrogate_or_strict')
n_fullpath = to_na(b_fullpath)
n_arcname = to_n(b_match_root.sub(b'', b_fullpath))
try:
if fmt == 'zip':
@@ -409,8 +408,8 @@ def main():
except Exception as e:
errors.append('Adding %s: %s' % (to_native(b_path), to_native(e)))
else:
path = to_native(b_path, errors='surrogate_or_strict', encoding='ascii')
arcname = to_native(b_match_root.sub(b'', b_path), errors='surrogate_or_strict')
path = to_na(b_path)
arcname = to_n(b_match_root.sub(b'', b_path))
if fmt == 'zip':
arcfile.write(path, arcname)
else:
@@ -444,14 +443,14 @@ def main():
shutil.rmtree(b_path)
elif not check_mode:
os.remove(b_path)
except OSError as e:
except OSError:
errors.append(to_native(b_path))
for b_path in b_expanded_paths:
try:
if os.path.isdir(b_path):
shutil.rmtree(b_path)
except OSError as e:
except OSError:
errors.append(to_native(b_path))
if errors:
@@ -490,25 +489,25 @@ def main():
try:
if fmt == 'zip':
arcfile = zipfile.ZipFile(
to_native(b_dest, errors='surrogate_or_strict', encoding='ascii'),
to_na(b_dest),
'w',
zipfile.ZIP_DEFLATED,
True
)
arcfile.write(
to_native(b_path, errors='surrogate_or_strict', encoding='ascii'),
to_native(b_path[len(b_arcroot):], errors='surrogate_or_strict')
to_na(b_path),
to_n(b_path[len(b_arcroot):])
)
arcfile.close()
state = 'archive' # because all zip files are archives
elif fmt == 'tar':
arcfile = tarfile.open(to_native(b_dest, errors='surrogate_or_strict', encoding='ascii'), 'w')
arcfile.add(to_native(b_path, errors='surrogate_or_strict', encoding='ascii'))
arcfile = tarfile.open(to_na(b_dest), 'w')
arcfile.add(to_na(b_path))
arcfile.close()
else:
f_in = open(b_path, 'rb')
n_dest = to_native(b_dest, errors='surrogate_or_strict', encoding='ascii')
n_dest = to_na(b_dest)
if fmt == 'gz':
f_out = gzip.open(n_dest, 'wb')
elif fmt == 'bz2':
@@ -564,14 +563,14 @@ def main():
changed = module.set_fs_attributes_if_different(file_args, changed)
module.exit_json(
archived=[to_native(p, errors='surrogate_or_strict') for p in b_successes],
archived=[to_n(p) for p in b_successes],
dest=dest,
changed=changed,
state=state,
arcroot=to_native(b_arcroot, errors='surrogate_or_strict'),
missing=[to_native(p, errors='surrogate_or_strict') for p in b_missing],
expanded_paths=[to_native(p, errors='surrogate_or_strict') for p in b_expanded_paths],
expanded_exclude_paths=[to_native(p, errors='surrogate_or_strict') for p in b_expanded_exclude_paths],
arcroot=to_n(b_arcroot),
missing=[to_n(p) for p in b_missing],
expanded_paths=[to_n(p) for p in b_expanded_paths],
expanded_exclude_paths=[to_n(p) for p in b_expanded_exclude_paths],
)

View File

@@ -137,26 +137,12 @@ list:
gid: 500
'''
import csv
from io import BytesIO, StringIO
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_text
from ansible.module_utils.six import PY3
from ansible.module_utils._text import to_native
# Add Unix dialect from Python 3
class unix_dialect(csv.Dialect):
"""Describe the usual properties of Unix-generated CSV files."""
delimiter = ','
quotechar = '"'
doublequote = True
skipinitialspace = False
lineterminator = '\n'
quoting = csv.QUOTE_ALL
csv.register_dialect("unix", unix_dialect)
from ansible_collections.community.general.plugins.module_utils.csv import (initialize_dialect, read_csv, CSVError,
DialectNotAvailableError,
CustomDialectFailureError)
def main():
@@ -164,7 +150,7 @@ def main():
argument_spec=dict(
path=dict(type='path', required=True, aliases=['filename']),
dialect=dict(type='str', default='excel'),
key=dict(type='str'),
key=dict(type='str', no_log=False),
fieldnames=dict(type='list', elements='str'),
unique=dict(type='bool', default=True),
delimiter=dict(type='str'),
@@ -180,38 +166,24 @@ def main():
fieldnames = module.params['fieldnames']
unique = module.params['unique']
if dialect not in csv.list_dialects():
module.fail_json(msg="Dialect '%s' is not supported by your version of python." % dialect)
dialect_params = {
"delimiter": module.params['delimiter'],
"skipinitialspace": module.params['skipinitialspace'],
"strict": module.params['strict'],
}
dialect_options = dict(
delimiter=module.params['delimiter'],
skipinitialspace=module.params['skipinitialspace'],
strict=module.params['strict'],
)
# Create a dictionary from only set options
dialect_params = dict((k, v) for k, v in dialect_options.items() if v is not None)
if dialect_params:
try:
csv.register_dialect('custom', dialect, **dialect_params)
except TypeError as e:
module.fail_json(msg="Unable to create custom dialect: %s" % to_text(e))
dialect = 'custom'
try:
dialect = initialize_dialect(dialect, **dialect_params)
except (CustomDialectFailureError, DialectNotAvailableError) as e:
module.fail_json(msg=to_native(e))
try:
with open(path, 'rb') as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg="Unable to open file: %s" % to_text(e))
module.fail_json(msg="Unable to open file: %s" % to_native(e))
if PY3:
# Manually decode on Python3 so that we can use the surrogateescape error handler
data = to_text(data, errors='surrogate_or_strict')
fake_fh = StringIO(data)
else:
fake_fh = BytesIO(data)
reader = csv.DictReader(fake_fh, fieldnames=fieldnames, dialect=dialect)
reader = read_csv(data, dialect, fieldnames)
if key and key not in reader.fieldnames:
module.fail_json(msg="Key '%s' was not found in the CSV header fields: %s" % (key, ', '.join(reader.fieldnames)))
@@ -223,16 +195,16 @@ def main():
try:
for row in reader:
data_list.append(row)
except csv.Error as e:
module.fail_json(msg="Unable to process file: %s" % to_text(e))
except CSVError as e:
module.fail_json(msg="Unable to process file: %s" % to_native(e))
else:
try:
for row in reader:
if unique and row[key] in data_dict:
module.fail_json(msg="Key '%s' is not unique for value '%s'" % (key, row[key]))
data_dict[row[key]] = row
except csv.Error as e:
module.fail_json(msg="Unable to process file: %s" % to_text(e))
except CSVError as e:
module.fail_json(msg="Unable to process file: %s" % to_native(e))
module.exit_json(dict=data_dict, list=data_list)

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:
@@ -172,7 +169,7 @@ def main():
argument_spec=dict(
path=dict(type='path', required=True, aliases=['name']),
namespace=dict(type='str', default='user'),
key=dict(type='str'),
key=dict(type='str', no_log=False),
value=dict(type='str'),
state=dict(type='str', default='read', choices=['absent', 'all', 'keys', 'present', 'read']),
follow=dict(type='bool', default=True),

View File

@@ -0,0 +1 @@
net_tools/gandi_livedns.py

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

@@ -63,7 +63,7 @@ EXAMPLES = r'''
- name: Changing Managing hosts list
community.general.ipa_service:
name: http/host01.example.com
host:
hosts:
- host01.example.com
- host02.example.com
ipa_host: ipa.example.com

View File

@@ -68,6 +68,12 @@ options:
- Option C(hostcategory) must be omitted to assign host groups.
type: list
elements: str
runasextusers:
description:
- List of external RunAs users
type: list
elements: str
version_added: 2.3.0
runasusercategory:
description:
- RunAs User category the rule applies to.
@@ -143,13 +149,15 @@ EXAMPLES = r'''
ipa_user: admin
ipa_pass: topsecret
- name: Ensure user group operations can run any commands that is part of operations-cmdgroup on any host.
- name: Ensure user group operations can run any commands that is part of operations-cmdgroup on any host as user root.
community.general.ipa_sudorule:
name: sudo_operations_all
description: Allow operators to run any commands that is part of operations-cmdgroup on any host.
description: Allow operators to run any commands that is part of operations-cmdgroup on any host as user root.
cmdgroup:
- operations-cmdgroup
hostcategory: all
runasextusers:
- root
sudoopt:
- '!authenticate'
usergroup:
@@ -183,6 +191,12 @@ class SudoRuleIPAClient(IPAClient):
def sudorule_add(self, name, item):
return self._post_json(method='sudorule_add', name=name, item=item)
def sudorule_add_runasuser(self, name, item):
return self._post_json(method='sudorule_add_runasuser', name=name, item={'user': item})
def sudorule_remove_runasuser(self, name, item):
return self._post_json(method='sudorule_remove_runasuser', name=name, item={'user': item})
def sudorule_mod(self, name, item):
return self._post_json(method='sudorule_mod', name=name, item=item)
@@ -287,6 +301,7 @@ def ensure(module, client):
hostgroup = module.params['hostgroup']
runasusercategory = module.params['runasusercategory']
runasgroupcategory = module.params['runasgroupcategory']
runasextusers = module.params['runasextusers']
if state in ['present', 'enabled']:
ipaenabledflag = 'TRUE'
@@ -371,6 +386,21 @@ def ensure(module, client):
for item in diff:
client.sudorule_add_option_ipasudoopt(name, item)
if runasextusers is not None:
ipa_sudorule_run_as_user = ipa_sudorule.get('ipasudorunasextuser', [])
diff = list(set(ipa_sudorule_run_as_user) - set(runasextusers))
if len(diff) > 0:
changed = True
if not module.check_mode:
for item in diff:
client.sudorule_remove_runasuser(name=name, item=item)
diff = list(set(runasextusers) - set(ipa_sudorule_run_as_user))
if len(diff) > 0:
changed = True
if not module.check_mode:
for item in diff:
client.sudorule_add_runasuser(name=name, item=item)
if user is not None:
changed = category_changed(module, client, 'usercategory', ipa_sudorule) or changed
changed = client.modify_if_diff(name, ipa_sudorule.get('memberuser_user', []), user,
@@ -406,8 +436,8 @@ def main():
state=dict(type='str', default='present', choices=['present', 'absent', 'enabled', 'disabled']),
user=dict(type='list', elements='str'),
usercategory=dict(type='str', choices=['all']),
usergroup=dict(type='list', elements='str'))
usergroup=dict(type='list', elements='str'),
runasextusers=dict(type='list', elements='str'))
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=[['cmdcategory', 'cmd'],
['cmdcategory', 'cmdgroup'],

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

@@ -120,7 +120,7 @@ def main():
host=dict(),
tags=dict(type='list', elements='str'),
alert_type=dict(default='info', choices=['error', 'warning', 'info', 'success']),
aggregation_key=dict(),
aggregation_key=dict(no_log=False),
validate_certs=dict(default=True, type='bool'),
)
)

View File

@@ -205,7 +205,7 @@ def main():
client=dict(required=False, default=None),
client_url=dict(required=False, default=None),
desc=dict(required=False, default='Created via Ansible'),
incident_key=dict(required=False, default=None)
incident_key=dict(required=False, default=None, no_log=False)
),
supports_check_mode=True
)

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

@@ -800,7 +800,7 @@ def main():
algorithm=dict(type='int'),
cert_usage=dict(type='int', choices=[0, 1, 2, 3]),
hash_type=dict(type='int', choices=[1, 2]),
key_tag=dict(type='int'),
key_tag=dict(type='int', no_log=False),
port=dict(type='int'),
priority=dict(type='int', default=1),
proto=dict(type='str'),

View File

@@ -0,0 +1,187 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright: (c) 2019 Gregory Thiemonge <gregory.thiemonge@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: gandi_livedns
author:
- Gregory Thiemonge (@gthiemonge)
version_added: "2.3.0"
short_description: Manage Gandi LiveDNS records
description:
- "Manages DNS records by the Gandi LiveDNS API, see the docs: U(https://doc.livedns.gandi.net/)."
options:
api_key:
description:
- Account API token.
type: str
required: true
record:
description:
- Record to add.
type: str
required: true
state:
description:
- Whether the record(s) should exist or not.
type: str
choices: [ absent, present ]
default: present
ttl:
description:
- The TTL to give the new record.
- Required when I(state=present).
type: int
type:
description:
- The type of DNS record to create.
type: str
required: true
values:
description:
- The record values.
- Required when I(state=present).
type: list
elements: str
domain:
description:
- The name of the Domain to work with (for example, "example.com").
required: true
type: str
notes:
- Supports C(check_mode).
'''
EXAMPLES = r'''
- name: Create a test A record to point to 127.0.0.1 in the my.com domain
community.general.gandi_livedns:
domain: my.com
record: test
type: A
values:
- 127.0.0.1
ttl: 7200
api_key: dummyapitoken
register: record
- name: Create a mail CNAME record to www.my.com domain
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
values:
- www
ttl: 7200
api_key: dummyapitoken
state: present
- name: Change its TTL
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
values:
- www
ttl: 10800
api_key: dummyapitoken
state: present
- name: Delete the record
community.general.gandi_livedns:
domain: my.com
type: CNAME
record: mail
api_key: dummyapitoken
state: absent
'''
RETURN = r'''
record:
description: A dictionary containing the record data.
returned: success, except on record deletion
type: dict
contains:
values:
description: The record content (details depend on record type).
returned: success
type: list
elements: str
sample:
- 192.0.2.91
- 192.0.2.92
record:
description: The record name.
returned: success
type: str
sample: www
ttl:
description: The time-to-live for the record.
returned: success
type: int
sample: 300
type:
description: The record type.
returned: success
type: str
sample: A
domain:
description: The domain associated with the record.
returned: success
type: str
sample: my.com
'''
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.gandi_livedns_api import GandiLiveDNSAPI
def main():
module = AnsibleModule(
argument_spec=dict(
api_key=dict(type='str', required=True, no_log=True),
record=dict(type='str', required=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
ttl=dict(type='int'),
type=dict(type='str', required=True),
values=dict(type='list', elements='str'),
domain=dict(type='str', required=True),
),
supports_check_mode=True,
required_if=[
('state', 'present', ['values', 'ttl']),
],
)
gandi_api = GandiLiveDNSAPI(module)
if module.params['state'] == 'present':
ret, changed = gandi_api.ensure_dns_record(module.params['record'],
module.params['type'],
module.params['ttl'],
module.params['values'],
module.params['domain'])
else:
ret, changed = gandi_api.delete_dns_record(module.params['record'],
module.params['type'],
module.params['values'],
module.params['domain'])
result = dict(
changed=changed,
)
if ret:
result['record'] = gandi_api.build_result(ret,
module.params['domain'])
module.exit_json(**result)
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

@@ -0,0 +1,343 @@
#!/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_user
author: "Florian Dambrine (@Lowess)"
version_added: 2.3.0
short_description: Manage Pritunl Users using the Pritunl API
description:
- A module to manage Pritunl users using the Pritunl API.
extends_documentation_fragment:
- community.general.pritunl
options:
organization:
type: str
required: true
aliases:
- org
description:
- The name of the organization the user is part of.
state:
type: str
default: 'present'
choices:
- present
- absent
description:
- If C(present), the module adds user I(user_name) to
the Pritunl I(organization). If C(absent), removes the user
I(user_name) from the Pritunl I(organization).
user_name:
type: str
required: true
default: null
description:
- Name of the user to create or delete from Pritunl.
user_email:
type: str
required: false
default: null
description:
- Email address associated with the user I(user_name).
user_type:
type: str
required: false
default: client
choices:
- client
- server
description:
- Type of the user I(user_name).
user_groups:
type: list
elements: str
required: false
default: null
description:
- List of groups associated with the user I(user_name).
user_disabled:
type: bool
required: false
default: null
description:
- Enable/Disable the user I(user_name).
user_gravatar:
type: bool
required: false
default: null
description:
- Enable/Disable Gravatar usage for the user I(user_name).
"""
EXAMPLES = """
- name: Create the user Foo with email address foo@bar.com in MyOrg
community.general.pritunl_user:
state: present
name: MyOrg
user_name: Foo
user_email: foo@bar.com
- name: Disable the user Foo but keep it in Pritunl
community.general.pritunl_user:
state: present
name: MyOrg
user_name: Foo
user_email: foo@bar.com
user_disabled: yes
- name: Make sure the user Foo is not part of MyOrg anymore
community.general.pritunl_user:
state: absent
name: MyOrg
user_name: Foo
"""
RETURN = """
response:
description: JSON representation of Pritunl Users.
returned: success
type: dict
sample:
{
"audit": false,
"auth_type": "google",
"bypass_secondary": false,
"client_to_client": false,
"disabled": false,
"dns_mapping": null,
"dns_servers": null,
"dns_suffix": null,
"email": "foo@bar.com",
"gravatar": true,
"groups": [
"foo", "bar"
],
"id": "5d070dafe63q3b2e6s472c3b",
"name": "foo@acme.com",
"network_links": [],
"organization": "58070daee6sf342e6e4s2c36",
"organization_name": "Acme",
"otp_auth": true,
"otp_secret": "35H5EJA3XB2$4CWG",
"pin": false,
"port_forwarding": [],
"servers": [],
}
"""
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_user,
get_pritunl_settings,
list_pritunl_organizations,
list_pritunl_users,
post_pritunl_user,
pritunl_argument_spec,
)
def add_or_update_pritunl_user(module):
result = {}
org_name = module.params.get("organization")
user_name = module.params.get("user_name")
user_params = {
"name": user_name,
"email": module.params.get("user_email"),
"groups": module.params.get("user_groups"),
"disabled": module.params.get("user_disabled"),
"gravatar": module.params.get("user_gravatar"),
"type": module.params.get("user_type"),
}
org_obj_list = list_pritunl_organizations(
**dict_merge(
get_pritunl_settings(module),
{"filters": {"name": org_name}},
)
)
if len(org_obj_list) == 0:
module.fail_json(
msg="Can not add user to organization '%s' which does not exist" % org_name
)
org_id = org_obj_list[0]["id"]
# Grab existing users from this org
users = list_pritunl_users(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"filters": {"name": user_name},
},
)
)
# Check if the pritunl user already exists
if len(users) > 0:
# Compare remote user params with local user_params and trigger update if needed
user_params_changed = False
for key in user_params.keys():
# When a param is not specified grab existing ones to prevent from changing it with the PUT request
if user_params[key] is None:
user_params[key] = users[0][key]
# 'groups' is a list comparison
if key == "groups":
if set(users[0][key]) != set(user_params[key]):
user_params_changed = True
# otherwise it is either a boolean or a string
else:
if users[0][key] != user_params[key]:
user_params_changed = True
# Trigger a PUT on the API to update the current user if settings have changed
if user_params_changed:
response = post_pritunl_user(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"user_id": users[0]["id"],
"user_data": user_params,
},
)
)
result["changed"] = True
result["response"] = response
else:
result["changed"] = False
result["response"] = users
else:
response = post_pritunl_user(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"user_data": user_params,
},
)
)
result["changed"] = True
result["response"] = response
module.exit_json(**result)
def remove_pritunl_user(module):
result = {}
org_name = module.params.get("organization")
user_name = module.params.get("user_name")
org_obj_list = []
org_obj_list = list_pritunl_organizations(
**dict_merge(
get_pritunl_settings(module),
{
"filters": {"name": org_name},
},
)
)
if len(org_obj_list) == 0:
module.fail_json(
msg="Can not remove user '%s' from a non existing organization '%s'"
% (user_name, org_name)
)
org_id = org_obj_list[0]["id"]
# Grab existing users from this org
users = list_pritunl_users(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"filters": {"name": user_name},
},
)
)
# Check if the pritunl user exists, if not, do nothing
if len(users) == 0:
result["changed"] = False
result["response"] = {}
# Otherwise remove the org from Pritunl
else:
response = delete_pritunl_user(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"user_id": users[0]["id"],
},
)
)
result["changed"] = True
result["response"] = response
module.exit_json(**result)
def main():
argument_spec = pritunl_argument_spec()
argument_spec.update(
dict(
organization=dict(required=True, type="str", aliases=["org"]),
state=dict(
required=False, choices=["present", "absent"], default="present"
),
user_name=dict(required=True, type="str"),
user_type=dict(
required=False, choices=["client", "server"], default="client"
),
user_email=dict(required=False, type="str", default=None),
user_groups=dict(required=False, type="list", elements="str", default=None),
user_disabled=dict(required=False, type="bool", default=None),
user_gravatar=dict(required=False, type="bool", default=None),
)
),
module = AnsibleModule(argument_spec=argument_spec)
state = module.params.get("state")
try:
if state == "present":
add_or_update_pritunl_user(module)
elif state == "absent":
remove_pritunl_user(module)
except PritunlException as e:
module.fail_json(msg=to_native(e))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,171 @@
#!/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_user_info
author: "Florian Dambrine (@Lowess)"
version_added: 2.3.0
short_description: List Pritunl Users using the Pritunl API
description:
- A module to list Pritunl users using the Pritunl API.
extends_documentation_fragment:
- community.general.pritunl
options:
organization:
type: str
required: true
aliases:
- org
description:
- The name of the organization the user is part of.
user_name:
type: str
required: false
description:
- Name of the user to filter on Pritunl.
user_type:
type: str
required: false
default: client
choices:
- client
- server
description:
- Type of the user I(user_name).
"""
EXAMPLES = """
- name: List all existing users part of the organization MyOrg
community.general.pritunl_user_info:
state: list
organization: MyOrg
- name: Search for the user named Florian part of the organization MyOrg
community.general.pritunl_user_info:
state: list
organization: MyOrg
user_name: Florian
"""
RETURN = """
users:
description: List of Pritunl users.
returned: success
type: list
elements: dict
sample:
[
{
"audit": false,
"auth_type": "google",
"bypass_secondary": false,
"client_to_client": false,
"disabled": false,
"dns_mapping": null,
"dns_servers": null,
"dns_suffix": null,
"email": "foo@bar.com",
"gravatar": true,
"groups": [
"foo", "bar"
],
"id": "5d070dafe63q3b2e6s472c3b",
"name": "foo@acme.com",
"network_links": [],
"organization": "58070daee6sf342e6e4s2c36",
"organization_name": "Acme",
"otp_auth": true,
"otp_secret": "35H5EJA3XB2$4CWG",
"pin": false,
"port_forwarding": [],
"servers": [],
}
]
"""
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,
list_pritunl_users,
pritunl_argument_spec,
)
def get_pritunl_user(module):
user_name = module.params.get("user_name")
user_type = module.params.get("user_type")
org_name = module.params.get("organization")
org_obj_list = []
org_obj_list = list_pritunl_organizations(
**dict_merge(get_pritunl_settings(module), {"filters": {"name": org_name}})
)
if len(org_obj_list) == 0:
module.fail_json(
msg="Can not list users from the organization '%s' which does not exist"
% org_name
)
org_id = org_obj_list[0]["id"]
users = list_pritunl_users(
**dict_merge(
get_pritunl_settings(module),
{
"organization_id": org_id,
"filters": (
{"type": user_type}
if user_name is None
else {"name": user_name, "type": user_type}
),
},
)
)
result = {}
result["changed"] = False
result["users"] = users
module.exit_json(**result)
def main():
argument_spec = pritunl_argument_spec()
argument_spec.update(
dict(
organization=dict(required=True, type="str", aliases=["org"]),
user_name=dict(required=False, type="str", default=None),
user_type=dict(
required=False,
choices=["client", "server"],
default="client",
),
)
),
module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True)
try:
get_pritunl_user(module)
except PritunlException as e:
module.fail_json(msg=to_native(e))
if __name__ == "__main__":
main()

View File

@@ -67,6 +67,16 @@ options:
- Encryption key.
- Required if I(level) is C(authPriv).
type: str
timeout:
description:
- Response timeout in seconds.
type: int
version_added: 2.3.0
retries:
description:
- Maximum number of request retries, 0 retries means just a single request.
type: int
version_added: 2.3.0
'''
EXAMPLES = r'''
@@ -271,6 +281,8 @@ def main():
privacy=dict(type='str', choices=['aes', 'des']),
authkey=dict(type='str', no_log=True),
privkey=dict(type='str', no_log=True),
timeout=dict(type='int'),
retries=dict(type='int'),
),
required_together=(
['username', 'level', 'integrity', 'authkey'],
@@ -285,6 +297,7 @@ def main():
module.fail_json(msg=missing_required_lib('pysnmp'), exception=PYSNMP_IMP_ERR)
cmdGen = cmdgen.CommandGenerator()
transport_opts = dict((k, m_args[k]) for k in ('timeout', 'retries') if m_args[k] is not None)
# Verify that we receive a community when using snmp v2
if m_args['version'] in ("v2", "v2c"):
@@ -333,7 +346,7 @@ def main():
errorIndication, errorStatus, errorIndex, varBinds = cmdGen.getCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args['host'], 161)),
cmdgen.UdpTransportTarget((m_args['host'], 161), **transport_opts),
cmdgen.MibVariable(p.sysDescr,),
cmdgen.MibVariable(p.sysObjectId,),
cmdgen.MibVariable(p.sysUpTime,),
@@ -364,7 +377,7 @@ def main():
errorIndication, errorStatus, errorIndex, varTable = cmdGen.nextCmd(
snmp_auth,
cmdgen.UdpTransportTarget((m_args['host'], 161)),
cmdgen.UdpTransportTarget((m_args['host'], 161), **transport_opts),
cmdgen.MibVariable(p.ifIndex,),
cmdgen.MibVariable(p.ifDescr,),
cmdgen.MibVariable(p.ifMtu,),

View File

@@ -0,0 +1 @@
./cloud/opennebula/one_template.py

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

@@ -16,6 +16,7 @@ author: "Joe Adams (@sysadmind)"
short_description: Add or remove Pulp repos from a remote host.
description:
- Add or remove Pulp repos from a remote host.
- Note, this is for Pulp 2 only.
options:
add_export_distributor:
description:

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

@@ -336,7 +336,7 @@ def get_cmd(m, subcommand):
"puts together the basic zypper command arguments with those passed to the module"
is_install = subcommand in ['install', 'update', 'patch', 'dist-upgrade']
is_refresh = subcommand == 'refresh'
cmd = ['/usr/bin/zypper', '--quiet', '--non-interactive', '--xmlout']
cmd = [m.get_bin_path('zypper', required=True), '--quiet', '--non-interactive', '--xmlout']
if m.params['extra_args_precommand']:
args_list = m.params['extra_args_precommand'].split()
cmd.extend(args_list)

View File

@@ -141,9 +141,9 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib
REPO_OPTS = ['alias', 'name', 'priority', 'enabled', 'autorefresh', 'gpgcheck']
def _get_cmd(*args):
def _get_cmd(module, *args):
"""Combines the non-interactive zypper command with arguments/subcommands"""
cmd = ['/usr/bin/zypper', '--quiet', '--non-interactive']
cmd = [module.get_bin_path('zypper', required=True), '--quiet', '--non-interactive']
cmd.extend(args)
return cmd
@@ -151,7 +151,7 @@ def _get_cmd(*args):
def _parse_repos(module):
"""parses the output of zypper --xmlout repos and return a parse repo dictionary"""
cmd = _get_cmd('--xmlout', 'repos')
cmd = _get_cmd(module, '--xmlout', 'repos')
if not HAS_XML:
module.fail_json(msg=missing_required_lib("python-xml"), exception=XML_IMP_ERR)
@@ -230,7 +230,7 @@ def repo_exists(module, repodata, overwrite_multiple):
def addmodify_repo(module, repodata, old_repos, zypper_version, warnings):
"Adds the repo, removes old repos before, that would conflict."
repo = repodata['url']
cmd = _get_cmd('addrepo', '--check')
cmd = _get_cmd(module, 'addrepo', '--check')
if repodata['name']:
cmd.extend(['--name', repodata['name']])
@@ -274,14 +274,14 @@ def addmodify_repo(module, repodata, old_repos, zypper_version, warnings):
def remove_repo(module, repo):
"Removes the repo."
cmd = _get_cmd('removerepo', repo)
cmd = _get_cmd(module, 'removerepo', repo)
rc, stdout, stderr = module.run_command(cmd, check_rc=True)
return rc, stdout, stderr
def get_zypper_version(module):
rc, stdout, stderr = module.run_command(['/usr/bin/zypper', '--version'])
rc, stdout, stderr = module.run_command([module.get_bin_path('zypper', required=True), '--version'])
if rc != 0 or not stdout.startswith('zypper '):
return LooseVersion('1.0')
return LooseVersion(stdout.split()[1])
@@ -290,9 +290,9 @@ def get_zypper_version(module):
def runrefreshrepo(module, auto_import_keys=False, shortname=None):
"Forces zypper to refresh repo metadata."
if auto_import_keys:
cmd = _get_cmd('--gpg-auto-import-keys', 'refresh', '--force')
cmd = _get_cmd(module, '--gpg-auto-import-keys', 'refresh', '--force')
else:
cmd = _get_cmd('refresh', '--force')
cmd = _get_cmd(module, 'refresh', '--force')
if shortname is not None:
cmd.extend(['-r', shortname])

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

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

View File

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

View File

@@ -0,0 +1,673 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
---
module: xcc_redfish_command
short_description: Manages Lenovo Out-Of-Band controllers using Redfish APIs
version_added: 2.4.0
description:
- Builds Redfish URIs locally and sends them to remote OOB controllers to
perform an action or get information back or update a configuration attribute.
- Manages virtual media.
- Supports getting information back via GET method.
- Supports updating a configuration attribute via PATCH method.
- Supports performing an action via POST method.
options:
category:
required: true
description:
- Category to execute on OOB controller.
type: str
command:
required: true
description:
- List of commands to execute on OOB controller.
type: list
elements: str
baseuri:
required: true
description:
- Base URI of OOB controller.
type: str
username:
description:
- Username for authentication with OOB controller.
type: str
password:
description:
- Password for authentication with OOB controller.
type: str
auth_token:
description:
- Security token for authentication with OOB controller
type: str
timeout:
description:
- Timeout in seconds for URL requests to OOB controller.
default: 10
type: int
resource_id:
required: false
description:
- The ID of the System, Manager or Chassis to modify.
type: str
virtual_media:
required: false
description:
- The options for VirtualMedia commands.
type: dict
suboptions:
media_types:
description:
- The list of media types appropriate for the image.
type: list
elements: str
image_url:
description:
- The URL of the image to insert or eject.
type: str
inserted:
description:
- Indicates if the image is treated as inserted on command completion.
type: bool
default: true
write_protected:
description:
- Indicates if the media is treated as write-protected.
type: bool
default: true
username:
description:
- The username for accessing the image URL.
type: str
password:
description:
- The password for accessing the image URL.
type: str
transfer_protocol_type:
description:
- The network protocol to use with the image.
type: str
transfer_method:
description:
- The transfer method to use with the image.
type: str
resource_uri:
required: false
description:
- The resource uri to get or patch or post.
type: str
request_body:
required: false
description:
- The request body to patch or post.
type: dict
author: "Yuyan Pan (@panyy3)"
'''
EXAMPLES = '''
- name: Insert Virtual Media
community.general.xcc_redfish_command:
category: Manager
command: VirtualMediaInsert
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
virtual_media:
image_url: "http://example.com/images/SomeLinux-current.iso"
media_types:
- CD
- DVD
resource_id: "1"
- name: Eject Virtual Media
community.general.xcc_redfish_command:
category: Manager
command: VirtualMediaEject
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
virtual_media:
image_url: "http://example.com/images/SomeLinux-current.iso"
resource_id: "1"
- name: Eject all Virtual Media
community.general.xcc_redfish_command:
category: Manager
command: VirtualMediaEject
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_id: "1"
- name: Get ComputeSystem Oem property SystemStatus via GetResource command
community.general.xcc_redfish_command:
category: Raw
command: GetResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Systems/1"
register: result
- ansible.builtin.debug:
msg: "{{ result.redfish_facts.data.Oem.Lenovo.SystemStatus }}"
- name: Get Oem DNS setting via GetResource command
community.general.xcc_redfish_command:
category: Raw
command: GetResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS"
register: result
- ansible.builtin.debug:
msg: "{{ result.redfish_facts.data }}"
- name: Get Lenovo FoD key collection resource via GetCollectionResource command
community.general.xcc_redfish_command:
category: Raw
command: GetCollectionResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Managers/1/Oem/Lenovo/FoD/Keys"
register: result
- ansible.builtin.debug:
msg: "{{ result.redfish_facts.data_list }}"
- name: Update ComputeSystem property AssetTag via PatchResource command
community.general.xcc_redfish_command:
category: Raw
command: PatchResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Systems/1"
request_body:
AssetTag: "new_asset_tag"
- name: Perform BootToBIOSSetup action via PostResource command
community.general.xcc_redfish_command:
category: Raw
command: PostResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Systems/1/Actions/Oem/LenovoComputerSystem.BootToBIOSSetup"
request_body: {}
- name: Perform SecureBoot.ResetKeys action via PostResource command
community.general.xcc_redfish_command:
category: Raw
command: PostResource
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
resource_uri: "/redfish/v1/Systems/1/SecureBoot/Actions/SecureBoot.ResetKeys"
request_body:
ResetKeysType: DeleteAllKeys
- name: Create session
community.general.redfish_command:
category: Sessions
command: CreateSession
baseuri: "{{ baseuri }}"
username: "{{ username }}"
password: "{{ password }}"
register: result
- name: Update Manager DateTimeLocalOffset property using security token for auth
community.general.xcc_redfish_command:
category: Raw
command: PatchResource
baseuri: "{{ baseuri }}"
auth_token: "{{ result.session.token }}"
resource_uri: "/redfish/v1/Managers/1"
request_body:
DateTimeLocalOffset: "+08:00"
- name: Delete session using security token created by CreateSesssion above
community.general.redfish_command:
category: Sessions
command: DeleteSession
baseuri: "{{ baseuri }}"
auth_token: "{{ result.session.token }}"
session_uri: "{{ result.session.uri }}"
'''
RETURN = '''
msg:
description: A message related to the performed action(s).
returned: when failure or action/update success
type: str
sample: "Action was successful"
redfish_facts:
description: Resource content.
returned: when command == GetResource or command == GetCollectionResource
type: dict
sample: '{
"redfish_facts": {
"data": {
"@odata.etag": "\"3179bf00d69f25a8b3c\"",
"@odata.id": "/redfish/v1/Managers/1/NetworkProtocol/Oem/Lenovo/DNS",
"@odata.type": "#LenovoDNS.v1_0_0.LenovoDNS",
"DDNS": [
{
"DDNSEnable": true,
"DomainName": "",
"DomainNameSource": "DHCP"
}
],
"DNSEnable": true,
"Description": "This resource is used to represent a DNS resource for a Redfish implementation.",
"IPv4Address1": "10.103.62.178",
"IPv4Address2": "0.0.0.0",
"IPv4Address3": "0.0.0.0",
"IPv6Address1": "::",
"IPv6Address2": "::",
"IPv6Address3": "::",
"Id": "LenovoDNS",
"PreferredAddresstype": "IPv4"
},
"ret": true
}
}'
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
from ansible_collections.community.general.plugins.module_utils.redfish_utils import RedfishUtils
class XCCRedfishUtils(RedfishUtils):
@staticmethod
def _find_empty_virt_media_slot(resources, media_types,
media_match_strict=True):
for uri, data in resources.items():
# check MediaTypes
if 'MediaTypes' in data and media_types:
if not set(media_types).intersection(set(data['MediaTypes'])):
continue
else:
if media_match_strict:
continue
if 'RDOC' in uri:
continue
# if ejected, 'Inserted' should be False and 'ImageName' cleared
if (not data.get('Inserted', False) and
not data.get('ImageName')):
return uri, data
return None, None
def virtual_media_eject_one(self, image_url):
# locate and read the VirtualMedia resources
response = self.get_request(self.root_uri + self.manager_uri)
if response['ret'] is False:
return response
data = response['data']
if 'VirtualMedia' not in data:
return {'ret': False, 'msg': "VirtualMedia resource not found"}
virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False:
return response
data = response['data']
virt_media_list = []
for member in data[u'Members']:
virt_media_list.append(member[u'@odata.id'])
resources, headers = self._read_virt_media_resources(virt_media_list)
# find the VirtualMedia resource to eject
uri, data, eject = self._find_virt_media_to_eject(resources, image_url)
if uri and eject:
if ('Actions' not in data or
'#VirtualMedia.EjectMedia' not in data['Actions']):
# try to eject via PATCH if no EjectMedia action found
h = headers[uri]
if 'allow' in h:
methods = [m.strip() for m in h.get('allow').split(',')]
if 'PATCH' not in methods:
# if Allow header present and PATCH missing, return error
return {'ret': False,
'msg': "%s action not found and PATCH not allowed"
% '#VirtualMedia.EjectMedia'}
return self.virtual_media_eject_via_patch(uri)
else:
# POST to the EjectMedia Action
action = data['Actions']['#VirtualMedia.EjectMedia']
if 'target' not in action:
return {'ret': False,
'msg': "target URI property missing from Action "
"#VirtualMedia.EjectMedia"}
action_uri = action['target']
# empty payload for Eject action
payload = {}
# POST to action
response = self.post_request(self.root_uri + action_uri,
payload)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True,
'msg': "VirtualMedia ejected"}
elif uri and not eject:
# already ejected: return success but changed=False
return {'ret': True, 'changed': False,
'msg': "VirtualMedia image '%s' already ejected" %
image_url}
else:
# return failure (no resources matching image_url found)
return {'ret': False, 'changed': False,
'msg': "No VirtualMedia resource found with image '%s' "
"inserted" % image_url}
def virtual_media_eject(self, options):
if options:
image_url = options.get('image_url')
if image_url: # eject specified one media
return self.virtual_media_eject_one(image_url)
# eject all inserted media when no image_url specified
# read all the VirtualMedia resources
response = self.get_request(self.root_uri + self.manager_uri)
if response['ret'] is False:
return response
data = response['data']
if 'VirtualMedia' not in data:
return {'ret': False, 'msg': "VirtualMedia resource not found"}
virt_media_uri = data["VirtualMedia"]["@odata.id"]
response = self.get_request(self.root_uri + virt_media_uri)
if response['ret'] is False:
return response
data = response['data']
virt_media_list = []
for member in data[u'Members']:
virt_media_list.append(member[u'@odata.id'])
resources, headers = self._read_virt_media_resources(virt_media_list)
# eject all inserted media one by one
ejected_media_list = []
for uri, data in resources.items():
if data.get('Image') and data.get('Inserted', True):
returndict = self.virtual_media_eject_one(data.get('Image'))
if not returndict['ret']:
return returndict
ejected_media_list.append(data.get('Image'))
if len(ejected_media_list) == 0:
# no media inserted: return success but changed=False
return {'ret': True, 'changed': False,
'msg': "No VirtualMedia image inserted"}
else:
return {'ret': True, 'changed': True,
'msg': "VirtualMedia %s ejected" % str(ejected_media_list)}
def raw_get_resource(self, resource_uri):
if resource_uri is None:
return {'ret': False, 'msg': "resource_uri is missing"}
response = self.get_request(self.root_uri + resource_uri)
if response['ret'] is False:
return response
data = response['data']
return {'ret': True, 'data': data}
def raw_get_collection_resource(self, resource_uri):
if resource_uri is None:
return {'ret': False, 'msg': "resource_uri is missing"}
response = self.get_request(self.root_uri + resource_uri)
if response['ret'] is False:
return response
if 'Members' not in response['data']:
return {'ret': False, 'msg': "Specified resource_uri doesn't have Members property"}
member_list = [i['@odata.id'] for i in response['data'].get('Members', [])]
# get member resource one by one
data_list = []
for member_uri in member_list:
uri = self.root_uri + member_uri
response = self.get_request(uri)
if response['ret'] is False:
return response
data = response['data']
data_list.append(data)
return {'ret': True, 'data_list': data_list}
def raw_patch_resource(self, resource_uri, request_body):
if resource_uri is None:
return {'ret': False, 'msg': "resource_uri is missing"}
if request_body is None:
return {'ret': False, 'msg': "request_body is missing"}
# check whether resource_uri existing or not
response = self.get_request(self.root_uri + resource_uri)
if response['ret'] is False:
return response
original_etag = response['data']['@odata.etag']
# check validity of keys in request_body
data = response['data']
for key in request_body.keys():
if key not in data:
return {'ret': False, 'msg': "Key %s not found. Supported key list: %s" % (key, str(data.keys()))}
# perform patch
response = self.patch_request(self.root_uri + resource_uri, request_body)
if response['ret'] is False:
return response
# check whether changed or not
current_etag = ''
if 'data' in response and '@odata.etag' in response['data']:
current_etag = response['data']['@odata.etag']
if current_etag != original_etag:
return {'ret': True, 'changed': True}
else:
return {'ret': True, 'changed': False}
def raw_post_resource(self, resource_uri, request_body):
if resource_uri is None:
return {'ret': False, 'msg': "resource_uri is missing"}
if '/Actions/' not in resource_uri:
return {'ret': False, 'msg': "Bad uri %s. Keyword /Actions/ should be included in uri" % resource_uri}
if request_body is None:
return {'ret': False, 'msg': "request_body is missing"}
# get action base uri data for further checking
action_base_uri = resource_uri.split('/Actions/')[0]
response = self.get_request(self.root_uri + action_base_uri)
if response['ret'] is False:
return response
if 'Actions' not in response['data']:
return {'ret': False, 'msg': "Actions property not found in %s" % action_base_uri}
# check resouce_uri with target uri found in action base uri data
action_found = False
action_info_uri = None
action_target_uri_list = []
for key in response['data']['Actions'].keys():
if action_found:
break
if not key.startswith('#'):
continue
if 'target' in response['data']['Actions'][key]:
if resource_uri == response['data']['Actions'][key]['target']:
action_found = True
if '@Redfish.ActionInfo' in response['data']['Actions'][key]:
action_info_uri = response['data']['Actions'][key]['@Redfish.ActionInfo']
else:
action_target_uri_list.append(response['data']['Actions'][key]['target'])
if not action_found and 'Oem' in response['data']['Actions']:
for key in response['data']['Actions']['Oem'].keys():
if action_found:
break
if not key.startswith('#'):
continue
if 'target' in response['data']['Actions']['Oem'][key]:
if resource_uri == response['data']['Actions']['Oem'][key]['target']:
action_found = True
if '@Redfish.ActionInfo' in response['data']['Actions']['Oem'][key]:
action_info_uri = response['data']['Actions']['Oem'][key]['@Redfish.ActionInfo']
else:
action_target_uri_list.append(response['data']['Actions']['Oem'][key]['target'])
if not action_found:
return {'ret': False,
'msg': 'Specified resource_uri is not a supported action target uri, please specify a supported target uri instead. Supported uri: %s'
% (str(action_target_uri_list))}
# check request_body with parameter name defined by @Redfish.ActionInfo
if action_info_uri is not None:
response = self.get_request(self.root_uri + action_info_uri)
if response['ret'] is False:
return response
for key in request_body.keys():
key_found = False
for para in response['data']['Parameters']:
if key == para['Name']:
key_found = True
break
if not key_found:
return {'ret': False,
'msg': 'Invalid property %s found in request_body. Please refer to @Redfish.ActionInfo Parameters: %s'
% (key, str(response['data']['Parameters']))}
# perform post
response = self.post_request(self.root_uri + resource_uri, request_body)
if response['ret'] is False:
return response
return {'ret': True, 'changed': True}
# More will be added as module features are expanded
CATEGORY_COMMANDS_ALL = {
"Manager": ["VirtualMediaInsert",
"VirtualMediaEject"],
"Raw": ["GetResource",
"GetCollectionResource",
"PatchResource",
"PostResource"]
}
def main():
result = {}
module = AnsibleModule(
argument_spec=dict(
category=dict(required=True),
command=dict(required=True, type='list', elements='str'),
baseuri=dict(required=True),
username=dict(),
password=dict(no_log=True),
auth_token=dict(no_log=True),
timeout=dict(type='int', default=10),
resource_id=dict(),
virtual_media=dict(
type='dict',
options=dict(
media_types=dict(type='list', elements='str', default=[]),
image_url=dict(),
inserted=dict(type='bool', default=True),
write_protected=dict(type='bool', default=True),
username=dict(),
password=dict(no_log=True),
transfer_protocol_type=dict(),
transfer_method=dict(),
)
),
resource_uri=dict(),
request_body=dict(
type='dict',
),
),
required_together=[
('username', 'password'),
],
required_one_of=[
('username', 'auth_token'),
],
mutually_exclusive=[
('username', 'auth_token'),
],
supports_check_mode=False
)
category = module.params['category']
command_list = module.params['command']
# admin credentials used for authentication
creds = {'user': module.params['username'],
'pswd': module.params['password'],
'token': module.params['auth_token']}
# timeout
timeout = module.params['timeout']
# System, Manager or Chassis ID to modify
resource_id = module.params['resource_id']
# VirtualMedia options
virtual_media = module.params['virtual_media']
# resource_uri
resource_uri = module.params['resource_uri']
# request_body
request_body = module.params['request_body']
# Build root URI
root_uri = "https://" + module.params['baseuri']
rf_utils = XCCRedfishUtils(creds, root_uri, timeout, module, resource_id=resource_id, data_modification=True)
# Check that Category is valid
if category not in CATEGORY_COMMANDS_ALL:
module.fail_json(msg=to_native("Invalid Category '%s'. Valid Categories = %s" % (category, CATEGORY_COMMANDS_ALL.keys())))
# Check that all commands are valid
for cmd in command_list:
# Fail if even one command given is invalid
if cmd not in CATEGORY_COMMANDS_ALL[category]:
module.fail_json(msg=to_native("Invalid Command '%s'. Valid Commands = %s" % (cmd, CATEGORY_COMMANDS_ALL[category])))
# Organize by Categories / Commands
if category == "Manager":
# execute only if we find a Manager service resource
result = rf_utils._find_managers_resource()
if result['ret'] is False:
module.fail_json(msg=to_native(result['msg']))
for command in command_list:
if command == 'VirtualMediaInsert':
result = rf_utils.virtual_media_insert(virtual_media)
elif command == 'VirtualMediaEject':
result = rf_utils.virtual_media_eject(virtual_media)
elif category == "Raw":
for command in command_list:
if command == 'GetResource':
result = rf_utils.raw_get_resource(resource_uri)
elif command == 'GetCollectionResource':
result = rf_utils.raw_get_collection_resource(resource_uri)
elif command == 'PatchResource':
result = rf_utils.raw_patch_resource(resource_uri, request_body)
elif command == 'PostResource':
result = rf_utils.raw_post_resource(resource_uri, request_body)
# Return data back or fail with proper message
if result['ret'] is True:
if command == 'GetResource' or command == 'GetCollectionResource':
module.exit_json(redfish_facts=result)
else:
changed = result.get('changed', True)
msg = result.get('msg', 'Action was successful')
module.exit_json(changed=changed, msg=msg)
else:
module.fail_json(msg=to_native(result['msg']))
if __name__ == '__main__':
main()

View File

@@ -569,7 +569,7 @@ def endpoint_list_spec():
provider=dict(type='dict', options=endpoint_argument_spec()),
metrics=dict(type='dict', options=endpoint_argument_spec()),
alerts=dict(type='dict', options=endpoint_argument_spec()),
ssh_keypair=dict(type='dict', options=endpoint_argument_spec()),
ssh_keypair=dict(type='dict', options=endpoint_argument_spec(), no_log=False),
)

View File

@@ -24,6 +24,7 @@ options:
name:
description:
- Data Center name.
type: str
options:
description:
- "Retrieve additional information. Options available: 'visualContent'."

View File

@@ -24,6 +24,7 @@ options:
name:
description:
- Enclosure name.
type: str
options:
description:
- "List with options to gather additional information about an Enclosure and related resources.

View File

@@ -24,11 +24,13 @@ options:
- C(present) will ensure data properties are compliant with OneView.
- C(absent) will remove the resource from OneView, if it exists.
- C(default_bandwidth_reset) will reset the network connection template to the default.
type: str
default: present
choices: [present, absent, default_bandwidth_reset]
data:
description:
- List with Ethernet Network properties.
type: dict
required: true
extends_documentation_fragment:
- community.general.oneview

View File

@@ -23,6 +23,7 @@ options:
name:
description:
- Ethernet Network name.
type: str
options:
description:
- "List with options to gather additional information about an Ethernet Network and related resources.

Some files were not shown because too many files have changed in this diff Show More