refactor: rework ci and testing (#3)
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/tag Build is passing Details

This commit is contained in:
Robert Kaussow 2023-01-31 20:09:29 +01:00
parent 579a9713c1
commit dcf04af784
20 changed files with 2090 additions and 592 deletions

View File

@ -5,9 +5,30 @@ local PythonVersion(pyversion='3.8') = {
PY_COLORS: 1, PY_COLORS: 1,
}, },
commands: [ commands: [
'pip install -r dev-requirements.txt -qq', 'pip install poetry -qq',
'pip install -r test/unit/requirements.txt -qq', 'poetry config experimental.new-installer false',
'python -m pytest --cov --cov-append --no-cov-on-fail', 'poetry install --all-extras',
'poetry run pytest',
],
depends_on: [
'clone',
],
};
local AnsibleVersion(version='devel') = {
local gitversion = if version == 'devel' then 'devel' else 'stable-' + version,
name: 'ansible-' + std.strReplace(version, '.', ''),
image: 'python:3.9',
environment: {
PY_COLORS: 1,
},
commands: [
'pip install poetry -qq',
'poetry config experimental.new-installer false',
'poetry install',
'poetry run pip install https://github.com/ansible/ansible/archive/' + gitversion + '.tar.gz --disable-pip-version-check',
'poetry run ansible --version',
'poetry run ansible-test sanity --exclude .chglog/ --exclude .drone.yml --python 3.9',
], ],
depends_on: [ depends_on: [
'clone', 'clone',
@ -23,14 +44,31 @@ local PipelineLint = {
}, },
steps: [ steps: [
{ {
name: 'flake8', name: 'check-format',
image: 'python:3.10', image: 'python:3.11',
environment: { environment: {
PY_COLORS: 1, PY_COLORS: 1,
}, },
commands: [ commands: [
'pip install -r dev-requirements.txt -qq', 'git fetch -tq',
'flake8', 'pip install poetry -qq',
'poetry config experimental.new-installer false',
'poetry install --all-extras',
'poetry run yapf -dr ./plugins',
],
},
{
name: 'check-coding',
image: 'python:3.11',
environment: {
PY_COLORS: 1,
},
commands: [
'git fetch -tq',
'pip install poetry -qq',
'poetry config experimental.new-installer false',
'poetry install --all-extras',
'poetry run ruff ./plugins',
], ],
}, },
], ],
@ -39,9 +77,9 @@ local PipelineLint = {
}, },
}; };
local PipelineTest = { local PipelineUnitTest = {
kind: 'pipeline', kind: 'pipeline',
name: 'test', name: 'unit-test',
platform: { platform: {
os: 'linux', os: 'linux',
arch: 'amd64', arch: 'amd64',
@ -50,6 +88,7 @@ local PipelineTest = {
PythonVersion(pyversion='3.8'), PythonVersion(pyversion='3.8'),
PythonVersion(pyversion='3.9'), PythonVersion(pyversion='3.9'),
PythonVersion(pyversion='3.10'), PythonVersion(pyversion='3.10'),
PythonVersion(pyversion='3.11'),
], ],
depends_on: [ depends_on: [
'lint', 'lint',
@ -59,6 +98,29 @@ local PipelineTest = {
}, },
}; };
local PipelineSanityTest = {
kind: 'pipeline',
name: 'sanity-test',
platform: {
os: 'linux',
arch: 'amd64',
},
workspace: {
path: '/drone/src/ansible_collections/${DRONE_REPO_NAME/./\\/}',
},
steps: [
AnsibleVersion(version='devel'),
AnsibleVersion(version='2.14'),
AnsibleVersion(version='2.13'),
],
depends_on: [
'unit-test',
],
trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
},
};
local PipelineBuild = { local PipelineBuild = {
kind: 'pipeline', kind: 'pipeline',
name: 'build', name: 'build',
@ -69,12 +131,14 @@ local PipelineBuild = {
steps: [ steps: [
{ {
name: 'build', name: 'build',
image: 'python:3.9', image: 'python:3.11',
commands: [ commands: [
'GALAXY_VERSION=${DRONE_TAG##v}', 'GALAXY_VERSION=${DRONE_TAG##v}',
"sed -i 's/version: 0.0.0/version: '\"$${GALAXY_VERSION:-0.0.0}\"'/g' galaxy.yml", "sed -i 's/version: 0.0.0/version: '\"$${GALAXY_VERSION:-0.0.0}\"'/g' galaxy.yml",
'pip install ansible -qq', 'pip install poetry -qq',
'ansible-galaxy collection build --output-path dist/', 'poetry config experimental.new-installer false',
'poetry install --all-extras',
'poetry run ansible-galaxy collection build --output-path dist/',
], ],
}, },
{ {
@ -117,7 +181,7 @@ local PipelineBuild = {
}, },
], ],
depends_on: [ depends_on: [
'test', 'sanity-test',
], ],
trigger: { trigger: {
ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'], ref: ['refs/heads/main', 'refs/tags/**', 'refs/pull/**'],
@ -188,7 +252,8 @@ local PipelineNotifications = {
[ [
PipelineLint, PipelineLint,
PipelineTest, PipelineUnitTest,
PipelineSanityTest,
PipelineBuild, PipelineBuild,
PipelineDocumentation, PipelineDocumentation,
PipelineNotifications, PipelineNotifications,

View File

@ -7,11 +7,25 @@ platform:
arch: amd64 arch: amd64
steps: steps:
- name: flake8 - name: check-format
image: python:3.10 image: python:3.11
commands: commands:
- pip install -r dev-requirements.txt -qq - git fetch -tq
- flake8 - pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install --all-extras
- poetry run yapf -dr ./plugins
environment:
PY_COLORS: 1
- name: check-coding
image: python:3.11
commands:
- git fetch -tq
- pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install --all-extras
- poetry run ruff ./plugins
environment: environment:
PY_COLORS: 1 PY_COLORS: 1
@ -23,7 +37,7 @@ trigger:
--- ---
kind: pipeline kind: pipeline
name: test name: unit-test
platform: platform:
os: linux os: linux
@ -33,9 +47,10 @@ steps:
- name: python38-pytest - name: python38-pytest
image: python:3.8 image: python:3.8
commands: commands:
- pip install -r dev-requirements.txt -qq - pip install poetry -qq
- pip install -r test/unit/requirements.txt -qq - poetry config experimental.new-installer false
- python -m pytest --cov --cov-append --no-cov-on-fail - poetry install --all-extras
- poetry run pytest
environment: environment:
PY_COLORS: 1 PY_COLORS: 1
depends_on: depends_on:
@ -44,9 +59,10 @@ steps:
- name: python39-pytest - name: python39-pytest
image: python:3.9 image: python:3.9
commands: commands:
- pip install -r dev-requirements.txt -qq - pip install poetry -qq
- pip install -r test/unit/requirements.txt -qq - poetry config experimental.new-installer false
- python -m pytest --cov --cov-append --no-cov-on-fail - poetry install --all-extras
- poetry run pytest
environment: environment:
PY_COLORS: 1 PY_COLORS: 1
depends_on: depends_on:
@ -55,9 +71,22 @@ steps:
- name: python310-pytest - name: python310-pytest
image: python:3.10 image: python:3.10
commands: commands:
- pip install -r dev-requirements.txt -qq - pip install poetry -qq
- pip install -r test/unit/requirements.txt -qq - poetry config experimental.new-installer false
- python -m pytest --cov --cov-append --no-cov-on-fail - poetry install --all-extras
- poetry run pytest
environment:
PY_COLORS: 1
depends_on:
- clone
- name: python311-pytest
image: python:3.11
commands:
- pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install --all-extras
- poetry run pytest
environment: environment:
PY_COLORS: 1 PY_COLORS: 1
depends_on: depends_on:
@ -72,6 +101,69 @@ trigger:
depends_on: depends_on:
- lint - lint
---
kind: pipeline
name: sanity-test
platform:
os: linux
arch: amd64
workspace:
path: /drone/src/ansible_collections/${DRONE_REPO_NAME/./\/}
steps:
- name: ansible-devel
image: python:3.9
commands:
- pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pip install https://github.com/ansible/ansible/archive/devel.tar.gz --disable-pip-version-check
- poetry run ansible --version
- poetry run ansible-test sanity --exclude .chglog/ --exclude .drone.yml --python 3.9
environment:
PY_COLORS: 1
depends_on:
- clone
- name: ansible-214
image: python:3.9
commands:
- pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pip install https://github.com/ansible/ansible/archive/stable-2.14.tar.gz --disable-pip-version-check
- poetry run ansible --version
- poetry run ansible-test sanity --exclude .chglog/ --exclude .drone.yml --python 3.9
environment:
PY_COLORS: 1
depends_on:
- clone
- name: ansible-213
image: python:3.9
commands:
- pip install poetry -qq
- poetry config experimental.new-installer false
- poetry install
- poetry run pip install https://github.com/ansible/ansible/archive/stable-2.13.tar.gz --disable-pip-version-check
- poetry run ansible --version
- poetry run ansible-test sanity --exclude .chglog/ --exclude .drone.yml --python 3.9
environment:
PY_COLORS: 1
depends_on:
- clone
trigger:
ref:
- refs/heads/main
- refs/tags/**
- refs/pull/**
depends_on:
- unit-test
--- ---
kind: pipeline kind: pipeline
name: build name: build
@ -82,12 +174,14 @@ platform:
steps: steps:
- name: build - name: build
image: python:3.9 image: python:3.11
commands: commands:
- GALAXY_VERSION=${DRONE_TAG##v} - GALAXY_VERSION=${DRONE_TAG##v}
- "sed -i 's/version: 0.0.0/version: '\"$${GALAXY_VERSION:-0.0.0}\"'/g' galaxy.yml" - "sed -i 's/version: 0.0.0/version: '\"$${GALAXY_VERSION:-0.0.0}\"'/g' galaxy.yml"
- pip install ansible -qq - pip install poetry -qq
- ansible-galaxy collection build --output-path dist/ - poetry config experimental.new-installer false
- poetry install --all-extras
- poetry run ansible-galaxy collection build --output-path dist/
- name: checksum - name: checksum
image: alpine image: alpine
@ -129,7 +223,7 @@ trigger:
- refs/pull/** - refs/pull/**
depends_on: depends_on:
- test - sanity-test
--- ---
kind: pipeline kind: pipeline
@ -195,6 +289,6 @@ depends_on:
--- ---
kind: signature kind: signature
hmac: 93f735c3d2fbaf499fd96b79301f6de2455349051dc320c511e6e62c8ba04a4d hmac: 407d145ab4483651c579eea4e1e9375536caee1aa0579abcdf57b5653e595cb5
... ...

1
.gitignore vendored
View File

@ -108,3 +108,4 @@ docs/public/
resources/_gen/ resources/_gen/
CHANGELOG.md CHANGELOG.md
tests/output

View File

@ -1,18 +0,0 @@
pydocstyle
flake8
flake8-blind-except
flake8-builtins
flake8-docstrings
flake8-isort
flake8-logging-format
flake8-polyfill
flake8-quotes
flake8-pep3101
flake8-eradicate
pep8-naming
wheel
pytest
pytest-mock
pytest-cov
bandit
yapf

View File

@ -1,11 +1,15 @@
"""Filter to prefix all itams from a list.""" """Filter to prefix all itams from a list."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
def prefix(value, prefix="--"): def prefix(value, prefix="--"):
return [prefix + x for x in value] return [prefix + x for x in value]
class FilterModule(object): class FilterModule(object): # noqa
def filters(self): def filters(self):
return {"prefix": prefix} return {"prefix": prefix}

View File

@ -1,11 +1,15 @@
"""Filter to wrap all items from a list.""" """Filter to wrap all items from a list."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
def wrap(value, wrapper="'"): def wrap(value, wrapper="'"):
return [wrapper + x + wrapper for x in value] return [wrapper + x + wrapper for x in value]
class FilterModule(object): class FilterModule(object): # noqa
def filters(self): def filters(self):
return {"wrap": wrap} return {"wrap": wrap}

View File

@ -1,95 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2014, Mathieu GAUTHIER-LAFAYE <gauthierl@lapth.cnrs.fr> # Copyright (c) 2014, Mathieu GAUTHIER-LAFAYE <gauthierl@lapth.cnrs.fr>
# Copyright (c) 2016, Matt Harris <matthaeus.harris@gmail.com> # Copyright (c) 2016, Matt Harris <matthaeus.harris@gmail.com>
# Copyright (c) 2020, Robert Kaussow <mail@thegeeklab.de> # Copyright (c) 2020, Robert Kaussow <mail@thegeeklab.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Dynamic inventory plugin for Proxmox VE.""" """Dynamic inventory plugin for Proxmox VE."""
from __future__ import absolute_import, division, print_function from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = """
name: proxmox ---
plugin_type: inventory name: proxmox
short_description: Proxmox VE inventory source short_description: Proxmox VE inventory source
version_added: 1.0.0 version_added: 1.1.0
description:
- Get inventory hosts from the proxmox service.
- "Uses a configuration file as an inventory source, it must end in C(.proxmox.yml) or C(.proxmox.yaml) and has a C(plugin: xoxys.general.proxmox) entry."
extends_documentation_fragment:
- inventory_cache
options:
plugin:
description: The name of this plugin, it should always be set to C(xoxys.general.proxmox) for this plugin to recognize it as it's own.
required: yes
choices: ["xoxys.general.proxmox"]
api_host:
description: description:
- Get inventory hosts from the proxmox service. - Specify the target host of the Proxmox VE cluster.
- "Uses a configuration file as an inventory source, it must end in C(.proxmox.yml) or C(.proxmox.yaml) and has a C(plugin: xoxys.general.proxmox) entry." type: str
extends_documentation_fragment: required: true
- inventory_cache api_user:
options: description:
plugin: - Specify the user to authenticate with.
description: The name of this plugin, it should always be set to C(xoxys.general.proxmox) for this plugin to recognize it as it's own. type: str
required: yes required: true
choices: ["xoxys.general.proxmox"] api_password:
server: description:
description: Proxmox VE server url. - Specify the password to authenticate with.
default: "pve.example.com" - You can use C(PROXMOX_PASSWORD) environment variable.
type: string type: str
required: yes api_token_id:
env: description:
- name: PROXMOX_SERVER - Specify the token ID.
user: type: str
description: Proxmox VE authentication user. api_token_secret:
type: string description:
required: yes - Specify the token secret.
env: type: str
- name: PROXMOX_USER verify_ssl:
password: description:
description: Proxmox VE authentication password. - If C(false), SSL certificates will not be validated.
type: string - This should only be used on personally controlled sites using self-signed certificates.
required: yes type: bool
env: default: True
- name: PROXMOX_PASSWORD auth_timeout:
verify_ssl: description: Proxmox VE authentication timeout.
description: Skip SSL certificate verification. type: int
type: boolean default: 5
default: yes exclude_vmid:
auth_timeout: description: VMID's to exclude from inventory.
description: Proxmox VE authentication timeout. type: list
type: int default: []
default: 5 elements: str
exclude_vmid: exclude_state:
description: VMID's to exclude from inventory. description: VM states to exclude from inventory.
type: list type: list
default: [] default: []
elements: str elements: str
exclude_state: group:
description: VM states to exclude from inventory. description: Group to place all hosts into.
type: list type: string
default: [] default: proxmox
elements: str want_facts:
group: description: Toggle, if C(true) the plugin will retrieve host facts from the server
description: Group to place all hosts into. type: boolean
type: string default: True
default: proxmox requirements:
want_facts: - "proxmoxer"
description: Toggle, if C(true) the plugin will retrieve host facts from the server """ # noqa
type: boolean
default: yes
""" # noqa
EXAMPLES = """ EXAMPLES = """
# proxmox.yml # proxmox.yml
plugin: xoxys.general.proxmox plugin: xoxys.general.proxmox
server: pve.example.com api_user: root@pam
user: admin@pve api_password: secret
password: secure api_host: helldorado
""" """
import json import json
import re import re
import socket import socket
from collections import defaultdict
from distutils.version import LooseVersion
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
from ansible.module_utils.six import iteritems from ansible.module_utils.six import iteritems
from ansible.plugins.inventory import BaseInventoryPlugin from ansible.plugins.inventory import BaseInventoryPlugin
from collections import defaultdict
from distutils.version import LooseVersion
try: try:
from proxmoxer import ProxmoxAPI from proxmoxer import ProxmoxAPI
@ -97,17 +104,39 @@ try:
except ImportError: except ImportError:
HAS_PROXMOXER = False HAS_PROXMOXER = False
try:
from requests.packages import urllib3
HAS_URLLIB3 = True
except ImportError:
try:
import urllib3
HAS_URLLIB3 = True
except ImportError:
HAS_URLLIB3 = False
class InventoryModule(BaseInventoryPlugin): class InventoryModule(BaseInventoryPlugin):
"""Provide Proxmox VE inventory."""
NAME = "xoxys.general.proxmox" NAME = "xoxys.general.proxmox"
def _auth(self): def _auth(self):
auth_args = {"user": self.get_option("api_user")}
if not (self.get_option("api_token_id") and self.get_option("api_token_secret")):
auth_args["password"] = self.get_option("api_password")
else:
auth_args["token_name"] = self.get_option("api_token_id")
auth_args["token_value"] = self.get_option("api_token_secret")
verify_ssl = boolean(self.get_option("verify_ssl"), strict=False)
if not verify_ssl and HAS_URLLIB3:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
return ProxmoxAPI( return ProxmoxAPI(
self.get_option("server"), self.get_option("api_host"),
user=self.get_option("user"), verify_ssl=verify_ssl,
password=self.get_option("password"), timeout=self.get_option("auth_timeout"),
verify_ssl=boolean(self.get_option("password"), strict=False), **auth_args
timeout=self.get_option("auth_timeout")
) )
def _get_version(self): def _get_version(self):
@ -117,14 +146,12 @@ class InventoryModule(BaseInventoryPlugin):
return LooseVersion(self.client.version.get()["release"]) return LooseVersion(self.client.version.get()["release"])
def _get_names(self, pve_list, pve_type): def _get_names(self, pve_list, pve_type):
names = []
if pve_type == "node": if pve_type == "node":
names = [node["node"] for node in pve_list] return [node["node"] for node in pve_list]
elif pve_type == "pool": if pve_type == "pool":
names = [pool["poolid"] for pool in pve_list] return [pool["poolid"] for pool in pve_list]
return names return []
def _get_variables(self, pve_list, pve_type): def _get_variables(self, pve_list, pve_type):
variables = {} variables = {}
@ -143,11 +170,9 @@ class InventoryModule(BaseInventoryPlugin):
def validate(address): def validate(address):
try: try:
# IP address validation # IP address validation
if socket.inet_aton(address): if socket.inet_aton(address) and address != "127.0.0.1":
# Ignore localhost return address
if address != "127.0.0.1": except OSError:
return address
except socket.error:
return False return False
address = False address = False
@ -159,19 +184,18 @@ class InventoryModule(BaseInventoryPlugin):
networks = self.client.nodes(pve_node).get( networks = self.client.nodes(pve_node).get(
"qemu", vmid, "agent", "network-get-interfaces" "qemu", vmid, "agent", "network-get-interfaces"
)["result"] )["result"]
except Exception: except Exception: # noqa
pass pass
if networks: if networks and isinstance(networks, list):
if type(networks) is list: for network in networks:
for network in networks: for ip_address in network["ip-addresses"]:
for ip_address in network["ip-addresses"]: address = validate(ip_address["ip-address"])
address = validate(ip_address["ip-address"])
else: else:
try: try:
config = self.client.nodes(pve_node).get(pve_type, vmid, "config") config = self.client.nodes(pve_node).get(pve_type, vmid, "config")
address = re.search(r"ip=(\d*\.\d*\.\d*\.\d*)", config["net0"]).group(1) address = re.search(r"ip=(\d*\.\d*\.\d*\.\d*)", config["net0"]).group(1)
except Exception: except Exception: # noqa
pass pass
return address return address
@ -197,8 +221,8 @@ class InventoryModule(BaseInventoryPlugin):
try: try:
qemu_list = self._exclude(self.client.nodes(node).qemu.get()) qemu_list = self._exclude(self.client.nodes(node).qemu.get())
container_list = self._exclude(self.client.nodes(node).lxc.get()) container_list = self._exclude(self.client.nodes(node).lxc.get())
except Exception as e: except Exception as e: # noqa
raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) raise AnsibleError(f"Proxmoxer API error: {to_native(e)}") from e
# Merge QEMU and Containers lists from this node # Merge QEMU and Containers lists from this node
instances = self._get_variables(qemu_list, "qemu").copy() instances = self._get_variables(qemu_list, "qemu").copy()
@ -217,8 +241,8 @@ class InventoryModule(BaseInventoryPlugin):
"config")["description"] "config")["description"]
except KeyError: except KeyError:
description = None description = None
except Exception as e: except Exception as e: # noqa
raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) raise AnsibleError(f"Proxmoxer API error: {to_native(e)}") from e
try: try:
metadata = json.loads(description) metadata = json.loads(description)
@ -252,8 +276,8 @@ class InventoryModule(BaseInventoryPlugin):
for pool in self._get_names(self.client.pools.get(), "pool"): for pool in self._get_names(self.client.pools.get(), "pool"):
try: try:
pool_list = self._exclude(self.client.pool(pool).get()["members"]) pool_list = self._exclude(self.client.pool(pool).get()["members"])
except Exception as e: except Exception as e: # noqa
raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) raise AnsibleError(f"Proxmoxer API error: {to_native(e)}") from e
members = [ members = [
member["name"] member["name"]
@ -266,13 +290,13 @@ class InventoryModule(BaseInventoryPlugin):
def verify_file(self, path): def verify_file(self, path):
"""Verify the Proxmox VE configuration file.""" """Verify the Proxmox VE configuration file."""
if super(InventoryModule, self).verify_file(path): if super().verify_file(path):
endings = ("proxmox.yaml", "proxmox.yml") endings = ("proxmox.yaml", "proxmox.yml")
if any((path.endswith(ending) for ending in endings)): if any(path.endswith(ending) for ending in endings):
return True return True
return False return False
def parse(self, inventory, loader, path, cache=True): def parse(self, inventory, loader, path, cache=True): # noqa
"""Dynamically parse the Proxmox VE cloud inventory.""" """Dynamically parse the Proxmox VE cloud inventory."""
if not HAS_PROXMOXER: if not HAS_PROXMOXER:
raise AnsibleError( raise AnsibleError(
@ -280,7 +304,7 @@ class InventoryModule(BaseInventoryPlugin):
"https://pypi.org/project/proxmoxer/" "https://pypi.org/project/proxmoxer/"
) )
super(InventoryModule, self).parse(inventory, loader, path) super().parse(inventory, loader, path)
self._read_config_data(path) self._read_config_data(path)
self.client = self._auth() self.client = self._auth()

View File

@ -1,3 +1,4 @@
#!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
IPtables raw module. IPtables raw module.
@ -18,16 +19,20 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details. GNU General Public License for more details.
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with Ansible. If not, see <http://www.gnu.org/licenses/>. along with Ansible. If not, see <http://www.gnu.org/licenses/>.
""" """
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'metadata_version': '1.0'} ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'metadata_version': '1.0'}
DOCUMENTATION = r''' DOCUMENTATION = r'''
--- ---
module: iptables_raw module: iptables_raw
short_description: Manage iptables rules short_description: Manage iptables rules
version_added: "2.4" version_added: 1.1.0
description: description:
- Add/remove iptables rules while keeping state. - Add/remove iptables rules while keeping state.
options: options:
@ -35,13 +40,14 @@ options:
description: description:
- Create a backup of the iptables state file before overwriting it. - Create a backup of the iptables state file before overwriting it.
required: false required: false
choices: ["yes", "no"] type: bool
default: "no" default: False
ipversion: ipversion:
description: description:
- Target the IP version this rule is for. - Target the IP version this rule is for.
required: false required: false
default: "4" default: "4"
type: str
choices: ["4", "6"] choices: ["4", "6"]
keep_unmanaged: keep_unmanaged:
description: description:
@ -54,8 +60,8 @@ options:
first time, since if you don't specify correct rules, you can block first time, since if you don't specify correct rules, you can block
yourself out of the managed host." yourself out of the managed host."
required: false required: false
choices: ["yes", "no"] type: bool
default: "yes" default: True
name: name:
description: description:
- Name that will be used as an identifier for these rules. It can contain - Name that will be used as an identifier for these rules. It can contain
@ -64,17 +70,21 @@ options:
C(state=absent) to flush all rules in the selected table, or even all C(state=absent) to flush all rules in the selected table, or even all
tables with C(table=*). tables with C(table=*).
required: true required: true
type: str
rules: rules:
description: description:
- The rules that we want to add. Accepts multiline values. - The rules that we want to add. Accepts multiline values.
- "Note: You can only use C(-A)/C(--append), C(-N)/C(--new-chain), and - "Note: You can only use C(-A)/C(--append), C(-N)/C(--new-chain), and
C(-P)/C(--policy) to specify rules." C(-P)/C(--policy) to specify rules."
required: false required: false
type: str
default: ""
state: state:
description: description:
- The state this rules fragment should be in. - The state this rules fragment should be in.
choices: ["present", "absent"] choices: ["present", "absent"]
required: false required: false
type: str
default: present default: present
table: table:
description: description:
@ -82,12 +92,13 @@ options:
with C(name=*) and C(state=absent) to flush all rules in all tables. with C(name=*) and C(state=absent) to flush all rules in all tables.
choices: ["filter", "nat", "mangle", "raw", "security", "*"] choices: ["filter", "nat", "mangle", "raw", "security", "*"]
required: false required: false
type: str
default: filter default: filter
weight: weight:
description: description:
- Determines the order of the rules. Lower C(weight) means higher - Determines the order of the rules. Lower C(weight) means higher
priority. Supported range is C(0 - 99) priority. Supported range is C(0 - 99)
choices: ["0 - 99"] type: int
required: false required: false
default: 40 default: 40
notes: notes:
@ -116,7 +127,7 @@ EXAMPLES = '''
- iptables_raw: - iptables_raw:
name: default_rules name: default_rules
weight: 10 weight: 10
keep_unmanaged: no keep_unmanaged: false
rules: | rules: |
-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT -A INPUT -i lo -j ACCEPT
@ -156,12 +167,12 @@ RETURN = '''
state: state:
description: state of the rules description: state of the rules
returned: success returned: success
type: string type: str
sample: present sample: present
name: name:
description: name of the rules description: name of the rules
returned: success returned: success
type: string type: str
sample: open_tcp_80 sample: open_tcp_80
weight: weight:
description: weight of the rules description: weight of the rules
@ -176,22 +187,22 @@ ipversion:
rules: rules:
description: passed rules description: passed rules
returned: success returned: success
type: string type: str
sample: "-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT" sample: "-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT"
table: table:
description: iptables table used description: iptables table used
returned: success returned: success
type: string type: str
sample: filter sample: filter
backup: backup:
description: if the iptables file should backed up description: if the iptables file should backed up
returned: success returned: success
type: boolean type: bool
sample: False sample: False
keep_unmanaged: keep_unmanaged:
description: if it should keep unmanaged rules description: if it should keep unmanaged rules
returned: success returned: success
type: boolean type: bool
sample: True sample: True
''' '''

View File

@ -1,89 +1,109 @@
#!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""OpenSSL PKCS12 module.""" """OpenSSL PKCS12 module."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {"metadata_version": "1.0", "status": ["preview"], "supported_by": "community"} ANSIBLE_METADATA = {"metadata_version": "1.0", "status": ["preview"], "supported_by": "community"}
DOCUMENTATION = """ DOCUMENTATION = """
--- ---
module: openssl_pkcs12 module: openssl_pkcs12
author: "Guillaume Delpierre (@gdelpierre)" author: "Guillaume Delpierre (@gdelpierre)"
version_added: "2.4" version_added: 1.1.0
short_description: Generate OpenSSL pkcs12 archive. short_description: Generate OpenSSL pkcs12 archive.
description: description:
- "This module allows one to (re-)generate PKCS#12." - "This module allows one to (re-)generate PKCS#12."
requirements: requirements:
- "python-pyOpenSSL" - "python-pyOpenSSL"
extends_documentation_fragment: files
options: options:
ca_certificates: ca_certificates:
required: False required: False
description: type: list
- List of CA certificate to include. elements: str
cert_path: description:
required: False - List of CA certificate to include.
description: cert_path:
- The path to read certificates and private keys from. required: False
Must be in PEM format. type: path
action: description:
required: False - The path to read certificates and private keys from.
default: "export" Must be in PEM format.
choices: ["parse", "export"] action:
description: required: False
- Create (export) or parse a PKCS#12. default: "export"
src: choices: ["parse", "export"]
required: False type: str
description: description:
- PKCS#12 file path to parse. - Create (export) or parse a PKCS#12.
path: src:
required: True required: False
default: null type: path
description: description:
- Filename to write the PKCS#12 file to. - PKCS#12 file path to parse.
force: path:
required: False required: True
default: False type: path
description: description:
- Should the file be regenerated even it it already exists. - Filename to write the PKCS#12 file to.
friendly_name: force:
required: False required: False
default: null default: False
aliases: "name" type: bool
description: description:
- Specifies the friendly name for the certificate and private key. - Should the file be regenerated even it it already exists.
iter_size: friendly_name:
required: False required: False
default: 2048 type: str
description: aliases:
- Number of times to repeat the encryption step. - "name"
maciter_size: description:
required: False - Specifies the friendly name for the certificate and private key.
default: 1 iter_size:
description: required: False
- Number of times to repeat the MAC step. default: 2048
mode: type: int
required: False description:
default: 0400 - Number of times to repeat the encryption step.
description: maciter_size:
- Default mode for the generated PKCS#12 file. required: False
passphrase: default: 1
required: False type: int
default: null description:
description: - Number of times to repeat the MAC step.
- The PKCS#12 password. mode:
privatekey_path: required: False
required: False default: "0400"
description: type: str
- File to read private key from. description:
privatekey_passphrase: - Default mode for the generated PKCS#12 file.
required: False passphrase:
default: null required: False
description: type: str
- Passphrase source to decrypt any input private keys with. description:
state: - The PKCS#12 password.
required: False privatekey_path:
default: "present" required: False
choices: ["present", "absent"] type: path
description: description:
- Whether the file should exist or not. - File to read private key from.
privatekey_passphrase:
required: False
type: str
description:
- Passphrase source to decrypt any input private keys with.
state:
required: False
default: "present"
choices: ["present", "absent"]
type: str
description:
- Whether the file should exist or not.
""" """
EXAMPLES = """ EXAMPLES = """
@ -131,10 +151,10 @@ EXAMPLES = """
RETURN = """ RETURN = """
filename: filename:
description: Path to the generate PKCS#12 file. description: Path to the generate PKCS#12 file.
returned: changed or success returned: changed or success
type: string type: str
sample: /opt/certs/ansible.p12 sample: /opt/certs/ansible.p12
""" """
import errno import errno
@ -151,11 +171,11 @@ else:
pyopenssl_found = True pyopenssl_found = True
class PkcsError(Exception): class PkcsError(Exception): # noqa
pass pass
class Pkcs(object): class Pkcs(object): # noqa
def __init__(self, module): def __init__(self, module):
self.path = module.params["path"] self.path = module.params["path"]
@ -181,36 +201,37 @@ class Pkcs(object):
def load_privatekey(self, path, passphrase=None): def load_privatekey(self, path, passphrase=None):
"""Load the specified OpenSSL private key.""" """Load the specified OpenSSL private key."""
try: try:
if passphrase: privatekey = crypto.load_privatekey(
privatekey = crypto.load_privatekey( crypto.FILETYPE_PEM,
crypto.FILETYPE_PEM, open(path, "rb").read(), # noqa
open(path, "rb").read(), passphrase passphrase
) ) if passphrase else crypto.load_privatekey(
else: crypto.FILETYPE_PEM,
privatekey = crypto.load_privatekey(crypto.FILETYPE_PEM, open(path, "rb").read()) open(path, "rb").read() # noqa
)
return privatekey return privatekey
except (IOError, OSError) as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
def load_certificate(self, path): def load_certificate(self, path):
"""Load the specified certificate.""" """Load the specified certificate."""
try: try:
cert_content = open(path, "rb").read() cert_content = open(path, "rb").read() # noqa
cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
return cert return cert
except (IOError, OSError) as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
def load_pkcs12(self, path, passphrase=None): def load_pkcs12(self, path, passphrase=None):
"""Load pkcs12 file.""" """Load pkcs12 file."""
try: try:
if passphrase: if passphrase:
return crypto.load_pkcs12(open(path, "rb").read(), passphrase) return crypto.load_pkcs12(open(path, "rb").read(), passphrase) # noqa
else:
return crypto.load_pkcs12(open(path, "rb").read()) return crypto.load_pkcs12(open(path, "rb").read()) # noqa
except (IOError, OSError) as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
def dump_privatekey(self, path): def dump_privatekey(self, path):
"""Dump the specified OpenSSL private key.""" """Dump the specified OpenSSL private key."""
@ -219,8 +240,8 @@ class Pkcs(object):
crypto.FILETYPE_PEM, crypto.FILETYPE_PEM,
self.load_pkcs12(path).get_privatekey() self.load_pkcs12(path).get_privatekey()
) )
except (IOError, OSError) as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
def dump_certificate(self, path): def dump_certificate(self, path):
"""Dump the specified certificate.""" """Dump the specified certificate."""
@ -229,8 +250,8 @@ class Pkcs(object):
crypto.FILETYPE_PEM, crypto.FILETYPE_PEM,
self.load_pkcs12(path).get_certificate() self.load_pkcs12(path).get_certificate()
) )
except (IOError, OSError) as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
def generate(self, module): def generate(self, module):
"""Generate PKCS#12 file archive.""" """Generate PKCS#12 file archive."""
@ -264,9 +285,9 @@ class Pkcs(object):
) )
module.set_mode_if_different(self.path, self.mode, False) module.set_mode_if_different(self.path, self.mode, False)
self.changed = True self.changed = True
except (IOError, OSError) as exc: except OSError as exc:
self.remove() self.remove()
raise PkcsError(exc) raise PkcsError(exc) from exc
file_args = module.load_file_common_arguments(module.params) file_args = module.load_file_common_arguments(module.params)
if module.set_fs_attributes_if_different(file_args, False): if module.set_fs_attributes_if_different(file_args, False):
@ -281,14 +302,12 @@ class Pkcs(object):
with open(self.path, "wb") as content: with open(self.path, "wb") as content:
content.write( content.write(
"{0}{1}".format( f"{self.dump_privatekey(self.src)}{self.dump_certificate(self.src)}"
self.dump_privatekey(self.src), self.dump_certificate(self.src)
)
) )
module.set_mode_if_different(self.path, self.mode, False) module.set_mode_if_different(self.path, self.mode, False)
self.changed = True self.changed = True
except IOError as exc: except OSError as exc:
raise PkcsError(exc) raise PkcsError(exc) from exc
file_args = module.load_file_common_arguments(module.params) file_args = module.load_file_common_arguments(module.params)
if module.set_fs_attributes_if_different(file_args, False): if module.set_fs_attributes_if_different(file_args, False):
@ -302,11 +321,11 @@ class Pkcs(object):
self.changed = True self.changed = True
except OSError as exc: except OSError as exc:
if exc.errno != errno.ENOENT: if exc.errno != errno.ENOENT:
raise PkcsError(exc) raise PkcsError(exc) from exc
else:
pass
def check(self, module, perms_required=True): pass
def check(self, module, perms_required=True): # noqa
def _check_pkey_passphrase(): def _check_pkey_passphrase():
if self.privatekey_passphrase: if self.privatekey_passphrase:
@ -337,19 +356,20 @@ class Pkcs(object):
def main(): def main():
argument_spec = dict( argument_spec = dict(
action=dict(default="export", choices=["parse", "export"], type="str"), action=dict(default="export", choices=["parse", "export"], type="str", required=False),
ca_certificates=dict(type="list"), ca_certificates=dict(type="list", elements="str", required=False),
cert_path=dict(type="path"), cert_path=dict(type="path"),
force=dict(default=False, type="bool"), force=dict(default=False, type="bool"),
friendly_name=dict(type="str", aliases=["name"]), friendly_name=dict(type="str", aliases=["name"]),
iter_size=dict(default=2048, type="int"), iter_size=dict(default=2048, type="int"),
maciter_size=dict(default=1, type="int"), maciter_size=dict(default=1, type="int"),
passphrase=dict(type="str", no_log=True), passphrase=dict(type="str", no_log=True),
path=dict(required=True, type="path"), path=dict(type="path", required=True),
privatekey_path=dict(type="path"), privatekey_path=dict(type="path"),
privatekey_passphrase=dict(type="str", no_log=True), privatekey_passphrase=dict(type="str", no_log=True),
state=dict(default="present", choices=["present", "absent"], type="str"), state=dict(default="present", choices=["present", "absent"], type="str"),
src=dict(type="path"), src=dict(type="path"),
mode=dict(default="0400", type="str", required=False)
) )
required_if = [ required_if = [
@ -376,8 +396,7 @@ def main():
if not os.path.isdir(base_dir): if not os.path.isdir(base_dir):
module.fail_json( module.fail_json(
name=base_dir, name=base_dir,
msg="The directory {0} does not exist or " msg=f"The directory {base_dir} does not exist or the file is not a directory"
"the file is not a directory".format(base_dir)
) )
pkcs12 = Pkcs(module) pkcs12 = Pkcs(module)

View File

@ -1,20 +1,69 @@
#!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright: (c) 2016, Abdoul Bah (@helldorado) <bahabdoul at gmail.com> # Copyright: (c) 2016, Abdoul Bah (@helldorado) <bahabdoul at gmail.com>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Module to control Proxmox KVM machines.""" """Control Proxmox KVM machines."""
from __future__ import absolute_import, division, print_function from __future__ import (absolute_import, division, print_function)
__metaclass__ = type __metaclass__ = type
DOCUMENTATION = """ DOCUMENTATION = r"""
--- ---
module: proxmox_kvm module: proxmox_kvm
short_description: Management of Qemu(KVM) Virtual Machines in Proxmox VE cluster. short_description: Management of Qemu(KVM) Virtual Machines in Proxmox VE cluster.
description: description:
- Allows you to create/delete/stop Qemu(KVM) Virtual Machines in Proxmox VE cluster. - Allows you to create/delete/stop Qemu(KVM) Virtual Machines in Proxmox VE cluster.
author: "Abdoul Bah (@helldorado) <bahabdoul at gmail.com>" author: "Abdoul Bah (@helldorado) <bahabdoul at gmail.com>"
version_added: 1.1.0
options: options:
vmid:
description:
- Specifies the instance ID.
- If not set the next available ID will be fetched from ProxmoxAPI.
type: int
node:
description:
- Proxmox VE node on which to operate.
- Only required for I(state=present).
- For every other states it will be autodiscovered.
type: str
pool:
description:
- Add the new VM to the specified pool.
type: str
api_host:
description:
- Specify the target host of the Proxmox VE cluster.
type: str
required: true
api_user:
description:
- Specify the user to authenticate with.
type: str
required: true
api_password:
description:
- Specify the password to authenticate with.
- You can use C(PROXMOX_PASSWORD) environment variable.
type: str
api_token_id:
description:
- Specify the token ID.
type: str
version_added: 1.3.0
api_token_secret:
description:
- Specify the token secret.
type: str
version_added: 1.3.0
verify_ssl:
description:
- If C(false), SSL certificates will not be validated.
- This should only be used on personally controlled sites using self-signed certificates.
type: bool
default: True
acpi: acpi:
description: description:
- Specify if ACPI should be enabled/disabled. - Specify if ACPI should be enabled/disabled.
@ -49,7 +98,7 @@ options:
description: description:
- Specify the BIOS implementation. - Specify the BIOS implementation.
type: str type: str
choices: ['seabios', 'ovmf'] choices: ["seabios", "ovmf"]
boot: boot:
description: description:
- Specify the boot order -> boot on floppy C(a), hard disk C(c), CD-ROM C(d), or network C(n). - Specify the boot order -> boot on floppy C(a), hard disk C(c), CD-ROM C(d), or network C(n).
@ -63,27 +112,23 @@ options:
type: str type: str
cicustom: cicustom:
description: description:
- 'cloud-init: Specify custom files to replace the automatically generated ones at start.' - "cloud-init: Specify custom files to replace the automatically generated ones at start."
type: str type: str
version_added: 1.3.0
cipassword: cipassword:
description: description:
- 'cloud-init: password of default user to create.' - "cloud-init: password of default user to create."
type: str type: str
version_added: 1.3.0
citype: citype:
description: description:
- 'cloud-init: Specifies the cloud-init configuration format.' - "cloud-init: Specifies the cloud-init configuration format."
- The default depends on the configured operating system type (C(ostype)). - The default depends on the configured operating system type (C(ostype)).
- We use the C(nocloud) format for Linux, and C(configdrive2) for Windows. - We use the C(nocloud) format for Linux, and C(configdrive2) for Windows.
type: str type: str
choices: ['nocloud', 'configdrive2'] choices: ["nocloud", "configdrive2"]
version_added: 1.3.0
ciuser: ciuser:
description: description:
- 'cloud-init: username of default user to create.' - "cloud-init: username of default user to create."
type: str type: str
version_added: 1.3.0
clone: clone:
description: description:
- Name of VM to be cloned. If C(vmid) is setted, C(clone) can take arbitrary value but required for initiating the clone. - Name of VM to be cloned. If C(vmid) is setted, C(clone) can take arbitrary value but required for initiating the clone.
@ -142,7 +187,8 @@ options:
option has a default of C(qcow2). If I(proxmox_default_behavior) is set to C(no_defaults), option has a default of C(qcow2). If I(proxmox_default_behavior) is set to C(no_defaults),
not specifying this option is equivalent to setting it to C(unspecified). not specifying this option is equivalent to setting it to C(unspecified).
type: str type: str
choices: [ "cloop", "cow", "qcow", "qcow2", "qed", "raw", "vmdk", "unspecified" ] choices:
["cloop", "cow", "qcow", "qcow2", "qed", "raw", "vmdk", "unspecified"]
freeze: freeze:
description: description:
- Specify if PVE should freeze CPU at startup (use 'c' monitor command to start execution). - Specify if PVE should freeze CPU at startup (use 'c' monitor command to start execution).
@ -153,7 +199,7 @@ options:
- For VM templates, we try to create a linked clone by default. - For VM templates, we try to create a linked clone by default.
- Used only with clone - Used only with clone
type: bool type: bool
default: 'yes' default: "yes"
hostpci: hostpci:
description: description:
- Specify a hash/dictionary of map host pci devices into guest. C(hostpci='{"key":"value", "key":"value"}'). - Specify a hash/dictionary of map host pci devices into guest. C(hostpci='{"key":"value", "key":"value"}').
@ -175,7 +221,7 @@ options:
description: description:
- Enable/disable hugepages memory. - Enable/disable hugepages memory.
type: str type: str
choices: ['any', '2', '1024'] choices: ["any", "2", "1024"]
ide: ide:
description: description:
- A hash/dictionary of volume used as IDE hard disk or CD-ROM. C(ide='{"key":"value", "key":"value"}'). - A hash/dictionary of volume used as IDE hard disk or CD-ROM. C(ide='{"key":"value", "key":"value"}').
@ -187,17 +233,16 @@ options:
type: dict type: dict
ipconfig: ipconfig:
description: description:
- 'cloud-init: Set the IP configuration.' - "cloud-init: Set the IP configuration."
- A hash/dictionary of network ip configurations. C(ipconfig='{"key":"value", "key":"value"}'). - A hash/dictionary of network ip configurations. C(ipconfig='{"key":"value", "key":"value"}').
- Keys allowed are - C(ipconfig[n]) where 0 n network interfaces. - Keys allowed are - C(ipconfig[n]) where 0 n network interfaces.
- Values allowed are - C("[gw=<GatewayIPv4>] [,gw6=<GatewayIPv6>] [,ip=<IPv4Format/CIDR>] [,ip6=<IPv6Format/CIDR>]"). - Values allowed are - C("[gw=<GatewayIPv4>] [,gw6=<GatewayIPv6>] [,ip=<IPv4Format/CIDR>] [,ip6=<IPv6Format/CIDR>]").
- 'cloud-init: Specify IP addresses and gateways for the corresponding interface.' - "cloud-init: Specify IP addresses and gateways for the corresponding interface."
- IP addresses use CIDR notation, gateways are optional but they should be in the same subnet of specified IP address. - IP addresses use CIDR notation, gateways are optional but they should be in the same subnet of specified IP address.
- The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided. - The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit gateway should be provided.
- For IPv6 the special string 'auto' can be used to use stateless autoconfiguration. - For IPv6 the special string 'auto' can be used to use stateless autoconfiguration.
- If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4. - If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using dhcp on IPv4.
type: dict type: dict
version_added: 1.3.0
keyboard: keyboard:
description: description:
- Sets the keyboard layout for VNC server. - Sets the keyboard layout for VNC server.
@ -217,7 +262,7 @@ options:
description: description:
- Lock/unlock the VM. - Lock/unlock the VM.
type: str type: str
choices: ['migrate', 'backup', 'snapshot', 'rollback'] choices: ["migrate", "backup", "snapshot", "rollback"]
machine: machine:
description: description:
- Specifies the Qemu machine type. - Specifies the Qemu machine type.
@ -245,11 +290,10 @@ options:
type: str type: str
nameservers: nameservers:
description: description:
- 'cloud-init: DNS server IP address(es).' - "cloud-init: DNS server IP address(es)."
- If unset, PVE host settings are used. - If unset, PVE host settings are used.
type: list type: list
elements: str elements: str
version_added: 1.3.0
net: net:
description: description:
- A hash/dictionary of network interfaces for the VM. C(net='{"key":"value", "key":"value"}'). - A hash/dictionary of network interfaces for the VM. C(net='{"key":"value", "key":"value"}').
@ -293,7 +337,21 @@ options:
- If I(proxmox_default_behavior) is set to C(compatiblity) (the default value), this - If I(proxmox_default_behavior) is set to C(compatiblity) (the default value), this
option has a default of C(l26). option has a default of C(l26).
type: str type: str
choices: ['other', 'wxp', 'w2k', 'w2k3', 'w2k8', 'wvista', 'win7', 'win8', 'win10', 'l24', 'l26', 'solaris'] choices:
[
"other",
"wxp",
"w2k",
"w2k3",
"w2k8",
"wvista",
"win7",
"win8",
"win10",
"l24",
"l26",
"solaris",
]
parallel: parallel:
description: description:
- A hash/dictionary of map host parallel devices. C(parallel='{"key":"value", "key":"value"}'). - A hash/dictionary of map host parallel devices. C(parallel='{"key":"value", "key":"value"}').
@ -334,14 +392,21 @@ options:
description: description:
- Specifies the SCSI controller model. - Specifies the SCSI controller model.
type: str type: str
choices: ['lsi', 'lsi53c810', 'virtio-scsi-pci', 'virtio-scsi-single', 'megasas', 'pvscsi'] choices:
[
"lsi",
"lsi53c810",
"virtio-scsi-pci",
"virtio-scsi-single",
"megasas",
"pvscsi",
]
searchdomains: searchdomains:
description: description:
- 'cloud-init: Sets DNS search domain(s).' - "cloud-init: Sets DNS search domain(s)."
- If unset, PVE host settings are used. - If unset, PVE host settings are used.
type: list type: list
elements: str elements: str
version_added: 1.3.0
serial: serial:
description: description:
- A hash/dictionary of serial device to create inside the VM. C('{"key":"value", "key":"value"}'). - A hash/dictionary of serial device to create inside the VM. C('{"key":"value", "key":"value"}').
@ -377,9 +442,8 @@ options:
type: int type: int
sshkeys: sshkeys:
description: description:
- 'cloud-init: SSH key to assign to the default user. NOT TESTED with multiple keys but a multi-line value should work.' - "cloud-init: SSH key to assign to the default user. NOT TESTED with multiple keys but a multi-line value should work."
type: str type: str
version_added: 1.3.0
startdate: startdate:
description: description:
- Sets the initial date of the real time clock. - Sets the initial date of the real time clock.
@ -396,7 +460,7 @@ options:
- Indicates desired state of the instance. - Indicates desired state of the instance.
- If C(current), the current state of the VM will be fetched. You can access it with C(results.status) - If C(current), the current state of the VM will be fetched. You can access it with C(results.status)
type: str type: str
choices: ['present', 'started', 'absent', 'stopped', 'restarted','current'] choices: ["present", "started", "absent", "stopped", "restarted", "current"]
default: present default: present
storage: storage:
description: description:
@ -415,7 +479,6 @@ options:
- Tags are only available in Proxmox 6+. - Tags are only available in Proxmox 6+.
type: list type: list
elements: str elements: str
version_added: 2.3.0
target: target:
description: description:
- Target node. Only allowed if the original VM is on shared storage. - Target node. Only allowed if the original VM is on shared storage.
@ -441,7 +504,7 @@ options:
- If C(yes), the VM will be updated with new value. - If C(yes), the VM will be updated with new value.
- Update of C(pool) is disabled. It needs an additional API endpoint not covered by this module. - Update of C(pool) is disabled. It needs an additional API endpoint not covered by this module.
type: bool type: bool
default: 'no' default: "no"
vcpus: vcpus:
description: description:
- Sets number of hotplugged vcpus. - Sets number of hotplugged vcpus.
@ -452,7 +515,20 @@ options:
- If I(proxmox_default_behavior) is set to C(compatiblity) (the default value), this - If I(proxmox_default_behavior) is set to C(compatiblity) (the default value), this
option has a default of C(std). option has a default of C(std).
type: str type: str
choices: ['std', 'cirrus', 'vmware', 'qxl', 'serial0', 'serial1', 'serial2', 'serial3', 'qxl2', 'qxl3', 'qxl4'] choices:
[
"std",
"cirrus",
"vmware",
"qxl",
"serial0",
"serial1",
"serial2",
"serial3",
"qxl2",
"qxl3",
"qxl4",
]
virtio: virtio:
description: description:
- A hash/dictionary of volume used as VIRTIO hard disk. C(virtio='{"key":"value", "key":"value"}'). - A hash/dictionary of volume used as VIRTIO hard disk. C(virtio='{"key":"value", "key":"value"}').
@ -477,10 +553,6 @@ options:
choices: choices:
- compatibility - compatibility
- no_defaults - no_defaults
version_added: "1.3.0"
extends_documentation_fragment:
- xoxys.general.proxmox.documentation
- xoxys.general.proxmox.selection
""" # noqa """ # noqa
EXAMPLES = """ EXAMPLES = """
@ -509,8 +581,8 @@ EXAMPLES = """
name: spynal name: spynal
node: sabrewulf node: sabrewulf
net: net:
net0: 'virtio,bridge=vmbr1,rate=200' net0: "virtio,bridge=vmbr1,rate=200"
net1: 'e1000,bridge=vmbr2' net1: "e1000,bridge=vmbr2"
- name: Create new VM with one network interface, three virto hard disk, 4 cores, and 2 vcpus - name: Create new VM with one network interface, three virto hard disk, 4 cores, and 2 vcpus
xoxys.general.proxmox_kvm: xoxys.general.proxmox_kvm:
@ -520,11 +592,11 @@ EXAMPLES = """
name: spynal name: spynal
node: sabrewulf node: sabrewulf
net: net:
net0: 'virtio,bridge=vmbr1,rate=200' net0: "virtio,bridge=vmbr1,rate=200"
virtio: virtio:
virtio0: 'VMs_LVM:10' virtio0: "VMs_LVM:10"
virtio1: 'VMs:2,format=qcow2' virtio1: "VMs:2,format=qcow2"
virtio2: 'VMs:5,format=raw' virtio2: "VMs:5,format=raw"
cores: 4 cores: 4
vcpus: 2 vcpus: 2
@ -599,15 +671,15 @@ EXAMPLES = """
api_host: helldorado api_host: helldorado
name: spynal name: spynal
ide: ide:
ide2: 'local:cloudinit,format=qcow2' ide2: "local:cloudinit,format=qcow2"
ciuser: mylinuxuser ciuser: mylinuxuser
cipassword: supersecret cipassword: supersecret
searchdomains: 'mydomain.internal' searchdomains: "mydomain.internal"
nameservers: 1.1.1.1 nameservers: 1.1.1.1
net: net:
net0: 'virtio,bridge=vmbr1,tag=77' net0: "virtio,bridge=vmbr1,tag=77"
ipconfig: ipconfig:
ipconfig0: 'ip=192.168.1.1/24,gw=192.168.1.1' ipconfig0: "ip=192.168.1.1/24,gw=192.168.1.1"
- name: Create new VM using Cloud-Init with an ssh key - name: Create new VM using Cloud-Init with an ssh key
xoxys.general.proxmox_kvm: xoxys.general.proxmox_kvm:
@ -617,16 +689,16 @@ EXAMPLES = """
api_host: helldorado api_host: helldorado
name: spynal name: spynal
ide: ide:
ide2: 'local:cloudinit,format=qcow2' ide2: "local:cloudinit,format=qcow2"
sshkeys: 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILJkVm98B71lD5XHfihwcYHE9TVpsJmK1vR1JcaU82L+' sshkeys: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILJkVm98B71lD5XHfihwcYHE9TVpsJmK1vR1JcaU82L+"
searchdomains: 'mydomain.internal' searchdomains: "mydomain.internal"
nameservers: nameservers:
- '1.1.1.1' - "1.1.1.1"
- '8.8.8.8' - "8.8.8.8"
net: net:
net0: 'virtio,bridge=vmbr1,tag=77' net0: "virtio,bridge=vmbr1,tag=77"
ipconfig: ipconfig:
ipconfig0: 'ip=192.168.1.1/24' ipconfig0: "ip=192.168.1.1/24"
- name: Start VM - name: Start VM
xoxys.general.proxmox_kvm: xoxys.general.proxmox_kvm:
@ -701,7 +773,7 @@ EXAMPLES = """
api_host: helldorado api_host: helldorado
name: spynal name: spynal
node: sabrewulf node: sabrewulf
delete: 'args,template,cpulimit' delete: "args,template,cpulimit"
- name: Revert a pending change - name: Revert a pending change
xoxys.general.proxmox_kvm: xoxys.general.proxmox_kvm:
@ -710,8 +782,9 @@ EXAMPLES = """
api_host: helldorado api_host: helldorado
name: spynal name: spynal
node: sabrewulf node: sabrewulf
revert: 'template,cpulimit' revert: "template,cpulimit"
""" # noqa
"""
RETURN = """ RETURN = """
vmid: vmid:
@ -735,9 +808,10 @@ import re
import string import string
import time import time
import traceback import traceback
from distutils.version import LooseVersion
from ansible.module_utils.six.moves.urllib.parse import quote
from collections import defaultdict from collections import defaultdict
from distutils.version import LooseVersion
from ansible.module_utils.six.moves.urllib.parse import quote
try: try:
from proxmoxer import ProxmoxAPI from proxmoxer import ProxmoxAPI
@ -745,17 +819,27 @@ try:
except ImportError: except ImportError:
HAS_PROXMOXER = False HAS_PROXMOXER = False
from ansible.module_utils.basic import AnsibleModule, env_fallback try:
from requests.packages import urllib3
HAS_URLLIB3 = True
except ImportError:
try:
import urllib3
HAS_URLLIB3 = True
except ImportError:
HAS_URLLIB3 = False
from ansible.module_utils._text import to_native from ansible.module_utils._text import to_native
from ansible.module_utils.basic import AnsibleModule, env_fallback
def get_nextvmid(module, proxmox): def get_nextvmid(module, proxmox):
try: try:
vmid = proxmox.cluster.nextid.get() vmid = proxmox.cluster.nextid.get()
return vmid return vmid
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(
msg="Unable to get next vmid. Failed with exception: {}".format(to_native(e)), msg=f"Unable to get next vmid. Failed with exception: {to_native(e)}",
exception=traceback.format_exc() exception=traceback.format_exc()
) )
@ -775,14 +859,13 @@ def node_check(proxmox, node):
def get_vminfo(module, proxmox, node, vmid, **kwargs): def get_vminfo(module, proxmox, node, vmid, **kwargs):
global results # noqa global results
results = {} results = {}
try: try:
vm = proxmox.nodes(node).qemu(vmid).config.get() vm = proxmox.nodes(node).qemu(vmid).config.get()
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(
msg="Getting information for VM with vmid = {} failed with exception: {}". msg=f"Getting information for VM with vmid = {vmid} failed with exception: {e}"
format(vmid, e)
) )
# Sanitize kwargs. Remove not defined args and ensure True and False converted to int. # Sanitize kwargs. Remove not defined args and ensure True and False converted to int.
@ -800,16 +883,13 @@ def get_vminfo(module, proxmox, node, vmid, **kwargs):
results["_raw"] = vm results["_raw"] = vm
def settings(module, proxmox, vmid, node, name, **kwargs): def settings(module, proxmox, vmid, node, name, **kwargs): # noqa
proxmox_node = proxmox.nodes(node) proxmox_node = proxmox.nodes(node)
# Sanitize kwargs. Remove not defined args and ensure True and False converted to int. # Sanitize kwargs. Remove not defined args and ensure True and False converted to int.
kwargs = dict((k, v) for k, v in kwargs.items() if v is not None) kwargs = dict((k, v) for k, v in kwargs.items() if v is not None)
if proxmox_node.qemu(vmid).config.set(**kwargs) is None: return (proxmox_node.qemu(vmid).config.set(**kwargs) is None)
return True
else:
return False
def wait_for_task(module, proxmox, node, taskid): def wait_for_task(module, proxmox, node, taskid):
@ -840,7 +920,7 @@ def create_vm(
clone_params = {} clone_params = {}
# Default args for vm. Note: -args option is for experts only. It allows you # Default args for vm. Note: -args option is for experts only. It allows you
# to pass arbitrary arguments to kvm. # to pass arbitrary arguments to kvm.
vm_args = "-serial unix:/var/run/qemu-server/{0}.serial,server,nowait".format(vmid) vm_args = f"-serial unix:/var/run/qemu-server/{vmid}.serial,server,nowait"
proxmox_node = proxmox.nodes(node) proxmox_node = proxmox.nodes(node)
@ -849,13 +929,13 @@ def create_vm(
kwargs.update(dict([k, int(v)] for k, v in kwargs.items() if isinstance(v, bool))) kwargs.update(dict([k, int(v)] for k, v in kwargs.items() if isinstance(v, bool)))
# The features work only on PVE 4+ # The features work only on PVE 4+
if PVE_MAJOR_VERSION < 4: # noqa if PVE_MAJOR_VERSION < 4:
for p in only_v4: for p in only_v4:
if p in kwargs: if p in kwargs:
del kwargs[p] del kwargs[p]
# The features work only on PVE 6 # The features work only on PVE 6
if PVE_MAJOR_VERSION < 6: # noqa if PVE_MAJOR_VERSION < 6:
for p in only_v6: for p in only_v6:
if p in kwargs: if p in kwargs:
del kwargs[p] del kwargs[p]
@ -877,17 +957,17 @@ def create_vm(
if update: if update:
for k, v in disks.items(): for k, v in disks.items():
if results["disks"].get(k): if results["disks"].get(k):
kwargs[k.rstrip(string.digits)][k] = "{0}:{1},{2}".format( storage_id = results["disks"][k]["storage_id"]
results["disks"][k]["storage_id"], results["disks"][k]["storage_opts"], storage_opts = results["disks"][k]["storage_opts"]
",".join(disks[k]["opts"]) opts = ",".join(v["opts"])
) kwargs[k.rstrip(string.digits)][k] = f"{storage_id}:{storage_opts},{opts}"
for k, v in nets.items(): for k, v in nets.items():
if results["nets"].get(k): if results["nets"].get(k):
kwargs[k.rstrip(string.digits)][k] = "{0}={1},{2}".format( net_id = results["nets"][k]["net_id"]
results["nets"][k]["net_id"], results["nets"][k]["net_opts"], net_opts = results["nets"][k]["net_opts"]
",".join(nets[k]["opts"]) opts = ",".join(v["opts"])
) kwargs[k.rstrip(string.digits)][k] = f"{net_id}={net_opts},{opts}"
# Convert all dict in kwargs to elements. # Convert all dict in kwargs to elements.
for k in list(kwargs.keys()): for k in list(kwargs.keys()):
@ -914,13 +994,15 @@ def create_vm(
if "tags" in kwargs: if "tags" in kwargs:
for tag in kwargs["tags"]: for tag in kwargs["tags"]:
if not re.match(r"^[a-z0-9_][a-z0-9_\-\+\.]*$", tag): if not re.match(r"^[a-z0-9_][a-z0-9_\-\+\.]*$", tag):
module.fail_json(msg="{} is not a valid tag".format(tag)) module.fail_json(msg=f"{tag} is not a valid tag")
kwargs["tags"] = ",".join(kwargs["tags"]) kwargs["tags"] = ",".join(kwargs["tags"])
# -args and skiplock require root@pam user - but can not use api tokens # -args and skiplock require root@pam user - but can not use api tokens
if module.params["api_user"] == "root@pam" and module.params["args"] is None: if (
if not update and module.params["proxmox_default_behavior"] == "compatibility": module.params["api_user"] == "root@pam" and module.params["args"] is None and not update
kwargs["args"] = vm_args and module.params["proxmox_default_behavior"] == "compatibility"
):
kwargs["args"] = vm_args
elif module.params["api_user"] == "root@pam" and module.params["args"] is not None: elif module.params["api_user"] == "root@pam" and module.params["args"] is not None:
kwargs["args"] = module.params["args"] kwargs["args"] = module.params["args"]
elif module.params["api_user"] != "root@pam" and module.params["args"] is not None: elif module.params["api_user"] != "root@pam" and module.params["args"] is not None:
@ -930,13 +1012,12 @@ def create_vm(
module.fail_json(msg="skiplock parameter require root@pam user. ") module.fail_json(msg="skiplock parameter require root@pam user. ")
if update: if update:
if proxmox_node.qemu(vmid).config.set( return (
name=name, memory=memory, cpu=cpu, cores=cores, sockets=sockets, **kwargs proxmox_node.qemu(vmid).config.
) is None: set(name=name, memory=memory, cpu=cpu, cores=cores, sockets=sockets, **kwargs) is None
return True )
else:
return False if module.params["clone"] is not None:
elif module.params["clone"] is not None:
for param in valid_clone_params: for param in valid_clone_params:
if module.params[param] is not None: if module.params[param] is not None:
clone_params[param] = module.params[param] clone_params[param] = module.params[param]
@ -952,8 +1033,7 @@ def create_vm(
if not wait_for_task(module, proxmox, node, taskid): if not wait_for_task(module, proxmox, node, taskid):
module.fail_json( module.fail_json(
msg="Reached timeout while waiting for creating VM." msg="Reached timeout while waiting for creating VM."
"Last line in task before timeout: {}". f"Last line in task before timeout: {proxmox_node.tasks(taskid).log.get()[:1]}"
format(proxmox_node.tasks(taskid).log.get()[:1])
) )
return False return False
return True return True
@ -966,8 +1046,7 @@ def start_vm(module, proxmox, vm):
if not wait_for_task(module, proxmox, vm[0]["node"], taskid): if not wait_for_task(module, proxmox, vm[0]["node"], taskid):
module.fail_json( module.fail_json(
msg="Reached timeout while waiting for starting VM." msg="Reached timeout while waiting for starting VM."
"Last line in task before timeout: {}". f"Last line in task before timeout: {proxmox_node.tasks(taskid).log.get()[:1]}"
format(proxmox_node.tasks(taskid).log.get()[:1])
) )
return False return False
return True return True
@ -980,8 +1059,7 @@ def stop_vm(module, proxmox, vm, force):
if not wait_for_task(module, proxmox, vm[0]["node"], taskid): if not wait_for_task(module, proxmox, vm[0]["node"], taskid):
module.fail_json( module.fail_json(
msg="Reached timeout while waiting for stopping VM." msg="Reached timeout while waiting for stopping VM."
"Last line in task before timeout: {}". f"Last line in task before timeout: {proxmox_node.tasks(taskid).log.get()[:1]}"
format(proxmox_node.tasks(taskid).log.get()[:1])
) )
return False return False
return True return True
@ -998,7 +1076,7 @@ def main():
acpi=dict(type="bool"), acpi=dict(type="bool"),
agent=dict(type="bool"), agent=dict(type="bool"),
args=dict(type="str"), args=dict(type="str"),
api_host=dict(required=True), api_host=dict(required=True, type="str"),
api_password=dict(no_log=True, fallback=(env_fallback, ["PROXMOX_PASSWORD"])), api_password=dict(no_log=True, fallback=(env_fallback, ["PROXMOX_PASSWORD"])),
api_token_id=dict(no_log=True), api_token_id=dict(no_log=True),
api_token_secret=dict(no_log=True), api_token_secret=dict(no_log=True),
@ -1044,7 +1122,7 @@ def main():
nameservers=dict(type="list", elements="str"), nameservers=dict(type="list", elements="str"),
net=dict(type="dict"), net=dict(type="dict"),
newid=dict(type="int"), newid=dict(type="int"),
node=dict(), node=dict(type="str"),
numa=dict(type="dict"), numa=dict(type="dict"),
numa_enabled=dict(type="bool"), numa_enabled=dict(type="bool"),
onboot=dict(type="bool"), onboot=dict(type="bool"),
@ -1076,7 +1154,7 @@ def main():
sockets=dict(type="int"), sockets=dict(type="int"),
sshkeys=dict(type="str", no_log=False), sshkeys=dict(type="str", no_log=False),
startdate=dict(type="str"), startdate=dict(type="str"),
startup=dict(), startup=dict(type="str"),
state=dict( state=dict(
default="present", default="present",
choices=["present", "absent", "stopped", "started", "restarted", "current"] choices=["present", "absent", "stopped", "started", "restarted", "current"]
@ -1089,7 +1167,7 @@ def main():
template=dict(type="bool"), template=dict(type="bool"),
timeout=dict(type="int", default=30), timeout=dict(type="int", default=30),
update=dict(type="bool", default=False), update=dict(type="bool", default=False),
validate_certs=dict(type="bool", default=False), verify_ssl=dict(type="bool", default=True),
vcpus=dict(type="int"), vcpus=dict(type="int"),
vga=dict( vga=dict(
choices=[ choices=[
@ -1099,7 +1177,7 @@ def main():
), ),
virtio=dict(type="dict"), virtio=dict(type="dict"),
vmid=dict(type="int"), vmid=dict(type="int"),
watchdog=dict(), watchdog=dict(type="str"),
proxmox_default_behavior=dict(type="str", choices=["compatibility", "no_defaults"]), proxmox_default_behavior=dict(type="str", choices=["compatibility", "no_defaults"]),
), ),
mutually_exclusive=[("delete", "revert"), ("delete", "update"), ("revert", "update"), mutually_exclusive=[("delete", "revert"), ("delete", "update"), ("revert", "update"),
@ -1130,7 +1208,7 @@ def main():
state = module.params["state"] state = module.params["state"]
update = bool(module.params["update"]) update = bool(module.params["update"])
vmid = module.params["vmid"] vmid = module.params["vmid"]
validate_certs = module.params["validate_certs"] verify_ssl = module.params["verify_ssl"]
if module.params["proxmox_default_behavior"] is None: if module.params["proxmox_default_behavior"] is None:
module.params["proxmox_default_behavior"] = "compatibility" module.params["proxmox_default_behavior"] = "compatibility"
@ -1166,15 +1244,16 @@ def main():
auth_args["token_name"] = api_token_id auth_args["token_name"] = api_token_id
auth_args["token_value"] = api_token_secret auth_args["token_value"] = api_token_secret
if not verify_ssl and HAS_URLLIB3:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
try: try:
proxmox = ProxmoxAPI(api_host, verify_ssl=validate_certs, **auth_args) proxmox = ProxmoxAPI(api_host, verify_ssl=verify_ssl, **auth_args)
global PVE_MAJOR_VERSION # noqa global PVE_MAJOR_VERSION
version = proxmox_version(proxmox) version = proxmox_version(proxmox)
PVE_MAJOR_VERSION = 3 if version < LooseVersion("4.0") else version.version[0] PVE_MAJOR_VERSION = 3 if version < LooseVersion("4.0") else version.version[0]
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(msg=f"authorization on proxmox cluster failed with exception: {e}")
msg="authorization on proxmox cluster failed with exception: {}".format(e)
)
# If vmid is not defined then retrieve its value from the vm name, # If vmid is not defined then retrieve its value from the vm name,
# the cloned vm name or retrieve the next free VM id from ProxmoxAPI. # the cloned vm name or retrieve the next free VM id from ProxmoxAPI.
@ -1182,16 +1261,16 @@ def main():
if state == "present" and not update and not clone and not delete and not revert: if state == "present" and not update and not clone and not delete and not revert:
try: try:
vmid = get_nextvmid(module, proxmox) vmid = get_nextvmid(module, proxmox)
except Exception: except Exception: # noqa
module.fail_json( module.fail_json(
msg="Can't get the next vmid for VM {0} automatically." msg=f"Can't get the next vmid for VM {name} automatically."
"Ensure your cluster state is good".format(name) "Ensure your cluster state is good"
) )
else: else:
clone_target = clone or name clone_target = clone or name
try: try:
vmid = get_vmid(proxmox, clone_target)[0] vmid = get_vmid(proxmox, clone_target)[0]
except Exception: except Exception: # noqa
vmid = -1 vmid = -1
if clone is not None: if clone is not None:
@ -1199,50 +1278,40 @@ def main():
if not newid: if not newid:
try: try:
newid = get_nextvmid(module, proxmox) newid = get_nextvmid(module, proxmox)
except Exception: except Exception: # noqa
module.fail_json( module.fail_json(
msg="Can't get the next vmid for VM {0} automatically." msg=f"Can't get the next vmid for VM {name} automatically."
"Ensure your cluster state is good".format(name) "Ensure your cluster state is good"
) )
# Ensure source VM name exists when cloning # Ensure source VM name exists when cloning
if -1 == vmid: if -1 == vmid:
module.fail_json(msg="VM with name = {} does not exist in cluster".format(clone)) module.fail_json(msg=f"VM with name = {clone} does not exist in cluster")
# Ensure source VM id exists when cloning # Ensure source VM id exists when cloning
if not get_vm(proxmox, vmid): if not get_vm(proxmox, vmid):
module.fail_json( module.fail_json(vmid=vmid, msg=f"VM with vmid = {vmid} does not exist in cluster")
vmid=vmid, msg="VM with vmid = {} does not exist in cluster".format(vmid)
)
# Ensure the choosen VM name doesn't already exist when cloning # Ensure the choosen VM name doesn't already exist when cloning
if get_vmid(proxmox, name): if get_vmid(proxmox, name):
module.exit_json( module.exit_json(changed=False, vmid=vmid, msg=f"VM with name <{name}> already exists")
changed=False, vmid=vmid, msg="VM with name <{}> already exists".format(name)
)
# Ensure the choosen VM id doesn't already exist when cloning # Ensure the choosen VM id doesn't already exist when cloning
if get_vm(proxmox, newid): if get_vm(proxmox, newid):
module.exit_json( module.exit_json(
changed=False, changed=False, vmid=vmid, msg=f"vmid {newid} with VM name {name} already exists"
vmid=vmid,
msg="vmid {} with VM name {} already exists".format(newid, name)
) )
if delete is not None: if delete is not None:
try: try:
settings(module, proxmox, vmid, node, name, delete=delete) settings(module, proxmox, vmid, node, name, delete=delete)
module.exit_json( module.exit_json(
changed=True, changed=True, vmid=vmid, msg=f"Settings has deleted on VM {name} with vmid {vmid}"
vmid=vmid,
msg="Settings has deleted on VM {0} with vmid {1}".format(name, vmid)
) )
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid,
msg="Unable to delete settings on VM {0} with vmid {1}: {2}".format( msg=f"Unable to delete settings on VM {name} with vmid {vmid}: {str(e)}"
name, vmid, str(e)
)
) )
if revert is not None: if revert is not None:
@ -1251,29 +1320,29 @@ def main():
module.exit_json( module.exit_json(
changed=True, changed=True,
vmid=vmid, vmid=vmid,
msg="Settings has reverted on VM {0} with vmid {1}".format(name, vmid) msg=f"Settings has reverted on VM {name} with vmid {vmid}"
) )
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid,
msg="Unable to revert settings on VM {0} with vmid {1}:" msg=f"Unable to revert settings on VM {name} with vmid {vmid}:"
"Maybe is not a pending task...".format(name, vmid) + str(e) f"Maybe is not a pending task...{str(e)}"
) )
if state == "present": if state == "present":
try: try:
if get_vm(proxmox, vmid) and not (update or clone): if get_vm(proxmox, vmid) and not (update or clone):
module.exit_json( module.exit_json(
changed=False, vmid=vmid, msg="VM with vmid <{}> already exists".format(vmid) changed=False, vmid=vmid, msg=f"VM with vmid <{vmid}> already exists"
) )
elif get_vmid(proxmox, name) and not (update or clone): elif get_vmid(proxmox, name) and not (update or clone):
module.exit_json( module.exit_json(
changed=False, vmid=vmid, msg="VM with name <{}> already exists".format(name) changed=False, vmid=vmid, msg=f"VM with name <{name}> already exists"
) )
elif not (node, name): elif not (node, name):
module.fail_json(msg="node, name is mandatory for creating/updating vm") module.fail_json(msg="node, name is mandatory for creating/updating vm")
elif not node_check(proxmox, node): elif not node_check(proxmox, node):
module.fail_json(msg="node '{}' does not exist in cluster".format(node)) module.fail_json(msg=f"node '{node}' does not exist in cluster")
if not clone: if not clone:
get_vminfo( get_vminfo(
@ -1362,130 +1431,100 @@ def main():
if update: if update:
module.exit_json( module.exit_json(
changed=True, vmid=vmid, msg="VM {} with vmid {} updated".format(name, vmid) changed=True, vmid=vmid, msg=f"VM {name} with vmid {vmid} updated"
) )
elif clone is not None: elif clone is not None:
module.exit_json( module.exit_json(
changed=True, changed=True,
vmid=vmid, vmid=vmid,
msg="VM {} with newid {} cloned from vm with vmid {}".format( msg=f"VM {name} with newid {newid} cloned from vm with vmid {vmid}"
name, newid, vmid
)
) )
else: else:
module.exit_json( module.exit_json(
changed=True, changed=True, msg=f"VM {name} with vmid {vmid} deployed", **results
msg="VM {} with vmid {} deployed".format(name, vmid),
**results # noqa
) )
except Exception as e: except Exception as e: # noqa
if update: if update:
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid, msg=f"Unable to update vm {name} with vmid {vmid}=" + str(e)
msg="Unable to update vm {0} with vmid {1}=".format(name, vmid) + str(e)
) )
elif clone is not None: elif clone is not None:
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid, msg=f"Unable to clone vm {name} from vmid {vmid}=" + str(e)
msg="Unable to clone vm {0} from vmid {1}=".format(name, vmid) + str(e)
) )
else: else:
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid,
msg="creation of qemu VM {} with vmid {} failed with exception={}".format( msg=f"creation of qemu VM {name} with vmid {vmid} failed with exception={e}"
name, vmid, e
)
) )
elif state == "started": elif state == "started":
status = {} status = {}
try: try:
if -1 == vmid: if -1 == vmid:
module.fail_json(msg="VM with name = {} does not exist in cluster".format(name)) module.fail_json(msg=f"VM with name = {name} does not exist in cluster")
vm = get_vm(proxmox, vmid) vm = get_vm(proxmox, vmid)
if not vm: if not vm:
module.fail_json( module.fail_json(vmid=vmid, msg=f"VM with vmid <{vmid}> does not exist in cluster")
vmid=vmid, msg="VM with vmid <{}> does not exist in cluster".format(vmid)
)
status["status"] = vm[0]["status"] status["status"] = vm[0]["status"]
if vm[0]["status"] == "running": if vm[0]["status"] == "running":
module.exit_json( module.exit_json(
changed=False, changed=False, vmid=vmid, msg=f"VM {vmid} is already running", **status
vmid=vmid,
msg="VM {} is already running".format(vmid),
**status
) )
if start_vm(module, proxmox, vm): if start_vm(module, proxmox, vm):
module.exit_json( module.exit_json(changed=True, vmid=vmid, msg=f"VM {vmid} started", **status)
changed=True, vmid=vmid, msg="VM {} started".format(vmid), **status except Exception as e: # noqa
)
except Exception as e:
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid, msg=f"starting of VM {vmid} failed with exception: {e}", **status
msg="starting of VM {} failed with exception: {}".format(vmid, e),
**status
) )
elif state == "stopped": elif state == "stopped":
status = {} status = {}
try: try:
if -1 == vmid: if -1 == vmid:
module.fail_json(msg="VM with name = {} does not exist in cluster".format(name)) module.fail_json(msg=f"VM with name = {name} does not exist in cluster")
vm = get_vm(proxmox, vmid) vm = get_vm(proxmox, vmid)
if not vm: if not vm:
module.fail_json( module.fail_json(vmid=vmid, msg=f"VM with vmid = {vmid} does not exist in cluster")
vmid=vmid, msg="VM with vmid = {} does not exist in cluster".format(vmid)
)
status["status"] = vm[0]["status"] status["status"] = vm[0]["status"]
if vm[0]["status"] == "stopped": if vm[0]["status"] == "stopped":
module.exit_json( module.exit_json(
changed=False, changed=False, vmid=vmid, msg=f"VM {vmid} is already stopped", **status
vmid=vmid,
msg="VM {} is already stopped".format(vmid),
**status
) )
if stop_vm(module, proxmox, vm, force=module.params["force"]): if stop_vm(module, proxmox, vm, force=module.params["force"]):
module.exit_json( module.exit_json(
changed=True, vmid=vmid, msg="VM {} is shutting down".format(vmid), **status changed=True, vmid=vmid, msg=f"VM {vmid} is shutting down", **status
) )
except Exception as e: except Exception as e: # noqa
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid, msg=f"stopping of VM {vmid} failed with exception: {e}", **status
msg="stopping of VM {} failed with exception: {}".format(vmid, e),
**status
) )
elif state == "restarted": elif state == "restarted":
status = {} status = {}
try: try:
if -1 == vmid: if -1 == vmid:
module.fail_json(msg="VM with name = {} does not exist in cluster".format(name)) module.fail_json(msg=f"VM with name = {name} does not exist in cluster")
vm = get_vm(proxmox, vmid) vm = get_vm(proxmox, vmid)
if not vm: if not vm:
module.fail_json( module.fail_json(vmid=vmid, msg=f"VM with vmid = {vmid} does not exist in cluster")
vmid=vmid, msg="VM with vmid = {} does not exist in cluster".format(vmid)
)
status["status"] = vm[0]["status"] status["status"] = vm[0]["status"]
if vm[0]["status"] == "stopped": if vm[0]["status"] == "stopped":
module.exit_json( module.exit_json(
changed=False, vmid=vmid, msg="VM {} is not running".format(vmid), **status changed=False, vmid=vmid, msg=f"VM {vmid} is not running", **status
) )
if stop_vm(module, proxmox, vm, if stop_vm(module, proxmox, vm,
force=module.params["force"]) and start_vm(module, proxmox, vm): force=module.params["force"]) and start_vm(module, proxmox, vm):
module.exit_json( module.exit_json(changed=True, vmid=vmid, msg=f"VM {vmid} is restarted", **status)
changed=True, vmid=vmid, msg="VM {} is restarted".format(vmid), **status except Exception as e: # noqa
)
except Exception as e:
module.fail_json( module.fail_json(
vmid=vmid, vmid=vmid, msg=f"restarting of VM {vmid} failed with exception: {e}", **status
msg="restarting of VM {} failed with exception: {}".format(vmid, e),
**status
) )
elif state == "absent": elif state == "absent":
@ -1504,28 +1543,26 @@ def main():
module.exit_json( module.exit_json(
changed=False, changed=False,
vmid=vmid, vmid=vmid,
msg="VM {} is running. Stop it before deletion or use force=yes.". msg=f"VM {vmid} is running. Stop it before deletion or use force=yes."
format(vmid)
) )
taskid = proxmox_node.qemu.delete(vmid) taskid = proxmox_node.qemu.delete(vmid)
if not wait_for_task(module, proxmox, vm[0]["node"], taskid): if not wait_for_task(module, proxmox, vm[0]["node"], taskid):
module.fail_json( module.fail_json(
msg="Reached timeout while waiting for removing VM." msg="Reached timeout while waiting for removing VM."
"Last line in task before timeout: {}". f"Last line in task before timeout: {proxmox_node.tasks(taskid).log.get()[:1]}"
format(proxmox_node.tasks(taskid).log.get()[:1])
) )
else: else:
module.exit_json(changed=True, vmid=vmid, msg="VM {} removed".format(vmid)) module.exit_json(changed=True, vmid=vmid, msg=f"VM {vmid} removed")
except Exception as e: except Exception as e: # noqa
module.fail_json(msg="deletion of VM {} failed with exception: {}".format(vmid, e)) module.fail_json(msg=f"deletion of VM {vmid} failed with exception: {e}")
elif state == "current": elif state == "current":
status = {} status = {}
if -1 == vmid: if -1 == vmid:
module.fail_json(msg="VM with name = {} does not exist in cluster".format(name)) module.fail_json(msg=f"VM with name = {name} does not exist in cluster")
vm = get_vm(proxmox, vmid) vm = get_vm(proxmox, vmid)
if not vm: if not vm:
module.fail_json(msg="VM with vmid = {} does not exist in cluster".format(vmid)) module.fail_json(msg=f"VM with vmid = {vmid} does not exist in cluster")
if not name: if not name:
name = vm[0]["name"] name = vm[0]["name"]
current = proxmox.nodes(vm[0]["node"]).qemu(vmid).status.current.get()["status"] current = proxmox.nodes(vm[0]["node"]).qemu(vmid).status.current.get()["status"]
@ -1534,7 +1571,7 @@ def main():
module.exit_json( module.exit_json(
changed=False, changed=False,
vmid=vmid, vmid=vmid,
msg="VM {} with vmid = {} is {}".format(name, vmid, current), msg=f"VM {name} with vmid = {vmid} is {current}",
**status **status
) )
@ -1561,11 +1598,13 @@ def _extract_nets(item):
if re.match(r"net[0-9]", k): if re.match(r"net[0-9]", k):
nets[k]["opts"] = [] nets[k]["opts"] = []
for val in v.split(","): for val in v.split(","):
if any(val.startswith(s) for s in ["e1000", "rtl8139", "virtio", "vmxnet3"]): if (
if len(val.split("=")) == 2: any(val.startswith(s) for s in ["e1000", "rtl8139", "virtio", "vmxnet3"])
net = val.split("=") and len(val.split("=")) == 2
nets[k]["net_id"] = net[0] ):
nets[k]["net_opts"] = net[1] net = val.split("=")
nets[k]["net_id"] = net[0]
nets[k]["net_opts"] = net[1]
else: else:
nets[k]["opts"].append(val) nets[k]["opts"].append(val)

View File

@ -1,5 +1,12 @@
#!/usr/bin/python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Module to control Univention Corporate Registry."""
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""Control Univention Corporate Registry."""
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"} ANSIBLE_METADATA = {"metadata_version": "1.1", "status": ["preview"], "supported_by": "community"}
@ -7,28 +14,33 @@ DOCUMENTATION = """
--- ---
module: ucr module: ucr
short_description: Manage variables in univention configuration registry. short_description: Manage variables in univention configuration registry.
version_added: "2.6" version_added: 1.1.0
description: description:
- "This module allows to manage variables inside the univention configuration registry - "This module allows to manage variables inside the univention configuration registry
on a univention corporate server (UCS)." on a univention corporate server (UCS)."
options: options:
path: path:
description: description:
- Path for the variable - Path for the variable
required: True aliases:
default: null - name
value: required: True
description: type: str
- New value of the variable value:
required: False description:
state: - New value of the variable
required: False required: False
default: "present" type: str
choices: ["present", "absent"] default: ""
description: state:
- Whether the variable should be exist or not. required: False
default: "present"
choices: ["present", "absent"]
type: str
description:
- Whether the variable should be exist or not.
author: author:
- Robert Kaussow (@xoxys) - Robert Kaussow (@xoxys)
""" """
EXAMPLES = """ EXAMPLES = """
@ -47,41 +59,43 @@ EXAMPLES = """
RETURN = """ RETURN = """
original_message: original_message:
description: The original name param that was passed in description: The original name param that was passed in
type: str type: str
returned: success
message: message:
description: The output message that the sample module generates description: The output message that the sample module generates
type: str
returned: success
""" """
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from univention.config_registry import ConfigRegistry # noqa
from univention.config_registry.frontend import ucr_update # noqa try:
from univention.config_registry import ConfigRegistry
from univention.config_registry.frontend import ucr_update
HAS_UNIVENTION = True
except ImportError:
HAS_UNIVENTION = False
def get_variable(ucr, path): def get_variable(ucr, path):
ucr.load() ucr.load()
if path in ucr: return ucr.get(path) if path in ucr else None
value = ucr.get(path)
else:
value = None
return value
def set_variable(ucr, path, value, result): def set_variable(ucr, path, value, result): # noqa
org_value = get_variable(ucr, path) org_value = get_variable(ucr, path)
ucr_update(ucr, {path: value}) ucr_update(ucr, {path: value})
new_value = get_variable(ucr, path) new_value = get_variable(ucr, path)
return not org_value == new_value return org_value != new_value
def dry_variable(ucr, path, value, result): def dry_variable(ucr, path, value, result): # noqa
org_value = get_variable(ucr, path) org_value = get_variable(ucr, path)
return not org_value == value return org_value != value
def main(): def main():
ucr = ConfigRegistry()
module_args = dict( module_args = dict(
path=dict(type="str", required=True, aliases=["name"]), path=dict(type="str", required=True, aliases=["name"]),
value=dict(type="str", required=False, default=""), value=dict(type="str", required=False, default=""),
@ -94,13 +108,17 @@ def main():
argument_spec=module_args, supports_check_mode=True, required_if=required_if argument_spec=module_args, supports_check_mode=True, required_if=required_if
) )
if not HAS_UNIVENTION:
module.fail_json(msg="univention required for this module")
ucr = ConfigRegistry()
result = dict(changed=False, original_message="", message="") result = dict(changed=False, original_message="", message="")
path = module.params["path"] path = module.params["path"]
value = module.params["value"] value = module.params["value"]
if module.params["state"] == "present": if module.params["state"] == "present" and (value is None or value == "None"):
if value is None or value == "None": value = ""
value = ""
elif module.params["state"] == "absent": elif module.params["state"] == "absent":
value = None value = None

1143
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

149
pyproject.toml Normal file
View File

@ -0,0 +1,149 @@
[tool.poetry]
authors = ["Robert Kaussow <mail@thegeeklab.de>"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Intended Audience :: Information Technology",
"Intended Audience :: System Administrators",
"Natural Language :: English",
"Operating System :: POSIX",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Utilities",
"Topic :: Software Development",
"Topic :: Software Development :: Documentation",
]
description = "Build environment for Ansible Collection."
license = "MIT"
name = "xoxys.general"
readme = "README.md"
repository = "https://gitea.rknet.org/ansible/xoxys.general"
version = "0.0.0"
[tool.poetry.dependencies]
python = "^3.8.0"
ansible-core = { version = "<=2.14.0", optional = true }
pyopenssl = "23.0.0"
proxmoxer = "2.0.1"
hcloud = "1.18.2"
[tool.poetry.extras]
ansible = ["ansible-core"]
[tool.poetry.group.dev.dependencies]
ruff = "0.0.230"
pytest = "7.2.1"
pytest-mock = "3.10.0"
pytest-cov = "4.0.0"
toml = "0.10.2"
yapf = "0.32.0"
pycodestyle = "2.10.0"
yamllint = "1.29.0"
pylint = "2.15.0"
voluptuous = "0.13.1"
[tool.pytest.ini_options]
addopts = "--cov --cov-report=xml:coverage.xml --cov-report=term --cov-append --no-cov-on-fail"
pythonpath = [
"."
]
testpaths = [
"tests",
]
filterwarnings = [
"ignore::FutureWarning",
"ignore::DeprecationWarning",
"ignore:.*pep8.*:FutureWarning",
]
[tool.coverage.run]
omit = ["**/tests/*"]
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
[tool.ruff]
exclude = [
".git",
"__pycache__",
"build",
"dist",
"tests",
"*.pyc",
"*.egg-info",
".cache",
".eggs",
"env*",
"iptables_raw.py",
]
# Explanation of errors
#
# D102: Missing docstring in public method
# D103: Missing docstring in public function
# D105: Missing docstring in magic method
# D107: Missing docstring in __init__
# D202: No blank lines allowed after function docstring
# D203: One blank line required before class docstring
# E402: Module level import not at top of file
# SIM105: Use `contextlib.suppress(Exception)` instead of try-except-pass
# C402: Unnecessary generator (rewrite as a `dict` comprehension)
# C408: Unnecessary `dict` call (rewrite as a literal)
# I001: Import block is un-sorted or un-formatted
# UP001: `__metaclass__ = type` is implied
# UP009: UTF-8 encoding declaration is unnecessary
# UP010: Unnecessary `__future__` imports `absolute_import`, `division`, `print_function` for target Python version
ignore = [
"D102",
"D103",
"D105",
"D107",
"D202",
"D203",
"D212",
"E402",
"SIM105",
"C402",
"C408",
"I001",
"UP001",
"UP009",
"UP010",
]
line-length = 99
select = [
"D",
"E",
"F",
"Q",
"W",
"I",
"S",
"BLE",
"N",
"UP",
"B",
"A",
"C4",
"T20",
"SIM",
"RET",
"ARG",
"ERA",
"RUF",
]
[tool.ruff.flake8-quotes]
inline-quotes = "double"
[tool.yapf]
based_on_style = "google"
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true

View File

@ -1,4 +0,0 @@
ansible-core
pyopenssl
proxmoxer
hcloud

View File

@ -1,42 +0,0 @@
[isort]
default_section = THIRDPARTY
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
force_single_line = true
line_length = 99
skip_glob = **/.venv*,**/venv/*,**/docs/*,**/inventory/*,**/modules/*
[yapf]
based_on_style = google
column_limit = 99
dedent_closing_brackets = true
coalesce_brackets = true
split_before_logical_operator = true
[tool:pytest]
filterwarnings =
ignore::FutureWarning
ignore:.*collections.*:DeprecationWarning
ignore:.*pep8.*:FutureWarning
[coverage:run]
omit =
**/test/*
**/.venv/*
[flake8]
ignore = D101, D102, D103, D105, D107, D202, E402, W503, B902
max-line-length = 99
inline-quotes = double
exclude =
.git
.tox
__pycache__
build
dist
test
*.pyc
*.egg-info
.cache
.eggs
env*
iptables_raw.py

View File

@ -1 +0,0 @@
# noqa

View File

@ -1,11 +0,0 @@
ansible
# requirement for the proxmox modules
proxmoxer
requests
# requirement for the corenetworks modules
corenetworks
# requirement for the openssl_pkcs12 module
pyOpenSSL

2
tests/config.yml Normal file
View File

@ -0,0 +1,2 @@
modules:
python_requires: ">=3.8"

View File

View File

@ -1,10 +1,11 @@
"""Test inventory plugin proxmox.""" """Test inventory plugin proxmox."""
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Robert Kaussow <mail@thegeeklab.de> # Copyright (c) 2020, Robert Kaussow <mail@thegeeklab.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # 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) from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import pytest import pytest
proxmox = pytest.importorskip("proxmoxer") proxmox = pytest.importorskip("proxmoxer")
@ -58,7 +59,7 @@ def test_get_ip_address(inventory, mocker):
inventory.client = mocker.MagicMock() inventory.client = mocker.MagicMock()
inventory.client.nodes.return_value.get.return_value = networks inventory.client.nodes.return_value.get.return_value = networks
assert "10.0.0.1" == inventory._get_ip_address("qemu", None, None) assert inventory._get_ip_address("qemu", None, None) == "10.0.0.1"
def test_exclude(inventory, mocker): def test_exclude(inventory, mocker):