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