initial commit

This commit is contained in:
Robert Kaussow 2020-08-18 15:10:09 +02:00
commit 9b49b3881a
Signed by: xoxys
GPG Key ID: 65362AE74AF98B61
15 changed files with 957 additions and 0 deletions

162
.drone.jsonnet Normal file
View File

@ -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 }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}<br/> 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,
]

183
.drone.yml Normal file
View File

@ -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 }}**<br/> Build: [{{ repo.Owner }}/{{ repo.Name }}]({{ build.link }}) ({{ build.branch }}) by {{ build.author }}<br/> 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
...

18
.flake8 Normal file
View File

@ -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

109
.gitignore vendored Normal file
View File

@ -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/

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Robert Kaussow <mail@thegeeklab.de>
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.

14
README.md Normal file
View File

@ -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)

19
dev-requirements.txt Normal file
View File

@ -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

15
galaxy.yml Normal file
View File

@ -0,0 +1,15 @@
namespace: xoxys
name: general
version: 1.0.0
readme: README.md
authors:
- Robert Kaussow <mail@thegeeklab.de>
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/

11
plugins/filters/prefix.py Normal file
View File

@ -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}

11
plugins/filters/wrap.py Normal file
View File

@ -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}

View File

@ -0,0 +1,273 @@
"""Dynamic inventory plugin for Proxmox VE."""
# -*- coding: utf-8 -*-
# Copyright (c) 2014, Mathieu GAUTHIER-LAFAYE <gauthierl@lapth.cnrs.fr>
# Copyright (c) 2016, Matt Harris <matthaeus.harris@gmail.com>
# 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)
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()

25
setup.cfg Normal file
View File

@ -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/*

View File

@ -0,0 +1 @@
# noqa

View File

@ -0,0 +1,90 @@
"""Test inventory plugin proxmox."""
# -*- coding: utf-8 -*-
# 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)
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)

View File

@ -0,0 +1,5 @@
ansible
# requirement for the proxmox module
proxmoxer
requests