mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-28 09:26:44 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6621eb8b87 | ||
|
|
f4b4a2813a | ||
|
|
6f2cb85fae | ||
|
|
5cdc70bda9 | ||
|
|
89498d3650 | ||
|
|
c553351563 | ||
|
|
72c1a17bd9 | ||
|
|
694584f907 | ||
|
|
73e2c2eb85 | ||
|
|
f3ddc8757d | ||
|
|
9241b853c0 | ||
|
|
1053b3c658 | ||
|
|
d9daa6b851 | ||
|
|
a876fa0262 | ||
|
|
f64ace97af | ||
|
|
b701b5893f | ||
|
|
24667e12d0 | ||
|
|
9d93760564 | ||
|
|
ec78558559 | ||
|
|
d5c8d7ddcc | ||
|
|
6338048c73 | ||
|
|
92b388817f | ||
|
|
c72b337327 | ||
|
|
e5080b7847 | ||
|
|
079925fe66 | ||
|
|
19a87874f7 | ||
|
|
809cdda9ef | ||
|
|
bec6f732ad | ||
|
|
d2cdca416c | ||
|
|
0f1ccc07c5 | ||
|
|
deb1071666 | ||
|
|
eb9c5eb796 | ||
|
|
5c8504323e | ||
|
|
ab391c2cfa | ||
|
|
a14b525bdc | ||
|
|
996ef6ab49 | ||
|
|
055c8dac9c | ||
|
|
f4a9c7cc8b | ||
|
|
0c1f96290a | ||
|
|
d260f7ffda | ||
|
|
35d81adabf | ||
|
|
10a61c9dc3 | ||
|
|
6f47bcc399 | ||
|
|
7140b456ae | ||
|
|
8c67a5bda9 | ||
|
|
4ae436a8cc | ||
|
|
5f5c07a942 | ||
|
|
1cef1359d0 | ||
|
|
0d28bfb67e | ||
|
|
ef304ed824 | ||
|
|
bf17f289b3 | ||
|
|
0eff87d0be | ||
|
|
f00fabfa48 | ||
|
|
426cbafa06 | ||
|
|
93fe1f9a3e | ||
|
|
4e944772d5 | ||
|
|
50abeee579 | ||
|
|
eccc8d88b6 | ||
|
|
6d2d364a00 | ||
|
|
e781dd3c9b | ||
|
|
362f899a99 |
@@ -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
|
||||
|
||||
14
.github/BOTMETA.yml
vendored
14
.github/BOTMETA.yml
vendored
@@ -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
|
||||
@@ -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 xmadsen renxulei
|
||||
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
|
||||
|
||||
168
CHANGELOG.rst
168
CHANGELOG.rst
@@ -6,6 +6,170 @@ Community General Release Notes
|
||||
|
||||
This changelog describes changes after version 1.0.0.
|
||||
|
||||
v2.5.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release for some bugs discovered right after the 2.5.0 release.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- funcd connection plugin - can now load (https://github.com/ansible-collections/community.general/pull/2235).
|
||||
- jira - fixed calling of ``isinstance`` (https://github.com/ansible-collections/community.general/issues/2234).
|
||||
|
||||
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
|
||||
======
|
||||
|
||||
@@ -432,7 +596,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).
|
||||
@@ -579,7 +743,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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -1629,3 +1629,190 @@ releases:
|
||||
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'
|
||||
2.5.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- funcd connection plugin - can now load (https://github.com/ansible-collections/community.general/pull/2235).
|
||||
- jira - fixed calling of ``isinstance`` (https://github.com/ansible-collections/community.general/issues/2234).
|
||||
release_summary: Bugfix release for some bugs discovered right after the 2.5.0
|
||||
release.
|
||||
fragments:
|
||||
- 2.5.1.yml
|
||||
- 2236-jira-isinstance.yml
|
||||
- allow_funcd_to_load.yml
|
||||
release_date: '2021-04-14'
|
||||
|
||||
72
commit-rights.md
Normal file
72
commit-rights.md
Normal 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 | |
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: general
|
||||
version: 2.3.0
|
||||
version: 2.5.1
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
||||
@@ -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
91
plugins/become/sudosu.py
Normal 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)])
|
||||
234
plugins/callback/loganalytics.py
Normal file
234
plugins/callback/loganalytics.py
Normal 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)
|
||||
)
|
||||
@@ -37,12 +37,13 @@ import tempfile
|
||||
import shutil
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
from ansible.utils.display import Display
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class Connection(object):
|
||||
class Connection(ConnectionBase):
|
||||
''' Func-based connections '''
|
||||
|
||||
has_pipelining = False
|
||||
|
||||
@@ -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
|
||||
|
||||
24
plugins/filter/dict.py
Normal file
24
plugins/filter/dict.py
Normal 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,
|
||||
}
|
||||
28
plugins/filter/path_join_shim.py
Normal file
28
plugins/filter/path_join_shim.py
Normal 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,
|
||||
}
|
||||
@@ -19,6 +19,7 @@ DOCUMENTATION = '''
|
||||
- Will retrieve the first network interface with an IP for Proxmox nodes.
|
||||
- Can retrieve LXC/QEMU configuration as facts.
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
options:
|
||||
plugin:
|
||||
@@ -69,6 +70,14 @@ DOCUMENTATION = '''
|
||||
description: Gather LXC/QEMU configuration facts.
|
||||
default: no
|
||||
type: bool
|
||||
strict:
|
||||
version_added: 2.5.0
|
||||
compose:
|
||||
version_added: 2.5.0
|
||||
groups:
|
||||
version_added: 2.5.0
|
||||
keyed_groups:
|
||||
version_added: 2.5.0
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
@@ -78,6 +87,15 @@ url: http://localhost:8006
|
||||
user: ansible@pve
|
||||
password: secure
|
||||
validate_certs: no
|
||||
keyed_groups:
|
||||
- key: proxmox_tags_parsed
|
||||
separator: ""
|
||||
prefix: group
|
||||
groups:
|
||||
webservers: "'web' in (proxmox_tags_parsed|list)"
|
||||
mailservers: "'mail' in (proxmox_tags_parsed|list)"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
'''
|
||||
|
||||
import re
|
||||
@@ -86,7 +104,7 @@ from ansible.module_utils.common._collections_compat import MutableMapping
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
|
||||
# 3rd party imports
|
||||
@@ -99,7 +117,7 @@ except ImportError:
|
||||
HAS_REQUESTS = False
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
''' Host inventory parser for ansible using Proxmox as source. '''
|
||||
|
||||
NAME = 'community.general.proxmox'
|
||||
@@ -206,9 +224,36 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _get_agent_network_interfaces(self, node, vmid, vmtype):
|
||||
result = []
|
||||
|
||||
try:
|
||||
ifaces = self._get_json(
|
||||
"%s/api2/json/nodes/%s/%s/%s/agent/network-get-interfaces" % (
|
||||
self.proxmox_url, node, vmtype, vmid
|
||||
)
|
||||
)['result']
|
||||
|
||||
for iface in ifaces:
|
||||
result.append({
|
||||
'name': iface['name'],
|
||||
'mac-address': iface['hardware-address'],
|
||||
'ip-addresses': [
|
||||
"%s/%s" % (ip['ip-address'], ip['prefix']) for ip in iface['ip-addresses']
|
||||
]
|
||||
})
|
||||
except requests.HTTPError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
def _get_vm_config(self, node, vmid, vmtype, name):
|
||||
ret = self._get_json("%s/api2/json/nodes/%s/%s/%s/config" % (self.proxmox_url, node, vmtype, vmid))
|
||||
|
||||
node_key = 'node'
|
||||
node_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), node_key.lower()))
|
||||
self.inventory.set_variable(name, node_key, node)
|
||||
|
||||
vmid_key = 'vmid'
|
||||
vmid_key = self.to_safe('%s%s' % (self.get_option('facts_prefix'), vmid_key.lower()))
|
||||
self.inventory.set_variable(name, vmid_key, vmid)
|
||||
@@ -236,6 +281,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
parsed_value = [tag.strip() for tag in value.split(",")]
|
||||
self.inventory.set_variable(name, parsed_key, parsed_value)
|
||||
|
||||
if config == 'agent' and int(value):
|
||||
agent_iface_key = self.to_safe('%s%s' % (key, "_interfaces"))
|
||||
agent_iface_value = self._get_agent_network_interfaces(node, vmid, vmtype)
|
||||
if agent_iface_value:
|
||||
self.inventory.set_variable(name, agent_iface_key, agent_iface_value)
|
||||
|
||||
if not (isinstance(value, int) or ',' not in value):
|
||||
# split off strings with commas to a dict
|
||||
# skip over any keys that cannot be processed
|
||||
@@ -264,6 +315,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
regex = r"[^A-Za-z0-9\_]"
|
||||
return re.sub(regex, "_", word.replace(" ", ""))
|
||||
|
||||
def _apply_constructable(self, name, variables):
|
||||
strict = self.get_option('strict')
|
||||
self._add_host_to_composed_groups(self.get_option('groups'), variables, name, strict=strict)
|
||||
self._add_host_to_keyed_groups(self.get_option('keyed_groups'), variables, name, strict=strict)
|
||||
self._set_composite_vars(self.get_option('compose'), variables, name, strict=strict)
|
||||
|
||||
def _populate(self):
|
||||
|
||||
self._get_auth()
|
||||
@@ -318,6 +375,8 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_config(node['node'], lxc['vmid'], 'lxc', lxc['name'])
|
||||
|
||||
self._apply_constructable(lxc["name"], self.inventory.get_host(lxc['name']).get_vars())
|
||||
|
||||
# get QEMU vm's for this node
|
||||
node_qemu_group = self.to_safe('%s%s' % (self.get_option('group_prefix'), ('%s_qemu' % node['node']).lower()))
|
||||
self.inventory.add_group(node_qemu_group)
|
||||
@@ -340,6 +399,8 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
if self.get_option('want_facts'):
|
||||
self._get_vm_config(node['node'], qemu['vmid'], 'qemu', qemu['name'])
|
||||
|
||||
self._apply_constructable(qemu["name"], self.inventory.get_host(qemu['name']).get_vars())
|
||||
|
||||
# gather vm's in pools
|
||||
for pool in self._get_pools():
|
||||
if pool.get('poolid'):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -10,6 +10,7 @@ from functools import partial, wraps
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.common.dict_transformations import dict_merge
|
||||
|
||||
|
||||
class ModuleHelperException(Exception):
|
||||
@@ -24,12 +25,12 @@ class ModuleHelperException(Exception):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.msg = self._get_remove('msg', kwargs) or "Module failed with exception: {0}".format(self)
|
||||
self.update_output = self._get_remove('update_output', kwargs) or {}
|
||||
super(ModuleHelperException, self).__init__(*args, **kwargs)
|
||||
super(ModuleHelperException, self).__init__(*args)
|
||||
|
||||
|
||||
class ArgFormat(object):
|
||||
"""
|
||||
Argument formatter
|
||||
Argument formatter for use as a command line parameter. Used in CmdMixin.
|
||||
"""
|
||||
BOOLEAN = 0
|
||||
PRINTF = 1
|
||||
@@ -50,7 +51,8 @@ class ArgFormat(object):
|
||||
|
||||
def __init__(self, name, fmt=None, style=FORMAT, stars=0):
|
||||
"""
|
||||
Creates a new formatter
|
||||
Creates a CLI-formatter for one specific argument. The argument may be a module parameter or just a named parameter for
|
||||
the CLI command execution.
|
||||
:param name: Name of the argument to be formatted
|
||||
:param fmt: Either a str to be formatted (using or not printf-style) or a callable that does that
|
||||
:param style: Whether arg_format (as str) should use printf-style formatting.
|
||||
@@ -99,18 +101,27 @@ class ArgFormat(object):
|
||||
return [str(p) for p in func(value)]
|
||||
|
||||
|
||||
def cause_changes(func, on_success=True, on_failure=False):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
if on_success:
|
||||
self.changed = True
|
||||
except Exception as e:
|
||||
if on_failure:
|
||||
self.changed = True
|
||||
raise
|
||||
return wrapper
|
||||
def cause_changes(on_success=None, on_failure=None):
|
||||
|
||||
def deco(func):
|
||||
if on_success is None and on_failure is None:
|
||||
return func
|
||||
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
self = args[0]
|
||||
func(*args, **kwargs)
|
||||
if on_success is not None:
|
||||
self.changed = on_success
|
||||
except Exception:
|
||||
if on_failure is not None:
|
||||
self.changed = on_failure
|
||||
raise
|
||||
|
||||
return wrapper
|
||||
|
||||
return deco
|
||||
|
||||
|
||||
def module_fails_on_exception(func):
|
||||
@@ -123,11 +134,12 @@ def module_fails_on_exception(func):
|
||||
except ModuleHelperException as e:
|
||||
if e.update_output:
|
||||
self.update_output(e.update_output)
|
||||
self.module.fail_json(changed=False, msg=e.msg, exception=traceback.format_exc(), output=self.output, vars=self.vars)
|
||||
self.module.fail_json(msg=e.msg, exception=traceback.format_exc(),
|
||||
output=self.output, vars=self.vars.output(), **self.output)
|
||||
except Exception as e:
|
||||
self.vars.msg = "Module failed with exception: {0}".format(str(e).strip())
|
||||
self.vars.exception = traceback.format_exc()
|
||||
self.module.fail_json(changed=False, msg=self.vars.msg, exception=self.vars.exception, output=self.output, vars=self.vars)
|
||||
msg = "Module failed with exception: {0}".format(str(e).strip())
|
||||
self.module.fail_json(msg=msg, exception=traceback.format_exc(),
|
||||
output=self.output, vars=self.vars.output(), **self.output)
|
||||
return wrapper
|
||||
|
||||
|
||||
@@ -141,7 +153,7 @@ class DependencyCtxMgr(object):
|
||||
self.exc_tb = None
|
||||
|
||||
def __enter__(self):
|
||||
pass
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.has_it = exc_type is None
|
||||
@@ -155,32 +167,157 @@ class DependencyCtxMgr(object):
|
||||
return self.msg or str(self.exc_val)
|
||||
|
||||
|
||||
class ModuleHelper(object):
|
||||
_dependencies = []
|
||||
module = {}
|
||||
facts_name = None
|
||||
class VarMeta(object):
|
||||
NOTHING = object()
|
||||
|
||||
def __init__(self, diff=False, output=True, change=None, fact=False):
|
||||
self.init = False
|
||||
self.initial_value = None
|
||||
self.value = None
|
||||
|
||||
self.diff = diff
|
||||
self.change = diff if change is None else change
|
||||
self.output = output
|
||||
self.fact = fact
|
||||
|
||||
def set(self, diff=None, output=None, change=None, fact=None, initial_value=NOTHING):
|
||||
if diff is not None:
|
||||
self.diff = diff
|
||||
if output is not None:
|
||||
self.output = output
|
||||
if change is not None:
|
||||
self.change = change
|
||||
if fact is not None:
|
||||
self.fact = fact
|
||||
if initial_value is not self.NOTHING:
|
||||
self.initial_value = initial_value
|
||||
|
||||
def set_value(self, value):
|
||||
if not self.init:
|
||||
self.initial_value = value
|
||||
self.init = True
|
||||
self.value = value
|
||||
return self
|
||||
|
||||
@property
|
||||
def has_changed(self):
|
||||
return self.change and (self.initial_value != self.value)
|
||||
|
||||
@property
|
||||
def diff_result(self):
|
||||
return None if not (self.diff and self.has_changed) else {
|
||||
'before': self.initial_value,
|
||||
'after': self.value,
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
return "<VarMeta: value={0}, initial={1}, diff={2}, output={3}, change={4}>".format(
|
||||
self.value, self.initial_value, self.diff, self.output, self.change
|
||||
)
|
||||
|
||||
|
||||
class ModuleHelper(object):
|
||||
_output_conflict_list = ('msg', 'exception', 'output', 'vars', 'changed')
|
||||
_dependencies = []
|
||||
module = None
|
||||
facts_name = None
|
||||
output_params = ()
|
||||
diff_params = ()
|
||||
change_params = ()
|
||||
facts_params = ()
|
||||
|
||||
class VarDict(object):
|
||||
def __init__(self):
|
||||
self._data = dict()
|
||||
self._meta = dict()
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._data[item]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.set(key, value)
|
||||
|
||||
class AttrDict(dict):
|
||||
def __getattr__(self, item):
|
||||
return self[item]
|
||||
try:
|
||||
return self._data[item]
|
||||
except KeyError:
|
||||
return getattr(self._data, item)
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if key in ('_data', '_meta'):
|
||||
super(ModuleHelper.VarDict, self).__setattr__(key, value)
|
||||
else:
|
||||
self.set(key, value)
|
||||
|
||||
def meta(self, name):
|
||||
return self._meta[name]
|
||||
|
||||
def set_meta(self, name, **kwargs):
|
||||
self.meta(name).set(**kwargs)
|
||||
|
||||
def set(self, name, value, **kwargs):
|
||||
if name in ('_data', '_meta'):
|
||||
raise ValueError("Names _data and _meta are reserved for use by ModuleHelper")
|
||||
self._data[name] = value
|
||||
if name in self._meta:
|
||||
meta = self.meta(name)
|
||||
else:
|
||||
meta = VarMeta(**kwargs)
|
||||
meta.set_value(value)
|
||||
self._meta[name] = meta
|
||||
|
||||
def output(self):
|
||||
return dict((k, v) for k, v in self._data.items() if self.meta(k).output)
|
||||
|
||||
def diff(self):
|
||||
diff_results = [(k, self.meta(k).diff_result) for k in self._data]
|
||||
diff_results = [dr for dr in diff_results if dr[1] is not None]
|
||||
if diff_results:
|
||||
before = dict((dr[0], dr[1]['before']) for dr in diff_results)
|
||||
after = dict((dr[0], dr[1]['after']) for dr in diff_results)
|
||||
return {'before': before, 'after': after}
|
||||
return None
|
||||
|
||||
def facts(self):
|
||||
facts_result = dict((k, v) for k, v in self._data.items() if self._meta[k].fact)
|
||||
return facts_result if facts_result else None
|
||||
|
||||
def change_vars(self):
|
||||
return [v for v in self._data if self.meta(v).change]
|
||||
|
||||
def has_changed(self, v):
|
||||
return self._meta[v].has_changed
|
||||
|
||||
def __init__(self, module=None):
|
||||
self.vars = ModuleHelper.AttrDict()
|
||||
self.output_dict = dict()
|
||||
self.facts_dict = dict()
|
||||
self.vars = ModuleHelper.VarDict()
|
||||
self._changed = False
|
||||
|
||||
if module:
|
||||
self.module = module
|
||||
|
||||
if isinstance(self.module, dict):
|
||||
if not isinstance(self.module, AnsibleModule):
|
||||
self.module = AnsibleModule(**self.module)
|
||||
|
||||
for name, value in self.module.params.items():
|
||||
self.vars.set(
|
||||
name, value,
|
||||
diff=name in self.diff_params,
|
||||
output=name in self.output_params,
|
||||
change=None if not self.change_params else name in self.change_params,
|
||||
fact=name in self.facts_params,
|
||||
)
|
||||
|
||||
def update_vars(self, meta=None, **kwargs):
|
||||
if meta is None:
|
||||
meta = {}
|
||||
for k, v in kwargs.items():
|
||||
self.vars.set(k, v, **meta)
|
||||
|
||||
def update_output(self, **kwargs):
|
||||
self.output_dict.update(kwargs)
|
||||
self.update_vars(meta={"output": True}, **kwargs)
|
||||
|
||||
def update_facts(self, **kwargs):
|
||||
self.facts_dict.update(kwargs)
|
||||
self.update_vars(meta={"fact": True}, **kwargs)
|
||||
|
||||
def __init_module__(self):
|
||||
pass
|
||||
@@ -191,6 +328,9 @@ class ModuleHelper(object):
|
||||
def __quit_module__(self):
|
||||
pass
|
||||
|
||||
def _vars_changed(self):
|
||||
return any(self.vars.has_changed(v) for v in self.vars.change_vars())
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
return self._changed
|
||||
@@ -199,12 +339,25 @@ class ModuleHelper(object):
|
||||
def changed(self, value):
|
||||
self._changed = value
|
||||
|
||||
def has_changed(self):
|
||||
return self.changed or self._vars_changed()
|
||||
|
||||
@property
|
||||
def output(self):
|
||||
result = dict(self.vars)
|
||||
result.update(self.output_dict)
|
||||
result = dict(self.vars.output())
|
||||
if self.facts_name:
|
||||
result['ansible_facts'] = {self.facts_name: self.facts_dict}
|
||||
facts = self.vars.facts()
|
||||
if facts is not None:
|
||||
result['ansible_facts'] = {self.facts_name: facts}
|
||||
if self.module._diff:
|
||||
diff = result.get('diff', {})
|
||||
vars_diff = self.vars.diff() or {}
|
||||
result['diff'] = dict_merge(dict(diff), vars_diff)
|
||||
|
||||
for varname in result:
|
||||
if varname in self._output_conflict_list:
|
||||
result["_" + varname] = result[varname]
|
||||
del result[varname]
|
||||
return result
|
||||
|
||||
@module_fails_on_exception
|
||||
@@ -213,7 +366,7 @@ class ModuleHelper(object):
|
||||
self.__init_module__()
|
||||
self.__run__()
|
||||
self.__quit_module__()
|
||||
self.module.exit_json(changed=self.changed, **self.output_dict)
|
||||
self.module.exit_json(changed=self.has_changed(), **self.output)
|
||||
|
||||
@classmethod
|
||||
def dependency(cls, name, msg):
|
||||
@@ -224,9 +377,9 @@ class ModuleHelper(object):
|
||||
for d in self._dependencies:
|
||||
if not d.has_it:
|
||||
self.module.fail_json(changed=False,
|
||||
exception=d.exc_val.__traceback__.format_exc(),
|
||||
exception="\n".join(traceback.format_exception(d.exc_type, d.exc_val, d.exc_tb)),
|
||||
msg=d.text,
|
||||
**self.output_dict)
|
||||
**self.output)
|
||||
|
||||
|
||||
class StateMixin(object):
|
||||
@@ -332,7 +485,7 @@ class CmdMixin(object):
|
||||
return rc, out, err
|
||||
|
||||
def run_command(self, extra_params=None, params=None, *args, **kwargs):
|
||||
self.vars['cmd_args'] = self._calculate_args(extra_params, params)
|
||||
self.vars.cmd_args = self._calculate_args(extra_params, params)
|
||||
options = dict(self.run_command_fixed_options)
|
||||
env_update = dict(options.get('environ_update', {}))
|
||||
options['check_rc'] = options.get('check_rc', self.check_rc)
|
||||
@@ -341,7 +494,7 @@ class CmdMixin(object):
|
||||
self.update_output(force_lang=self.force_lang)
|
||||
options['environ_update'] = env_update
|
||||
options.update(kwargs)
|
||||
rc, out, err = self.module.run_command(self.vars['cmd_args'], *args, **options)
|
||||
rc, out, err = self.module.run_command(self.vars.cmd_args, *args, **options)
|
||||
self.update_output(rc=rc, stdout=out, stderr=err)
|
||||
return self.process_command_output(rc, out, err)
|
||||
|
||||
|
||||
@@ -57,6 +57,34 @@ def _get_pritunl_organizations(api_token, api_secret, base_url, validate_certs=T
|
||||
)
|
||||
|
||||
|
||||
def _delete_pritunl_organization(
|
||||
api_token, api_secret, base_url, organization_id, validate_certs=True
|
||||
):
|
||||
return pritunl_auth_request(
|
||||
base_url=base_url,
|
||||
api_token=api_token,
|
||||
api_secret=api_secret,
|
||||
method="DELETE",
|
||||
path="/organization/%s" % (organization_id),
|
||||
validate_certs=validate_certs,
|
||||
)
|
||||
|
||||
|
||||
def _post_pritunl_organization(
|
||||
api_token, api_secret, base_url, organization_data, validate_certs=True
|
||||
):
|
||||
return pritunl_auth_request(
|
||||
api_token=api_token,
|
||||
api_secret=api_secret,
|
||||
base_url=base_url,
|
||||
method="POST",
|
||||
path="/organization/%s",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(organization_data),
|
||||
validate_certs=validate_certs,
|
||||
)
|
||||
|
||||
|
||||
def _get_pritunl_users(
|
||||
api_token, api_secret, base_url, organization_id, validate_certs=True
|
||||
):
|
||||
@@ -179,6 +207,29 @@ def list_pritunl_users(
|
||||
return users
|
||||
|
||||
|
||||
def post_pritunl_organization(
|
||||
api_token,
|
||||
api_secret,
|
||||
base_url,
|
||||
organization_name,
|
||||
validate_certs=True,
|
||||
):
|
||||
response = _post_pritunl_organization(
|
||||
api_token=api_token,
|
||||
api_secret=api_secret,
|
||||
base_url=base_url,
|
||||
organization_data={"name": organization_name},
|
||||
validate_certs=True,
|
||||
)
|
||||
|
||||
if response.getcode() != 200:
|
||||
raise PritunlException(
|
||||
"Could not add organization %s to Pritunl" % (organization_name)
|
||||
)
|
||||
# The user PUT request returns the updated user object
|
||||
return json.loads(response.read())
|
||||
|
||||
|
||||
def post_pritunl_user(
|
||||
api_token,
|
||||
api_secret,
|
||||
@@ -227,6 +278,25 @@ def post_pritunl_user(
|
||||
return json.loads(response.read())
|
||||
|
||||
|
||||
def delete_pritunl_organization(
|
||||
api_token, api_secret, base_url, organization_id, validate_certs=True
|
||||
):
|
||||
response = _delete_pritunl_organization(
|
||||
api_token=api_token,
|
||||
api_secret=api_secret,
|
||||
base_url=base_url,
|
||||
organization_id=organization_id,
|
||||
validate_certs=True,
|
||||
)
|
||||
|
||||
if response.getcode() != 200:
|
||||
raise PritunlException(
|
||||
"Could not remove organization %s from Pritunl" % (organization_id)
|
||||
)
|
||||
|
||||
return json.loads(response.read())
|
||||
|
||||
|
||||
def delete_pritunl_user(
|
||||
api_token, api_secret, base_url, organization_id, user_id, validate_certs=True
|
||||
):
|
||||
|
||||
@@ -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='')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
276
plugins/modules/cloud/opennebula/one_template.py
Normal file
276
plugins/modules/cloud/opennebula/one_template.py
Normal 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()
|
||||
@@ -162,7 +162,6 @@ def waitForTaskDone(client, name, taskId, timeout):
|
||||
currentTimeout -= 5
|
||||
if currentTimeout < 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -72,10 +72,7 @@ class ImageFacts(object):
|
||||
self.filters = module.params['filters']
|
||||
|
||||
def return_all_installed_images(self):
|
||||
cmd = [self.module.get_bin_path('imgadm')]
|
||||
|
||||
cmd.append('list')
|
||||
cmd.append('-j')
|
||||
cmd = [self.module.get_bin_path('imgadm'), 'list', '-j']
|
||||
|
||||
if self.filters:
|
||||
cmd.append(self.filters)
|
||||
|
||||
@@ -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'),
|
||||
)
|
||||
|
||||
|
||||
@@ -36,13 +36,13 @@ seealso:
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: Get info for job awx
|
||||
community.general.nomad_job:
|
||||
community.general.nomad_job_info:
|
||||
host: localhost
|
||||
name: awx
|
||||
register: result
|
||||
|
||||
- name: List Nomad jobs
|
||||
community.general.nomad_job:
|
||||
community.general.nomad_job_info:
|
||||
host: localhost
|
||||
register: result
|
||||
|
||||
|
||||
@@ -170,25 +170,23 @@ def install_plugin(module, plugin_bin, plugin_name, url, timeout, allow_root, ki
|
||||
cmd_args = [plugin_bin, "plugin", PACKAGE_STATE_MAP["present"], plugin_name]
|
||||
|
||||
if url:
|
||||
cmd_args.append("--url %s" % url)
|
||||
cmd_args.extend(["--url", url])
|
||||
|
||||
if timeout:
|
||||
cmd_args.append("--timeout %s" % timeout)
|
||||
cmd_args.extend(["--timeout", timeout])
|
||||
|
||||
if allow_root:
|
||||
cmd_args.append('--allow-root')
|
||||
|
||||
cmd = " ".join(cmd_args)
|
||||
|
||||
if module.check_mode:
|
||||
return True, cmd, "check mode", ""
|
||||
return True, " ".join(cmd_args), "check mode", ""
|
||||
|
||||
rc, out, err = module.run_command(cmd)
|
||||
rc, out, err = module.run_command(cmd_args)
|
||||
if rc != 0:
|
||||
reason = parse_error(out)
|
||||
module.fail_json(msg=reason)
|
||||
|
||||
return True, cmd, out, err
|
||||
return True, " ".join(cmd_args), out, err
|
||||
|
||||
|
||||
def remove_plugin(module, plugin_bin, plugin_name, allow_root, kibana_version='4.6'):
|
||||
@@ -201,17 +199,15 @@ def remove_plugin(module, plugin_bin, plugin_name, allow_root, kibana_version='4
|
||||
if allow_root:
|
||||
cmd_args.append('--allow-root')
|
||||
|
||||
cmd = " ".join(cmd_args)
|
||||
|
||||
if module.check_mode:
|
||||
return True, cmd, "check mode", ""
|
||||
return True, " ".join(cmd_args), "check mode", ""
|
||||
|
||||
rc, out, err = module.run_command(cmd)
|
||||
rc, out, err = module.run_command(cmd_args)
|
||||
if rc != 0:
|
||||
reason = parse_error(out)
|
||||
module.fail_json(msg=reason)
|
||||
|
||||
return True, cmd, out, err
|
||||
return True, " ".join(cmd_args), out, err
|
||||
|
||||
|
||||
def get_kibana_version(module, plugin_bin, allow_root):
|
||||
@@ -220,8 +216,7 @@ def get_kibana_version(module, plugin_bin, allow_root):
|
||||
if allow_root:
|
||||
cmd_args.append('--allow-root')
|
||||
|
||||
cmd = " ".join(cmd_args)
|
||||
rc, out, err = module.run_command(cmd)
|
||||
rc, out, err = module.run_command(cmd_args)
|
||||
if rc != 0:
|
||||
module.fail_json(msg="Failed to get Kibana version : %s" % err)
|
||||
|
||||
@@ -269,7 +264,7 @@ def main():
|
||||
|
||||
if state == "present":
|
||||
if force:
|
||||
remove_plugin(module, plugin_bin, name)
|
||||
remove_plugin(module, plugin_bin, name, allow_root, kibana_version)
|
||||
changed, cmd, out, err = install_plugin(module, plugin_bin, name, url, timeout, allow_root, kibana_version)
|
||||
|
||||
elif state == "absent":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
172
plugins/modules/identity/ipa/ipa_otpconfig.py
Normal file
172
plugins/modules/identity/ipa/ipa_otpconfig.py
Normal 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()
|
||||
527
plugins/modules/identity/ipa/ipa_otptoken.py
Normal file
527
plugins/modules/identity/ipa/ipa_otptoken.py
Normal 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()
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
1
plugins/modules/ipa_otpconfig.py
Symbolic link
1
plugins/modules/ipa_otpconfig.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./identity/ipa/ipa_otpconfig.py
|
||||
1
plugins/modules/ipa_otptoken.py
Symbolic link
1
plugins/modules/ipa_otptoken.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./identity/ipa/ipa_otptoken.py
|
||||
528
plugins/modules/monitoring/spectrum_model_attrs.py
Normal file
528
plugins/modules/monitoring/spectrum_model_attrs.py
Normal 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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
0
plugins/modules/net_tools/pritunl/__init__.py
Normal file
0
plugins/modules/net_tools/pritunl/__init__.py
Normal file
199
plugins/modules/net_tools/pritunl/pritunl_org.py
Normal file
199
plugins/modules/net_tools/pritunl/pritunl_org.py
Normal 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()
|
||||
129
plugins/modules/net_tools/pritunl/pritunl_org_info.py
Normal file
129
plugins/modules/net_tools/pritunl/pritunl_org_info.py
Normal 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()
|
||||
1
plugins/modules/one_template.py
Symbolic link
1
plugins/modules/one_template.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./cloud/opennebula/one_template.py
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
1
plugins/modules/pritunl_org.py
Symbolic link
1
plugins/modules/pritunl_org.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./net_tools/pritunl/pritunl_org.py
|
||||
1
plugins/modules/pritunl_org_info.py
Symbolic link
1
plugins/modules/pritunl_org_info.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./net_tools/pritunl/pritunl_org_info.py
|
||||
@@ -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()
|
||||
@@ -24,6 +24,7 @@ options:
|
||||
name:
|
||||
description:
|
||||
- Data Center name.
|
||||
type: str
|
||||
options:
|
||||
description:
|
||||
- "Retrieve additional information. Options available: 'visualContent'."
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -20,11 +20,13 @@ options:
|
||||
- Indicates the desired state for the Fibre Channel Network resource.
|
||||
C(present) will ensure data properties are compliant with OneView.
|
||||
C(absent) will remove the resource from OneView, if it exists.
|
||||
type: str
|
||||
choices: ['present', 'absent']
|
||||
required: true
|
||||
data:
|
||||
description:
|
||||
- List with the Fibre Channel Network properties.
|
||||
type: dict
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
|
||||
@@ -23,6 +23,7 @@ options:
|
||||
name:
|
||||
description:
|
||||
- Fibre Channel Network name.
|
||||
type: str
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
|
||||
@@ -21,11 +21,13 @@ options:
|
||||
- Indicates the desired state for the FCoE Network resource.
|
||||
C(present) will ensure data properties are compliant with OneView.
|
||||
C(absent) will remove the resource from OneView, if it exists.
|
||||
type: str
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
data:
|
||||
description:
|
||||
- List with FCoE Network properties.
|
||||
type: dict
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
|
||||
@@ -23,6 +23,7 @@ options:
|
||||
name:
|
||||
description:
|
||||
- FCoE Network name.
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
- community.general.oneview.factsparams
|
||||
|
||||
@@ -24,11 +24,13 @@ options:
|
||||
- Indicates the desired state for the Logical Interconnect Group resource.
|
||||
C(absent) will remove the resource from OneView, if it exists.
|
||||
C(present) will ensure data properties are compliant with OneView.
|
||||
type: str
|
||||
choices: [absent, present]
|
||||
default: present
|
||||
data:
|
||||
description:
|
||||
- List with the Logical Interconnect Group properties.
|
||||
type: dict
|
||||
required: true
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
|
||||
@@ -24,6 +24,7 @@ options:
|
||||
name:
|
||||
description:
|
||||
- Logical Interconnect Group name.
|
||||
type: str
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
- community.general.oneview.factsparams
|
||||
|
||||
@@ -23,11 +23,13 @@ options:
|
||||
- Indicates the desired state for the Network Set resource.
|
||||
- C(present) will ensure data properties are compliant with OneView.
|
||||
- C(absent) will remove the resource from OneView, if it exists.
|
||||
type: str
|
||||
default: present
|
||||
choices: ['present', 'absent']
|
||||
data:
|
||||
description:
|
||||
- List with the Network Set properties.
|
||||
type: dict
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
|
||||
@@ -23,6 +23,7 @@ options:
|
||||
name:
|
||||
description:
|
||||
- Network Set name.
|
||||
type: str
|
||||
|
||||
options:
|
||||
description:
|
||||
|
||||
@@ -24,12 +24,14 @@ options:
|
||||
- C(present) ensures data properties are compliant with OneView.
|
||||
- C(absent) removes the resource from OneView, if it exists.
|
||||
- C(connection_information_set) updates the connection information for the SAN Manager. This operation is non-idempotent.
|
||||
type: str
|
||||
default: present
|
||||
choices: [present, absent, connection_information_set]
|
||||
data:
|
||||
description:
|
||||
- List with SAN Manager properties.
|
||||
required: true
|
||||
description:
|
||||
- List with SAN Manager properties.
|
||||
type: dict
|
||||
required: true
|
||||
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
|
||||
@@ -23,6 +23,7 @@ options:
|
||||
provider_display_name:
|
||||
description:
|
||||
- Provider Display Name.
|
||||
type: str
|
||||
params:
|
||||
description:
|
||||
- List of params to delimit, filter and sort the list of resources.
|
||||
@@ -31,6 +32,7 @@ options:
|
||||
- C(count): The number of resources to return.
|
||||
- C(query): A general query string to narrow the list of resources returned.
|
||||
- C(sort): The sort order of the returned data set."
|
||||
type: dict
|
||||
extends_documentation_fragment:
|
||||
- community.general.oneview
|
||||
|
||||
|
||||
@@ -149,8 +149,6 @@ def get_existing_pipeline_variable(module, bitbucket):
|
||||
var['name'] = var.pop('key')
|
||||
return var
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def create_pipeline_variable(module, bitbucket):
|
||||
info, content = bitbucket.request(
|
||||
|
||||
@@ -216,14 +216,13 @@ def main():
|
||||
args = [git_path, "config", "--includes"]
|
||||
if params['list_all']:
|
||||
args.append('-l')
|
||||
if scope:
|
||||
args.append("--" + scope)
|
||||
if name:
|
||||
args.append(name)
|
||||
|
||||
if scope == 'file':
|
||||
args.append('-f')
|
||||
args.append(params['file'])
|
||||
elif scope:
|
||||
args.append("--" + scope)
|
||||
if name:
|
||||
args.append(name)
|
||||
|
||||
if scope == 'local':
|
||||
dir = params['repo']
|
||||
|
||||
@@ -121,9 +121,9 @@ except Exception:
|
||||
|
||||
def authenticate(username=None, password=None, access_token=None):
|
||||
if access_token:
|
||||
return Github(base_url="https://api.github.com:443", login_or_token=access_token)
|
||||
return Github(base_url="https://api.github.com", login_or_token=access_token)
|
||||
else:
|
||||
return Github(base_url="https://api.github.com:443", login_or_token=username, password=password)
|
||||
return Github(base_url="https://api.github.com", login_or_token=username, password=password)
|
||||
|
||||
|
||||
def create_repo(gh, name, organization=None, private=False, description='', check_mode=False):
|
||||
|
||||
1
plugins/modules/spectrum_model_attrs.py
Symbolic link
1
plugins/modules/spectrum_model_attrs.py
Symbolic link
@@ -0,0 +1 @@
|
||||
./monitoring/spectrum_model_attrs.py
|
||||
@@ -175,10 +175,7 @@ class ZFSFacts(object):
|
||||
self.facts = []
|
||||
|
||||
def dataset_exists(self):
|
||||
cmd = [self.module.get_bin_path('zfs')]
|
||||
|
||||
cmd.append('list')
|
||||
cmd.append(self.name)
|
||||
cmd = [self.module.get_bin_path('zfs'), 'list', self.name]
|
||||
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
|
||||
@@ -188,10 +185,7 @@ class ZFSFacts(object):
|
||||
return False
|
||||
|
||||
def get_facts(self):
|
||||
cmd = [self.module.get_bin_path('zfs')]
|
||||
|
||||
cmd.append('get')
|
||||
cmd.append('-H')
|
||||
cmd = [self.module.get_bin_path('zfs'), 'get', '-H']
|
||||
if self.parsable:
|
||||
cmd.append('-p')
|
||||
if self.recurse:
|
||||
@@ -202,10 +196,7 @@ class ZFSFacts(object):
|
||||
if self.type:
|
||||
cmd.append('-t')
|
||||
cmd.append(self.type)
|
||||
cmd.append('-o')
|
||||
cmd.append('name,property,value')
|
||||
cmd.append(self.properties)
|
||||
cmd.append(self.name)
|
||||
cmd.extend(['-o', 'name,property,value', self.properties, self.name])
|
||||
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
|
||||
|
||||
@@ -134,10 +134,7 @@ class ZPoolFacts(object):
|
||||
self.facts = []
|
||||
|
||||
def pool_exists(self):
|
||||
cmd = [self.module.get_bin_path('zpool')]
|
||||
|
||||
cmd.append('list')
|
||||
cmd.append(self.name)
|
||||
cmd = [self.module.get_bin_path('zpool'), 'list', self.name]
|
||||
|
||||
(rc, out, err) = self.module.run_command(cmd)
|
||||
|
||||
@@ -147,10 +144,7 @@ class ZPoolFacts(object):
|
||||
return False
|
||||
|
||||
def get_facts(self):
|
||||
cmd = [self.module.get_bin_path('zpool')]
|
||||
|
||||
cmd.append('get')
|
||||
cmd.append('-H')
|
||||
cmd = [self.module.get_bin_path('zpool'), 'get', '-H']
|
||||
if self.parsable:
|
||||
cmd.append('-p')
|
||||
cmd.append('-o')
|
||||
|
||||
@@ -154,9 +154,7 @@ class BE(object):
|
||||
self.is_freebsd = os.uname()[0] == 'FreeBSD'
|
||||
|
||||
def _beadm_list(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
cmd.append('list')
|
||||
cmd.append('-H')
|
||||
cmd = [self.module.get_bin_path('beadm'), 'list', '-H']
|
||||
if '@' in self.name:
|
||||
cmd.append('-s')
|
||||
return self.module.run_command(cmd)
|
||||
@@ -218,42 +216,26 @@ class BE(object):
|
||||
return False
|
||||
|
||||
def activate_be(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
|
||||
cmd.append('activate')
|
||||
cmd.append(self.name)
|
||||
|
||||
cmd = [self.module.get_bin_path('beadm'), 'activate', self.name]
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def create_be(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
|
||||
cmd.append('create')
|
||||
cmd = [self.module.get_bin_path('beadm'), 'create']
|
||||
|
||||
if self.snapshot:
|
||||
cmd.append('-e')
|
||||
cmd.append(self.snapshot)
|
||||
|
||||
cmd.extend(['-e', self.snapshot])
|
||||
if not self.is_freebsd:
|
||||
if self.description:
|
||||
cmd.append('-d')
|
||||
cmd.append(self.description)
|
||||
|
||||
cmd.extend(['-d', self.description])
|
||||
if self.options:
|
||||
cmd.append('-o')
|
||||
cmd.append(self.options)
|
||||
cmd.extend(['-o', self.options])
|
||||
|
||||
cmd.append(self.name)
|
||||
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def destroy_be(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
|
||||
cmd.append('destroy')
|
||||
cmd.append('-F')
|
||||
cmd.append(self.name)
|
||||
|
||||
cmd = [self.module.get_bin_path('beadm'), 'destroy', '-F', self.name]
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def is_mounted(self):
|
||||
@@ -276,10 +258,7 @@ class BE(object):
|
||||
return False
|
||||
|
||||
def mount_be(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
|
||||
cmd.append('mount')
|
||||
cmd.append(self.name)
|
||||
cmd = [self.module.get_bin_path('beadm'), 'mount', self.name]
|
||||
|
||||
if self.mountpoint:
|
||||
cmd.append(self.mountpoint)
|
||||
@@ -287,9 +266,7 @@ class BE(object):
|
||||
return self.module.run_command(cmd)
|
||||
|
||||
def unmount_be(self):
|
||||
cmd = [self.module.get_bin_path('beadm')]
|
||||
|
||||
cmd.append('unmount')
|
||||
cmd = [self.module.get_bin_path('beadm'), 'unmount']
|
||||
if self.force:
|
||||
cmd.append('-f')
|
||||
cmd.append(self.name)
|
||||
|
||||
@@ -10,6 +10,7 @@ __metaclass__ = type
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: java_cert
|
||||
|
||||
short_description: Uses keytool to import/remove key from java keystore (cacerts)
|
||||
description:
|
||||
- This is a wrapper module around keytool, which can be used to import/remove
|
||||
@@ -81,9 +82,12 @@ options:
|
||||
state:
|
||||
description:
|
||||
- Defines action which can be either certificate import or removal.
|
||||
- When state is present, the certificate will always idempotently be inserted
|
||||
into the keystore, even if there already exists a cert alias that is different.
|
||||
type: str
|
||||
choices: [ absent, present ]
|
||||
default: present
|
||||
requirements: [openssl, keytool]
|
||||
author:
|
||||
- Adam Hamsik (@haad)
|
||||
'''
|
||||
@@ -166,41 +170,143 @@ cmd:
|
||||
'''
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from ansible.module_utils.six.moves.urllib.request import getproxies
|
||||
|
||||
|
||||
def get_keystore_type(keystore_type):
|
||||
def _get_keystore_type_keytool_parameters(keystore_type):
|
||||
''' Check that custom keystore is presented in parameters '''
|
||||
if keystore_type:
|
||||
return " -storetype '%s'" % keystore_type
|
||||
return ''
|
||||
return ["-storetype", keystore_type]
|
||||
return []
|
||||
|
||||
|
||||
def check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
|
||||
def _check_cert_present(module, executable, keystore_path, keystore_pass, alias, keystore_type):
|
||||
''' Check if certificate with alias is present in keystore
|
||||
located at keystore_path '''
|
||||
test_cmd = ("%s -noprompt -list -keystore '%s' -storepass '%s' "
|
||||
"-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
|
||||
test_cmd = [
|
||||
executable,
|
||||
"-list",
|
||||
"-keystore",
|
||||
keystore_path,
|
||||
"-alias",
|
||||
alias,
|
||||
"-rfc"
|
||||
]
|
||||
test_cmd += _get_keystore_type_keytool_parameters(keystore_type)
|
||||
|
||||
check_rc, dummy, dummy = module.run_command(test_cmd)
|
||||
(check_rc, stdout, dummy) = module.run_command(test_cmd, data=keystore_pass, check_rc=False)
|
||||
if check_rc == 0:
|
||||
return True
|
||||
return False
|
||||
return (True, stdout)
|
||||
return (False, '')
|
||||
|
||||
|
||||
def import_cert_url(module, executable, url, port, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
|
||||
''' Import certificate from URL into keystore located at keystore_path '''
|
||||
def _get_certificate_from_url(module, executable, url, port, pem_certificate_output):
|
||||
remote_cert_pem_chain = _download_cert_url(module, executable, url, port)
|
||||
with open(pem_certificate_output, 'w') as f:
|
||||
f.write(remote_cert_pem_chain)
|
||||
|
||||
https_proxy = os.getenv("https_proxy")
|
||||
|
||||
def _get_first_certificate_from_x509_file(module, pem_certificate_file, pem_certificate_output, openssl_bin):
|
||||
""" Read a X509 certificate chain file and output the first certificate in the list """
|
||||
extract_cmd = [
|
||||
openssl_bin,
|
||||
"x509",
|
||||
"-in",
|
||||
pem_certificate_file,
|
||||
"-out",
|
||||
pem_certificate_output
|
||||
]
|
||||
(extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
|
||||
|
||||
if extract_rc != 0:
|
||||
# trying der encoded file
|
||||
extract_cmd += ["-inform", "der"]
|
||||
(extract_rc, dummy, extract_stderr) = module.run_command(extract_cmd, check_rc=False)
|
||||
|
||||
if extract_rc != 0:
|
||||
# this time it's a real failure
|
||||
module.fail_json(msg="Internal module failure, cannot extract certificate, error: %s" % extract_stderr,
|
||||
rc=extract_rc, cmd=extract_cmd)
|
||||
|
||||
return extract_rc
|
||||
|
||||
|
||||
def _get_digest_from_x509_file(module, pem_certificate_file, openssl_bin):
|
||||
""" Read a X509 certificate file and output sha256 digest using openssl """
|
||||
# cleanup file before to compare
|
||||
(dummy, tmp_certificate) = tempfile.mkstemp()
|
||||
module.add_cleanup_file(tmp_certificate)
|
||||
_get_first_certificate_from_x509_file(module, pem_certificate_file, tmp_certificate, openssl_bin)
|
||||
dgst_cmd = [
|
||||
openssl_bin,
|
||||
"dgst",
|
||||
"-r",
|
||||
"-sha256",
|
||||
tmp_certificate
|
||||
]
|
||||
(dgst_rc, dgst_stdout, dgst_stderr) = module.run_command(dgst_cmd, check_rc=False)
|
||||
|
||||
if dgst_rc != 0:
|
||||
module.fail_json(msg="Internal module failure, cannot compute digest for certificate, error: %s" % dgst_stderr,
|
||||
rc=dgst_rc, cmd=dgst_cmd)
|
||||
|
||||
return dgst_stdout.split(' ')[0]
|
||||
|
||||
|
||||
def _export_public_cert_from_pkcs12(module, executable, pkcs_file, alias, password, dest):
|
||||
""" Runs keytools to extract the public cert from a PKCS12 archive and write it to a file. """
|
||||
export_cmd = [
|
||||
executable,
|
||||
"-list",
|
||||
"-keystore",
|
||||
pkcs_file,
|
||||
"-alias",
|
||||
alias,
|
||||
"-storetype",
|
||||
"pkcs12",
|
||||
"-rfc"
|
||||
]
|
||||
(export_rc, export_stdout, export_err) = module.run_command(export_cmd, data=password, check_rc=False)
|
||||
|
||||
if export_rc != 0:
|
||||
module.fail_json(msg="Internal module failure, cannot extract public certificate from pkcs12, error: %s" % export_err,
|
||||
rc=export_rc)
|
||||
|
||||
with open(dest, 'w') as f:
|
||||
f.write(export_stdout)
|
||||
|
||||
|
||||
def get_proxy_settings(scheme='https'):
|
||||
""" Returns a tuple containing (proxy_host, proxy_port). (False, False) if no proxy is found """
|
||||
proxy_url = getproxies().get(scheme, '')
|
||||
if not proxy_url:
|
||||
return (False, False)
|
||||
else:
|
||||
parsed_url = urlparse(proxy_url)
|
||||
if parsed_url.scheme:
|
||||
(proxy_host, proxy_port) = parsed_url.netloc.split(':')
|
||||
else:
|
||||
(proxy_host, proxy_port) = parsed_url.path.split(':')
|
||||
return (proxy_host, proxy_port)
|
||||
|
||||
|
||||
def build_proxy_options():
|
||||
""" Returns list of valid proxy options for keytool """
|
||||
(proxy_host, proxy_port) = get_proxy_settings()
|
||||
no_proxy = os.getenv("no_proxy")
|
||||
|
||||
proxy_opts = ''
|
||||
if https_proxy is not None:
|
||||
(proxy_host, proxy_port) = https_proxy.split(':')
|
||||
proxy_opts = "-J-Dhttps.proxyHost=%s -J-Dhttps.proxyPort=%s" % (proxy_host, proxy_port)
|
||||
proxy_opts = []
|
||||
if proxy_host:
|
||||
proxy_opts.extend(["-J-Dhttps.proxyHost=%s" % proxy_host, "-J-Dhttps.proxyPort=%s" % proxy_port])
|
||||
|
||||
if no_proxy is not None:
|
||||
# For Java's nonProxyHosts property, items are separated by '|',
|
||||
@@ -210,46 +316,48 @@ def import_cert_url(module, executable, url, port, keystore_path, keystore_pass,
|
||||
|
||||
# The property name is http.nonProxyHosts, there is no
|
||||
# separate setting for HTTPS.
|
||||
proxy_opts += " -J-Dhttp.nonProxyHosts='%s'" % non_proxy_hosts
|
||||
proxy_opts.extend(["-J-Dhttp.nonProxyHosts=%s" % non_proxy_hosts])
|
||||
return proxy_opts
|
||||
|
||||
fetch_cmd = "%s -printcert -rfc -sslserver %s %s:%d" % (executable, proxy_opts, url, port)
|
||||
import_cmd = ("%s -importcert -noprompt -keystore '%s' "
|
||||
"-storepass '%s' -alias '%s' %s") % (executable, keystore_path,
|
||||
keystore_pass, alias,
|
||||
get_keystore_type(keystore_type))
|
||||
if trust_cacert:
|
||||
import_cmd = import_cmd + " -trustcacerts"
|
||||
|
||||
def _download_cert_url(module, executable, url, port):
|
||||
""" Fetches the certificate from the remote URL using `keytool -printcert...`
|
||||
The PEM formatted string is returned """
|
||||
proxy_opts = build_proxy_options()
|
||||
fetch_cmd = [executable, "-printcert", "-rfc", "-sslserver"] + proxy_opts + ["%s:%d" % (url, port)]
|
||||
|
||||
# Fetch SSL certificate from remote host.
|
||||
dummy, fetch_out, dummy = module.run_command(fetch_cmd, check_rc=True)
|
||||
(fetch_rc, fetch_out, fetch_err) = module.run_command(fetch_cmd, check_rc=False)
|
||||
|
||||
# Use remote certificate from remote host and import it to a java keystore
|
||||
(import_rc, import_out, import_err) = module.run_command(import_cmd,
|
||||
data=fetch_out,
|
||||
check_rc=False)
|
||||
diff = {'before': '\n', 'after': '%s\n' % alias}
|
||||
if import_rc == 0:
|
||||
module.exit_json(changed=True, msg=import_out,
|
||||
rc=import_rc, cmd=import_cmd, stdout=import_out,
|
||||
diff=diff)
|
||||
else:
|
||||
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd,
|
||||
error=import_err)
|
||||
if fetch_rc != 0:
|
||||
module.fail_json(msg="Internal module failure, cannot download certificate, error: %s" % fetch_err,
|
||||
rc=fetch_rc, cmd=fetch_cmd)
|
||||
|
||||
return fetch_out
|
||||
|
||||
|
||||
def import_cert_path(module, executable, path, keystore_path, keystore_pass, alias, keystore_type, trust_cacert):
|
||||
''' Import certificate from path into keystore located on
|
||||
keystore_path as alias '''
|
||||
import_cmd = ("%s -importcert -noprompt -keystore '%s' "
|
||||
"-storepass '%s' -file '%s' -alias '%s' %s") % (executable, keystore_path,
|
||||
keystore_pass, path, alias,
|
||||
get_keystore_type(keystore_type))
|
||||
import_cmd = [
|
||||
executable,
|
||||
"-importcert",
|
||||
"-noprompt",
|
||||
"-keystore",
|
||||
keystore_path,
|
||||
"-file",
|
||||
path,
|
||||
"-alias",
|
||||
alias
|
||||
]
|
||||
import_cmd += _get_keystore_type_keytool_parameters(keystore_type)
|
||||
|
||||
if trust_cacert:
|
||||
import_cmd = import_cmd + " -trustcacerts"
|
||||
import_cmd.extend(["-trustcacerts"])
|
||||
|
||||
# Use local certificate from local path and import it to a java keystore
|
||||
(import_rc, import_out, import_err) = module.run_command(import_cmd,
|
||||
data="%s\n%s" % (keystore_pass, keystore_pass),
|
||||
check_rc=False)
|
||||
|
||||
diff = {'before': '\n', 'after': '%s\n' % alias}
|
||||
@@ -261,41 +369,29 @@ def import_cert_path(module, executable, path, keystore_path, keystore_pass, ali
|
||||
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
|
||||
|
||||
|
||||
def import_pkcs12_path(module, executable, path, keystore_path, keystore_pass, pkcs12_pass, pkcs12_alias, alias, keystore_type):
|
||||
''' Import pkcs12 from path into keystore located on
|
||||
keystore_path as alias '''
|
||||
import_cmd = ("%s -importkeystore -noprompt -destkeystore '%s' -srcstoretype PKCS12 "
|
||||
"-deststorepass '%s' -destkeypass '%s' -srckeystore '%s' -srcstorepass '%s' "
|
||||
"-srcalias '%s' -destalias '%s' %s") % (executable, keystore_path, keystore_pass,
|
||||
keystore_pass, path, pkcs12_pass, pkcs12_alias,
|
||||
alias, get_keystore_type(keystore_type))
|
||||
|
||||
# Use local certificate from local path and import it to a java keystore
|
||||
(import_rc, import_out, import_err) = module.run_command(import_cmd,
|
||||
check_rc=False)
|
||||
|
||||
diff = {'before': '\n', 'after': '%s\n' % alias}
|
||||
if import_rc == 0:
|
||||
module.exit_json(changed=True, msg=import_out,
|
||||
rc=import_rc, cmd=import_cmd, stdout=import_out,
|
||||
error=import_err, diff=diff)
|
||||
else:
|
||||
module.fail_json(msg=import_out, rc=import_rc, cmd=import_cmd)
|
||||
|
||||
|
||||
def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type):
|
||||
def delete_cert(module, executable, keystore_path, keystore_pass, alias, keystore_type, exit_after=True):
|
||||
''' Delete certificate identified with alias from keystore on keystore_path '''
|
||||
del_cmd = ("%s -delete -keystore '%s' -storepass '%s' "
|
||||
"-alias '%s' %s") % (executable, keystore_path, keystore_pass, alias, get_keystore_type(keystore_type))
|
||||
del_cmd = [
|
||||
executable,
|
||||
"-delete",
|
||||
"-noprompt",
|
||||
"-keystore",
|
||||
keystore_path,
|
||||
"-alias",
|
||||
alias
|
||||
]
|
||||
|
||||
del_cmd += _get_keystore_type_keytool_parameters(keystore_type)
|
||||
|
||||
# Delete SSL certificate from keystore
|
||||
(del_rc, del_out, del_err) = module.run_command(del_cmd, check_rc=True)
|
||||
(del_rc, del_out, del_err) = module.run_command(del_cmd, data=keystore_pass, check_rc=True)
|
||||
|
||||
diff = {'before': '%s\n' % alias, 'after': None}
|
||||
if exit_after:
|
||||
diff = {'before': '%s\n' % alias, 'after': None}
|
||||
|
||||
module.exit_json(changed=True, msg=del_out,
|
||||
rc=del_rc, cmd=del_cmd, stdout=del_out,
|
||||
error=del_err, diff=diff)
|
||||
module.exit_json(changed=True, msg=del_out,
|
||||
rc=del_rc, cmd=del_cmd, stdout=del_out,
|
||||
error=del_err, diff=diff)
|
||||
|
||||
|
||||
def test_keytool(module, executable):
|
||||
@@ -333,7 +429,8 @@ def main():
|
||||
|
||||
module = AnsibleModule(
|
||||
argument_spec=argument_spec,
|
||||
required_one_of=[['cert_path', 'cert_url', 'pkcs12_path']],
|
||||
required_if=[['state', 'present', ('cert_path', 'cert_url', 'pkcs12_path'), True],
|
||||
['state', 'absent', ('cert_url', 'cert_alias'), True]],
|
||||
required_together=[['keystore_path', 'keystore_pass']],
|
||||
mutually_exclusive=[
|
||||
['cert_url', 'cert_path', 'pkcs12_path']
|
||||
@@ -359,6 +456,9 @@ def main():
|
||||
executable = module.params.get('executable')
|
||||
state = module.params.get('state')
|
||||
|
||||
# openssl dependency resolution
|
||||
openssl_bin = module.get_bin_path('openssl', True)
|
||||
|
||||
if path and not cert_alias:
|
||||
module.fail_json(changed=False,
|
||||
msg="Using local path import from %s requires alias argument."
|
||||
@@ -369,31 +469,62 @@ def main():
|
||||
if not keystore_create:
|
||||
test_keystore(module, keystore_path)
|
||||
|
||||
cert_present = check_cert_present(module, executable, keystore_path,
|
||||
keystore_pass, cert_alias, keystore_type)
|
||||
alias_exists, alias_exists_output = _check_cert_present(
|
||||
module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
|
||||
|
||||
if state == 'absent' and cert_present:
|
||||
(dummy, new_certificate) = tempfile.mkstemp()
|
||||
(dummy, old_certificate) = tempfile.mkstemp()
|
||||
module.add_cleanup_file(new_certificate)
|
||||
module.add_cleanup_file(old_certificate)
|
||||
|
||||
if state == 'absent' and alias_exists:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
# delete and exit
|
||||
delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type)
|
||||
|
||||
elif state == 'present' and not cert_present:
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
# dump certificate to enroll in the keystore on disk and compute digest
|
||||
if state == 'present':
|
||||
# The alias exists in the keystore so we must now compare the SHA256 hash of the
|
||||
# public certificate already in the keystore, and the certificate we are wanting to add
|
||||
if alias_exists:
|
||||
with open(old_certificate, "w") as f:
|
||||
f.write(alias_exists_output)
|
||||
keystore_cert_digest = _get_digest_from_x509_file(module, old_certificate, openssl_bin)
|
||||
|
||||
else:
|
||||
keystore_cert_digest = ''
|
||||
|
||||
if pkcs12_path:
|
||||
import_pkcs12_path(module, executable, pkcs12_path, keystore_path,
|
||||
keystore_pass, pkcs12_pass, pkcs12_alias, cert_alias, keystore_type)
|
||||
# Extracting certificate with openssl
|
||||
_export_public_cert_from_pkcs12(module, executable, pkcs12_path, cert_alias, pkcs12_pass, new_certificate)
|
||||
|
||||
if path:
|
||||
import_cert_path(module, executable, path, keystore_path,
|
||||
elif path:
|
||||
# Extracting the X509 digest is a bit easier. Keytool will print the PEM
|
||||
# certificate to stdout so we don't need to do any transformations.
|
||||
new_certificate = path
|
||||
|
||||
elif url:
|
||||
# Getting the X509 digest from a URL is the same as from a path, we just have
|
||||
# to download the cert first
|
||||
_get_certificate_from_url(module, executable, url, port, new_certificate)
|
||||
|
||||
new_cert_digest = _get_digest_from_x509_file(module, new_certificate, openssl_bin)
|
||||
|
||||
if keystore_cert_digest != new_cert_digest:
|
||||
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
|
||||
if alias_exists:
|
||||
# The certificate in the keystore does not match with the one we want to be present
|
||||
# The existing certificate must first be deleted before we insert the correct one
|
||||
delete_cert(module, executable, keystore_path, keystore_pass, cert_alias, keystore_type, exit_after=False)
|
||||
|
||||
import_cert_path(module, executable, new_certificate, keystore_path,
|
||||
keystore_pass, cert_alias, keystore_type, trust_cacert)
|
||||
|
||||
if url:
|
||||
import_cert_url(module, executable, url, port, keystore_path,
|
||||
keystore_pass, cert_alias, keystore_type, trust_cacert)
|
||||
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
|
||||
@@ -114,13 +114,15 @@ cmd:
|
||||
description: Executed command to get action done
|
||||
returned: changed and failure
|
||||
type: str
|
||||
sample: "openssl x509 -noout -in /tmp/cert.crt -fingerprint -sha256"
|
||||
sample: "/usr/bin/openssl x509 -noout -in /tmp/user/1000/tmp8jd_lh23 -fingerprint -sha256"
|
||||
'''
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
def read_certificate_fingerprint(module, openssl_bin, certificate_path):
|
||||
@@ -129,59 +131,70 @@ def read_certificate_fingerprint(module, openssl_bin, certificate_path):
|
||||
if rc != 0:
|
||||
return module.fail_json(msg=current_certificate_fingerprint_out,
|
||||
err=current_certificate_fingerprint_err,
|
||||
rc=rc,
|
||||
cmd=current_certificate_fingerprint_cmd)
|
||||
cmd=current_certificate_fingerprint_cmd,
|
||||
rc=rc)
|
||||
|
||||
current_certificate_match = re.search(r"=([\w:]+)", current_certificate_fingerprint_out)
|
||||
if not current_certificate_match:
|
||||
return module.fail_json(
|
||||
msg="Unable to find the current certificate fingerprint in %s" % current_certificate_fingerprint_out,
|
||||
rc=rc,
|
||||
cmd=current_certificate_fingerprint_err
|
||||
)
|
||||
return module.fail_json(msg="Unable to find the current certificate fingerprint in %s" % current_certificate_fingerprint_out,
|
||||
cmd=current_certificate_fingerprint_cmd,
|
||||
rc=rc)
|
||||
|
||||
return current_certificate_match.group(1)
|
||||
|
||||
|
||||
def read_stored_certificate_fingerprint(module, keytool_bin, alias, keystore_path, keystore_password):
|
||||
stored_certificate_fingerprint_cmd = [keytool_bin, "-list", "-alias", alias, "-keystore", keystore_path, "-storepass", keystore_password, "-v"]
|
||||
(rc, stored_certificate_fingerprint_out, stored_certificate_fingerprint_err) = run_commands(module, stored_certificate_fingerprint_cmd)
|
||||
stored_certificate_fingerprint_cmd = [keytool_bin, "-list", "-alias", alias, "-keystore", keystore_path, "-storepass:env", "STOREPASS", "-v"]
|
||||
(rc, stored_certificate_fingerprint_out, stored_certificate_fingerprint_err) = run_commands(
|
||||
module, stored_certificate_fingerprint_cmd, environ_update=dict(STOREPASS=keystore_password))
|
||||
if rc != 0:
|
||||
if "keytool error: java.lang.Exception: Alias <%s> does not exist" % alias not in stored_certificate_fingerprint_out:
|
||||
return module.fail_json(msg=stored_certificate_fingerprint_out,
|
||||
err=stored_certificate_fingerprint_err,
|
||||
rc=rc,
|
||||
cmd=stored_certificate_fingerprint_cmd)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
stored_certificate_match = re.search(r"SHA256: ([\w:]+)", stored_certificate_fingerprint_out)
|
||||
if not stored_certificate_match:
|
||||
return module.fail_json(
|
||||
msg="Unable to find the stored certificate fingerprint in %s" % stored_certificate_fingerprint_out,
|
||||
rc=rc,
|
||||
cmd=stored_certificate_fingerprint_cmd
|
||||
)
|
||||
# First intention was to not fail, and overwrite the keystore instead,
|
||||
# in case of alias mismatch; but an issue in error handling caused the
|
||||
# module to fail anyway.
|
||||
# See: https://github.com/ansible-collections/community.general/issues/1671
|
||||
# And: https://github.com/ansible-collections/community.general/pull/2183
|
||||
# if "keytool error: java.lang.Exception: Alias <%s> does not exist" % alias in stored_certificate_fingerprint_out:
|
||||
# return "alias mismatch"
|
||||
# if re.match(r'keytool error: java\.io\.IOException: [Kk]eystore( was tampered with, or)? password was incorrect',
|
||||
# stored_certificate_fingerprint_out):
|
||||
# return "password mismatch"
|
||||
return module.fail_json(msg=stored_certificate_fingerprint_out,
|
||||
err=stored_certificate_fingerprint_err,
|
||||
cmd=stored_certificate_fingerprint_cmd,
|
||||
rc=rc)
|
||||
|
||||
return stored_certificate_match.group(1)
|
||||
stored_certificate_match = re.search(r"SHA256: ([\w:]+)", stored_certificate_fingerprint_out)
|
||||
if not stored_certificate_match:
|
||||
return module.fail_json(msg="Unable to find the stored certificate fingerprint in %s" % stored_certificate_fingerprint_out,
|
||||
cmd=stored_certificate_fingerprint_cmd,
|
||||
rc=rc)
|
||||
|
||||
return stored_certificate_match.group(1)
|
||||
|
||||
|
||||
def run_commands(module, cmd, data=None, check_rc=True):
|
||||
return module.run_command(cmd, check_rc=check_rc, data=data)
|
||||
def run_commands(module, cmd, data=None, environ_update=None, check_rc=False):
|
||||
return module.run_command(cmd, check_rc=check_rc, data=data, environ_update=environ_update)
|
||||
|
||||
|
||||
def create_file(path, content):
|
||||
with open(path, 'w') as f:
|
||||
def create_path():
|
||||
dummy, tmpfile = tempfile.mkstemp()
|
||||
os.remove(tmpfile)
|
||||
return tmpfile
|
||||
|
||||
|
||||
def create_file(content):
|
||||
tmpfd, tmpfile = tempfile.mkstemp()
|
||||
with os.fdopen(tmpfd, 'w') as f:
|
||||
f.write(content)
|
||||
return path
|
||||
return tmpfile
|
||||
|
||||
|
||||
def create_tmp_certificate(module):
|
||||
return create_file("/tmp/%s.crt" % module.params['name'], module.params['certificate'])
|
||||
return create_file(module.params['certificate'])
|
||||
|
||||
|
||||
def create_tmp_private_key(module):
|
||||
return create_file("/tmp/%s.key" % module.params['name'], module.params['private_key'])
|
||||
return create_file(module.params['private_key'])
|
||||
|
||||
|
||||
def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass, alias):
|
||||
@@ -196,59 +209,57 @@ def cert_changed(module, openssl_bin, keytool_bin, keystore_path, keystore_pass,
|
||||
|
||||
def create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass):
|
||||
if module.check_mode:
|
||||
module.exit_json(changed=True)
|
||||
else:
|
||||
certificate_path = create_tmp_certificate(module)
|
||||
private_key_path = create_tmp_private_key(module)
|
||||
try:
|
||||
if os.path.exists(keystore_path):
|
||||
os.remove(keystore_path)
|
||||
return module.exit_json(changed=True)
|
||||
|
||||
keystore_p12_path = "/tmp/keystore.p12"
|
||||
if os.path.exists(keystore_p12_path):
|
||||
os.remove(keystore_p12_path)
|
||||
certificate_path = create_tmp_certificate(module)
|
||||
private_key_path = create_tmp_private_key(module)
|
||||
keystore_p12_path = create_path()
|
||||
try:
|
||||
if os.path.exists(keystore_path):
|
||||
os.remove(keystore_path)
|
||||
|
||||
export_p12_cmd = [openssl_bin, "pkcs12", "-export", "-name", name, "-in", certificate_path,
|
||||
"-inkey", private_key_path, "-out",
|
||||
keystore_p12_path, "-passout", "stdin"]
|
||||
export_p12_cmd = [openssl_bin, "pkcs12", "-export", "-name", name, "-in", certificate_path,
|
||||
"-inkey", private_key_path, "-out", keystore_p12_path, "-passout", "stdin"]
|
||||
|
||||
# when keypass is provided, add -passin
|
||||
cmd_stdin = ""
|
||||
if keypass:
|
||||
export_p12_cmd.append("-passin")
|
||||
export_p12_cmd.append("stdin")
|
||||
cmd_stdin = "%s\n" % keypass
|
||||
# when keypass is provided, add -passin
|
||||
cmd_stdin = ""
|
||||
if keypass:
|
||||
export_p12_cmd.append("-passin")
|
||||
export_p12_cmd.append("stdin")
|
||||
cmd_stdin = "%s\n" % keypass
|
||||
cmd_stdin += "%s\n%s" % (password, password)
|
||||
|
||||
cmd_stdin += "%s\n%s" % (password, password)
|
||||
(rc, export_p12_out, export_p12_err) = run_commands(module, export_p12_cmd, data=cmd_stdin)
|
||||
if rc != 0:
|
||||
return module.fail_json(msg=export_p12_out,
|
||||
rc=rc,
|
||||
cmd=export_p12_cmd)
|
||||
(rc, export_p12_out, dummy) = run_commands(module, export_p12_cmd, data=cmd_stdin)
|
||||
if rc != 0:
|
||||
return module.fail_json(msg=export_p12_out,
|
||||
cmd=export_p12_cmd,
|
||||
rc=rc)
|
||||
|
||||
import_keystore_cmd = [keytool_bin, "-importkeystore",
|
||||
"-destkeystore", keystore_path,
|
||||
"-srckeystore", keystore_p12_path,
|
||||
"-srcstoretype", "pkcs12",
|
||||
"-alias", name,
|
||||
"-deststorepass", password,
|
||||
"-srcstorepass", password,
|
||||
"-noprompt"]
|
||||
(rc, import_keystore_out, import_keystore_err) = run_commands(module, import_keystore_cmd, data=None)
|
||||
if rc == 0:
|
||||
update_jks_perm(module, keystore_path)
|
||||
return module.exit_json(changed=True,
|
||||
msg=import_keystore_out,
|
||||
rc=rc,
|
||||
cmd=import_keystore_cmd,
|
||||
stdout_lines=import_keystore_out)
|
||||
else:
|
||||
return module.fail_json(msg=import_keystore_out,
|
||||
rc=rc,
|
||||
cmd=import_keystore_cmd)
|
||||
finally:
|
||||
os.remove(certificate_path)
|
||||
os.remove(private_key_path)
|
||||
import_keystore_cmd = [keytool_bin, "-importkeystore",
|
||||
"-destkeystore", keystore_path,
|
||||
"-srckeystore", keystore_p12_path,
|
||||
"-srcstoretype", "pkcs12",
|
||||
"-alias", name,
|
||||
"-deststorepass:env", "STOREPASS",
|
||||
"-srcstorepass:env", "STOREPASS",
|
||||
"-noprompt"]
|
||||
|
||||
(rc, import_keystore_out, dummy) = run_commands(module, import_keystore_cmd, data=None,
|
||||
environ_update=dict(STOREPASS=password))
|
||||
if rc != 0:
|
||||
return module.fail_json(msg=import_keystore_out,
|
||||
cmd=import_keystore_cmd,
|
||||
rc=rc)
|
||||
|
||||
update_jks_perm(module, keystore_path)
|
||||
return module.exit_json(changed=True,
|
||||
msg=import_keystore_out,
|
||||
cmd=import_keystore_cmd,
|
||||
rc=rc)
|
||||
finally:
|
||||
os.remove(certificate_path)
|
||||
os.remove(private_key_path)
|
||||
os.remove(keystore_p12_path)
|
||||
|
||||
|
||||
def update_jks_perm(module, keystore_path):
|
||||
@@ -280,7 +291,7 @@ def process_jks(module):
|
||||
else:
|
||||
if not module.check_mode:
|
||||
update_jks_perm(module, keystore_path)
|
||||
return module.exit_json(changed=False)
|
||||
module.exit_json(changed=False)
|
||||
else:
|
||||
create_jks(module, name, openssl_bin, keytool_bin, keystore_path, password, keypass)
|
||||
|
||||
@@ -308,6 +319,7 @@ def main():
|
||||
add_file_common_args=spec.add_file_common_args,
|
||||
supports_check_mode=spec.supports_check_mode
|
||||
)
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')
|
||||
process_jks(module)
|
||||
|
||||
|
||||
|
||||
@@ -258,6 +258,18 @@ options:
|
||||
configured setting unless a different value is specified
|
||||
in the playbook.
|
||||
type: str
|
||||
force:
|
||||
description:
|
||||
- When creating a volume, ignores any existing file system
|
||||
or VDO signature already present in the storage device.
|
||||
When stopping or removing a VDO volume, first unmounts
|
||||
the file system stored on the device if mounted.
|
||||
- "B(Warning:) Since this parameter removes all safety
|
||||
checks it is important to make sure that all parameters
|
||||
provided are accurate and intentional."
|
||||
type: bool
|
||||
default: no
|
||||
version_added: 2.4.0
|
||||
notes:
|
||||
- In general, the default thread configuration should be used.
|
||||
requirements:
|
||||
@@ -409,6 +421,9 @@ def add_vdooptions(params):
|
||||
if ('indexmode' in params) and (params['indexmode'] == 'sparse'):
|
||||
options.append("--sparseIndex=enabled")
|
||||
|
||||
if ('force' in params) and (params['force']):
|
||||
options.append("--force")
|
||||
|
||||
# Entering an invalid thread config results in a cryptic
|
||||
# 'Could not set up device mapper for %s' error from the 'vdo'
|
||||
# command execution. The dmsetup module on the system will
|
||||
@@ -465,7 +480,8 @@ def run_module():
|
||||
biothreads=dict(type='str'),
|
||||
cputhreads=dict(type='str'),
|
||||
logicalthreads=dict(type='str'),
|
||||
physicalthreads=dict(type='str')
|
||||
physicalthreads=dict(type='str'),
|
||||
force=dict(type='bool', default=False),
|
||||
)
|
||||
|
||||
# Seed the result dictionary in the object. There will be an
|
||||
|
||||
@@ -143,10 +143,7 @@ def values_fmt(values, value_types):
|
||||
for value, value_type in zip(values, value_types):
|
||||
if value_type == 'bool':
|
||||
value = fix_bool(value)
|
||||
result.append('--type')
|
||||
result.append('{0}'.format(value_type))
|
||||
result.append('--set')
|
||||
result.append('{0}'.format(value))
|
||||
result.extend(['--type', '{0}'.format(value_type), '--set', '{0}'.format(value)])
|
||||
return result
|
||||
|
||||
|
||||
@@ -155,6 +152,10 @@ class XFConfException(Exception):
|
||||
|
||||
|
||||
class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
|
||||
change_params = 'value',
|
||||
diff_params = 'value',
|
||||
output_params = ('property', 'channel', 'value')
|
||||
facts_params = ('property', 'channel', 'value')
|
||||
module = dict(
|
||||
argument_spec=dict(
|
||||
state=dict(default="present",
|
||||
@@ -185,17 +186,15 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
|
||||
)
|
||||
|
||||
def update_xfconf_output(self, **kwargs):
|
||||
self.update_output(**kwargs)
|
||||
if not self.module.params['disable_facts']:
|
||||
self.update_facts(**kwargs)
|
||||
self.update_vars(meta={"output": True, "fact": True}, **kwargs)
|
||||
|
||||
def __init_module__(self):
|
||||
self.does_not = 'Property "{0}" does not exist on channel "{1}".'.format(self.module.params['property'],
|
||||
self.module.params['channel'])
|
||||
self.vars.previous_value = self._get()
|
||||
self.update_xfconf_output(property=self.module.params['property'],
|
||||
channel=self.module.params['channel'],
|
||||
previous_value=None)
|
||||
self.vars.set('previous_value', self._get(), fact=True)
|
||||
self.vars.set('type', self.vars.value_type, fact=True)
|
||||
self.vars.meta('value').set(initial_value=self.vars.previous_value)
|
||||
|
||||
if not self.module.params['disable_facts']:
|
||||
self.facts_name = "xfconf"
|
||||
self.module.deprecate(
|
||||
@@ -220,33 +219,23 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
|
||||
|
||||
return result
|
||||
|
||||
@property
|
||||
def changed(self):
|
||||
if self.vars.previous_value is None:
|
||||
return self.vars.value is not None
|
||||
elif self.vars.value is None:
|
||||
return self.vars.previous_value is not None
|
||||
else:
|
||||
return set(self.vars.previous_value) != set(self.vars.value)
|
||||
|
||||
def _get(self):
|
||||
return self.run_command(params=('channel', 'property'))
|
||||
|
||||
def state_get(self):
|
||||
self.vars.value = self.vars.previous_value
|
||||
self.update_xfconf_output(value=self.vars.value)
|
||||
self.vars.previous_value = None
|
||||
|
||||
def state_absent(self):
|
||||
if not self.module.check_mode:
|
||||
self.run_command(params=('channel', 'property', {'reset': True}))
|
||||
self.vars.value = None
|
||||
self.run_command(params=('channel', 'property', {'reset': True}))
|
||||
self.update_xfconf_output(previous_value=self.vars.previous_value,
|
||||
value=None)
|
||||
|
||||
def state_present(self):
|
||||
# stringify all values - in the CLI they will all be happy strings anyway
|
||||
# and by doing this here the rest of the code can be agnostic to it
|
||||
self.vars.value = [str(v) for v in self.module.params['value']]
|
||||
value_type = self.module.params['value_type']
|
||||
self.vars.value = [str(v) for v in self.vars.value]
|
||||
value_type = self.vars.value_type
|
||||
|
||||
values_len = len(self.vars.value)
|
||||
types_len = len(value_type)
|
||||
@@ -263,7 +252,7 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
|
||||
|
||||
# calculates if it is an array
|
||||
self.vars.is_array = \
|
||||
bool(self.module.params['force_array']) or \
|
||||
bool(self.vars.force_array) or \
|
||||
isinstance(self.vars.previous_value, list) or \
|
||||
values_len > 1
|
||||
|
||||
@@ -277,11 +266,9 @@ class XFConfProperty(CmdMixin, StateMixin, ModuleHelper):
|
||||
|
||||
if not self.vars.is_array:
|
||||
self.vars.value = self.vars.value[0]
|
||||
value_type = value_type[0]
|
||||
|
||||
self.update_xfconf_output(previous_value=self.vars.previous_value,
|
||||
value=self.vars.value,
|
||||
type=value_type)
|
||||
self.vars.type = value_type[0]
|
||||
else:
|
||||
self.vars.type = value_type
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -198,6 +198,10 @@ members:
|
||||
import re
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.six import iteritems
|
||||
|
||||
BEAUTIFUL_SOUP_IMP_ERR = None
|
||||
try:
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
@@ -273,13 +277,8 @@ class BalancerMember(object):
|
||||
'drained': 'Drn',
|
||||
'hot_standby': 'Stby',
|
||||
'ignore_errors': 'Ign'}
|
||||
status = {}
|
||||
actual_status = str(self.attributes['Status'])
|
||||
for mode in status_mapping.keys():
|
||||
if re.search(pattern=status_mapping[mode], string=actual_status):
|
||||
status[mode] = True
|
||||
else:
|
||||
status[mode] = False
|
||||
status = dict((mode, patt in actual_status) for mode, patt in iteritems(status_mapping))
|
||||
return status
|
||||
|
||||
def set_member_status(self, values):
|
||||
@@ -290,13 +289,10 @@ class BalancerMember(object):
|
||||
'ignore_errors': '&w_status_I'}
|
||||
|
||||
request_body = regexp_extraction(self.management_url, EXPRESSION, 1)
|
||||
for k in values_mapping.keys():
|
||||
if values[str(k)]:
|
||||
request_body = request_body + str(values_mapping[k]) + '=1'
|
||||
else:
|
||||
request_body = request_body + str(values_mapping[k]) + '=0'
|
||||
values_url = "".join("{0}={1}".format(url_param, 1 if values[mode] else 0) for mode, url_param in iteritems(values_mapping))
|
||||
request_body = "{0}{1}".format(request_body, values_url)
|
||||
|
||||
response = fetch_url(self.module, self.management_url, data=str(request_body))
|
||||
response = fetch_url(self.module, self.management_url, data=request_body)
|
||||
if response[1]['status'] != 200:
|
||||
self.module.fail_json(msg="Could not set the member status! " + self.host + " " + response[1]['status'])
|
||||
|
||||
@@ -309,11 +305,11 @@ class Balancer(object):
|
||||
|
||||
def __init__(self, host, suffix, module, members=None, tls=False):
|
||||
if tls:
|
||||
self.base_url = str(str('https://') + str(host))
|
||||
self.url = str(str('https://') + str(host) + str(suffix))
|
||||
self.base_url = 'https://' + str(host)
|
||||
self.url = 'https://' + str(host) + str(suffix)
|
||||
else:
|
||||
self.base_url = str(str('http://') + str(host))
|
||||
self.url = str(str('http://') + str(host) + str(suffix))
|
||||
self.base_url = 'http://' + str(host)
|
||||
self.url = 'http://' + str(host) + str(suffix)
|
||||
self.module = module
|
||||
self.page = self.fetch_balancer_page()
|
||||
if members is None:
|
||||
@@ -444,7 +440,5 @@ def main():
|
||||
module.fail_json(msg=str(module.params['member_host']) + ' is not a member of the balancer ' + str(module.params['balancer_vhost']) + '!')
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
# Atlassian open-source approval reference OSR-76.
|
||||
#
|
||||
# (c) 2020, Per Abildgaard Toft <per@minfejl.dk> Search and update function
|
||||
# (c) 2021, Brandon McNama <brandonmcnama@outlook.com> Issue attachment functionality
|
||||
#
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
@@ -29,7 +30,7 @@ options:
|
||||
type: str
|
||||
required: true
|
||||
aliases: [ command ]
|
||||
choices: [ comment, create, edit, fetch, link, search, transition, update ]
|
||||
choices: [ attach, comment, create, edit, fetch, link, search, transition, update ]
|
||||
description:
|
||||
- The operation to perform.
|
||||
|
||||
@@ -56,12 +57,14 @@ options:
|
||||
required: false
|
||||
description:
|
||||
- The issue summary, where appropriate.
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
|
||||
description:
|
||||
type: str
|
||||
required: false
|
||||
description:
|
||||
- The issue description, where appropriate.
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
|
||||
issuetype:
|
||||
type: str
|
||||
@@ -81,18 +84,28 @@ options:
|
||||
required: false
|
||||
description:
|
||||
- The comment text to add.
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
|
||||
status:
|
||||
type: str
|
||||
required: false
|
||||
description:
|
||||
- The desired status; only relevant for the transition operation.
|
||||
- Only used when I(operation) is C(transition), and a bit of a misnomer, it actually refers to the transition name.
|
||||
|
||||
assignee:
|
||||
type: str
|
||||
required: false
|
||||
description:
|
||||
- Sets the assignee on create or transition operations. Note not all transitions will allow this.
|
||||
- Sets the the assignee when I(operation) is C(create), C(transition) or C(edit).
|
||||
- Recent versions of JIRA no longer accept a user name as a user identifier. In that case, use I(account_id) instead.
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
|
||||
account_id:
|
||||
type: str
|
||||
description:
|
||||
- Sets the account identifier for the assignee when I(operation) is C(create), C(transition) or C(edit).
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
version_added: 2.5.0
|
||||
|
||||
linktype:
|
||||
type: str
|
||||
@@ -119,6 +132,7 @@ options:
|
||||
- This is a free-form data structure that can contain arbitrary data. This is passed directly to the JIRA REST API
|
||||
(possibly after merging with other required data, as when passed to create). See examples for more information,
|
||||
and the JIRA REST API for the structure required for various fields.
|
||||
- Note that JIRA may not allow changing field values on specific transitions or states.
|
||||
|
||||
jql:
|
||||
required: false
|
||||
@@ -149,12 +163,37 @@ options:
|
||||
default: true
|
||||
type: bool
|
||||
|
||||
attachment:
|
||||
type: dict
|
||||
version_added: 2.5.0
|
||||
description:
|
||||
- Information about the attachment being uploaded.
|
||||
suboptions:
|
||||
filename:
|
||||
required: true
|
||||
type: path
|
||||
description:
|
||||
- The path to the file to upload (from the remote node) or, if I(content) is specified,
|
||||
the filename to use for the attachment.
|
||||
content:
|
||||
type: str
|
||||
description:
|
||||
- The Base64 encoded contents of the file to attach. If not specified, the contents of I(filename) will be
|
||||
used instead.
|
||||
mimetype:
|
||||
type: str
|
||||
description:
|
||||
- The MIME type to supply for the upload. If not specified, best-effort detection will be
|
||||
done.
|
||||
|
||||
notes:
|
||||
- "Currently this only works with basic-auth."
|
||||
- "To use with JIRA Cloud, pass the login e-mail as the I(username) and the API token as I(password)."
|
||||
|
||||
author:
|
||||
- "Steve Smith (@tarka)"
|
||||
- "Per Abildgaard Toft (@pertoft)"
|
||||
- "Brandon McNama (@DWSR)"
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
@@ -172,7 +211,7 @@ EXAMPLES = r"""
|
||||
args:
|
||||
fields:
|
||||
customfield_13225: "test"
|
||||
customfield_12931: '{"value": "Test"}'
|
||||
customfield_12931: {"value": "Test"}
|
||||
register: issue
|
||||
|
||||
- name: Comment on issue
|
||||
@@ -282,24 +321,40 @@ EXAMPLES = r"""
|
||||
inwardissue: HSP-1
|
||||
outwardissue: MKY-1
|
||||
|
||||
# Transition an issue by target status
|
||||
- name: Close the issue
|
||||
# Transition an issue
|
||||
- name: Resolve the issue
|
||||
community.general.jira:
|
||||
uri: '{{ server }}'
|
||||
username: '{{ user }}'
|
||||
password: '{{ pass }}'
|
||||
issue: '{{ issue.meta.key }}'
|
||||
operation: transition
|
||||
status: Done
|
||||
args:
|
||||
status: Resolve Issue
|
||||
account_id: 112233445566778899aabbcc
|
||||
fields:
|
||||
customfield_14321: [ {'set': {'value': 'Value of Select' }} ]
|
||||
comment: [ { 'add': { 'body' : 'Test' } }]
|
||||
resolution:
|
||||
name: Done
|
||||
description: I am done! This is the last description I will ever give you.
|
||||
|
||||
# Attach a file to an issue
|
||||
- name: Attach a file
|
||||
community.general.jira:
|
||||
uri: '{{ server }}'
|
||||
username: '{{ user }}'
|
||||
password: '{{ pass }}'
|
||||
issue: HSP-1
|
||||
operation: attach
|
||||
attachment:
|
||||
filename: topsecretreport.xlsx
|
||||
"""
|
||||
|
||||
import base64
|
||||
import binascii
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
@@ -311,8 +366,17 @@ from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
|
||||
|
||||
def request(url, user, passwd, timeout, data=None, method=None):
|
||||
if data:
|
||||
def request(
|
||||
url,
|
||||
user,
|
||||
passwd,
|
||||
timeout,
|
||||
data=None,
|
||||
method=None,
|
||||
content_type='application/json',
|
||||
additional_headers=None
|
||||
):
|
||||
if data and content_type == 'application/json':
|
||||
data = json.dumps(data)
|
||||
|
||||
# NOTE: fetch_url uses a password manager, which follows the
|
||||
@@ -323,9 +387,18 @@ def request(url, user, passwd, timeout, data=None, method=None):
|
||||
# inject the basic-auth header up-front to ensure that JIRA treats
|
||||
# the requests as authorized for this user.
|
||||
auth = to_text(base64.b64encode(to_bytes('{0}:{1}'.format(user, passwd), errors='surrogate_or_strict')))
|
||||
response, info = fetch_url(module, url, data=data, method=method, timeout=timeout,
|
||||
headers={'Content-Type': 'application/json',
|
||||
'Authorization': "Basic %s" % auth})
|
||||
|
||||
headers = {}
|
||||
if isinstance(additional_headers, dict):
|
||||
headers = additional_headers.copy()
|
||||
headers.update({
|
||||
"Content-Type": content_type,
|
||||
"Authorization": "Basic %s" % auth,
|
||||
})
|
||||
|
||||
response, info = fetch_url(
|
||||
module, url, data=data, method=method, timeout=timeout, headers=headers
|
||||
)
|
||||
|
||||
if info['status'] not in (200, 201, 204):
|
||||
error = None
|
||||
@@ -351,8 +424,8 @@ def request(url, user, passwd, timeout, data=None, method=None):
|
||||
return {}
|
||||
|
||||
|
||||
def post(url, user, passwd, timeout, data):
|
||||
return request(url, user, passwd, timeout, data=data, method='POST')
|
||||
def post(url, user, passwd, timeout, data, content_type='application/json', additional_headers=None):
|
||||
return request(url, user, passwd, timeout, data=data, method='POST', content_type=content_type, additional_headers=additional_headers)
|
||||
|
||||
|
||||
def put(url, user, passwd, timeout, data):
|
||||
@@ -440,10 +513,22 @@ def transition(restbase, user, passwd, params):
|
||||
if not tid:
|
||||
raise ValueError("Failed find valid transition for '%s'" % target)
|
||||
|
||||
fields = dict(params['fields'])
|
||||
if params['summary'] is not None:
|
||||
fields.update({'summary': params['summary']})
|
||||
if params['description'] is not None:
|
||||
fields.update({'description': params['description']})
|
||||
|
||||
# Perform it
|
||||
url = restbase + '/issue/' + params['issue'] + "/transitions"
|
||||
data = {'transition': {"id": tid},
|
||||
'update': params['fields']}
|
||||
'fields': fields}
|
||||
if params['comment'] is not None:
|
||||
data.update({"update": {
|
||||
"comment": [{
|
||||
"add": {"body": params['comment']}
|
||||
}],
|
||||
}})
|
||||
|
||||
return True, post(url, user, passwd, params['timeout'], data)
|
||||
|
||||
@@ -460,13 +545,89 @@ def link(restbase, user, passwd, params):
|
||||
return True, post(url, user, passwd, params['timeout'], data)
|
||||
|
||||
|
||||
def attach(restbase, user, passwd, params):
|
||||
filename = params['attachment'].get('filename')
|
||||
content = params['attachment'].get('content')
|
||||
|
||||
if not any((filename, content)):
|
||||
raise ValueError('at least one of filename or content must be provided')
|
||||
mime = params['attachment'].get('mimetype')
|
||||
|
||||
if not os.path.isfile(filename):
|
||||
raise ValueError('The provided filename does not exist: %s' % filename)
|
||||
|
||||
content_type, data = _prepare_attachment(filename, content, mime)
|
||||
|
||||
url = restbase + '/issue/' + params['issue'] + '/attachments'
|
||||
return True, post(
|
||||
url, user, passwd, params['timeout'], data, content_type=content_type,
|
||||
additional_headers={"X-Atlassian-Token": "no-check"}
|
||||
)
|
||||
|
||||
|
||||
# Ideally we'd just use prepare_multipart from ansible.module_utils.urls, but
|
||||
# unfortunately it does not support specifying the encoding and also defaults to
|
||||
# base64. Jira doesn't support base64 encoded attachments (and is therefore not
|
||||
# spec compliant. Go figure). I originally wrote this function as an almost
|
||||
# exact copypasta of prepare_multipart, but ran into some encoding issues when
|
||||
# using the noop encoder. Hand rolling the entire message body seemed to work
|
||||
# out much better.
|
||||
#
|
||||
# https://community.atlassian.com/t5/Jira-questions/Jira-dosen-t-decode-base64-attachment-request-REST-API/qaq-p/916427
|
||||
#
|
||||
# content is expected to be a base64 encoded string since Ansible doesn't
|
||||
# support passing raw bytes objects.
|
||||
def _prepare_attachment(filename, content=None, mime_type=None):
|
||||
def escape_quotes(s):
|
||||
return s.replace('"', '\\"')
|
||||
|
||||
boundary = "".join(random.choice(string.digits + string.ascii_letters) for i in range(30))
|
||||
name = to_native(os.path.basename(filename))
|
||||
|
||||
if not mime_type:
|
||||
try:
|
||||
mime_type = mimetypes.guess_type(filename or '', strict=False)[0] or 'application/octet-stream'
|
||||
except Exception:
|
||||
mime_type = 'application/octet-stream'
|
||||
main_type, sep, sub_type = mime_type.partition('/')
|
||||
|
||||
if not content and filename:
|
||||
with open(to_bytes(filename, errors='surrogate_or_strict'), 'rb') as f:
|
||||
content = f.read()
|
||||
else:
|
||||
try:
|
||||
content = base64.decode(content)
|
||||
except binascii.Error as e:
|
||||
raise Exception("Unable to base64 decode file content: %s" % e)
|
||||
|
||||
lines = [
|
||||
"--{0}".format(boundary),
|
||||
'Content-Disposition: form-data; name="file"; filename={0}'.format(escape_quotes(name)),
|
||||
"Content-Type: {0}".format("{0}/{1}".format(main_type, sub_type)),
|
||||
'',
|
||||
to_text(content),
|
||||
"--{0}--".format(boundary),
|
||||
""
|
||||
]
|
||||
|
||||
return (
|
||||
"multipart/form-data; boundary={0}".format(boundary),
|
||||
"\r\n".join(lines)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
global module
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
attachment=dict(type='dict', options=dict(
|
||||
content=dict(type='str'),
|
||||
filename=dict(type='path', required=True),
|
||||
mimetype=dict(type='str')
|
||||
)),
|
||||
uri=dict(type='str', required=True),
|
||||
operation=dict(type='str', choices=['create', 'comment', 'edit', 'update', 'fetch', 'transition', 'link', 'search'],
|
||||
operation=dict(type='str', choices=['attach', 'create', 'comment', 'edit', 'update', 'fetch', 'transition', 'link', 'search'],
|
||||
aliases=['command'], required=True),
|
||||
username=dict(type='str', required=True),
|
||||
password=dict(type='str', required=True, no_log=True),
|
||||
@@ -486,8 +647,10 @@ def main():
|
||||
maxresults=dict(type='int'),
|
||||
timeout=dict(type='float', default=10),
|
||||
validate_certs=dict(default=True, type='bool'),
|
||||
account_id=dict(type='str'),
|
||||
),
|
||||
required_if=(
|
||||
('operation', 'attach', ['issue', 'attachment']),
|
||||
('operation', 'create', ['project', 'issuetype', 'summary']),
|
||||
('operation', 'comment', ['issue', 'comment']),
|
||||
('operation', 'fetch', ['issue']),
|
||||
@@ -495,6 +658,7 @@ def main():
|
||||
('operation', 'link', ['linktype', 'inwardissue', 'outwardissue']),
|
||||
('operation', 'search', ['jql']),
|
||||
),
|
||||
mutually_exclusive=[('assignee', 'account_id')],
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
@@ -506,6 +670,8 @@ def main():
|
||||
passwd = module.params['password']
|
||||
if module.params['assignee']:
|
||||
module.params['fields']['assignee'] = {'name': module.params['assignee']}
|
||||
if module.params['account_id']:
|
||||
module.params['fields']['assignee'] = {'accountId': module.params['account_id']}
|
||||
|
||||
if not uri.endswith('/'):
|
||||
uri = uri + '/'
|
||||
|
||||
1
plugins/modules/xcc_redfish_command.py
Symbolic link
1
plugins/modules/xcc_redfish_command.py
Symbolic link
@@ -0,0 +1 @@
|
||||
remote_management/lenovoxcc/xcc_redfish_command.py
|
||||
@@ -13,6 +13,11 @@ matrix:
|
||||
- env: T=devel/sanity/3
|
||||
- env: T=devel/sanity/4
|
||||
|
||||
- env: T=2.11/sanity/1
|
||||
- env: T=2.11/sanity/2
|
||||
- env: T=2.11/sanity/3
|
||||
- env: T=2.11/sanity/4
|
||||
|
||||
- env: T=2.10/sanity/1
|
||||
- env: T=2.10/sanity/2
|
||||
- env: T=2.10/sanity/3
|
||||
|
||||
2
tests/integration/targets/filter_dict/aliases
Normal file
2
tests/integration/targets/filter_dict/aliases
Normal file
@@ -0,0 +1,2 @@
|
||||
shippable/posix/group3
|
||||
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller
|
||||
7
tests/integration/targets/filter_dict/tasks/main.yml
Normal file
7
tests/integration/targets/filter_dict/tasks/main.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: "Test dict filter"
|
||||
assert:
|
||||
that:
|
||||
- "[['a', 'b']] | community.general.dict == dict([['a', 'b']])"
|
||||
- "[['a', 'b'], [1, 2]] | community.general.dict == dict([['a', 'b'], [1, 2]])"
|
||||
- "[] | community.general.dict == dict([])"
|
||||
2
tests/integration/targets/filter_path_join_shim/aliases
Normal file
2
tests/integration/targets/filter_path_join_shim/aliases
Normal file
@@ -0,0 +1,2 @@
|
||||
shippable/posix/group1
|
||||
skip/python2.6 # filters are controller only, and we no longer support Python 2.6 on the controller
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: "Test path_join filter"
|
||||
assert:
|
||||
that:
|
||||
- "['a', 'b'] | community.general.path_join == 'a/b'"
|
||||
- "['a', '/b'] | community.general.path_join == '/b'"
|
||||
- "[''] | community.general.path_join == ''"
|
||||
@@ -1,2 +1,3 @@
|
||||
shippable/posix/group3
|
||||
skip/aix
|
||||
destructive
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- import_tasks: setup_no_value.yml
|
||||
|
||||
- name: setting value with state=present
|
||||
git_config:
|
||||
name: "{{ option_name }}"
|
||||
value: "{{ option_value }}"
|
||||
scope: "file"
|
||||
file: "{{ output_dir }}/gitconfig_file"
|
||||
state: present
|
||||
register: result
|
||||
|
||||
- name: getting value with state=present
|
||||
git_config:
|
||||
name: "{{ option_name }}"
|
||||
scope: "file"
|
||||
file: "{{ output_dir }}/gitconfig_file"
|
||||
state: present
|
||||
register: get_result
|
||||
|
||||
- name: assert set changed and value is correct with state=present
|
||||
assert:
|
||||
that:
|
||||
- set_result is changed
|
||||
- set_result.diff.before == "\n"
|
||||
- set_result.diff.after == option_value + "\n"
|
||||
- get_result is not changed
|
||||
- get_result.config_value == option_value
|
||||
...
|
||||
@@ -16,6 +16,8 @@
|
||||
- import_tasks: get_set_no_state.yml
|
||||
# testing get/set option with state=present
|
||||
- import_tasks: get_set_state_present.yml
|
||||
# testing get/set option with state=present and scope=file
|
||||
- import_tasks: get_set_state_present_file.yml
|
||||
# testing state=absent without value to delete
|
||||
- import_tasks: unset_no_value.yml
|
||||
# testing state=absent with value to delete
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
file:
|
||||
path: ~/.gitconfig
|
||||
state: absent
|
||||
...
|
||||
|
||||
- name: set up without value (file)
|
||||
file:
|
||||
path: "{{ output_dir }}/gitconfig_file"
|
||||
state: absent
|
||||
...
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
copy:
|
||||
src: gitconfig
|
||||
dest: ~/.gitconfig
|
||||
...
|
||||
|
||||
- name: set up with value (file)
|
||||
copy:
|
||||
src: gitconfig
|
||||
dest: "{{ output_dir }}/gitconfig_file"
|
||||
...
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
---
|
||||
test_pkcs12_path: testpkcs.p12
|
||||
test_keystore_path: keystore.jks
|
||||
test_keystore_path: keystore.jks
|
||||
test_keystore2_path: "{{ output_dir }}/keystore2.jks"
|
||||
test_keystore2_password: changeit
|
||||
test_cert_path: "{{ output_dir }}/cert.pem"
|
||||
test_key_path: "{{ output_dir }}/key.pem"
|
||||
test_cert2_path: "{{ output_dir }}/cert2.pem"
|
||||
test_key2_path: "{{ output_dir }}/key2.pem"
|
||||
test_pkcs_path: "{{ output_dir }}/cert.p12"
|
||||
test_pkcs2_path: "{{ output_dir }}/cert2.p12"
|
||||
test_ssl: setupSSLServer.py
|
||||
test_ssl_port: 21500
|
||||
20
tests/integration/targets/java_cert/files/setupSSLServer.py
Normal file
20
tests/integration/targets/java_cert/files/setupSSLServer.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
import ssl
|
||||
import os
|
||||
import sys
|
||||
|
||||
root_dir = sys.argv[1]
|
||||
port = int(sys.argv[2])
|
||||
|
||||
try:
|
||||
from BaseHTTPServer import HTTPServer
|
||||
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
||||
except ModuleNotFoundError:
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
|
||||
httpd = HTTPServer(('localhost', port), SimpleHTTPRequestHandler)
|
||||
httpd.socket = ssl.wrap_socket(httpd.socket, server_side=True,
|
||||
certfile=os.path.join(root_dir, 'cert.pem'),
|
||||
keyfile=os.path.join(root_dir, 'key.pem'))
|
||||
httpd.handle_request()
|
||||
@@ -1,2 +1,3 @@
|
||||
dependencies:
|
||||
- setup_java_keytool
|
||||
- setup_openssl
|
||||
|
||||
@@ -11,15 +11,16 @@
|
||||
|
||||
- name: import pkcs12
|
||||
java_cert:
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
pkcs12_password: changeit
|
||||
pkcs12_alias: default
|
||||
cert_alias: default
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
pkcs12_password: changeit
|
||||
pkcs12_alias: default
|
||||
cert_alias: default
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
register: result_success
|
||||
|
||||
- name: verify success
|
||||
assert:
|
||||
that:
|
||||
@@ -27,14 +28,14 @@
|
||||
|
||||
- name: import pkcs12 with wrong password
|
||||
java_cert:
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
pkcs12_password: wrong_pass
|
||||
pkcs12_alias: default
|
||||
cert_alias: default_new
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
pkcs12_password: wrong_pass
|
||||
pkcs12_alias: default
|
||||
cert_alias: default_new
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
ignore_errors: true
|
||||
register: result_wrong_pass
|
||||
|
||||
@@ -45,16 +46,62 @@
|
||||
|
||||
- name: test fail on mutually exclusive params
|
||||
java_cert:
|
||||
cert_path: ca.crt
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
cert_alias: default
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
cert_path: ca.crt
|
||||
pkcs12_path: "{{output_dir}}/{{ test_pkcs12_path }}"
|
||||
cert_alias: default
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
keystore_create: yes
|
||||
state: present
|
||||
ignore_errors: true
|
||||
register: result_excl_params
|
||||
|
||||
- name: verify failed exclusive params
|
||||
assert:
|
||||
that:
|
||||
- result_excl_params is failed
|
||||
|
||||
- name: test fail on missing required params
|
||||
java_cert:
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
register: result_missing_required_param
|
||||
|
||||
- name: verify failed missing required params
|
||||
assert:
|
||||
that:
|
||||
- result_missing_required_param is failed
|
||||
|
||||
- name: delete object based on cert_alias parameter
|
||||
java_cert:
|
||||
keystore_path: "{{output_dir}}/{{ test_keystore_path }}"
|
||||
keystore_pass: changeme_keystore
|
||||
cert_alias: default
|
||||
state: absent
|
||||
ignore_errors: true
|
||||
register: result_alias_deleted
|
||||
|
||||
- name: verify object successfully deleted
|
||||
assert:
|
||||
that:
|
||||
- result_alias_deleted is successful
|
||||
|
||||
- name: include extended test suite
|
||||
import_tasks: state_change.yml
|
||||
|
||||
- name: cleanup environment
|
||||
file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- "{{ output_dir }}/{{ test_pkcs12_path }}"
|
||||
- "{{ output_dir }}/{{ test_keystore_path }}"
|
||||
- "{{ test_keystore2_path }}"
|
||||
- "{{ test_cert_path }}"
|
||||
- "{{ test_key_path }}"
|
||||
- "{{ test_cert2_path }}"
|
||||
- "{{ test_key2_path }}"
|
||||
- "{{ test_pkcs_path }}"
|
||||
- "{{ test_pkcs2_path }}"
|
||||
169
tests/integration/targets/java_cert/tasks/state_change.yml
Normal file
169
tests/integration/targets/java_cert/tasks/state_change.yml
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
- name: Generate the self signed cert used as a place holder to create the java keystore
|
||||
command: openssl req -x509 -newkey rsa:4096 -keyout {{ test_key_path }} -out {{ test_cert_path }} -days 365 -nodes -subj '/CN=localhost'
|
||||
args:
|
||||
creates: "{{ test_key_path }}"
|
||||
|
||||
- name: Create the test keystore
|
||||
java_keystore:
|
||||
name: placeholder
|
||||
dest: "{{ test_keystore2_path }}"
|
||||
password: "{{ test_keystore2_password }}"
|
||||
private_key: "{{ lookup('file', '{{ test_key_path }}') }}"
|
||||
certificate: "{{ lookup('file', '{{ test_cert_path }}') }}"
|
||||
|
||||
- name: Generate the self signed cert we will use for testing
|
||||
command: openssl req -x509 -newkey rsa:4096 -keyout '{{ test_key2_path }}' -out '{{ test_cert2_path }}' -days 365 -nodes -subj '/CN=localhost'
|
||||
args:
|
||||
creates: "{{ test_key2_path }}"
|
||||
|
||||
- name: |
|
||||
Import the newly created certificate. This is our main test.
|
||||
If the java_cert has been updated properly, then this task will report changed each time
|
||||
since the module will be comparing the hash of the certificate instead of validating that the alias
|
||||
simply exists
|
||||
java_cert:
|
||||
cert_alias: test_cert
|
||||
cert_path: "{{ test_cert2_path }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: present
|
||||
register: result_x509_changed
|
||||
|
||||
- name: Verify the x509 status has changed
|
||||
assert:
|
||||
that:
|
||||
- result_x509_changed is changed
|
||||
|
||||
- name: |
|
||||
We also want to make sure that the status doesnt change if we import the same cert
|
||||
java_cert:
|
||||
cert_alias: test_cert
|
||||
cert_path: "{{ test_cert2_path }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: present
|
||||
register: result_x509_succeeded
|
||||
|
||||
- name: Verify the x509 status is ok
|
||||
assert:
|
||||
that:
|
||||
- result_x509_succeeded is succeeded
|
||||
|
||||
- name: Create the pkcs12 archive from the test x509 cert
|
||||
command: >
|
||||
openssl pkcs12
|
||||
-in {{ test_cert_path }}
|
||||
-inkey {{ test_key_path }}
|
||||
-export
|
||||
-name test_pkcs12_cert
|
||||
-out {{ test_pkcs_path }}
|
||||
-passout pass:"{{ test_keystore2_password }}"
|
||||
|
||||
- name: Create the pkcs12 archive from the certificate we will be trying to add to the keystore
|
||||
command: >
|
||||
openssl pkcs12
|
||||
-in {{ test_cert2_path }}
|
||||
-inkey {{ test_key2_path }}
|
||||
-export
|
||||
-name test_pkcs12_cert
|
||||
-out {{ test_pkcs2_path }}
|
||||
-passout pass:"{{ test_keystore2_password }}"
|
||||
|
||||
- name: >
|
||||
Ensure the original pkcs12 cert is in the keystore
|
||||
java_cert:
|
||||
cert_alias: test_pkcs12_cert
|
||||
pkcs12_alias: test_pkcs12_cert
|
||||
pkcs12_path: "{{ test_pkcs_path }}"
|
||||
pkcs12_password: "{{ test_keystore2_password }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: present
|
||||
|
||||
- name: |
|
||||
Perform the same test, but we will now be testing the pkcs12 functionality
|
||||
If we add a different pkcs12 cert with the same alias, we should have a chnaged result, NOT the same
|
||||
java_cert:
|
||||
cert_alias: test_pkcs12_cert
|
||||
pkcs12_alias: test_pkcs12_cert
|
||||
pkcs12_path: "{{ test_pkcs2_path }}"
|
||||
pkcs12_password: "{{ test_keystore2_password }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: present
|
||||
register: result_pkcs12_changed
|
||||
|
||||
- name: Verify the pkcs12 status has changed
|
||||
assert:
|
||||
that:
|
||||
- result_pkcs12_changed is changed
|
||||
|
||||
- name: |
|
||||
We are requesting the same cert now, so the status should show OK
|
||||
java_cert:
|
||||
cert_alias: test_pkcs12_cert
|
||||
pkcs12_alias: test_pkcs12_cert
|
||||
pkcs12_path: "{{ test_pkcs2_path }}"
|
||||
pkcs12_password: "{{ test_keystore2_password }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
register: result_pkcs12_succeeded
|
||||
|
||||
- name: Verify the pkcs12 status is ok
|
||||
assert:
|
||||
that:
|
||||
- result_pkcs12_succeeded is succeeded
|
||||
|
||||
- name: Copy the ssl server script
|
||||
copy:
|
||||
src: "setupSSLServer.py"
|
||||
dest: "{{ output_dir }}"
|
||||
|
||||
- name: Create an SSL server that we will use for testing URL imports
|
||||
command: python {{ output_dir }}/setupSSLServer.py {{ output_dir }} {{ test_ssl_port }}
|
||||
async: 10
|
||||
poll: 0
|
||||
|
||||
- name: |
|
||||
Download the original cert.pem from our temporary server. The current cert should contain
|
||||
cert2.pem. Importing this cert should return a status of changed
|
||||
java_cert:
|
||||
cert_alias: test_cert_localhost
|
||||
cert_url: localhost
|
||||
cert_port: "{{ test_ssl_port }}"
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: present
|
||||
register: result_url_changed
|
||||
|
||||
- name: Verify that the url status is changed
|
||||
assert:
|
||||
that:
|
||||
- result_url_changed is changed
|
||||
|
||||
- name: Ensure we can remove the x509 cert
|
||||
java_cert:
|
||||
cert_alias: test_cert
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: absent
|
||||
register: result_x509_absent
|
||||
|
||||
- name: Verify the x509 cert is absent
|
||||
assert:
|
||||
that:
|
||||
- result_x509_absent is changed
|
||||
|
||||
- name: Ensure we can remove the pkcs12 archive
|
||||
java_cert:
|
||||
cert_alias: test_pkcs12_cert
|
||||
keystore_path: "{{ test_keystore2_path }}"
|
||||
keystore_pass: "{{ test_keystore2_password }}"
|
||||
state: absent
|
||||
register: result_pkcs12_absent
|
||||
|
||||
- name: Verify the pkcs12 archive is absent
|
||||
assert:
|
||||
that:
|
||||
- result_pkcs12_absent is changed
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user