1
0
Fork 0

moved the systemd modules to an own collection

Dieser Commit ist enthalten in:
Sebastian Tobie 2023-04-15 00:58:51 +02:00
Ursprung 6f52f5b114
Commit aa0723809a
15 geänderte Dateien mit 971 neuen und 2 gelöschten Zeilen

1
.gitignore vendored
Datei anzeigen

@ -1,5 +1,6 @@
# ---> Ansible
*.retry
tests/output
# ---> Python
# Byte-compiled / optimized / DLL files

Datei anzeigen

@ -1,3 +1,3 @@
# ansible-systemd
# Ansible Collection - stop50.systemd
an collection for my own collection for different things
Documentation for the collection.

24
galaxy.yml Normale Datei
Datei anzeigen

@ -0,0 +1,24 @@
---
namespace: sebastian
name: systemd
version: 0.1.0
# The path to the Markdown (.md) readme file. This path is relative to the root of the collection
readme: README.md
# A list of the collection's content authors. Can be just the name or in the format 'Full Name <email> (url)
# @nicks:irc/im.site#channel'
authors:
- Sebastian Tobie
description: An simple for generating systemd units with ansible
license_file: 'LICENSE'
tags:
- systemd
- linux
dependencies: {}
repository: https://gitea.sebastian-tobie.de/sebastian/ansible-systemd
# documentation: http://docs.example.com
# homepage:
issues: https://gitea.sebastian-tobie.de/sebastian/ansible-systemd/issues
build_ignore: []
# manifest: null

52
meta/runtime.yml Normale Datei
Datei anzeigen

@ -0,0 +1,52 @@
---
# Collections must specify a minimum required ansible version to upload
# to galaxy
# requires_ansible: '>=2.9.10'
# Content that Ansible needs to load from another location or that has
# been deprecated/removed
# plugin_routing:
# action:
# redirected_plugin_name:
# redirect: ns.col.new_location
# deprecated_plugin_name:
# deprecation:
# removal_version: "4.0.0"
# warning_text: |
# See the porting guide on how to update your playbook to
# use ns.col.another_plugin instead.
# removed_plugin_name:
# tombstone:
# removal_version: "2.0.0"
# warning_text: |
# See the porting guide on how to update your playbook to
# use ns.col.another_plugin instead.
# become:
# cache:
# callback:
# cliconf:
# connection:
# doc_fragments:
# filter:
# httpapi:
# inventory:
# lookup:
# module_utils:
# modules:
# netconf:
# shell:
# strategy:
# terminal:
# test:
# vars:
# Python import statements that Ansible needs to load from another location
# import_redirection:
# ansible_collections.ns.col.plugins.module_utils.old_location:
# redirect: ansible_collections.ns.col.plugins.module_utils.new_location
# Groups of actions/modules that take a common set of options
# action_groups:
# group_name:
# - module1
# - module2

31
plugins/README.md Normale Datei
Datei anzeigen

@ -0,0 +1,31 @@
# Collections Plugins Directory
This directory can be used to ship various plugins inside an Ansible collection. Each plugin is placed in a folder that
is named after the type of plugin it is in. It can also include the `module_utils` and `modules` directory that
would contain module utils and modules respectively.
Here is an example directory of the majority of plugins currently supported by Ansible:
```
└── plugins
├── action
├── become
├── cache
├── callback
├── cliconf
├── connection
├── filter
├── httpapi
├── inventory
├── lookup
├── module_utils
├── modules
├── netconf
├── shell
├── strategy
├── terminal
├── test
└── vars
```
A full list of plugin types can be found at [Working With Plugins](https://docs.ansible.com/ansible-core/2.14/plugins/plugins.html).

0
plugins/__init__.py Normale Datei
Datei anzeigen

Datei anzeigen

Datei anzeigen

@ -0,0 +1,121 @@
import pathlib
from functools import partial
from typing import Any, Callable, Dict, Optional, Sequence, Tuple, Type, Union
__all__ = (
"Types",
"SYSTEMD_SERVICE_CONFIG",
"SYSTEMD_NETWORK_CONFIG",
"SYSTEMD_CONFIG_ROOT",
)
SYSTEMD_CONFIG_ROOT = pathlib.Path("/etc/systemd")
SYSTEMD_NETWORK_CONFIG = SYSTEMD_CONFIG_ROOT / "network"
SYSTEMD_SERVICE_CONFIG = SYSTEMD_CONFIG_ROOT / "system"
class _sdict(dict):
_help: Optional[str]
__name__: str
class _Type(type):
def __new__(metacls, cls, bases, classdict, **kwds):
individual = dict()
virtfunc = None
virtual = ()
special = dict()
for key, value in classdict.items():
if key.startswith("_"):
if key == "__getattr__":
virtfunc = value
elif key == "__dir__":
virtual = tuple(value(None))
elif key in ("__doc__",):
special[key] = value
else:
individual[key] = value
if len(virtual) != 0 and virtfunc is None: # pragma: nocover
raise TypeError(
"Virtual funcs defined, but no func to generate them defined"
)
special["_attr"] = tuple(virtual + tuple(individual.keys()))
special["_vfunc"] = virtfunc
special["_virtual"] = virtual
special["_individual"] = individual
annotations = dict()
if len(virtual) != 0 and virtfunc is not None: # pragma: nocover
anno = virtfunc(None, virtual[0]).__annotations__
for virtualkey in virtual:
annotations[virtualkey] = Callable[[*anno.values()], Dict[str, Any]]
annotations["__dir__"] = Callable[[], Tuple[str]]
special["__annotations__"] = annotations
inst = super().__new__(metacls, cls, bases, special, **kwds)
return inst
def __getattribute__(self, __name: str) -> Any:
if __name in (
"__dict__",
"__doc__",
"_attr",
"__annotations__",
"_virtual",
"_vfunc",
"_individual",
):
return super().__getattribute__(__name)
if __name in self._virtual:
return self._vfunc(self, __name)
if __name in self._individual:
return partial(self._individual[__name], self)
raise AttributeError(f"Attribut {__name} not found.")
class Types(metaclass=_Type):
"""Provides helpers for the ansible types"""
def list(
self,
elements: Union[Type[object], str],
required: bool = False,
help: Optional[str] = None,
) -> dict:
if not isinstance(elements, str):
elements = elements.__name__
option = _sdict(type="list", elements=elements, required=required)
option._help = help
return option
def __dir__(self) -> tuple:
return (
"str",
"dict",
"bool",
"int",
"float",
"path",
"raw",
"jsonarg",
"json",
"bytes",
"bits",
)
def __getattr__(self, name: str):
def argument(
required: bool = False,
help: Optional[str] = None,
choices: Optional[Sequence] = None,
default: Optional[Any] = None,
):
output = _sdict(type=name, required=required)
if choices is not None:
output["choices"] = choices
if default is not None:
output["default"] = default
output._help = help
return output
argument.__name__ = name
return argument

221
plugins/module_utils/module.py Normale Datei
Datei anzeigen

@ -0,0 +1,221 @@
import pathlib
from typing import Any, Callable, ClassVar, Dict, Optional, TypeVar
import ansible.module_utils.basic as basic
from ansible.module_utils.generic import _sdict
__all__ = (
"AnsibleModule",
"SystemdUnitModule",
)
T = TypeVar("T")
class AnsibleModule(object):
"""Simple wrapper for the mo"""
name: ClassVar[str]
module: basic.AnsibleModule
msg: str
result: dict
module_spec: ClassVar[dict]
_common_args = dict()
@property
def params(self) -> Dict[str, Any]:
return self.module.params # type: ignore
def __init__(self):
self.result = dict(changed=False)
specs = dict()
specs.update(self._common_args)
specs.update(self.module_spec)
self.module = basic.AnsibleModule(**specs)
self.msg = ""
self.tmpdir = pathlib.Path(self.module.tmpdir)
def set(self, key: str, value):
self.result[key] = value
def diff(
self,
before,
after,
before_header: Optional[str] = None,
after_header: Optional[str] = None,
):
if "diff" not in self.result:
self.result["diff"] = list()
diff = dict(
before=before,
after=after,
)
if before_header is not None:
diff["before_header"] = before_header
if after_header is not None:
diff["after_header"] = after_header
self.result["diff"].append(diff)
def get(self, key: str, default: T = None) -> T:
if key not in self.params.keys():
return default
if self.params[key] is None and default is not None:
return default
if self.params[key] is None or key not in self.params:
raise KeyError()
return self.params[key]
@property
def changed(self):
return self.result["changed"]
@changed.setter
def changed_set(self, value):
self.result["changed"] = not not value
def prepare(self):
raise NotImplementedError()
def check(self):
raise NotImplementedError()
def run(self):
raise NotImplementedError()
def __call__(self):
self.prepare()
try:
if self.module.check_mode:
self.check()
else:
self.run()
except Exception as exc:
import traceback
self.module.fail_json(
"".join(traceback.format_exception(type(exc), exc, exc.__traceback__)),
**self.result,
)
self.module.exit_json(**self.result)
@classmethod
def doc(cls) -> str:
try:
import yaml
except ImportError:
return "---\n"
doc = cls.__doc__
if doc is None:
doc = ""
options = dict()
help: _sdict
for option, help in cls.module_spec["argument_spec"].items():
options[option] = dict(
type=help["type"],
)
if hasattr(help, "_help") and help._help is not None:
options[option]["description"] = help._help.split("\n")
if "required" in help and help["required"]:
options[option]["required"] = True
else:
options[option]["required"] = False
if help["type"] == "list":
options[option]["elements"] = help["elements"]
if not options[option]["required"]:
options[option]["default"] = []
if "default" in help:
options[option]["default"] = help["default"]
if "choices" in help:
options[option]["choices"] = tuple(help["choices"])
docu = doc.split("\n")
return str(
yaml.safe_dump(
dict(
module=cls.name,
short_description=docu[0],
description=docu,
options=options,
),
stream=None,
explicit_start="---",
)
)
class SystemdUnitModule(AnsibleModule):
unitfile: pathlib.Path
_common_args = dict(
supports_check_mode=True,
add_file_common_args=True,
)
post: Optional[Callable[[], None]]
def unit(self) -> str:
raise NotImplementedError()
def unitfile_gen(self):
path = self.tmpdir / "newunit"
with open(path, "w") as unit:
unit.write(self.unit())
self.module.set_owner_if_different(path.as_posix(), "root", False)
self.module.set_group_if_different(path.as_posix(), "root", False)
self.module.set_mode_if_different(path.as_posix(), "0644", False)
if self.unitfile.exists():
if "diff" not in self.result:
self.result["diff"] = list()
diff = dict()
self.result["changed"] = self.module.set_owner_if_different(
self.unitfile.as_posix(),
"root",
self.result["changed"],
diff,
)
self.result["changed"] = self.module.set_group_if_different(
self.unitfile.as_posix(),
"root",
self.result["changed"],
diff,
)
self.result["changed"] = self.module.set_mode_if_different(
self.unitfile.as_posix(),
"0644",
self.result["changed"],
diff,
)
self.result["diff"].append(diff)
def check(self):
if "changed" in self.result:
changed = self.result["changed"]
else:
changed = False
self.unitfile_gen()
if not self.unitfile.exists():
self.diff("", self.unit(), self.unitfile.as_posix())
changed = True
else:
if self.module.sha256(self.unitfile.as_posix()) != self.module.sha256(
(self.tmpdir / "newunit").as_posix()
):
changed = True
self.diff(
before=self.unitfile.read_text(),
after=self.unit(),
before_header=self.unitfile.as_posix(),
)
self.set("changed", changed)
if hasattr(self, "post") and self.post is not None:
self.post()
return changed
def run(self):
if not self.check():
return
self.module.atomic_move(
src=(self.tmpdir / "newunit").as_posix(),
dest=self.unitfile.as_posix(),
)
if hasattr(self, "post") and self.post is not None:
self.post()

0
plugins/modules/__init__.py Normale Datei
Datei anzeigen

Datei anzeigen

@ -0,0 +1,157 @@
#!/usr/bin/python3
import pathlib
from typing import List
from ansible.module_utils.generic import SYSTEMD_NETWORK_CONFIG as SYSTEMD_PATH
from ansible.module_utils.generic import Types
from ansible.module_utils.module import SystemdUnitModule
class Module(SystemdUnitModule):
"""generates an systemd-networkd link"""
name = "systemd_link"
module_spec = dict(
argument_spec=dict(
mac=Types.str(help="The Mac address of the device"),
permanentmac=Types.str(
help="The Permanent Mac address advertised by the device"
),
path=Types.str(
help="A shell-style glob matching the persistent path, as exposed by the udev property ID_PATH."
),
driver=Types.str(
help="A glob matching the driver currently bound to the device"
),
type=Types.str(
help="A glob matching the device type, as exposed by networkctl list"
),
kind=Types.str(
help="a glob matching the device kind, as exposed by networkctl status INTERFACE or ip -d link show INTERFACE."
),
description=Types.str("The description for the link"),
name=Types.str(required=True, help="The new name of the device"),
mtu=Types.int(help="The maximum Transmission unit for the link"),
),
required_one_of=(
("mac", "permanentmac", "path", "driver", "type", "kind"),
("name", "mac", "permanentmac"),
),
)
def prepare(self):
self.__unit = None
newname = (
self.get("name", "") or self.get("mac", "") or self.get("permanentmac", "")
)
newname = newname.replace(":", "").replace("/", "-").lower()
self.unitfile = SYSTEMD_PATH.joinpath("50-" + newname).with_suffix(".link")
def unit(self) -> str:
if self.__unit is None:
self.__unit = "\n".join((self.match(), self.link()))
return self.__unit
def match(self) -> str:
options = []
if self.get("mac", False):
options.append("MACAddress={}\n".format(self.get("mac", False)))
if self.get("permanentmac", False):
options.append(
"PermanentAddress={}\n".format(self.get("permanentmac", False))
)
if self.get("path", False):
options.append("Path={}\n".format(self.get("path", False)))
if self.get("driver", False):
options.append("Driver={}\n".format(self.get("driver", False)))
if self.get("type", False):
options.append("Type={}\n".format(self.get("type", False)))
if self.get("kind", False):
options.append("Kind={}\n".format(self.get("kind", False)))
return "[Match]\n" + "".join(options)
def link(self) -> str:
options = []
if self.get("description", False):
options.append("Description={}\n".format(self.get("description", False)))
if self.get("name", False):
options.append("Name={}\n".format(self.get("name", False)))
if self.get("mtu", False):
options.append("MTUBytes={}\n".format(self.get("mtu", False)))
return "[Link]\n" + "".join(options)
def post(self):
if not self.changed:
return
args = [
"/usr/bin/udevadm",
"trigger",
"-c",
"add",
]
if self.module.check_mode:
args.append("-n")
if self.get("mac", False):
args.append("--attr-match=address={}".format(self.get("mac")))
if self.get("path", False):
args.append(self.get("path"))
self.module.run_command(args, check_rc=True)
DOCUMENTATION = """---
description:
- generates an systemd-networkd link
module: systemd_link
options:
description:
description:
- The description for the link
required: false
type: str
driver:
description:
- A glob matching the driver currently bound to the device
required: false
type: str
kind:
description:
- a glob matching the device kind, as exposed by networkctl status INTERFACE or
ip -d link show INTERFACE.
required: false
type: str
mac:
description:
- The Mac address of the device
required: false
type: str
mtu:
description:
- The maximum Transmission unit for the link
required: false
type: int
name:
description:
- The new name of the device
required: false
type: str
path:
description:
- A shell-style glob matching the persistent path, as exposed by the udev property
ID_PATH.
required: false
type: str
permanentmac:
description:
- The Permanent Mac address advertised by the device
required: false
type: str
type:
description:
- A glob matching the device type, as exposed by networkctl list
required: false
type: str
short_description: generates an systemd-networkd link
"""
if __name__ == "__main__":
Module()()

Datei anzeigen

@ -0,0 +1,165 @@
#!/usr/bin/python3
import pathlib
from typing import List, Optional
from ansible.module_utils.generic import SYSTEMD_SERVICE_CONFIG as SYSTEMD_PATH
from ansible.module_utils.generic import Types
from ansible.module_utils.module import SystemdUnitModule
SYSTEMD_PATH = pathlib.Path("/etc/systemd/system")
OPTION_MAPPING = dict(
required_by="RequiredBy",
wanted_by="WantedBy",
)
class Module(SystemdUnitModule):
"""Creates an systemd mount"""
name = "systemd_mount"
module_spec = dict(
argument_spec=dict(
fs=Types.str(
required=True, help="The filesystem that is used for the mount"
),
where=Types.path(
required=True, help="The Path where the filesystem is mounted to"
),
what=Types.str(
required=True, help="The device or an string that will be mounted"
),
state=Types.str(
choices=("present", "absent"),
default="present",
help="the state the mount is",
),
options=Types.list(elements=str, help="The options for the mount"),
description=Types.str(
help="An description for programs that access systemd"
),
required_by=Types.list(
elements=str, help="systemd units that require this mount"
),
wanted_by=Types.list(
elements=str,
help="systemd units that want the mount, but not explicitly require it. Commonly used for target if not service explicitly require it.",
),
),
)
def prepare(self):
self.mountdir = pathlib.Path(self.params["where"])
self.unitfile = SYSTEMD_PATH.joinpath(
self.mountdir.relative_to("/").as_posix().replace("/", "-")
).with_suffix(".mount")
self.__unit = None
def unit(self) -> str:
if self.__unit is None:
self.__unit = "\n".join(
(
self.header(),
self.mount(),
self.install(),
)
)
return self.__unit
def header(self) -> str:
return "[Unit]\nDescription={}\n".format(
self.get("description", "Mount for {}".format(self.get("where")))
)
def mount(self) -> str:
output = "[Mount]\n"
output += "Where={}\n".format(self.get("where"))
output += "What={}\n".format(self.get("what"))
output += "Type={}\n".format(self.get("fs"))
if self.get("options", False):
output += "Options={}\n".format(",".join(self.get("options")))
return output
def install(self) -> str:
output = "[Install]\n"
for argument, key in OPTION_MAPPING.items():
if self.get(argument, False):
for unit in self.get(argument):
output += "{}={}\n".format(key, unit)
return output
def post(self):
if not self.changed:
return
systemctl = self.module.get_bin_path("systemctl", required=True)
self.module.run_command([systemctl, "daemon-reload"], check_rc=True)
(rc, _, _) = self.module.run_command(
[systemctl, "is-enabled", self.unitfile.name], check_rc=False
)
if rc == 0:
self.module.run_command(
[systemctl, "restart", self.unitfile.name], check_rc=True
)
DOCUMENTATION = """---
description:
- Creates an systemd mount
module: systemd_mount
options:
description:
description:
- An description for programs that access systemd
required: false
type: str
fs:
description:
- The filesystem that is used for the mount
required: true
type: str
options:
default: []
description:
- The options for the mount
elements: str
required: false
type: list
required_by:
default: []
description:
- systemd units that require this mount
elements: str
required: false
type: list
state:
choices:
- present
- absent
default: present
description:
- the state the mount is
required: false
type: str
wanted_by:
default: []
description:
- systemd units that want the mount, but not explicitly require it. Commonly used
for target if not service explicitly require it.
elements: str
required: false
type: list
what:
description:
- The device or an string that will be mounted
required: true
type: str
where:
description:
- The Path where the filesystem is mounted to
required: true
type: path
short_description: Creates an systemd mount
"""
if __name__ == "__main__":
Module()()

Datei anzeigen

@ -0,0 +1,157 @@
#!/usr/bin/python3
import pathlib
from typing import List, Union
from ansible.module_utils.generic import SYSTEMD_NETWORK_CONFIG as SYSTEMD_PATH
from ansible.module_utils.generic import Types
from ansible.module_utils.module import SystemdUnitModule
def boolconvert(b: Union[bool, str]) -> str:
if b is True:
return "yes"
elif b is False:
return "no"
return b
class Module(SystemdUnitModule):
"""Sets up the systemd network unit"""
name = "systemd_network"
module_spec = dict(
argument_spec=dict(
mac=Types.str(),
device=Types.str(),
name=Types.str(required=True),
description=Types.str(),
dot=Types.bool(),
dnssec=Types.bool(),
dns=Types.list(elements=str),
domain=Types.list(elements=str),
defaultdns=Types.bool(),
address=Types.list(elements=str, required=True),
route=Types.list(elements=str),
),
required_if=(("defaultdns", True, ("dns",), False),),
required_one_of=(("mac", "device"),),
)
def prepare(self):
self.unitfile = SYSTEMD_PATH.joinpath(self.get("name")).with_suffix(".network")
self.__unit = None
def unit(self) -> str:
if self.__unit is None:
self.__unit = "\n".join(
(
self.match(),
self.network(),
self.addresses(),
self.routes(),
)
)
return self.__unit
def match(self) -> str:
matches = []
if self.get("mac", False):
matches.append("MACAddress={}\n".format(self.get("mac")))
if self.get("device", False):
matches.append("Name={}\n".format(self.get("device")))
return "[Match]\n" + "".join(matches)
def network(self) -> str:
output = "[Network]\n"
options = []
try:
options.append("Description={}".format(self.get("description")))
except KeyError:
pass
try:
for server in self.get("dns", []):
options.append(f"DNS={server}")
options.append("DNSDefaultRoute={}".format(self.get("defaultdns", False)))
except KeyError:
pass
try:
domain = self.get("domain")
self.set("domainlog", str(domain))
options.append("Domains={}".format(" ".join(domain)))
options.append(
"DNSOverTLS={}".format(boolconvert(self.get("dot", "opportunistic")))
)
options.append(
"DNSSEC={}".format(boolconvert(self.get("dnssec", "allow-downgrade")))
)
except KeyError:
pass
output += "\n".join(options)
return output
def addresses(self) -> str:
output = []
for address in self.get("address"):
output.append(f"[Address]\nAddress={address}\n")
return "\n".join(output)
def routes(self) -> str:
output = []
routes = self.get("route", [])
self.set("routes", routes)
for gw in routes:
output.append(f"[Route]\nGateway={gw}\nGatewayOnLink=yes\nQuickAck=yes\n")
self.set("routes", output)
return "\n".join(output)
DOCUMENTATION = """---
description:
- Sets up the systemd network unit
module: systemd_network
options:
address:
elements: str
required: true
type: list
defaultdns:
required: false
type: bool
description:
required: false
type: str
dns:
default: []
elements: str
required: false
type: list
dnssec:
required: false
type: bool
domain:
default: []
elements: str
required: false
type: list
dot:
required: false
type: bool
name:
required: true
type: str
mac:
elements: str
required: true
type: list
route:
default: []
elements: str
required: false
type: list
short_description: Sets up the systemd network unit
"""
if __name__ == "__main__":
Module()()

Datei anzeigen

@ -0,0 +1,40 @@
import os
import unittest
try: # pragma: nocover
from ansible_collections.sebastian.systemd.plugins.module_utils.generic import \
Types
except ImportError: # pragma: nocover
import sys
sys.path.append("plugins/module_utils")
from generic import Types
class TestTypes(unittest.TestCase):
"""tests the Types class"""
def testsimpletype(self):
"""this tests if an simple type is correctly build"""
output = Types.str(required=True, help="test", choices=("a", "1"), default="1")
self.assertIn("type", output)
self.assertIn("required", output)
self.assertIn("default", output)
self.assertIn("choices", output)
self.assertEquals(output["type"], "str")
self.assertEquals(Types.str.__name__, "str")
self.assertEquals(output["required"], True)
self.assertEquals(output["default"], "1")
self.assertTupleEqual(output["choices"], ("a", "1"))
Types.str()
def testlisttype(self):
"""this tests if the special type list is correctly build"""
output = Types.list(str)
Types.list("str")
self.assertIn("type", output)
self.assertIn("elements", output)
self.assertIn("required", output)
self.assertEquals(output["type"], "list")
self.assertEquals(output["required"], False)
self.assertEquals(output["elements"], "str")