commit 9b49b3881a16756ae93407913b97039b294a1a66 Author: Robert Kaussow Date: Tue Aug 18 15:10:09 2020 +0200 initial commit diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 0000000..1f6cbe5 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,162 @@ +local PythonVersion(pyversion='3.5') = { + name: 'python' + std.strReplace(pyversion, '.', '') + '-pytest', + image: 'python:' + pyversion, + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install -r dev-requirements.txt -qq', + 'pip install -r test/unit/requirements.txt -qq', + 'python -m pytest --cov --cov-append --no-cov-on-fail', + ], + depends_on: [ + 'clone', + ], +}; + +local PipelineLint = { + kind: 'pipeline', + name: 'lint', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'flake8', + image: 'python:3.8', + environment: { + PY_COLORS: 1, + }, + commands: [ + 'pip install -r dev-requirements.txt -qq', + 'flake8', + ], + }, + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineTest = { + kind: 'pipeline', + name: 'test', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + PythonVersion(pyversion='3.5'), + PythonVersion(pyversion='3.6'), + PythonVersion(pyversion='3.7'), + PythonVersion(pyversion='3.8'), + { + name: 'codecov', + image: 'python:3.8', + environment: { + PY_COLORS: 1, + CODECOV_TOKEN: { from_secret: 'codecov_token' }, + }, + commands: [ + 'pip install codecov -qq', + 'codecov --required -X gcov', + ], + depends_on: [ + 'python35-pytest', + 'python36-pytest', + 'python37-pytest', + 'python38-pytest', + ], + }, + ], + depends_on: [ + 'lint', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineBuild = { + kind: 'pipeline', + name: 'build', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'build', + image: 'python:3.8', + commands: [ + 'pip install ansible -qq', + 'ansible-galaxy collection build --output-path dist/', + ], + }, + { + name: 'checksum', + image: 'alpine', + commands: [ + 'cd dist/ && sha256sum * > ../sha256sum.txt', + ], + }, + { + name: 'publish-gitea', + image: 'plugins/gitea-release', + settings: { + overwrite: true, + api_key: { from_secret: 'gitea_token' }, + files: ['dist/*', 'sha256sum.txt'], + base_url: 'https://gitea.rknet.org', + title: '${DRONE_TAG}', + note: 'CHANGELOG.md', + }, + when: { + ref: ['refs/tags/**'], + }, + }, + ], + depends_on: [ + 'security', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**', 'refs/pull/**'], + }, +}; + +local PipelineNotifications = { + kind: 'pipeline', + name: 'notifications', + platform: { + os: 'linux', + arch: 'amd64', + }, + steps: [ + { + name: 'matrix', + image: 'plugins/matrix', + settings: { + homeserver: { from_secret: 'matrix_homeserver' }, + roomid: { from_secret: 'matrix_roomid' }, + template: 'Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}', + username: { from_secret: 'matrix_username' }, + password: { from_secret: 'matrix_password' }, + }, + }, + ], + depends_on: [ + 'docs', + ], + trigger: { + ref: ['refs/heads/master', 'refs/tags/**'], + status: ['success', 'failure'], + }, +}; + +[ + PipelineLint, + PipelineTest, + PipelineBuild, + PipelineNotifications, +] diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..5824895 --- /dev/null +++ b/.drone.yml @@ -0,0 +1,183 @@ +--- +kind: pipeline +name: lint + +platform: + os: linux + arch: amd64 + +steps: +- name: flake8 + image: python:3.8 + commands: + - pip install -r dev-requirements.txt -qq + - flake8 + environment: + PY_COLORS: 1 + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +--- +kind: pipeline +name: test + +platform: + os: linux + arch: amd64 + +steps: +- name: python35-pytest + image: python:3.5 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -r test/unit/requirements.txt -qq + - python -m pytest --cov --cov-append --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python36-pytest + image: python:3.6 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -r test/unit/requirements.txt -qq + - python -m pytest --cov --cov-append --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python37-pytest + image: python:3.7 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -r test/unit/requirements.txt -qq + - python -m pytest --cov --cov-append --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: python38-pytest + image: python:3.8 + commands: + - pip install -r dev-requirements.txt -qq + - pip install -r test/unit/requirements.txt -qq + - python -m pytest --cov --cov-append --no-cov-on-fail + environment: + PY_COLORS: 1 + depends_on: + - clone + +- name: codecov + image: python:3.8 + commands: + - pip install codecov -qq + - codecov --required -X gcov + environment: + CODECOV_TOKEN: + from_secret: codecov_token + PY_COLORS: 1 + depends_on: + - python35-pytest + - python36-pytest + - python37-pytest + - python38-pytest + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +depends_on: +- lint + +--- +kind: pipeline +name: build + +platform: + os: linux + arch: amd64 + +steps: +- name: build + image: python:3.8 + commands: + - pip install ansible -qq + - ansible-galaxy collection build --output-path dist/ + +- name: checksum + image: alpine + commands: + - cd dist/ && sha256sum * > ../sha256sum.txt + +- name: publish-gitea + image: plugins/gitea-release + settings: + api_key: + from_secret: gitea_token + base_url: https://gitea.rknet.org + files: + - dist/* + - sha256sum.txt + note: CHANGELOG.md + overwrite: true + title: ${DRONE_TAG} + when: + ref: + - refs/tags/** + +trigger: + ref: + - refs/heads/master + - refs/tags/** + - refs/pull/** + +depends_on: +- security + +--- +kind: pipeline +name: notifications + +platform: + os: linux + arch: amd64 + +steps: +- name: matrix + image: plugins/matrix + settings: + homeserver: + from_secret: matrix_homeserver + password: + from_secret: matrix_password + roomid: + from_secret: matrix_roomid + template: "Status: **{{ build.status }}**
Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}
Message: {{ build.message }}" + username: + from_secret: matrix_username + +trigger: + ref: + - refs/heads/master + - refs/tags/** + status: + - success + - failure + +depends_on: +- docs + +--- +kind: signature +hmac: 4521a2c60992b46f1bf8ac0400420d6259c7de4fbd3dfec9ee59cacc8cde1ee4 + +... diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..af539aa --- /dev/null +++ b/.flake8 @@ -0,0 +1,18 @@ +[flake8] +ignore = D101, D102, D103, D107, D202, E402, W503 +max-line-length = 99 +inline-quotes = double +exclude = + .git + .tox + __pycache__ + build + dist + test + *.pyc + *.egg-info + .cache + .eggs + env* +application-import-names = ansiblelater +format = ${cyan}%(path)s:%(row)d:%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d55068e --- /dev/null +++ b/.gitignore @@ -0,0 +1,109 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ +env/ +env*/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# Ignore ide addons +.server-script +.on-save.json +.vscode +.pytest_cache + +pip-wheel-metadata + +# Hugo documentation +docs/themes/ +docs/public/ +resources/_gen/ + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..44e4d67 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Robert Kaussow + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next +paragraph) shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b85d251 --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# xoxys.general + +[![Build Status](https://img.shields.io/drone/build/ansible/xoxys.general?logo=drone&server=https%3A%2F%2Fdrone.rknet.org)](https://drone.rknet.org/ansible/xoxys.general) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?label=license)](LICENSE) + +Custom general Ansible collection. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## Maintainers and Contributors + +[Robert Kaussow](https://gitea.rknet.org/xoxys) diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..1d053e7 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,19 @@ +pydocstyle<4.0.0 +flake8 +flake8-colors +flake8-blind-except +flake8-builtins +flake8-docstrings<=3.0.0 +flake8-isort +flake8-logging-format +flake8-polyfill +flake8-quotes +flake8-pep3101 +flake8-eradicate; python_version >= "3.6" +pep8-naming +wheel +pytest +pytest-mock +pytest-cov +bandit +yapf diff --git a/galaxy.yml b/galaxy.yml new file mode 100644 index 0000000..757439c --- /dev/null +++ b/galaxy.yml @@ -0,0 +1,15 @@ +namespace: xoxys +name: general +version: 1.0.0 +readme: README.md +authors: + - Robert Kaussow +description: Custom general Ansible collection +license: + - MIT +license_file: "LICENSE" +tags: + - misc +dependencies: {} +repository: https://gitea.rknet.org/ansible/xoxys.general +homepage: https://thegeeklab.de/ diff --git a/plugins/filters/prefix.py b/plugins/filters/prefix.py new file mode 100644 index 0000000..3db8213 --- /dev/null +++ b/plugins/filters/prefix.py @@ -0,0 +1,11 @@ +"""Filter to prefix all itams from a list.""" + + +def prefix(value, prefix="--"): + return [prefix + x for x in value] + + +class FilterModule(object): + + def filters(self): + return {"prefix": prefix} diff --git a/plugins/filters/wrap.py b/plugins/filters/wrap.py new file mode 100644 index 0000000..ffe03e4 --- /dev/null +++ b/plugins/filters/wrap.py @@ -0,0 +1,11 @@ +"""Filter to wrap all items from a list.""" + + +def wrap(value, wrapper="'"): + return [wrapper + x + wrapper for x in value] + + +class FilterModule(object): + + def filters(self): + return {"wrap": wrap} diff --git a/plugins/inventory/proxmox.py b/plugins/inventory/proxmox.py new file mode 100644 index 0000000..8cfd757 --- /dev/null +++ b/plugins/inventory/proxmox.py @@ -0,0 +1,273 @@ +"""Dynamic inventory plugin for Proxmox VE.""" +# -*- coding: utf-8 -*- +# Copyright (c) 2014, Mathieu GAUTHIER-LAFAYE +# Copyright (c) 2016, Matt Harris +# Copyright (c) 2020, Robert Kaussow +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +DOCUMENTATION = """ + name: proxmox + plugin_type: inventory + short_description: Proxmox VE inventory source + version_added: 1.0.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: proxmox) entry." + extends_documentation_fragment: + - inventory_cache + options: + plugin: + description: The name of this plugin, it should always be set to C(proxmox) for this plugin to recognize it as it"s own. + required: yes + choices: ["xoxys.general.proxmox"] + server: + description: Proxmox VE server url. + default: "pve.example.com" + required: yes + env: + - name: PROXMOX_SERVER + user: + description: Proxmox VE authentication user. + required: yes + env: + - name: PROXMOX_USER + password: + description: Proxmox VE authentication password + required: yes + env: + - name: PROXMOX_PASSWORD + exclude_vmid: + description: VMID"s to exclude from inventory + type: list + default: [] + elements: str + exclude_state: + description: VM states to exclude from inventory + type: list + default: [] + elements: str + group: + description: Group to place all hosts into + default: proxmox + want_facts: + description: Toggle, if C(true) the plugin will retrieve host facts from the server + type: boolean + default: yes +""" # noqa + +EXAMPLES = """ +# proxmox.yml +plugin: community.general.proxmox +server: pve.example.com +user: admin@pve +password: secure +""" + +import json +import re +import socket + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.module_utils.six import iteritems +from ansible.plugins.inventory import BaseInventoryPlugin +from collections import defaultdict +from distutils.version import LooseVersion + +try: + from proxmoxer import ProxmoxAPI + HAS_PROXMOXER = True +except ImportError: + HAS_PROXMOXER = False + + +class InventoryModule(BaseInventoryPlugin): + NAME = "community.general.proxmox" + + def _auth(self): + return ProxmoxAPI( + self.get_option("server"), + user=self.get_option("user"), + password=self.get_option("password"), + verify_ssl=False + ) + + def _get_version(self): + return LooseVersion(self.client.version.get()["version"]) + + def _get_major(self): + return LooseVersion(self.client.version.get()["release"]) + + def _get_names(self, pve_list, pve_type): + names = [] + + if pve_type == "node": + names = [node["node"] for node in pve_list] + elif pve_type == "pool": + names = [pool["poolid"] for pool in pve_list] + + return names + + def _get_variables(self, pve_list, pve_type): + variables = {} + + if pve_type in ["qemu", "container"]: + for vm in pve_list: + nested = {} + for key, value in iteritems(vm): + nested["proxmox_" + key] = value + variables[vm["name"]] = nested + + return variables + + def _get_ip_address(self, pve_type, pve_node, vmid): + + def validate(address): + try: + # IP address validation + if socket.inet_aton(address): + # Ignore localhost + if address != "127.0.0.1": + return address + except socket.error: + return False + + address = False + networks = False + if pve_type == "qemu": + # If qemu agent is enabled, try to gather the IP address + try: + if self.client.nodes(pve_node).get(pve_type, vmid, "agent", "info") is not None: + networks = self.client.nodes(pve_node).get( + "qemu", vmid, "agent", "network-get-interfaces" + )["result"] + except Exception: + pass + + if networks: + if type(networks) is list: + for network in networks: + for ip_address in network["ip-addresses"]: + address = validate(ip_address["ip-address"]) + else: + try: + config = self.client.nodes(pve_node).get(pve_type, vmid, "config") + address = re.search(r"ip=(\d*\.\d*\.\d*\.\d*)", config["net0"]).group(1) + except Exception: + pass + + return address + + def _exclude(self, pve_list): + filtered = [] + for item in pve_list: + obj = defaultdict(dict, item) + if obj["template"] == 1: + continue + + if obj["status"] in self.get_option("exclude_state"): + continue + + if obj["vmid"] in self.get_option("exclude_vmid"): + continue + + filtered.append(item.copy()) + return filtered + + def _propagate(self): + for node in self._get_names(self.client.nodes.get(), "node"): + try: + qemu_list = self._exclude(self.client.nodes(node).qemu.get()) + container_list = self._exclude(self.client.nodes(node).lxc.get()) + except Exception as e: + raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) + + # Merge QEMU and Containers lists from this node + instances = self._get_variables(qemu_list.copy(), "qemu") + instances.update(self._get_variables(container_list, "container")) + + for host in instances: + vmid = instances[host]["proxmox_vmid"] + + try: + pve_type = instances[host]["proxmox_type"] + except KeyError: + pve_type = "qemu" + + try: + description = self.client.nodes(node).get(pve_type, vmid, + "config")["description"] + except KeyError: + description = None + except Exception as e: + raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) + + try: + metadata = json.loads(description) + except TypeError: + metadata = {} + except ValueError: + metadata = {"notes": description} + + # Add hosts to default group + self.inventory.add_group(group=self.get_option("group")) + self.inventory.add_host(group=self.get_option("group"), host=host) + + # Group hosts by status + self.inventory.add_group(group=instances[host]["proxmox_status"]) + self.inventory.add_host(group=instances[host]["proxmox_status"], host=host) + + if "groups" in metadata: + for group in metadata["groups"]: + self.inventory.add_group(group=group) + self.inventory.add_host(group=group, host=host) + + if self.get_option("want_facts"): + for attr in instances[host]: + if attr not in ["proxmox_template"]: + self.inventory.set_variable(host, attr, instances[host][attr]) + + address = self._get_ip_address(pve_type, node, vmid) + if address: + self.inventory.set_variable(host, "ansible_host", address) + + for pool in self._get_names(self.client.pools.get(), "pool"): + try: + pool_list = self._exclude(self.client.pool(pool).get()["members"]) + except Exception as e: + raise AnsibleError("Proxmoxer API error: {0}".format(to_native(e))) + + members = [ + member["name"] + for member in pool_list + if (member["type"] == "qemu" or member["type"] == "lxc") + ] + + for member in members: + self.inventory.add_host(group=pool, host=member) + + def verify_file(self, path): + """Verify the Proxmox VE configuration file.""" + if super(InventoryModule, self).verify_file(path): + endings = ("proxmox.yaml", "proxmox.yml") + if any((path.endswith(ending) for ending in endings)): + return True + return False + + def parse(self, inventory, loader, path, cache=True): + """Dynamically parse the Proxmox VE cloud inventory.""" + if not HAS_PROXMOXER: + raise AnsibleError( + "The Proxmox VE dynamic inventory plugin requires proxmoxer: " + "https://pypi.org/project/proxmoxer/" + ) + + super(InventoryModule, self).parse(inventory, loader, path) + + self._read_config_data(path) + self.client = self._auth() + self._propagate() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1789a04 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,25 @@ +[isort] +default_section = THIRDPARTY +known_first_party = ansiblelater +sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER +force_single_line = true +line_length = 99 +skip_glob = **/.env*,**/env/*,**/docs/*,**/inventory/* + +[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/* + **/.env/* diff --git a/test/unit/plugins/inventory/__init__.py b/test/unit/plugins/inventory/__init__.py new file mode 100644 index 0000000..4ede8e6 --- /dev/null +++ b/test/unit/plugins/inventory/__init__.py @@ -0,0 +1 @@ +# noqa diff --git a/test/unit/plugins/inventory/test_proxmox.py b/test/unit/plugins/inventory/test_proxmox.py new file mode 100644 index 0000000..cb08524 --- /dev/null +++ b/test/unit/plugins/inventory/test_proxmox.py @@ -0,0 +1,90 @@ +"""Test inventory plugin proxmox.""" +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Robert Kaussow +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) + +import pytest + +proxmox = pytest.importorskip("proxmoxer") + +from ansible.errors import AnsibleError, AnsibleParserError # noqa +from plugins.inventory.proxmox import InventoryModule + + +@pytest.fixture +def inventory(): + return InventoryModule() + + +def test_get_names(inventory): + nodes = [{"status": "online", "type": "node", "id": "node/testnode", "node": "testnode"}] + pools = [{"poolid": "testpool"}] + + assert ["testnode"] == inventory._get_names(nodes, "node") + assert ["testpool"] == inventory._get_names(pools, "pool") + + +def test_get_variables(inventory): + pve_list = [{ + "status": "running", + "vmid": "100", + "name": "test", + }] + + variables = { + "test": { + "proxmox_status": "running", + "proxmox_vmid": "100", + "proxmox_name": "test", + } + } + + assert variables == inventory._get_variables(pve_list, "qemu") + + +def test_get_ip_address(inventory, mocker): + networks = { + "result": [{ + "ip-addresses": [{ + "ip-address": "10.0.0.1", + "prefix": 26, + "ip-address-type": "ipv4" + }], + "name": "eth0" + }] + } + inventory.client = mocker.MagicMock() + inventory.client.nodes.return_value.get.return_value = networks + + assert "10.0.0.1" == inventory._get_ip_address("qemu", None, None) + + +def test_exclude(inventory, mocker): + + def get_option(name, *args, **kwargs): + if name == "exclude_state": + return ["stopped"] + + return [] + + inventory.get_option = mocker.MagicMock(side_effect=get_option) + + pve_list = [{ + "status": "running", + "vmid": "100", + "name": "test", + }, { + "status": "stopped", + "vmid": "101", + "name": "stop", + }] + + filtered = [{ + "status": "running", + "vmid": "100", + "name": "test", + }] + + assert filtered == inventory._exclude(pve_list) diff --git a/test/unit/requirements.txt b/test/unit/requirements.txt new file mode 100644 index 0000000..c199d3d --- /dev/null +++ b/test/unit/requirements.txt @@ -0,0 +1,5 @@ +ansible + +# requirement for the proxmox module +proxmoxer +requests