diff --git a/.gitignore b/.gitignore index 445c72e..ea18ecb 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ TODO* *.pyc credentials*.yml *.uyuni.yml +test-*.yml +test-*.ini ansible.cfg *.tar.gz __pycache__ diff --git a/plugins/module_utils/exceptions.py b/plugins/module_utils/exceptions.py index 3d19acd..c67e8d0 100644 --- a/plugins/module_utils/exceptions.py +++ b/plugins/module_utils/exceptions.py @@ -91,3 +91,10 @@ class SSLCertVerificationError(Exception): .. class:: SSLCertVerificationError """ + +class AlreadyExistsException(Exception): + """ + Exception for already existing content + + .. class:: AlreadyExistsException + """ diff --git a/plugins/module_utils/uyuni.py b/plugins/module_utils/uyuni.py index a27a601..5dcb71b 100644 --- a/plugins/module_utils/uyuni.py +++ b/plugins/module_utils/uyuni.py @@ -16,7 +16,8 @@ InvalidCredentialsException, SessionException, SSLCertVerificationError, - CustomVariableExistsException + CustomVariableExistsException, + AlreadyExistsException ) __metaclass__ = type @@ -189,6 +190,214 @@ def get_all_hostgroups(self): f"Generic remote communication error: {err.faultString!r}" ) from err + def add_system_group(self, name, description): + """ + Creates a system group + + :param name: group name + :type name: str + :param description: group description + :type description: str + """ + try: + return self._session.systemgroup.create( + self._api_key, name, description + ) + except Fault as err: + if "already exists" in err.faultString.lower(): + raise AlreadyExistsException( + f"System group already exists: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def update_system_group(self, name, description): + """ + Updates a system group + + :param name: group name + :type name: str + :param description: group description + :type description: str + """ + try: + return self._session.systemgroup.update( + self._api_key, name, description + ) + except Fault as err: + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def remove_system_group(self, name): + """ + Removes a system group + + :param name: group name + :type name: str + """ + try: + return self._session.systemgroup.delete( + self._api_key, name + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def get_system_group_details(self, name): + """ + Retrieves details about a particular system group + + :param name: group name + :type name: str + """ + try: + return self._session.systemgroup.getDetails( + self._api_key, name + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def get_system_group_admins(self, name): + """ + Retrieves admins from a particular system group + + :param name: group name + :type name: str + """ + try: + return self._session.systemgroup.listAdministrators( + self._api_key, name + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def add_or_remove_system_group_admins(self, name, admins, mode=1): + """ + Adds/removes system group admins + + :param name: group name + :type name: str + :param admins: admins + :type admins: list(str) + :type mode: add (1) or remove (0) + :param mode: int + """ + if not isinstance(admins, list): + raise EmptySetException( + "Format error - use list of usernames" + ) + try: + return self._session.systemgroup.addOrRemoveAdmins( + self._api_key, name, admins, mode + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + # def get_system_group_systems(self, ): + # """ + # """ + + # def add_or_remove_system_group_systems(self,): + # """ + # """ + + def get_system_group_config_channels(self, ): + """ + Retrieves configuration channels from a particular system group + + :param name: group name + :type name: str + """ + try: + return self._session.systemgroup.listAssignedConfigChannels( + self._api_key, name + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def add_system_group_config_channels(self, name, channels): + """ + Adds system group configuration channels + + :param name: group name + :type name: str + :param channels: configuration channels + :type channels: list(str) + """ + if not isinstance(admins, list): + raise EmptySetException( + "Format error - use list of configuration channels" + ) + try: + return self._session.systemgroup.subscribeConfigChannel( + self._api_key, name, channels + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def remove_system_group_config_channels(self, ): + """ + Removes system group configuration channels + + :param name: group name + :type name: str + :param channels: configuration channels + :type channels: list(str) + """ + if not isinstance(admins, list): + raise EmptySetException( + "Format error - use list of configuration channels" + ) + try: + return self._session.systemgroup.unsubscribeConfigChannel( + self._api_key, name, channels + ) + except Fault as err: + if "unable to locate or access server group" in err.faultString.lower(): + raise EmptySetException( + f"System group not found: {name!r}" + ) from err + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + def get_hostgroups_by_host(self, system_id): """ Returns all groups for a specific host @@ -1307,3 +1516,114 @@ def schedule_openscap_run(self, system_id, document, arguments=None): raise SessionException( f"Generic remote communication error: {err.faultString!r}" ) from err + + def get_activationkeys(self): + """ + Returns all defined activation keys + """ + try: + return self._session.activationkey.listActivationKeys( + self._api_key + ) + except Fault as err: + raise SessionException( + f"Generic remote communication error: {err.faultString!r}" + ) from err + + def add_activationkey( + self, key, description, + basechannel=None, usage=None, entitlements=None, + universal_default=None + ): + """ + Creates a new activation key + + :param key: key name + :type key: str + :param description: key description + :type description: str + :param basechannel: assigned software basechannel + :type basechannel: str + :param usage: key usage limit + :type usage: int + :param entitlements: key entitlements + :type entitlements: list (str) + :param universal_default: flag whether key is universal default + :type universal_default: bool + """ + # TODO + print("TODO") + + def activationkey_set_child_channels(self, key, child_channels): + """ + Assigns child channels to an activation key + + :param key: key name + :type key: str + :param child_channels: child channel labels + :type child_channels: list (str) + """ + # TODO + print("remove child channels") + print("add child channels") + + def activationkey_set_details( + self, key, description, + basechannel, usage, universal_default, + contact_method + ): + """ + Sets a activation keys details + + :param key: key name + :type key: str + :param description: key description + :type description: str + :param basechannel: assigned software basechannel + :type basechannel: str + :param usage: key usage limit + :type usage: int + :param universal_default: flag whether key is universal default + :type universal_default: bool + :param contact_method: machine contact method + :type contact_method: str + """ + # TODO + print("TODO") + + def activationkey_set_config_channels(self, key, child_channels): + """ + Assigns config channels to an activation key + + :param key: key name + :type key: str + :param config_channels: config channel labels + :type config_channels: list (str) + """ + # TODO + print("TODO: remove config channels") + print("TODO: add config channels") + + def activationkey_set_packages(self, key, packages): + """ + Assigns packages to an activation key + + :param key: key name + :type key: str + :param packages: software packages + :type packages: list (str) + """ + print("TODO: remove packages") + print("TODO: add packages") + + def activationkey_set_hostgroups(self, key, hostgroups): + """ + Assigns hostgroups to an activation key + + :param key: key name + :type key: str + :param hostgroups: hostgroup names + :type hostgroups: list (str) + """ + print("TODO: remove hostgroups") + print("TODO: add hostgroups") diff --git a/plugins/modules/activation_key.py b/plugins/modules/activation_key.py new file mode 100644 index 0000000..6cc0c46 --- /dev/null +++ b/plugins/modules/activation_key.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +""" +Ansible Module for managing activation keys + +2023 Christian Stankowic + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: activation_key +short_description: Manage activation key +description: + - Manage activation key +author: + - "Christian Stankowic (@stdevel)" +extends_documentation_fragment: + - stdevel.uyuni.uyuni_auth +options: + name: + description: Name of the activation key + required: True + type: str + description: + description: Description of the activation key + required: True + type: str + base_channel: + description: Software base channel to assign + required: True + type: str + child_channels: + description: Child channels to assign + required: False + type: list + elements: str + contact_method: + description: System contact method + required: True + type: str + default: default + choices: + - default + - ssh-push + - ssh-push-tunnel + config_channels: + description: Configuration channels to assign + required: False + type: list + elements: str + entitlements: + description: Entitlements to assign + required: False + type: list + elements: str + choices: + - container_build_host + - monitoring_entitled + - osimage_build_host + - virtualization_host + - ansible_control_node + packages: + description: Packages to install + required: False + type: list + elements: str + hostgroups: + description: Hostgroups to assign + required: False + type: list + elements: str + limit: + description: Usage limit + required: False + type: int + universal_default: + description: Define as universal default + required: False + type: bool +''' + +EXAMPLES = ''' +- name: Create activation key + stdevel.uyuni.activation_key: + uyuni_host: 192.168.1.1 + uyuni_user: admin + uyuni_password: admin + name: 1-ak-debian11 + base_channel: debian-11-pool-amd64-uyuni + child_channels: + - debian-11-amd64-main-updates-uyuni + - debian-11-amd64-main-security-uyuni + - debian-11-amd64-uyuni-client + packages: neofetch + hostgroups: + - Debian + - Test +''' + +RETURN = ''' +entity: + description: State whether activation key was created, updated or removed + returned: success + type: bool +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError +from ..module_utils.helper_functions import _configure_connection, get_host_id + + +# def _reboot_host(module, api_instance): +# """ +# Reboots the host +# """ +# try: +# api_instance.reboot_host( +# get_host_id( +# module.params.get('name'), +# api_instance +# ) +# ) +# module.exit_json(changed=True) +# except SSLCertVerificationError: +# module.fail_json(msg="Failed to verify SSL certificate") +# except EmptySetException as err: +# module.fail_json(msg=f"Exception when calling UyuniAPI->reboot_host: {err}") + + +def main(): + argument_spec = dict( + uyuni_host=dict(required=True), + uyuni_user=dict(required=True), + uyuni_password=dict(required=True, no_log=True), + uyuni_port=dict(default=443, type='int'), + uyuni_verify_ssl=dict(default=True, type='bool'), + name=dict(required=True), + description=dict(required=True), + base_channel=dict(required=True), + child_channels=dict(required=False, type='list', elements='str'), + contact_method=dict(required=True, type='str', choices=['default', 'ssh-push', 'ssh-push-tunnel']), + config_channels=dict(required=False, type='list', elements='str'), + entitlements=dict(required=False, type='list', elements='str', choices=['container_build_host', 'monitoring_entitled', 'osimage_build_host', 'virtualization_host', 'ansible_control_node']), + packages=dict(required=False, type='list', elements='str'), + hostgroups=dict(required=False, type='list', elements='str'), + limit=dict(required=False, type='int'), + universal_default=dict(required=False, type='bool') + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + connection_params = dict( + host=module.params.get('uyuni_host'), + username=module.params.get('uyuni_user'), + password=module.params.get('uyuni_password'), + port=module.params.get('uyuni_port'), + verify_ssl=module.params.get('uyuni_verify_ssl') + ) + + api_instance = _configure_connection(connection_params) + # TODO: create, update or remove activation key + #_create_activationkey(module, api_instance) + #_update_activationkey(module, api_instance) + #_remove_activationkey(module, api_instance) + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/system_group.py b/plugins/modules/system_group.py new file mode 100644 index 0000000..a7046b4 --- /dev/null +++ b/plugins/modules/system_group.py @@ -0,0 +1,263 @@ +#!/usr/bin/python +""" +Ansible Module for managing system groups + +2023 Christian Stankowic + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: system_group +short_description: Manage system group +description: + - Manage system group +author: + - "Christian Stankowic (@stdevel)" +extends_documentation_fragment: + - stdevel.uyuni.uyuni_auth +options: + name: + description: Name of the system group + required: True + type: str + description: + description: Description of the system group + required: True + type: str + admins: + description: Admins to assign + required: False + type: list + elements: str + append_admins: + description: Appends admins instead of replacing them + type: bool + default: False + systems: + description: Systems to assign + required: False + type: list + elements: str + append_systems: + description: Appends systems instead of replacing them + type: bool + default: False + config_channels: + description: Defines configuration channels to use + required: False + type: list + elements: str + append_config_channels: + description: Appends config channels instead of replacing them + type: bool + default: False + state: + description: Defines whether ressources should be created (present) or removed (absent) + default: present + choices: + - present + - absent +''' + +EXAMPLES = ''' +- name: Create system group + stdevel.uyuni.system_group: + uyuni_host: 192.168.1.1 + uyuni_user: admin + uyuni_password: admin + name: debian-hosts + description: Debian servers + admins: + - sgiertz + - ppinkepank + systems: + - debsrv001 + - debsrv002 + config_channels: + - base-configs + +- name: Remove system group + stdevel.uyuni.system_group: + uyuni_host: 192.168.1.1 + uyuni_user: admin + uyuni_password: admin + name: devuan-hosts + state: absent +''' + +RETURN = ''' +entity: + description: State whether system group was created, updated or removed + returned: success + type: bool +''' + +from ansible.module_utils.basic import AnsibleModule +from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError, AlreadyExistsException +from ..module_utils.helper_functions import _configure_connection, get_host_id + + +def _manage_system_group_assignments(module, api_instance): + """ + Manage system, admin and configuration channel assignments + """ + # get current assignments + _admins = api_instance.get_system_group_admins( + module.params.get('name') + ) + _configs = api_instance.get_system_group_config_channels( + module.params.get('name') + ) + + if module.params.get('append_admins') == False: + # remove non-matched admins + _delete = [x for x in _admins if x not in module.params.get('admins')] + api_instance.add_or_remove_system_group_admins( + module.params.get('name'), + _delete, + mode=0 + ) + if module.params.get('append_config_channels') == False: + # remove non-matched config channels + _delete = [x for x in _configs if x not in module.params.get('config_channels')] + api_instance.remove_system_group_config_channels( + module.params.get('name'), + _delete + ) + + # add missing admins + _add = [x for x in module.params.get('admins') if x not in _admins] + api_instance.add_or_remove_system_group_admins( + module.params.get('name'), + _add + ) + # add missing config channels + _add = [x for x in module.params.get('config_channels') if x not in _configs] + api_instance.add_system_group_channels( + module.params.get('name'), + _add + ) + +def _create_system_group(module, api_instance): + """ + Creates a system group + """ + try: + api_instance.add_system_group( + module.params.get('name'), + module.params.get('description') + ) + # manage systems, admins and configuration channels + _manage_system_group_assignments(module, api_instance) + module.exit_json(changed=True) + except SSLCertVerificationError: + module.fail_json(msg="Failed to verify SSL certificate") + except EmptySetException as err: + module.fail_json(msg=f"Exception when calling UyuniAPI->create_system_group: {err}") + +def _update_system_group(module, api_instance): + """ + Update a system group + """ + try: + # update group + api_instance.update_system_group( + module.params.get('name'), + module.params.get('description') + ) + # manage systems, admins and configuration channels + _manage_system_group_assignments(module, api_instance) + module.exit_json(changed=True) + except SSLCertVerificationError: + module.fail_json(msg="Failed to verify SSL certificate") + except EmptySetException as err: + module.fail_json(msg=f"Exception when calling UyuniAPI->update_system_group: {err}") + +def _remove_system_group(module, api_instance): + """ + Removes a system group + """ + try: + api_instance.remove_system_group( + module.params.get('name') + ) + module.exit_json(changed=True) + except EmptySetException: + module.exit_json(changed=False) + except SSLCertVerificationError: + module.fail_json(msg="Failed to verify SSL certificate") + except EmptySetException as err: + module.fail_json(msg=f"Exception when calling UyuniAPI->remove_system_group: {err}") + + +def main(): + argument_spec = dict( + uyuni_host=dict(required=True), + uyuni_user=dict(required=True), + uyuni_password=dict(required=True, no_log=True), + uyuni_port=dict(default=443, type='int'), + uyuni_verify_ssl=dict(default=True, type='bool'), + name=dict(required=True), + description=dict(required=True), + admins=dict(required=False, type='list', elements='str'), + append_admins=dict(default=False), + systems=dict(required=False, type='list', elements='str'), + append_systems=dict(default=False), + config_channels=dict(required=False, type='list', elements='str'), + append_config_channels=dict(default=False), + state=dict(default='present', choices=['present', 'absent']) + ) + + module = AnsibleModule( + argument_spec=argument_spec, + supports_check_mode=False + ) + + connection_params = dict( + host=module.params.get('uyuni_host'), + username=module.params.get('uyuni_user'), + password=module.params.get('uyuni_password'), + port=module.params.get('uyuni_port'), + verify_ssl=module.params.get('uyuni_verify_ssl') + ) + + api_instance = _configure_connection(connection_params) + + if module.params.get('state') == 'absent': + # remove system group + _remove_system_group(module, api_instance) + + # create/update system group + try: + _create_system_group(module, api_instance) + except AlreadyExistsException: + # update only if necessary + _details = api_instance.get_system_group_details( + module.params.get('name') + ) + if _details['description'] != module.params.get('description'): + _update_system_group(module, api_instance) + module.exit_json(changed=False) + +if __name__ == '__main__': + main()