mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-04-28 17:36:49 +00:00
Compare commits
151 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945bb91e04 | ||
|
|
b48a5c264f | ||
|
|
5bae017de9 | ||
|
|
e568a760ac | ||
|
|
8132568d2f | ||
|
|
0e320641b8 | ||
|
|
8679d59376 | ||
|
|
2554b4b0f4 | ||
|
|
379b6d3523 | ||
|
|
fe4f4198af | ||
|
|
db84ea4ab6 | ||
|
|
de5970d17a | ||
|
|
433d0571b4 | ||
|
|
53b95fd182 | ||
|
|
ad1f25e576 | ||
|
|
49eda7270e | ||
|
|
9c4799c903 | ||
|
|
88bf99b272 | ||
|
|
3ca6e8525e | ||
|
|
0169cb8358 | ||
|
|
499f4b4066 | ||
|
|
ff08c20f12 | ||
|
|
d27c06faeb | ||
|
|
0f98b63944 | ||
|
|
55c70dfb72 | ||
|
|
f78993ba12 | ||
|
|
b97ce10156 | ||
|
|
9250430d7d | ||
|
|
d61305d267 | ||
|
|
198b813b55 | ||
|
|
9e6df4f1c9 | ||
|
|
a477044fb7 | ||
|
|
2a97812856 | ||
|
|
c85bb8713e | ||
|
|
5cdc8f4b07 | ||
|
|
50131f5dfa | ||
|
|
c734e7c2e5 | ||
|
|
7e6e8f7749 | ||
|
|
687acdc961 | ||
|
|
16092feaab | ||
|
|
6676fb8fb4 | ||
|
|
a860f537dd | ||
|
|
f1a9c2f00a | ||
|
|
f8de068e32 | ||
|
|
70b4bacf0f | ||
|
|
41f5d1741c | ||
|
|
54ede7dd7f | ||
|
|
7f0702b786 | ||
|
|
89a3abe64a | ||
|
|
59eff2e3e0 | ||
|
|
1115b463fe | ||
|
|
77bf1fedf5 | ||
|
|
89560ea2e7 | ||
|
|
f9919d28d4 | ||
|
|
7b4660d28a | ||
|
|
29496be80e | ||
|
|
991c96615c | ||
|
|
fe5ad997c1 | ||
|
|
468b28bbb8 | ||
|
|
9b57221d9a | ||
|
|
cd1a92d417 | ||
|
|
7486e3a074 | ||
|
|
6661917370 | ||
|
|
ec0bd3143a | ||
|
|
cce68def8b | ||
|
|
6f5ad22d28 | ||
|
|
6c53a09eef | ||
|
|
9b6e75f7f4 | ||
|
|
e09650140d | ||
|
|
67388be1a9 | ||
|
|
130d07948a | ||
|
|
5d6fcaef53 | ||
|
|
f044a83c49 | ||
|
|
e3f7e8dadf | ||
|
|
8d1a028dbd | ||
|
|
8823e5c061 | ||
|
|
102456d033 | ||
|
|
aad4c55d3d | ||
|
|
e31c98f17f | ||
|
|
6a5dfc5579 | ||
|
|
ab7efef9df | ||
|
|
ca9c763b57 | ||
|
|
cfeb40ed23 | ||
|
|
c495d136fa | ||
|
|
d9e2d6682b | ||
|
|
d7fe288ffd | ||
|
|
7de89699f7 | ||
|
|
b0a9cceeb5 | ||
|
|
b08f0b2f82 | ||
|
|
f23f409bd6 | ||
|
|
cfea62793f | ||
|
|
62bda91466 | ||
|
|
473d5fa2af | ||
|
|
cc76d684d5 | ||
|
|
7a6770c731 | ||
|
|
d2214af6e8 | ||
|
|
fad1220869 | ||
|
|
fe09516235 | ||
|
|
78cd8886f4 | ||
|
|
6b99d48f06 | ||
|
|
6e0e17a7e3 | ||
|
|
90de95c7b2 | ||
|
|
07c6b8b24e | ||
|
|
d106de6d51 | ||
|
|
e96101fb3f | ||
|
|
a60d55f03c | ||
|
|
d6a09ada98 | ||
|
|
9ddb75a3a2 | ||
|
|
b85ff2a997 | ||
|
|
3d1ca5638b | ||
|
|
35fd4700bf | ||
|
|
9add9df7d6 | ||
|
|
cdb747b41d | ||
|
|
7a70fda784 | ||
|
|
13d1b9569e | ||
|
|
c8c5021773 | ||
|
|
206ac72bd8 | ||
|
|
1ee123bb1e | ||
|
|
aa0bcad9df | ||
|
|
6e51690a95 | ||
|
|
3eab4faf0b | ||
|
|
c6589a772b | ||
|
|
cb06c4ff77 | ||
|
|
78316fbb75 | ||
|
|
c7df82652f | ||
|
|
342a1a7faa | ||
|
|
17431bd42c | ||
|
|
eacf663999 | ||
|
|
84a806be08 | ||
|
|
7db5c86dc8 | ||
|
|
1e82b5c580 | ||
|
|
7f6be665f9 | ||
|
|
174b00cd29 | ||
|
|
a7c92f491d | ||
|
|
592cd3747b | ||
|
|
4295ee0bb4 | ||
|
|
7e1ff300f8 | ||
|
|
b3ddec2b29 | ||
|
|
227f6e333e | ||
|
|
94711ca506 | ||
|
|
d53052f27a | ||
|
|
95d57c338a | ||
|
|
4fd7a65a52 | ||
|
|
0e3c1f867d | ||
|
|
fa24edf89c | ||
|
|
ac3e803a36 | ||
|
|
8168ddca4f | ||
|
|
cc264be644 | ||
|
|
9bd2d1ec90 | ||
|
|
01b2c48161 | ||
|
|
a26792418e |
@@ -24,14 +24,15 @@ schedules:
|
||||
always: true
|
||||
branches:
|
||||
include:
|
||||
- stable-2
|
||||
- stable-3
|
||||
- stable-4
|
||||
- cron: 0 11 * * 0
|
||||
displayName: Weekly (old stable branches)
|
||||
always: true
|
||||
branches:
|
||||
include:
|
||||
- stable-1
|
||||
- stable-2
|
||||
|
||||
variables:
|
||||
- name: checkoutPath
|
||||
@@ -205,14 +206,14 @@ stages:
|
||||
parameters:
|
||||
testFormat: devel/{0}
|
||||
targets:
|
||||
- name: macOS 11.1
|
||||
test: macos/11.1
|
||||
- name: macOS 12.0
|
||||
test: macos/12.0
|
||||
- name: RHEL 7.9
|
||||
test: rhel/7.9
|
||||
- name: RHEL 8.4
|
||||
test: rhel/8.4
|
||||
- name: FreeBSD 12.2
|
||||
test: freebsd/12.2
|
||||
- name: RHEL 8.5
|
||||
test: rhel/8.5
|
||||
- name: FreeBSD 12.3
|
||||
test: freebsd/12.3
|
||||
- name: FreeBSD 13.0
|
||||
test: freebsd/13.0
|
||||
groups:
|
||||
@@ -297,10 +298,10 @@ stages:
|
||||
targets:
|
||||
- name: CentOS 7
|
||||
test: centos7
|
||||
- name: Fedora 33
|
||||
test: fedora33
|
||||
- name: Fedora 34
|
||||
test: fedora34
|
||||
- name: Fedora 35
|
||||
test: fedora35
|
||||
- name: openSUSE 15 py2
|
||||
test: opensuse15py2
|
||||
- name: openSUSE 15 py3
|
||||
|
||||
@@ -11,7 +11,7 @@ mkdir "${agent_temp_directory}/coverage/"
|
||||
|
||||
options=(--venv --venv-system-site-packages --color -v)
|
||||
|
||||
ansible-test coverage combine --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
ansible-test coverage combine --group-by command --export "${agent_temp_directory}/coverage/" "${options[@]}"
|
||||
|
||||
if ansible-test coverage analyze targets generate --help >/dev/null 2>&1; then
|
||||
# Only analyze coverage if the installed version of ansible-test supports it.
|
||||
|
||||
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
101
.azure-pipelines/scripts/publish-codecov.py
Executable file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Upload code coverage reports to codecov.io.
|
||||
Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||
Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import dataclasses
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import typing as t
|
||||
import urllib.request
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CoverageFile:
|
||||
name: str
|
||||
path: pathlib.Path
|
||||
flags: t.List[str]
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Args:
|
||||
dry_run: bool
|
||||
path: pathlib.Path
|
||||
|
||||
|
||||
def parse_args() -> Args:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-n', '--dry-run', action='store_true')
|
||||
parser.add_argument('path', type=pathlib.Path)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Store arguments in a typed dataclass
|
||||
fields = dataclasses.fields(Args)
|
||||
kwargs = {field.name: getattr(args, field.name) for field in fields}
|
||||
|
||||
return Args(**kwargs)
|
||||
|
||||
|
||||
def process_files(directory: pathlib.Path) -> t.Tuple[CoverageFile, ...]:
|
||||
processed = []
|
||||
for file in directory.joinpath('reports').glob('coverage*.xml'):
|
||||
name = file.stem.replace('coverage=', '')
|
||||
|
||||
# Get flags from name
|
||||
flags = name.replace('-powershell', '').split('=') # Drop '-powershell' suffix
|
||||
flags = [flag if not flag.startswith('stub') else flag.split('-')[0] for flag in flags] # Remove "-01" from stub files
|
||||
|
||||
processed.append(CoverageFile(name, file, flags))
|
||||
|
||||
return tuple(processed)
|
||||
|
||||
|
||||
def upload_files(codecov_bin: pathlib.Path, files: t.Tuple[CoverageFile, ...], dry_run: bool = False) -> None:
|
||||
for file in files:
|
||||
cmd = [
|
||||
str(codecov_bin),
|
||||
'--name', file.name,
|
||||
'--file', str(file.path),
|
||||
]
|
||||
for flag in file.flags:
|
||||
cmd.extend(['--flags', flag])
|
||||
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would run command: {cmd}')
|
||||
continue
|
||||
|
||||
subprocess.run(cmd, check=True)
|
||||
|
||||
|
||||
def download_file(url: str, dest: pathlib.Path, flags: int, dry_run: bool = False) -> None:
|
||||
if dry_run:
|
||||
print(f'DRY-RUN: Would download {url} to {dest} and set mode to {flags:o}')
|
||||
return
|
||||
|
||||
with urllib.request.urlopen(url) as resp:
|
||||
with dest.open('w+b') as f:
|
||||
# Read data in chunks rather than all at once
|
||||
shutil.copyfileobj(resp, f, 64 * 1024)
|
||||
|
||||
dest.chmod(flags)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
url = 'https://ansible-ci-files.s3.amazonaws.com/codecov/linux/codecov'
|
||||
with tempfile.TemporaryDirectory(prefix='codecov-') as tmpdir:
|
||||
codecov_bin = pathlib.Path(tmpdir) / 'codecov'
|
||||
download_file(url, codecov_bin, 0o755, args.dry_run)
|
||||
|
||||
files = process_files(args.path)
|
||||
upload_files(codecov_bin, files, args.dry_run)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Upload code coverage reports to codecov.io.
|
||||
# Multiple coverage files from multiple languages are accepted and aggregated after upload.
|
||||
# Python coverage, as well as PowerShell and Python stubs can all be uploaded.
|
||||
|
||||
set -o pipefail -eu
|
||||
|
||||
output_path="$1"
|
||||
|
||||
curl --silent --show-error https://ansible-ci-files.s3.us-east-1.amazonaws.com/codecov/codecov.sh > codecov.sh
|
||||
|
||||
for file in "${output_path}"/reports/coverage*.xml; do
|
||||
name="${file}"
|
||||
name="${name##*/}" # remove path
|
||||
name="${name##coverage=}" # remove 'coverage=' prefix if present
|
||||
name="${name%.xml}" # remove '.xml' suffix
|
||||
|
||||
bash codecov.sh \
|
||||
-f "${file}" \
|
||||
-n "${name}" \
|
||||
-X coveragepy \
|
||||
-X gcov \
|
||||
-X fix \
|
||||
-X search \
|
||||
-X xcode \
|
||||
|| echo "Failed to upload code coverage report to codecov.io: ${file}"
|
||||
done
|
||||
@@ -12,4 +12,4 @@ if ! ansible-test --help >/dev/null 2>&1; then
|
||||
pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
|
||||
fi
|
||||
|
||||
ansible-test coverage xml --stub --venv --venv-system-site-packages --color -v
|
||||
ansible-test coverage xml --group-by command --stub --venv --venv-system-site-packages --color -v
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
summaryFileLocation: "$(outputPath)/reports/$(pipelinesCoverage).xml"
|
||||
displayName: Publish to Azure Pipelines
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.sh "$(outputPath)"
|
||||
- bash: .azure-pipelines/scripts/publish-codecov.py "$(outputPath)"
|
||||
displayName: Publish to codecov.io
|
||||
condition: gt(variables.coverageFileCount, 0)
|
||||
continueOnError: true
|
||||
|
||||
42
.github/BOTMETA.yml
vendored
42
.github/BOTMETA.yml
vendored
@@ -118,6 +118,8 @@ files:
|
||||
$doc_fragments/xenserver.py:
|
||||
maintainers: bvitnik
|
||||
labels: xenserver
|
||||
$filters/counter.py:
|
||||
maintainers: keilr
|
||||
$filters/dict.py:
|
||||
maintainers: felixfontein
|
||||
$filters/dict_kv.py:
|
||||
@@ -156,15 +158,17 @@ files:
|
||||
maintainers: conloos
|
||||
$inventories/nmap.py: {}
|
||||
$inventories/online.py:
|
||||
maintainers: sieben
|
||||
maintainers: remyleone
|
||||
$inventories/opennebula.py:
|
||||
maintainers: feldsam
|
||||
labels: cloud opennebula
|
||||
keywords: opennebula dynamic inventory script
|
||||
$inventories/proxmox.py:
|
||||
maintainers: $team_virt ilijamt
|
||||
$inventories/xen_orchestra.py:
|
||||
maintainers: ddelnano shinuza
|
||||
$inventories/icinga2.py:
|
||||
maintainers: bongoeadgc6
|
||||
maintainers: BongoEADGC6
|
||||
$inventories/scaleway.py:
|
||||
maintainers: $team_scaleway
|
||||
labels: cloud scaleway
|
||||
@@ -223,6 +227,8 @@ files:
|
||||
maintainers: konstruktoid
|
||||
$lookups/redis.py:
|
||||
maintainers: $team_ansible_core jpmens
|
||||
$lookups/revbitspss.py:
|
||||
maintainers: RevBits
|
||||
$lookups/shelvefile.py: {}
|
||||
$lookups/tss.py:
|
||||
maintainers: amigus endlesstrax
|
||||
@@ -318,6 +324,10 @@ files:
|
||||
$modules/cloud/misc/proxmox_kvm.py:
|
||||
maintainers: helldorado
|
||||
ignore: skvidal
|
||||
$modules/cloud/misc/proxmox_nic.py:
|
||||
maintainers: Kogelvis
|
||||
$modules/cloud/misc/proxmox_tasks_info:
|
||||
maintainers: paginabianca
|
||||
$modules/cloud/misc/proxmox_template.py:
|
||||
maintainers: UnderGreen
|
||||
ignore: skvidal
|
||||
@@ -337,7 +347,7 @@ files:
|
||||
$modules/cloud/oneandone/:
|
||||
maintainers: aajdinov edevenport
|
||||
$modules/cloud/online/:
|
||||
maintainers: sieben
|
||||
maintainers: remyleone
|
||||
$modules/cloud/opennebula/:
|
||||
maintainers: $team_opennebula
|
||||
$modules/cloud/opennebula/one_host.py:
|
||||
@@ -407,11 +417,11 @@ files:
|
||||
$modules/cloud/scaleway/scaleway_ip_info.py:
|
||||
maintainers: Spredzy
|
||||
$modules/cloud/scaleway/scaleway_organization_info.py:
|
||||
maintainers: sieben Spredzy
|
||||
maintainers: Spredzy
|
||||
$modules/cloud/scaleway/scaleway_security_group.py:
|
||||
maintainers: DenBeke
|
||||
$modules/cloud/scaleway/scaleway_security_group_info.py:
|
||||
maintainers: sieben Spredzy
|
||||
maintainers: Spredzy
|
||||
$modules/cloud/scaleway/scaleway_security_group_rule.py:
|
||||
maintainers: DenBeke
|
||||
$modules/cloud/scaleway/scaleway_server_info.py:
|
||||
@@ -530,6 +540,8 @@ files:
|
||||
maintainers: adamgoossens
|
||||
$modules/identity/keycloak/keycloak_identity_provider.py:
|
||||
maintainers: laurpaum
|
||||
$modules/identity/keycloak/keycloak_realm_info.py:
|
||||
maintainers: fynncfchen
|
||||
$modules/identity/keycloak/keycloak_realm.py:
|
||||
maintainers: kris2kris
|
||||
$modules/identity/keycloak/keycloak_role.py:
|
||||
@@ -615,6 +627,8 @@ files:
|
||||
labels: cloudflare_dns
|
||||
$modules/net_tools/dnsimple.py:
|
||||
maintainers: drcapulet
|
||||
$modules/net_tools/dnsimple_info.py:
|
||||
maintainers: edhilgendorf
|
||||
$modules/net_tools/dnsmadeeasy.py:
|
||||
maintainers: briceburg
|
||||
$modules/net_tools/gandi_livedns.py:
|
||||
@@ -716,6 +730,8 @@ files:
|
||||
maintainers: mwarkentin
|
||||
$modules/packaging/language/bundler.py:
|
||||
maintainers: thoiberg
|
||||
$modules/packaging/language/cargo.py:
|
||||
maintainers: radek-sprta
|
||||
$modules/packaging/language/composer.py:
|
||||
maintainers: dmtrs
|
||||
ignore: resmo
|
||||
@@ -897,6 +913,10 @@ files:
|
||||
$modules/remote_management/manageiq/:
|
||||
labels: manageiq
|
||||
maintainers: $team_manageiq
|
||||
$modules/remote_management/manageiq/manageiq_alert_profiles.py:
|
||||
maintainers: elad661
|
||||
$modules/remote_management/manageiq/manageiq_alerts.py:
|
||||
maintainers: elad661
|
||||
$modules/remote_management/manageiq/manageiq_group.py:
|
||||
maintainers: evertmulder
|
||||
$modules/remote_management/manageiq/manageiq_tenant.py:
|
||||
@@ -947,6 +967,8 @@ files:
|
||||
maintainers: SamyCoenen
|
||||
$modules/source_control/gitlab/gitlab_user.py:
|
||||
maintainers: LennertMertens stgrace
|
||||
$modules/source_control/gitlab/gitlab_branch.py:
|
||||
maintainers: paytroff
|
||||
$modules/source_control/hg.py:
|
||||
maintainers: yeukhon
|
||||
$modules/storage/emc/emc_vnx_sg_member.py:
|
||||
@@ -1000,6 +1022,8 @@ files:
|
||||
$modules/system/gconftool2.py:
|
||||
maintainers: Akasurde kevensen
|
||||
labels: gconftool2
|
||||
$modules/system/homectl.py:
|
||||
maintainers: jameslivulpi
|
||||
$modules/system/interfaces_file.py:
|
||||
maintainers: obourdon hryamzik
|
||||
labels: interfaces_file
|
||||
@@ -1082,6 +1106,8 @@ files:
|
||||
keywords: beadm dladm illumos ipadm nexenta omnios openindiana pfexec smartos solaris sunos zfs zpool
|
||||
$modules/system/ssh_config.py:
|
||||
maintainers: gaqzi Akasurde
|
||||
$modules/system/sudoers.py:
|
||||
maintainers: JonEllis
|
||||
$modules/system/svc.py:
|
||||
maintainers: bcoca
|
||||
$modules/system/syspatch.py:
|
||||
@@ -1207,7 +1233,7 @@ macros:
|
||||
team_cyberark_conjur: jvanderhoof ryanprior
|
||||
team_e_spirit: MatrixCrawler getjack
|
||||
team_flatpak: JayKayy oolongbrothers
|
||||
team_gitlab: Lunik Shaps dj-wasabi marwatk waheedi zanssa scodeman metanovii
|
||||
team_gitlab: Lunik Shaps dj-wasabi marwatk waheedi zanssa scodeman metanovii sh0shin
|
||||
team_hpux: bcoca davx8342
|
||||
team_huawei: QijunPan TommyLike edisonxiang freesky-edward hwDCN niuzhenguo xuxiaowei0512 yanzhangi zengchen1024 zhongjun2
|
||||
team_ipa: Akasurde Nosmoht fxfitz justchris1
|
||||
@@ -1220,9 +1246,9 @@ macros:
|
||||
team_opennebula: ilicmilan meerkampdvv rsmontero xorel nilsding
|
||||
team_oracle: manojmeda mross22 nalsaber
|
||||
team_purestorage: bannaych dnix101 genegr lionmax opslounge raekins sdodsley sile16
|
||||
team_redfish: mraineri tomasg2012 xmadsen renxulei
|
||||
team_redfish: mraineri tomasg2012 xmadsen renxulei rajeevkallur bhavya06
|
||||
team_rhn: FlossWare alikins barnabycourt vritant
|
||||
team_scaleway: QuentinBrosse abarbare jerome-quere kindermoumoute remyleone sieben
|
||||
team_scaleway: remyleone abarbare
|
||||
team_solaris: bcoca fishman jasperla jpdasma mator scathatheworm troy2914 xen0l
|
||||
team_suse: commel dcermak evrardjp lrupp toabctl AnderEnder alxgu andytom sealor
|
||||
team_virt: joshainglis karmab tleguern Thulium-Drake Ajpantuso
|
||||
|
||||
280
CHANGELOG.rst
280
CHANGELOG.rst
@@ -6,6 +6,286 @@ Community General Release Notes
|
||||
|
||||
This changelog describes changes after version 3.0.0.
|
||||
|
||||
v4.4.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular features and bugfixes release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- cobbler inventory plugin - add ``include_profiles`` option (https://github.com/ansible-collections/community.general/pull/4068).
|
||||
- gitlab_project_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- icinga2 inventory plugin - implemented constructed interface (https://github.com/ansible-collections/community.general/pull/4088).
|
||||
- linode inventory plugin - allow templating of ``access_token`` variable in Linode inventory plugin (https://github.com/ansible-collections/community.general/pull/4040).
|
||||
- lists_mergeby filter plugin - add parameters ``list_merge`` and ``recursive``. These are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9 (https://github.com/ansible-collections/community.general/pull/4058).
|
||||
- lxc_container - added ``wait_for_container`` parameter. If ``true`` the module will wait until the running task reports success as the status (https://github.com/ansible-collections/community.general/pull/4039).
|
||||
- mail callback plugin - add ``Message-ID`` and ``Date`` headers (https://github.com/ansible-collections/community.general/issues/4055, https://github.com/ansible-collections/community.general/pull/4056).
|
||||
- mail callback plugin - properly use Ansible's option handling to split lists (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
- nmcli - adds ``routes6`` and ``route_metric6`` parameters for supporting IPv6 routes (https://github.com/ansible-collections/community.general/issues/4059).
|
||||
- opennebula - add the release action for VMs in the ``HOLD`` state (https://github.com/ansible-collections/community.general/pull/4036).
|
||||
- opentelemetry_plugin - enrich service when using the ``docker_login`` (https://github.com/ansible-collections/community.general/pull/4104).
|
||||
- proxmox modules - move ``HAS_PROXMOXER`` check into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4030).
|
||||
- scaleway inventory plugin - add profile parameter ``scw_profile`` (https://github.com/ansible-collections/community.general/pull/4049).
|
||||
- snap - add option ``options`` permitting to set options using the ``snap set`` command (https://github.com/ansible-collections/community.general/pull/3943).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- mail callback plugin - not specifying ``sender`` is deprecated and will be disallowed in community.general 6.0.0 (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- cargo - fix detection of outdated packages when ``state=latest`` (https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- cargo - fix incorrectly reported changed status for packages with a name containing a hyphen (https://github.com/ansible-collections/community.general/issues/4044, https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- gitlab_project_variable - add missing documentation about GitLab versions that support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_project_variable - allow to set same variable name under different environment scopes. Due this change, the return value ``project_variable`` differs from previous version in check mode. It was counting ``updated`` values, because it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_project_variable - fix idempotent change behaviour for float and integer variables (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_runner - use correct API endpoint to create and retrieve project level runners when using ``project`` (https://github.com/ansible-collections/community.general/pull/3965).
|
||||
- listen_ports_facts - local port regex was not handling well IPv6 only binding. Fixes the regex for ``ss`` (https://github.com/ansible-collections/community.general/pull/4092).
|
||||
- mail callback plugin - fix crash on Python 3 (https://github.com/ansible-collections/community.general/issues/4025, https://github.com/ansible-collections/community.general/pull/4026).
|
||||
- opentelemetry - fix generating a trace with a task containing ``no_log: true`` (https://github.com/ansible-collections/community.general/pull/4043).
|
||||
- python_requirements_info - store ``mismatched`` return values per package as documented in the module (https://github.com/ansible-collections/community.general/pull/4078).
|
||||
- yarn - fix incorrect handling of ``yarn list`` and ``yarn global list`` output that could result in fatal error (https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix incorrectly reported status when installing a package globally (https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix missing ``~`` expansion in yarn global install folder which resulted in incorrect task status (https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4048).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
System
|
||||
~~~~~~
|
||||
|
||||
- homectl - Manage user accounts with systemd-homed
|
||||
|
||||
v4.3.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular feature and bugfix release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- ipa_dnszone - ``dynamicupdate`` is now a boolean parameter, instead of a string parameter accepting ``"true"`` and ``"false"``. Also the module is now idempotent with respect to ``dynamicupdate`` (https://github.com/ansible-collections/community.general/pull/3374).
|
||||
- ipa_dnszone - add DNS zone synchronization support (https://github.com/ansible-collections/community.general/pull/3374).
|
||||
- ipmi_power - add ``machine`` option to ensure the power state via the remote target address (https://github.com/ansible-collections/community.general/pull/3968).
|
||||
- mattermost - add the possibility to send attachments instead of text messages (https://github.com/ansible-collections/community.general/pull/3946).
|
||||
- nmcli - add ``wireguard`` connection type (https://github.com/ansible-collections/community.general/pull/3985).
|
||||
- proxmox - add ``clone`` parameter (https://github.com/ansible-collections/community.general/pull/3930).
|
||||
- puppet - remove deprecation for ``show_diff`` parameter. Its alias ``show-diff`` is still deprecated and will be removed in community.general 7.0.0 (https://github.com/ansible-collections/community.general/pull/3980).
|
||||
- scaleway_compute - add possibility to use project identifier (new ``project`` option) instead of deprecated organization identifier (https://github.com/ansible-collections/community.general/pull/3951).
|
||||
- scaleway_volume - all volumes are systematically created on par1 (https://github.com/ansible-collections/community.general/pull/3964).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- Various modules and plugins - use vendored version of ``distutils.version`` instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.general/pull/3936).
|
||||
- alternatives - fix output parsing for alternatives groups (https://github.com/ansible-collections/community.general/pull/3976).
|
||||
- jail connection plugin - replace deprecated ``distutils.spawn.find_executable`` with Ansible's ``get_bin_path`` to find the executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- lxd connection plugin - replace deprecated ``distutils.spawn.find_executable`` with Ansible's ``get_bin_path`` to find the ``lxc`` executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- passwordstore lookup plugin - replace deprecated ``distutils.util.strtobool`` with Ansible's ``convert_bool.boolean`` to interpret values for the ``create``, ``returnall``, ``overwrite``, 'backup``, and ``nosymbols`` options (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- say callback plugin - replace deprecated ``distutils.spawn.find_executable`` with Ansible's ``get_bin_path`` to find the ``say`` resp. ``espeak`` executables (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- scaleway_user_data - fix double-quote added where no double-quote is needed to user data in scaleway's server (``Content-type`` -> ``Content-Type``) (https://github.com/ansible-collections/community.general/pull/3940).
|
||||
- slack - add ``charset`` to HTTP headers to avoid Slack API warning (https://github.com/ansible-collections/community.general/issues/3932).
|
||||
- zone connection plugin - replace deprecated ``distutils.spawn.find_executable`` with Ansible's ``get_bin_path`` to find the executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
|
||||
New Plugins
|
||||
-----------
|
||||
|
||||
Filter
|
||||
~~~~~~
|
||||
|
||||
- counter - Counts hashable elements in a sequence
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
Identity
|
||||
~~~~~~~~
|
||||
|
||||
keycloak
|
||||
^^^^^^^^
|
||||
|
||||
- keycloak_realm_info - Allows obtaining Keycloak realm public information via Keycloak API
|
||||
|
||||
Packaging
|
||||
~~~~~~~~~
|
||||
|
||||
language
|
||||
^^^^^^^^
|
||||
|
||||
- cargo - Manage Rust packages with cargo
|
||||
|
||||
System
|
||||
~~~~~~
|
||||
|
||||
- sudoers - Manage sudoers files
|
||||
|
||||
v4.2.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- aix_filesystem - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3833).
|
||||
- aix_lvg - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3834).
|
||||
- gitlab - add more token authentication support with the new options ``api_oauth_token`` and ``api_job_token`` (https://github.com/ansible-collections/community.general/issues/705).
|
||||
- gitlab_group, gitlab_project - add new option ``avatar_path`` (https://github.com/ansible-collections/community.general/pull/3792).
|
||||
- gitlab_project - add new option ``default_branch`` to gitlab_project (if ``readme = true``) (https://github.com/ansible-collections/community.general/pull/3792).
|
||||
- hponcfg - revamped module using ModuleHelper (https://github.com/ansible-collections/community.general/pull/3840).
|
||||
- icinga2 inventory plugin - added the ``display_name`` field to variables (https://github.com/ansible-collections/community.general/issues/3875, https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- icinga2 inventory plugin - inventory object names are changable using ``inventory_attr`` in your config file to the host object name, address, or display_name fields (https://github.com/ansible-collections/community.general/issues/3875, https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- ip_netns - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3822).
|
||||
- iso_extract - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3805).
|
||||
- java_cert - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3835).
|
||||
- jira - add support for Bearer token auth (https://github.com/ansible-collections/community.general/pull/3838).
|
||||
- keycloak_user_federation - add sssd user federation support (https://github.com/ansible-collections/community.general/issues/3767).
|
||||
- logentries - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3807).
|
||||
- logstash_plugin - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3808).
|
||||
- lxc_container - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3851).
|
||||
- lxd connection plugin - make sure that ``ansible_lxd_host``, ``ansible_executable``, and ``ansible_lxd_executable`` work (https://github.com/ansible-collections/community.general/pull/3798).
|
||||
- lxd inventory plugin - support virtual machines (https://github.com/ansible-collections/community.general/pull/3519).
|
||||
- module_helper module utils - added decorators ``check_mode_skip`` and ``check_mode_skip_returns`` for skipping methods when ``check_mode=True`` (https://github.com/ansible-collections/community.general/pull/3849).
|
||||
- monit - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3821).
|
||||
- nmcli - add multiple addresses support for ``ip6`` parameter (https://github.com/ansible-collections/community.general/issues/1088).
|
||||
- nmcli - add support for ``eui64`` and ``ipv6privacy`` parameters (https://github.com/ansible-collections/community.general/issues/3357).
|
||||
- python_requirements_info - returns python version broken down into its components, and some minor refactoring (https://github.com/ansible-collections/community.general/pull/3797).
|
||||
- svc - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3829).
|
||||
- xattr - calling ``run_command`` with arguments as ``list`` instead of ``str`` (https://github.com/ansible-collections/community.general/pull/3806).
|
||||
- xfconf - minor refactor on the base class for the module (https://github.com/ansible-collections/community.general/pull/3919).
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- module_helper module utils - deprecated the attribute ``ModuleHelper.VarDict`` (https://github.com/ansible-collections/community.general/pull/3801).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- icinga2 inventory plugin - handle 404 error when filter produces no results (https://github.com/ansible-collections/community.general/issues/3875, https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- interfaces_file - fixed the check for existing option in interface (https://github.com/ansible-collections/community.general/issues/3841).
|
||||
- jira - fixed bug where module returns error related to dictionary key ``body`` (https://github.com/ansible-collections/community.general/issues/3419).
|
||||
- nmcli - fix returning "changed" when no mask set for IPv4 or IPv6 addresses on task rerun (https://github.com/ansible-collections/community.general/issues/3768).
|
||||
- nmcli - pass ``flags``, ``ingress``, ``egress`` params to ``nmcli`` (https://github.com/ansible-collections/community.general/issues/1086).
|
||||
- nrdp callback plugin - fix error ``string arguments without an encoding`` (https://github.com/ansible-collections/community.general/issues/3903).
|
||||
- opentelemetry_plugin - honour ``ignore_errors`` when a task has failed instead of reporting an error (https://github.com/ansible-collections/community.general/pull/3837).
|
||||
- pipx - passes the correct command line option ``--include-apps`` (https://github.com/ansible-collections/community.general/issues/3791).
|
||||
- proxmox - fixed ``onboot`` parameter causing module failures when undefined (https://github.com/ansible-collections/community.general/issues/3844).
|
||||
- python_requirements_info - fails if version operator used without version (https://github.com/ansible-collections/community.general/pull/3785).
|
||||
|
||||
New Modules
|
||||
-----------
|
||||
|
||||
Net Tools
|
||||
~~~~~~~~~
|
||||
|
||||
- dnsimple_info - Pull basic info from DNSimple API
|
||||
|
||||
Remote Management
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
redfish
|
||||
^^^^^^^
|
||||
|
||||
- ilo_redfish_config - Sets or updates configuration attributes on HPE iLO with Redfish OEM extensions
|
||||
- ilo_redfish_info - Gathers server information through iLO using Redfish APIs
|
||||
|
||||
Source Control
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
gitlab
|
||||
^^^^^^
|
||||
|
||||
- gitlab_branch - Create or delete a branch
|
||||
|
||||
v4.1.0
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Regular bugfix and feature release.
|
||||
|
||||
Minor Changes
|
||||
-------------
|
||||
|
||||
- gitlab - clean up modules and utils (https://github.com/ansible-collections/community.general/pull/3694).
|
||||
- ipmi_boot - add support for user-specified IPMI encryption key (https://github.com/ansible-collections/community.general/issues/3698).
|
||||
- ipmi_power - add support for user-specified IPMI encryption key (https://github.com/ansible-collections/community.general/issues/3698).
|
||||
- listen_ports_facts - add support for ``ss`` command besides ``netstat`` (https://github.com/ansible-collections/community.general/pull/3708).
|
||||
- lxd_container - adds ``type`` option which also allows to operate on virtual machines and not just containers (https://github.com/ansible-collections/community.general/pull/3661).
|
||||
- nmcli - add multiple addresses support for ``ip4`` parameter (https://github.com/ansible-collections/community.general/issues/1088, https://github.com/ansible-collections/community.general/pull/3738).
|
||||
- open_iscsi - extended module to allow rescanning of established session for one or all targets (https://github.com/ansible-collections/community.general/issues/3763).
|
||||
- pacman - add ``stdout`` and ``stderr`` as return values (https://github.com/ansible-collections/community.general/pull/3758).
|
||||
- redfish_command - add ``GetHostInterfaces`` command to enable reporting Redfish Host Interface information (https://github.com/ansible-collections/community.general/issues/3693).
|
||||
- redfish_command - add ``SetHostInterface`` command to enable configuring the Redfish Host Interface (https://github.com/ansible-collections/community.general/issues/3632).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- github_repo - ``private`` and ``description`` attributes should not be set to default values when the repo already exists (https://github.com/ansible-collections/community.general/pull/2386).
|
||||
- terraform - fix command options being ignored during planned/plan in function ``build_plan`` such as ``lock`` or ``lock_timeout`` (https://github.com/ansible-collections/community.general/issues/3707, https://github.com/ansible-collections/community.general/pull/3726).
|
||||
|
||||
New Plugins
|
||||
-----------
|
||||
|
||||
Inventory
|
||||
~~~~~~~~~
|
||||
|
||||
- xen_orchestra - Xen Orchestra inventory source
|
||||
|
||||
Lookup
|
||||
~~~~~~
|
||||
|
||||
- revbitspss - Get secrets from RevBits PAM server
|
||||
|
||||
v4.0.2
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release for today's Ansible 5.0.0 beta 2.
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
- Support for Ansible 2.9 and ansible-base 2.10 is deprecated, and will be removed in the next major release (community.general 5.0.0) next spring. While most content will probably still work with ansible-base 2.10, we will remove symbolic links for modules and action plugins, which will make it impossible to use them with Ansible 2.9 anymore. Please use community.general 4.x.y with Ansible 2.9 and ansible-base 2.10, as these releases will continue to support Ansible 2.9 and ansible-base 2.10 even after they are End of Life (https://github.com/ansible-community/community-topics/issues/50, https://github.com/ansible-collections/community.general/pull/3723).
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- counter_enabled callback plugin - fix output to correctly display host and task counters in serial mode (https://github.com/ansible-collections/community.general/pull/3709).
|
||||
- ldap_search - allow it to be used even in check mode (https://github.com/ansible-collections/community.general/issues/3619).
|
||||
- lvol - allows logical volumes to be created with certain size arguments prefixed with ``+`` to preserve behavior of older versions of this module (https://github.com/ansible-collections/community.general/issues/3665).
|
||||
- nmcli - fixed falsely reported changed status when ``mtu`` is omitted with ``dummy`` connections (https://github.com/ansible-collections/community.general/issues/3612, https://github.com/ansible-collections/community.general/pull/3625).
|
||||
|
||||
v4.0.1
|
||||
======
|
||||
|
||||
Release Summary
|
||||
---------------
|
||||
|
||||
Bugfix release for today's Ansible 5.0.0 beta 1.
|
||||
|
||||
Bugfixes
|
||||
--------
|
||||
|
||||
- a_module test plugin - fix crash when testing a module name that was tombstoned (https://github.com/ansible-collections/community.general/pull/3660).
|
||||
- xattr - fix exception caused by ``_run_xattr()`` raising a ``ValueError`` due to a mishandling of base64-encoded value (https://github.com/ansible-collections/community.general/issues/3673).
|
||||
|
||||
v4.0.0
|
||||
======
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Also, consider taking up a valuable, reviewed, but abandoned pull request which
|
||||
|
||||
* Try committing your changes with an informative but short commit message.
|
||||
* Do not squash your commits and force-push to your branch if not needed. Reviews of your pull request are much easier with individual commits to comprehend the pull request history. All commits of your pull request branch will be squashed into one commit by GitHub upon merge.
|
||||
* Do not add merge commits to your PR. The bot will complain and you will have to rebase ([instructions for rebasing](https://docs.ansible.com/ansible/latest/dev_guide/developing_rebasing.html)) to remove them before your PR can be merged. To avoid that git automatically does merges during pulls, you can configure it to do rebases instead by running `git config pull.rebase true` inside the respository checkout.
|
||||
* Do not add merge commits to your PR. The bot will complain and you will have to rebase ([instructions for rebasing](https://docs.ansible.com/ansible/latest/dev_guide/developing_rebasing.html)) to remove them before your PR can be merged. To avoid that git automatically does merges during pulls, you can configure it to do rebases instead by running `git config pull.rebase true` inside the repository checkout.
|
||||
* Make sure your PR includes a [changelog fragment](https://docs.ansible.com/ansible/devel/community/development_process.html#changelogs-how-to). (You must not include a fragment for new modules or new plugins, except for test and filter plugins. Also you shouldn't include one for docs-only changes. If you're not sure, simply don't include one, we'll tell you whether one is needed or not :) )
|
||||
* Avoid reformatting unrelated parts of the codebase in your PR. These types of changes will likely be requested for reversion, create additional work for reviewers, and may cause approval to be delayed.
|
||||
|
||||
@@ -36,6 +36,54 @@ If you want to test a PR locally, refer to [our testing guide](https://github.co
|
||||
|
||||
If you find any inconsistencies or places in this document which can be improved, feel free to raise an issue or pull request to fix it.
|
||||
|
||||
## Run sanity, unit or integration tests locally
|
||||
|
||||
You have to check out the repository into a specific path structure to be able to run `ansible-test`. The path to the git checkout must end with `.../ansible_collections/community/general`. Please see [our testing guide](https://github.com/ansible/community-docs/blob/main/test_pr_locally_guide.rst) for instructions on how to check out the repository into a correct path structure. The short version of these instructions is:
|
||||
|
||||
```.bash
|
||||
mkdir -p ~/dev/ansible_collections/community
|
||||
git clone https://github.com/ansible-collections/community.general.git ~/dev/ansible_collections/community/general
|
||||
cd ~/dev/ansible_collections/community/general
|
||||
```
|
||||
|
||||
Then you can run `ansible-test` (which is a part of [ansible-core](https://pypi.org/project/ansible-core/)) inside the checkout. The following example commands expect that you have installed Docker or Podman. Note that Podman has only been supported by more recent ansible-core releases. If you are using Docker, the following will work with Ansible 2.9+.
|
||||
|
||||
The following commands show how to run sanity tests:
|
||||
|
||||
```.bash
|
||||
# Run sanity tests for all files in the collection:
|
||||
ansible-test sanity --docker -v
|
||||
|
||||
# Run sanity tests for the given files and directories:
|
||||
ansible-test sanity --docker -v plugins/modules/system/pids.py tests/integration/targets/pids/
|
||||
```
|
||||
|
||||
The following commands show how to run unit tests:
|
||||
|
||||
```.bash
|
||||
# Run all unit tests:
|
||||
ansible-test units --docker -v
|
||||
|
||||
# Run all unit tests for one Python version (a lot faster):
|
||||
ansible-test units --docker -v --python 3.8
|
||||
|
||||
# Run a specific unit test (for the nmcli module) for one Python version:
|
||||
ansible-test units --docker -v --python 3.8 tests/unit/plugins/modules/net_tools/test_nmcli.py
|
||||
```
|
||||
|
||||
The following commands show how to run integration tests:
|
||||
|
||||
```.bash
|
||||
# Run integration tests for the interfaces_files module in a Docker container using the
|
||||
# fedora35 operating system image (the supported images depend on your ansible-core version):
|
||||
ansible-test integration --docker fedora35 -v interfaces_file
|
||||
|
||||
# Run integration tests for the flattened lookup **without any isolation**:
|
||||
ansible-test integration -v lookup_flattened
|
||||
```
|
||||
|
||||
If you are unsure about the integration test target name for a module or plugin, you can take a look in `tests/integration/targets/`. Tests for plugins have the plugin type prepended.
|
||||
|
||||
## Creating new modules or plugins
|
||||
|
||||
Creating new modules and plugins requires a bit more work than other Pull Requests.
|
||||
|
||||
@@ -1027,3 +1027,375 @@ releases:
|
||||
name: a_module
|
||||
namespace: null
|
||||
release_date: '2021-11-02'
|
||||
4.0.1:
|
||||
changes:
|
||||
bugfixes:
|
||||
- a_module test plugin - fix crash when testing a module name that was tombstoned
|
||||
(https://github.com/ansible-collections/community.general/pull/3660).
|
||||
- xattr - fix exception caused by ``_run_xattr()`` raising a ``ValueError``
|
||||
due to a mishandling of base64-encoded value (https://github.com/ansible-collections/community.general/issues/3673).
|
||||
release_summary: Bugfix release for today's Ansible 5.0.0 beta 1.
|
||||
fragments:
|
||||
- 3660-a_module-tombstone.yml
|
||||
- 3675-xattr-handle-base64-values.yml
|
||||
- 4.0.1.yml
|
||||
release_date: '2021-11-09'
|
||||
4.0.2:
|
||||
changes:
|
||||
bugfixes:
|
||||
- counter_enabled callback plugin - fix output to correctly display host and
|
||||
task counters in serial mode (https://github.com/ansible-collections/community.general/pull/3709).
|
||||
- ldap_search - allow it to be used even in check mode (https://github.com/ansible-collections/community.general/issues/3619).
|
||||
- lvol - allows logical volumes to be created with certain size arguments prefixed
|
||||
with ``+`` to preserve behavior of older versions of this module (https://github.com/ansible-collections/community.general/issues/3665).
|
||||
- nmcli - fixed falsely reported changed status when ``mtu`` is omitted with
|
||||
``dummy`` connections (https://github.com/ansible-collections/community.general/issues/3612,
|
||||
https://github.com/ansible-collections/community.general/pull/3625).
|
||||
deprecated_features:
|
||||
- Support for Ansible 2.9 and ansible-base 2.10 is deprecated, and will be removed
|
||||
in the next major release (community.general 5.0.0) next spring. While most
|
||||
content will probably still work with ansible-base 2.10, we will remove symbolic
|
||||
links for modules and action plugins, which will make it impossible to use
|
||||
them with Ansible 2.9 anymore. Please use community.general 4.x.y with Ansible
|
||||
2.9 and ansible-base 2.10, as these releases will continue to support Ansible
|
||||
2.9 and ansible-base 2.10 even after they are End of Life (https://github.com/ansible-community/community-topics/issues/50,
|
||||
https://github.com/ansible-collections/community.general/pull/3723).
|
||||
release_summary: Bugfix release for today's Ansible 5.0.0 beta 2.
|
||||
fragments:
|
||||
- 3625-nmcli_false_changed_mtu_fix.yml
|
||||
- 3667-ldap_search.yml
|
||||
- 3681-lvol-fix-create.yml
|
||||
- 3709-support-batch-mode.yml
|
||||
- 4.0.2.yml
|
||||
- deprecate-ansible-2.9-2.10.yml
|
||||
release_date: '2021-11-16'
|
||||
4.1.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- github_repo - ``private`` and ``description`` attributes should not be set
|
||||
to default values when the repo already exists (https://github.com/ansible-collections/community.general/pull/2386).
|
||||
- terraform - fix command options being ignored during planned/plan in function
|
||||
``build_plan`` such as ``lock`` or ``lock_timeout`` (https://github.com/ansible-collections/community.general/issues/3707,
|
||||
https://github.com/ansible-collections/community.general/pull/3726).
|
||||
minor_changes:
|
||||
- gitlab - clean up modules and utils (https://github.com/ansible-collections/community.general/pull/3694).
|
||||
- ipmi_boot - add support for user-specified IPMI encryption key (https://github.com/ansible-collections/community.general/issues/3698).
|
||||
- ipmi_power - add support for user-specified IPMI encryption key (https://github.com/ansible-collections/community.general/issues/3698).
|
||||
- listen_ports_facts - add support for ``ss`` command besides ``netstat`` (https://github.com/ansible-collections/community.general/pull/3708).
|
||||
- lxd_container - adds ``type`` option which also allows to operate on virtual
|
||||
machines and not just containers (https://github.com/ansible-collections/community.general/pull/3661).
|
||||
- nmcli - add multiple addresses support for ``ip4`` parameter (https://github.com/ansible-collections/community.general/issues/1088,
|
||||
https://github.com/ansible-collections/community.general/pull/3738).
|
||||
- open_iscsi - extended module to allow rescanning of established session for
|
||||
one or all targets (https://github.com/ansible-collections/community.general/issues/3763).
|
||||
- pacman - add ``stdout`` and ``stderr`` as return values (https://github.com/ansible-collections/community.general/pull/3758).
|
||||
- redfish_command - add ``GetHostInterfaces`` command to enable reporting Redfish
|
||||
Host Interface information (https://github.com/ansible-collections/community.general/issues/3693).
|
||||
- redfish_command - add ``SetHostInterface`` command to enable configuring the
|
||||
Redfish Host Interface (https://github.com/ansible-collections/community.general/issues/3632).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 1088-nmcli_add_multiple_addresses_support.yml
|
||||
- 2386-github_repo-fix-idempotency-issues.yml
|
||||
- 3632-add-redfish-host-interface-config-support.yml
|
||||
- 3661-lxd_container-add-vm-support.yml
|
||||
- 3693-add-redfish-host-interface-info-support.yml
|
||||
- 3694-gitlab-cleanup.yml
|
||||
- 3702-ipmi-encryption-key.yml
|
||||
- 3708-listen_ports_facts-add-ss-support.yml
|
||||
- 3726-terraform-missing-parameters-planned-fix.yml
|
||||
- 3758-pacman-add-stdout-stderr.yml
|
||||
- 3765-extend-open_iscsi-with-rescan.yml
|
||||
- 4.1.0.yml
|
||||
plugins:
|
||||
inventory:
|
||||
- description: Xen Orchestra inventory source
|
||||
name: xen_orchestra
|
||||
namespace: null
|
||||
lookup:
|
||||
- description: Get secrets from RevBits PAM server
|
||||
name: revbitspss
|
||||
namespace: null
|
||||
release_date: '2021-11-23'
|
||||
4.2.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- icinga2 inventory plugin - handle 404 error when filter produces no results
|
||||
(https://github.com/ansible-collections/community.general/issues/3875, https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- interfaces_file - fixed the check for existing option in interface (https://github.com/ansible-collections/community.general/issues/3841).
|
||||
- jira - fixed bug where module returns error related to dictionary key ``body``
|
||||
(https://github.com/ansible-collections/community.general/issues/3419).
|
||||
- nmcli - fix returning "changed" when no mask set for IPv4 or IPv6 addresses
|
||||
on task rerun (https://github.com/ansible-collections/community.general/issues/3768).
|
||||
- nmcli - pass ``flags``, ``ingress``, ``egress`` params to ``nmcli`` (https://github.com/ansible-collections/community.general/issues/1086).
|
||||
- nrdp callback plugin - fix error ``string arguments without an encoding``
|
||||
(https://github.com/ansible-collections/community.general/issues/3903).
|
||||
- opentelemetry_plugin - honour ``ignore_errors`` when a task has failed instead
|
||||
of reporting an error (https://github.com/ansible-collections/community.general/pull/3837).
|
||||
- pipx - passes the correct command line option ``--include-apps`` (https://github.com/ansible-collections/community.general/issues/3791).
|
||||
- proxmox - fixed ``onboot`` parameter causing module failures when undefined
|
||||
(https://github.com/ansible-collections/community.general/issues/3844).
|
||||
- python_requirements_info - fails if version operator used without version
|
||||
(https://github.com/ansible-collections/community.general/pull/3785).
|
||||
deprecated_features:
|
||||
- module_helper module utils - deprecated the attribute ``ModuleHelper.VarDict``
|
||||
(https://github.com/ansible-collections/community.general/pull/3801).
|
||||
minor_changes:
|
||||
- aix_filesystem - calling ``run_command`` with arguments as ``list`` instead
|
||||
of ``str`` (https://github.com/ansible-collections/community.general/pull/3833).
|
||||
- aix_lvg - calling ``run_command`` with arguments as ``list`` instead of ``str``
|
||||
(https://github.com/ansible-collections/community.general/pull/3834).
|
||||
- gitlab - add more token authentication support with the new options ``api_oauth_token``
|
||||
and ``api_job_token`` (https://github.com/ansible-collections/community.general/issues/705).
|
||||
- gitlab_group, gitlab_project - add new option ``avatar_path`` (https://github.com/ansible-collections/community.general/pull/3792).
|
||||
- gitlab_project - add new option ``default_branch`` to gitlab_project (if ``readme
|
||||
= true``) (https://github.com/ansible-collections/community.general/pull/3792).
|
||||
- hponcfg - revamped module using ModuleHelper (https://github.com/ansible-collections/community.general/pull/3840).
|
||||
- icinga2 inventory plugin - added the ``display_name`` field to variables (https://github.com/ansible-collections/community.general/issues/3875,
|
||||
https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- icinga2 inventory plugin - inventory object names are changable using ``inventory_attr``
|
||||
in your config file to the host object name, address, or display_name fields
|
||||
(https://github.com/ansible-collections/community.general/issues/3875, https://github.com/ansible-collections/community.general/pull/3906).
|
||||
- ip_netns - calling ``run_command`` with arguments as ``list`` instead of ``str``
|
||||
(https://github.com/ansible-collections/community.general/pull/3822).
|
||||
- iso_extract - calling ``run_command`` with arguments as ``list`` instead of
|
||||
``str`` (https://github.com/ansible-collections/community.general/pull/3805).
|
||||
- java_cert - calling ``run_command`` with arguments as ``list`` instead of
|
||||
``str`` (https://github.com/ansible-collections/community.general/pull/3835).
|
||||
- jira - add support for Bearer token auth (https://github.com/ansible-collections/community.general/pull/3838).
|
||||
- keycloak_user_federation - add sssd user federation support (https://github.com/ansible-collections/community.general/issues/3767).
|
||||
- logentries - calling ``run_command`` with arguments as ``list`` instead of
|
||||
``str`` (https://github.com/ansible-collections/community.general/pull/3807).
|
||||
- logstash_plugin - calling ``run_command`` with arguments as ``list`` instead
|
||||
of ``str`` (https://github.com/ansible-collections/community.general/pull/3808).
|
||||
- lxc_container - calling ``run_command`` with arguments as ``list`` instead
|
||||
of ``str`` (https://github.com/ansible-collections/community.general/pull/3851).
|
||||
- lxd connection plugin - make sure that ``ansible_lxd_host``, ``ansible_executable``,
|
||||
and ``ansible_lxd_executable`` work (https://github.com/ansible-collections/community.general/pull/3798).
|
||||
- lxd inventory plugin - support virtual machines (https://github.com/ansible-collections/community.general/pull/3519).
|
||||
- module_helper module utils - added decorators ``check_mode_skip`` and ``check_mode_skip_returns``
|
||||
for skipping methods when ``check_mode=True`` (https://github.com/ansible-collections/community.general/pull/3849).
|
||||
- monit - calling ``run_command`` with arguments as ``list`` instead of ``str``
|
||||
(https://github.com/ansible-collections/community.general/pull/3821).
|
||||
- nmcli - add multiple addresses support for ``ip6`` parameter (https://github.com/ansible-collections/community.general/issues/1088).
|
||||
- nmcli - add support for ``eui64`` and ``ipv6privacy`` parameters (https://github.com/ansible-collections/community.general/issues/3357).
|
||||
- python_requirements_info - returns python version broken down into its components,
|
||||
and some minor refactoring (https://github.com/ansible-collections/community.general/pull/3797).
|
||||
- svc - calling ``run_command`` with arguments as ``list`` instead of ``str``
|
||||
(https://github.com/ansible-collections/community.general/pull/3829).
|
||||
- xattr - calling ``run_command`` with arguments as ``list`` instead of ``str``
|
||||
(https://github.com/ansible-collections/community.general/pull/3806).
|
||||
- xfconf - minor refactor on the base class for the module (https://github.com/ansible-collections/community.general/pull/3919).
|
||||
release_summary: Regular bugfix and feature release.
|
||||
fragments:
|
||||
- 1088-add_multiple_ipv6_address_support.yml
|
||||
- 3357-nmcli-eui64-and-ipv6privacy.yml
|
||||
- 3519-inventory-support-lxd-4.yml
|
||||
- 3768-nmcli_fix_changed_when_no_mask_set.yml
|
||||
- 3780-add-keycloak-sssd-user-federation.yml
|
||||
- 3785-python_requirements_info-versionless-op.yaml
|
||||
- 3792-improve_gitlab_group_and_project.yml
|
||||
- 3797-python_requirements_info-improvements.yaml
|
||||
- 3798-fix-lxd-connection-option-vars-support.yml
|
||||
- 3800-pipx-include-apps.yaml
|
||||
- 3801-mh-deprecate-vardict-attr.yaml
|
||||
- 3805-iso_extract-run_command-list.yaml
|
||||
- 3806-xattr-run_command-list.yaml
|
||||
- 3807-logentries-run_command-list.yaml
|
||||
- 3808-logstash_plugin-run_command-list.yaml
|
||||
- 3821-monit-run-list.yaml
|
||||
- 3822-ip_netns-run-list.yaml
|
||||
- 3829-svc-run-list.yaml
|
||||
- 3833-aix_filesystem-run-list.yaml
|
||||
- 3834-aix-lvg-run-list.yaml
|
||||
- 3835-java-cert-run-list.yaml
|
||||
- 3837-opentelemetry_plugin-honour_ignore_errors.yaml
|
||||
- 3838-jira-token.yaml
|
||||
- 3840-hponcfg-mh-revamp.yaml
|
||||
- 3849-mh-check-mode-decos.yaml
|
||||
- 3851-lxc-container-run-list.yaml
|
||||
- 3862-interfaces-file-fix-dup-option.yaml
|
||||
- 3867-jira-fix-body.yaml
|
||||
- 3874-proxmox-fix-onboot-param.yml
|
||||
- 3875-icinga2-inv-fix.yml
|
||||
- 3896-nmcli_vlan_missing_options.yaml
|
||||
- 3909-nrdp_fix_string_args_without_encoding.yaml
|
||||
- 3919-xfconf-baseclass.yaml
|
||||
- 4.2.0.yml
|
||||
- 705-gitlab-auth-support.yml
|
||||
modules:
|
||||
- description: Pull basic info from DNSimple API
|
||||
name: dnsimple_info
|
||||
namespace: net_tools
|
||||
- description: Create or delete a branch
|
||||
name: gitlab_branch
|
||||
namespace: source_control.gitlab
|
||||
- description: Sets or updates configuration attributes on HPE iLO with Redfish
|
||||
OEM extensions
|
||||
name: ilo_redfish_config
|
||||
namespace: remote_management.redfish
|
||||
- description: Gathers server information through iLO using Redfish APIs
|
||||
name: ilo_redfish_info
|
||||
namespace: remote_management.redfish
|
||||
release_date: '2021-12-21'
|
||||
4.3.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- Various modules and plugins - use vendored version of ``distutils.version``
|
||||
instead of the deprecated Python standard library ``distutils`` (https://github.com/ansible-collections/community.general/pull/3936).
|
||||
- alternatives - fix output parsing for alternatives groups (https://github.com/ansible-collections/community.general/pull/3976).
|
||||
- jail connection plugin - replace deprecated ``distutils.spawn.find_executable``
|
||||
with Ansible's ``get_bin_path`` to find the executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- lxd connection plugin - replace deprecated ``distutils.spawn.find_executable``
|
||||
with Ansible's ``get_bin_path`` to find the ``lxc`` executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- passwordstore lookup plugin - replace deprecated ``distutils.util.strtobool``
|
||||
with Ansible's ``convert_bool.boolean`` to interpret values for the ``create``,
|
||||
``returnall``, ``overwrite``, 'backup``, and ``nosymbols`` options (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- say callback plugin - replace deprecated ``distutils.spawn.find_executable``
|
||||
with Ansible's ``get_bin_path`` to find the ``say`` resp. ``espeak`` executables
|
||||
(https://github.com/ansible-collections/community.general/pull/3934).
|
||||
- scaleway_user_data - fix double-quote added where no double-quote is needed
|
||||
to user data in scaleway's server (``Content-type`` -> ``Content-Type``) (https://github.com/ansible-collections/community.general/pull/3940).
|
||||
- slack - add ``charset`` to HTTP headers to avoid Slack API warning (https://github.com/ansible-collections/community.general/issues/3932).
|
||||
- zone connection plugin - replace deprecated ``distutils.spawn.find_executable``
|
||||
with Ansible's ``get_bin_path`` to find the executable (https://github.com/ansible-collections/community.general/pull/3934).
|
||||
minor_changes:
|
||||
- ipa_dnszone - ``dynamicupdate`` is now a boolean parameter, instead of a string
|
||||
parameter accepting ``"true"`` and ``"false"``. Also the module is now idempotent
|
||||
with respect to ``dynamicupdate`` (https://github.com/ansible-collections/community.general/pull/3374).
|
||||
- ipa_dnszone - add DNS zone synchronization support (https://github.com/ansible-collections/community.general/pull/3374).
|
||||
- ipmi_power - add ``machine`` option to ensure the power state via the remote
|
||||
target address (https://github.com/ansible-collections/community.general/pull/3968).
|
||||
- mattermost - add the possibility to send attachments instead of text messages
|
||||
(https://github.com/ansible-collections/community.general/pull/3946).
|
||||
- nmcli - add ``wireguard`` connection type (https://github.com/ansible-collections/community.general/pull/3985).
|
||||
- proxmox - add ``clone`` parameter (https://github.com/ansible-collections/community.general/pull/3930).
|
||||
- puppet - remove deprecation for ``show_diff`` parameter. Its alias ``show-diff``
|
||||
is still deprecated and will be removed in community.general 7.0.0 (https://github.com/ansible-collections/community.general/pull/3980).
|
||||
- scaleway_compute - add possibility to use project identifier (new ``project``
|
||||
option) instead of deprecated organization identifier (https://github.com/ansible-collections/community.general/pull/3951).
|
||||
- scaleway_volume - all volumes are systematically created on par1 (https://github.com/ansible-collections/community.general/pull/3964).
|
||||
release_summary: Regular feature and bugfix release.
|
||||
fragments:
|
||||
- 3374-add-ipa-ptr-sync-support.yml
|
||||
- 3921-add-counter-filter-plugin.yml
|
||||
- 3930-proxmox-add-clone.yaml
|
||||
- 3933-slack-charset-header.yaml
|
||||
- 3934-distutils.yml
|
||||
- 3936-distutils.version.yml
|
||||
- 3940_fix_contenttype_scaleway_user_data.yml
|
||||
- 3946-mattermost_attachments.yml
|
||||
- 3951-scaleway_compute_add_project_id.yml
|
||||
- 3964-scaleway_volume_add_region.yml
|
||||
- 3968-ipmi_power-add-machine-option.yaml
|
||||
- 3976-fix-alternatives-parsing.yml
|
||||
- 3980-puppet-show_diff.yml
|
||||
- 3985-nmcli-add-wireguard-connection-type.yml
|
||||
- 4.3.0.yml
|
||||
modules:
|
||||
- description: Manage Rust packages with cargo
|
||||
name: cargo
|
||||
namespace: packaging.language
|
||||
- description: Allows obtaining Keycloak realm public information via Keycloak
|
||||
API
|
||||
name: keycloak_realm_info
|
||||
namespace: identity.keycloak
|
||||
- description: Manage sudoers files
|
||||
name: sudoers
|
||||
namespace: system
|
||||
plugins:
|
||||
filter:
|
||||
- description: Counts hashable elements in a sequence
|
||||
name: counter
|
||||
namespace: null
|
||||
release_date: '2022-01-11'
|
||||
4.4.0:
|
||||
changes:
|
||||
bugfixes:
|
||||
- cargo - fix detection of outdated packages when ``state=latest`` (https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- cargo - fix incorrectly reported changed status for packages with a name containing
|
||||
a hyphen (https://github.com/ansible-collections/community.general/issues/4044,
|
||||
https://github.com/ansible-collections/community.general/pull/4052).
|
||||
- gitlab_project_variable - add missing documentation about GitLab versions
|
||||
that support ``environment_scope`` and ``variable_type`` (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- 'gitlab_project_variable - allow to set same variable name under different
|
||||
environment scopes. Due this change, the return value ``project_variable``
|
||||
differs from previous version in check mode. It was counting ``updated`` values,
|
||||
because it was accidentally overwriting environment scopes (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
|
||||
'
|
||||
- gitlab_project_variable - fix idempotent change behaviour for float and integer
|
||||
variables (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- gitlab_runner - use correct API endpoint to create and retrieve project level
|
||||
runners when using ``project`` (https://github.com/ansible-collections/community.general/pull/3965).
|
||||
- listen_ports_facts - local port regex was not handling well IPv6 only binding.
|
||||
Fixes the regex for ``ss`` (https://github.com/ansible-collections/community.general/pull/4092).
|
||||
- mail callback plugin - fix crash on Python 3 (https://github.com/ansible-collections/community.general/issues/4025,
|
||||
https://github.com/ansible-collections/community.general/pull/4026).
|
||||
- 'opentelemetry - fix generating a trace with a task containing ``no_log: true``
|
||||
(https://github.com/ansible-collections/community.general/pull/4043).'
|
||||
- python_requirements_info - store ``mismatched`` return values per package
|
||||
as documented in the module (https://github.com/ansible-collections/community.general/pull/4078).
|
||||
- yarn - fix incorrect handling of ``yarn list`` and ``yarn global list`` output
|
||||
that could result in fatal error (https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix incorrectly reported status when installing a package globally
|
||||
(https://github.com/ansible-collections/community.general/issues/4045, https://github.com/ansible-collections/community.general/pull/4050).
|
||||
- yarn - fix missing ``~`` expansion in yarn global install folder which resulted
|
||||
in incorrect task status (https://github.com/ansible-collections/community.general/issues/4045,
|
||||
https://github.com/ansible-collections/community.general/pull/4048).
|
||||
deprecated_features:
|
||||
- mail callback plugin - not specifying ``sender`` is deprecated and will be
|
||||
disallowed in community.general 6.0.0 (https://github.com/ansible-collections/community.general/pull/4140).
|
||||
minor_changes:
|
||||
- cobbler inventory plugin - add ``include_profiles`` option (https://github.com/ansible-collections/community.general/pull/4068).
|
||||
- gitlab_project_variable - new ``variables`` parameter (https://github.com/ansible-collections/community.general/issues/4038).
|
||||
- icinga2 inventory plugin - implemented constructed interface (https://github.com/ansible-collections/community.general/pull/4088).
|
||||
- linode inventory plugin - allow templating of ``access_token`` variable in
|
||||
Linode inventory plugin (https://github.com/ansible-collections/community.general/pull/4040).
|
||||
- lists_mergeby filter plugin - add parameters ``list_merge`` and ``recursive``.
|
||||
These are only supported when used with ansible-base 2.10 or ansible-core,
|
||||
but not with Ansible 2.9 (https://github.com/ansible-collections/community.general/pull/4058).
|
||||
- lxc_container - added ``wait_for_container`` parameter. If ``true`` the module
|
||||
will wait until the running task reports success as the status (https://github.com/ansible-collections/community.general/pull/4039).
|
||||
- mail callback plugin - add ``Message-ID`` and ``Date`` headers (https://github.com/ansible-collections/community.general/issues/4055,
|
||||
https://github.com/ansible-collections/community.general/pull/4056).
|
||||
- mail callback plugin - properly use Ansible's option handling to split lists
|
||||
(https://github.com/ansible-collections/community.general/pull/4140).
|
||||
- nmcli - adds ``routes6`` and ``route_metric6`` parameters for supporting IPv6
|
||||
routes (https://github.com/ansible-collections/community.general/issues/4059).
|
||||
- opennebula - add the release action for VMs in the ``HOLD`` state (https://github.com/ansible-collections/community.general/pull/4036).
|
||||
- opentelemetry_plugin - enrich service when using the ``docker_login`` (https://github.com/ansible-collections/community.general/pull/4104).
|
||||
- proxmox modules - move ``HAS_PROXMOXER`` check into ``module_utils`` (https://github.com/ansible-collections/community.general/pull/4030).
|
||||
- scaleway inventory plugin - add profile parameter ``scw_profile`` (https://github.com/ansible-collections/community.general/pull/4049).
|
||||
- snap - add option ``options`` permitting to set options using the ``snap set``
|
||||
command (https://github.com/ansible-collections/community.general/pull/3943).
|
||||
release_summary: Regular features and bugfixes release.
|
||||
fragments:
|
||||
- 3935-use-gitlab-instance-runner-to-create-runner.yml
|
||||
- 3943-add-option-options-to-snap-module.yml
|
||||
- 4.4.0.yml
|
||||
- 4026-fix-mail-callback.yml
|
||||
- 4030-proxmox-has-proxmoxer.yml
|
||||
- 4036-onevm-add-release-action.yaml
|
||||
- 4038-fix-and-rework-gitlb-project-variable.yml
|
||||
- 4039-cluster-container-wait.yml
|
||||
- 4040-linode-token-templating.yaml
|
||||
- 4043-fix-no-log-opentelemetry.yml
|
||||
- 4048-expand-tilde-in-yarn-global-install-folder.yaml
|
||||
- 4049-profile-for-scaleway-inventory.yml
|
||||
- 4050-properly-parse-json-lines-output-from-yarn.yaml
|
||||
- 4052-fix-detection-of-installed-cargo-packages-with-hyphens.yaml
|
||||
- 4056-add-missing-mail-headers.yml
|
||||
- 4058-lists_mergeby-add-parameters.yml
|
||||
- 4062-nmcli-ipv6-routes-support.yml
|
||||
- 4068-add-include_file-option.yml
|
||||
- 4078-python_requirements_info.yaml
|
||||
- 4088-add-constructed-interface-for-icinga2-inventory.yml
|
||||
- 4092-fix_local_ports_regex_listen_ports_facts.yaml
|
||||
- 4104-opentelemetry_plugin-enrich_docker_login.yaml
|
||||
- 4140-mail-callback-options.yml
|
||||
modules:
|
||||
- description: Manage user accounts with systemd-homed
|
||||
name: homectl
|
||||
namespace: system
|
||||
release_date: '2022-02-01'
|
||||
|
||||
8
docs/docsite/helper/lists_mergeby/examples.rst.j2
Normal file
8
docs/docsite/helper/lists_mergeby/examples.rst.j2
Normal file
@@ -0,0 +1,8 @@
|
||||
{% for i in examples %}
|
||||
{{ i.label }}
|
||||
|
||||
.. code-block:: {{ i.lang }}
|
||||
|
||||
{{ lookup('file', source_path ~ i.file)|indent(2) }}
|
||||
|
||||
{% endfor %}
|
||||
38
docs/docsite/helper/lists_mergeby/examples.yml
Normal file
38
docs/docsite/helper/lists_mergeby/examples.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
examples:
|
||||
- label: 'Example ``list_merge=replace`` (default):'
|
||||
file: example-003.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-003.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=keep``:'
|
||||
file: example-004.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-004.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=append``:'
|
||||
file: example-005.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-005.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=prepend``:'
|
||||
file: example-006.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-006.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=append_rp``:'
|
||||
file: example-007.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-007.out
|
||||
lang: 'yaml'
|
||||
- label: 'Example ``list_merge=prepend_rp``:'
|
||||
file: example-008.yml
|
||||
lang: 'yaml+jinja'
|
||||
- label: 'This produces:'
|
||||
file: example-008.out
|
||||
lang: 'yaml'
|
||||
41
docs/docsite/helper/lists_mergeby/playbook.yml
Normal file
41
docs/docsite/helper/lists_mergeby/playbook.yml
Normal file
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# The following runs all examples:
|
||||
#
|
||||
# ANSIBLE_STDOUT_CALLBACK=community.general.yaml ansible-playbook playbook.yml -e examples=true
|
||||
#
|
||||
# You need to copy the YAML output of example-XXX.yml into example-XXX.out.
|
||||
#
|
||||
# The following generates examples.rst out of the .out files:
|
||||
#
|
||||
# ansible-playbook playbook.yml -e template=true
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
vars:
|
||||
source_path: ../../rst/examples/lists_mergeby/
|
||||
tasks:
|
||||
|
||||
- block:
|
||||
- import_tasks: '{{ source_path }}example-001.yml'
|
||||
tags: t001
|
||||
- import_tasks: '{{ source_path }}example-002.yml'
|
||||
tags: t002
|
||||
- import_tasks: '{{ source_path }}example-003.yml'
|
||||
tags: t003
|
||||
- import_tasks: '{{ source_path }}example-004.yml'
|
||||
tags: t004
|
||||
- import_tasks: '{{ source_path }}example-005.yml'
|
||||
tags: t005
|
||||
- import_tasks: '{{ source_path }}example-006.yml'
|
||||
tags: t006
|
||||
- import_tasks: '{{ source_path }}example-007.yml'
|
||||
tags: t007
|
||||
- import_tasks: '{{ source_path }}example-008.yml'
|
||||
tags: t008
|
||||
when: examples|d(false)|bool
|
||||
|
||||
- block:
|
||||
- include_vars: examples.yml
|
||||
- template:
|
||||
src: examples.rst.j2
|
||||
dest: examples.rst
|
||||
when: template|d(false)|bool
|
||||
10
docs/docsite/rst/examples/lists_mergeby/example-001.out
Normal file
10
docs/docsite/rst/examples/lists_mergeby/example-001.out
Normal file
@@ -0,0 +1,10 @@
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
20
docs/docsite/rst/examples/lists_mergeby/example-001.yml
Normal file
20
docs/docsite/rst/examples/lists_mergeby/example-001.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
set_fact:
|
||||
list3: "{{ list1|
|
||||
community.general.lists_mergeby(list2, 'name') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
- debug:
|
||||
var: list3
|
||||
10
docs/docsite/rst/examples/lists_mergeby/example-002.out
Normal file
10
docs/docsite/rst/examples/lists_mergeby/example-002.out
Normal file
@@ -0,0 +1,10 @@
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
20
docs/docsite/rst/examples/lists_mergeby/example-002.yml
Normal file
20
docs/docsite/rst/examples/lists_mergeby/example-002.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
- debug:
|
||||
var: list3
|
||||
14
docs/docsite/rst/examples/lists_mergeby/example-003.out
Normal file
14
docs/docsite/rst/examples/lists_mergeby/example-003.out
Normal file
@@ -0,0 +1,14 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
28
docs/docsite/rst/examples/lists_mergeby/example-003.yml
Normal file
28
docs/docsite/rst/examples/lists_mergeby/example-003.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', replace lists (default)
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true) }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
14
docs/docsite/rst/examples/lists_mergeby/example-004.out
Normal file
14
docs/docsite/rst/examples/lists_mergeby/example-004.out
Normal file
@@ -0,0 +1,14 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
29
docs/docsite/rst/examples/lists_mergeby/example-004.yml
Normal file
29
docs/docsite/rst/examples/lists_mergeby/example-004.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', keep lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='keep') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
19
docs/docsite/rst/examples/lists_mergeby/example-005.out
Normal file
19
docs/docsite/rst/examples/lists_mergeby/example-005.out
Normal file
@@ -0,0 +1,19 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
29
docs/docsite/rst/examples/lists_mergeby/example-005.yml
Normal file
29
docs/docsite/rst/examples/lists_mergeby/example-005.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', append lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
19
docs/docsite/rst/examples/lists_mergeby/example-006.out
Normal file
19
docs/docsite/rst/examples/lists_mergeby/example-006.out
Normal file
@@ -0,0 +1,19 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
29
docs/docsite/rst/examples/lists_mergeby/example-006.yml
Normal file
29
docs/docsite/rst/examples/lists_mergeby/example-006.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', prepend lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
18
docs/docsite/rst/examples/lists_mergeby/example-007.out
Normal file
18
docs/docsite/rst/examples/lists_mergeby/example-007.out
Normal file
@@ -0,0 +1,18 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
29
docs/docsite/rst/examples/lists_mergeby/example-007.yml
Normal file
29
docs/docsite/rst/examples/lists_mergeby/example-007.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', append lists 'remove present'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append_rp') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
18
docs/docsite/rst/examples/lists_mergeby/example-008.out
Normal file
18
docs/docsite/rst/examples/lists_mergeby/example-008.out
Normal file
@@ -0,0 +1,18 @@
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
29
docs/docsite/rst/examples/lists_mergeby/example-008.yml
Normal file
29
docs/docsite/rst/examples/lists_mergeby/example-008.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
- name: Merge recursive by 'name', prepend lists 'remove present'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend_rp') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
.. _ansible_collections.community.general.docsite.filter_guide:
|
||||
|
||||
community.general Filter Guide
|
||||
@@ -5,780 +6,14 @@ community.general Filter Guide
|
||||
|
||||
The :ref:`community.general collection <plugins_in_community.general>` offers several useful filter plugins.
|
||||
|
||||
.. contents:: Topics
|
||||
|
||||
Paths
|
||||
-----
|
||||
|
||||
The ``path_join`` filter has been added in ansible-base 2.10. If you want to use this filter, but also need to support Ansible 2.9, you can use ``community.general``'s ``path_join`` shim, ``community.general.path_join``. This filter redirects to ``path_join`` for ansible-base 2.10 and ansible-core 2.11 or newer, and re-implements the filter for Ansible 2.9.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
# ansible-base 2.10 or newer:
|
||||
path: {{ ('/etc', path, 'subdir', file) | path_join }}
|
||||
|
||||
# Also works with Ansible 2.9:
|
||||
path: {{ ('/etc', path, 'subdir', file) | community.general.path_join }}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
Abstract transformations
|
||||
------------------------
|
||||
|
||||
Dictionaries
|
||||
^^^^^^^^^^^^
|
||||
|
||||
You can use the ``dict_kv`` filter to create a single-entry dictionary with ``value | community.general.dict_kv(key)``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a single-entry dictionary
|
||||
debug:
|
||||
msg: "{{ myvar | community.general.dict_kv('thatsmyvar') }}"
|
||||
vars:
|
||||
myvar: myvalue
|
||||
|
||||
- name: Create a list of dictionaries where the 'server' field is taken from a list
|
||||
debug:
|
||||
msg: >-
|
||||
{{ myservers | map('community.general.dict_kv', 'server')
|
||||
| map('combine', common_config) }}
|
||||
vars:
|
||||
common_config:
|
||||
type: host
|
||||
database: all
|
||||
myservers:
|
||||
- server1
|
||||
- server2
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a single-entry dictionary] **************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"thatsmyvar": "myvalue"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries where the 'server' field is taken from a list] *******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server1",
|
||||
"type": "host"
|
||||
},
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server2",
|
||||
"type": "host"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
If you need to convert a list of key-value pairs to a dictionary, you can use the ``dict`` function. Unfortunately, this function cannot be used with ``map``. For this, the ``community.general.dict`` filter can be used:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a dictionary with the dict function
|
||||
debug:
|
||||
msg: "{{ dict([[1, 2], ['a', 'b']]) }}"
|
||||
|
||||
- name: Create a dictionary with the community.general.dict filter
|
||||
debug:
|
||||
msg: "{{ [[1, 2], ['a', 'b']] | community.general.dict }}"
|
||||
|
||||
- name: Create a list of dictionaries with map and the community.general.dict filter
|
||||
debug:
|
||||
msg: >-
|
||||
{{ values | map('zip', ['k1', 'k2', 'k3'])
|
||||
| map('map', 'reverse')
|
||||
| map('community.general.dict') }}
|
||||
vars:
|
||||
values:
|
||||
- - foo
|
||||
- 23
|
||||
- a
|
||||
- - bar
|
||||
- 42
|
||||
- b
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a dictionary with the dict function] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a dictionary with the community.general.dict filter] ************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries with map and the community.general.dict filter] ******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"k1": "foo",
|
||||
"k2": 23,
|
||||
"k3": "a"
|
||||
},
|
||||
{
|
||||
"k1": "bar",
|
||||
"k2": 42,
|
||||
"k3": "b"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
|
||||
Grouping
|
||||
^^^^^^^^
|
||||
|
||||
If you have a list of dictionaries, the Jinja2 ``groupby`` filter allows to group the list by an attribute. This results in a list of ``(grouper, list)`` namedtuples, where ``list`` contains all dictionaries where the selected attribute equals ``grouper``. If you know that for every ``grouper``, there will be a most one entry in that list, you can use the ``community.general.groupby_as_dict`` filter to convert the original list into a dictionary which maps ``grouper`` to the corresponding dictionary.
|
||||
|
||||
One example is ``ansible_facts.mounts``, which is a list of dictionaries where each has one ``device`` element to indicate the device which is mounted. Therefore, ``ansible_facts.mounts | community.general.groupby_as_dict('device')`` is a dictionary mapping a device to the mount information:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Output mount facts grouped by device name
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('device')
|
||||
|
||||
- name: Output mount facts grouped by mount point
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('mount')
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Output mount facts grouped by device name] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('device')": {
|
||||
"/dev/sda1": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
},
|
||||
"/dev/sda2": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "abcdef01-2345-6789-0abc-def012345678"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Output mount facts grouped by mount point] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('mount')": {
|
||||
"/": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "bdf50b7d-4859-40af-8665-c637ee7a7808"
|
||||
},
|
||||
"/boot": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Merging lists of dictionaries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have two lists of dictionaries and want to combine them into a list of merged dictionaries, where two dictionaries are merged if they coincide in one attribute, you can use the ``lists_mergeby`` filter.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
debug:
|
||||
var: list1 | community.general.lists_mergeby(list2, 'name')
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /bazzz
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Merge two lists by common attribute 'name'] ****************************************
|
||||
ok: [localhost] => {
|
||||
"list1 | community.general.lists_mergeby(list2, 'name')": [
|
||||
{
|
||||
"extra": false,
|
||||
"name": "bar"
|
||||
},
|
||||
{
|
||||
"name": "baz",
|
||||
"path": "/bazzz"
|
||||
},
|
||||
{
|
||||
"extra": true,
|
||||
"name": "foo",
|
||||
"path": "/foo"
|
||||
},
|
||||
{
|
||||
"extra": true,
|
||||
"name": "meh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
|
||||
Working with times
|
||||
------------------
|
||||
|
||||
The ``to_time_unit`` filter allows to convert times from a human-readable string to a unit. For example, ``'4h 30min 12second' | community.general.to_time_unit('hour')`` gives the number of hours that correspond to 4 hours, 30 minutes and 12 seconds.
|
||||
|
||||
There are shorthands to directly convert to various units, like ``to_hours``, ``to_minutes``, ``to_seconds``, and so on. The following table lists all units that can be used:
|
||||
|
||||
.. list-table:: Units
|
||||
:widths: 25 25 25 25
|
||||
:header-rows: 1
|
||||
|
||||
* - Unit name
|
||||
- Unit value in seconds
|
||||
- Unit strings for filter
|
||||
- Shorthand filter
|
||||
* - Millisecond
|
||||
- 1/1000 second
|
||||
- ``ms``, ``millisecond``, ``milliseconds``, ``msec``, ``msecs``, ``msecond``, ``mseconds``
|
||||
- ``to_milliseconds``
|
||||
* - Second
|
||||
- 1 second
|
||||
- ``s``, ``sec``, ``secs``, ``second``, ``seconds``
|
||||
- ``to_seconds``
|
||||
* - Minute
|
||||
- 60 seconds
|
||||
- ``m``, ``min``, ``mins``, ``minute``, ``minutes``
|
||||
- ``to_minutes``
|
||||
* - Hour
|
||||
- 60*60 seconds
|
||||
- ``h``, ``hour``, ``hours``
|
||||
- ``to_hours``
|
||||
* - Day
|
||||
- 24*60*60 seconds
|
||||
- ``d``, ``day``, ``days``
|
||||
- ``to_days``
|
||||
* - Week
|
||||
- 7*24*60*60 seconds
|
||||
- ``w``, ``week``, ``weeks``
|
||||
- ``to_weeks``
|
||||
* - Month
|
||||
- 30*24*60*60 seconds
|
||||
- ``mo``, ``month``, ``months``
|
||||
- ``to_months``
|
||||
* - Year
|
||||
- 365*24*60*60 seconds
|
||||
- ``y``, ``year``, ``years``
|
||||
- ``to_years``
|
||||
|
||||
Note that months and years are using a simplified representation: a month is 30 days, and a year is 365 days. If you need different definitions of months or years, you can pass them as keyword arguments. For example, if you want a year to be 365.25 days, and a month to be 30.5 days, you can write ``'11months 4' | community.general.to_years(year=365.25, month=30.5)``. These keyword arguments can be specified to ``to_time_unit`` and to all shorthand filters.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Convert string to seconds
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_time_unit('seconds') }}"
|
||||
|
||||
- name: Convert string to hours
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_hours }}"
|
||||
|
||||
- name: Convert string to years (using 365.25 days == 1 year)
|
||||
debug:
|
||||
msg: "{{ '400d 15h' | community.general.to_years(year=365.25) }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Convert string to seconds] **********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "109210.123"
|
||||
}
|
||||
|
||||
TASK [Convert string to hours] ************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "30.336145277778"
|
||||
}
|
||||
|
||||
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
||||
ok: [localhost] => {
|
||||
"msg": "1.096851471595"
|
||||
}
|
||||
|
||||
.. versionadded: 0.2.0
|
||||
|
||||
Working with versions
|
||||
---------------------
|
||||
|
||||
If you need to sort a list of version numbers, the Jinja ``sort`` filter is problematic. Since it sorts lexicographically, ``2.10`` will come before ``2.9``. To treat version numbers correctly, you can use the ``version_sort`` filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Sort list by version number
|
||||
debug:
|
||||
var: ansible_versions | community.general.version_sort
|
||||
vars:
|
||||
ansible_versions:
|
||||
- '2.8.0'
|
||||
- '2.11.0'
|
||||
- '2.7.0'
|
||||
- '2.10.0'
|
||||
- '2.9.0'
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Sort list by version number] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_versions | community.general.version_sort": [
|
||||
"2.7.0",
|
||||
"2.8.0",
|
||||
"2.9.0",
|
||||
"2.10.0",
|
||||
"2.11.0"
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.2.0
|
||||
|
||||
Creating identifiers
|
||||
--------------------
|
||||
|
||||
The following filters allow to create identifiers.
|
||||
|
||||
Hashids
|
||||
^^^^^^^
|
||||
|
||||
`Hashids <https://hashids.org/>`_ allow to convert sequences of integers to short unique string identifiers. This filter needs the `hashids Python library <https://pypi.org/project/hashids/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create hashid"
|
||||
debug:
|
||||
msg: "{{ [1234, 5, 6] | community.general.hashids_encode }}"
|
||||
|
||||
- name: "Decode hashid"
|
||||
debug:
|
||||
msg: "{{ 'jm2Cytn' | community.general.hashids_decode }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "jm2Cytn"
|
||||
}
|
||||
|
||||
TASK [Decode hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
1234,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
|
||||
The hashids filters accept keyword arguments to allow fine-tuning the hashids generated:
|
||||
|
||||
:salt: String to use as salt when hashing.
|
||||
:alphabet: String of 16 or more unique characters to produce a hash.
|
||||
:min_length: Minimum length of hash produced.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Random MACs
|
||||
^^^^^^^^^^^
|
||||
|
||||
You can use the ``random_mac`` filter to complete a partial `MAC address <https://en.wikipedia.org/wiki/MAC_address>`_ to a random 6-byte MAC address.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create a random MAC starting with ff:"
|
||||
debug:
|
||||
msg: "{{ 'FF' | community.general.random_mac }}"
|
||||
|
||||
- name: "Create a random MAC starting with 00:11:22:"
|
||||
debug:
|
||||
msg: "{{ '00:11:22' | community.general.random_mac }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a random MAC starting with ff:] **********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "ff:69:d3:78:7f:b4"
|
||||
}
|
||||
|
||||
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "00:11:22:71:5d:3b"
|
||||
}
|
||||
|
||||
You can also initialize the random number generator from a seed to create random-but-idempotent MAC addresses:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
|
||||
|
||||
Conversions
|
||||
-----------
|
||||
|
||||
Parsing CSV files
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ansible offers the :ref:`community.general.read_csv module <ansible_collections.community.general.read_csv_module>` to read CSV files. Sometimes you need to convert strings to CSV files instead. For this, the ``from_csv`` filter exists.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Parse CSV from string"
|
||||
debug:
|
||||
msg: "{{ csv_string | community.general.from_csv }}"
|
||||
vars:
|
||||
csv_string: |
|
||||
foo,bar,baz
|
||||
1,2,3
|
||||
you,this,then
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Parse CSV from string] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"bar": "2",
|
||||
"baz": "3",
|
||||
"foo": "1"
|
||||
},
|
||||
{
|
||||
"bar": "this",
|
||||
"baz": "then",
|
||||
"foo": "you"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The ``from_csv`` filter has several keyword arguments to control its behavior:
|
||||
|
||||
:dialect: Dialect of the CSV file. Default is ``excel``. Other possible choices are ``excel-tab`` and ``unix``. If one of ``delimiter``, ``skipinitialspace`` or ``strict`` is specified, ``dialect`` is ignored.
|
||||
:fieldnames: A set of column names to use. If not provided, the first line of the CSV is assumed to contain the column names.
|
||||
:delimiter: Sets the delimiter to use. Default depends on the dialect used.
|
||||
:skipinitialspace: Set to ``true`` to ignore space directly after the delimiter. Default depends on the dialect used (usually ``false``).
|
||||
:strict: Set to ``true`` to error out on invalid CSV input.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Converting to JSON
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`JC <https://pypi.org/project/jc/>`_ is a CLI tool and Python library which allows to interpret output of various CLI programs as JSON. It is also available as a filter in community.general. This filter needs the `jc Python library <https://pypi.org/project/jc/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Run 'ls' to list files in /
|
||||
command: ls /
|
||||
register: result
|
||||
|
||||
- name: Parse the ls output
|
||||
debug:
|
||||
msg: "{{ result.stdout | community.general.jc('ls') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Run 'ls' to list files in /] ********************************************************
|
||||
changed: [localhost]
|
||||
|
||||
TASK [Parse the ls output] ****************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"filename": "bin"
|
||||
},
|
||||
{
|
||||
"filename": "boot"
|
||||
},
|
||||
{
|
||||
"filename": "dev"
|
||||
},
|
||||
{
|
||||
"filename": "etc"
|
||||
},
|
||||
{
|
||||
"filename": "home"
|
||||
},
|
||||
{
|
||||
"filename": "lib"
|
||||
},
|
||||
{
|
||||
"filename": "proc"
|
||||
},
|
||||
{
|
||||
"filename": "root"
|
||||
},
|
||||
{
|
||||
"filename": "run"
|
||||
},
|
||||
{
|
||||
"filename": "tmp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
|
||||
.. _ansible_collections.community.general.docsite.json_query_filter:
|
||||
|
||||
Selecting JSON data: JSON queries
|
||||
---------------------------------
|
||||
|
||||
To select a single element or a data subset from a complex data structure in JSON format (for example, Ansible facts), use the ``json_query`` filter. The ``json_query`` filter lets you query a complex JSON structure and iterate over it using a loop structure.
|
||||
|
||||
.. note:: You must manually install the **jmespath** dependency on the Ansible controller before using this filter. This filter is built upon **jmespath**, and you can use the same syntax. For examples, see `jmespath examples <http://jmespath.org/examples.html>`_.
|
||||
|
||||
Consider this data structure:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
{
|
||||
"domain_definition": {
|
||||
"domain": {
|
||||
"cluster": [
|
||||
{
|
||||
"name": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "cluster2"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
"name": "server11",
|
||||
"cluster": "cluster1",
|
||||
"port": "8080"
|
||||
},
|
||||
{
|
||||
"name": "server12",
|
||||
"cluster": "cluster1",
|
||||
"port": "8090"
|
||||
},
|
||||
{
|
||||
"name": "server21",
|
||||
"cluster": "cluster2",
|
||||
"port": "9080"
|
||||
},
|
||||
{
|
||||
"name": "server22",
|
||||
"cluster": "cluster2",
|
||||
"port": "9090"
|
||||
}
|
||||
],
|
||||
"library": [
|
||||
{
|
||||
"name": "lib1",
|
||||
"target": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "lib2",
|
||||
"target": "cluster2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
To extract all clusters from this structure, you can use the following query:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all cluster names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"
|
||||
|
||||
To extract all server names:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[*].name') }}"
|
||||
|
||||
To extract ports from cluster1:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"
|
||||
|
||||
.. note:: You can use a variable to make the query more readable.
|
||||
|
||||
To print out the ports from cluster1 in a comma separated string:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1 as a string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | community.general.json_query('domain.server[?cluster==`cluster1`].port') | join(', ') }}"
|
||||
|
||||
.. note:: In the example above, quoting literals using backticks avoids escaping quotes and maintains readability.
|
||||
|
||||
You can use YAML `single quote escaping <https://yaml.org/spec/current.html#id2534365>`_:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[?cluster==''cluster1''].port') }}"
|
||||
|
||||
.. note:: Escaping single quotes within single quotes in YAML is done by doubling the single quote.
|
||||
|
||||
To get a hash map with all ports and names of a cluster:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server ports and names from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"
|
||||
|
||||
To extract ports from all clusters with name starting with 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?starts_with(name,'server1')].port"
|
||||
|
||||
To extract ports from all clusters with name containing 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?contains(name,'server1')].port"
|
||||
|
||||
.. note:: while using ``starts_with`` and ``contains``, you have to use `` to_json | from_json `` filter for correct parsing of data structure.
|
||||
|
||||
Working with Unicode
|
||||
---------------------
|
||||
|
||||
`Unicode <https://unicode.org/main.html>`_ makes it possible to produce two strings which may be visually equivalent, but are comprised of distinctly different characters/character sequences. To address this ``Unicode`` defines `normalization forms <https://unicode.org/reports/tr15/>`_ which avoid these distinctions by choosing a unique character sequence for a given visual representation.
|
||||
|
||||
You can use the ``community.general.unicode_normalize`` filter to normalize ``Unicode`` strings within your playbooks.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Compare Unicode representations
|
||||
debug:
|
||||
msg: "{{ with_combining_character | community.general.unicode_normalize == without_combining_character }}"
|
||||
vars:
|
||||
with_combining_character: "{{ 'Mayagu\u0308ez' }}"
|
||||
without_combining_character: Mayagüez
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Compare Unicode representations] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": true
|
||||
}
|
||||
|
||||
The ``community.general.unicode_normalize`` filter accepts a keyword argument to select the ``Unicode`` form used to normalize the input string.
|
||||
|
||||
:form: One of ``'NFC'`` (default), ``'NFD'``, ``'NFKC'``, or ``'NFKD'``. See the `Unicode reference <https://unicode.org/reports/tr15/>`_ for more information.
|
||||
|
||||
.. versionadded:: 3.7.0
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
filter_guide_paths
|
||||
filter_guide_abstract_informations
|
||||
filter_guide_working_with_times
|
||||
filter_guide_working_with_versions
|
||||
filter_guide_creating_identifiers
|
||||
filter_guide_conversions
|
||||
filter_guide_selecting_json_data
|
||||
filter_guide_working_with_unicode
|
||||
|
||||
10
docs/docsite/rst/filter_guide_abstract_informations.rst
Normal file
10
docs/docsite/rst/filter_guide_abstract_informations.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Abstract transformations
|
||||
------------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
filter_guide_abstract_informations_dictionaries
|
||||
filter_guide_abstract_informations_grouping
|
||||
filter_guide_abstract_informations_merging_lists_of_dictionaries
|
||||
filter_guide_abstract_informations_counting_elements_in_sequence
|
||||
@@ -0,0 +1,77 @@
|
||||
Counting elements in a sequence
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The ``community.general.counter`` filter plugin allows you to count (hashable) elements in a sequence. Elements are returned as dictionary keys and their counts are stored as dictionary values.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Count character occurrences in a string
|
||||
debug:
|
||||
msg: "{{ 'abccbaabca' | community.general.counter }}"
|
||||
|
||||
- name: Count items in a list
|
||||
debug:
|
||||
msg: "{{ ['car', 'car', 'bike', 'plane', 'bike'] | community.general.counter }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Count character occurrences in a string] ********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"a": 4,
|
||||
"b": 3,
|
||||
"c": 3
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Count items in a list] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"bike": 2,
|
||||
"car": 2,
|
||||
"plane": 1
|
||||
}
|
||||
}
|
||||
|
||||
This plugin is useful for selecting resources based on current allocation:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks
|
||||
debug:
|
||||
msg: >-
|
||||
{{
|
||||
( disks | dict2items | map(attribute='value.adapter') | list
|
||||
| community.general.counter | dict2items
|
||||
| rejectattr('value', '>=', 4) | sort(attribute='value') | first
|
||||
).key
|
||||
}}
|
||||
vars:
|
||||
disks:
|
||||
sda:
|
||||
adapter: scsi_1
|
||||
sdb:
|
||||
adapter: scsi_1
|
||||
sdc:
|
||||
adapter: scsi_1
|
||||
sdd:
|
||||
adapter: scsi_1
|
||||
sde:
|
||||
adapter: scsi_2
|
||||
sdf:
|
||||
adapter: scsi_3
|
||||
sdg:
|
||||
adapter: scsi_3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Get ID of SCSI controller(s) with less than 4 disks attached and choose the one with the least disks]
|
||||
ok: [localhost] => {
|
||||
"msg": "scsi_2"
|
||||
}
|
||||
|
||||
.. versionadded:: 4.3.0
|
||||
@@ -0,0 +1,119 @@
|
||||
Dictionaries
|
||||
^^^^^^^^^^^^
|
||||
|
||||
You can use the ``dict_kv`` filter to create a single-entry dictionary with ``value | community.general.dict_kv(key)``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a single-entry dictionary
|
||||
debug:
|
||||
msg: "{{ myvar | community.general.dict_kv('thatsmyvar') }}"
|
||||
vars:
|
||||
myvar: myvalue
|
||||
|
||||
- name: Create a list of dictionaries where the 'server' field is taken from a list
|
||||
debug:
|
||||
msg: >-
|
||||
{{ myservers | map('community.general.dict_kv', 'server')
|
||||
| map('combine', common_config) }}
|
||||
vars:
|
||||
common_config:
|
||||
type: host
|
||||
database: all
|
||||
myservers:
|
||||
- server1
|
||||
- server2
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a single-entry dictionary] **************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"thatsmyvar": "myvalue"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries where the 'server' field is taken from a list] *******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server1",
|
||||
"type": "host"
|
||||
},
|
||||
{
|
||||
"database": "all",
|
||||
"server": "server2",
|
||||
"type": "host"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
If you need to convert a list of key-value pairs to a dictionary, you can use the ``dict`` function. Unfortunately, this function cannot be used with ``map``. For this, the ``community.general.dict`` filter can be used:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Create a dictionary with the dict function
|
||||
debug:
|
||||
msg: "{{ dict([[1, 2], ['a', 'b']]) }}"
|
||||
|
||||
- name: Create a dictionary with the community.general.dict filter
|
||||
debug:
|
||||
msg: "{{ [[1, 2], ['a', 'b']] | community.general.dict }}"
|
||||
|
||||
- name: Create a list of dictionaries with map and the community.general.dict filter
|
||||
debug:
|
||||
msg: >-
|
||||
{{ values | map('zip', ['k1', 'k2', 'k3'])
|
||||
| map('map', 'reverse')
|
||||
| map('community.general.dict') }}
|
||||
vars:
|
||||
values:
|
||||
- - foo
|
||||
- 23
|
||||
- a
|
||||
- - bar
|
||||
- 42
|
||||
- b
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a dictionary with the dict function] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a dictionary with the community.general.dict filter] ************************
|
||||
ok: [localhost] => {
|
||||
"msg": {
|
||||
"1": 2,
|
||||
"a": "b"
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Create a list of dictionaries with map and the community.general.dict filter] ******
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"k1": "foo",
|
||||
"k2": 23,
|
||||
"k3": "a"
|
||||
},
|
||||
{
|
||||
"k1": "bar",
|
||||
"k2": 42,
|
||||
"k3": "b"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
@@ -0,0 +1,98 @@
|
||||
Grouping
|
||||
^^^^^^^^
|
||||
|
||||
If you have a list of dictionaries, the Jinja2 ``groupby`` filter allows to group the list by an attribute. This results in a list of ``(grouper, list)`` namedtuples, where ``list`` contains all dictionaries where the selected attribute equals ``grouper``. If you know that for every ``grouper``, there will be a most one entry in that list, you can use the ``community.general.groupby_as_dict`` filter to convert the original list into a dictionary which maps ``grouper`` to the corresponding dictionary.
|
||||
|
||||
One example is ``ansible_facts.mounts``, which is a list of dictionaries where each has one ``device`` element to indicate the device which is mounted. Therefore, ``ansible_facts.mounts | community.general.groupby_as_dict('device')`` is a dictionary mapping a device to the mount information:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Output mount facts grouped by device name
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('device')
|
||||
|
||||
- name: Output mount facts grouped by mount point
|
||||
debug:
|
||||
var: ansible_facts.mounts | community.general.groupby_as_dict('mount')
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Output mount facts grouped by device name] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('device')": {
|
||||
"/dev/sda1": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
},
|
||||
"/dev/sda2": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "abcdef01-2345-6789-0abc-def012345678"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TASK [Output mount facts grouped by mount point] ******************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_facts.mounts | community.general.groupby_as_dict('mount')": {
|
||||
"/": {
|
||||
"block_available": 1234,
|
||||
"block_size": 4096,
|
||||
"block_total": 12345,
|
||||
"block_used": 11111,
|
||||
"device": "/dev/sda2",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 1111,
|
||||
"inode_total": 1234,
|
||||
"inode_used": 123,
|
||||
"mount": "/",
|
||||
"options": "rw,relatime",
|
||||
"size_available": 42143,
|
||||
"size_total": 543210,
|
||||
"uuid": "bdf50b7d-4859-40af-8665-c637ee7a7808"
|
||||
},
|
||||
"/boot": {
|
||||
"block_available": 2000,
|
||||
"block_size": 4096,
|
||||
"block_total": 2345,
|
||||
"block_used": 345,
|
||||
"device": "/dev/sda1",
|
||||
"fstype": "ext4",
|
||||
"inode_available": 500,
|
||||
"inode_total": 512,
|
||||
"inode_used": 12,
|
||||
"mount": "/boot",
|
||||
"options": "rw,relatime,data=ordered",
|
||||
"size_available": 56821,
|
||||
"size_total": 543210,
|
||||
"uuid": "ab31cade-d9c1-484d-8482-8a4cbee5241a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
@@ -0,0 +1,433 @@
|
||||
Merging lists of dictionaries
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have two or more lists of dictionaries and want to combine them into a list of merged dictionaries, where the dictionaries are merged by an attribute, you can use the ``lists_mergeby`` filter.
|
||||
|
||||
.. note:: The output of the examples in this section use the YAML callback plugin. Quoting: "Ansible output that can be quite a bit easier to read than the default JSON formatting." See :ref:`the documentation for the community.general.yaml callback plugin <ansible_collections.community.general.yaml_callback>`.
|
||||
|
||||
In the example below the lists are merged by the attribute ``name``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
set_fact:
|
||||
list3: "{{ list1|
|
||||
community.general.lists_mergeby(list2, 'name') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
|
||||
.. versionadded:: 2.0.0
|
||||
|
||||
It is possible to use a list of lists as an input of the filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge two lists by common attribute 'name'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: foo
|
||||
extra: true
|
||||
- name: bar
|
||||
extra: false
|
||||
- name: meh
|
||||
extra: true
|
||||
list2:
|
||||
- name: foo
|
||||
path: /foo
|
||||
- name: baz
|
||||
path: /baz
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces the same result as in the previous example:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- extra: false
|
||||
name: bar
|
||||
- name: baz
|
||||
path: /baz
|
||||
- extra: true
|
||||
name: foo
|
||||
path: /foo
|
||||
- extra: true
|
||||
name: meh
|
||||
|
||||
The filter also accepts two optional parameters: ``recursive`` and ``list_merge``. These parameters are only supported when used with ansible-base 2.10 or ansible-core, but not with Ansible 2.9. This is available since community.general 4.4.0.
|
||||
|
||||
**recursive**
|
||||
Is a boolean, default to ``False``. Should the ``community.general.lists_mergeby`` recursively merge nested hashes. Note: It does not depend on the value of the ``hash_behaviour`` setting in ``ansible.cfg``.
|
||||
|
||||
**list_merge**
|
||||
Is a string, its possible values are ``replace`` (default), ``keep``, ``append``, ``prepend``, ``append_rp`` or ``prepend_rp``. It modifies the behaviour of ``community.general.lists_mergeby`` when the hashes to merge contain arrays/lists.
|
||||
|
||||
The examples below set ``recursive=true`` and display the differences among all six options of ``list_merge``. Functionality of the parameters is exactly the same as in the filter ``combine``. See :ref:`Combining hashes/dictionaries <combine_filter>` to learn details about these options.
|
||||
|
||||
Example ``list_merge=replace`` (default):
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', replace lists (default)
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true) }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=keep``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', keep lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='keep') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
Example ``list_merge=append``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', append lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=prepend``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', prepend lists
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
||||
Example ``list_merge=append_rp``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', append lists 'remove present'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='append_rp') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- default_value
|
||||
- patch_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
|
||||
Example ``list_merge=prepend_rp``:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
---
|
||||
- name: Merge recursive by 'name', prepend lists 'remove present'
|
||||
set_fact:
|
||||
list3: "{{ [list1, list2]|
|
||||
community.general.lists_mergeby('name',
|
||||
recursive=true,
|
||||
list_merge='prepend_rp') }}"
|
||||
vars:
|
||||
list1:
|
||||
- name: myname01
|
||||
param01:
|
||||
x: default_value
|
||||
y: default_value
|
||||
list:
|
||||
- default_value
|
||||
- name: myname02
|
||||
param01: [1, 1, 2, 3]
|
||||
|
||||
list2:
|
||||
- name: myname01
|
||||
param01:
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
list:
|
||||
- patch_value
|
||||
- name: myname02
|
||||
param01: [3, 4, 4, {key: value}]
|
||||
- debug:
|
||||
var: list3
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
list3:
|
||||
- name: myname01
|
||||
param01:
|
||||
list:
|
||||
- patch_value
|
||||
- default_value
|
||||
x: default_value
|
||||
y: patch_value
|
||||
z: patch_value
|
||||
- name: myname02
|
||||
param01:
|
||||
- 3
|
||||
- 4
|
||||
- 4
|
||||
- key: value
|
||||
- 1
|
||||
- 1
|
||||
- 2
|
||||
108
docs/docsite/rst/filter_guide_conversions.rst
Normal file
108
docs/docsite/rst/filter_guide_conversions.rst
Normal file
@@ -0,0 +1,108 @@
|
||||
Conversions
|
||||
-----------
|
||||
|
||||
Parsing CSV files
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
Ansible offers the :ref:`community.general.read_csv module <ansible_collections.community.general.read_csv_module>` to read CSV files. Sometimes you need to convert strings to CSV files instead. For this, the ``from_csv`` filter exists.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Parse CSV from string"
|
||||
debug:
|
||||
msg: "{{ csv_string | community.general.from_csv }}"
|
||||
vars:
|
||||
csv_string: |
|
||||
foo,bar,baz
|
||||
1,2,3
|
||||
you,this,then
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Parse CSV from string] **************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"bar": "2",
|
||||
"baz": "3",
|
||||
"foo": "1"
|
||||
},
|
||||
{
|
||||
"bar": "this",
|
||||
"baz": "then",
|
||||
"foo": "you"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
The ``from_csv`` filter has several keyword arguments to control its behavior:
|
||||
|
||||
:dialect: Dialect of the CSV file. Default is ``excel``. Other possible choices are ``excel-tab`` and ``unix``. If one of ``delimiter``, ``skipinitialspace`` or ``strict`` is specified, ``dialect`` is ignored.
|
||||
:fieldnames: A set of column names to use. If not provided, the first line of the CSV is assumed to contain the column names.
|
||||
:delimiter: Sets the delimiter to use. Default depends on the dialect used.
|
||||
:skipinitialspace: Set to ``true`` to ignore space directly after the delimiter. Default depends on the dialect used (usually ``false``).
|
||||
:strict: Set to ``true`` to error out on invalid CSV input.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Converting to JSON
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
`JC <https://pypi.org/project/jc/>`_ is a CLI tool and Python library which allows to interpret output of various CLI programs as JSON. It is also available as a filter in community.general. This filter needs the `jc Python library <https://pypi.org/project/jc/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Run 'ls' to list files in /
|
||||
command: ls /
|
||||
register: result
|
||||
|
||||
- name: Parse the ls output
|
||||
debug:
|
||||
msg: "{{ result.stdout | community.general.jc('ls') }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Run 'ls' to list files in /] ********************************************************
|
||||
changed: [localhost]
|
||||
|
||||
TASK [Parse the ls output] ****************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
{
|
||||
"filename": "bin"
|
||||
},
|
||||
{
|
||||
"filename": "boot"
|
||||
},
|
||||
{
|
||||
"filename": "dev"
|
||||
},
|
||||
{
|
||||
"filename": "etc"
|
||||
},
|
||||
{
|
||||
"filename": "home"
|
||||
},
|
||||
{
|
||||
"filename": "lib"
|
||||
},
|
||||
{
|
||||
"filename": "proc"
|
||||
},
|
||||
{
|
||||
"filename": "root"
|
||||
},
|
||||
{
|
||||
"filename": "run"
|
||||
},
|
||||
{
|
||||
"filename": "tmp"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.0.0
|
||||
80
docs/docsite/rst/filter_guide_creating_identifiers.rst
Normal file
80
docs/docsite/rst/filter_guide_creating_identifiers.rst
Normal file
@@ -0,0 +1,80 @@
|
||||
Creating identifiers
|
||||
--------------------
|
||||
|
||||
The following filters allow to create identifiers.
|
||||
|
||||
Hashids
|
||||
^^^^^^^
|
||||
|
||||
`Hashids <https://hashids.org/>`_ allow to convert sequences of integers to short unique string identifiers. This filter needs the `hashids Python library <https://pypi.org/project/hashids/>`_ installed on the controller.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create hashid"
|
||||
debug:
|
||||
msg: "{{ [1234, 5, 6] | community.general.hashids_encode }}"
|
||||
|
||||
- name: "Decode hashid"
|
||||
debug:
|
||||
msg: "{{ 'jm2Cytn' | community.general.hashids_decode }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "jm2Cytn"
|
||||
}
|
||||
|
||||
TASK [Decode hashid] **********************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": [
|
||||
1234,
|
||||
5,
|
||||
6
|
||||
]
|
||||
}
|
||||
|
||||
The hashids filters accept keyword arguments to allow fine-tuning the hashids generated:
|
||||
|
||||
:salt: String to use as salt when hashing.
|
||||
:alphabet: String of 16 or more unique characters to produce a hash.
|
||||
:min_length: Minimum length of hash produced.
|
||||
|
||||
.. versionadded: 3.0.0
|
||||
|
||||
Random MACs
|
||||
^^^^^^^^^^^
|
||||
|
||||
You can use the ``random_mac`` filter to complete a partial `MAC address <https://en.wikipedia.org/wiki/MAC_address>`_ to a random 6-byte MAC address.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: "Create a random MAC starting with ff:"
|
||||
debug:
|
||||
msg: "{{ 'FF' | community.general.random_mac }}"
|
||||
|
||||
- name: "Create a random MAC starting with 00:11:22:"
|
||||
debug:
|
||||
msg: "{{ '00:11:22' | community.general.random_mac }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Create a random MAC starting with ff:] **********************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "ff:69:d3:78:7f:b4"
|
||||
}
|
||||
|
||||
TASK [Create a random MAC starting with 00:11:22:] ****************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "00:11:22:71:5d:3b"
|
||||
}
|
||||
|
||||
You can also initialize the random number generator from a seed to create random-but-idempotent MAC addresses:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
"{{ '52:54:00' | community.general.random_mac(seed=inventory_hostname) }}"
|
||||
14
docs/docsite/rst/filter_guide_paths.rst
Normal file
14
docs/docsite/rst/filter_guide_paths.rst
Normal file
@@ -0,0 +1,14 @@
|
||||
Paths
|
||||
-----
|
||||
|
||||
The ``path_join`` filter has been added in ansible-base 2.10. If you want to use this filter, but also need to support Ansible 2.9, you can use ``community.general``'s ``path_join`` shim, ``community.general.path_join``. This filter redirects to ``path_join`` for ansible-base 2.10 and ansible-core 2.11 or newer, and re-implements the filter for Ansible 2.9.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
# ansible-base 2.10 or newer:
|
||||
path: {{ ('/etc', path, 'subdir', file) | path_join }}
|
||||
|
||||
# Also works with Ansible 2.9:
|
||||
path: {{ ('/etc', path, 'subdir', file) | community.general.path_join }}
|
||||
|
||||
.. versionadded:: 3.0.0
|
||||
144
docs/docsite/rst/filter_guide_selecting_json_data.rst
Normal file
144
docs/docsite/rst/filter_guide_selecting_json_data.rst
Normal file
@@ -0,0 +1,144 @@
|
||||
.. _ansible_collections.community.general.docsite.json_query_filter:
|
||||
|
||||
Selecting JSON data: JSON queries
|
||||
---------------------------------
|
||||
|
||||
To select a single element or a data subset from a complex data structure in JSON format (for example, Ansible facts), use the ``json_query`` filter. The ``json_query`` filter lets you query a complex JSON structure and iterate over it using a loop structure.
|
||||
|
||||
.. note:: You must manually install the **jmespath** dependency on the Ansible controller before using this filter. This filter is built upon **jmespath**, and you can use the same syntax. For examples, see `jmespath examples <http://jmespath.org/examples.html>`_.
|
||||
|
||||
Consider this data structure:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
{
|
||||
"domain_definition": {
|
||||
"domain": {
|
||||
"cluster": [
|
||||
{
|
||||
"name": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "cluster2"
|
||||
}
|
||||
],
|
||||
"server": [
|
||||
{
|
||||
"name": "server11",
|
||||
"cluster": "cluster1",
|
||||
"port": "8080"
|
||||
},
|
||||
{
|
||||
"name": "server12",
|
||||
"cluster": "cluster1",
|
||||
"port": "8090"
|
||||
},
|
||||
{
|
||||
"name": "server21",
|
||||
"cluster": "cluster2",
|
||||
"port": "9080"
|
||||
},
|
||||
{
|
||||
"name": "server22",
|
||||
"cluster": "cluster2",
|
||||
"port": "9090"
|
||||
}
|
||||
],
|
||||
"library": [
|
||||
{
|
||||
"name": "lib1",
|
||||
"target": "cluster1"
|
||||
},
|
||||
{
|
||||
"name": "lib2",
|
||||
"target": "cluster2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
To extract all clusters from this structure, you can use the following query:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all cluster names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.cluster[*].name') }}"
|
||||
|
||||
To extract all server names:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server names
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[*].name') }}"
|
||||
|
||||
To extract ports from cluster1:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"
|
||||
|
||||
.. note:: You can use a variable to make the query more readable.
|
||||
|
||||
To print out the ports from cluster1 in a comma separated string:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1 as a string
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | community.general.json_query('domain.server[?cluster==`cluster1`].port') | join(', ') }}"
|
||||
|
||||
.. note:: In the example above, quoting literals using backticks avoids escaping quotes and maintains readability.
|
||||
|
||||
You can use YAML `single quote escaping <https://yaml.org/spec/current.html#id2534365>`_:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query('domain.server[?cluster==''cluster1''].port') }}"
|
||||
|
||||
.. note:: Escaping single quotes within single quotes in YAML is done by doubling the single quote.
|
||||
|
||||
To get a hash map with all ports and names of a cluster:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all server ports and names from cluster1
|
||||
ansible.builtin.debug:
|
||||
var: item
|
||||
loop: "{{ domain_definition | community.general.json_query(server_name_cluster1_query) }}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"
|
||||
|
||||
To extract ports from all clusters with name starting with 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?starts_with(name,'server1')].port"
|
||||
|
||||
To extract ports from all clusters with name containing 'server1':
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Display all ports from cluster1
|
||||
ansible.builtin.debug:
|
||||
msg: "{{ domain_definition | to_json | from_json | community.general.json_query(server_name_query) }}"
|
||||
vars:
|
||||
server_name_query: "domain.server[?contains(name,'server1')].port"
|
||||
|
||||
.. note:: while using ``starts_with`` and ``contains``, you have to use `` to_json | from_json `` filter for correct parsing of data structure.
|
||||
84
docs/docsite/rst/filter_guide_working_with_times.rst
Normal file
84
docs/docsite/rst/filter_guide_working_with_times.rst
Normal file
@@ -0,0 +1,84 @@
|
||||
Working with times
|
||||
------------------
|
||||
|
||||
The ``to_time_unit`` filter allows to convert times from a human-readable string to a unit. For example, ``'4h 30min 12second' | community.general.to_time_unit('hour')`` gives the number of hours that correspond to 4 hours, 30 minutes and 12 seconds.
|
||||
|
||||
There are shorthands to directly convert to various units, like ``to_hours``, ``to_minutes``, ``to_seconds``, and so on. The following table lists all units that can be used:
|
||||
|
||||
.. list-table:: Units
|
||||
:widths: 25 25 25 25
|
||||
:header-rows: 1
|
||||
|
||||
* - Unit name
|
||||
- Unit value in seconds
|
||||
- Unit strings for filter
|
||||
- Shorthand filter
|
||||
* - Millisecond
|
||||
- 1/1000 second
|
||||
- ``ms``, ``millisecond``, ``milliseconds``, ``msec``, ``msecs``, ``msecond``, ``mseconds``
|
||||
- ``to_milliseconds``
|
||||
* - Second
|
||||
- 1 second
|
||||
- ``s``, ``sec``, ``secs``, ``second``, ``seconds``
|
||||
- ``to_seconds``
|
||||
* - Minute
|
||||
- 60 seconds
|
||||
- ``m``, ``min``, ``mins``, ``minute``, ``minutes``
|
||||
- ``to_minutes``
|
||||
* - Hour
|
||||
- 60*60 seconds
|
||||
- ``h``, ``hour``, ``hours``
|
||||
- ``to_hours``
|
||||
* - Day
|
||||
- 24*60*60 seconds
|
||||
- ``d``, ``day``, ``days``
|
||||
- ``to_days``
|
||||
* - Week
|
||||
- 7*24*60*60 seconds
|
||||
- ``w``, ``week``, ``weeks``
|
||||
- ``to_weeks``
|
||||
* - Month
|
||||
- 30*24*60*60 seconds
|
||||
- ``mo``, ``month``, ``months``
|
||||
- ``to_months``
|
||||
* - Year
|
||||
- 365*24*60*60 seconds
|
||||
- ``y``, ``year``, ``years``
|
||||
- ``to_years``
|
||||
|
||||
Note that months and years are using a simplified representation: a month is 30 days, and a year is 365 days. If you need different definitions of months or years, you can pass them as keyword arguments. For example, if you want a year to be 365.25 days, and a month to be 30.5 days, you can write ``'11months 4' | community.general.to_years(year=365.25, month=30.5)``. These keyword arguments can be specified to ``to_time_unit`` and to all shorthand filters.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Convert string to seconds
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_time_unit('seconds') }}"
|
||||
|
||||
- name: Convert string to hours
|
||||
debug:
|
||||
msg: "{{ '30h 20m 10s 123ms' | community.general.to_hours }}"
|
||||
|
||||
- name: Convert string to years (using 365.25 days == 1 year)
|
||||
debug:
|
||||
msg: "{{ '400d 15h' | community.general.to_years(year=365.25) }}"
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Convert string to seconds] **********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "109210.123"
|
||||
}
|
||||
|
||||
TASK [Convert string to hours] ************************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": "30.336145277778"
|
||||
}
|
||||
|
||||
TASK [Convert string to years (using 365.25 days == 1 year)] ******************************
|
||||
ok: [localhost] => {
|
||||
"msg": "1.096851471595"
|
||||
}
|
||||
|
||||
.. versionadded: 0.2.0
|
||||
30
docs/docsite/rst/filter_guide_working_with_unicode.rst
Normal file
30
docs/docsite/rst/filter_guide_working_with_unicode.rst
Normal file
@@ -0,0 +1,30 @@
|
||||
Working with Unicode
|
||||
---------------------
|
||||
|
||||
`Unicode <https://unicode.org/main.html>`_ makes it possible to produce two strings which may be visually equivalent, but are comprised of distinctly different characters/character sequences. To address this ``Unicode`` defines `normalization forms <https://unicode.org/reports/tr15/>`_ which avoid these distinctions by choosing a unique character sequence for a given visual representation.
|
||||
|
||||
You can use the ``community.general.unicode_normalize`` filter to normalize ``Unicode`` strings within your playbooks.
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Compare Unicode representations
|
||||
debug:
|
||||
msg: "{{ with_combining_character | community.general.unicode_normalize == without_combining_character }}"
|
||||
vars:
|
||||
with_combining_character: "{{ 'Mayagu\u0308ez' }}"
|
||||
without_combining_character: Mayagüez
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Compare Unicode representations] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"msg": true
|
||||
}
|
||||
|
||||
The ``community.general.unicode_normalize`` filter accepts a keyword argument to select the ``Unicode`` form used to normalize the input string.
|
||||
|
||||
:form: One of ``'NFC'`` (default), ``'NFD'``, ``'NFKC'``, or ``'NFKD'``. See the `Unicode reference <https://unicode.org/reports/tr15/>`_ for more information.
|
||||
|
||||
.. versionadded:: 3.7.0
|
||||
34
docs/docsite/rst/filter_guide_working_with_versions.rst
Normal file
34
docs/docsite/rst/filter_guide_working_with_versions.rst
Normal file
@@ -0,0 +1,34 @@
|
||||
Working with versions
|
||||
---------------------
|
||||
|
||||
If you need to sort a list of version numbers, the Jinja ``sort`` filter is problematic. Since it sorts lexicographically, ``2.10`` will come before ``2.9``. To treat version numbers correctly, you can use the ``version_sort`` filter:
|
||||
|
||||
.. code-block:: yaml+jinja
|
||||
|
||||
- name: Sort list by version number
|
||||
debug:
|
||||
var: ansible_versions | community.general.version_sort
|
||||
vars:
|
||||
ansible_versions:
|
||||
- '2.8.0'
|
||||
- '2.11.0'
|
||||
- '2.7.0'
|
||||
- '2.10.0'
|
||||
- '2.9.0'
|
||||
|
||||
This produces:
|
||||
|
||||
.. code-block:: ansible-output
|
||||
|
||||
TASK [Sort list by version number] ********************************************************
|
||||
ok: [localhost] => {
|
||||
"ansible_versions | community.general.version_sort": [
|
||||
"2.7.0",
|
||||
"2.8.0",
|
||||
"2.9.0",
|
||||
"2.10.0",
|
||||
"2.11.0"
|
||||
]
|
||||
}
|
||||
|
||||
.. versionadded: 2.2.0
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace: community
|
||||
name: general
|
||||
version: 4.0.0
|
||||
version: 4.4.0
|
||||
readme: README.md
|
||||
authors:
|
||||
- Ansible (https://github.com/ansible)
|
||||
|
||||
@@ -45,6 +45,8 @@ class CallbackModule(CallbackBase):
|
||||
_task_total = 0
|
||||
_host_counter = 1
|
||||
_host_total = 0
|
||||
_current_batch_total = 0
|
||||
_previous_batch_total = 0
|
||||
|
||||
def __init__(self):
|
||||
super(CallbackModule, self).__init__()
|
||||
@@ -76,8 +78,11 @@ class CallbackModule(CallbackBase):
|
||||
self._display.banner(msg)
|
||||
self._play = play
|
||||
|
||||
self._previous_batch_total = self._current_batch_total
|
||||
self._current_batch_total = self._previous_batch_total + len(self._all_vars()['vars']['ansible_play_batch'])
|
||||
self._host_total = len(self._all_vars()['vars']['ansible_play_hosts_all'])
|
||||
self._task_total = len(self._play.get_tasks()[0])
|
||||
self._task_counter = 1
|
||||
|
||||
def v2_playbook_on_stats(self, stats):
|
||||
self._display.banner("PLAY RECAP")
|
||||
@@ -145,7 +150,7 @@ class CallbackModule(CallbackBase):
|
||||
path = task.get_path()
|
||||
if path:
|
||||
self._display.display("task path: %s" % path, color=C.COLOR_DEBUG)
|
||||
self._host_counter = 0
|
||||
self._host_counter = self._previous_batch_total
|
||||
self._task_counter += 1
|
||||
|
||||
def v2_runner_on_ok(self, result):
|
||||
|
||||
@@ -11,14 +11,16 @@ name: mail
|
||||
type: notification
|
||||
short_description: Sends failure events via email
|
||||
description:
|
||||
- This callback will report failures via email
|
||||
- This callback will report failures via email.
|
||||
author:
|
||||
- Dag Wieers (@dagwieers)
|
||||
requirements:
|
||||
- whitelisting in configuration
|
||||
options:
|
||||
mta:
|
||||
description: Mail Transfer Agent, server that accepts SMTP
|
||||
description:
|
||||
- Mail Transfer Agent, server that accepts SMTP.
|
||||
type: str
|
||||
env:
|
||||
- name: SMTPHOST
|
||||
ini:
|
||||
@@ -26,39 +28,53 @@ options:
|
||||
key: smtphost
|
||||
default: localhost
|
||||
mtaport:
|
||||
description: Mail Transfer Agent Port, port at which server SMTP
|
||||
description:
|
||||
- Mail Transfer Agent Port.
|
||||
- Port at which server SMTP.
|
||||
type: int
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: smtpport
|
||||
default: 25
|
||||
to:
|
||||
description: Mail recipient
|
||||
description:
|
||||
- Mail recipient.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: to
|
||||
default: root
|
||||
default: [root]
|
||||
sender:
|
||||
description: Mail sender
|
||||
description:
|
||||
- Mail sender.
|
||||
- Note that this will be required from community.general 6.0.0 on.
|
||||
type: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: sender
|
||||
cc:
|
||||
description: CC'd recipient
|
||||
description:
|
||||
- CC'd recipients.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: cc
|
||||
bcc:
|
||||
description: BCC'd recipient
|
||||
description:
|
||||
- BCC'd recipients.
|
||||
type: list
|
||||
elements: str
|
||||
ini:
|
||||
- section: callback_mail
|
||||
key: bcc
|
||||
notes:
|
||||
- "TODO: expand configuration options now that plugins can leverage Ansible's configuration"
|
||||
'''
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import email.utils
|
||||
import smtplib
|
||||
|
||||
from ansible.module_utils.six import string_types
|
||||
@@ -88,9 +104,13 @@ class CallbackModule(CallbackBase):
|
||||
super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct)
|
||||
|
||||
self.sender = self.get_option('sender')
|
||||
if self.sender is None:
|
||||
self._display.deprecated(
|
||||
'The sender for the mail callback has not been specified. This will be an error in the future',
|
||||
version='6.0.0', collection_name='community.general')
|
||||
self.to = self.get_option('to')
|
||||
self.smtphost = self.get_option('mta')
|
||||
self.smtpport = int(self.get_option('mtaport'))
|
||||
self.smtpport = self.get_option('mtaport')
|
||||
self.cc = self.get_option('cc')
|
||||
self.bcc = self.get_option('bcc')
|
||||
|
||||
@@ -100,28 +120,27 @@ class CallbackModule(CallbackBase):
|
||||
|
||||
smtp = smtplib.SMTP(self.smtphost, port=self.smtpport)
|
||||
|
||||
b_sender = to_bytes(self.sender)
|
||||
b_to = to_bytes(self.to)
|
||||
b_cc = to_bytes(self.cc)
|
||||
b_bcc = to_bytes(self.bcc)
|
||||
b_subject = to_bytes(subject)
|
||||
b_body = to_bytes(body)
|
||||
|
||||
b_content = b'From: %s\n' % b_sender
|
||||
b_content += b'To: %s\n' % b_to
|
||||
content = 'Date: %s\n' % email.utils.formatdate()
|
||||
content += 'From: %s\n' % self.sender
|
||||
if self.to:
|
||||
content += 'To: %s\n' % ','.join(self.to)
|
||||
if self.cc:
|
||||
b_content += b'Cc: %s\n' % b_cc
|
||||
b_content += b'Subject: %s\n\n' % b_subject
|
||||
b_content += b_body
|
||||
content += 'Cc: %s\n' % ','.join(self.cc)
|
||||
content += 'Message-ID: %s\n' % email.utils.make_msgid()
|
||||
content += 'Subject: %s\n\n' % subject.strip()
|
||||
content += body
|
||||
|
||||
b_addresses = b_to.split(b',')
|
||||
addresses = self.to
|
||||
if self.cc:
|
||||
b_addresses += b_cc.split(b',')
|
||||
addresses += self.cc
|
||||
if self.bcc:
|
||||
b_addresses += b_bcc.split(b',')
|
||||
addresses += self.bcc
|
||||
|
||||
for b_address in b_addresses:
|
||||
smtp.sendmail(b_sender, b_address, b_content)
|
||||
if not addresses:
|
||||
self._display.warning('No receiver has been specified for the mail callback plugin.')
|
||||
|
||||
for address in addresses:
|
||||
smtp.sendmail(self.sender, address, to_bytes(content))
|
||||
|
||||
smtp.quit()
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ import os
|
||||
import json
|
||||
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
@@ -143,7 +144,7 @@ class CallbackModule(CallbackBase):
|
||||
body = {
|
||||
'cmd': 'submitcheck',
|
||||
'token': self.token,
|
||||
'XMLDATA': bytes(xmldata)
|
||||
'XMLDATA': to_bytes(xmldata)
|
||||
}
|
||||
|
||||
try:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# (C) 2021, Victor Martinez <VictorMartinezRubio@gmail.com>
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
@@ -267,6 +268,8 @@ class OpenTelemetrySource(object):
|
||||
elif host_data.status == 'skipped':
|
||||
message = res['skip_reason'] if 'skip_reason' in res else 'skipped'
|
||||
status = Status(status_code=StatusCode.UNSET)
|
||||
elif host_data.status == 'ignored':
|
||||
status = Status(status_code=StatusCode.UNSET)
|
||||
|
||||
span.set_status(status)
|
||||
if isinstance(task_data.args, dict) and "gather_facts" not in task_data.action:
|
||||
@@ -316,9 +319,9 @@ class OpenTelemetrySource(object):
|
||||
@staticmethod
|
||||
def url_from_args(args):
|
||||
# the order matters
|
||||
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url")
|
||||
url_args = ("url", "api_url", "baseurl", "repo", "server_url", "chart_repo_url", "registry_url")
|
||||
for arg in url_args:
|
||||
if args.get(arg):
|
||||
if args is not None and args.get(arg):
|
||||
return args.get(arg)
|
||||
return ""
|
||||
|
||||
@@ -462,10 +465,15 @@ class CallbackModule(CallbackBase):
|
||||
)
|
||||
|
||||
def v2_runner_on_failed(self, result, ignore_errors=False):
|
||||
self.errors += 1
|
||||
if ignore_errors:
|
||||
status = 'ignored'
|
||||
else:
|
||||
status = 'failed'
|
||||
self.errors += 1
|
||||
|
||||
self.opentelemetry.finish_task(
|
||||
self.tasks_data,
|
||||
'failed',
|
||||
status,
|
||||
result
|
||||
)
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ DOCUMENTATION = '''
|
||||
- In 2.8, this callback has been renamed from C(osx_say) into M(community.general.say).
|
||||
'''
|
||||
|
||||
import distutils.spawn
|
||||
import platform
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.plugins.callback import CallbackBase
|
||||
|
||||
|
||||
@@ -47,21 +47,24 @@ class CallbackModule(CallbackBase):
|
||||
self.HAPPY_VOICE = None
|
||||
self.LASER_VOICE = None
|
||||
|
||||
self.synthesizer = distutils.spawn.find_executable('say')
|
||||
if not self.synthesizer:
|
||||
self.synthesizer = distutils.spawn.find_executable('espeak')
|
||||
if self.synthesizer:
|
||||
try:
|
||||
self.synthesizer = get_bin_path('say')
|
||||
if platform.system() != 'Darwin':
|
||||
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
|
||||
self._display.warning("'say' executable found but system is '%s': ignoring voice parameter" % platform.system())
|
||||
else:
|
||||
self.FAILED_VOICE = 'Zarvox'
|
||||
self.REGULAR_VOICE = 'Trinoids'
|
||||
self.HAPPY_VOICE = 'Cellos'
|
||||
self.LASER_VOICE = 'Princess'
|
||||
except ValueError:
|
||||
try:
|
||||
self.synthesizer = get_bin_path('espeak')
|
||||
self.FAILED_VOICE = 'klatt'
|
||||
self.HAPPY_VOICE = 'f5'
|
||||
self.LASER_VOICE = 'whisper'
|
||||
elif platform.system() != 'Darwin':
|
||||
# 'say' binary available, it might be GNUstep tool which doesn't support 'voice' parameter
|
||||
self._display.warning("'say' executable found but system is '%s': ignoring voice parameter" % platform.system())
|
||||
else:
|
||||
self.FAILED_VOICE = 'Zarvox'
|
||||
self.REGULAR_VOICE = 'Trinoids'
|
||||
self.HAPPY_VOICE = 'Cellos'
|
||||
self.LASER_VOICE = 'Princess'
|
||||
except ValueError:
|
||||
self.synthesizer = None
|
||||
|
||||
# plugin disable itself if say is not present
|
||||
# ansible will not call any callback if disabled is set to True
|
||||
|
||||
@@ -31,7 +31,6 @@ DOCUMENTATION = '''
|
||||
- name: ansible_jail_user
|
||||
'''
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import os.path
|
||||
import subprocess
|
||||
@@ -39,6 +38,7 @@ import traceback
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.six.moves import shlex_quote
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.plugins.connection import ConnectionBase, BUFSIZE
|
||||
from ansible.utils.display import Display
|
||||
@@ -75,10 +75,10 @@ class Connection(ConnectionBase):
|
||||
|
||||
@staticmethod
|
||||
def _search_executable(executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
try:
|
||||
return get_bin_path(executable)
|
||||
except ValueError:
|
||||
raise AnsibleError("%s command not found in PATH" % executable)
|
||||
return cmd
|
||||
|
||||
def list_jails(self):
|
||||
p = subprocess.Popen([self.jls_cmd, '-q', 'name'],
|
||||
|
||||
@@ -43,10 +43,10 @@ DOCUMENTATION = '''
|
||||
'''
|
||||
|
||||
import os
|
||||
from distutils.spawn import find_executable
|
||||
from subprocess import Popen, PIPE
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_text
|
||||
from ansible.plugins.connection import ConnectionBase
|
||||
|
||||
@@ -62,9 +62,9 @@ class Connection(ConnectionBase):
|
||||
super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs)
|
||||
|
||||
self._host = self._play_context.remote_addr
|
||||
self._lxc_cmd = find_executable("lxc")
|
||||
|
||||
if not self._lxc_cmd:
|
||||
try:
|
||||
self._lxc_cmd = get_bin_path("lxc")
|
||||
except ValueError:
|
||||
raise AnsibleError("lxc command not found in PATH")
|
||||
|
||||
if self._play_context.remote_user is not None and self._play_context.remote_user != 'root':
|
||||
@@ -89,9 +89,9 @@ class Connection(ConnectionBase):
|
||||
local_cmd.extend(["--project", self.get_option("project")])
|
||||
local_cmd.extend([
|
||||
"exec",
|
||||
"%s:%s" % (self.get_option("remote"), self._host),
|
||||
"%s:%s" % (self.get_option("remote"), self.get_option("remote_addr")),
|
||||
"--",
|
||||
self._play_context.executable, "-c", cmd
|
||||
self.get_option("executable"), "-c", cmd
|
||||
])
|
||||
|
||||
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||
@@ -126,7 +126,7 @@ class Connection(ConnectionBase):
|
||||
local_cmd.extend([
|
||||
"file", "push",
|
||||
in_path,
|
||||
"%s:%s/%s" % (self.get_option("remote"), self._host, out_path)
|
||||
"%s:%s/%s" % (self.get_option("remote"), self.get_option("remote_addr"), out_path)
|
||||
])
|
||||
|
||||
local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
|
||||
@@ -145,7 +145,7 @@ class Connection(ConnectionBase):
|
||||
local_cmd.extend(["--project", self.get_option("project")])
|
||||
local_cmd.extend([
|
||||
"file", "pull",
|
||||
"%s:%s/%s" % (self.get_option("remote"), self._host, in_path),
|
||||
"%s:%s/%s" % (self.get_option("remote"), self.get_option("remote_addr"), in_path),
|
||||
out_path
|
||||
])
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ DOCUMENTATION = '''
|
||||
- name: ansible_zone_host
|
||||
'''
|
||||
|
||||
import distutils.spawn
|
||||
import os
|
||||
import os.path
|
||||
import subprocess
|
||||
@@ -34,6 +33,7 @@ import traceback
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.six.moves import shlex_quote
|
||||
from ansible.module_utils.common.process import get_bin_path
|
||||
from ansible.module_utils.common.text.converters import to_bytes
|
||||
from ansible.plugins.connection import ConnectionBase, BUFSIZE
|
||||
from ansible.utils.display import Display
|
||||
@@ -64,10 +64,10 @@ class Connection(ConnectionBase):
|
||||
|
||||
@staticmethod
|
||||
def _search_executable(executable):
|
||||
cmd = distutils.spawn.find_executable(executable)
|
||||
if not cmd:
|
||||
try:
|
||||
return get_bin_path(executable)
|
||||
except ValueError:
|
||||
raise AnsibleError("%s command not found in PATH" % executable)
|
||||
return cmd
|
||||
|
||||
def list_zones(self):
|
||||
process = subprocess.Popen([self.zoneadm_cmd, 'list', '-ip'],
|
||||
|
||||
31
plugins/doc_fragments/gitlab.py
Normal file
31
plugins/doc_fragments/gitlab.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class ModuleDocFragment(object):
|
||||
|
||||
# Standard files documentation fragment
|
||||
DOCUMENTATION = r'''
|
||||
requirements:
|
||||
- requests (Python library U(https://pypi.org/project/requests/))
|
||||
|
||||
options:
|
||||
api_token:
|
||||
description:
|
||||
- GitLab access token with API permissions.
|
||||
type: str
|
||||
api_oauth_token:
|
||||
description:
|
||||
- GitLab OAuth token for logging in.
|
||||
type: str
|
||||
version_added: 4.2.0
|
||||
api_job_token:
|
||||
description:
|
||||
- GitLab CI job token for logging in.
|
||||
type: str
|
||||
version_added: 4.2.0
|
||||
'''
|
||||
36
plugins/filter/counter.py
Normal file
36
plugins/filter/counter.py
Normal file
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Remy Keil <remy.keil@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
|
||||
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from ansible.module_utils.common._collections_compat import Sequence
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def counter(sequence):
|
||||
''' Count elements in a sequence. Returns dict with count result. '''
|
||||
if not isinstance(sequence, Sequence):
|
||||
raise AnsibleFilterError('Argument for community.general.counter must be a sequence (string or list). %s is %s' %
|
||||
(sequence, type(sequence)))
|
||||
|
||||
try:
|
||||
result = dict(Counter(sequence))
|
||||
except TypeError as e:
|
||||
raise AnsibleFilterError(
|
||||
"community.general.counter needs a sequence with hashable elements (int, float or str) - %s" % (e)
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
''' Ansible counter jinja2 filters '''
|
||||
|
||||
def filters(self):
|
||||
filters = {
|
||||
'counter': counter,
|
||||
}
|
||||
|
||||
return filters
|
||||
@@ -1,43 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Vladimir Botka <vbotka@gmail.com>
|
||||
# Copyright (c) 2020-2022, Vladimir Botka <vbotka@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
|
||||
|
||||
from ansible.errors import AnsibleError, AnsibleFilterError
|
||||
from ansible.errors import AnsibleFilterError
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.module_utils.common._collections_compat import Mapping, Sequence
|
||||
from ansible.utils.vars import merge_hash
|
||||
from ansible.release import __version__ as ansible_version
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
from collections import defaultdict
|
||||
from operator import itemgetter
|
||||
|
||||
|
||||
def lists_mergeby(l1, l2, index):
|
||||
''' merge lists by attribute index. Example:
|
||||
- debug: msg="{{ l1|community.general.lists_mergeby(l2, 'index')|list }}" '''
|
||||
def merge_hash_wrapper(x, y, recursive=False, list_merge='replace'):
|
||||
''' Wrapper of the function merge_hash from ansible.utils.vars. Only 2 paramaters are allowed
|
||||
for Ansible 2.9 and lower.'''
|
||||
|
||||
if not isinstance(l1, Sequence):
|
||||
raise AnsibleFilterError('First argument for community.general.lists_mergeby must be list. %s is %s' %
|
||||
(l1, type(l1)))
|
||||
if LooseVersion(ansible_version) < LooseVersion('2.10'):
|
||||
if list_merge != 'replace' or recursive:
|
||||
msg = ("Non default options of list_merge(default=replace) or recursive(default=False) "
|
||||
"are not allowed in Ansible version 2.9 or lower. Ansible version is %s, "
|
||||
"recursive=%s, and list_merge=%s.")
|
||||
raise AnsibleFilterError(msg % (ansible_version, recursive, list_merge))
|
||||
else:
|
||||
return merge_hash(x, y)
|
||||
else:
|
||||
return merge_hash(x, y, recursive, list_merge)
|
||||
|
||||
if not isinstance(l2, Sequence):
|
||||
raise AnsibleFilterError('Second argument for community.general.lists_mergeby must be list. %s is %s' %
|
||||
(l2, type(l2)))
|
||||
|
||||
if not isinstance(index, string_types):
|
||||
raise AnsibleFilterError('Third argument for community.general.lists_mergeby must be string. %s is %s' %
|
||||
(index, type(index)))
|
||||
def list_mergeby(x, y, index, recursive=False, list_merge='replace'):
|
||||
''' Merge 2 lists by attribute 'index'. The function merge_hash from ansible.utils.vars is used.
|
||||
This function is used by the function lists_mergeby.
|
||||
'''
|
||||
|
||||
d = defaultdict(dict)
|
||||
for l in (l1, l2):
|
||||
for l in (x, y):
|
||||
for elem in l:
|
||||
if not isinstance(elem, Mapping):
|
||||
raise AnsibleFilterError('Elements of list arguments for lists_mergeby must be dictionaries. Found {0!r}.'.format(elem))
|
||||
msg = "Elements of list arguments for lists_mergeby must be dictionaries. %s is %s"
|
||||
raise AnsibleFilterError(msg % (elem, type(elem)))
|
||||
if index in elem.keys():
|
||||
d[elem[index]].update(elem)
|
||||
d[elem[index]].update(merge_hash_wrapper(d[elem[index]], elem, recursive, list_merge))
|
||||
return sorted(d.values(), key=itemgetter(index))
|
||||
|
||||
|
||||
def lists_mergeby(*terms, **kwargs):
|
||||
''' Merge 2 or more lists by attribute 'index'. Optional parameters 'recursive' and 'list_merge'
|
||||
control the merging of the lists in values. The function merge_hash from ansible.utils.vars
|
||||
is used. To learn details on how to use the parameters 'recursive' and 'list_merge' see
|
||||
Ansible User's Guide chapter "Using filters to manipulate data" section "Combining
|
||||
hashes/dictionaries".
|
||||
|
||||
Example:
|
||||
- debug:
|
||||
msg: "{{ list1|
|
||||
community.general.lists_mergeby(list2,
|
||||
'index',
|
||||
recursive=True,
|
||||
list_merge='append')|
|
||||
list }}"
|
||||
'''
|
||||
|
||||
recursive = kwargs.pop('recursive', False)
|
||||
list_merge = kwargs.pop('list_merge', 'replace')
|
||||
if kwargs:
|
||||
raise AnsibleFilterError("'recursive' and 'list_merge' are the only valid keyword arguments.")
|
||||
if len(terms) < 2:
|
||||
raise AnsibleFilterError("At least one list and index are needed.")
|
||||
|
||||
# allow the user to do `[list1, list2, ...] | lists_mergeby('index')`
|
||||
flat_list = []
|
||||
for sublist in terms[:-1]:
|
||||
if not isinstance(sublist, Sequence):
|
||||
msg = ("All arguments before the argument index for community.general.lists_mergeby "
|
||||
"must be lists. %s is %s")
|
||||
raise AnsibleFilterError(msg % (sublist, type(sublist)))
|
||||
if len(sublist) > 0:
|
||||
if all(isinstance(l, Sequence) for l in sublist):
|
||||
for item in sublist:
|
||||
flat_list.append(item)
|
||||
else:
|
||||
flat_list.append(sublist)
|
||||
lists = flat_list
|
||||
|
||||
if not lists:
|
||||
return []
|
||||
|
||||
if len(lists) == 1:
|
||||
return lists[0]
|
||||
|
||||
index = terms[-1]
|
||||
|
||||
if not isinstance(index, string_types):
|
||||
msg = ("First argument after the lists for community.general.lists_mergeby must be string. "
|
||||
"%s is %s")
|
||||
raise AnsibleFilterError(msg % (index, type(index)))
|
||||
|
||||
high_to_low_prio_list_iterator = reversed(lists)
|
||||
result = next(high_to_low_prio_list_iterator)
|
||||
for list in high_to_low_prio_list_iterator:
|
||||
result = list_mergeby(list, result, index, recursive, list_merge)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class FilterModule(object):
|
||||
''' Ansible list filters '''
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
|
||||
def version_sort(value, reverse=False):
|
||||
|
||||
@@ -40,10 +40,21 @@ DOCUMENTATION = '''
|
||||
type: boolean
|
||||
default: no
|
||||
exclude_profiles:
|
||||
description: Profiles to exclude from inventory
|
||||
description:
|
||||
- Profiles to exclude from inventory.
|
||||
- Ignored if I(include_profiles) is specified.
|
||||
type: list
|
||||
default: []
|
||||
elements: str
|
||||
include_profiles:
|
||||
description:
|
||||
- Profiles to include from inventory.
|
||||
- If specified, all other profiles will be excluded.
|
||||
- I(exclude_profiles) is ignored if I(include_profiles) is specified.
|
||||
type: list
|
||||
default: []
|
||||
elements: str
|
||||
version_added: 4.4.0
|
||||
group_by:
|
||||
description: Keys to group hosts by
|
||||
type: list
|
||||
@@ -68,12 +79,10 @@ user: ansible-tester
|
||||
password: secure
|
||||
'''
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
import socket
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.common._collections_compat import MutableMapping
|
||||
from ansible.module_utils.common.text.converters import to_text
|
||||
from ansible.module_utils.six import iteritems
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, to_safe_group_name
|
||||
|
||||
@@ -95,18 +104,9 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
NAME = 'community.general.cobbler'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
# from config
|
||||
self.cobbler_url = None
|
||||
self.exclude_profiles = [] # A list of profiles to exclude
|
||||
|
||||
self.connection = None
|
||||
self.token = None
|
||||
|
||||
self.cache_key = None
|
||||
self.use_cache = None
|
||||
self.connection = None
|
||||
|
||||
def verify_file(self, path):
|
||||
valid = False
|
||||
@@ -178,6 +178,12 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
self.inventory.add_child(group_name, child)
|
||||
return group_name
|
||||
|
||||
def _exclude_profile(self, profile):
|
||||
if self.include_profiles:
|
||||
return profile not in self.include_profiles
|
||||
else:
|
||||
return profile in self.exclude_profiles
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
@@ -191,15 +197,16 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
self.use_cache = cache and self.get_option('cache')
|
||||
|
||||
self.exclude_profiles = self.get_option('exclude_profiles')
|
||||
self.include_profiles = self.get_option('include_profiles')
|
||||
self.group_by = self.get_option('group_by')
|
||||
|
||||
for profile in self._get_profiles():
|
||||
if profile['parent']:
|
||||
self.display.vvvv('Processing profile %s with parent %s\n' % (profile['name'], profile['parent']))
|
||||
if profile['parent'] not in self.exclude_profiles:
|
||||
if not self._exclude_profile(profile['parent']):
|
||||
parent_group_name = self._add_safe_group_name(profile['parent'])
|
||||
self.display.vvvv('Added profile parent group %s\n' % parent_group_name)
|
||||
if profile['name'] not in self.exclude_profiles:
|
||||
if not self._exclude_profile(profile['name']):
|
||||
group_name = self._add_safe_group_name(profile['name'])
|
||||
self.display.vvvv('Added profile group %s\n' % group_name)
|
||||
self.inventory.add_child(parent_group_name, group_name)
|
||||
@@ -211,7 +218,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
while i < len(profile_elements) - 1:
|
||||
profile_group = '-'.join(profile_elements[0:i + 1])
|
||||
profile_group_child = '-'.join(profile_elements[0:i + 2])
|
||||
if profile_group in self.exclude_profiles:
|
||||
if self._exclude_profile(profile_group):
|
||||
self.display.vvvv('Excluding profile %s\n' % profile_group)
|
||||
break
|
||||
group_name = self._add_safe_group_name(profile_group)
|
||||
@@ -232,7 +239,7 @@ class InventoryModule(BaseInventoryPlugin, Cacheable):
|
||||
hostname = host['hostname'] # None
|
||||
interfaces = host['interfaces']
|
||||
|
||||
if host['profile'] in self.exclude_profiles:
|
||||
if self._exclude_profile(host['profile']):
|
||||
self.display.vvvv('Excluding host %s in profile %s\n' % (host['name'], host['profile']))
|
||||
continue
|
||||
|
||||
|
||||
@@ -16,7 +16,17 @@ DOCUMENTATION = '''
|
||||
- Get inventory hosts from the Icinga2 API.
|
||||
- "Uses a configuration file as an inventory source, it must end in
|
||||
C(.icinga2.yml) or C(.icinga2.yaml)."
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
options:
|
||||
strict:
|
||||
version_added: 4.4.0
|
||||
compose:
|
||||
version_added: 4.4.0
|
||||
groups:
|
||||
version_added: 4.4.0
|
||||
keyed_groups:
|
||||
version_added: 4.4.0
|
||||
plugin:
|
||||
description: Name of the plugin.
|
||||
required: true
|
||||
@@ -35,13 +45,23 @@ DOCUMENTATION = '''
|
||||
type: string
|
||||
required: true
|
||||
host_filter:
|
||||
description: An Icinga2 API valid host filter.
|
||||
description:
|
||||
- An Icinga2 API valid host filter. Leave blank for no filtering
|
||||
type: string
|
||||
required: false
|
||||
validate_certs:
|
||||
description: Enables or disables SSL certificate verification.
|
||||
type: boolean
|
||||
default: true
|
||||
inventory_attr:
|
||||
description:
|
||||
- Allows the override of the inventory name based on different attributes.
|
||||
- This allows for changing the way limits are used.
|
||||
- The current default, C(address), is sometimes not unique or present. We recommend to use C(name) instead.
|
||||
type: string
|
||||
default: address
|
||||
choices: ['name', 'display_name', 'address']
|
||||
version_added: 4.2.0
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
@@ -52,6 +72,21 @@ user: ansible
|
||||
password: secure
|
||||
host_filter: \"linux-servers\" in host.groups
|
||||
validate_certs: false
|
||||
inventory_attr: name
|
||||
groups:
|
||||
# simple name matching
|
||||
webservers: inventory_hostname.startswith('web')
|
||||
|
||||
# using icinga2 template
|
||||
databaseservers: "'db-template' in (icinga2_attributes.templates|list)"
|
||||
|
||||
compose:
|
||||
# set all icinga2 attributes to a host variable 'icinga2_attrs'
|
||||
icinga2_attrs: icinga2_attributes
|
||||
|
||||
# set 'ansible_user' and 'ansible_port' from icinga2 host vars
|
||||
ansible_user: icinga2_attributes.vars.ansible_user
|
||||
ansible_port: icinga2_attributes.vars.ansible_port | default(22)
|
||||
'''
|
||||
|
||||
import json
|
||||
@@ -59,6 +94,7 @@ import json
|
||||
from ansible.errors import AnsibleParserError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.module_utils.urls import open_url
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
@@ -76,6 +112,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
self.icinga2_password = None
|
||||
self.ssl_verify = None
|
||||
self.host_filter = None
|
||||
self.inventory_attr = None
|
||||
|
||||
self.cache_key = None
|
||||
self.use_cache = None
|
||||
@@ -114,9 +151,21 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
if data is not None:
|
||||
request_args['data'] = json.dumps(data)
|
||||
self.display.vvv("Request Args: %s" % request_args)
|
||||
response = open_url(request_url, **request_args)
|
||||
try:
|
||||
response = open_url(request_url, **request_args)
|
||||
except HTTPError as e:
|
||||
try:
|
||||
error_body = json.loads(e.read().decode())
|
||||
self.display.vvv("Error returned: {0}".format(error_body))
|
||||
except Exception:
|
||||
error_body = {"status": None}
|
||||
if e.code == 404 and error_body.get('status') == "No objects found.":
|
||||
raise AnsibleParserError("Host filter returned no data. Please confirm your host_filter value is valid")
|
||||
raise AnsibleParserError("Unexpected data returned: {0} -- {1}".format(e, error_body))
|
||||
|
||||
response_body = response.read()
|
||||
json_data = json.loads(response_body.decode('utf-8'))
|
||||
self.display.vvv("Returned Data: %s" % json.dumps(json_data, indent=4, sort_keys=True))
|
||||
if 200 <= response.status <= 299:
|
||||
return json_data
|
||||
if response.status == 404 and json_data['status'] == "No objects found.":
|
||||
@@ -155,7 +204,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
"""Query for all hosts """
|
||||
self.display.vvv("Querying Icinga2 for inventory")
|
||||
query_args = {
|
||||
"attrs": ["address", "state_type", "state", "groups"],
|
||||
"attrs": ["address", "address6", "name", "display_name", "state_type", "state", "templates", "groups", "vars", "zone"],
|
||||
}
|
||||
if self.host_filter is not None:
|
||||
query_args['host_filter'] = self.host_filter
|
||||
@@ -165,6 +214,12 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
ansible_inv = self._convert_inv(results_json)
|
||||
return ansible_inv
|
||||
|
||||
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):
|
||||
groups = self._to_json(self.get_inventory_from_icinga())
|
||||
return groups
|
||||
@@ -177,25 +232,40 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
"""Convert Icinga2 API data to JSON format for Ansible"""
|
||||
groups_dict = {"_meta": {"hostvars": {}}}
|
||||
for entry in json_data:
|
||||
host_name = entry['name']
|
||||
host_attrs = entry['attrs']
|
||||
if self.inventory_attr == "name":
|
||||
host_name = entry.get('name')
|
||||
if self.inventory_attr == "address":
|
||||
# When looking for address for inventory, if missing fallback to object name
|
||||
if host_attrs.get('address', '') != '':
|
||||
host_name = host_attrs.get('address')
|
||||
else:
|
||||
host_name = entry.get('name')
|
||||
if self.inventory_attr == "display_name":
|
||||
host_name = host_attrs.get('display_name')
|
||||
if host_attrs['state'] == 0:
|
||||
host_attrs['state'] = 'on'
|
||||
else:
|
||||
host_attrs['state'] = 'off'
|
||||
host_groups = host_attrs['groups']
|
||||
host_addr = host_attrs['address']
|
||||
self.inventory.add_host(host_addr)
|
||||
host_groups = host_attrs.get('groups')
|
||||
self.inventory.add_host(host_name)
|
||||
for group in host_groups:
|
||||
if group not in self.inventory.groups.keys():
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_child(group, host_addr)
|
||||
self.inventory.set_variable(host_addr, 'address', host_addr)
|
||||
self.inventory.set_variable(host_addr, 'hostname', host_name)
|
||||
self.inventory.set_variable(host_addr, 'state',
|
||||
self.inventory.add_child(group, host_name)
|
||||
# If the address attribute is populated, override ansible_host with the value
|
||||
if host_attrs.get('address') != '':
|
||||
self.inventory.set_variable(host_name, 'ansible_host', host_attrs.get('address'))
|
||||
self.inventory.set_variable(host_name, 'hostname', entry.get('name'))
|
||||
self.inventory.set_variable(host_name, 'display_name', host_attrs.get('display_name'))
|
||||
self.inventory.set_variable(host_name, 'state',
|
||||
host_attrs['state'])
|
||||
self.inventory.set_variable(host_addr, 'state_type',
|
||||
self.inventory.set_variable(host_name, 'state_type',
|
||||
host_attrs['state_type'])
|
||||
# Adds all attributes to a variable 'icinga2_attributes'
|
||||
construct_vars = dict(self.inventory.get_host(host_name).get_vars())
|
||||
construct_vars['icinga2_attributes'] = host_attrs
|
||||
self._apply_constructable(host_name, construct_vars)
|
||||
return groups_dict
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
@@ -211,6 +281,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
self.icinga2_password = self.get_option('password')
|
||||
self.ssl_verify = self.get_option('validate_certs')
|
||||
self.host_filter = self.get_option('host_filter')
|
||||
self.inventory_attr = self.get_option('inventory_attr')
|
||||
# Not currently enabled
|
||||
# self.cache_key = self.get_cache_key(path)
|
||||
# self.use_cache = cache and self.get_option('cache')
|
||||
|
||||
@@ -66,6 +66,12 @@ EXAMPLES = r'''
|
||||
# Minimal example. `LINODE_ACCESS_TOKEN` is exposed in environment.
|
||||
plugin: community.general.linode
|
||||
|
||||
# You can use Jinja to template the access token.
|
||||
plugin: community.general.linode
|
||||
access_token: "{{ lookup('ini', 'token', section='your_username', file='~/.config/linode-cli') }}"
|
||||
# For older Ansible versions, you need to write this as:
|
||||
# access_token: "{{ lookup('ini', 'token section=your_username file=~/.config/linode-cli') }}"
|
||||
|
||||
# Example with regions, types, groups and access token
|
||||
plugin: community.general.linode
|
||||
access_token: foobar
|
||||
@@ -105,6 +111,7 @@ import os
|
||||
from ansible.errors import AnsibleError, AnsibleParserError
|
||||
from ansible.module_utils.six import string_types
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
|
||||
from ansible.template import Templar
|
||||
|
||||
|
||||
try:
|
||||
@@ -119,10 +126,14 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
|
||||
NAME = 'community.general.linode'
|
||||
|
||||
def _build_client(self):
|
||||
def _build_client(self, loader):
|
||||
"""Build the Linode client."""
|
||||
|
||||
t = Templar(loader=loader)
|
||||
|
||||
access_token = self.get_option('access_token')
|
||||
if t.is_template(access_token):
|
||||
access_token = t.template(variable=access_token, disable_lookups=False)
|
||||
|
||||
if access_token is None:
|
||||
try:
|
||||
@@ -287,7 +298,7 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
raise AnsibleError('the Linode dynamic inventory plugin requires linode_api4.')
|
||||
|
||||
config_data = self._read_config_data(path)
|
||||
self._build_client()
|
||||
self._build_client(loader)
|
||||
|
||||
self._get_instances_inventory()
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ DOCUMENTATION = r'''
|
||||
author: "Frank Dornheim (@conloos)"
|
||||
requirements:
|
||||
- ipaddress
|
||||
- lxd >= 4.0
|
||||
options:
|
||||
plugin:
|
||||
description: Token that ensures this is a source file for the 'lxd' plugin.
|
||||
@@ -49,26 +50,38 @@ DOCUMENTATION = r'''
|
||||
- If I(trust_password) is set, this module send a request for authentication before sending any requests.
|
||||
type: str
|
||||
state:
|
||||
description: Filter the container according to the current status.
|
||||
description: Filter the instance according to the current status.
|
||||
type: str
|
||||
default: none
|
||||
choices: [ 'STOPPED', 'STARTING', 'RUNNING', 'none' ]
|
||||
prefered_container_network_interface:
|
||||
type_filter:
|
||||
description:
|
||||
- If a container has multiple network interfaces, select which one is the prefered as pattern.
|
||||
- Filter the instances by type C(virtual-machine), C(container) or C(both).
|
||||
- The first version of the inventory only supported containers.
|
||||
type: str
|
||||
default: container
|
||||
choices: [ 'virtual-machine', 'container', 'both' ]
|
||||
version_added: 4.2.0
|
||||
prefered_instance_network_interface:
|
||||
description:
|
||||
- If an instance has multiple network interfaces, select which one is the prefered as pattern.
|
||||
- Combined with the first number that can be found e.g. 'eth' + 0.
|
||||
- The option has been renamed from I(prefered_container_network_interface) to I(prefered_instance_network_interface) in community.general 3.8.0.
|
||||
The old name still works as an alias.
|
||||
type: str
|
||||
default: eth
|
||||
prefered_container_network_family:
|
||||
aliases:
|
||||
- prefered_container_network_interface
|
||||
prefered_instance_network_family:
|
||||
description:
|
||||
- If a container has multiple network interfaces, which one is the prefered by family.
|
||||
- If an instance has multiple network interfaces, which one is the prefered by family.
|
||||
- Specify C(inet) for IPv4 and C(inet6) for IPv6.
|
||||
type: str
|
||||
default: inet
|
||||
choices: [ 'inet', 'inet6' ]
|
||||
groupby:
|
||||
description:
|
||||
- Create groups by the following keywords C(location), C(pattern), C(network_range), C(os), C(release), C(profile), C(vlanid).
|
||||
- Create groups by the following keywords C(location), C(network_range), C(os), C(pattern), C(profile), C(release), C(type), C(vlanid).
|
||||
- See example for syntax.
|
||||
type: dict
|
||||
'''
|
||||
@@ -83,38 +96,49 @@ plugin: community.general.lxd
|
||||
url: unix:/var/snap/lxd/common/lxd/unix.socket
|
||||
state: RUNNING
|
||||
|
||||
# simple lxd.yml including virtual machines and containers
|
||||
plugin: community.general.lxd
|
||||
url: unix:/var/snap/lxd/common/lxd/unix.socket
|
||||
type_filter: both
|
||||
|
||||
# grouping lxd.yml
|
||||
groupby:
|
||||
testpattern:
|
||||
type: pattern
|
||||
attribute: test
|
||||
vlan666:
|
||||
type: vlanid
|
||||
attribute: 666
|
||||
locationBerlin:
|
||||
type: location
|
||||
attribute: Berlin
|
||||
osUbuntu:
|
||||
type: os
|
||||
attribute: ubuntu
|
||||
releaseFocal:
|
||||
type: release
|
||||
attribute: focal
|
||||
releaseBionic:
|
||||
type: release
|
||||
attribute: bionic
|
||||
profileDefault:
|
||||
type: profile
|
||||
attribute: default
|
||||
profileX11:
|
||||
type: profile
|
||||
attribute: x11
|
||||
netRangeIPv4:
|
||||
type: network_range
|
||||
attribute: 10.98.143.0/24
|
||||
netRangeIPv6:
|
||||
type: network_range
|
||||
attribute: fd42:bd00:7b11:2167:216:3eff::/24
|
||||
osUbuntu:
|
||||
type: os
|
||||
attribute: ubuntu
|
||||
testpattern:
|
||||
type: pattern
|
||||
attribute: test
|
||||
profileDefault:
|
||||
type: profile
|
||||
attribute: default
|
||||
profileX11:
|
||||
type: profile
|
||||
attribute: x11
|
||||
releaseFocal:
|
||||
type: release
|
||||
attribute: focal
|
||||
releaseBionic:
|
||||
type: release
|
||||
attribute: bionic
|
||||
typeVM:
|
||||
type: type
|
||||
attribute: virtual-machine
|
||||
typeContainer:
|
||||
type: type
|
||||
attribute: container
|
||||
vlan666:
|
||||
type: vlanid
|
||||
attribute: 666
|
||||
'''
|
||||
|
||||
import binascii
|
||||
@@ -283,10 +307,10 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
network_configs = self.socket.do('GET', '/1.0/networks')
|
||||
return [m.split('/')[3] for m in network_configs['metadata']]
|
||||
|
||||
def _get_containers(self):
|
||||
"""Get Containernames
|
||||
def _get_instances(self):
|
||||
"""Get instancenames
|
||||
|
||||
Returns all containernames
|
||||
Returns all instancenames
|
||||
|
||||
Args:
|
||||
None
|
||||
@@ -295,25 +319,27 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
list(names): names of all containers"""
|
||||
# e.g. {'type': 'sync',
|
||||
# 'status': 'Success',
|
||||
# 'status_code': 200,
|
||||
# 'operation': '',
|
||||
# 'error_code': 0,
|
||||
# 'error': '',
|
||||
# 'metadata': ['/1.0/containers/udemy-ansible-ubuntu-2004']}
|
||||
containers = self.socket.do('GET', '/1.0/containers')
|
||||
return [m.split('/')[3] for m in containers['metadata']]
|
||||
list(names): names of all instances"""
|
||||
# e.g. {
|
||||
# "metadata": [
|
||||
# "/1.0/instances/foo",
|
||||
# "/1.0/instances/bar"
|
||||
# ],
|
||||
# "status": "Success",
|
||||
# "status_code": 200,
|
||||
# "type": "sync"
|
||||
# }
|
||||
instances = self.socket.do('GET', '/1.0/instances')
|
||||
return [m.split('/')[3] for m in instances['metadata']]
|
||||
|
||||
def _get_config(self, branch, name):
|
||||
"""Get inventory of container
|
||||
"""Get inventory of instance
|
||||
|
||||
Get config of container
|
||||
Get config of instance
|
||||
|
||||
Args:
|
||||
str(branch): Name oft the API-Branch
|
||||
str(name): Name of Container
|
||||
str(name): Name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Source:
|
||||
@@ -321,7 +347,7 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
dict(config): Config of the container"""
|
||||
dict(config): Config of the instance"""
|
||||
config = {}
|
||||
if isinstance(branch, (tuple, list)):
|
||||
config[name] = {branch[1]: self.socket.do('GET', '/1.0/{0}/{1}/{2}'.format(to_native(branch[0]), to_native(name), to_native(branch[1])))}
|
||||
@@ -329,13 +355,13 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
config[name] = {branch: self.socket.do('GET', '/1.0/{0}/{1}'.format(to_native(branch), to_native(name)))}
|
||||
return config
|
||||
|
||||
def get_container_data(self, names):
|
||||
"""Create Inventory of the container
|
||||
def get_instance_data(self, names):
|
||||
"""Create Inventory of the instance
|
||||
|
||||
Iterate through the different branches of the containers and collect Informations.
|
||||
Iterate through the different branches of the instances and collect Informations.
|
||||
|
||||
Args:
|
||||
list(names): List of container names
|
||||
list(names): List of instance names
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -344,20 +370,20 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
None"""
|
||||
# tuple(('instances','metadata/templates')) to get section in branch
|
||||
# e.g. /1.0/instances/<name>/metadata/templates
|
||||
branches = ['containers', ('instances', 'state')]
|
||||
container_config = {}
|
||||
branches = ['instances', ('instances', 'state')]
|
||||
instance_config = {}
|
||||
for branch in branches:
|
||||
for name in names:
|
||||
container_config['containers'] = self._get_config(branch, name)
|
||||
self.data = dict_merge(container_config, self.data)
|
||||
instance_config['instances'] = self._get_config(branch, name)
|
||||
self.data = dict_merge(instance_config, self.data)
|
||||
|
||||
def get_network_data(self, names):
|
||||
"""Create Inventory of the container
|
||||
"""Create Inventory of the instance
|
||||
|
||||
Iterate through the different branches of the containers and collect Informations.
|
||||
Iterate through the different branches of the instances and collect Informations.
|
||||
|
||||
Args:
|
||||
list(names): List of container names
|
||||
list(names): List of instance names
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -376,26 +402,26 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
network_config['networks'] = {name: None}
|
||||
self.data = dict_merge(network_config, self.data)
|
||||
|
||||
def extract_network_information_from_container_config(self, container_name):
|
||||
def extract_network_information_from_instance_config(self, instance_name):
|
||||
"""Returns the network interface configuration
|
||||
|
||||
Returns the network ipv4 and ipv6 config of the container without local-link
|
||||
Returns the network ipv4 and ipv6 config of the instance without local-link
|
||||
|
||||
Args:
|
||||
str(container_name): Name oft he container
|
||||
str(instance_name): Name oft he instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
dict(network_configuration): network config"""
|
||||
container_network_interfaces = self._get_data_entry('containers/{0}/state/metadata/network'.format(container_name))
|
||||
instance_network_interfaces = self._get_data_entry('instances/{0}/state/metadata/network'.format(instance_name))
|
||||
network_configuration = None
|
||||
if container_network_interfaces:
|
||||
if instance_network_interfaces:
|
||||
network_configuration = {}
|
||||
gen_interface_names = [interface_name for interface_name in container_network_interfaces if interface_name != 'lo']
|
||||
gen_interface_names = [interface_name for interface_name in instance_network_interfaces if interface_name != 'lo']
|
||||
for interface_name in gen_interface_names:
|
||||
gen_address = [address for address in container_network_interfaces[interface_name]['addresses'] if address.get('scope') != 'link']
|
||||
gen_address = [address for address in instance_network_interfaces[interface_name]['addresses'] if address.get('scope') != 'link']
|
||||
network_configuration[interface_name] = []
|
||||
for address in gen_address:
|
||||
address_set = {}
|
||||
@@ -406,24 +432,24 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
network_configuration[interface_name].append(address_set)
|
||||
return network_configuration
|
||||
|
||||
def get_prefered_container_network_interface(self, container_name):
|
||||
"""Helper to get the prefered interface of thr container
|
||||
def get_prefered_instance_network_interface(self, instance_name):
|
||||
"""Helper to get the prefered interface of thr instance
|
||||
|
||||
Helper to get the prefered interface provide by neme pattern from 'prefered_container_network_interface'.
|
||||
Helper to get the prefered interface provide by neme pattern from 'prefered_instance_network_interface'.
|
||||
|
||||
Args:
|
||||
str(containe_name): name of container
|
||||
str(containe_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
str(prefered_interface): None or interface name"""
|
||||
container_network_interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name))
|
||||
instance_network_interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name))
|
||||
prefered_interface = None # init
|
||||
if container_network_interfaces: # container have network interfaces
|
||||
if instance_network_interfaces: # instance have network interfaces
|
||||
# generator if interfaces which start with the desired pattern
|
||||
net_generator = [interface for interface in container_network_interfaces if interface.startswith(self.prefered_container_network_interface)]
|
||||
net_generator = [interface for interface in instance_network_interfaces if interface.startswith(self.prefered_instance_network_interface)]
|
||||
selected_interfaces = [] # init
|
||||
for interface in net_generator:
|
||||
selected_interfaces.append(interface)
|
||||
@@ -431,13 +457,13 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
prefered_interface = sorted(selected_interfaces)[0]
|
||||
return prefered_interface
|
||||
|
||||
def get_container_vlans(self, container_name):
|
||||
"""Get VLAN(s) from container
|
||||
def get_instance_vlans(self, instance_name):
|
||||
"""Get VLAN(s) from instance
|
||||
|
||||
Helper to get the VLAN_ID from the container
|
||||
Helper to get the VLAN_ID from the instance
|
||||
|
||||
Args:
|
||||
str(containe_name): name of container
|
||||
str(containe_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -450,13 +476,13 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if self._get_data_entry('state/metadata/vlan/vid', data=self.data['networks'].get(network)):
|
||||
network_vlans[network] = self._get_data_entry('state/metadata/vlan/vid', data=self.data['networks'].get(network))
|
||||
|
||||
# get networkdevices of container and return
|
||||
# get networkdevices of instance and return
|
||||
# e.g.
|
||||
# "eth0":{ "name":"eth0",
|
||||
# "network":"lxdbr0",
|
||||
# "type":"nic"},
|
||||
vlan_ids = {}
|
||||
devices = self._get_data_entry('containers/{0}/containers/metadata/expanded_devices'.format(to_native(container_name)))
|
||||
devices = self._get_data_entry('instances/{0}/instances/metadata/expanded_devices'.format(to_native(instance_name)))
|
||||
for device in devices:
|
||||
if 'network' in devices[device]:
|
||||
if devices[device]['network'] in network_vlans:
|
||||
@@ -492,14 +518,14 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _set_data_entry(self, container_name, key, value, path=None):
|
||||
def _set_data_entry(self, instance_name, key, value, path=None):
|
||||
"""Helper to save data
|
||||
|
||||
Helper to save the data in self.data
|
||||
Detect if data is allready in branch and use dict_merge() to prevent that branch is overwritten.
|
||||
|
||||
Args:
|
||||
str(container_name): name of container
|
||||
str(instance_name): name of instance
|
||||
str(key): same as dict
|
||||
*(value): same as dict
|
||||
Kwargs:
|
||||
@@ -510,24 +536,24 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
None"""
|
||||
if not path:
|
||||
path = self.data['inventory']
|
||||
if container_name not in path:
|
||||
path[container_name] = {}
|
||||
if instance_name not in path:
|
||||
path[instance_name] = {}
|
||||
|
||||
try:
|
||||
if isinstance(value, dict) and key in path[container_name]:
|
||||
path[container_name] = dict_merge(value, path[container_name][key])
|
||||
if isinstance(value, dict) and key in path[instance_name]:
|
||||
path[instance_name] = dict_merge(value, path[instance_name][key])
|
||||
else:
|
||||
path[container_name][key] = value
|
||||
path[instance_name][key] = value
|
||||
except KeyError as err:
|
||||
raise AnsibleParserError("Unable to store Informations: {0}".format(to_native(err)))
|
||||
|
||||
def extract_information_from_container_configs(self):
|
||||
def extract_information_from_instance_configs(self):
|
||||
"""Process configuration information
|
||||
|
||||
Preparation of the data
|
||||
|
||||
Args:
|
||||
dict(configs): Container configurations
|
||||
dict(configs): instance configurations
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -538,33 +564,35 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if 'inventory' not in self.data:
|
||||
self.data['inventory'] = {}
|
||||
|
||||
for container_name in self.data['containers']:
|
||||
self._set_data_entry(container_name, 'os', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/config/image.os'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'release', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/config/image.release'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'version', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/config/image.version'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'profile', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/profiles'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'location', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/location'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'state', self._get_data_entry(
|
||||
'containers/{0}/containers/metadata/config/volatile.last_state.power'.format(container_name)))
|
||||
self._set_data_entry(container_name, 'network_interfaces', self.extract_network_information_from_container_config(container_name))
|
||||
self._set_data_entry(container_name, 'preferred_interface', self.get_prefered_container_network_interface(container_name))
|
||||
self._set_data_entry(container_name, 'vlan_ids', self.get_container_vlans(container_name))
|
||||
for instance_name in self.data['instances']:
|
||||
self._set_data_entry(instance_name, 'os', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/config/image.os'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'release', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/config/image.release'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'version', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/config/image.version'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'profile', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/profiles'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'location', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/location'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'state', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/config/volatile.last_state.power'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'type', self._get_data_entry(
|
||||
'instances/{0}/instances/metadata/type'.format(instance_name)))
|
||||
self._set_data_entry(instance_name, 'network_interfaces', self.extract_network_information_from_instance_config(instance_name))
|
||||
self._set_data_entry(instance_name, 'preferred_interface', self.get_prefered_instance_network_interface(instance_name))
|
||||
self._set_data_entry(instance_name, 'vlan_ids', self.get_instance_vlans(instance_name))
|
||||
|
||||
def build_inventory_network(self, container_name):
|
||||
"""Add the network interfaces of the container to the inventory
|
||||
def build_inventory_network(self, instance_name):
|
||||
"""Add the network interfaces of the instance to the inventory
|
||||
|
||||
Logic:
|
||||
- if the container have no interface -> 'ansible_connection: local'
|
||||
- get preferred_interface & prefered_container_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
|
||||
- first Interface from: network_interfaces prefered_container_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
|
||||
- if the instance have no interface -> 'ansible_connection: local'
|
||||
- get preferred_interface & prefered_instance_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
|
||||
- first Interface from: network_interfaces prefered_instance_network_family -> 'ansible_connection: ssh' & 'ansible_host: <IP>'
|
||||
|
||||
Args:
|
||||
str(container_name): name of container
|
||||
str(instance_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
@@ -572,45 +600,45 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Returns:
|
||||
None"""
|
||||
|
||||
def interface_selection(container_name):
|
||||
"""Select container Interface for inventory
|
||||
def interface_selection(instance_name):
|
||||
"""Select instance Interface for inventory
|
||||
|
||||
Logic:
|
||||
- get preferred_interface & prefered_container_network_family -> str(IP)
|
||||
- first Interface from: network_interfaces prefered_container_network_family -> str(IP)
|
||||
- get preferred_interface & prefered_instance_network_family -> str(IP)
|
||||
- first Interface from: network_interfaces prefered_instance_network_family -> str(IP)
|
||||
|
||||
Args:
|
||||
str(container_name): name of container
|
||||
str(instance_name): name of instance
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
dict(interface_name: ip)"""
|
||||
prefered_interface = self._get_data_entry('inventory/{0}/preferred_interface'.format(container_name)) # name or None
|
||||
prefered_container_network_family = self.prefered_container_network_family
|
||||
prefered_interface = self._get_data_entry('inventory/{0}/preferred_interface'.format(instance_name)) # name or None
|
||||
prefered_instance_network_family = self.prefered_instance_network_family
|
||||
|
||||
ip_address = ''
|
||||
if prefered_interface:
|
||||
interface = self._get_data_entry('inventory/{0}/network_interfaces/{1}'.format(container_name, prefered_interface))
|
||||
interface = self._get_data_entry('inventory/{0}/network_interfaces/{1}'.format(instance_name, prefered_interface))
|
||||
for config in interface:
|
||||
if config['family'] == prefered_container_network_family:
|
||||
if config['family'] == prefered_instance_network_family:
|
||||
ip_address = config['address']
|
||||
break
|
||||
else:
|
||||
interface = self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name))
|
||||
for config in interface:
|
||||
if config['family'] == prefered_container_network_family:
|
||||
ip_address = config['address']
|
||||
break
|
||||
interfaces = self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name))
|
||||
for interface in interfaces.values():
|
||||
for config in interface:
|
||||
if config['family'] == prefered_instance_network_family:
|
||||
ip_address = config['address']
|
||||
break
|
||||
return ip_address
|
||||
|
||||
if self._get_data_entry('inventory/{0}/network_interfaces'.format(container_name)): # container have network interfaces
|
||||
if self._get_data_entry('inventory/{0}/preferred_interface'.format(container_name)): # container have a preferred interface
|
||||
self.inventory.set_variable(container_name, 'ansible_connection', 'ssh')
|
||||
self.inventory.set_variable(container_name, 'ansible_host', interface_selection(container_name))
|
||||
if self._get_data_entry('inventory/{0}/network_interfaces'.format(instance_name)): # instance have network interfaces
|
||||
self.inventory.set_variable(instance_name, 'ansible_connection', 'ssh')
|
||||
self.inventory.set_variable(instance_name, 'ansible_host', interface_selection(instance_name))
|
||||
else:
|
||||
self.inventory.set_variable(container_name, 'ansible_connection', 'local')
|
||||
self.inventory.set_variable(instance_name, 'ansible_connection', 'local')
|
||||
|
||||
def build_inventory_hosts(self):
|
||||
"""Build host-part dynamic inventory
|
||||
@@ -626,29 +654,33 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
None
|
||||
Returns:
|
||||
None"""
|
||||
for container_name in self.data['inventory']:
|
||||
# Only consider containers that match the "state" filter, if self.state is not None
|
||||
for instance_name in self.data['inventory']:
|
||||
instance_state = str(self._get_data_entry('inventory/{0}/state'.format(instance_name)) or "STOPPED").lower()
|
||||
|
||||
# Only consider instances that match the "state" filter, if self.state is not None
|
||||
if self.filter:
|
||||
if self.filter.lower() != self._get_data_entry('inventory/{0}/state'.format(container_name)).lower():
|
||||
if self.filter.lower() != instance_state:
|
||||
continue
|
||||
# add container
|
||||
self.inventory.add_host(container_name)
|
||||
# add instance
|
||||
self.inventory.add_host(instance_name)
|
||||
# add network informations
|
||||
self.build_inventory_network(container_name)
|
||||
self.build_inventory_network(instance_name)
|
||||
# add os
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(container_name)).lower())
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_os', self._get_data_entry('inventory/{0}/os'.format(instance_name)).lower())
|
||||
# add release
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(container_name)).lower())
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_release', self._get_data_entry('inventory/{0}/release'.format(instance_name)).lower())
|
||||
# add profile
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(container_name)))
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_profile', self._get_data_entry('inventory/{0}/profile'.format(instance_name)))
|
||||
# add state
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_state', self._get_data_entry('inventory/{0}/state'.format(container_name)).lower())
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_state', instance_state)
|
||||
# add type
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_type', self._get_data_entry('inventory/{0}/type'.format(instance_name)))
|
||||
# add location information
|
||||
if self._get_data_entry('inventory/{0}/location'.format(container_name)) != "none": # wrong type by lxd 'none' != 'None'
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_location', self._get_data_entry('inventory/{0}/location'.format(container_name)))
|
||||
if self._get_data_entry('inventory/{0}/location'.format(instance_name)) != "none": # wrong type by lxd 'none' != 'None'
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_location', self._get_data_entry('inventory/{0}/location'.format(instance_name)))
|
||||
# add VLAN_ID information
|
||||
if self._get_data_entry('inventory/{0}/vlan_ids'.format(container_name)):
|
||||
self.inventory.set_variable(container_name, 'ansible_lxd_vlan_ids', self._get_data_entry('inventory/{0}/vlan_ids'.format(container_name)))
|
||||
if self._get_data_entry('inventory/{0}/vlan_ids'.format(instance_name)):
|
||||
self.inventory.set_variable(instance_name, 'ansible_lxd_vlan_ids', self._get_data_entry('inventory/{0}/vlan_ids'.format(instance_name)))
|
||||
|
||||
def build_inventory_groups_location(self, group_name):
|
||||
"""create group by attribute: location
|
||||
@@ -665,9 +697,9 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
for container_name in self.inventory.hosts:
|
||||
if 'ansible_lxd_location' in self.inventory.get_host(container_name).get_vars():
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
for instance_name in self.inventory.hosts:
|
||||
if 'ansible_lxd_location' in self.inventory.get_host(instance_name).get_vars():
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_pattern(self, group_name):
|
||||
"""create group by name pattern
|
||||
@@ -686,10 +718,10 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
|
||||
regex_pattern = self.groupby[group_name].get('attribute')
|
||||
|
||||
for container_name in self.inventory.hosts:
|
||||
result = re.search(regex_pattern, container_name)
|
||||
for instance_name in self.inventory.hosts:
|
||||
result = re.search(regex_pattern, instance_name)
|
||||
if result:
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_network_range(self, group_name):
|
||||
"""check if IP is in network-class
|
||||
@@ -712,14 +744,14 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
raise AnsibleParserError(
|
||||
'Error while parsing network range {0}: {1}'.format(self.groupby[group_name].get('attribute'), to_native(err)))
|
||||
|
||||
for container_name in self.inventory.hosts:
|
||||
if self.data['inventory'][container_name].get('network_interfaces') is not None:
|
||||
for interface in self.data['inventory'][container_name].get('network_interfaces'):
|
||||
for interface_family in self.data['inventory'][container_name].get('network_interfaces')[interface]:
|
||||
for instance_name in self.inventory.hosts:
|
||||
if self.data['inventory'][instance_name].get('network_interfaces') is not None:
|
||||
for interface in self.data['inventory'][instance_name].get('network_interfaces'):
|
||||
for interface_family in self.data['inventory'][instance_name].get('network_interfaces')[interface]:
|
||||
try:
|
||||
address = ipaddress.ip_address(to_text(interface_family['address']))
|
||||
if address.version == network.version and address in network:
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
except ValueError:
|
||||
# Ignore invalid IP addresses returned by lxd
|
||||
pass
|
||||
@@ -730,7 +762,7 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
Args:
|
||||
str(group_name): Group name
|
||||
Kwargs:
|
||||
Noneself.data['inventory'][container_name][interface]
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
@@ -739,12 +771,12 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
gen_containers = [
|
||||
container_name for container_name in self.inventory.hosts
|
||||
if 'ansible_lxd_os' in self.inventory.get_host(container_name).get_vars()]
|
||||
for container_name in gen_containers:
|
||||
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(container_name).get_vars().get('ansible_lxd_os'):
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
gen_instances = [
|
||||
instance_name for instance_name in self.inventory.hosts
|
||||
if 'ansible_lxd_os' in self.inventory.get_host(instance_name).get_vars()]
|
||||
for instance_name in gen_instances:
|
||||
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_os'):
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_release(self, group_name):
|
||||
"""create group by attribute: release
|
||||
@@ -761,12 +793,12 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
gen_containers = [
|
||||
container_name for container_name in self.inventory.hosts
|
||||
if 'ansible_lxd_release' in self.inventory.get_host(container_name).get_vars()]
|
||||
for container_name in gen_containers:
|
||||
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(container_name).get_vars().get('ansible_lxd_release'):
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
gen_instances = [
|
||||
instance_name for instance_name in self.inventory.hosts
|
||||
if 'ansible_lxd_release' in self.inventory.get_host(instance_name).get_vars()]
|
||||
for instance_name in gen_instances:
|
||||
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_release'):
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_profile(self, group_name):
|
||||
"""create group by attribute: profile
|
||||
@@ -783,12 +815,12 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
gen_containers = [
|
||||
container_name for container_name in self.inventory.hosts.keys()
|
||||
if 'ansible_lxd_profile' in self.inventory.get_host(container_name).get_vars().keys()]
|
||||
for container_name in gen_containers:
|
||||
if self.groupby[group_name].get('attribute').lower() in self.inventory.get_host(container_name).get_vars().get('ansible_lxd_profile'):
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
gen_instances = [
|
||||
instance_name for instance_name in self.inventory.hosts.keys()
|
||||
if 'ansible_lxd_profile' in self.inventory.get_host(instance_name).get_vars().keys()]
|
||||
for instance_name in gen_instances:
|
||||
if self.groupby[group_name].get('attribute').lower() in self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_profile'):
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_vlanid(self, group_name):
|
||||
"""create group by attribute: vlanid
|
||||
@@ -805,12 +837,34 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
gen_containers = [
|
||||
container_name for container_name in self.inventory.hosts.keys()
|
||||
if 'ansible_lxd_vlan_ids' in self.inventory.get_host(container_name).get_vars().keys()]
|
||||
for container_name in gen_containers:
|
||||
if self.groupby[group_name].get('attribute') in self.inventory.get_host(container_name).get_vars().get('ansible_lxd_vlan_ids').values():
|
||||
self.inventory.add_child(group_name, container_name)
|
||||
gen_instances = [
|
||||
instance_name for instance_name in self.inventory.hosts.keys()
|
||||
if 'ansible_lxd_vlan_ids' in self.inventory.get_host(instance_name).get_vars().keys()]
|
||||
for instance_name in gen_instances:
|
||||
if self.groupby[group_name].get('attribute') in self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_vlan_ids').values():
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups_type(self, group_name):
|
||||
"""create group by attribute: type
|
||||
|
||||
Args:
|
||||
str(group_name): Group name
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
None"""
|
||||
# maybe we just want to expand one group
|
||||
if group_name not in self.inventory.groups:
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
gen_instances = [
|
||||
instance_name for instance_name in self.inventory.hosts
|
||||
if 'ansible_lxd_type' in self.inventory.get_host(instance_name).get_vars()]
|
||||
for instance_name in gen_instances:
|
||||
if self.groupby[group_name].get('attribute').lower() == self.inventory.get_host(instance_name).get_vars().get('ansible_lxd_type'):
|
||||
self.inventory.add_child(group_name, instance_name)
|
||||
|
||||
def build_inventory_groups(self):
|
||||
"""Build group-part dynamic inventory
|
||||
@@ -839,6 +893,7 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
* 'release'
|
||||
* 'profile'
|
||||
* 'vlanid'
|
||||
* 'type'
|
||||
|
||||
Args:
|
||||
str(group_name): Group name
|
||||
@@ -864,6 +919,8 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
self.build_inventory_groups_profile(group_name)
|
||||
elif self.groupby[group_name].get('type') == 'vlanid':
|
||||
self.build_inventory_groups_vlanid(group_name)
|
||||
elif self.groupby[group_name].get('type') == 'type':
|
||||
self.build_inventory_groups_type(group_name)
|
||||
else:
|
||||
raise AnsibleParserError('Unknown group type: {0}'.format(to_native(group_name)))
|
||||
|
||||
@@ -890,10 +947,30 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
self.build_inventory_hosts()
|
||||
self.build_inventory_groups()
|
||||
|
||||
def cleandata(self):
|
||||
"""Clean the dynamic inventory
|
||||
|
||||
The first version of the inventory only supported container.
|
||||
This will change in the future.
|
||||
The following function cleans up the data and remove the all items with the wrong type.
|
||||
|
||||
Args:
|
||||
None
|
||||
Kwargs:
|
||||
None
|
||||
Raises:
|
||||
None
|
||||
Returns:
|
||||
None"""
|
||||
iter_keys = list(self.data['instances'].keys())
|
||||
for instance_name in iter_keys:
|
||||
if self._get_data_entry('instances/{0}/instances/metadata/type'.format(instance_name)) != self.type_filter:
|
||||
del self.data['instances'][instance_name]
|
||||
|
||||
def _populate(self):
|
||||
"""Return the hosts and groups
|
||||
|
||||
Returns the processed container configurations from the lxd import
|
||||
Returns the processed instance configurations from the lxd import
|
||||
|
||||
Args:
|
||||
None
|
||||
@@ -906,10 +983,16 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
|
||||
if len(self.data) == 0: # If no data is injected by unittests open socket
|
||||
self.socket = self._connect_to_socket()
|
||||
self.get_container_data(self._get_containers())
|
||||
self.get_instance_data(self._get_instances())
|
||||
self.get_network_data(self._get_networks())
|
||||
|
||||
self.extract_information_from_container_configs()
|
||||
# The first version of the inventory only supported containers.
|
||||
# This will change in the future.
|
||||
# The following function cleans up the data.
|
||||
if self.type_filter != 'both':
|
||||
self.cleandata()
|
||||
|
||||
self.extract_information_from_instance_configs()
|
||||
|
||||
# self.display.vvv(self.save_json_data([os.path.abspath(__file__)]))
|
||||
|
||||
@@ -948,8 +1031,9 @@ class InventoryModule(BaseInventoryPlugin):
|
||||
self.data = {} # store for inventory-data
|
||||
self.groupby = self.get_option('groupby')
|
||||
self.plugin = self.get_option('plugin')
|
||||
self.prefered_container_network_family = self.get_option('prefered_container_network_family')
|
||||
self.prefered_container_network_interface = self.get_option('prefered_container_network_interface')
|
||||
self.prefered_instance_network_family = self.get_option('prefered_instance_network_family')
|
||||
self.prefered_instance_network_interface = self.get_option('prefered_instance_network_interface')
|
||||
self.type_filter = self.get_option('type_filter')
|
||||
if self.get_option('state').lower() == 'none': # none in config is str()
|
||||
self.filter = None
|
||||
else:
|
||||
|
||||
@@ -8,7 +8,7 @@ __metaclass__ = type
|
||||
DOCUMENTATION = r'''
|
||||
name: online
|
||||
author:
|
||||
- Remy Leone (@sieben)
|
||||
- Remy Leone (@remyleone)
|
||||
short_description: Scaleway (previously Online SAS or Online.net) inventory source
|
||||
description:
|
||||
- Get inventory hosts from Scaleway (previously Online SAS or Online.net).
|
||||
|
||||
@@ -119,12 +119,13 @@ compose:
|
||||
import re
|
||||
|
||||
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, Constructable, Cacheable
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
# 3rd party imports
|
||||
try:
|
||||
import requests
|
||||
|
||||
@@ -9,7 +9,7 @@ __metaclass__ = type
|
||||
DOCUMENTATION = r'''
|
||||
name: scaleway
|
||||
author:
|
||||
- Remy Leone (@sieben)
|
||||
- Remy Leone (@remyleone)
|
||||
short_description: Scaleway inventory source
|
||||
description:
|
||||
- Get inventory hosts from Scaleway.
|
||||
@@ -31,6 +31,12 @@ DOCUMENTATION = r'''
|
||||
tags:
|
||||
description: Filter results on a specific tag.
|
||||
type: list
|
||||
scw_profile:
|
||||
description:
|
||||
- The config profile to use in config file.
|
||||
- By default uses the one specified as C(active_profile) in the config file, or falls back to C(default) if that is not defined.
|
||||
type: string
|
||||
version_added: 4.4.0
|
||||
oauth_token:
|
||||
description:
|
||||
- Scaleway OAuth token.
|
||||
@@ -303,7 +309,13 @@ class InventoryModule(BaseInventoryPlugin, Constructable):
|
||||
if not oauth_token and os.path.exists(scw_config_path):
|
||||
with open(scw_config_path) as fh:
|
||||
scw_config = yaml.safe_load(fh)
|
||||
active_profile = scw_config.get('active_profile', 'default')
|
||||
ansible_profile = self.get_option('scw_profile')
|
||||
|
||||
if ansible_profile:
|
||||
active_profile = ansible_profile
|
||||
else:
|
||||
active_profile = scw_config.get('active_profile', 'default')
|
||||
|
||||
if active_profile == 'default':
|
||||
oauth_token = scw_config.get('secret_key')
|
||||
else:
|
||||
|
||||
327
plugins/inventory/xen_orchestra.py
Normal file
327
plugins/inventory/xen_orchestra.py
Normal file
@@ -0,0 +1,327 @@
|
||||
# -*- 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 = '''
|
||||
name: xen_orchestra
|
||||
short_description: Xen Orchestra inventory source
|
||||
version_added: 4.1.0
|
||||
author:
|
||||
- Dom Del Nano (@ddelnano) <ddelnano@gmail.com>
|
||||
- Samori Gorse (@shinuza) <samorigorse@gmail.com>
|
||||
requirements:
|
||||
- websocket-client >= 1.0.0
|
||||
description:
|
||||
- Get inventory hosts from a Xen Orchestra deployment.
|
||||
- 'Uses a configuration file as an inventory source, it must end in C(.xen_orchestra.yml) or C(.xen_orchestra.yaml).'
|
||||
extends_documentation_fragment:
|
||||
- constructed
|
||||
- inventory_cache
|
||||
options:
|
||||
plugin:
|
||||
description: The name of this plugin, it should always be set to C(community.general.xen_orchestra) for this plugin to recognize it as its own.
|
||||
required: yes
|
||||
choices: ['community.general.xen_orchestra']
|
||||
type: str
|
||||
api_host:
|
||||
description:
|
||||
- API host to XOA API.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(ANSIBLE_XO_HOST) will be used instead.
|
||||
type: str
|
||||
env:
|
||||
- name: ANSIBLE_XO_HOST
|
||||
user:
|
||||
description:
|
||||
- Xen Orchestra user.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(ANSIBLE_XO_USER) will be used instead.
|
||||
required: yes
|
||||
type: str
|
||||
env:
|
||||
- name: ANSIBLE_XO_USER
|
||||
password:
|
||||
description:
|
||||
- Xen Orchestra password.
|
||||
- If the value is not specified in the inventory configuration, the value of environment variable C(ANSIBLE_XO_PASSWORD) will be used instead.
|
||||
required: yes
|
||||
type: str
|
||||
env:
|
||||
- name: ANSIBLE_XO_PASSWORD
|
||||
validate_certs:
|
||||
description: Verify TLS certificate if using HTTPS.
|
||||
type: boolean
|
||||
default: true
|
||||
use_ssl:
|
||||
description: Use wss when connecting to the Xen Orchestra API
|
||||
type: boolean
|
||||
default: true
|
||||
'''
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
# file must be named xen_orchestra.yaml or xen_orchestra.yml
|
||||
plugin: community.general.xen_orchestra
|
||||
api_host: 192.168.1.255
|
||||
user: xo
|
||||
password: xo_pwd
|
||||
validate_certs: true
|
||||
use_ssl: true
|
||||
groups:
|
||||
kube_nodes: "'kube_node' in tags"
|
||||
compose:
|
||||
ansible_port: 2222
|
||||
|
||||
'''
|
||||
|
||||
import json
|
||||
import ssl
|
||||
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
# 3rd party imports
|
||||
try:
|
||||
HAS_WEBSOCKET = True
|
||||
import websocket
|
||||
from websocket import create_connection
|
||||
|
||||
if LooseVersion(websocket.__version__) <= LooseVersion('1.0.0'):
|
||||
raise ImportError
|
||||
except ImportError as e:
|
||||
HAS_WEBSOCKET = False
|
||||
|
||||
|
||||
HALTED = 'Halted'
|
||||
PAUSED = 'Paused'
|
||||
RUNNING = 'Running'
|
||||
SUSPENDED = 'Suspended'
|
||||
POWER_STATES = [RUNNING, HALTED, SUSPENDED, PAUSED]
|
||||
HOST_GROUP = 'xo_hosts'
|
||||
POOL_GROUP = 'xo_pools'
|
||||
|
||||
|
||||
def clean_group_name(label):
|
||||
return label.lower().replace(' ', '-').replace('-', '_')
|
||||
|
||||
|
||||
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
||||
''' Host inventory parser for ansible using XenOrchestra as source. '''
|
||||
|
||||
NAME = 'community.general.xen_orchestra'
|
||||
|
||||
def __init__(self):
|
||||
|
||||
super(InventoryModule, self).__init__()
|
||||
|
||||
# from config
|
||||
self.counter = -1
|
||||
self.session = None
|
||||
self.cache_key = None
|
||||
self.use_cache = None
|
||||
|
||||
@property
|
||||
def pointer(self):
|
||||
self.counter += 1
|
||||
return self.counter
|
||||
|
||||
def create_connection(self, xoa_api_host):
|
||||
validate_certs = self.get_option('validate_certs')
|
||||
use_ssl = self.get_option('use_ssl')
|
||||
proto = 'wss' if use_ssl else 'ws'
|
||||
|
||||
sslopt = None if validate_certs else {'cert_reqs': ssl.CERT_NONE}
|
||||
self.conn = create_connection(
|
||||
'{0}://{1}/api/'.format(proto, xoa_api_host), sslopt=sslopt)
|
||||
|
||||
def login(self, user, password):
|
||||
payload = {'id': self.pointer, 'jsonrpc': '2.0', 'method': 'session.signIn', 'params': {
|
||||
'username': user, 'password': password}}
|
||||
self.conn.send(json.dumps(payload))
|
||||
result = json.loads(self.conn.recv())
|
||||
|
||||
if 'error' in result:
|
||||
raise AnsibleError(
|
||||
'Could not connect: {0}'.format(result['error']))
|
||||
|
||||
def get_object(self, name):
|
||||
payload = {'id': self.pointer, 'jsonrpc': '2.0',
|
||||
'method': 'xo.getAllObjects', 'params': {'filter': {'type': name}}}
|
||||
self.conn.send(json.dumps(payload))
|
||||
answer = json.loads(self.conn.recv())
|
||||
|
||||
if 'error' in answer:
|
||||
raise AnsibleError(
|
||||
'Could not request: {0}'.format(answer['error']))
|
||||
|
||||
return answer['result']
|
||||
|
||||
def _get_objects(self):
|
||||
self.create_connection(self.xoa_api_host)
|
||||
self.login(self.xoa_user, self.xoa_password)
|
||||
|
||||
return {
|
||||
'vms': self.get_object('VM'),
|
||||
'pools': self.get_object('pool'),
|
||||
'hosts': self.get_object('host'),
|
||||
}
|
||||
|
||||
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 _add_vms(self, vms, hosts, pools):
|
||||
for uuid, vm in vms.items():
|
||||
group = 'with_ip'
|
||||
ip = vm.get('mainIpAddress')
|
||||
entry_name = uuid
|
||||
power_state = vm['power_state'].lower()
|
||||
pool_name = self._pool_group_name_for_uuid(pools, vm['$poolId'])
|
||||
host_name = self._host_group_name_for_uuid(hosts, vm['$container'])
|
||||
|
||||
self.inventory.add_host(entry_name)
|
||||
|
||||
# Grouping by power state
|
||||
self.inventory.add_child(power_state, entry_name)
|
||||
|
||||
# Grouping by host
|
||||
if host_name:
|
||||
self.inventory.add_child(host_name, entry_name)
|
||||
|
||||
# Grouping by pool
|
||||
if pool_name:
|
||||
self.inventory.add_child(pool_name, entry_name)
|
||||
|
||||
# Grouping VMs with an IP together
|
||||
if ip is None:
|
||||
group = 'without_ip'
|
||||
self.inventory.add_group(group)
|
||||
self.inventory.add_child(group, entry_name)
|
||||
|
||||
# Adding meta
|
||||
self.inventory.set_variable(entry_name, 'uuid', uuid)
|
||||
self.inventory.set_variable(entry_name, 'ip', ip)
|
||||
self.inventory.set_variable(entry_name, 'ansible_host', ip)
|
||||
self.inventory.set_variable(entry_name, 'power_state', power_state)
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'name_label', vm['name_label'])
|
||||
self.inventory.set_variable(entry_name, 'type', vm['type'])
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'cpus', vm['CPUs']['number'])
|
||||
self.inventory.set_variable(entry_name, 'tags', vm['tags'])
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'memory', vm['memory']['size'])
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'has_ip', group == 'with_ip')
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'is_managed', vm.get('managementAgentDetected', False))
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'os_version', vm['os_version'])
|
||||
|
||||
self._apply_constructable(entry_name, self.inventory.get_host(entry_name).get_vars())
|
||||
|
||||
def _add_hosts(self, hosts, pools):
|
||||
for host in hosts.values():
|
||||
entry_name = host['uuid']
|
||||
group_name = 'xo_host_{0}'.format(
|
||||
clean_group_name(host['name_label']))
|
||||
pool_name = self._pool_group_name_for_uuid(pools, host['$poolId'])
|
||||
|
||||
self.inventory.add_group(group_name)
|
||||
self.inventory.add_host(entry_name)
|
||||
self.inventory.add_child(HOST_GROUP, entry_name)
|
||||
self.inventory.add_child(pool_name, entry_name)
|
||||
|
||||
self.inventory.set_variable(entry_name, 'enabled', host['enabled'])
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'hostname', host['hostname'])
|
||||
self.inventory.set_variable(entry_name, 'memory', host['memory'])
|
||||
self.inventory.set_variable(entry_name, 'address', host['address'])
|
||||
self.inventory.set_variable(entry_name, 'cpus', host['cpus'])
|
||||
self.inventory.set_variable(entry_name, 'type', 'host')
|
||||
self.inventory.set_variable(entry_name, 'tags', host['tags'])
|
||||
self.inventory.set_variable(entry_name, 'version', host['version'])
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'power_state', host['power_state'].lower())
|
||||
self.inventory.set_variable(
|
||||
entry_name, 'product_brand', host['productBrand'])
|
||||
|
||||
for pool in pools.values():
|
||||
group_name = 'xo_pool_{0}'.format(
|
||||
clean_group_name(pool['name_label']))
|
||||
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
def _add_pools(self, pools):
|
||||
for pool in pools.values():
|
||||
group_name = 'xo_pool_{0}'.format(
|
||||
clean_group_name(pool['name_label']))
|
||||
|
||||
self.inventory.add_group(group_name)
|
||||
|
||||
# TODO: Refactor
|
||||
def _pool_group_name_for_uuid(self, pools, pool_uuid):
|
||||
for pool in pools:
|
||||
if pool == pool_uuid:
|
||||
return 'xo_pool_{0}'.format(
|
||||
clean_group_name(pools[pool_uuid]['name_label']))
|
||||
|
||||
# TODO: Refactor
|
||||
def _host_group_name_for_uuid(self, hosts, host_uuid):
|
||||
for host in hosts:
|
||||
if host == host_uuid:
|
||||
return 'xo_host_{0}'.format(
|
||||
clean_group_name(hosts[host_uuid]['name_label']
|
||||
))
|
||||
|
||||
def _populate(self, objects):
|
||||
# Prepare general groups
|
||||
self.inventory.add_group(HOST_GROUP)
|
||||
self.inventory.add_group(POOL_GROUP)
|
||||
for group in POWER_STATES:
|
||||
self.inventory.add_group(group.lower())
|
||||
|
||||
self._add_pools(objects['pools'])
|
||||
self._add_hosts(objects['hosts'], objects['pools'])
|
||||
self._add_vms(objects['vms'], objects['hosts'], objects['pools'])
|
||||
|
||||
def verify_file(self, path):
|
||||
|
||||
valid = False
|
||||
if super(InventoryModule, self).verify_file(path):
|
||||
if path.endswith(('xen_orchestra.yaml', 'xen_orchestra.yml')):
|
||||
valid = True
|
||||
else:
|
||||
self.display.vvv(
|
||||
'Skipping due to inventory source not ending in "xen_orchestra.yaml" nor "xen_orchestra.yml"')
|
||||
return valid
|
||||
|
||||
def parse(self, inventory, loader, path, cache=True):
|
||||
if not HAS_WEBSOCKET:
|
||||
raise AnsibleError('This plugin requires websocket-client 1.0.0 or higher: '
|
||||
'https://github.com/websocket-client/websocket-client.')
|
||||
|
||||
super(InventoryModule, self).parse(inventory, loader, path)
|
||||
|
||||
# read config from file, this sets 'options'
|
||||
self._read_config_data(path)
|
||||
self.inventory = inventory
|
||||
|
||||
self.protocol = 'wss'
|
||||
self.xoa_api_host = self.get_option('api_host')
|
||||
self.xoa_user = self.get_option('user')
|
||||
self.xoa_password = self.get_option('password')
|
||||
self.cache_key = self.get_cache_key(path)
|
||||
self.use_cache = cache and self.get_option('cache')
|
||||
|
||||
self.validate_certs = self.get_option('validate_certs')
|
||||
if not self.get_option('use_ssl'):
|
||||
self.protocol = 'ws'
|
||||
|
||||
objects = self._get_objects()
|
||||
self._populate(objects)
|
||||
@@ -93,7 +93,7 @@ DOCUMENTATION = '''
|
||||
environment variable and keep I(endpoints), I(host), and I(port) unused.
|
||||
seealso:
|
||||
- module: community.general.etcd3
|
||||
- ref: etcd_lookup
|
||||
- ref: ansible_collections.community.general.etcd_lookup
|
||||
description: The etcd v2 lookup.
|
||||
|
||||
requirements:
|
||||
|
||||
@@ -23,7 +23,7 @@ DOCUMENTATION = '''
|
||||
EXAMPLES = """
|
||||
- name: "'unnest' all elements into single list"
|
||||
ansible.builtin.debug:
|
||||
msg: "all in one list {{lookup('community.general.flattened', [1,2,3,[5,6]], [a,b,c], [[5,6,1,3], [34,a,b,c]])}}"
|
||||
msg: "all in one list {{lookup('community.general.flattened', [1,2,3,[5,6]], ['a','b','c'], [[5,6,1,3], [34,'a','b','c']])}}"
|
||||
"""
|
||||
|
||||
RETURN = """
|
||||
|
||||
@@ -141,9 +141,9 @@ import time
|
||||
import yaml
|
||||
|
||||
|
||||
from distutils import util
|
||||
from ansible.errors import AnsibleError, AnsibleAssertionError
|
||||
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
|
||||
from ansible.module_utils.parsing.convert_bool import boolean
|
||||
from ansible.utils.display import Display
|
||||
from ansible.utils.encrypt import random_password
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
@@ -211,7 +211,7 @@ class LookupModule(LookupBase):
|
||||
try:
|
||||
for key in ['create', 'returnall', 'overwrite', 'backup', 'nosymbols']:
|
||||
if not isinstance(self.paramvals[key], bool):
|
||||
self.paramvals[key] = util.strtobool(self.paramvals[key])
|
||||
self.paramvals[key] = boolean(self.paramvals[key])
|
||||
except (ValueError, AssertionError) as e:
|
||||
raise AnsibleError(e)
|
||||
if self.paramvals['missing'] not in ['error', 'warn', 'create', 'empty']:
|
||||
|
||||
107
plugins/lookup/revbitspss.py
Normal file
107
plugins/lookup/revbitspss.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright: (c) 2021, RevBits <info@revbits.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"""
|
||||
name: revbitspss
|
||||
author: RevBits (@RevBits) <info@revbits.com>
|
||||
short_description: Get secrets from RevBits PAM server
|
||||
version_added: 4.1.0
|
||||
description:
|
||||
- Uses the revbits_ansible Python SDK to get Secrets from RevBits PAM
|
||||
Server using API key authentication with the REST API.
|
||||
requirements:
|
||||
- revbits_ansible - U(https://pypi.org/project/revbits_ansible/)
|
||||
options:
|
||||
_terms:
|
||||
description:
|
||||
- This will be an array of keys for secrets which you want to fetch from RevBits PAM.
|
||||
required: true
|
||||
type: list
|
||||
elements: string
|
||||
base_url:
|
||||
description:
|
||||
- This will be the base URL of the server, for example C(https://server-url-here).
|
||||
required: true
|
||||
type: string
|
||||
api_key:
|
||||
description:
|
||||
- This will be the API key for authentication. You can get it from the RevBits PAM secret manager module.
|
||||
required: true
|
||||
type: string
|
||||
"""
|
||||
|
||||
RETURN = r"""
|
||||
_list:
|
||||
description:
|
||||
- The JSON responses which you can access with defined keys.
|
||||
- If you are fetching secrets named as UUID, PASSWORD it will gives you the dict of all secrets.
|
||||
type: list
|
||||
elements: dict
|
||||
"""
|
||||
|
||||
EXAMPLES = r"""
|
||||
- hosts: localhost
|
||||
vars:
|
||||
secret: >-
|
||||
{{
|
||||
lookup(
|
||||
'community.general.revbitspss',
|
||||
'UUIDPAM', 'DB_PASS',
|
||||
base_url='https://server-url-here',
|
||||
api_key='API_KEY_GOES_HERE'
|
||||
)
|
||||
}}
|
||||
tasks:
|
||||
- ansible.builtin.debug:
|
||||
msg: >
|
||||
UUIDPAM is {{ (secret['UUIDPAM']) }} and DB_PASS is {{ (secret['DB_PASS']) }}
|
||||
"""
|
||||
|
||||
from ansible.plugins.lookup import LookupBase
|
||||
from ansible.utils.display import Display
|
||||
from ansible.errors import AnsibleError
|
||||
from ansible.module_utils.six import raise_from
|
||||
|
||||
try:
|
||||
from pam.revbits_ansible.server import SecretServer
|
||||
except ImportError as imp_exc:
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR = imp_exc
|
||||
else:
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR = None
|
||||
|
||||
|
||||
display = Display()
|
||||
|
||||
|
||||
class LookupModule(LookupBase):
|
||||
|
||||
@staticmethod
|
||||
def Client(server_parameters):
|
||||
return SecretServer(**server_parameters)
|
||||
|
||||
def run(self, terms, variables, **kwargs):
|
||||
if ANOTHER_LIBRARY_IMPORT_ERROR:
|
||||
raise_from(
|
||||
AnsibleError('revbits_ansible must be installed to use this plugin'),
|
||||
ANOTHER_LIBRARY_IMPORT_ERROR
|
||||
)
|
||||
self.set_options(var_options=variables, direct=kwargs)
|
||||
secret_server = LookupModule.Client(
|
||||
{
|
||||
"base_url": self.get_option('base_url'),
|
||||
"api_key": self.get_option('api_key'),
|
||||
}
|
||||
)
|
||||
result = []
|
||||
for term in terms:
|
||||
try:
|
||||
display.vvv(u"Secret Server lookup of Secret with ID %s" % term)
|
||||
result.append({term: secret_server.get_pam_secret(term)})
|
||||
except Exception as error:
|
||||
raise AnsibleError("Secret Server lookup failure: %s" % error.message)
|
||||
return result
|
||||
343
plugins/module_utils/_version.py
Normal file
343
plugins/module_utils/_version.py
Normal file
@@ -0,0 +1,343 @@
|
||||
# Vendored copy of distutils/version.py from CPython 3.9.5
|
||||
#
|
||||
# Implements multiple version numbering conventions for the
|
||||
# Python Module Distribution Utilities.
|
||||
#
|
||||
# PSF License (see licenses/PSF-license.txt or https://opensource.org/licenses/Python-2.0)
|
||||
#
|
||||
|
||||
"""Provides classes to represent module version numbers (one class for
|
||||
each style of version numbering). There are currently two such classes
|
||||
implemented: StrictVersion and LooseVersion.
|
||||
|
||||
Every version number class implements the following interface:
|
||||
* the 'parse' method takes a string and parses it to some internal
|
||||
representation; if the string is an invalid version number,
|
||||
'parse' raises a ValueError exception
|
||||
* the class constructor takes an optional string argument which,
|
||||
if supplied, is passed to 'parse'
|
||||
* __str__ reconstructs the string that was passed to 'parse' (or
|
||||
an equivalent string -- ie. one that will generate an equivalent
|
||||
version number instance)
|
||||
* __repr__ generates Python code to recreate the version number instance
|
||||
* _cmp compares the current instance with either another instance
|
||||
of the same class or a string (which will be parsed to an instance
|
||||
of the same class, thus must follow the same rules)
|
||||
"""
|
||||
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import re
|
||||
|
||||
try:
|
||||
RE_FLAGS = re.VERBOSE | re.ASCII
|
||||
except AttributeError:
|
||||
RE_FLAGS = re.VERBOSE
|
||||
|
||||
|
||||
class Version:
|
||||
"""Abstract base class for version numbering classes. Just provides
|
||||
constructor (__init__) and reproducer (__repr__), because those
|
||||
seem to be the same for all version numbering classes; and route
|
||||
rich comparisons to _cmp.
|
||||
"""
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s ('%s')" % (self.__class__.__name__, str(self))
|
||||
|
||||
def __eq__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c == 0
|
||||
|
||||
def __lt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c < 0
|
||||
|
||||
def __le__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c <= 0
|
||||
|
||||
def __gt__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c > 0
|
||||
|
||||
def __ge__(self, other):
|
||||
c = self._cmp(other)
|
||||
if c is NotImplemented:
|
||||
return c
|
||||
return c >= 0
|
||||
|
||||
|
||||
# Interface for version-number classes -- must be implemented
|
||||
# by the following classes (the concrete ones -- Version should
|
||||
# be treated as an abstract class).
|
||||
# __init__ (string) - create and take same action as 'parse'
|
||||
# (string parameter is optional)
|
||||
# parse (string) - convert a string representation to whatever
|
||||
# internal representation is appropriate for
|
||||
# this style of version numbering
|
||||
# __str__ (self) - convert back to a string; should be very similar
|
||||
# (if not identical to) the string supplied to parse
|
||||
# __repr__ (self) - generate Python code to recreate
|
||||
# the instance
|
||||
# _cmp (self, other) - compare two version numbers ('other' may
|
||||
# be an unparsed version string, or another
|
||||
# instance of your version class)
|
||||
|
||||
|
||||
class StrictVersion(Version):
|
||||
"""Version numbering for anal retentives and software idealists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of two or three
|
||||
dot-separated numeric components, with an optional "pre-release" tag
|
||||
on the end. The pre-release tag consists of the letter 'a' or 'b'
|
||||
followed by a number. If the numeric components of two version
|
||||
numbers are equal, then one with a pre-release tag will always
|
||||
be deemed earlier (lesser) than one without.
|
||||
|
||||
The following are valid version numbers (shown in the order that
|
||||
would be obtained by sorting according to the supplied cmp function):
|
||||
|
||||
0.4 0.4.0 (these two are equivalent)
|
||||
0.4.1
|
||||
0.5a1
|
||||
0.5b3
|
||||
0.5
|
||||
0.9.6
|
||||
1.0
|
||||
1.0.4a3
|
||||
1.0.4b1
|
||||
1.0.4
|
||||
|
||||
The following are examples of invalid version numbers:
|
||||
|
||||
1
|
||||
2.7.2.2
|
||||
1.3.a4
|
||||
1.3pl1
|
||||
1.3c4
|
||||
|
||||
The rationale for this version numbering system will be explained
|
||||
in the distutils documentation.
|
||||
"""
|
||||
|
||||
version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
|
||||
RE_FLAGS)
|
||||
|
||||
def parse(self, vstring):
|
||||
match = self.version_re.match(vstring)
|
||||
if not match:
|
||||
raise ValueError("invalid version number '%s'" % vstring)
|
||||
|
||||
(major, minor, patch, prerelease, prerelease_num) = \
|
||||
match.group(1, 2, 4, 5, 6)
|
||||
|
||||
if patch:
|
||||
self.version = tuple(map(int, [major, minor, patch]))
|
||||
else:
|
||||
self.version = tuple(map(int, [major, minor])) + (0,)
|
||||
|
||||
if prerelease:
|
||||
self.prerelease = (prerelease[0], int(prerelease_num))
|
||||
else:
|
||||
self.prerelease = None
|
||||
|
||||
def __str__(self):
|
||||
if self.version[2] == 0:
|
||||
vstring = '.'.join(map(str, self.version[0:2]))
|
||||
else:
|
||||
vstring = '.'.join(map(str, self.version))
|
||||
|
||||
if self.prerelease:
|
||||
vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
|
||||
|
||||
return vstring
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = StrictVersion(other)
|
||||
elif not isinstance(other, StrictVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version != other.version:
|
||||
# numeric versions don't match
|
||||
# prerelease stuff doesn't matter
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
|
||||
# have to compare prerelease
|
||||
# case 1: neither has prerelease; they're equal
|
||||
# case 2: self has prerelease, other doesn't; other is greater
|
||||
# case 3: self doesn't have prerelease, other does: self is greater
|
||||
# case 4: both have prerelease: must compare them!
|
||||
|
||||
if (not self.prerelease and not other.prerelease):
|
||||
return 0
|
||||
elif (self.prerelease and not other.prerelease):
|
||||
return -1
|
||||
elif (not self.prerelease and other.prerelease):
|
||||
return 1
|
||||
elif (self.prerelease and other.prerelease):
|
||||
if self.prerelease == other.prerelease:
|
||||
return 0
|
||||
elif self.prerelease < other.prerelease:
|
||||
return -1
|
||||
else:
|
||||
return 1
|
||||
else:
|
||||
raise AssertionError("never get here")
|
||||
|
||||
# end class StrictVersion
|
||||
|
||||
# The rules according to Greg Stein:
|
||||
# 1) a version number has 1 or more numbers separated by a period or by
|
||||
# sequences of letters. If only periods, then these are compared
|
||||
# left-to-right to determine an ordering.
|
||||
# 2) sequences of letters are part of the tuple for comparison and are
|
||||
# compared lexicographically
|
||||
# 3) recognize the numeric components may have leading zeroes
|
||||
#
|
||||
# The LooseVersion class below implements these rules: a version number
|
||||
# string is split up into a tuple of integer and string components, and
|
||||
# comparison is a simple tuple comparison. This means that version
|
||||
# numbers behave in a predictable and obvious way, but a way that might
|
||||
# not necessarily be how people *want* version numbers to behave. There
|
||||
# wouldn't be a problem if people could stick to purely numeric version
|
||||
# numbers: just split on period and compare the numbers as tuples.
|
||||
# However, people insist on putting letters into their version numbers;
|
||||
# the most common purpose seems to be:
|
||||
# - indicating a "pre-release" version
|
||||
# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
|
||||
# - indicating a post-release patch ('p', 'pl', 'patch')
|
||||
# but of course this can't cover all version number schemes, and there's
|
||||
# no way to know what a programmer means without asking him.
|
||||
#
|
||||
# The problem is what to do with letters (and other non-numeric
|
||||
# characters) in a version number. The current implementation does the
|
||||
# obvious and predictable thing: keep them as strings and compare
|
||||
# lexically within a tuple comparison. This has the desired effect if
|
||||
# an appended letter sequence implies something "post-release":
|
||||
# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
|
||||
#
|
||||
# However, if letters in a version number imply a pre-release version,
|
||||
# the "obvious" thing isn't correct. Eg. you would expect that
|
||||
# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
|
||||
# implemented here, this just isn't so.
|
||||
#
|
||||
# Two possible solutions come to mind. The first is to tie the
|
||||
# comparison algorithm to a particular set of semantic rules, as has
|
||||
# been done in the StrictVersion class above. This works great as long
|
||||
# as everyone can go along with bondage and discipline. Hopefully a
|
||||
# (large) subset of Python module programmers will agree that the
|
||||
# particular flavour of bondage and discipline provided by StrictVersion
|
||||
# provides enough benefit to be worth using, and will submit their
|
||||
# version numbering scheme to its domination. The free-thinking
|
||||
# anarchists in the lot will never give in, though, and something needs
|
||||
# to be done to accommodate them.
|
||||
#
|
||||
# Perhaps a "moderately strict" version class could be implemented that
|
||||
# lets almost anything slide (syntactically), and makes some heuristic
|
||||
# assumptions about non-digits in version number strings. This could
|
||||
# sink into special-case-hell, though; if I was as talented and
|
||||
# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
|
||||
# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
|
||||
# just as happy dealing with things like "2g6" and "1.13++". I don't
|
||||
# think I'm smart enough to do it right though.
|
||||
#
|
||||
# In any case, I've coded the test suite for this module (see
|
||||
# ../test/test_version.py) specifically to fail on things like comparing
|
||||
# "1.2a2" and "1.2". That's not because the *code* is doing anything
|
||||
# wrong, it's because the simple, obvious design doesn't match my
|
||||
# complicated, hairy expectations for real-world version numbers. It
|
||||
# would be a snap to fix the test suite to say, "Yep, LooseVersion does
|
||||
# the Right Thing" (ie. the code matches the conception). But I'd rather
|
||||
# have a conception that matches common notions about version numbers.
|
||||
|
||||
|
||||
class LooseVersion(Version):
|
||||
"""Version numbering for anarchists and software realists.
|
||||
Implements the standard interface for version number classes as
|
||||
described above. A version number consists of a series of numbers,
|
||||
separated by either periods or strings of letters. When comparing
|
||||
version numbers, the numeric components will be compared
|
||||
numerically, and the alphabetic components lexically. The following
|
||||
are all valid version numbers, in no particular order:
|
||||
|
||||
1.5.1
|
||||
1.5.2b2
|
||||
161
|
||||
3.10a
|
||||
8.02
|
||||
3.4j
|
||||
1996.07.12
|
||||
3.2.pl0
|
||||
3.1.1.6
|
||||
2g6
|
||||
11g
|
||||
0.960923
|
||||
2.2beta29
|
||||
1.13++
|
||||
5.5.kw
|
||||
2.0b1pl0
|
||||
|
||||
In fact, there is no such thing as an invalid version number under
|
||||
this scheme; the rules for comparison are simple and predictable,
|
||||
but may not always give the results you want (for some definition
|
||||
of "want").
|
||||
"""
|
||||
|
||||
component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
|
||||
|
||||
def __init__(self, vstring=None):
|
||||
if vstring:
|
||||
self.parse(vstring)
|
||||
|
||||
def parse(self, vstring):
|
||||
# I've given up on thinking I can reconstruct the version string
|
||||
# from the parsed tuple -- so I just store the string here for
|
||||
# use by __str__
|
||||
self.vstring = vstring
|
||||
components = [x for x in self.component_re.split(vstring) if x and x != '.']
|
||||
for i, obj in enumerate(components):
|
||||
try:
|
||||
components[i] = int(obj)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.version = components
|
||||
|
||||
def __str__(self):
|
||||
return self.vstring
|
||||
|
||||
def __repr__(self):
|
||||
return "LooseVersion ('%s')" % str(self)
|
||||
|
||||
def _cmp(self, other):
|
||||
if isinstance(other, str):
|
||||
other = LooseVersion(other)
|
||||
elif not isinstance(other, LooseVersion):
|
||||
return NotImplemented
|
||||
|
||||
if self.version == other.version:
|
||||
return 0
|
||||
if self.version < other.version:
|
||||
return -1
|
||||
if self.version > other.version:
|
||||
return 1
|
||||
|
||||
# end class LooseVersion
|
||||
@@ -7,54 +7,41 @@
|
||||
from __future__ import (absolute_import, division, print_function)
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
from distutils.version import StrictVersion
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from urllib import quote_plus # Python 2.X
|
||||
from urlparse import urljoin
|
||||
except ImportError:
|
||||
from urllib.parse import quote_plus # Python 3+
|
||||
from urllib.parse import quote_plus, urljoin # Python 3+
|
||||
|
||||
import traceback
|
||||
|
||||
GITLAB_IMP_ERR = None
|
||||
try:
|
||||
import gitlab
|
||||
import requests
|
||||
HAS_GITLAB_PACKAGE = True
|
||||
except Exception:
|
||||
GITLAB_IMP_ERR = traceback.format_exc()
|
||||
HAS_GITLAB_PACKAGE = False
|
||||
|
||||
|
||||
def request(module, api_url, project, path, access_token, private_token, rawdata='', method='GET'):
|
||||
url = "%s/v4/projects/%s%s" % (api_url, quote_plus(project), path)
|
||||
headers = {}
|
||||
if access_token:
|
||||
headers['Authorization'] = "Bearer %s" % access_token
|
||||
else:
|
||||
headers['Private-Token'] = private_token
|
||||
|
||||
headers['Accept'] = "application/json"
|
||||
headers['Content-Type'] = "application/json"
|
||||
|
||||
response, info = fetch_url(module=module, url=url, headers=headers, data=rawdata, method=method)
|
||||
status = info['status']
|
||||
content = ""
|
||||
if response:
|
||||
content = response.read()
|
||||
if status == 204:
|
||||
return True, content
|
||||
elif status == 200 or status == 201:
|
||||
return True, json.loads(content)
|
||||
else:
|
||||
return False, str(status) + ": " + content
|
||||
def auth_argument_spec(spec=None):
|
||||
arg_spec = (dict(
|
||||
api_token=dict(type='str', no_log=True),
|
||||
api_oauth_token=dict(type='str', no_log=True),
|
||||
api_job_token=dict(type='str', no_log=True),
|
||||
))
|
||||
if spec:
|
||||
arg_spec.update(spec)
|
||||
return arg_spec
|
||||
|
||||
|
||||
def findProject(gitlab_instance, identifier):
|
||||
def find_project(gitlab_instance, identifier):
|
||||
try:
|
||||
project = gitlab_instance.projects.get(identifier)
|
||||
except Exception as e:
|
||||
@@ -67,7 +54,7 @@ def findProject(gitlab_instance, identifier):
|
||||
return project
|
||||
|
||||
|
||||
def findGroup(gitlab_instance, identifier):
|
||||
def find_group(gitlab_instance, identifier):
|
||||
try:
|
||||
project = gitlab_instance.groups.get(identifier)
|
||||
except Exception as e:
|
||||
@@ -76,12 +63,14 @@ def findGroup(gitlab_instance, identifier):
|
||||
return project
|
||||
|
||||
|
||||
def gitlabAuthentication(module):
|
||||
def gitlab_authentication(module):
|
||||
gitlab_url = module.params['api_url']
|
||||
validate_certs = module.params['validate_certs']
|
||||
gitlab_user = module.params['api_username']
|
||||
gitlab_password = module.params['api_password']
|
||||
gitlab_token = module.params['api_token']
|
||||
gitlab_oauth_token = module.params['api_oauth_token']
|
||||
gitlab_job_token = module.params['api_job_token']
|
||||
|
||||
if not HAS_GITLAB_PACKAGE:
|
||||
module.fail_json(msg=missing_required_lib("python-gitlab"), exception=GITLAB_IMP_ERR)
|
||||
@@ -90,11 +79,20 @@ def gitlabAuthentication(module):
|
||||
# python-gitlab library remove support for username/password authentication since 1.13.0
|
||||
# Changelog : https://github.com/python-gitlab/python-gitlab/releases/tag/v1.13.0
|
||||
# This condition allow to still support older version of the python-gitlab library
|
||||
if StrictVersion(gitlab.__version__) < StrictVersion("1.13.0"):
|
||||
if LooseVersion(gitlab.__version__) < LooseVersion("1.13.0"):
|
||||
gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, email=gitlab_user, password=gitlab_password,
|
||||
private_token=gitlab_token, api_version=4)
|
||||
else:
|
||||
gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, private_token=gitlab_token, api_version=4)
|
||||
# We can create an oauth_token using a username and password
|
||||
# https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow
|
||||
if gitlab_user:
|
||||
data = {'grant_type': 'password', 'username': gitlab_user, 'password': gitlab_password}
|
||||
resp = requests.post(urljoin(gitlab_url, "oauth/token"), data=data, verify=validate_certs)
|
||||
resp_data = resp.json()
|
||||
gitlab_oauth_token = resp_data["access_token"]
|
||||
|
||||
gitlab_instance = gitlab.Gitlab(url=gitlab_url, ssl_verify=validate_certs, private_token=gitlab_token,
|
||||
oauth_token=gitlab_oauth_token, job_token=gitlab_job_token, api_version=4)
|
||||
|
||||
gitlab_instance.auth()
|
||||
except (gitlab.exceptions.GitlabAuthenticationError, gitlab.exceptions.GitlabGetError) as e:
|
||||
|
||||
@@ -38,6 +38,7 @@ from ansible.module_utils.six.moves.urllib.parse import urlencode, quote
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
from ansible.module_utils.common.text.converters import to_native, to_text
|
||||
|
||||
URL_REALM_INFO = "{url}/realms/{realm}"
|
||||
URL_REALMS = "{url}/admin/realms"
|
||||
URL_REALM = "{url}/admin/realms/{realm}"
|
||||
|
||||
@@ -230,6 +231,31 @@ class KeycloakAPI(object):
|
||||
self.validate_certs = self.module.params.get('validate_certs')
|
||||
self.restheaders = connection_header
|
||||
|
||||
def get_realm_info_by_id(self, realm='master'):
|
||||
""" Obtain realm public info by id
|
||||
|
||||
:param realm: realm id
|
||||
:return: dict of real, representation or None if none matching exist
|
||||
"""
|
||||
realm_info_url = URL_REALM_INFO.format(url=self.baseurl, realm=realm)
|
||||
|
||||
try:
|
||||
return json.loads(to_native(open_url(realm_info_url, method='GET', headers=self.restheaders,
|
||||
validate_certs=self.validate_certs).read()))
|
||||
|
||||
except HTTPError as e:
|
||||
if e.code == 404:
|
||||
return None
|
||||
else:
|
||||
self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)),
|
||||
exception=traceback.format_exc())
|
||||
except ValueError as e:
|
||||
self.module.fail_json(msg='API returned incorrect JSON when trying to obtain realm %s: %s' % (realm, str(e)),
|
||||
exception=traceback.format_exc())
|
||||
except Exception as e:
|
||||
self.module.fail_json(msg='Could not obtain realm %s: %s' % (realm, str(e)),
|
||||
exception=traceback.format_exc())
|
||||
|
||||
def get_realm_by_id(self, realm='master'):
|
||||
""" Obtain realm representation by id
|
||||
|
||||
|
||||
232
plugins/module_utils/ilo_redfish_utils.py
Normal file
232
plugins/module_utils/ilo_redfish_utils.py
Normal file
@@ -0,0 +1,232 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright (c) 2021-2022 Hewlett Packard Enterprise, Inc. All rights reserved.
|
||||
# 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
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.redfish_utils import RedfishUtils
|
||||
|
||||
|
||||
class iLORedfishUtils(RedfishUtils):
|
||||
|
||||
def get_ilo_sessions(self):
|
||||
result = {}
|
||||
# listing all users has always been slower than other operations, why?
|
||||
session_list = []
|
||||
sessions_results = []
|
||||
# Get these entries, but does not fail if not found
|
||||
properties = ['Description', 'Id', 'Name', 'UserName']
|
||||
|
||||
# Changed self.sessions_uri to Hardcoded string.
|
||||
response = self.get_request(
|
||||
self.root_uri + self.service_root + "SessionService/Sessions/")
|
||||
if not response['ret']:
|
||||
return response
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
|
||||
if 'Oem' in data:
|
||||
if data["Oem"]["Hpe"]["Links"]["MySession"]["@odata.id"]:
|
||||
current_session = data["Oem"]["Hpe"]["Links"]["MySession"]["@odata.id"]
|
||||
|
||||
for sessions in data[u'Members']:
|
||||
# session_list[] are URIs
|
||||
session_list.append(sessions[u'@odata.id'])
|
||||
# for each session, get details
|
||||
for uri in session_list:
|
||||
session = {}
|
||||
if uri != current_session:
|
||||
response = self.get_request(self.root_uri + uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
data = response['data']
|
||||
for property in properties:
|
||||
if property in data:
|
||||
session[property] = data[property]
|
||||
sessions_results.append(session)
|
||||
result["msg"] = sessions_results
|
||||
result["ret"] = True
|
||||
return result
|
||||
|
||||
def set_ntp_server(self, mgr_attributes):
|
||||
result = {}
|
||||
setkey = mgr_attributes['mgr_attr_name']
|
||||
|
||||
nic_info = self.get_manager_ethernet_uri()
|
||||
ethuri = nic_info["nic_addr"]
|
||||
|
||||
response = self.get_request(self.root_uri + ethuri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
payload = {"DHCPv4": {
|
||||
"UseNTPServers": ""
|
||||
}}
|
||||
|
||||
if data["DHCPv4"]["UseNTPServers"]:
|
||||
payload["DHCPv4"]["UseNTPServers"] = False
|
||||
res_dhv4 = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not res_dhv4['ret']:
|
||||
return res_dhv4
|
||||
|
||||
payload = {"DHCPv6": {
|
||||
"UseNTPServers": ""
|
||||
}}
|
||||
|
||||
if data["DHCPv6"]["UseNTPServers"]:
|
||||
payload["DHCPv6"]["UseNTPServers"] = False
|
||||
res_dhv6 = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not res_dhv6['ret']:
|
||||
return res_dhv6
|
||||
|
||||
datetime_uri = self.manager_uri + "DateTime"
|
||||
|
||||
response = self.get_request(self.root_uri + datetime_uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
data = response['data']
|
||||
|
||||
ntp_list = data[setkey]
|
||||
if(len(ntp_list) == 2):
|
||||
ntp_list.pop(0)
|
||||
|
||||
ntp_list.append(mgr_attributes['mgr_attr_value'])
|
||||
|
||||
payload = {setkey: ntp_list}
|
||||
|
||||
response1 = self.patch_request(self.root_uri + datetime_uri, payload)
|
||||
if not response1['ret']:
|
||||
return response1
|
||||
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified %s" % mgr_attributes['mgr_attr_name']}
|
||||
|
||||
def set_time_zone(self, attr):
|
||||
key = attr['mgr_attr_name']
|
||||
|
||||
uri = self.manager_uri + "DateTime/"
|
||||
response = self.get_request(self.root_uri + uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
data = response["data"]
|
||||
|
||||
if key not in data:
|
||||
return {'ret': False, 'changed': False, 'msg': "Key %s not found" % key}
|
||||
|
||||
timezones = data["TimeZoneList"]
|
||||
index = ""
|
||||
for tz in timezones:
|
||||
if attr['mgr_attr_value'] in tz["Name"]:
|
||||
index = tz["Index"]
|
||||
break
|
||||
|
||||
payload = {key: {"Index": index}}
|
||||
response = self.patch_request(self.root_uri + uri, payload)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified %s" % attr['mgr_attr_name']}
|
||||
|
||||
def set_dns_server(self, attr):
|
||||
key = attr['mgr_attr_name']
|
||||
nic_info = self.get_manager_ethernet_uri()
|
||||
uri = nic_info["nic_addr"]
|
||||
|
||||
response = self.get_request(self.root_uri + uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
data = response['data']
|
||||
|
||||
dns_list = data["Oem"]["Hpe"]["IPv4"][key]
|
||||
|
||||
if len(dns_list) == 3:
|
||||
dns_list.pop(0)
|
||||
|
||||
dns_list.append(attr['mgr_attr_value'])
|
||||
|
||||
payload = {
|
||||
"Oem": {
|
||||
"Hpe": {
|
||||
"IPv4": {
|
||||
key: dns_list
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = self.patch_request(self.root_uri + uri, payload)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified %s" % attr['mgr_attr_name']}
|
||||
|
||||
def set_domain_name(self, attr):
|
||||
key = attr['mgr_attr_name']
|
||||
|
||||
nic_info = self.get_manager_ethernet_uri()
|
||||
ethuri = nic_info["nic_addr"]
|
||||
|
||||
response = self.get_request(self.root_uri + ethuri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
|
||||
data = response['data']
|
||||
|
||||
payload = {"DHCPv4": {
|
||||
"UseDomainName": ""
|
||||
}}
|
||||
|
||||
if data["DHCPv4"]["UseDomainName"]:
|
||||
payload["DHCPv4"]["UseDomainName"] = False
|
||||
res_dhv4 = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not res_dhv4['ret']:
|
||||
return res_dhv4
|
||||
|
||||
payload = {"DHCPv6": {
|
||||
"UseDomainName": ""
|
||||
}}
|
||||
|
||||
if data["DHCPv6"]["UseDomainName"]:
|
||||
payload["DHCPv6"]["UseDomainName"] = False
|
||||
res_dhv6 = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not res_dhv6['ret']:
|
||||
return res_dhv6
|
||||
|
||||
domain_name = attr['mgr_attr_value']
|
||||
|
||||
payload = {"Oem": {
|
||||
"Hpe": {
|
||||
key: domain_name
|
||||
}
|
||||
}}
|
||||
|
||||
response = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not response['ret']:
|
||||
return response
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified %s" % attr['mgr_attr_name']}
|
||||
|
||||
def set_wins_registration(self, mgrattr):
|
||||
Key = mgrattr['mgr_attr_name']
|
||||
|
||||
nic_info = self.get_manager_ethernet_uri()
|
||||
ethuri = nic_info["nic_addr"]
|
||||
|
||||
payload = {
|
||||
"Oem": {
|
||||
"Hpe": {
|
||||
"IPv4": {
|
||||
Key: False
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response = self.patch_request(self.root_uri + ethuri, payload)
|
||||
if not response['ret']:
|
||||
return response
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified %s" % mgrattr['mgr_attr_name']}
|
||||
@@ -9,7 +9,8 @@ __metaclass__ = type
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
|
||||
@@ -75,11 +75,14 @@ class LXDClient(object):
|
||||
else:
|
||||
raise LXDClientException('URL scheme must be unix: or https:')
|
||||
|
||||
def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None):
|
||||
def do(self, method, url, body_json=None, ok_error_codes=None, timeout=None, wait_for_container=None):
|
||||
resp_json = self._send_request(method, url, body_json=body_json, ok_error_codes=ok_error_codes, timeout=timeout)
|
||||
if resp_json['type'] == 'async':
|
||||
url = '{0}/wait'.format(resp_json['operation'])
|
||||
resp_json = self._send_request('GET', url)
|
||||
if wait_for_container:
|
||||
while resp_json['metadata']['status'] == 'Running':
|
||||
resp_json = self._send_request('GET', url)
|
||||
if resp_json['metadata']['status'] != 'Success':
|
||||
self._raise_err_from_json(resp_json)
|
||||
return resp_json
|
||||
|
||||
@@ -52,3 +52,36 @@ def module_fails_on_exception(func):
|
||||
self.module.fail_json(msg=msg, exception=traceback.format_exc(),
|
||||
output=self.output, vars=self.vars.output(), **self.output)
|
||||
return wrapper
|
||||
|
||||
|
||||
def check_mode_skip(func):
|
||||
@wraps(func)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
if not self.module.check_mode:
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def check_mode_skip_returns(callable=None, value=None):
|
||||
|
||||
def deco(func):
|
||||
if callable is not None:
|
||||
@wraps(func)
|
||||
def wrapper_callable(self, *args, **kwargs):
|
||||
if self.module.check_mode:
|
||||
return callable(self, *args, **kwargs)
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper_callable
|
||||
|
||||
if value is not None:
|
||||
@wraps(func)
|
||||
def wrapper_value(self, *args, **kwargs):
|
||||
if self.module.check_mode:
|
||||
return value
|
||||
return func(self, *args, **kwargs)
|
||||
return wrapper_value
|
||||
|
||||
if callable is None and value is None:
|
||||
return check_mode_skip
|
||||
|
||||
return deco
|
||||
|
||||
61
plugins/module_utils/mh/mixins/deprecate_attrs.py
Normal file
61
plugins/module_utils/mh/mixins/deprecate_attrs.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# (c) 2020, Alexei Znamensky <russoz@gmail.com>
|
||||
# Copyright: (c) 2020, Ansible Project
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
|
||||
|
||||
class DeprecateAttrsMixin(object):
|
||||
|
||||
def _deprecate_setup(self, attr, target, module):
|
||||
if target is None:
|
||||
target = self
|
||||
if not hasattr(target, attr):
|
||||
raise ValueError("Target {0} has no attribute {1}".format(target, attr))
|
||||
if module is None:
|
||||
if isinstance(target, AnsibleModule):
|
||||
module = target
|
||||
elif hasattr(target, "module") and isinstance(target.module, AnsibleModule):
|
||||
module = target.module
|
||||
else:
|
||||
raise ValueError("Failed to automatically discover the AnsibleModule instance. Pass 'module' parameter explicitly.")
|
||||
|
||||
# setup internal state dicts
|
||||
value_attr = "__deprecated_attr_value"
|
||||
trigger_attr = "__deprecated_attr_trigger"
|
||||
if not hasattr(target, value_attr):
|
||||
setattr(target, value_attr, {})
|
||||
if not hasattr(target, trigger_attr):
|
||||
setattr(target, trigger_attr, {})
|
||||
value_dict = getattr(target, value_attr)
|
||||
trigger_dict = getattr(target, trigger_attr)
|
||||
return target, module, value_dict, trigger_dict
|
||||
|
||||
def _deprecate_attr(self, attr, msg, version=None, date=None, collection_name=None, target=None, value=None, module=None):
|
||||
target, module, value_dict, trigger_dict = self._deprecate_setup(attr, target, module)
|
||||
|
||||
value_dict[attr] = getattr(target, attr, value)
|
||||
trigger_dict[attr] = False
|
||||
|
||||
def _trigger():
|
||||
if not trigger_dict[attr]:
|
||||
module.deprecate(msg, version=version, date=date, collection_name=collection_name)
|
||||
trigger_dict[attr] = True
|
||||
|
||||
def _getter(_self):
|
||||
_trigger()
|
||||
return value_dict[attr]
|
||||
|
||||
def _setter(_self, new_value):
|
||||
_trigger()
|
||||
value_dict[attr] = new_value
|
||||
|
||||
# override attribute
|
||||
prop = property(_getter)
|
||||
setattr(target, attr, prop)
|
||||
setattr(target, "_{0}_setter".format(attr), prop.setter(_setter))
|
||||
@@ -13,9 +13,10 @@ from ansible_collections.community.general.plugins.module_utils.mh.mixins.cmd im
|
||||
from ansible_collections.community.general.plugins.module_utils.mh.mixins.state import StateMixin
|
||||
from ansible_collections.community.general.plugins.module_utils.mh.mixins.deps import DependencyMixin
|
||||
from ansible_collections.community.general.plugins.module_utils.mh.mixins.vars import VarsMixin, VarDict as _VD
|
||||
from ansible_collections.community.general.plugins.module_utils.mh.mixins.deprecate_attrs import DeprecateAttrsMixin
|
||||
|
||||
|
||||
class ModuleHelper(VarsMixin, DependencyMixin, ModuleHelperBase):
|
||||
class ModuleHelper(DeprecateAttrsMixin, VarsMixin, DependencyMixin, ModuleHelperBase):
|
||||
_output_conflict_list = ('msg', 'exception', 'output', 'vars', 'changed')
|
||||
facts_name = None
|
||||
output_params = ()
|
||||
@@ -36,6 +37,15 @@ class ModuleHelper(VarsMixin, DependencyMixin, ModuleHelperBase):
|
||||
fact=name in self.facts_params,
|
||||
)
|
||||
|
||||
self._deprecate_attr(
|
||||
attr="VarDict",
|
||||
msg="ModuleHelper.VarDict attribute is deprecated, use VarDict from "
|
||||
"the ansible_collections.community.general.plugins.module_utils.mh.mixins.vars module instead",
|
||||
version="6.0.0",
|
||||
collection_name="community.general",
|
||||
target=ModuleHelper,
|
||||
module=self.module)
|
||||
|
||||
def update_output(self, **kwargs):
|
||||
self.update_vars(meta={"output": True}, **kwargs)
|
||||
|
||||
|
||||
@@ -54,9 +54,23 @@ def proxmox_to_ansible_bool(value):
|
||||
return True if value == 1 else False
|
||||
|
||||
|
||||
def ansible_to_proxmox_bool(value):
|
||||
'''Convert Ansible representation of a boolean to be proxmox-friendly'''
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
if not isinstance(value, bool):
|
||||
raise ValueError("%s must be of type bool not %s" % (value, type(value)))
|
||||
|
||||
return 1 if value else 0
|
||||
|
||||
|
||||
class ProxmoxAnsible(object):
|
||||
"""Base class for Proxmox modules"""
|
||||
def __init__(self, module):
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
self.module = module
|
||||
self.proxmox_api = self._connect()
|
||||
# Test token validity
|
||||
|
||||
@@ -1834,12 +1834,16 @@ class RedfishUtils(object):
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
|
||||
for device in data[u'Fans']:
|
||||
fan = {}
|
||||
for property in properties:
|
||||
if property in device:
|
||||
fan[property] = device[property]
|
||||
fan_results.append(fan)
|
||||
# Checking if fans are present
|
||||
if u'Fans' in data:
|
||||
for device in data[u'Fans']:
|
||||
fan = {}
|
||||
for property in properties:
|
||||
if property in device:
|
||||
fan[property] = device[property]
|
||||
fan_results.append(fan)
|
||||
else:
|
||||
return {'ret': False, 'msg': "No Fans present"}
|
||||
result["entries"] = fan_results
|
||||
return result
|
||||
|
||||
@@ -2029,15 +2033,28 @@ class RedfishUtils(object):
|
||||
def get_multi_memory_inventory(self):
|
||||
return self.aggregate_systems(self.get_memory_inventory)
|
||||
|
||||
def get_nic(self, resource_uri):
|
||||
result = {}
|
||||
properties = ['Name', 'Id', 'Description', 'FQDN', 'IPv4Addresses', 'IPv6Addresses',
|
||||
'NameServers', 'MACAddress', 'PermanentMACAddress',
|
||||
'SpeedMbps', 'MTUSize', 'AutoNeg', 'Status']
|
||||
response = self.get_request(self.root_uri + resource_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
nic = {}
|
||||
for property in properties:
|
||||
if property in data:
|
||||
nic[property] = data[property]
|
||||
result['entries'] = nic
|
||||
return(result)
|
||||
|
||||
def get_nic_inventory(self, resource_uri):
|
||||
result = {}
|
||||
nic_list = []
|
||||
nic_results = []
|
||||
key = "EthernetInterfaces"
|
||||
# Get these entries, but does not fail if not found
|
||||
properties = ['Name', 'Id', 'Description', 'FQDN', 'IPv4Addresses', 'IPv6Addresses',
|
||||
'NameServers', 'MACAddress', 'PermanentMACAddress',
|
||||
'SpeedMbps', 'MTUSize', 'AutoNeg', 'Status']
|
||||
|
||||
response = self.get_request(self.root_uri + resource_uri)
|
||||
if response['ret'] is False:
|
||||
@@ -2061,18 +2078,9 @@ class RedfishUtils(object):
|
||||
nic_list.append(nic[u'@odata.id'])
|
||||
|
||||
for n in nic_list:
|
||||
nic = {}
|
||||
uri = self.root_uri + n
|
||||
response = self.get_request(uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
|
||||
for property in properties:
|
||||
if property in data:
|
||||
nic[property] = data[property]
|
||||
|
||||
nic_results.append(nic)
|
||||
nic = self.get_nic(n)
|
||||
if nic['ret']:
|
||||
nic_results.append(nic['entries'])
|
||||
result["entries"] = nic_results
|
||||
return result
|
||||
|
||||
@@ -2697,39 +2705,14 @@ class RedfishUtils(object):
|
||||
return self.aggregate_managers(self.get_manager_health_report)
|
||||
|
||||
def set_manager_nic(self, nic_addr, nic_config):
|
||||
# Get EthernetInterface collection
|
||||
response = self.get_request(self.root_uri + self.manager_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
if 'EthernetInterfaces' not in data:
|
||||
return {'ret': False, 'msg': "EthernetInterfaces resource not found"}
|
||||
ethernetinterfaces_uri = data["EthernetInterfaces"]["@odata.id"]
|
||||
response = self.get_request(self.root_uri + ethernetinterfaces_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
uris = [a.get('@odata.id') for a in data.get('Members', []) if
|
||||
a.get('@odata.id')]
|
||||
# Get the manager ethernet interface uri
|
||||
nic_info = self.get_manager_ethernet_uri(nic_addr)
|
||||
|
||||
# Find target EthernetInterface
|
||||
target_ethernet_uri = None
|
||||
target_ethernet_current_setting = None
|
||||
if nic_addr == 'null':
|
||||
# Find root_uri matched EthernetInterface when nic_addr is not specified
|
||||
nic_addr = (self.root_uri).split('/')[-1]
|
||||
nic_addr = nic_addr.split(':')[0] # split port if existing
|
||||
for uri in uris:
|
||||
response = self.get_request(self.root_uri + uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
if '"' + nic_addr.lower() + '"' in str(data).lower() or "'" + nic_addr.lower() + "'" in str(data).lower():
|
||||
target_ethernet_uri = uri
|
||||
target_ethernet_current_setting = data
|
||||
break
|
||||
if target_ethernet_uri is None:
|
||||
return {'ret': False, 'msg': "No matched EthernetInterface found under Manager"}
|
||||
if nic_info.get('nic_addr') is None:
|
||||
return nic_info
|
||||
else:
|
||||
target_ethernet_uri = nic_info['nic_addr']
|
||||
target_ethernet_current_setting = nic_info['ethernet_setting']
|
||||
|
||||
# Convert input to payload and check validity
|
||||
payload = {}
|
||||
@@ -2792,3 +2775,208 @@ class RedfishUtils(object):
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified Manager NIC"}
|
||||
|
||||
# A helper function to get the EthernetInterface URI
|
||||
def get_manager_ethernet_uri(self, nic_addr='null'):
|
||||
# Get EthernetInterface collection
|
||||
response = self.get_request(self.root_uri + self.manager_uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
data = response['data']
|
||||
if 'EthernetInterfaces' not in data:
|
||||
return {'ret': False, 'msg': "EthernetInterfaces resource not found"}
|
||||
ethernetinterfaces_uri = data["EthernetInterfaces"]["@odata.id"]
|
||||
response = self.get_request(self.root_uri + ethernetinterfaces_uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
data = response['data']
|
||||
uris = [a.get('@odata.id') for a in data.get('Members', []) if
|
||||
a.get('@odata.id')]
|
||||
|
||||
# Find target EthernetInterface
|
||||
target_ethernet_uri = None
|
||||
target_ethernet_current_setting = None
|
||||
if nic_addr == 'null':
|
||||
# Find root_uri matched EthernetInterface when nic_addr is not specified
|
||||
nic_addr = (self.root_uri).split('/')[-1]
|
||||
nic_addr = nic_addr.split(':')[0] # split port if existing
|
||||
for uri in uris:
|
||||
response = self.get_request(self.root_uri + uri)
|
||||
if not response['ret']:
|
||||
return response
|
||||
data = response['data']
|
||||
data_string = json.dumps(data)
|
||||
if nic_addr.lower() in data_string.lower():
|
||||
target_ethernet_uri = uri
|
||||
target_ethernet_current_setting = data
|
||||
break
|
||||
|
||||
nic_info = {}
|
||||
nic_info['nic_addr'] = target_ethernet_uri
|
||||
nic_info['ethernet_setting'] = target_ethernet_current_setting
|
||||
|
||||
if target_ethernet_uri is None:
|
||||
return {}
|
||||
else:
|
||||
return nic_info
|
||||
|
||||
def set_hostinterface_attributes(self, hostinterface_config, hostinterface_id=None):
|
||||
response = self.get_request(self.root_uri + self.manager_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
if 'HostInterfaces' not in data:
|
||||
return {'ret': False, 'msg': "HostInterfaces resource not found"}
|
||||
|
||||
hostinterfaces_uri = data["HostInterfaces"]["@odata.id"]
|
||||
response = self.get_request(self.root_uri + hostinterfaces_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
data = response['data']
|
||||
uris = [a.get('@odata.id') for a in data.get('Members', []) if a.get('@odata.id')]
|
||||
# Capture list of URIs that match a specified HostInterface resource ID
|
||||
if hostinterface_id:
|
||||
matching_hostinterface_uris = [uri for uri in uris if hostinterface_id in uri.split('/')[-1]]
|
||||
|
||||
if hostinterface_id and matching_hostinterface_uris:
|
||||
hostinterface_uri = list.pop(matching_hostinterface_uris)
|
||||
elif hostinterface_id and not matching_hostinterface_uris:
|
||||
return {'ret': False, 'msg': "HostInterface ID %s not present." % hostinterface_id}
|
||||
elif len(uris) == 1:
|
||||
hostinterface_uri = list.pop(uris)
|
||||
else:
|
||||
return {'ret': False, 'msg': "HostInterface ID not defined and multiple interfaces detected."}
|
||||
|
||||
response = self.get_request(self.root_uri + hostinterface_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
current_hostinterface_config = response['data']
|
||||
payload = {}
|
||||
for property in hostinterface_config.keys():
|
||||
value = hostinterface_config[property]
|
||||
if property not in current_hostinterface_config:
|
||||
return {'ret': False, 'msg': "Property %s in hostinterface_config is invalid" % property}
|
||||
if isinstance(value, dict):
|
||||
if isinstance(current_hostinterface_config[property], dict):
|
||||
payload[property] = value
|
||||
elif isinstance(current_hostinterface_config[property], list):
|
||||
payload[property] = list()
|
||||
payload[property].append(value)
|
||||
else:
|
||||
return {'ret': False, 'msg': "Value of property %s in hostinterface_config is invalid" % property}
|
||||
else:
|
||||
payload[property] = value
|
||||
|
||||
need_change = False
|
||||
for property in payload.keys():
|
||||
set_value = payload[property]
|
||||
cur_value = current_hostinterface_config[property]
|
||||
if not isinstance(set_value, dict) and not isinstance(set_value, list):
|
||||
if set_value != cur_value:
|
||||
need_change = True
|
||||
if isinstance(set_value, dict):
|
||||
for subprop in payload[property].keys():
|
||||
if subprop not in current_hostinterface_config[property]:
|
||||
need_change = True
|
||||
break
|
||||
sub_set_value = payload[property][subprop]
|
||||
sub_cur_value = current_hostinterface_config[property][subprop]
|
||||
if sub_set_value != sub_cur_value:
|
||||
need_change = True
|
||||
if isinstance(set_value, list):
|
||||
if len(set_value) != len(cur_value):
|
||||
need_change = True
|
||||
continue
|
||||
for i in range(len(set_value)):
|
||||
for subprop in payload[property][i].keys():
|
||||
if subprop not in current_hostinterface_config[property][i]:
|
||||
need_change = True
|
||||
break
|
||||
sub_set_value = payload[property][i][subprop]
|
||||
sub_cur_value = current_hostinterface_config[property][i][subprop]
|
||||
if sub_set_value != sub_cur_value:
|
||||
need_change = True
|
||||
if not need_change:
|
||||
return {'ret': True, 'changed': False, 'msg': "Host Interface already configured"}
|
||||
|
||||
response = self.patch_request(self.root_uri + hostinterface_uri, payload)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
return {'ret': True, 'changed': True, 'msg': "Modified Host Interface"}
|
||||
|
||||
def get_hostinterfaces(self):
|
||||
result = {}
|
||||
hostinterface_results = []
|
||||
properties = ['Id', 'Name', 'Description', 'HostInterfaceType', 'Status',
|
||||
'InterfaceEnabled', 'ExternallyAccessible', 'AuthenticationModes',
|
||||
'AuthNoneRoleId', 'CredentialBootstrapping']
|
||||
manager_uri_list = self.manager_uris
|
||||
for manager_uri in manager_uri_list:
|
||||
response = self.get_request(self.root_uri + manager_uri)
|
||||
if response['ret'] is False:
|
||||
return response
|
||||
|
||||
result['ret'] = True
|
||||
data = response['data']
|
||||
|
||||
if 'HostInterfaces' in data:
|
||||
hostinterfaces_uri = data[u'HostInterfaces'][u'@odata.id']
|
||||
else:
|
||||
continue
|
||||
|
||||
response = self.get_request(self.root_uri + hostinterfaces_uri)
|
||||
data = response['data']
|
||||
|
||||
if 'Members' in data:
|
||||
for hostinterface in data['Members']:
|
||||
hostinterface_uri = hostinterface['@odata.id']
|
||||
hostinterface_response = self.get_request(self.root_uri + hostinterface_uri)
|
||||
# dictionary for capturing individual HostInterface properties
|
||||
hostinterface_data_temp = {}
|
||||
if hostinterface_response['ret'] is False:
|
||||
return hostinterface_response
|
||||
hostinterface_data = hostinterface_response['data']
|
||||
for property in properties:
|
||||
if property in hostinterface_data:
|
||||
if hostinterface_data[property] is not None:
|
||||
hostinterface_data_temp[property] = hostinterface_data[property]
|
||||
# Check for the presence of a ManagerEthernetInterface
|
||||
# object, a link to a _single_ EthernetInterface that the
|
||||
# BMC uses to communicate with the host.
|
||||
if 'ManagerEthernetInterface' in hostinterface_data:
|
||||
interface_uri = hostinterface_data['ManagerEthernetInterface']['@odata.id']
|
||||
interface_response = self.get_nic(interface_uri)
|
||||
if interface_response['ret'] is False:
|
||||
return interface_response
|
||||
hostinterface_data_temp['ManagerEthernetInterface'] = interface_response['entries']
|
||||
|
||||
# Check for the presence of a HostEthernetInterfaces
|
||||
# object, a link to a _collection_ of EthernetInterfaces
|
||||
# that the host uses to communicate with the BMC.
|
||||
if 'HostEthernetInterfaces' in hostinterface_data:
|
||||
interfaces_uri = hostinterface_data['HostEthernetInterfaces']['@odata.id']
|
||||
interfaces_response = self.get_request(self.root_uri + interfaces_uri)
|
||||
if interfaces_response['ret'] is False:
|
||||
return interfaces_response
|
||||
interfaces_data = interfaces_response['data']
|
||||
if 'Members' in interfaces_data:
|
||||
for interface in interfaces_data['Members']:
|
||||
interface_uri = interface['@odata.id']
|
||||
interface_response = self.get_nic(interface_uri)
|
||||
if interface_response['ret'] is False:
|
||||
return interface_response
|
||||
# Check if this is the first
|
||||
# HostEthernetInterfaces item and create empty
|
||||
# list if so.
|
||||
if 'HostEthernetInterfaces' not in hostinterface_data_temp:
|
||||
hostinterface_data_temp['HostEthernetInterfaces'] = []
|
||||
|
||||
hostinterface_data_temp['HostEthernetInterfaces'].append(interface_response['entries'])
|
||||
|
||||
hostinterface_results.append(hostinterface_data_temp)
|
||||
else:
|
||||
continue
|
||||
result["entries"] = hostinterface_results
|
||||
if not result["entries"]:
|
||||
return {'ret': False, 'msg': "No HostInterface objects found"}
|
||||
return result
|
||||
|
||||
17
plugins/module_utils/version.py
Normal file
17
plugins/module_utils/version.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- 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)
|
||||
|
||||
"""Provide version object to compare version numbers."""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
|
||||
# remove the _version.py file, and replace the following import by
|
||||
#
|
||||
# from ansible.module_utils.compat.version import LooseVersion
|
||||
|
||||
from ._version import LooseVersion
|
||||
1
plugins/modules/cargo.py
Symbolic link
1
plugins/modules/cargo.py
Symbolic link
@@ -0,0 +1 @@
|
||||
packaging/language/cargo.py
|
||||
@@ -120,7 +120,7 @@ __version__ = '${version}'
|
||||
import os
|
||||
import traceback
|
||||
|
||||
from distutils.version import LooseVersion
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
|
||||
@@ -161,7 +161,8 @@ __version__ = '${version}'
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
|
||||
@@ -89,7 +89,8 @@ __version__ = '${version}'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -132,8 +133,7 @@ class ClcBlueprintPackage:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -162,7 +162,8 @@ import os
|
||||
import traceback
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlparse
|
||||
from time import sleep
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -203,8 +204,7 @@ class ClcFirewallPolicy:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -207,7 +207,8 @@ __version__ = '${version}'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
|
||||
@@ -210,7 +210,8 @@ import json
|
||||
import os
|
||||
import traceback
|
||||
from time import sleep
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -255,8 +256,7 @@ class ClcLoadBalancer:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -311,7 +311,8 @@ __version__ = '${version}'
|
||||
import json
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -355,8 +356,7 @@ class ClcModifyServer:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -117,7 +117,8 @@ __version__ = '${version}'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
|
||||
@@ -433,7 +433,8 @@ import json
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -478,8 +479,7 @@ class ClcServer:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -101,7 +101,8 @@ __version__ = '${version}'
|
||||
|
||||
import os
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
REQUESTS_IMP_ERR = None
|
||||
try:
|
||||
@@ -145,8 +146,7 @@ class ClcSnapshot:
|
||||
self.module.fail_json(msg=missing_required_lib('clc-sdk'), exception=CLC_IMP_ERR)
|
||||
if not REQUESTS_FOUND:
|
||||
self.module.fail_json(msg=missing_required_lib('requests'), exception=REQUESTS_IMP_ERR)
|
||||
if requests.__version__ and LooseVersion(
|
||||
requests.__version__) < LooseVersion('2.5.0'):
|
||||
if requests.__version__ and LooseVersion(requests.__version__) < LooseVersion('2.5.0'):
|
||||
self.module.fail_json(
|
||||
msg='requests library version should be >= 2.5.0')
|
||||
|
||||
|
||||
@@ -422,6 +422,7 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import shlex
|
||||
|
||||
try:
|
||||
import lxc
|
||||
@@ -661,9 +662,8 @@ class LxcContainerManagement(object):
|
||||
"""
|
||||
|
||||
for key, value in variables_dict.items():
|
||||
build_command.append(
|
||||
'%s %s' % (key, value)
|
||||
)
|
||||
build_command.append(str(key))
|
||||
build_command.append(str(value))
|
||||
return build_command
|
||||
|
||||
def _get_vars(self, variables):
|
||||
@@ -686,24 +686,6 @@ class LxcContainerManagement(object):
|
||||
return_dict[v] = _var
|
||||
return return_dict
|
||||
|
||||
def _run_command(self, build_command, unsafe_shell=False):
|
||||
"""Return information from running an Ansible Command.
|
||||
|
||||
This will squash the build command list into a string and then
|
||||
execute the command via Ansible. The output is returned to the method.
|
||||
This output is returned as `return_code`, `stdout`, `stderr`.
|
||||
|
||||
:param build_command: Used for the command and all options.
|
||||
:type build_command: ``list``
|
||||
:param unsafe_shell: Enable or Disable unsafe sell commands.
|
||||
:type unsafe_shell: ``bol``
|
||||
"""
|
||||
|
||||
return self.module.run_command(
|
||||
' '.join(build_command),
|
||||
use_unsafe_shell=unsafe_shell
|
||||
)
|
||||
|
||||
def _config(self):
|
||||
"""Configure an LXC container.
|
||||
|
||||
@@ -810,7 +792,7 @@ class LxcContainerManagement(object):
|
||||
elif self.module.params.get('backing_store') == 'overlayfs':
|
||||
build_command.append('--snapshot')
|
||||
|
||||
rc, return_data, err = self._run_command(build_command)
|
||||
rc, return_data, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
message = "Failed executing %s." % os.path.basename(clone_cmd)
|
||||
self.failure(
|
||||
@@ -843,7 +825,7 @@ class LxcContainerManagement(object):
|
||||
|
||||
build_command = [
|
||||
self.module.get_bin_path('lxc-create', True),
|
||||
'--name %s' % self.container_name,
|
||||
'--name', self.container_name,
|
||||
'--quiet'
|
||||
]
|
||||
|
||||
@@ -869,10 +851,12 @@ class LxcContainerManagement(object):
|
||||
log_path = os.getenv('HOME')
|
||||
|
||||
build_command.extend([
|
||||
'--logfile %s' % os.path.join(
|
||||
'--logfile',
|
||||
os.path.join(
|
||||
log_path, 'lxc-%s.log' % self.container_name
|
||||
),
|
||||
'--logpriority %s' % self.module.params.get(
|
||||
'--logpriority',
|
||||
self.module.params.get(
|
||||
'container_log_level'
|
||||
).upper()
|
||||
])
|
||||
@@ -880,9 +864,10 @@ class LxcContainerManagement(object):
|
||||
# Add the template commands to the end of the command if there are any
|
||||
template_options = self.module.params.get('template_options', None)
|
||||
if template_options:
|
||||
build_command.append('-- %s' % template_options)
|
||||
build_command.append('--')
|
||||
build_command += shlex.split(template_options)
|
||||
|
||||
rc, return_data, err = self._run_command(build_command)
|
||||
rc, return_data, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
message = "Failed executing lxc-create."
|
||||
self.failure(
|
||||
@@ -1186,7 +1171,7 @@ class LxcContainerManagement(object):
|
||||
self.module.get_bin_path('lxc-config', True),
|
||||
"lxc.bdev.lvm.vg"
|
||||
]
|
||||
rc, vg, err = self._run_command(build_command)
|
||||
rc, vg, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1204,7 +1189,7 @@ class LxcContainerManagement(object):
|
||||
build_command = [
|
||||
self.module.get_bin_path('lvs', True)
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1231,7 +1216,7 @@ class LxcContainerManagement(object):
|
||||
'--units',
|
||||
'g'
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1262,7 +1247,7 @@ class LxcContainerManagement(object):
|
||||
'--units',
|
||||
'g'
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1311,7 +1296,7 @@ class LxcContainerManagement(object):
|
||||
os.path.join(vg, source_lv),
|
||||
"-L%sg" % snapshot_size_gb
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1336,7 +1321,7 @@ class LxcContainerManagement(object):
|
||||
"/dev/%s/%s" % (vg, lv_name),
|
||||
mount_point,
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1380,9 +1365,8 @@ class LxcContainerManagement(object):
|
||||
'.'
|
||||
]
|
||||
|
||||
rc, stdout, err = self._run_command(
|
||||
build_command=build_command,
|
||||
unsafe_shell=True
|
||||
rc, stdout, err = self.module.run_command(
|
||||
build_command
|
||||
)
|
||||
|
||||
os.umask(old_umask)
|
||||
@@ -1410,7 +1394,7 @@ class LxcContainerManagement(object):
|
||||
"-f",
|
||||
"%s/%s" % (vg, lv_name),
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1442,11 +1426,10 @@ class LxcContainerManagement(object):
|
||||
self.module.get_bin_path('rsync', True),
|
||||
'-aHAX',
|
||||
fs_path,
|
||||
temp_dir
|
||||
temp_dir,
|
||||
]
|
||||
rc, stdout, err = self._run_command(
|
||||
rc, stdout, err = self.module.run_command(
|
||||
build_command,
|
||||
unsafe_shell=True
|
||||
)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
@@ -1467,7 +1450,7 @@ class LxcContainerManagement(object):
|
||||
self.module.get_bin_path('umount', True),
|
||||
mount_point,
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
@@ -1489,12 +1472,12 @@ class LxcContainerManagement(object):
|
||||
|
||||
build_command = [
|
||||
self.module.get_bin_path('mount', True),
|
||||
'-t overlayfs',
|
||||
'-o lowerdir=%s,upperdir=%s' % (lowerdir, upperdir),
|
||||
'-t', 'overlayfs',
|
||||
'-o', 'lowerdir=%s,upperdir=%s' % (lowerdir, upperdir),
|
||||
'overlayfs',
|
||||
mount_point,
|
||||
]
|
||||
rc, stdout, err = self._run_command(build_command)
|
||||
rc, stdout, err = self.module.run_command(build_command)
|
||||
if rc != 0:
|
||||
self.failure(
|
||||
err=err,
|
||||
|
||||
@@ -11,29 +11,28 @@ __metaclass__ = type
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: lxd_container
|
||||
short_description: Manage LXD Containers
|
||||
short_description: Manage LXD instances
|
||||
description:
|
||||
- Management of LXD containers
|
||||
- Management of LXD containers and virtual machines.
|
||||
author: "Hiroaki Nakamura (@hnakamur)"
|
||||
options:
|
||||
name:
|
||||
description:
|
||||
- Name of a container.
|
||||
- Name of an instance.
|
||||
type: str
|
||||
required: true
|
||||
architecture:
|
||||
description:
|
||||
- 'The architecture for the container (for example C(x86_64) or C(i686)).
|
||||
- 'The architecture for the instance (for example C(x86_64) or C(i686)).
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
|
||||
type: str
|
||||
required: false
|
||||
config:
|
||||
description:
|
||||
- 'The config for the container (for example C({"limits.cpu": "2"})).
|
||||
- 'The config for the instance (for example C({"limits.cpu": "2"})).
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
|
||||
- If the container already exists and its "config" values in metadata
|
||||
obtained from GET /1.0/containers/<name>
|
||||
U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#10containersname)
|
||||
- If the instance already exists and its "config" values in metadata
|
||||
obtained from the LXD API U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#instances-containers-and-virtual-machines)
|
||||
are different, this module tries to apply the configurations.
|
||||
- The keys starting with C(volatile.) are ignored for this comparison when I(ignore_volatile_options=true).
|
||||
type: dict
|
||||
@@ -50,25 +49,25 @@ options:
|
||||
version_added: 3.7.0
|
||||
profiles:
|
||||
description:
|
||||
- Profile to be used by the container.
|
||||
- Profile to be used by the instance.
|
||||
type: list
|
||||
elements: str
|
||||
devices:
|
||||
description:
|
||||
- 'The devices for the container
|
||||
- 'The devices for the instance
|
||||
(for example C({ "rootfs": { "path": "/dev/kvm", "type": "unix-char" }})).
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).'
|
||||
type: dict
|
||||
required: false
|
||||
ephemeral:
|
||||
description:
|
||||
- Whether or not the container is ephemeral (for example C(true) or C(false)).
|
||||
- Whether or not the instance is ephemeral (for example C(true) or C(false)).
|
||||
See U(https://github.com/lxc/lxd/blob/master/doc/rest-api.md#post-1).
|
||||
required: false
|
||||
type: bool
|
||||
source:
|
||||
description:
|
||||
- 'The source for the container
|
||||
- 'The source for the instance
|
||||
(e.g. { "type": "image",
|
||||
"mode": "pull",
|
||||
"server": "https://images.linuxcontainers.org",
|
||||
@@ -86,39 +85,56 @@ options:
|
||||
- absent
|
||||
- frozen
|
||||
description:
|
||||
- Define the state of a container.
|
||||
- Define the state of an instance.
|
||||
required: false
|
||||
default: started
|
||||
type: str
|
||||
target:
|
||||
description:
|
||||
- For cluster deployments. Will attempt to create a container on a target node.
|
||||
If container exists elsewhere in a cluster, then container will not be replaced or moved.
|
||||
- For cluster deployments. Will attempt to create an instance on a target node.
|
||||
If the instance exists elsewhere in a cluster, then it will not be replaced or moved.
|
||||
The name should respond to same name of the node you see in C(lxc cluster list).
|
||||
type: str
|
||||
required: false
|
||||
version_added: 1.0.0
|
||||
timeout:
|
||||
description:
|
||||
- A timeout for changing the state of the container.
|
||||
- A timeout for changing the state of the instance.
|
||||
- This is also used as a timeout for waiting until IPv4 addresses
|
||||
are set to the all network interfaces in the container after
|
||||
are set to the all network interfaces in the instance after
|
||||
starting or restarting.
|
||||
required: false
|
||||
default: 30
|
||||
type: int
|
||||
type:
|
||||
description:
|
||||
- Instance type can be either C(virtual-machine) or C(container).
|
||||
required: false
|
||||
default: container
|
||||
choices:
|
||||
- container
|
||||
- virtual-machine
|
||||
type: str
|
||||
version_added: 4.1.0
|
||||
wait_for_ipv4_addresses:
|
||||
description:
|
||||
- If this is true, the C(lxd_container) waits until IPv4 addresses
|
||||
are set to the all network interfaces in the container after
|
||||
are set to the all network interfaces in the instance after
|
||||
starting or restarting.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
wait_for_container:
|
||||
description:
|
||||
- If set to C(true), the tasks will wait till the task reports a
|
||||
success status when performing container operations.
|
||||
default: false
|
||||
type: bool
|
||||
version_added: 4.4.0
|
||||
force_stop:
|
||||
description:
|
||||
- If this is true, the C(lxd_container) forces to stop the container
|
||||
when it stops or restarts the container.
|
||||
- If this is true, the C(lxd_container) forces to stop the instance
|
||||
when it stops or restarts the instance.
|
||||
required: false
|
||||
default: false
|
||||
type: bool
|
||||
@@ -160,18 +176,18 @@ options:
|
||||
required: false
|
||||
type: str
|
||||
notes:
|
||||
- Containers must have a unique name. If you attempt to create a container
|
||||
- Instances can be a container or a virtual machine, both of them must have unique name. If you attempt to create an instance
|
||||
with a name that already existed in the users namespace the module will
|
||||
simply return as "unchanged".
|
||||
- There are two ways to run commands in containers, using the command
|
||||
- There are two ways to run commands inside a container or virtual machine, using the command
|
||||
module or using the ansible lxd connection plugin bundled in Ansible >=
|
||||
2.1, the later requires python to be installed in the container which can
|
||||
2.1, the later requires python to be installed in the instance which can
|
||||
be done with the command module.
|
||||
- You can copy a file from the host to the container
|
||||
- You can copy a file from the host to the instance
|
||||
with the Ansible M(ansible.builtin.copy) and M(ansible.builtin.template) module and the `lxd` connection plugin.
|
||||
See the example below.
|
||||
- You can copy a file in the created container to the localhost
|
||||
with `command=lxc file pull container_name/dir/filename filename`.
|
||||
- You can copy a file in the created instance to the localhost
|
||||
with `command=lxc file pull instance_name/dir/filename filename`.
|
||||
See the first example below.
|
||||
'''
|
||||
|
||||
@@ -240,6 +256,7 @@ EXAMPLES = '''
|
||||
community.general.lxd_container:
|
||||
name: mycontainer
|
||||
state: absent
|
||||
type: container
|
||||
|
||||
# An example for restarting a container
|
||||
- hosts: localhost
|
||||
@@ -249,6 +266,7 @@ EXAMPLES = '''
|
||||
community.general.lxd_container:
|
||||
name: mycontainer
|
||||
state: restarted
|
||||
type: container
|
||||
|
||||
# An example for restarting a container using https to connect to the LXD server
|
||||
- hosts: localhost
|
||||
@@ -306,16 +324,36 @@ EXAMPLES = '''
|
||||
mode: pull
|
||||
alias: ubuntu/xenial/amd64
|
||||
target: node02
|
||||
|
||||
# An example for creating a virtual machine
|
||||
- hosts: localhost
|
||||
connection: local
|
||||
tasks:
|
||||
- name: Create container on another node
|
||||
community.general.lxd_container:
|
||||
name: new-vm-1
|
||||
type: virtual-machine
|
||||
state: started
|
||||
ignore_volatile_options: true
|
||||
wait_for_ipv4_addresses: true
|
||||
profiles: ["default"]
|
||||
source:
|
||||
protocol: simplestreams
|
||||
type: image
|
||||
mode: pull
|
||||
server: https://images.linuxcontainers.org
|
||||
alias: debian/11
|
||||
timeout: 600
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
addresses:
|
||||
description: Mapping from the network device name to a list of IPv4 addresses in the container
|
||||
description: Mapping from the network device name to a list of IPv4 addresses in the instance.
|
||||
returned: when state is started or restarted
|
||||
type: dict
|
||||
sample: {"eth0": ["10.155.92.191"]}
|
||||
old_state:
|
||||
description: The old state of the container
|
||||
description: The old state of the instance.
|
||||
returned: when state is started or restarted
|
||||
type: str
|
||||
sample: "stopped"
|
||||
@@ -325,7 +363,7 @@ logs:
|
||||
type: list
|
||||
sample: "(too long to be placed here)"
|
||||
actions:
|
||||
description: List of actions performed for the container.
|
||||
description: List of actions performed for the instance.
|
||||
returned: success
|
||||
type: list
|
||||
sample: '["create", "start"]'
|
||||
@@ -383,6 +421,16 @@ class LXDContainerManagement(object):
|
||||
self.force_stop = self.module.params['force_stop']
|
||||
self.addresses = None
|
||||
self.target = self.module.params['target']
|
||||
self.wait_for_container = self.module.params['wait_for_container']
|
||||
|
||||
self.type = self.module.params['type']
|
||||
|
||||
# LXD Rest API provides additional endpoints for creating containers and virtual-machines.
|
||||
self.api_endpoint = None
|
||||
if self.type == 'container':
|
||||
self.api_endpoint = '/1.0/containers'
|
||||
elif self.type == 'virtual-machine':
|
||||
self.api_endpoint = '/1.0/virtual-machines'
|
||||
|
||||
self.key_file = self.module.params.get('client_key')
|
||||
if self.key_file is None:
|
||||
@@ -419,20 +467,20 @@ class LXDContainerManagement(object):
|
||||
if param_val is not None:
|
||||
self.config[attr] = param_val
|
||||
|
||||
def _get_container_json(self):
|
||||
def _get_instance_json(self):
|
||||
return self.client.do(
|
||||
'GET', '/1.0/containers/{0}'.format(self.name),
|
||||
'GET', '{0}/{1}'.format(self.api_endpoint, self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
|
||||
def _get_container_state_json(self):
|
||||
def _get_instance_state_json(self):
|
||||
return self.client.do(
|
||||
'GET', '/1.0/containers/{0}/state'.format(self.name),
|
||||
'GET', '{0}/{1}/state'.format(self.api_endpoint, self.name),
|
||||
ok_error_codes=[404]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _container_json_to_module_state(resp_json):
|
||||
def _instance_json_to_module_state(resp_json):
|
||||
if resp_json['type'] == 'error':
|
||||
return 'absent'
|
||||
return ANSIBLE_LXD_STATES[resp_json['metadata']['status']]
|
||||
@@ -441,45 +489,45 @@ class LXDContainerManagement(object):
|
||||
body_json = {'action': action, 'timeout': self.timeout}
|
||||
if force_stop:
|
||||
body_json['force'] = True
|
||||
return self.client.do('PUT', '/1.0/containers/{0}/state'.format(self.name), body_json=body_json)
|
||||
return self.client.do('PUT', '{0}/{1}/state'.format(self.api_endpoint, self.name), body_json=body_json)
|
||||
|
||||
def _create_container(self):
|
||||
def _create_instance(self):
|
||||
config = self.config.copy()
|
||||
config['name'] = self.name
|
||||
if self.target:
|
||||
self.client.do('POST', '/1.0/containers?' + urlencode(dict(target=self.target)), config)
|
||||
self.client.do('POST', '{0}?{1}'.format(self.api_endpoint, urlencode(dict(target=self.target))), config, wait_for_container=self.wait_for_container)
|
||||
else:
|
||||
self.client.do('POST', '/1.0/containers', config)
|
||||
self.client.do('POST', self.api_endpoint, config, wait_for_container=self.wait_for_container)
|
||||
self.actions.append('create')
|
||||
|
||||
def _start_container(self):
|
||||
def _start_instance(self):
|
||||
self._change_state('start')
|
||||
self.actions.append('start')
|
||||
|
||||
def _stop_container(self):
|
||||
def _stop_instance(self):
|
||||
self._change_state('stop', self.force_stop)
|
||||
self.actions.append('stop')
|
||||
|
||||
def _restart_container(self):
|
||||
def _restart_instance(self):
|
||||
self._change_state('restart', self.force_stop)
|
||||
self.actions.append('restart')
|
||||
|
||||
def _delete_container(self):
|
||||
self.client.do('DELETE', '/1.0/containers/{0}'.format(self.name))
|
||||
def _delete_instance(self):
|
||||
self.client.do('DELETE', '{0}/{1}'.format(self.api_endpoint, self.name))
|
||||
self.actions.append('delete')
|
||||
|
||||
def _freeze_container(self):
|
||||
def _freeze_instance(self):
|
||||
self._change_state('freeze')
|
||||
self.actions.append('freeze')
|
||||
|
||||
def _unfreeze_container(self):
|
||||
def _unfreeze_instance(self):
|
||||
self._change_state('unfreeze')
|
||||
self.actions.append('unfreez')
|
||||
|
||||
def _container_ipv4_addresses(self, ignore_devices=None):
|
||||
def _instance_ipv4_addresses(self, ignore_devices=None):
|
||||
ignore_devices = ['lo'] if ignore_devices is None else ignore_devices
|
||||
|
||||
resp_json = self._get_container_state_json()
|
||||
resp_json = self._get_instance_state_json()
|
||||
network = resp_json['metadata']['network'] or {}
|
||||
network = dict((k, v) for k, v in network.items() if k not in ignore_devices) or {}
|
||||
addresses = dict((k, [a['address'] for a in v['addresses'] if a['family'] == 'inet']) for k, v in network.items()) or {}
|
||||
@@ -494,7 +542,7 @@ class LXDContainerManagement(object):
|
||||
due = datetime.datetime.now() + datetime.timedelta(seconds=self.timeout)
|
||||
while datetime.datetime.now() < due:
|
||||
time.sleep(1)
|
||||
addresses = self._container_ipv4_addresses()
|
||||
addresses = self._instance_ipv4_addresses()
|
||||
if self._has_all_ipv4_addresses(addresses):
|
||||
self.addresses = addresses
|
||||
return
|
||||
@@ -504,72 +552,72 @@ class LXDContainerManagement(object):
|
||||
|
||||
def _started(self):
|
||||
if self.old_state == 'absent':
|
||||
self._create_container()
|
||||
self._start_container()
|
||||
self._create_instance()
|
||||
self._start_instance()
|
||||
else:
|
||||
if self.old_state == 'frozen':
|
||||
self._unfreeze_container()
|
||||
self._unfreeze_instance()
|
||||
elif self.old_state == 'stopped':
|
||||
self._start_container()
|
||||
if self._needs_to_apply_container_configs():
|
||||
self._apply_container_configs()
|
||||
self._start_instance()
|
||||
if self._needs_to_apply_instance_configs():
|
||||
self._apply_instance_configs()
|
||||
if self.wait_for_ipv4_addresses:
|
||||
self._get_addresses()
|
||||
|
||||
def _stopped(self):
|
||||
if self.old_state == 'absent':
|
||||
self._create_container()
|
||||
self._create_instance()
|
||||
else:
|
||||
if self.old_state == 'stopped':
|
||||
if self._needs_to_apply_container_configs():
|
||||
self._start_container()
|
||||
self._apply_container_configs()
|
||||
self._stop_container()
|
||||
if self._needs_to_apply_instance_configs():
|
||||
self._start_instance()
|
||||
self._apply_instance_configs()
|
||||
self._stop_instance()
|
||||
else:
|
||||
if self.old_state == 'frozen':
|
||||
self._unfreeze_container()
|
||||
if self._needs_to_apply_container_configs():
|
||||
self._apply_container_configs()
|
||||
self._stop_container()
|
||||
self._unfreeze_instance()
|
||||
if self._needs_to_apply_instance_configs():
|
||||
self._apply_instance_configs()
|
||||
self._stop_instance()
|
||||
|
||||
def _restarted(self):
|
||||
if self.old_state == 'absent':
|
||||
self._create_container()
|
||||
self._start_container()
|
||||
self._create_instance()
|
||||
self._start_instance()
|
||||
else:
|
||||
if self.old_state == 'frozen':
|
||||
self._unfreeze_container()
|
||||
if self._needs_to_apply_container_configs():
|
||||
self._apply_container_configs()
|
||||
self._restart_container()
|
||||
self._unfreeze_instance()
|
||||
if self._needs_to_apply_instance_configs():
|
||||
self._apply_instance_configs()
|
||||
self._restart_instance()
|
||||
if self.wait_for_ipv4_addresses:
|
||||
self._get_addresses()
|
||||
|
||||
def _destroyed(self):
|
||||
if self.old_state != 'absent':
|
||||
if self.old_state == 'frozen':
|
||||
self._unfreeze_container()
|
||||
self._unfreeze_instance()
|
||||
if self.old_state != 'stopped':
|
||||
self._stop_container()
|
||||
self._delete_container()
|
||||
self._stop_instance()
|
||||
self._delete_instance()
|
||||
|
||||
def _frozen(self):
|
||||
if self.old_state == 'absent':
|
||||
self._create_container()
|
||||
self._start_container()
|
||||
self._freeze_container()
|
||||
self._create_instance()
|
||||
self._start_instance()
|
||||
self._freeze_instance()
|
||||
else:
|
||||
if self.old_state == 'stopped':
|
||||
self._start_container()
|
||||
if self._needs_to_apply_container_configs():
|
||||
self._apply_container_configs()
|
||||
self._freeze_container()
|
||||
self._start_instance()
|
||||
if self._needs_to_apply_instance_configs():
|
||||
self._apply_instance_configs()
|
||||
self._freeze_instance()
|
||||
|
||||
def _needs_to_change_container_config(self, key):
|
||||
def _needs_to_change_instance_config(self, key):
|
||||
if key not in self.config:
|
||||
return False
|
||||
if key == 'config' and self.ignore_volatile_options: # the old behavior is to ignore configurations by keyword "volatile"
|
||||
old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items() if not k.startswith('volatile.'))
|
||||
old_configs = dict((k, v) for k, v in self.old_instance_json['metadata'][key].items() if not k.startswith('volatile.'))
|
||||
for k, v in self.config['config'].items():
|
||||
if k not in old_configs:
|
||||
return True
|
||||
@@ -577,7 +625,7 @@ class LXDContainerManagement(object):
|
||||
return True
|
||||
return False
|
||||
elif key == 'config': # next default behavior
|
||||
old_configs = dict((k, v) for k, v in self.old_container_json['metadata'][key].items())
|
||||
old_configs = dict((k, v) for k, v in self.old_instance_json['metadata'][key].items())
|
||||
for k, v in self.config['config'].items():
|
||||
if k not in old_configs:
|
||||
return True
|
||||
@@ -585,39 +633,41 @@ class LXDContainerManagement(object):
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
old_configs = self.old_container_json['metadata'][key]
|
||||
old_configs = self.old_instance_json['metadata'][key]
|
||||
return self.config[key] != old_configs
|
||||
|
||||
def _needs_to_apply_container_configs(self):
|
||||
def _needs_to_apply_instance_configs(self):
|
||||
return (
|
||||
self._needs_to_change_container_config('architecture') or
|
||||
self._needs_to_change_container_config('config') or
|
||||
self._needs_to_change_container_config('ephemeral') or
|
||||
self._needs_to_change_container_config('devices') or
|
||||
self._needs_to_change_container_config('profiles')
|
||||
self._needs_to_change_instance_config('architecture') or
|
||||
self._needs_to_change_instance_config('config') or
|
||||
self._needs_to_change_instance_config('ephemeral') or
|
||||
self._needs_to_change_instance_config('devices') or
|
||||
self._needs_to_change_instance_config('profiles')
|
||||
)
|
||||
|
||||
def _apply_container_configs(self):
|
||||
old_metadata = self.old_container_json['metadata']
|
||||
def _apply_instance_configs(self):
|
||||
old_metadata = self.old_instance_json['metadata']
|
||||
body_json = {
|
||||
'architecture': old_metadata['architecture'],
|
||||
'config': old_metadata['config'],
|
||||
'devices': old_metadata['devices'],
|
||||
'profiles': old_metadata['profiles']
|
||||
}
|
||||
if self._needs_to_change_container_config('architecture'):
|
||||
|
||||
if self._needs_to_change_instance_config('architecture'):
|
||||
body_json['architecture'] = self.config['architecture']
|
||||
if self._needs_to_change_container_config('config'):
|
||||
if self._needs_to_change_instance_config('config'):
|
||||
for k, v in self.config['config'].items():
|
||||
body_json['config'][k] = v
|
||||
if self._needs_to_change_container_config('ephemeral'):
|
||||
if self._needs_to_change_instance_config('ephemeral'):
|
||||
body_json['ephemeral'] = self.config['ephemeral']
|
||||
if self._needs_to_change_container_config('devices'):
|
||||
if self._needs_to_change_instance_config('devices'):
|
||||
body_json['devices'] = self.config['devices']
|
||||
if self._needs_to_change_container_config('profiles'):
|
||||
if self._needs_to_change_instance_config('profiles'):
|
||||
body_json['profiles'] = self.config['profiles']
|
||||
self.client.do('PUT', '/1.0/containers/{0}'.format(self.name), body_json=body_json)
|
||||
self.actions.append('apply_container_configs')
|
||||
|
||||
self.client.do('PUT', '{0}/{1}'.format(self.api_endpoint, self.name), body_json=body_json)
|
||||
self.actions.append('apply_instance_configs')
|
||||
|
||||
def run(self):
|
||||
"""Run the main method."""
|
||||
@@ -627,8 +677,8 @@ class LXDContainerManagement(object):
|
||||
self.client.authenticate(self.trust_password)
|
||||
self.ignore_volatile_options = self.module.params.get('ignore_volatile_options')
|
||||
|
||||
self.old_container_json = self._get_container_json()
|
||||
self.old_state = self._container_json_to_module_state(self.old_container_json)
|
||||
self.old_instance_json = self._get_instance_json()
|
||||
self.old_state = self._instance_json_to_module_state(self.old_instance_json)
|
||||
action = getattr(self, LXD_ANSIBLE_STATES[self.state])
|
||||
action()
|
||||
|
||||
@@ -698,6 +748,15 @@ def main():
|
||||
type='int',
|
||||
default=30
|
||||
),
|
||||
type=dict(
|
||||
type='str',
|
||||
default='container',
|
||||
choices=['container', 'virtual-machine'],
|
||||
),
|
||||
wait_for_container=dict(
|
||||
type='bool',
|
||||
default=False
|
||||
),
|
||||
wait_for_ipv4_addresses=dict(
|
||||
type='bool',
|
||||
default=False
|
||||
@@ -736,6 +795,7 @@ def main():
|
||||
'This will change in the future. Please test your scripts'
|
||||
'by "ignore_volatile_options: false". To keep the old behavior, set that option explicitly to "true"',
|
||||
version='6.0.0', collection_name='community.general')
|
||||
|
||||
lxd_manage = LXDContainerManagement(module=module)
|
||||
lxd_manage.run()
|
||||
|
||||
|
||||
@@ -167,6 +167,25 @@ options:
|
||||
- compatibility
|
||||
- no_defaults
|
||||
version_added: "1.3.0"
|
||||
clone:
|
||||
description:
|
||||
- ID of the container to be cloned.
|
||||
- I(description), I(hostname), and I(pool) will be copied from the cloned container if not specified.
|
||||
- The type of clone created is defined by the I(clone_type) parameter.
|
||||
- This operator is only supported for Proxmox clusters that use LXC containerization (PVE version >= 4).
|
||||
type: int
|
||||
version_added: 4.3.0
|
||||
clone_type:
|
||||
description:
|
||||
- Type of the clone created.
|
||||
- C(full) creates a full clone, and I(storage) must be specified.
|
||||
- C(linked) creates a linked clone, and the cloned container must be a template container.
|
||||
- C(opportunistic) creates a linked clone if the cloned container is a template container, and a full clone if not.
|
||||
I(storage) may be specified, if not it will fall back to the default.
|
||||
type: str
|
||||
choices: ['full', 'linked', 'opportunistic']
|
||||
default: opportunistic
|
||||
version_added: 4.3.0
|
||||
author: Sergei Antipov (@UnderGreen)
|
||||
extends_documentation_fragment:
|
||||
- community.general.proxmox.documentation
|
||||
@@ -292,6 +311,28 @@ EXAMPLES = r'''
|
||||
- nesting=1
|
||||
- mount=cifs,nfs
|
||||
|
||||
- name: >
|
||||
Create a linked clone of the template container with id 100. The newly created container with be a
|
||||
linked clone, because no storage parameter is defined
|
||||
community.general.proxmox:
|
||||
vmid: 201
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
clone: 100
|
||||
hostname: clone.example.org
|
||||
|
||||
- name: Create a full clone of the container with id 100
|
||||
community.general.proxmox:
|
||||
vmid: 201
|
||||
node: uk-mc02
|
||||
api_user: root@pam
|
||||
api_password: 1q2w3e
|
||||
api_host: node1
|
||||
clone: 100
|
||||
hostname: clone.example.org
|
||||
storage: local
|
||||
|
||||
- name: Start container
|
||||
community.general.proxmox:
|
||||
@@ -348,7 +389,8 @@ EXAMPLES = r'''
|
||||
|
||||
import time
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from proxmoxer import ProxmoxAPI
|
||||
@@ -359,6 +401,10 @@ except ImportError:
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback
|
||||
from ansible.module_utils.common.text.converters import to_native
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
ansible_to_proxmox_bool
|
||||
)
|
||||
|
||||
|
||||
VZ_TYPE = None
|
||||
|
||||
@@ -384,6 +430,13 @@ def content_check(proxmox, node, ostemplate, template_store):
|
||||
return [True for cnt in proxmox.nodes(node).storage(template_store).content.get() if cnt['volid'] == ostemplate]
|
||||
|
||||
|
||||
def is_template_container(proxmox, node, vmid):
|
||||
"""Check if the specified container is a template."""
|
||||
proxmox_node = proxmox.nodes(node)
|
||||
config = getattr(proxmox_node, VZ_TYPE)(vmid).config.get()
|
||||
return config['template']
|
||||
|
||||
|
||||
def node_check(proxmox, node):
|
||||
return [True for nd in proxmox.nodes.get() if nd['node'] == node]
|
||||
|
||||
@@ -393,8 +446,10 @@ def proxmox_version(proxmox):
|
||||
return LooseVersion(apireturn['version'])
|
||||
|
||||
|
||||
def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, **kwargs):
|
||||
def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone, **kwargs):
|
||||
proxmox_node = proxmox.nodes(node)
|
||||
|
||||
# Remove all empty kwarg entries
|
||||
kwargs = dict((k, v) for k, v in kwargs.items() if v is not None)
|
||||
|
||||
if VZ_TYPE == 'lxc':
|
||||
@@ -414,7 +469,49 @@ def create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, sw
|
||||
kwargs['cpus'] = cpus
|
||||
kwargs['disk'] = disk
|
||||
|
||||
taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)
|
||||
if clone is not None:
|
||||
if VZ_TYPE != 'lxc':
|
||||
module.fail_json(changed=False, msg="Clone operator is only supported for LXC enabled proxmox clusters.")
|
||||
|
||||
clone_is_template = is_template_container(proxmox, node, clone)
|
||||
|
||||
# By default, create a full copy only when the cloned container is not a template.
|
||||
create_full_copy = not clone_is_template
|
||||
|
||||
# Only accept parameters that are compatible with the clone endpoint.
|
||||
valid_clone_parameters = ['hostname', 'pool', 'description']
|
||||
if module.params['storage'] is not None and clone_is_template:
|
||||
# Cloning a template, so create a full copy instead of a linked copy
|
||||
create_full_copy = True
|
||||
elif module.params['storage'] is None and not clone_is_template:
|
||||
# Not cloning a template, but also no defined storage. This isn't possible.
|
||||
module.fail_json(changed=False, msg="Cloned container is not a template, storage needs to be specified.")
|
||||
|
||||
if module.params['clone_type'] == 'linked':
|
||||
if not clone_is_template:
|
||||
module.fail_json(changed=False, msg="'linked' clone type is specified, but cloned container is not a template container.")
|
||||
# Don't need to do more, by default create_full_copy is set to false already
|
||||
elif module.params['clone_type'] == 'opportunistic':
|
||||
if not clone_is_template:
|
||||
# Cloned container is not a template, so we need our 'storage' parameter
|
||||
valid_clone_parameters.append('storage')
|
||||
elif module.params['clone_type'] == 'full':
|
||||
create_full_copy = True
|
||||
valid_clone_parameters.append('storage')
|
||||
|
||||
clone_parameters = {}
|
||||
|
||||
if create_full_copy:
|
||||
clone_parameters['full'] = '1'
|
||||
else:
|
||||
clone_parameters['full'] = '0'
|
||||
for param in valid_clone_parameters:
|
||||
if module.params[param] is not None:
|
||||
clone_parameters[param] = module.params[param]
|
||||
|
||||
taskid = getattr(proxmox_node, VZ_TYPE)(clone).clone.post(newid=vmid, **clone_parameters)
|
||||
else:
|
||||
taskid = getattr(proxmox_node, VZ_TYPE).create(vmid=vmid, storage=storage, memory=memory, swap=swap, **kwargs)
|
||||
|
||||
while timeout:
|
||||
if (proxmox_node.tasks(taskid).status.get()['status'] == 'stopped' and
|
||||
@@ -515,10 +612,19 @@ def main():
|
||||
description=dict(type='str'),
|
||||
hookscript=dict(type='str'),
|
||||
proxmox_default_behavior=dict(type='str', default='no_defaults', choices=['compatibility', 'no_defaults']),
|
||||
clone=dict(type='int'),
|
||||
clone_type=dict(default='opportunistic', choices=['full', 'linked', 'opportunistic']),
|
||||
),
|
||||
required_if=[('state', 'present', ['node', 'hostname', 'ostemplate'])],
|
||||
required_together=[('api_token_id', 'api_token_secret')],
|
||||
required_if=[
|
||||
('state', 'present', ['node', 'hostname']),
|
||||
('state', 'present', ('clone', 'ostemplate'), True), # Require one of clone and ostemplate. Together with mutually_exclusive this ensures that we
|
||||
# either clone a container or create a new one from a template file.
|
||||
],
|
||||
required_together=[
|
||||
('api_token_id', 'api_token_secret')
|
||||
],
|
||||
required_one_of=[('api_password', 'api_token_id')],
|
||||
mutually_exclusive=[('clone', 'ostemplate')], # Creating a new container is done either by cloning an existing one, or based on a template.
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
@@ -542,6 +648,7 @@ def main():
|
||||
if module.params['ostemplate'] is not None:
|
||||
template_store = module.params['ostemplate'].split(":")[0]
|
||||
timeout = module.params['timeout']
|
||||
clone = module.params['clone']
|
||||
|
||||
if module.params['proxmox_default_behavior'] == 'compatibility':
|
||||
old_default_values = dict(
|
||||
@@ -583,7 +690,8 @@ def main():
|
||||
elif not vmid:
|
||||
module.exit_json(changed=False, msg="Vmid could not be fetched for the following action: %s" % state)
|
||||
|
||||
if state == 'present':
|
||||
# Create a new container
|
||||
if state == 'present' and clone is None:
|
||||
try:
|
||||
if get_instance(proxmox, vmid) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
|
||||
@@ -595,8 +703,11 @@ def main():
|
||||
elif not content_check(proxmox, node, module.params['ostemplate'], template_store):
|
||||
module.fail_json(msg="ostemplate '%s' not exists on node %s and storage %s"
|
||||
% (module.params['ostemplate'], node, template_store))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Pre-creation checks of {VZ_TYPE} VM {vmid} failed with exception: {e}".format(VZ_TYPE=VZ_TYPE, vmid=vmid, e=e))
|
||||
|
||||
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout,
|
||||
try:
|
||||
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone,
|
||||
cores=module.params['cores'],
|
||||
pool=module.params['pool'],
|
||||
password=module.params['password'],
|
||||
@@ -605,20 +716,40 @@ def main():
|
||||
netif=module.params['netif'],
|
||||
mounts=module.params['mounts'],
|
||||
ip_address=module.params['ip_address'],
|
||||
onboot=int(module.params['onboot']),
|
||||
onboot=ansible_to_proxmox_bool(module.params['onboot']),
|
||||
cpuunits=module.params['cpuunits'],
|
||||
nameserver=module.params['nameserver'],
|
||||
searchdomain=module.params['searchdomain'],
|
||||
force=int(module.params['force']),
|
||||
force=ansible_to_proxmox_bool(module.params['force']),
|
||||
pubkey=module.params['pubkey'],
|
||||
features=",".join(module.params['features']) if module.params['features'] is not None else None,
|
||||
unprivileged=int(module.params['unprivileged']),
|
||||
unprivileged=ansible_to_proxmox_bool(module.params['unprivileged']),
|
||||
description=module.params['description'],
|
||||
hookscript=module.params['hookscript'])
|
||||
|
||||
module.exit_json(changed=True, msg="deployed VM %s from template %s" % (vmid, module.params['ostemplate']))
|
||||
module.exit_json(changed=True, msg="Deployed VM %s from template %s" % (vmid, module.params['ostemplate']))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="creation of %s VM %s failed with exception: %s" % (VZ_TYPE, vmid, e))
|
||||
module.fail_json(msg="Creation of %s VM %s failed with exception: %s" % (VZ_TYPE, vmid, e))
|
||||
|
||||
# Clone a container
|
||||
elif state == 'present' and clone is not None:
|
||||
try:
|
||||
if get_instance(proxmox, vmid) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with vmid = %s is already exists" % vmid)
|
||||
# If no vmid was passed, there cannot be another VM named 'hostname'
|
||||
if not module.params['vmid'] and get_vmid(proxmox, hostname) and not module.params['force']:
|
||||
module.exit_json(changed=False, msg="VM with hostname %s already exists and has ID number %s" % (hostname, get_vmid(proxmox, hostname)[0]))
|
||||
if not get_instance(proxmox, clone):
|
||||
module.exit_json(changed=False, msg="Container to be cloned does not exist")
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Pre-clone checks of {VZ_TYPE} VM {vmid} failed with exception: {e}".format(VZ_TYPE=VZ_TYPE, vmid=vmid, e=e))
|
||||
|
||||
try:
|
||||
create_instance(module, proxmox, vmid, node, disk, storage, cpus, memory, swap, timeout, clone)
|
||||
|
||||
module.exit_json(changed=True, msg="Cloned VM %s from %s" % (vmid, clone))
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Cloning %s VM %s failed with exception: %s" % (VZ_TYPE, vmid, e))
|
||||
|
||||
elif state == 'started':
|
||||
try:
|
||||
|
||||
@@ -76,7 +76,7 @@ proxmox_domains:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxDomainInfoAnsible(ProxmoxAnsible):
|
||||
@@ -114,9 +114,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxDomainInfoAnsible(module)
|
||||
domain = module.params['domain']
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ proxmox_groups:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxGroupInfoAnsible(ProxmoxAnsible):
|
||||
@@ -124,9 +124,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxGroupInfoAnsible(module)
|
||||
group = module.params['group']
|
||||
|
||||
|
||||
@@ -725,9 +725,10 @@ msg:
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
from ansible.module_utils.six.moves.urllib.parse import quote
|
||||
|
||||
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
||||
|
||||
try:
|
||||
from proxmoxer import ProxmoxAPI
|
||||
HAS_PROXMOXER = True
|
||||
|
||||
@@ -197,7 +197,7 @@ def main():
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('python-proxmoxer'),
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'),
|
||||
exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
state = module.params['state']
|
||||
|
||||
@@ -111,7 +111,7 @@ proxmox_storages:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR, proxmox_to_ansible_bool)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool)
|
||||
|
||||
|
||||
class ProxmoxStorageInfoAnsible(ProxmoxAnsible):
|
||||
@@ -170,9 +170,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxStorageInfoAnsible(module)
|
||||
storage = module.params['storage']
|
||||
storagetype = module.params['type']
|
||||
|
||||
@@ -116,7 +116,7 @@ msg:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible)
|
||||
|
||||
|
||||
class ProxmoxTaskInfoAnsible(ProxmoxAnsible):
|
||||
@@ -163,9 +163,6 @@ def main():
|
||||
supports_check_mode=True)
|
||||
result = dict(changed=False)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib(
|
||||
'proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
proxmox = ProxmoxTaskInfoAnsible(module)
|
||||
upid = module.params['task']
|
||||
node = module.params['node']
|
||||
|
||||
@@ -156,7 +156,7 @@ proxmox_users:
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible_collections.community.general.plugins.module_utils.proxmox import (
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool, HAS_PROXMOXER, PROXMOXER_IMP_ERR)
|
||||
proxmox_auth_argument_spec, ProxmoxAnsible, proxmox_to_ansible_bool)
|
||||
|
||||
|
||||
class ProxmoxUserInfoAnsible(ProxmoxAnsible):
|
||||
@@ -232,9 +232,6 @@ def main():
|
||||
changed=False
|
||||
)
|
||||
|
||||
if not HAS_PROXMOXER:
|
||||
module.fail_json(msg=missing_required_lib('proxmoxer'), exception=PROXMOXER_IMP_ERR)
|
||||
|
||||
proxmox = ProxmoxUserInfoAnsible(module)
|
||||
domain = module.params['domain']
|
||||
user = module.params['user']
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user