diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:03:42 +0000 |
commit | 66cec45960ce1d9c794e9399de15c138acb18aed (patch) | |
tree | 59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/theforeman/foreman/plugins | |
parent | Initial commit. (diff) | |
download | ansible-upstream.tar.xz ansible-upstream.zip |
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/theforeman/foreman/plugins')
87 files changed, 18860 insertions, 0 deletions
diff --git a/ansible_collections/theforeman/foreman/plugins/callback/foreman.py b/ansible_collections/theforeman/foreman/plugins/callback/foreman.py new file mode 100644 index 00000000..674d77ac --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/callback/foreman.py @@ -0,0 +1,395 @@ +# -*- coding: utf-8 -*- +# (c) 2015, 2016 Daniel Lobato <elobatocs@gmail.com> +# (c) 2016 Guido Günther <agx@sigxcpu.org> +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# pylint: disable=super-with-arguments + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: foreman + type: notification + short_description: Sends events to Foreman + description: + - This callback will report facts and task events to Foreman + requirements: + - whitelisting in configuration + - requests (python library) + options: + report_type: + description: + - "endpoint type for reports: foreman or proxy" + env: + - name: FOREMAN_REPORT_TYPE + default: foreman + ini: + - section: callback_foreman + key: report_type + url: + description: + - URL of the Foreman server. + env: + - name: FOREMAN_URL + - name: FOREMAN_SERVER_URL + - name: FOREMAN_SERVER + required: True + ini: + - section: callback_foreman + key: url + proxy_url: + description: + - URL of the Foreman Smart Proxy server. + env: + - name: FOREMAN_PROXY_URL + ini: + - section: callback_foreman + key: proxy_url + client_cert: + description: + - X509 certificate to authenticate to Foreman if https is used + env: + - name: FOREMAN_SSL_CERT + default: /etc/foreman/client_cert.pem + ini: + - section: callback_foreman + key: ssl_cert + - section: callback_foreman + key: client_cert + aliases: [ ssl_cert ] + client_key: + description: + - the corresponding private key + env: + - name: FOREMAN_SSL_KEY + default: /etc/foreman/client_key.pem + ini: + - section: callback_foreman + key: ssl_key + - section: callback_foreman + key: client_key + aliases: [ ssl_key ] + verify_certs: + description: + - Toggle to decide whether to verify the Foreman certificate. + - It can be set to '1' to verify SSL certificates using the installed CAs or to a path pointing to a CA bundle. + - Set to '0' to disable certificate checking. + env: + - name: FOREMAN_SSL_VERIFY + default: 1 + ini: + - section: callback_foreman + key: verify_certs + dir_store: + description: + - When set, callback does not perform HTTP calls but stores results in a given directory. + - For each report, new file in the form of SEQ_NO-hostname.json is created. + - For each facts, new file in the form of SEQ_NO-hostname.json is created. + - The value must be a valid directory. + - This is meant for debugging and testing purposes. + - When set to blank (default) this functionality is turned off. + env: + - name: FOREMAN_DIR_STORE + default: '' + ini: + - section: callback_foreman + key: dir_store + disable_callback: + description: + - Toggle to make the callback plugin disable itself even if it is loaded. + - It can be set to '1' to prevent the plugin from being used even if it gets loaded. + env: + - name: FOREMAN_CALLBACK_DISABLE + default: 0 +''' + +import os +from datetime import datetime +from collections import defaultdict +import json +import time + +try: + import requests + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + +from ansible.module_utils._text import to_text +from ansible.module_utils.parsing.convert_bool import boolean as to_bool +from ansible.plugins.callback import CallbackBase + + +def build_log_foreman(data_list): + """ + Transform the internal log structure to one accepted by Foreman's + config_report API. + """ + for data in data_list: + result = data.pop('result') + task = data.pop('task') + result['failed'] = data.get('failed') + result['module'] = task.get('action') + if data.get('failed'): + level = 'err' + elif result.get('changed'): + level = 'notice' + else: + level = 'info' + + yield { + "log": { + 'sources': { + 'source': task.get('name'), + }, + 'messages': { + 'message': json.dumps(result, sort_keys=True), + }, + 'level': level, + } + } + + +def get_time(): + """ + Return the time for measuring duration. Prefers monotonic time but + falls back to the regular time on older Python versions. + """ + try: + return time.monotonic() + except AttributeError: + return time.time() + + +def get_now(): + """ + Return the current timestamp as a string to be sent over the network. + The time is always in UTC *with* timezone information, so that Ruby + DateTime can easily parse it. + """ + return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S+00:00") + + +class CallbackModule(CallbackBase): + CALLBACK_VERSION = 2.0 + CALLBACK_TYPE = 'notification' + CALLBACK_NAME = 'theforeman.foreman.foreman' + CALLBACK_NEEDS_WHITELIST = True + + def __init__(self): + super(CallbackModule, self).__init__() + self.items = defaultdict(list) + self.facts = defaultdict(dict) + self.start_time = get_time() + + def set_options(self, task_keys=None, var_options=None, direct=None): + + super(CallbackModule, self).set_options(task_keys=task_keys, var_options=var_options, direct=direct) + + if self.get_option('disable_callback'): + self._disable_plugin('Callback disabled by environment.') + + self.report_type = self.get_option('report_type') + self.foreman_url = self.get_option('url') + self.proxy_url = self.get_option('proxy_url') + ssl_cert = self.get_option('client_cert') + ssl_key = self.get_option('client_key') + self.dir_store = self.get_option('dir_store') + + if not HAS_REQUESTS: + self._disable_plugin(u'The `requests` python module is not installed') + + self.session = requests.Session() + if self.foreman_url.startswith('https://'): + if not os.path.exists(ssl_cert): + self._disable_plugin(u'FOREMAN_SSL_CERT %s not found.' % ssl_cert) + + if not os.path.exists(ssl_key): + self._disable_plugin(u'FOREMAN_SSL_KEY %s not found.' % ssl_key) + + self.session.verify = self._ssl_verify(str(self.get_option('verify_certs'))) + self.session.cert = (ssl_cert, ssl_key) + + def _disable_plugin(self, msg): + self.disabled = True + if msg: + self._display.warning(msg + u' Disabling the Foreman callback plugin.') + else: + self._display.warning(u'Disabling the Foreman callback plugin.') + + def _ssl_verify(self, option): + try: + verify = to_bool(option) + except TypeError: + verify = option + + if verify is False: # is only set to bool if try block succeeds + requests.packages.urllib3.disable_warnings() + self._display.warning( + u"SSL verification of %s disabled" % self.foreman_url, + ) + + return verify + + def _send_data(self, data_type, report_type, host, data): + if data_type == 'facts': + url = self.foreman_url + '/api/v2/hosts/facts' + elif data_type == 'report' and report_type == 'foreman': + url = self.foreman_url + '/api/v2/config_reports' + elif data_type == 'report' and report_type == 'proxy': + url = self.proxy_url + '/reports/ansible' + else: + self._display.warning(u'Unknown report_type: {rt}'.format(rt=report_type)) + + if len(self.dir_store) > 0: + filename = u'{host}.json'.format(host=to_text(host)) + filename = os.path.join(self.dir_store, filename) + with open(filename, 'w') as f: + json.dump(data, f, indent=2, sort_keys=True) + else: + try: + response = self.session.post(url=url, json=data) + response.raise_for_status() + except requests.exceptions.RequestException as err: + self._display.warning(u'Sending data to Foreman at {url} failed for {host}: {err}'.format( + host=to_text(host), err=to_text(err), url=to_text(self.foreman_url))) + + def send_facts(self): + """ + Sends facts to Foreman, to be parsed by foreman_ansible fact + parser. The default fact importer should import these facts + properly. + """ + # proxy parses facts from report directly + if self.report_type == "proxy": + return + + for host, facts in self.facts.items(): + facts = { + "name": host, + "facts": { + "ansible_facts": facts, + "_type": "ansible", + "_timestamp": get_now(), + }, + } + + self._send_data('facts', 'foreman', host, facts) + + def send_reports_proxy_host_report(self, stats): + """ + Send reports to Foreman Smart Proxy running Host Reports + plugin. The format is native Ansible report without any + changes. + """ + for host in stats.processed.keys(): + report = { + "host": host, + "reported_at": get_now(), + "metrics": { + "time": { + "total": int(get_time() - self.start_time) + } + }, + "summary": stats.summarize(host), + "results": self.items[host], + "check_mode": self.check_mode, + } + + self._send_data('report', 'proxy', host, report) + self.items[host] = [] + + def send_reports_foreman(self, stats): + """ + Send reports to Foreman to be parsed by its config report + importer. The data is in a format that Foreman can handle + without writing another report importer. + """ + for host in stats.processed.keys(): + total = stats.summarize(host) + report = { + "config_report": { + "host": host, + "reported_at": get_now(), + "metrics": { + "time": { + "total": int(get_time() - self.start_time) + } + }, + "status": { + "applied": total['changed'], + "failed": total['failures'] + total['unreachable'], + "skipped": total['skipped'], + }, + "logs": list(build_log_foreman(self.items[host])), + "reporter": "ansible", + "check_mode": self.check_mode, + } + } + if self.check_mode: + report['config_report']['status']['pending'] = total['changed'] + report['config_report']['status']['applied'] = 0 + + self._send_data('report', 'foreman', host, report) + self.items[host] = [] + + def send_reports(self, stats): + if self.report_type == "foreman": + self.send_reports_foreman(stats) + elif self.report_type == "proxy": + self.send_reports_proxy_host_report(stats) + else: + self._display.warning(u'Unknown foreman endpoint type: {type}'.format(type=self.report_type)) + + def drop_nones(self, d): + """Recursively drop Nones or empty dicts/arrays in dict d and return a new dict""" + dd = {} + for k, v in d.items(): + if isinstance(v, dict) and v: + dd[k] = self.drop_nones(v) + elif isinstance(v, list) and len(v) == 1 and v[0] == {}: + pass + elif isinstance(v, (list, set, tuple)) and v: + dd[k] = type(v)(self.drop_nones(vv) if isinstance(vv, dict) else vv + for vv in v) + elif not isinstance(v, (dict, list, set, tuple)) and v is not None: + dd[k] = v + return dd + + def append_result(self, result, failed=False): + result_info = result._result + task_info = result._task.serialize() + task_info['args'] = None + value = {} + value['result'] = result_info + value['task'] = task_info + value['failed'] = failed + if self.report_type == "proxy": + value = self.drop_nones(value) + host = result._host.get_name() + self.items[host].append(value) + self.check_mode = result._task.check_mode + if 'ansible_facts' in result_info: + self.facts[host].update(result_info['ansible_facts']) + + # Ansible callback API + def v2_runner_on_failed(self, result, ignore_errors=False): + self.append_result(result, True) + + def v2_runner_on_unreachable(self, result): + self.append_result(result, True) + + def v2_runner_on_async_ok(self, result): + self.append_result(result) + + def v2_runner_on_async_failed(self, result): + self.append_result(result, True) + + def v2_playbook_on_stats(self, stats): + self.send_facts() + self.send_reports(stats) + + def v2_runner_on_ok(self, result): + self.append_result(result) diff --git a/ansible_collections/theforeman/foreman/plugins/doc_fragments/foreman.py b/ansible_collections/theforeman/foreman/plugins/doc_fragments/foreman.py new file mode 100644 index 00000000..0fb75059 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/doc_fragments/foreman.py @@ -0,0 +1,381 @@ +# (c) 2019, Evgeni Golov <evgeni@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. +# + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +class ModuleDocFragment(object): + + # Foreman documentation fragment + DOCUMENTATION = ''' +requirements: + - requests +options: + server_url: + description: + - URL of the Foreman server. + - If the value is not specified in the task, the value of environment variable C(FOREMAN_SERVER_URL) will be used instead. + required: true + type: str + username: + description: + - Username accessing the Foreman server. + - If the value is not specified in the task, the value of environment variable C(FOREMAN_USERNAME) will be used instead. + required: true + type: str + password: + description: + - Password of the user accessing the Foreman server. + - If the value is not specified in the task, the value of environment variable C(FOREMAN_PASSWORD) will be used instead. + required: true + type: str + validate_certs: + description: + - Whether or not to verify the TLS certificates of the Foreman server. + - If the value is not specified in the task, the value of environment variable C(FOREMAN_VALIDATE_CERTS) will be used instead. + default: true + type: bool +''' + + NESTED_PARAMETERS = ''' +options: + parameters: + description: + - Entity domain specific host parameters + required: false + type: list + elements: dict + suboptions: + name: + description: + - Name of the parameter + required: true + type: str + value: + description: + - Value of the parameter + required: true + type: raw + parameter_type: + description: + - Type of the parameter + default: 'string' + choices: + - 'string' + - 'boolean' + - 'integer' + - 'real' + - 'array' + - 'hash' + - 'yaml' + - 'json' + type: str +''' + + OS_FAMILY = ''' +options: + os_family: + description: + - The OS family the entity shall be assigned with. + required: false + choices: + - AIX + - Altlinux + - Archlinux + - Coreos + - Debian + - Fcos + - Freebsd + - Gentoo + - Junos + - NXOS + - Rancheros + - Redhat + - Rhcos + - Solaris + - Suse + - VRP + - Windows + - Xenserver + type: str +''' + + TAXONOMY = ''' +options: + organizations: + description: List of organizations the entity should be assigned to + type: list + elements: str + locations: + description: List of locations the entity should be assigned to + type: list + elements: str +''' + + ENTITY_STATE = ''' +options: + state: + description: + - State of the entity + default: present + choices: + - present + - absent + type: str +''' + + ENTITY_STATE_WITH_DEFAULTS = ''' +options: + state: + description: + - State of the entity + - C(present_with_defaults) will ensure the entity exists, but won't update existing ones + default: present + choices: + - present + - present_with_defaults + - absent + type: str +''' + + HOST_OPTIONS = ''' +options: + compute_resource: + description: Compute resource name + required: false + type: str + compute_profile: + description: Compute profile name + required: false + type: str + domain: + description: Domain name + required: false + type: str + subnet: + description: IPv4 Subnet name + required: false + type: str + subnet6: + description: IPv6 Subnet name + required: false + type: str + root_pass: + description: + - Root password. + - Will result in the entity always being updated, as the current password cannot be retrieved. + type: str + required: false + realm: + description: Realm name + required: false + type: str + architecture: + description: Architecture name + required: False + type: str + medium: + aliases: [ media ] + description: + - Medium name + - Mutually exclusive with I(kickstart_repository). + required: False + type: str + pxe_loader: + description: PXE Bootloader + required: false + choices: + - PXELinux BIOS + - PXELinux UEFI + - Grub UEFI + - Grub2 BIOS + - Grub2 ELF + - Grub2 UEFI + - Grub2 UEFI SecureBoot + - Grub2 UEFI HTTP + - Grub2 UEFI HTTPS + - Grub2 UEFI HTTPS SecureBoot + - iPXE Embedded + - iPXE UEFI HTTP + - iPXE Chain BIOS + - iPXE Chain UEFI + - None + type: str + ptable: + description: Partition table name + required: False + type: str + environment: + description: Puppet environment name + required: false + type: str + puppetclasses: + description: List of puppet classes to include in this host group. Must exist for hostgroup's puppet environment. + required: false + type: list + elements: str + config_groups: + description: Config groups list + required: false + type: list + elements: str + puppet_proxy: + description: Puppet server proxy name + required: false + type: str + puppet_ca_proxy: + description: Puppet CA proxy name + required: false + type: str + openscap_proxy: + description: + - OpenSCAP proxy name. + - Only available when the OpenSCAP plugin is installed. + required: false + type: str + content_source: + description: + - Content source. + - Only available for Katello installations. + required: false + type: str + lifecycle_environment: + description: + - Lifecycle environment. + - Only available for Katello installations. + required: false + type: str + kickstart_repository: + description: + - Kickstart repository name. + - You need to provide this to use the "Synced Content" feature. + - Mutually exclusive with I(medium). + - Only available for Katello installations. + required: false + type: str + content_view: + description: + - Content view. + - Only available for Katello installations. + required: false + type: str + activation_keys: + description: + - Activation Keys used for deployment. + - Comma separated list. + - Only available for Katello installations. + required: false + type: str +''' + + ORGANIZATION = ''' +options: + organization: + description: + - Organization that the entity is in + required: true + type: str +''' + + SCAP_DATASTREAM = ''' +options: + scap_file: + description: + - File containing XML DataStream content. + - Required when creating a new DataStream. + required: false + type: path + original_filename: + description: + - Original file name of the XML file. + - If unset, the filename of I(scap_file) will be used. + required: false + type: str +''' + + OPERATINGSYSTEMS = ''' +options: + operatingsystems: + description: + - List of operating systems the entity should be assigned to. + - Operating systems are looked up by their title which is composed as "<name> <major>.<minor>". + - You can omit the version part as long as you only have one operating system by that name. + required: false + type: list + elements: str +''' + + OPERATINGSYSTEM = ''' +options: + operatingsystem: + description: + - Operating systems are looked up by their title which is composed as "<name> <major>.<minor>". + - You can omit the version part as long as you only have one operating system by that name. + type: str + required: False +''' + + INFOMODULE = ''' +options: + name: + description: + - Name of the resource to fetch information for. + - Mutually exclusive with I(search). + required: false + type: str + location: + description: + - Label of the Location to scope the search for. + required: false + type: str + organization: + description: + - Name of the Organization to scope the search for. + required: false + type: str + search: + description: + - Search query to use + - If None, and I(name) is not set, all resources are returned. + - Mutually exclusive with I(name). + type: str +''' + + INFOMODULEWITHOUTNAME = ''' +options: + location: + description: + - Label of the Location to scope the search for. + required: false + type: str + organization: + description: + - Name of the Organization to scope the search for. + required: false + type: str + search: + description: + - Search query to use + - If None, all resources are returned. + type: str +''' + + KATELLOINFOMODULE = ''' +options: + organization: + required: true +''' diff --git a/ansible_collections/theforeman/foreman/plugins/filter/cp_label.yml b/ansible_collections/theforeman/foreman/plugins/filter/cp_label.yml new file mode 100644 index 00000000..94c7fa76 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/filter/cp_label.yml @@ -0,0 +1,21 @@ +DOCUMENTATION: + name: cp_label + author: Matthias Dellweg + version_added: '0.1.0' + short_description: Convert strings to Candlepin labels + description: + - Converts an arbitrary string to a valid Candlepin label + options: + _input: + description: String that should be converted + type: string + required: true + +EXAMPLES: | + organization_label: "{{ 'Default Organization' | cp_label }}" + # => 'Default_Organization' + +RETURN: + _value: + description: The converted Candlepin label + type: string diff --git a/ansible_collections/theforeman/foreman/plugins/filter/foreman.py b/ansible_collections/theforeman/foreman/plugins/filter/foreman.py new file mode 100644 index 00000000..e5e7871d --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/filter/foreman.py @@ -0,0 +1,24 @@ +# Copyright (c) 2019 Matthias Dellweg +# 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 + + +import re + + +def cp_label(value): + p = re.compile(r'[^-\w]+') + return p.sub('_', value) + + +# ---- Ansible filters ---- +class FilterModule(object): + ''' Foreman filter ''' + + def filters(self): + return { + 'cp_label': cp_label, + } diff --git a/ansible_collections/theforeman/foreman/plugins/inventory/foreman.py b/ansible_collections/theforeman/foreman/plugins/inventory/foreman.py new file mode 100644 index 00000000..9f6cafec --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/inventory/foreman.py @@ -0,0 +1,671 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016 Guido Günther <agx@sigxcpu.org>, Daniel Lobato Garcia <dlobatog@redhat.com> +# Copyright (c) 2018 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# pylint: disable=raise-missing-from +# pylint: disable=super-with-arguments + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = ''' + name: foreman + short_description: Foreman inventory source + requirements: + - requests >= 1.1 + description: + - Get inventory hosts from Foreman. + - Uses a YAML configuration file that ends with ``foreman.(yml|yaml)``. + extends_documentation_fragment: + - inventory_cache + - constructed + options: + plugin: + description: token that ensures this is a source file for the C(foreman) plugin. + required: True + choices: ['theforeman.foreman.foreman'] + url: + description: + - URL of the Foreman server. + default: 'http://localhost:3000' + env: + - name: FOREMAN_SERVER + - name: FOREMAN_SERVER_URL + - name: FOREMAN_URL + user: + description: + - Username accessing the Foreman server. + required: True + env: + - name: FOREMAN_USER + - name: FOREMAN_USERNAME + password: + description: + - Password of the user accessing the Foreman server. + required: True + env: + - name: FOREMAN_PASSWORD + validate_certs: + description: + - Whether or not to verify the TLS certificates of the Foreman server. + type: boolean + default: True + env: + - name: FOREMAN_VALIDATE_CERTS + group_prefix: + description: prefix to apply to foreman groups + default: foreman_ + vars_prefix: + description: prefix to apply to host variables, does not include facts nor params + default: foreman_ + want_facts: + description: Toggle, if True the plugin will retrieve host facts from the server + type: boolean + default: False + want_params: + description: Toggle, if true the inventory will retrieve 'all_parameters' information as host vars + type: boolean + default: False + want_hostcollections: + description: Toggle, if true the plugin will create Ansible groups for host collections + type: boolean + default: False + legacy_hostvars: + description: + - Toggle, if true the plugin will build legacy hostvars present in the foreman script + - Places hostvars in a dictionary with keys `foreman`, `foreman_facts`, and `foreman_params` + type: boolean + default: False + host_filters: + description: This can be used to restrict the list of returned host + type: string + batch_size: + description: Number of hosts per batch that will be retrieved from the Foreman API per individual call + type: int + default: 250 + use_reports_api: + description: Use Reporting API. + type: boolean + default: True + foreman: + description: + - Foreman server related configuration, deprecated. + - You can pass I(use_reports_api) in this dict to enable the Reporting API. + - Only for backward compatibility. + report: + description: + - Report API specific configuration, deprecated. + - You can pass the Report API specific params as part of this dict, instead of the main configuration. + - Only for backward compatibility. + type: dict + poll_interval: + description: The polling interval between 2 calls to the report_data endpoint while polling. + type: int + default: 10 + max_timeout: + description: Timeout before falling back to old host API when using report_data endpoint while polling. + type: int + default: 600 + want_organization: + description: Toggle, if true the inventory will fetch organization the host belongs to and create groupings for the same. + type: boolean + default: True + want_location: + description: Toggle, if true the inventory will fetch location the host belongs to and create groupings for the same. + type: boolean + default: True + want_ipv4: + description: Toggle, if true the inventory will fetch ipv4 address of the host. + type: boolean + default: True + want_ipv6: + description: Toggle, if true the inventory will fetch ipv6 address of the host. + type: boolean + default: True + want_host_group: + description: Toggle, if true the inventory will fetch host_groups and create groupings for the same. + type: boolean + default: True + want_subnet: + description: Toggle, if true the inventory will fetch subnet. + type: boolean + default: True + want_subnet_v6: + description: Toggle, if true the inventory will fetch ipv6 subnet. + type: boolean + default: True + want_smart_proxies: + description: Toggle, if true the inventory will fetch smart proxy that the host is registered to. + type: boolean + default: True + want_content_facet_attributes: + description: Toggle, if true the inventory will fetch content view details that the host is tied to. + type: boolean + default: True + hostnames: + description: + - A list of templates in order of precedence to compose inventory_hostname. + - If the template results in an empty string or None value it is ignored. + type: list + elements: str + default: ['name'] +''' + +EXAMPLES = ''' +# my.foreman.yml +plugin: theforeman.foreman.foreman +url: https://foreman.example.com +user: ansibleinventory +password: changeme +host_filters: 'organization="Web Engineering"' + +# shortname.foreman.yml +plugin: theforeman.foreman.foreman +url: https://foreman.example.com +user: ansibleinventory +password: changeme +hostnames: + - name.split('.')[0] +''' +import copy +import json +from ansible_collections.theforeman.foreman.plugins.module_utils._version import LooseVersion +from time import sleep +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.common._collections_compat import MutableMapping +from ansible.plugins.inventory import BaseInventoryPlugin, Cacheable, to_safe_group_name, Constructable + +# 3rd party imports +try: + import requests + if LooseVersion(requests.__version__) < LooseVersion('1.1.0'): + raise ImportError + from requests.auth import HTTPBasicAuth + HAS_REQUESTS = True +except ImportError: + HAS_REQUESTS = False + + +class InventoryModule(BaseInventoryPlugin, Cacheable, Constructable): + ''' Host inventory parser for ansible using foreman as source. ''' + + NAME = 'theforeman.foreman.foreman' + + def __init__(self): + + super(InventoryModule, self).__init__() + self.MINIMUM_FOREMAN_VERSION_FOR_REPORTING_API = '1.24.0' + # from config + self.foreman_url = None + + self.session = None + self.cache_key = None + self.use_cache = None + + if not HAS_REQUESTS: + raise AnsibleError('This script requires python-requests 1.1 as a minimum version') + + def verify_file(self, path): + + valid = False + if super(InventoryModule, self).verify_file(path): + if path.endswith(('foreman.yaml', 'foreman.yml')): + valid = True + else: + self.display.vvv('Skipping due to inventory source not ending in "foreman.yaml" nor "foreman.yml"') + return valid + + def _get_session(self): + if not self.session: + self.session = requests.session() + self.session.auth = HTTPBasicAuth(self.get_option('user'), to_bytes(self.get_option('password'))) + self.session.verify = self.get_option('validate_certs') + return self.session + + def _get_json(self, url, ignore_errors=None, params=None): + + if not self.use_cache or url not in self._cache.get(self.cache_key, {}): + + if self.cache_key not in self._cache: + self._cache[self.cache_key] = {url: ''} + + results = [] + s = self._get_session() + if params is None: + params = {} + params['page'] = 1 + params['per_page'] = self.get_option('batch_size') + while True: + # workaround to address the follwing issues where 'verify' is overridden in Requests: + # - https://github.com/psf/requests/issues/3829 + # - https://github.com/psf/requests/issues/5209 + ret = s.get(url, params=params, verify=self.get_option('validate_certs')) + + if ignore_errors and ret.status_code in ignore_errors: + break + ret.raise_for_status() + json = ret.json() + + # process results + # FIXME: This assumes 'return type' matches a specific query, + # it will break if we expand the queries and they dont have different types + if 'results' not in json: # pylint: disable=no-else-break + # /hosts/:id dos not have a 'results' key + results = json + break + elif isinstance(json['results'], MutableMapping): + # /facts are returned as dict in 'results' + if not isinstance(results, MutableMapping): + results = {} + + # check for end of paging + if len(json['results']) == 0: + break + + for host, facts in json['results'].items(): + if host not in results: + results[host] = {} + results[host].update(facts) + + # get next page + params['page'] += 1 + else: + # /hosts 's 'results' is a list of all hosts, returned is paginated + results = results + json['results'] + + # check for end of paging + if len(results) >= json['subtotal']: + break + if len(json['results']) == 0: + self.display.warning("Did not make any progress during loop. expected %d got %d" % (json['subtotal'], len(results))) + break + + # get next page + params['page'] += 1 + + self._cache[self.cache_key][url] = results + + return self._cache[self.cache_key][url] + + def _get_hosts(self): + url = "%s/api/v2/hosts" % self.foreman_url + params = {} + if self.get_option('host_filters'): + params['search'] = self.get_option('host_filters') + return self._get_json(url, params=params) + + def _get_all_params_by_id(self, hid): + url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid) + ret = self._get_json(url, [404]) + if not ret or not isinstance(ret, MutableMapping) or not ret.get('all_parameters', False): + return {} + return ret.get('all_parameters') + + def _get_facts_by_id(self, hid): + url = "%s/api/v2/hosts/%s/facts" % (self.foreman_url, hid) + return self._get_json(url) + + def _get_host_data_by_id(self, hid): + url = "%s/api/v2/hosts/%s" % (self.foreman_url, hid) + return self._get_json(url) + + def _get_facts(self, host): + """Fetch all host facts of the host""" + + ret = self._get_facts_by_id(host['id']) + if len(ret.values()) == 0: + facts = {} + elif len(ret.values()) == 1: + facts = list(ret.values())[0] + else: + raise ValueError("More than one set of facts returned for '%s'" % host) + return facts + + def _get_hostvars(self, host, vars_prefix='', omitted_vars=()): + hostvars = {} + for k, v in host.items(): + if k not in omitted_vars: + hostvars[vars_prefix + k] = v + return hostvars + + def _fetch_params(self): + options = ("no", "yes") + params = dict() + + report_options = self.get_option('report') or {} + + self.want_location = report_options.get('want_location', self.get_option('want_location')) + self.want_organization = report_options.get('want_organization', self.get_option('want_organization')) + self.want_IPv4 = report_options.get('want_ipv4', self.get_option('want_ipv4')) + self.want_IPv6 = report_options.get('want_ipv6', self.get_option('want_ipv6')) + self.want_host_group = report_options.get('want_host_group', self.get_option('want_host_group')) + self.want_hostcollections = report_options.get('want_hostcollections', self.get_option('want_hostcollections')) + self.want_subnet = report_options.get('want_subnet', self.get_option('want_subnet')) + self.want_subnet_v6 = report_options.get('want_subnet_v6', self.get_option('want_subnet_v6')) + self.want_smart_proxies = report_options.get('want_smart_proxies', self.get_option('want_smart_proxies')) + self.want_content_facet_attributes = report_options.get('want_content_facet_attributes', self.get_option('want_content_facet_attributes')) + self.want_params = self.get_option('want_params') + self.want_facts = self.get_option('want_facts') + self.host_filters = self.get_option('host_filters') + + params["Organization"] = options[self.want_organization] + params["Location"] = options[self.want_location] + params["IPv4"] = options[self.want_IPv4] + params["IPv6"] = options[self.want_IPv6] + params["Facts"] = options[self.want_facts] + params["Host Group"] = options[self.want_host_group] + params["Host Collections"] = options[self.want_hostcollections] + params["Subnet"] = options[self.want_subnet] + params["Subnet v6"] = options[self.want_subnet_v6] + params["Smart Proxies"] = options[self.want_smart_proxies] + params["Content Attributes"] = options[self.want_content_facet_attributes] + params["Host Parameters"] = options[self.want_params] + if self.host_filters: + params["Hosts"] = self.host_filters + return params + + def _use_inventory_report(self): + use_inventory_report = self.get_option('use_reports_api') + # backward compatibility + try: + use_inventory_report = self.get_option('foreman').get('use_reports_api') + except Exception: + pass + if not use_inventory_report: + return False + status_url = "%s/api/v2/status" % self.foreman_url + result = self._get_json(status_url) + foreman_version = (LooseVersion(result.get('version')) >= LooseVersion(self.MINIMUM_FOREMAN_VERSION_FOR_REPORTING_API)) + return foreman_version + + def _post_request(self): + url = "%s/ansible/api/v2/ansible_inventories/schedule" % self.foreman_url + params = {'input_values': self._fetch_params()} + + if self.use_cache and url in self._cache.get(self.cache_key, {}): + return self._cache[self.cache_key][url] + + if self.cache_key not in self._cache: + self._cache[self.cache_key] = {} + + session = self._get_session() + self.poll_interval = self.get_option('poll_interval') + self.max_timeout = self.get_option('max_timeout') + # backward compatibility + try: + self.poll_interval = int(self.get_option('report').get('poll_interval')) + self.max_timeout = int(self.get_option('report').get('max_timeout')) + except Exception: + pass + max_polls = self.max_timeout / self.poll_interval + ret = session.post(url, json=params) + if not ret: + raise Exception("Error scheduling inventory report on foreman. Please check foreman logs!") + data_url = "{0}/{1}".format(self.foreman_url, ret.json().get('data_url')) + polls = 0 + response = session.get(data_url) + while response: + if response.status_code != 204 or polls > max_polls: + break + sleep(self.poll_interval) + polls += 1 + response = session.get(data_url) + if not response: + raise Exception("Error receiving inventory report from foreman. Please check foreman logs!") + elif (response.status_code == 204 and polls > max_polls): + raise Exception("Timeout receiving inventory report from foreman. Check foreman server and max_timeout in foreman.yml") + else: + self._cache[self.cache_key][url] = json.loads(response.json()) + return self._cache[self.cache_key][url] + + def _populate(self): + if self._use_inventory_report(): + self._populate_report_api() + else: + self._populate_host_api() + + def _get_hostname(self, properties, hostnames, strict=False): + hostname = None + errors = [] + + for preference in hostnames: + try: + hostname = self._compose(preference, properties) + except Exception as e: # pylint: disable=broad-except + if strict: + raise AnsibleError("Could not compose %s as hostnames - %s" % (preference, to_native(e))) + else: + errors.append( + (preference, str(e)) + ) + if hostname: + return to_text(hostname) + + raise AnsibleError( + 'Could not template any hostname for host, errors for each preference: %s' % ( + ', '.join(['%s: %s' % (pref, err) for pref, err in errors]) + ) + ) + + def _populate_report_api(self): + self.groups = dict() + self.hosts = dict() + try: + # We need a deep copy of the data, as we modify it below and this would also modify the cache + host_data = copy.deepcopy(self._post_request()) + except Exception as exc: + self.display.warning("Failed to use Reports API, falling back to Hosts API: {0}".format(exc)) + self._populate_host_api() + return + self.group_prefix = self.get_option('group_prefix') + + hostnames = self.get_option('hostnames') + strict = self.get_option('strict') + + for host in host_data: + if not host: + continue + + composed_host_name = self._get_hostname(host, hostnames, strict=strict) + + if (composed_host_name in self._cache.keys()): + continue + + host_name = self.inventory.add_host(composed_host_name) + + group_name = host.get('hostgroup_title', host.get('hostgroup_name')) + if group_name: + group_name = to_safe_group_name('%s%s' % (self.get_option('group_prefix'), group_name.lower().replace(" ", ""))) + group_name = self.inventory.add_group(group_name) + self.inventory.add_child(group_name, host_name) + + host_params = host.pop('host_parameters', {}) + fact_list = host.pop('facts', {}) + + if self.get_option('legacy_hostvars'): + hostvars = self._get_hostvars(host) + self.inventory.set_variable(host_name, 'foreman', hostvars) + else: + omitted_vars = ('name', 'hostgroup_title', 'hostgroup_name') + hostvars = self._get_hostvars(host, self.get_option('vars_prefix'), omitted_vars) + + for k, v in hostvars.items(): + try: + self.inventory.set_variable(host_name, k, v) + except ValueError as e: + self.display.warning("Could not set host info hostvar for %s, skipping %s: %s" % (host, k, to_text(e))) + + content_facet_attributes = host.get('content_attributes', {}) or {} + if self.get_option('want_facts'): + self.inventory.set_variable(host_name, 'foreman_facts', fact_list) + + # Create ansible groups for hostgroup + group = 'host_group' + group_name = host.get(group) + if group_name: + parent_name = None + group_label_parts = [] + for part in group_name.split('/'): + group_label_parts.append(part.lower().replace(" ", "")) + gname = to_safe_group_name('%s%s' % (self.get_option('group_prefix'), '/'.join(group_label_parts))) + result_gname = self.inventory.add_group(gname) + if parent_name: + self.inventory.add_child(parent_name, result_gname) + parent_name = result_gname + self.inventory.add_child(result_gname, host_name) + + # Create ansible groups for environment, location and organization + for group in ['environment', 'location', 'organization']: + val = host.get('%s' % group) + if val: + safe_key = to_safe_group_name('%s%s_%s' % ( + to_text(self.group_prefix), + group, + to_text(val).lower() + )) + env_lo_org = self.inventory.add_group(safe_key) + self.inventory.add_child(env_lo_org, host_name) + + for group in ['lifecycle_environment', 'content_view']: + val = content_facet_attributes.get('%s_name' % group) + if val: + safe_key = to_safe_group_name('%s%s_%s' % ( + to_text(self.group_prefix), + group, + to_text(val).lower() + )) + le_cv_group = self.inventory.add_group(safe_key) + self.inventory.add_child(le_cv_group, host_name) + params = host_params + + if self.want_hostcollections: + hostcollections = host.get('host_collections') + + if hostcollections: + # Create Ansible groups for host collections + for hostcollection in hostcollections: + try: + host_collection_group_name = to_safe_group_name('%shostcollection_%s' % ( + to_text(self.group_prefix), + to_text(hostcollection).lower() + )) + hostcollection_group = self.inventory.add_group(host_collection_group_name) + self.inventory.add_child(hostcollection_group, host_name) + except ValueError as e: + self.display.warning("Could not create groups for host collections for %s, skipping: %s" % (host_name, to_text(e))) + + # set host vars from params + if self.get_option('want_params'): + if self.get_option('legacy_hostvars'): + self.inventory.set_variable(host_name, 'foreman_params', params) + else: + for k, v in params.items(): + try: + self.inventory.set_variable(host_name, k, v) + except ValueError as e: + self.display.warning("Could not set hostvar %s to '%s' for the '%s' host, skipping: %s" % + (k, to_native(v), host, to_native(e))) + hostvars = self.inventory.get_host(host_name).get_vars() + self._set_composite_vars(self.get_option('compose'), hostvars, host_name, strict) + self._add_host_to_composed_groups(self.get_option('groups'), hostvars, host_name, strict) + self._add_host_to_keyed_groups(self.get_option('keyed_groups'), hostvars, host_name, strict) + + def _populate_host_api(self): + hostnames = self.get_option('hostnames') + strict = self.get_option('strict') + for host in self._get_hosts(): + if not host: + continue + + composed_host_name = self._get_hostname(host, hostnames, strict=strict) + + if (composed_host_name in self._cache.keys()): + continue + + host_name = self.inventory.add_host(composed_host_name) + + # create directly mapped groups + group_name = host.get('hostgroup_title', host.get('hostgroup_name')) + if group_name: + parent_name = None + group_label_parts = [] + for part in group_name.split('/'): + group_label_parts.append(part.lower().replace(" ", "")) + gname = to_safe_group_name('%s%s' % (self.get_option('group_prefix'), '/'.join(group_label_parts))) + result_gname = self.inventory.add_group(gname) + if parent_name: + self.inventory.add_child(parent_name, result_gname) + parent_name = result_gname + self.inventory.add_child(result_gname, host_name) + + if self.get_option('legacy_hostvars'): + hostvars = self._get_hostvars(host) + self.inventory.set_variable(host_name, 'foreman', hostvars) + else: + omitted_vars = ('name', 'hostgroup_title', 'hostgroup_name') + hostvars = self._get_hostvars(host, self.get_option('vars_prefix'), omitted_vars) + + for k, v in hostvars.items(): + try: + self.inventory.set_variable(host_name, k, v) + except ValueError as e: + self.display.warning("Could not set host info hostvar for %s, skipping %s: %s" % (host, k, to_text(e))) + + # set host vars from params + if self.get_option('want_params'): + params = self._get_all_params_by_id(host['id']) + filtered_params = {} + for p in params: + if 'name' in p and 'value' in p: + filtered_params[p['name']] = p['value'] + + if self.get_option('legacy_hostvars'): + self.inventory.set_variable(host_name, 'foreman_params', filtered_params) + else: + for k, v in filtered_params.items(): + try: + self.inventory.set_variable(host_name, k, v) + except ValueError as e: + self.display.warning("Could not set hostvar %s to '%s' for the '%s' host, skipping: %s" % + (k, to_native(v), host, to_native(e))) + + # set host vars from facts + if self.get_option('want_facts'): + self.inventory.set_variable(host_name, 'foreman_facts', self._get_facts(host)) + + # create group for host collections + if self.get_option('want_hostcollections'): + host_data = self._get_host_data_by_id(host['id']) + hostcollections = host_data.get('host_collections') + if hostcollections: + # Create Ansible groups for host collections + for hostcollection in hostcollections: + try: + hostcollection_group = to_safe_group_name('%shostcollection_%s' % (self.get_option('group_prefix'), + hostcollection['name'].lower().replace(" ", ""))) + hostcollection_group = self.inventory.add_group(hostcollection_group) + self.inventory.add_child(hostcollection_group, host_name) + except ValueError as e: + self.display.warning("Could not create groups for host collections for %s, skipping: %s" % (host_name, to_text(e))) + + hostvars = self.inventory.get_host(host_name).get_vars() + self._set_composite_vars(self.get_option('compose'), hostvars, host_name, strict) + self._add_host_to_composed_groups(self.get_option('groups'), hostvars, host_name, strict) + self._add_host_to_keyed_groups(self.get_option('keyed_groups'), hostvars, host_name, strict) + + def parse(self, inventory, loader, path, cache=True): + + super(InventoryModule, self).parse(inventory, loader, path) + + # read config from file, this sets 'options' + self._read_config_data(path) + + # get connection host + self.foreman_url = self.get_option('url') + self.cache_key = self.get_cache_key(path) + self.use_cache = cache and self.get_option('cache') + + # actually populate inventory + self._populate() diff --git a/ansible_collections/theforeman/foreman/plugins/module_utils/_apypie.py b/ansible_collections/theforeman/foreman/plugins/module_utils/_apypie.py new file mode 100644 index 00000000..8052d1a8 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/module_utils/_apypie.py @@ -0,0 +1,907 @@ +# pylint: disable=ansible-format-automatic-specification,raise-missing-from +from __future__ import absolute_import, division, print_function +__metaclass__ = type +try: + from typing import Any, Iterable, List, Optional, Tuple # pylint: disable=unused-import +except ImportError: + pass + + +""" +Apypie Action module +""" + +try: + base_string = basestring +except NameError: # Python 3 has no base_string + base_string = str # pylint: disable=invalid-name,redefined-builtin + + +class Action(object): + """ + Apipie Action + """ + + def __init__(self, name, resource, api): + # type: (str, str, Api) -> None + self.name = name + self.resource = resource + self.api = api + + @property + def apidoc(self): + # type: () -> dict + """ + The apidoc of this action. + + :returns: The apidoc. + """ + + resource_methods = self.api.apidoc['docs']['resources'][self.resource]['methods'] + return [method for method in resource_methods if method['name'] == self.name][0] + + @property + def routes(self): + # type: () -> List[Route] + """ + The routes this action can be invoked by. + + :returns: The routes + """ + + return [Route(route['api_url'], route['http_method'], route['short_description']) for route in self.apidoc['apis']] + + @property + def params(self): + # type: () -> List[Param] + """ + The params accepted by this action. + + :returns: The params. + """ + + return [Param(**param) for param in self.apidoc['params']] + + @property + def examples(self): + # type: () -> List[Example] + """ + The examples of this action. + + :returns: The examples. + """ + + return [Example.parse(example) for example in self.apidoc['examples']] + + def call(self, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments + # type: (dict, Optional[dict], Optional[dict], Optional[Any], Optional[dict]) -> dict + """ + Call the API to execute the action. + + :param params: The params that should be passed to the API. + :param headers: Additional headers to be passed to the API. + :param options: Options + :param data: Binary data to be submitted to the API. + :param files: Files to be submitted to the API. + + :returns: The API response. + """ + + return self.api.call(self.resource, self.name, params, headers, options, data, files) + + def find_route(self, params=None): + # type: (Optional[dict]) -> Route + """ + Find the best matching route for a given set of params. + + :param params: Params that should be submitted to the API. + + :returns: The best route. + """ + + param_keys = set(self.filter_empty_params(params).keys()) + sorted_routes = sorted(self.routes, key=lambda route: [-1 * len(route.params_in_path), route.path]) + for route in sorted_routes: + if set(route.params_in_path) <= param_keys: + return route + return sorted_routes[-1] + + def validate(self, values, data=None, files=None): + # type: (dict, Optional[Any], Optional[dict]) -> None + """ + Validate a given set of parameter values against the required set of parameters. + + :param values: The values to validate. + :param data: Additional binary data to validate. + :param files: Additional files to validate. + """ + + self._validate(self.params, values, data, files) + + @staticmethod + def _add_to_path(path=None, additions=None): + # type: (Optional[str], Optional[List[str]]) -> str + if path is None: + path = '' + if additions is None: + additions = [] + for addition in additions: + if path == '': + path = "{}".format(addition) + else: + path = "{}[{}]".format(path, addition) + return path + + def _validate(self, params, values, data=None, files=None, path=None): # pylint: disable=too-many-arguments,too-many-locals + # type: (Iterable[Param], dict, Optional[Any], Optional[dict], Optional[str]) -> None + if not isinstance(values, dict): + raise InvalidArgumentTypesError + given_params = set(values.keys()) + given_files = set((files or {}).keys()) + given_data = set((data or {}).keys()) + required_params = {param.name for param in params if param.required} + missing_params = required_params - given_params - given_files - given_data + if missing_params: + missing_params_with_path = [self._add_to_path(path, [param]) for param in missing_params] + message = "The following required parameters are missing: {}".format(', '.join(missing_params_with_path)) + raise MissingArgumentsError(message) + + for param, value in values.items(): + param_descriptions = [p for p in params if p.name == param] + if param_descriptions: + param_description = param_descriptions[0] + if param_description.params and value is not None: + if param_description.expected_type == 'array': + for num, item in enumerate(value): + self._validate(param_description.params, item, path=self._add_to_path(path, [param_description.name, str(num)])) + elif param_description.expected_type == 'hash': + self._validate(param_description.params, value, path=self._add_to_path(path, [param_description.name])) + if (param_description.expected_type == 'numeric' and isinstance(value, base_string)): + try: + value = int(value) + except ValueError: + # this will be caught in the next check + pass + if (not param_description.allow_nil and value is None): + raise ValueError("{} can't be {}".format(param, value)) + # pylint: disable=too-many-boolean-expressions + if (value is not None + and ((param_description.expected_type == 'boolean' and not isinstance(value, bool) and not (isinstance(value, int) and value in [0, 1])) + or (param_description.expected_type == 'numeric' and not isinstance(value, int)) + or (param_description.expected_type == 'string' and not isinstance(value, (base_string, int))))): + raise ValueError("{} ({}): {}".format(param, value, param_description.validator)) + + @staticmethod + def filter_empty_params(params=None): + # type: (Optional[dict]) -> dict + """ + Filter out any params that have no value. + + :param params: The params to filter. + + :returns: The filtered params. + """ + result = {} + if params is not None: + if isinstance(params, dict): + result = {k: v for k, v in params.items() if v is not None} + else: + raise InvalidArgumentTypesError + return result + + def prepare_params(self, input_dict): + # type: (dict) -> dict + """ + Transform a dict with data into one that can be accepted as params for calling the action. + + This will ignore any keys that are not accepted as params when calling the action. + It also allows generating nested params without forcing the user to care about them. + + :param input_dict: a dict with data that should be used to fill in the params + :return: :class:`dict` object + :rtype: dict + + Usage:: + + >>> action.prepare_params({'id': 1}) + {'user': {'id': 1}} + """ + params = self._prepare_params(self.params, input_dict) + route_params = self._prepare_route_params(input_dict) + params.update(route_params) + return params + + def _prepare_params(self, action_params, input_dict): + # type: (Iterable[Param], dict) -> dict + result = {} + + for param in action_params: + if param.expected_type == 'hash' and param.params: + nested_dict = input_dict.get(param.name, input_dict) + nested_result = self._prepare_params(param.params, nested_dict) + if nested_result: + result[param.name] = nested_result + elif param.name in input_dict: + result[param.name] = input_dict[param.name] + + return result + + def _prepare_route_params(self, input_dict): + # type: (dict) -> dict + result = {} + + route = self.find_route(input_dict) + + for url_param in route.params_in_path: + if url_param in input_dict: + result[url_param] = input_dict[url_param] + + return result + + +""" +Apypie Api module +""" + + +import errno +import glob +import json +try: + import requests +except ImportError: + pass +try: + from json.decoder import JSONDecodeError # type: ignore +except ImportError: + JSONDecodeError = ValueError # type: ignore +import os +try: + from urlparse import urljoin # type: ignore +except ImportError: + from urllib.parse import urljoin # type: ignore + + +def _qs_param(param): + # type: (Any) -> Any + if isinstance(param, bool): + return str(param).lower() + return param + + +class Api(object): + """ + Apipie API bindings + + :param uri: base URL of the server + :param username: username to access the API + :param password: username to access the API + :param api_version: version of the API. Defaults to `1` + :param language: prefered locale for the API description + :param apidoc_cache_base_dir: base directory for building apidoc_cache_dir. Defaults to `~/.cache/apipie_bindings`. + :param apidoc_cache_dir: where to cache the JSON description of the API. Defaults to `apidoc_cache_base_dir/<URI>`. + :param apidoc_cache_name: name of the cache file. If there is cache in the `apidoc_cache_dir`, it is used. Defaults to `default`. + :param verify_ssl: should the SSL certificate be verified. Defaults to `True`. + + Usage:: + + >>> import apypie + >>> api = apypie.Api(uri='https://api.example.com', username='admin', password='changeme') + """ + + def __init__(self, **kwargs): + self.uri = kwargs.get('uri') + self.api_version = kwargs.get('api_version', 1) + self.language = kwargs.get('language') + + # Find where to put the cache by default according to the XDG spec + # Not using just get('XDG_CACHE_HOME', '~/.cache') because the spec says + # that the defaut should be used if "$XDG_CACHE_HOME is either not set or empty" + xdg_cache_home = os.environ.get('XDG_CACHE_HOME', None) + if not xdg_cache_home: + xdg_cache_home = '~/.cache' + + apidoc_cache_base_dir = kwargs.get('apidoc_cache_base_dir', os.path.join(os.path.expanduser(xdg_cache_home), 'apypie')) + apidoc_cache_dir_default = os.path.join(apidoc_cache_base_dir, self.uri.replace(':', '_').replace('/', '_'), 'v{}'.format(self.api_version)) + self.apidoc_cache_dir = kwargs.get('apidoc_cache_dir', apidoc_cache_dir_default) + self.apidoc_cache_name = kwargs.get('apidoc_cache_name', self._find_cache_name()) + + self._session = requests.Session() + self._session.verify = kwargs.get('verify_ssl', True) + + self._session.headers['Accept'] = 'application/json;version={}'.format(self.api_version) + self._session.headers['User-Agent'] = 'apypie (https://github.com/Apipie/apypie)' + if self.language: + self._session.headers['Accept-Language'] = self.language + + if kwargs.get('username') and kwargs.get('password'): + self._session.auth = (kwargs['username'], kwargs['password']) + + self._apidoc = None + + @property + def apidoc(self): + # type: () -> dict + """ + The full apidoc. + + The apidoc will be fetched from the server, if that didn't happen yet. + + :returns: The apidoc. + """ + + if self._apidoc is None: + self._apidoc = self._load_apidoc() + return self._apidoc + + @property + def apidoc_cache_file(self): + # type: () -> str + """ + Full local path to the cached apidoc. + """ + + return os.path.join(self.apidoc_cache_dir, '{0}{1}'.format(self.apidoc_cache_name, self.cache_extension)) + + def _cache_dir_contents(self): + # type: () -> Iterable[str] + return glob.iglob(os.path.join(self.apidoc_cache_dir, '*{}'.format(self.cache_extension))) + + def _find_cache_name(self, default='default'): + cache_file = next(self._cache_dir_contents(), None) + cache_name = default + if cache_file: + cache_name = os.path.basename(cache_file)[:-len(self.cache_extension)] + return cache_name + + def validate_cache(self, cache_name): + # type: (str) -> None + """ + Ensure the cached apidoc matches the one presented by the server. + + :param cache_name: The name of the apidoc on the server. + """ + + if cache_name is not None and cache_name != self.apidoc_cache_name: + self.clean_cache() + self.apidoc_cache_name = os.path.basename(os.path.normpath(cache_name)) + + def clean_cache(self): + # type: () -> None + """ + Remove any locally cached apidocs. + """ + + self._apidoc = None + for filename in self._cache_dir_contents(): + os.unlink(filename) + + @property + def resources(self): + # type: () -> Iterable + """ + List of available resources. + + Usage:: + + >>> api.resources + ['comments', 'users'] + """ + return sorted(self.apidoc['docs']['resources'].keys()) + + def resource(self, name): + # type: (str) -> Resource + """ + Get a resource. + + :param name: the name of the resource to load + :return: :class:`Resource <Resource>` object + :rtype: apypie.Resource + + Usage:: + + >>> api.resource('users') + """ + if name in self.resources: + return Resource(self, name) + message = "Resource '{}' does not exist in the API. Existing resources: {}".format(name, ', '.join(sorted(self.resources))) + raise KeyError(message) + + def _load_apidoc(self): + # type: () -> dict + try: + with open(self.apidoc_cache_file, 'r') as apidoc_file: + api_doc = json.load(apidoc_file) + except (IOError, JSONDecodeError): + api_doc = self._retrieve_apidoc() + return api_doc + + def _retrieve_apidoc(self): + # type: () -> dict + try: + os.makedirs(self.apidoc_cache_dir) + except OSError as err: + if err.errno != errno.EEXIST or not os.path.isdir(self.apidoc_cache_dir): + raise + response = None + if self.language: + response = self._retrieve_apidoc_call('/apidoc/v{0}.{1}.json'.format(self.api_version, self.language), safe=True) + language_family = self.language.split('_')[0] + if not response and language_family != self.language: + response = self._retrieve_apidoc_call('/apidoc/v{0}.{1}.json'.format(self.api_version, language_family), safe=True) + if not response: + try: + response = self._retrieve_apidoc_call('/apidoc/v{}.json'.format(self.api_version)) + except Exception as exc: + raise DocLoadingError("""Could not load data from {0}: {1} + - is your server down? + - was rake apipie:cache run when using apipie cache? (typical production settings)""".format(self.uri, exc)) + with open(self.apidoc_cache_file, 'w') as apidoc_file: + apidoc_file.write(json.dumps(response)) + return response + + def _retrieve_apidoc_call(self, path, safe=False): + try: + return self.http_call('get', path) + except requests.exceptions.HTTPError: + if not safe: + raise + + def call(self, resource_name, action_name, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments + """ + Call an action in the API. + + It finds most fitting route based on given parameters + with other attributes necessary to do an API call. + + :param resource_name: name of the resource + :param action_name: action_name name of the action + :param params: Dict of parameters to be sent in the request + :param headers: Dict of headers to be sent in the request + :param options: Dict of options to influence the how the call is processed + * `skip_validation` (Bool) *false* - skip validation of parameters + :param data: Binary data to be sent in the request + :param files: Binary files to be sent in the request + :return: :class:`dict` object + :rtype: dict + + Usage:: + + >>> api.call('users', 'show', {'id': 1}) + """ + if options is None: + options = {} + if params is None: + params = {} + + resource = Resource(self, resource_name) + action = resource.action(action_name) + if not options.get('skip_validation', False): + action.validate(params, data, files) + + return self._call_action(action, params, headers, data, files) + + def _call_action(self, action, params=None, headers=None, data=None, files=None): # pylint: disable=too-many-arguments + if params is None: + params = {} + + route = action.find_route(params) + get_params = {key: value for key, value in params.items() if key not in route.params_in_path} + return self.http_call( + route.method, + route.path_with_params(params), + get_params, + headers, data, files) + + def http_call(self, http_method, path, params=None, headers=None, data=None, files=None): # pylint: disable=too-many-arguments + """ + Execute an HTTP request. + + :param params: Dict of parameters to be sent in the request + :param headers: Dict of headers to be sent in the request + :param data: Binary data to be sent in the request + :param files: Binary files to be sent in the request + + :return: :class:`dict` object + :rtype: dict + """ + + full_path = urljoin(self.uri, path) + kwargs = { + 'verify': self._session.verify, + } + + if headers: + kwargs['headers'] = headers + + if params: + if http_method in ['get', 'head']: + kwargs['params'] = {k: _qs_param(v) for k, v in params.items()} + else: + kwargs['json'] = params + elif http_method in ['post', 'put', 'patch'] and not data and not files: + kwargs['json'] = {} + + if files: + kwargs['files'] = files + + if data: + kwargs['data'] = data + + request = self._session.request(http_method, full_path, **kwargs) + request.raise_for_status() + self.validate_cache(request.headers.get('apipie-checksum')) + if request.status_code == requests.codes['no_content']: + return None + return request.json() + + @property + def cache_extension(self): + """ + File extension for the local cache file. + + Will include the language if set. + """ + + if self.language: + ext = '.{}.json'.format(self.language) + else: + ext = '.json' + return ext + + +""" +Apypie Example module +""" + + +import re + +EXAMPLE_PARSER = re.compile(r'(\w+)\s+([^\n]*)\n?(.*)\n(\d+)\n(.*)', re.DOTALL) + + +class Example(object): # pylint: disable=too-few-public-methods + """ + Apipie Example + """ + + def __init__(self, http_method, path, args, status, response): # pylint: disable=too-many-arguments + # type: (str, str, str, str, str) -> None + self.http_method = http_method + self.path = path + self.args = args + self.status = int(status) + self.response = response + + @classmethod + def parse(cls, example): + """ + Parse an example from an apidoc string + + :returns: The parsed :class:`Example` + """ + parsed = EXAMPLE_PARSER.match(example) + return cls(*parsed.groups()) + + +""" +Apypie Exceptions +""" + + +class DocLoadingError(Exception): + """ + Exception to be raised when apidoc could not be loaded. + """ + + +class MissingArgumentsError(Exception): + """ + Exception to be raised when required arguments are missing. + """ + + +class InvalidArgumentTypesError(Exception): + """ + Exception to be raised when arguments are of the wrong type. + """ + + +""" +Apypie Inflector module + +Based on ActiveSupport Inflector (https://github.com/rails/rails.git) +Inflection rules taken from davidcelis's Inflections (https://github.com/davidcelis/inflections.git) +""" + + +import re + + +class Inflections(object): + """ + Inflections - rules how to convert words from singular to plural and vice versa. + """ + + def __init__(self): + self.plurals = [] + self.singulars = [] + self.uncountables = [] + self.humans = [] + self.acronyms = {} + self.acronym_regex = r'/(?=a)b/' + + def acronym(self, word): + # type: (str) -> None + """ + Add a new acronym. + """ + + self.acronyms[word.lower()] = word + self.acronym_regex = '|'.join(self.acronyms.values()) + + def plural(self, rule, replacement): + # type: (str, str) -> None + """ + Add a new plural rule. + """ + + if rule in self.uncountables: + self.uncountables.remove(rule) + if replacement in self.uncountables: + self.uncountables.remove(replacement) + + self.plurals.insert(0, (rule, replacement)) + + def singular(self, rule, replacement): + # type: (str, str) -> None + """ + Add a new singular rule. + """ + + if rule in self.uncountables: + self.uncountables.remove(rule) + if replacement in self.uncountables: + self.uncountables.remove(replacement) + + self.singulars.insert(0, (rule, replacement)) + + def irregular(self, singular, plural): + # type: (str, str) -> None + """ + Add a new irregular rule + """ + + if singular in self.uncountables: + self.uncountables.remove(singular) + if plural in self.uncountables: + self.uncountables.remove(plural) + + sfirst = singular[0] + srest = singular[1:] + + pfirst = plural[0] + prest = plural[1:] + + if sfirst.upper() == pfirst.upper(): + self.plural(r'(?i)({}){}$'.format(sfirst, srest), r'\1' + prest) + self.plural(r'(?i)({}){}$'.format(pfirst, prest), r'\1' + prest) + + self.singular(r'(?i)({}){}$'.format(sfirst, srest), r'\1' + srest) + self.singular(r'(?i)({}){}$'.format(pfirst, prest), r'\1' + srest) + else: + self.plural(r'{}(?i){}$'.format(sfirst.upper(), srest), pfirst.upper() + prest) + self.plural(r'{}(?i){}$'.format(sfirst.lower(), srest), pfirst.lower() + prest) + self.plural(r'{}(?i){}$'.format(pfirst.upper(), prest), pfirst.upper() + prest) + self.plural(r'{}(?i){}$'.format(pfirst.lower(), prest), pfirst.lower() + prest) + + self.singular(r'{}(?i){}$'.format(sfirst.upper(), srest), sfirst.upper() + srest) + self.singular(r'{}(?i){}$'.format(sfirst.lower(), srest), sfirst.lower() + srest) + self.singular(r'{}(?i){}$'.format(pfirst.upper(), prest), sfirst.upper() + srest) + self.singular(r'{}(?i){}$'.format(pfirst.lower(), prest), sfirst.lower() + srest) + + def uncountable(self, *words): + """ + Add new uncountables. + """ + + self.uncountables.extend(words) + + def human(self, rule, replacement): + # type: (str, str) -> None + """ + Add a new humanize rule. + """ + + self.humans.insert(0, (rule, replacement)) + + +class Inflector(object): + """ + Inflector - perform inflections + """ + + def __init__(self): + # type: () -> None + self.inflections = Inflections() + self.inflections.plural(r'$', 's') + self.inflections.plural(r'(?i)([sxz]|[cs]h)$', r'\1es') + self.inflections.plural(r'(?i)([^aeiouy]o)$', r'\1es') + self.inflections.plural(r'(?i)([^aeiouy])y$', r'\1ies') + + self.inflections.singular(r'(?i)s$', r'') + self.inflections.singular(r'(?i)(ss)$', r'\1') + self.inflections.singular(r'([sxz]|[cs]h)es$', r'\1') + self.inflections.singular(r'([^aeiouy]o)es$', r'\1') + self.inflections.singular(r'(?i)([^aeiouy])ies$', r'\1y') + + self.inflections.irregular('child', 'children') + self.inflections.irregular('man', 'men') + self.inflections.irregular('medium', 'media') + self.inflections.irregular('move', 'moves') + self.inflections.irregular('person', 'people') + self.inflections.irregular('self', 'selves') + self.inflections.irregular('sex', 'sexes') + + self.inflections.uncountable('equipment', 'information', 'money', 'species', 'series', 'fish', 'sheep', 'police') + + def pluralize(self, word): + # type: (str) -> str + """ + Pluralize a word. + """ + + return self._apply_inflections(word, self.inflections.plurals) + + def singularize(self, word): + # type: (str) -> str + """ + Singularize a word. + """ + + return self._apply_inflections(word, self.inflections.singulars) + + def _apply_inflections(self, word, rules): + # type: (str, Iterable[Tuple[str, str]]) -> str + result = word + + if word != '' and result.lower() not in self.inflections.uncountables: + for (rule, replacement) in rules: + result = re.sub(rule, replacement, result) + if result != word: + break + + return result + + +""" +Apypie Param module +""" + + +import re + +HTML_STRIP = re.compile(r'<\/?[^>]+?>') + + +class Param(object): # pylint: disable=too-many-instance-attributes,too-few-public-methods + """ + Apipie Param + """ + + def __init__(self, **kwargs): + self.allow_nil = kwargs.get('allow_nil') + self.description = HTML_STRIP.sub('', kwargs.get('description')) + self.expected_type = kwargs.get('expected_type') + self.full_name = kwargs.get('full_name') + self.name = kwargs.get('name') + self.params = [Param(**param) for param in kwargs.get('params', [])] + self.required = bool(kwargs.get('required')) + self.validator = kwargs.get('validator') + + +""" +Apypie Resource module +""" + + +class Resource(object): + """ + Apipie Resource + """ + + def __init__(self, api, name): + # type: (Api, str) -> None + self.api = api + self.name = name + + @property + def actions(self): + # type: () -> List + """ + Actions available for this resource. + + :returns: The actions. + """ + return sorted([method['name'] for method in self.api.apidoc['docs']['resources'][self.name]['methods']]) + + def action(self, name): + # type: (str) -> Action + """ + Get an :class:`Action` for this resource. + + :param name: The name of the action. + """ + if self.has_action(name): + return Action(name, self.name, self.api) + message = "Unknown action '{}'. Supported actions: {}".format(name, ', '.join(sorted(self.actions))) + raise KeyError(message) + + def has_action(self, name): + # type: (str) -> bool + """ + Check whether the resource has a given action. + + :param name: The name of the action. + """ + return name in self.actions + + def call(self, action, params=None, headers=None, options=None, data=None, files=None): # pylint: disable=too-many-arguments + # type: (str, Optional[dict], Optional[dict], Optional[dict], Optional[Any], Optional[dict]) -> dict + """ + Call the API to execute an action for this resource. + + :param action: The action to call. + :param params: The params that should be passed to the API. + :param headers: Additional headers to be passed to the API. + :param options: Options + :param data: Binary data to be submitted to the API. + :param files: Files to be submitted to the API. + + :returns: The API response. + """ + + return self.api.call(self.name, action, params, headers, options, data, files) + + +""" +Apypie Route module +""" + + +class Route(object): + """ + Apipie Route + """ + + def __init__(self, path, method, description=""): + # type: (str, str, str) -> None + self.path = path + self.method = method.lower() + self.description = description + + @property + def params_in_path(self): + # type: () -> List + """ + Params that can be passed in the path (URL) of the route. + + :returns: The params. + """ + return [part[1:] for part in self.path.split('/') if part.startswith(':')] + + def path_with_params(self, params=None): + # type: (Optional[dict]) -> str + """ + Fill in the params into the path. + + :returns: The path with params. + """ + result = self.path + if params is not None: + for param in self.params_in_path: + if param in params: + result = result.replace(':{}'.format(param), str(params[param])) + else: + raise KeyError("missing param '{}' in parameters".format(param)) + return result diff --git a/ansible_collections/theforeman/foreman/plugins/module_utils/_version.py b/ansible_collections/theforeman/foreman/plugins/module_utils/_version.py new file mode 100644 index 00000000..0a34929e --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/module_utils/_version.py @@ -0,0 +1,335 @@ +# Vendored copy of distutils/version.py from CPython 3.9.5 +# +# Implements multiple version numbering conventions for the +# Python Module Distribution Utilities. +# +# PSF License (see PSF-license.txt or https://opensource.org/licenses/Python-2.0) +# + +"""Provides classes to represent module version numbers (one class for +each style of version numbering). There are currently two such classes +implemented: StrictVersion and LooseVersion. +Every version number class implements the following interface: + * the 'parse' method takes a string and parses it to some internal + representation; if the string is an invalid version number, + 'parse' raises a ValueError exception + * the class constructor takes an optional string argument which, + if supplied, is passed to 'parse' + * __str__ reconstructs the string that was passed to 'parse' (or + an equivalent string -- ie. one that will generate an equivalent + version number instance) + * __repr__ generates Python code to recreate the version number instance + * _cmp compares the current instance with either another instance + of the same class or a string (which will be parsed to an instance + of the same class, thus must follow the same rules) +""" + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import re + +try: + RE_FLAGS = re.VERBOSE | re.ASCII +except AttributeError: + RE_FLAGS = re.VERBOSE + + +class Version: + """Abstract base class for version numbering classes. Just provides + constructor (__init__) and reproducer (__repr__), because those + seem to be the same for all version numbering classes; and route + rich comparisons to _cmp. + """ + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def __repr__(self): + return "%s ('%s')" % (self.__class__.__name__, str(self)) + + def __eq__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c == 0 + + def __lt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c < 0 + + def __le__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c <= 0 + + def __gt__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c > 0 + + def __ge__(self, other): + c = self._cmp(other) + if c is NotImplemented: + return c + return c >= 0 + + +# Interface for version-number classes -- must be implemented +# by the following classes (the concrete ones -- Version should +# be treated as an abstract class). +# __init__ (string) - create and take same action as 'parse' +# (string parameter is optional) +# parse (string) - convert a string representation to whatever +# internal representation is appropriate for +# this style of version numbering +# __str__ (self) - convert back to a string; should be very similar +# (if not identical to) the string supplied to parse +# __repr__ (self) - generate Python code to recreate +# the instance +# _cmp (self, other) - compare two version numbers ('other' may +# be an unparsed version string, or another +# instance of your version class) + + +class StrictVersion(Version): + """Version numbering for anal retentives and software idealists. + Implements the standard interface for version number classes as + described above. A version number consists of two or three + dot-separated numeric components, with an optional "pre-release" tag + on the end. The pre-release tag consists of the letter 'a' or 'b' + followed by a number. If the numeric components of two version + numbers are equal, then one with a pre-release tag will always + be deemed earlier (lesser) than one without. + The following are valid version numbers (shown in the order that + would be obtained by sorting according to the supplied cmp function): + 0.4 0.4.0 (these two are equivalent) + 0.4.1 + 0.5a1 + 0.5b3 + 0.5 + 0.9.6 + 1.0 + 1.0.4a3 + 1.0.4b1 + 1.0.4 + The following are examples of invalid version numbers: + 1 + 2.7.2.2 + 1.3.a4 + 1.3pl1 + 1.3c4 + The rationale for this version numbering system will be explained + in the distutils documentation. + """ + + version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$', + RE_FLAGS) + + def parse(self, vstring): + match = self.version_re.match(vstring) + if not match: + raise ValueError("invalid version number '%s'" % vstring) + + (major, minor, patch, prerelease, prerelease_num) = \ + match.group(1, 2, 4, 5, 6) + + if patch: + self.version = tuple(map(int, [major, minor, patch])) + else: + self.version = tuple(map(int, [major, minor])) + (0,) + + if prerelease: + self.prerelease = (prerelease[0], int(prerelease_num)) + else: + self.prerelease = None + + def __str__(self): + if self.version[2] == 0: + vstring = '.'.join(map(str, self.version[0:2])) + else: + vstring = '.'.join(map(str, self.version)) + + if self.prerelease: + vstring = vstring + self.prerelease[0] + str(self.prerelease[1]) + + return vstring + + def _cmp(self, other): + if isinstance(other, str): + other = StrictVersion(other) + elif not isinstance(other, StrictVersion): + return NotImplemented + + if self.version != other.version: + # numeric versions don't match + # prerelease stuff doesn't matter + if self.version < other.version: + return -1 + else: + return 1 + + # have to compare prerelease + # case 1: neither has prerelease; they're equal + # case 2: self has prerelease, other doesn't; other is greater + # case 3: self doesn't have prerelease, other does: self is greater + # case 4: both have prerelease: must compare them! + + if (not self.prerelease and not other.prerelease): + return 0 + elif (self.prerelease and not other.prerelease): + return -1 + elif (not self.prerelease and other.prerelease): + return 1 + elif (self.prerelease and other.prerelease): + if self.prerelease == other.prerelease: + return 0 + elif self.prerelease < other.prerelease: + return -1 + else: + return 1 + else: + raise AssertionError("never get here") + +# end class StrictVersion + +# The rules according to Greg Stein: +# 1) a version number has 1 or more numbers separated by a period or by +# sequences of letters. If only periods, then these are compared +# left-to-right to determine an ordering. +# 2) sequences of letters are part of the tuple for comparison and are +# compared lexicographically +# 3) recognize the numeric components may have leading zeroes +# +# The LooseVersion class below implements these rules: a version number +# string is split up into a tuple of integer and string components, and +# comparison is a simple tuple comparison. This means that version +# numbers behave in a predictable and obvious way, but a way that might +# not necessarily be how people *want* version numbers to behave. There +# wouldn't be a problem if people could stick to purely numeric version +# numbers: just split on period and compare the numbers as tuples. +# However, people insist on putting letters into their version numbers; +# the most common purpose seems to be: +# - indicating a "pre-release" version +# ('alpha', 'beta', 'a', 'b', 'pre', 'p') +# - indicating a post-release patch ('p', 'pl', 'patch') +# but of course this can't cover all version number schemes, and there's +# no way to know what a programmer means without asking him. +# +# The problem is what to do with letters (and other non-numeric +# characters) in a version number. The current implementation does the +# obvious and predictable thing: keep them as strings and compare +# lexically within a tuple comparison. This has the desired effect if +# an appended letter sequence implies something "post-release": +# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002". +# +# However, if letters in a version number imply a pre-release version, +# the "obvious" thing isn't correct. Eg. you would expect that +# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison +# implemented here, this just isn't so. +# +# Two possible solutions come to mind. The first is to tie the +# comparison algorithm to a particular set of semantic rules, as has +# been done in the StrictVersion class above. This works great as long +# as everyone can go along with bondage and discipline. Hopefully a +# (large) subset of Python module programmers will agree that the +# particular flavour of bondage and discipline provided by StrictVersion +# provides enough benefit to be worth using, and will submit their +# version numbering scheme to its domination. The free-thinking +# anarchists in the lot will never give in, though, and something needs +# to be done to accommodate them. +# +# Perhaps a "moderately strict" version class could be implemented that +# lets almost anything slide (syntactically), and makes some heuristic +# assumptions about non-digits in version number strings. This could +# sink into special-case-hell, though; if I was as talented and +# idiosyncratic as Larry Wall, I'd go ahead and implement a class that +# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is +# just as happy dealing with things like "2g6" and "1.13++". I don't +# think I'm smart enough to do it right though. +# +# In any case, I've coded the test suite for this module (see +# ../test/test_version.py) specifically to fail on things like comparing +# "1.2a2" and "1.2". That's not because the *code* is doing anything +# wrong, it's because the simple, obvious design doesn't match my +# complicated, hairy expectations for real-world version numbers. It +# would be a snap to fix the test suite to say, "Yep, LooseVersion does +# the Right Thing" (ie. the code matches the conception). But I'd rather +# have a conception that matches common notions about version numbers. + + +class LooseVersion(Version): + """Version numbering for anarchists and software realists. + Implements the standard interface for version number classes as + described above. A version number consists of a series of numbers, + separated by either periods or strings of letters. When comparing + version numbers, the numeric components will be compared + numerically, and the alphabetic components lexically. The following + are all valid version numbers, in no particular order: + 1.5.1 + 1.5.2b2 + 161 + 3.10a + 8.02 + 3.4j + 1996.07.12 + 3.2.pl0 + 3.1.1.6 + 2g6 + 11g + 0.960923 + 2.2beta29 + 1.13++ + 5.5.kw + 2.0b1pl0 + In fact, there is no such thing as an invalid version number under + this scheme; the rules for comparison are simple and predictable, + but may not always give the results you want (for some definition + of "want"). + """ + + component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE) + + def __init__(self, vstring=None): + if vstring: + self.parse(vstring) + + def parse(self, vstring): + # I've given up on thinking I can reconstruct the version string + # from the parsed tuple -- so I just store the string here for + # use by __str__ + self.vstring = vstring + components = [x for x in self.component_re.split(vstring) if x and x != '.'] + for i, obj in enumerate(components): + try: + components[i] = int(obj) + except ValueError: + pass + + self.version = components + + def __str__(self): + return self.vstring + + def __repr__(self): + return "LooseVersion ('%s')" % str(self) + + def _cmp(self, other): + if isinstance(other, str): + other = LooseVersion(other) + elif not isinstance(other, LooseVersion): + return NotImplemented + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + +# end class LooseVersion diff --git a/ansible_collections/theforeman/foreman/plugins/module_utils/foreman_helper.py b/ansible_collections/theforeman/foreman/plugins/module_utils/foreman_helper.py new file mode 100644 index 00000000..a6dc0384 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/module_utils/foreman_helper.py @@ -0,0 +1,1864 @@ +# -*- coding: utf-8 -*- +# (c) Matthias Dellweg (ATIX AG) 2017 + +# pylint: disable=raise-missing-from +# pylint: disable=super-with-arguments + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import hashlib +import json +import os +import operator +import re +import time +import traceback + +from contextlib import contextmanager + +from collections import defaultdict +from functools import wraps + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib, env_fallback +from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils import six + +try: + from ansible_collections.theforeman.foreman.plugins.module_utils._version import LooseVersion +except ImportError: + from plugins.module_utils._version import LooseVersion + +try: + try: + from ansible_collections.theforeman.foreman.plugins.module_utils import _apypie as apypie + except ImportError: + from plugins.module_utils import _apypie as apypie + import requests.exceptions + HAS_APYPIE = True + APYPIE_IMP_ERR = None + inflector = apypie.Inflector() +except ImportError: + HAS_APYPIE = False + APYPIE_IMP_ERR = traceback.format_exc() + +try: + import yaml + HAS_PYYAML = True + PYYAML_IMP_ERR = None +except ImportError: + HAS_PYYAML = False + PYYAML_IMP_ERR = traceback.format_exc() + +parameter_foreman_spec = dict( + id=dict(invisible=True), + name=dict(required=True), + value=dict(type='raw', required=True), + parameter_type=dict(default='string', choices=['string', 'boolean', 'integer', 'real', 'array', 'hash', 'yaml', 'json']), +) + +parameter_ansible_spec = {k: v for (k, v) in parameter_foreman_spec.items() if k != 'id'} + +_PLUGIN_RESOURCES = { + 'ansible': 'ansible_roles', + 'discovery': 'discovery_rules', + 'katello': 'subscriptions', + 'openscap': 'scap_contents', + 'remote_execution': 'remote_execution_features', + 'scc_manager': 'scc_accounts', + 'snapshot_management': 'snapshots', + 'templates': 'templates', +} + +ENTITY_KEYS = dict( + hostgroups='title', + locations='title', + operatingsystems='title', + # TODO: Organizations should be search by title (as foreman allows nested orgs) but that's not the case ATM. + # Applying this will need to record a lot of tests that is out of scope for the moment. + # organizations='title', + scap_contents='title', + users='login', +) + +PER_PAGE = 2 << 31 + + +class NoEntity(object): + pass + + +def _exception2fail_json(msg='Generic failure: {0}'): + """ + Decorator to convert Python exceptions into Ansible errors that can be reported to the user. + """ + + def decor(f): + @wraps(f) + def inner(self, *args, **kwargs): + try: + return f(self, *args, **kwargs) + except Exception as e: + err_msg = "{0}: {1}".format(e.__class__.__name__, to_native(e)) + self.fail_from_exception(e, msg.format(err_msg)) + return inner + return decor + + +def _check_patch_needed(introduced_version=None, fixed_version=None, plugins=None): + """ + Decorator to check whether a specific apidoc patch is required. + + :param introduced_version: The version of Foreman the API bug was introduced. + :type introduced_version: str, optional + :param fixed_version: The version of Foreman the API bug was fixed. + :type fixed_version: str, optional + :param plugins: Which plugins are required for this patch. + :type plugins: list, optional + """ + + def decor(f): + @wraps(f) + def inner(self, *args, **kwargs): + if plugins is not None and not all(self.has_plugin(plugin) for plugin in plugins): + return + + if fixed_version is not None and self.foreman_version >= LooseVersion(fixed_version): + return + + if introduced_version is not None and self.foreman_version < LooseVersion(introduced_version): + return + + return f(self, *args, **kwargs) + return inner + return decor + + +class KatelloMixin(): + """ + Katello Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with Katello entities. + + This includes: + + * add a required ``organization`` parameter to the module + * add Katello to the list of required plugins + """ + + def __init__(self, **kwargs): + foreman_spec = dict( + organization=dict(type='entity', required=True), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + required_plugins = kwargs.pop('required_plugins', []) + required_plugins.append(('katello', ['*'])) + super(KatelloMixin, self).__init__(foreman_spec=foreman_spec, required_plugins=required_plugins, **kwargs) + + +class TaxonomyMixin(object): + """ + Taxonomy Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with taxonomic entities. + + This adds optional ``organizations`` and ``locations`` parameters to the module. + """ + + def __init__(self, **kwargs): + foreman_spec = dict( + organizations=dict(type='entity_list'), + locations=dict(type='entity_list'), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + super(TaxonomyMixin, self).__init__(foreman_spec=foreman_spec, **kwargs) + + +class ParametersMixinBase(object): + """ + Base Class for the Parameters Mixins. + + Provides a function to verify no duplicate parameters are set. + """ + + def validate_parameters(self): + parameters = self.foreman_params.get('parameters') + if parameters is not None: + parameter_names = [param['name'] for param in parameters] + duplicate_params = set([x for x in parameter_names if parameter_names.count(x) > 1]) + if duplicate_params: + self.fail_json(msg="There are duplicate keys in 'parameters': {0}.".format(duplicate_params)) + + +class ParametersMixin(ParametersMixinBase): + """ + Parameters Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with entities that support parameters. + + This allows to submit parameters to Foreman in the same request as modifying the main entity, thus making the parameters + available to any action that might be triggered when the entity is saved. + + By default, parametes are submited to the API using the ``<entity_name>_parameters_attributes`` key. + If you need to override this, set the ``PARAMETERS_FLAT_NAME`` attribute to the key that shall be used instead. + + This adds optional ``parameters`` parameter to the module. It also enhances the ``run()`` method to properly handle the + provided parameters. + """ + + def __init__(self, **kwargs): + self.entity_name = kwargs.pop('entity_name', self.entity_name_from_class) + parameters_flat_name = getattr(self, "PARAMETERS_FLAT_NAME", None) or '{0}_parameters_attributes'.format(self.entity_name) + foreman_spec = dict( + parameters=dict(type='list', elements='dict', options=parameter_ansible_spec, flat_name=parameters_flat_name), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + super(ParametersMixin, self).__init__(foreman_spec=foreman_spec, **kwargs) + + self.validate_parameters() + + def run(self, **kwargs): + entity = self.lookup_entity('entity') + if not self.desired_absent: + if entity and 'parameters' in entity: + entity['parameters'] = parameters_list_to_str_list(entity['parameters']) + parameters = self.foreman_params.get('parameters') + if parameters is not None: + self.foreman_params['parameters'] = parameters_list_to_str_list(parameters) + + return super(ParametersMixin, self).run(**kwargs) + + +class NestedParametersMixin(ParametersMixinBase): + """ + Nested Parameters Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with entities that support parameters, + but require them to be managed in separate API requests. + + This adds optional ``parameters`` parameter to the module. It also enhances the ``run()`` method to properly handle the + provided parameters. + """ + + def __init__(self, **kwargs): + foreman_spec = dict( + parameters=dict(type='nested_list', foreman_spec=parameter_foreman_spec), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + super(NestedParametersMixin, self).__init__(foreman_spec=foreman_spec, **kwargs) + + self.validate_parameters() + + def run(self, **kwargs): + new_entity = super(NestedParametersMixin, self).run(**kwargs) + if new_entity: + scope = {'{0}_id'.format(self.entity_name): new_entity['id']} + self.ensure_scoped_parameters(scope) + return new_entity + + def ensure_scoped_parameters(self, scope): + parameters = self.foreman_params.get('parameters') + if parameters is not None: + entity = self.lookup_entity('entity') + if self.state == 'present' or (self.state == 'present_with_defaults' and entity is None): + if entity: + current_parameters = {parameter['name']: parameter for parameter in self.list_resource('parameters', params=scope)} + else: + current_parameters = {} + desired_parameters = {parameter['name']: parameter for parameter in parameters} + + for name in desired_parameters: + desired_parameter = desired_parameters[name] + desired_parameter['value'] = parameter_value_to_str(desired_parameter['value'], desired_parameter['parameter_type']) + current_parameter = current_parameters.pop(name, None) + if current_parameter: + if 'parameter_type' not in current_parameter: + current_parameter['parameter_type'] = 'string' + current_parameter['value'] = parameter_value_to_str(current_parameter['value'], current_parameter['parameter_type']) + self.ensure_entity( + 'parameters', desired_parameter, current_parameter, state="present", foreman_spec=parameter_foreman_spec, params=scope) + for current_parameter in current_parameters.values(): + self.ensure_entity( + 'parameters', None, current_parameter, state="absent", foreman_spec=parameter_foreman_spec, params=scope) + + +class HostMixin(ParametersMixin): + """ + Host Mixin to extend a :class:`ForemanAnsibleModule` (or any subclass) to work with host-related entities (Hosts, Hostgroups). + + This adds many optional parameters that are specific to Hosts and Hostgroups to the module. + It also includes :class:`ParametersMixin`. + """ + + def __init__(self, **kwargs): + foreman_spec = dict( + compute_resource=dict(type='entity'), + compute_profile=dict(type='entity'), + domain=dict(type='entity'), + subnet=dict(type='entity'), + subnet6=dict(type='entity', resource_type='subnets'), + root_pass=dict(no_log=True), + realm=dict(type='entity'), + architecture=dict(type='entity'), + operatingsystem=dict(type='entity'), + medium=dict(aliases=['media'], type='entity'), + ptable=dict(type='entity'), + pxe_loader=dict(choices=['PXELinux BIOS', 'PXELinux UEFI', 'Grub UEFI', 'Grub2 BIOS', 'Grub2 ELF', + 'Grub2 UEFI', 'Grub2 UEFI SecureBoot', 'Grub2 UEFI HTTP', 'Grub2 UEFI HTTPS', + 'Grub2 UEFI HTTPS SecureBoot', 'iPXE Embedded', 'iPXE UEFI HTTP', 'iPXE Chain BIOS', 'iPXE Chain UEFI', 'None']), + environment=dict(type='entity'), + puppetclasses=dict(type='entity_list', resolve=False), + config_groups=dict(type='entity_list'), + puppet_proxy=dict(type='entity', resource_type='smart_proxies'), + puppet_ca_proxy=dict(type='entity', resource_type='smart_proxies'), + openscap_proxy=dict(type='entity', resource_type='smart_proxies'), + content_source=dict(type='entity', scope=['organization'], resource_type='smart_proxies'), + lifecycle_environment=dict(type='entity', scope=['organization']), + kickstart_repository=dict(type='entity', scope=['organization'], optional_scope=['lifecycle_environment', 'content_view'], + resource_type='repositories'), + content_view=dict(type='entity', scope=['organization'], optional_scope=['lifecycle_environment']), + activation_keys=dict(no_log=False), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + required_plugins = kwargs.pop('required_plugins', []) + [ + ('katello', ['activation_keys', 'content_source', 'lifecycle_environment', 'kickstart_repository', 'content_view']), + ('openscap', ['openscap_proxy']), + ] + mutually_exclusive = kwargs.pop('mutually_exclusive', []) + [['medium', 'kickstart_repository']] + super(HostMixin, self).__init__(foreman_spec=foreman_spec, required_plugins=required_plugins, mutually_exclusive=mutually_exclusive, **kwargs) + + def run(self, **kwargs): + entity = self.lookup_entity('entity') + + if not self.desired_absent: + if 'activation_keys' in self.foreman_params: + if 'parameters' not in self.foreman_params: + parameters = [param for param in (entity or {}).get('parameters', []) if param['name'] != 'kt_activation_keys'] + else: + parameters = self.foreman_params['parameters'] + ak_param = {'name': 'kt_activation_keys', 'parameter_type': 'string', 'value': self.foreman_params.pop('activation_keys')} + self.foreman_params['parameters'] = parameters + [ak_param] + elif 'parameters' in self.foreman_params and entity is not None: + current_ak_param = next((param for param in entity.get('parameters') if param['name'] == 'kt_activation_keys'), None) + desired_ak_param = next((param for param in self.foreman_params['parameters'] if param['name'] == 'kt_activation_keys'), None) + if current_ak_param and desired_ak_param is None: + self.foreman_params['parameters'].append(current_ak_param) + + self.validate_parameters() + + return super(HostMixin, self).run(**kwargs) + + +class ForemanAnsibleModule(AnsibleModule): + """ Baseclass for all foreman related Ansible modules. + It handles connection parameters and adds the concept of the `foreman_spec`. + This adds automatic entities resolution based on provided attributes/ sub entities options. + + It adds the following options to foreman_spec 'entity' and 'entity_list' types: + + * search_by (str): Field used to search the sub entity. Defaults to 'name' unless `parent` was set, in which case it defaults to `title`. + * search_operator (str): Operator used to search the sub entity. Defaults to '='. For fuzzy search use '~'. + * resource_type (str): Resource type used to build API resource PATH. Defaults to pluralized entity key. + * resolve (boolean): Defaults to 'True'. If set to false, the sub entity will not be resolved automatically + * ensure (boolean): Defaults to 'True'. If set to false, it will be removed before sending data to the foreman server. + """ + + def __init__(self, **kwargs): + # State recording for changed and diff reporting + self._changed = False + self._before = defaultdict(list) + self._after = defaultdict(list) + self._after_full = defaultdict(list) + + self.foreman_spec, gen_args = _foreman_spec_helper(kwargs.pop('foreman_spec', {})) + argument_spec = dict( + server_url=dict(required=True, fallback=(env_fallback, ['FOREMAN_SERVER_URL', 'FOREMAN_SERVER', 'FOREMAN_URL'])), + username=dict(required=True, fallback=(env_fallback, ['FOREMAN_USERNAME', 'FOREMAN_USER'])), + password=dict(required=True, no_log=True, fallback=(env_fallback, ['FOREMAN_PASSWORD'])), + validate_certs=dict(type='bool', default=True, fallback=(env_fallback, ['FOREMAN_VALIDATE_CERTS'])), + ) + argument_spec.update(gen_args) + argument_spec.update(kwargs.pop('argument_spec', {})) + supports_check_mode = kwargs.pop('supports_check_mode', True) + + self.required_plugins = kwargs.pop('required_plugins', []) + + super(ForemanAnsibleModule, self).__init__(argument_spec=argument_spec, supports_check_mode=supports_check_mode, **kwargs) + + aliases = {alias for arg in argument_spec.values() for alias in arg.get('aliases', [])} + self.foreman_params = _recursive_dict_without_none(self.params, aliases) + + self.check_requirements() + + self._foremanapi_server_url = self.foreman_params.pop('server_url') + self._foremanapi_username = self.foreman_params.pop('username') + self._foremanapi_password = self.foreman_params.pop('password') + self._foremanapi_validate_certs = self.foreman_params.pop('validate_certs') + + if self._foremanapi_server_url.lower().startswith('http://'): + self.warn("You have configured a plain HTTP server URL. All communication will happen unencrypted.") + elif not self._foremanapi_server_url.lower().startswith('https://'): + self.fail_json(msg="The server URL needs to be either HTTPS or HTTP!") + + self.task_timeout = 60 + self.task_poll = 4 + + self._thin_default = False + self.state = 'undefined' + + @contextmanager + def api_connection(self): + """ + Execute a given code block after connecting to the API. + + When the block has finished, call :func:`exit_json` to report that the module has finished to Ansible. + """ + + self.connect() + yield + self.exit_json() + + @property + def changed(self): + return self._changed + + def set_changed(self): + self._changed = True + + def _patch_host_update(self): + _host_methods = self.foremanapi.apidoc['docs']['resources']['hosts']['methods'] + + _host_update = next(x for x in _host_methods if x['name'] == 'update') + for param in ['location_id', 'organization_id']: + _host_update_taxonomy_param = next(x for x in _host_update['params'] if x['name'] == param) + _host_update['params'].remove(_host_update_taxonomy_param) + + @_check_patch_needed(fixed_version='2.2.0', plugins=['remote_execution']) + def _patch_subnet_rex_api(self): + """ + This is a workaround for the broken subnet apidoc in foreman remote execution. + See https://projects.theforeman.org/issues/19086 and https://projects.theforeman.org/issues/30651 + """ + + _subnet_rex_proxies_parameter = { + u'validations': [], + u'name': u'remote_execution_proxy_ids', + u'show': True, + u'description': u'\n<p>Remote Execution Proxy IDs</p>\n', + u'required': False, + u'allow_nil': True, + u'allow_blank': False, + u'full_name': u'subnet[remote_execution_proxy_ids]', + u'expected_type': u'array', + u'metadata': None, + u'validator': u'', + } + _subnet_methods = self.foremanapi.apidoc['docs']['resources']['subnets']['methods'] + + _subnet_create = next(x for x in _subnet_methods if x['name'] == 'create') + _subnet_create_params_subnet = next(x for x in _subnet_create['params'] if x['name'] == 'subnet') + _subnet_create_params_subnet['params'].append(_subnet_rex_proxies_parameter) + + _subnet_update = next(x for x in _subnet_methods if x['name'] == 'update') + _subnet_update_params_subnet = next(x for x in _subnet_update['params'] if x['name'] == 'subnet') + _subnet_update_params_subnet['params'].append(_subnet_rex_proxies_parameter) + + @_check_patch_needed(introduced_version='2.1.0', fixed_version='2.3.0') + def _patch_subnet_externalipam_group_api(self): + """ + This is a workaround for the broken subnet apidoc for External IPAM. + See https://projects.theforeman.org/issues/30890 + """ + + _subnet_externalipam_group_parameter = { + u'validations': [], + u'name': u'externalipam_group', + u'show': True, + u'description': u'\n<p>External IPAM group - only relevant when IPAM is set to external</p>\n', + u'required': False, + u'allow_nil': True, + u'allow_blank': False, + u'full_name': u'subnet[externalipam_group]', + u'expected_type': u'string', + u'metadata': None, + u'validator': u'', + } + _subnet_methods = self.foremanapi.apidoc['docs']['resources']['subnets']['methods'] + + _subnet_create = next(x for x in _subnet_methods if x['name'] == 'create') + _subnet_create_params_subnet = next(x for x in _subnet_create['params'] if x['name'] == 'subnet') + _subnet_create_params_subnet['params'].append(_subnet_externalipam_group_parameter) + + _subnet_update = next(x for x in _subnet_methods if x['name'] == 'update') + _subnet_update_params_subnet = next(x for x in _subnet_update['params'] if x['name'] == 'subnet') + _subnet_update_params_subnet['params'].append(_subnet_externalipam_group_parameter) + + @_check_patch_needed(plugins=['katello']) + def _patch_organization_update_api(self): + """ + This is a workaround for the broken organization update apidoc in Katello. + See https://projects.theforeman.org/issues/27538 + """ + + _organization_methods = self.foremanapi.apidoc['docs']['resources']['organizations']['methods'] + + _organization_update = next(x for x in _organization_methods if x['name'] == 'update') + _organization_update_params_organization = next(x for x in _organization_update['params'] if x['name'] == 'organization') + _organization_update_params_organization['required'] = False + + @_check_patch_needed(plugins=['katello']) + def _patch_cv_filter_rule_api(self): + """ + This is a workaround for missing params of CV Filter Rule update controller in Katello. + See https://projects.theforeman.org/issues/30908 + """ + + _content_view_filter_rule_methods = self.foremanapi.apidoc['docs']['resources']['content_view_filter_rules']['methods'] + + _content_view_filter_rule_create = next(x for x in _content_view_filter_rule_methods if x['name'] == 'create') + _content_view_filter_rule_update = next(x for x in _content_view_filter_rule_methods if x['name'] == 'update') + + for param_name in ['uuid', 'errata_ids', 'date_type', 'module_stream_ids']: + create_param = next((x for x in _content_view_filter_rule_create['params'] if x['name'] == param_name), None) + update_param = next((x for x in _content_view_filter_rule_update['params'] if x['name'] == param_name), None) + if create_param is not None and update_param is None: + _content_view_filter_rule_update['params'].append(create_param) + + @_check_patch_needed(fixed_version='3.5.0', plugins=['katello']) + def _patch_ak_product_content_per_page(self): + """ + This is a workaround for the API not exposing the per_page param on the product_content endpoint + See https://projects.theforeman.org/issues/35633 + """ + + _per_page_param = { + "name": "per_page", + "full_name": "per_page", + "description": "\n<p>Number of results per page to return</p>\n", + "required": False, + "allow_nil": False, + "allow_blank": False, + "validator": "Must be a number.", + "expected_type": "numeric", + "metadata": None, + "show": True, + "validations": [] + } + + _ak_methods = self.foremanapi.apidoc['docs']['resources']['activation_keys']['methods'] + + _ak_product_content = next(x for x in _ak_methods if x['name'] == 'product_content') + + if next((x for x in _ak_product_content['params'] if x['name'] == 'per_page'), None) is None: + _ak_product_content['params'].append(_per_page_param) + + @_check_patch_needed(fixed_version='3.5.0', plugins=['katello']) + def _patch_organization_ignore_types_api(self): + """ + This is a workaround for the missing ignore_types in the organization apidoc in Katello. + See https://projects.theforeman.org/issues/35687 + """ + + _ignore_types_param = { + "name": "ignore_types", + "full_name": "organization[ignore_types]", + "description": "\n<p>List of resources types that will be automatically associated</p>\n", + "required": False, + "allow_nil": True, + "allow_blank": False, + "validator": "Must be an array of any type", + "expected_type": "array", + "metadata": None, + "show": True, + "validations": [] + } + + _organization_methods = self.foremanapi.apidoc['docs']['resources']['organizations']['methods'] + + _organization_create = next(x for x in _organization_methods if x['name'] == 'create') + _organization_update = next(x for x in _organization_methods if x['name'] == 'update') + if next((x for x in _organization_create['params'] if x['name'] == 'ignore_types'), None) is None: + _organization_create['params'].append(_ignore_types_param) + _organization_update['params'].append(_ignore_types_param) + + def check_requirements(self): + if not HAS_APYPIE: + self.fail_json(msg=missing_required_lib("requests"), exception=APYPIE_IMP_ERR) + + @_exception2fail_json(msg="Failed to connect to Foreman server: {0}") + def connect(self): + """ + Connect to the Foreman API. + + This will create a new ``apypie.Api`` instance using the provided server information, + check that the API is actually reachable (by calling :func:`status`), + apply any required patches to the apidoc and ensure the server has all the plugins installed + that are required by the module. + """ + + self.foremanapi = apypie.Api( + uri=self._foremanapi_server_url, + username=to_bytes(self._foremanapi_username), + password=to_bytes(self._foremanapi_password), + api_version=2, + verify_ssl=self._foremanapi_validate_certs, + ) + + _status = self.status() + self.foreman_version = LooseVersion(_status.get('version', '0.0.0')) + self.apply_apidoc_patches() + self.check_required_plugins() + + def apply_apidoc_patches(self): + """ + Apply patches to the local apidoc representation. + When adding another patch, consider that the endpoint in question may depend on a plugin to be available. + If possible, make the patch only execute on specific server/plugin versions. + """ + + self._patch_host_update() + + self._patch_subnet_rex_api() + self._patch_subnet_externalipam_group_api() + + # Katello + self._patch_organization_update_api() + self._patch_cv_filter_rule_api() + self._patch_ak_product_content_per_page() + self._patch_organization_ignore_types_api() + + @_exception2fail_json(msg="Failed to connect to Foreman server: {0}") + def status(self): + """ + Call the ``status`` API endpoint to ensure the server is reachable. + + :return: The full API response + :rtype: dict + """ + + return self.foremanapi.resource('home').call('status') + + def _resource(self, resource): + if resource not in self.foremanapi.resources: + raise Exception("The server doesn't know about {0}, is the right plugin installed?".format(resource)) + return self.foremanapi.resource(resource) + + def _resource_call(self, resource, *args, **kwargs): + return self._resource(resource).call(*args, **kwargs) + + def _resource_prepare_params(self, resource, action, params): + api_action = self._resource(resource).action(action) + return api_action.prepare_params(params) + + @_exception2fail_json(msg='Failed to show resource: {0}') + def show_resource(self, resource, resource_id, params=None): + """ + Execute the ``show`` action on an entity. + + :param resource: Plural name of the api resource to show + :type resource: str + :param resource_id: The ID of the entity to show + :type resource_id: int + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: Union[dict,None], optional + """ + + if params is None: + params = {} + else: + params = params.copy() + + params['id'] = resource_id + + params = self._resource_prepare_params(resource, 'show', params) + + return self._resource_call(resource, 'show', params) + + @_exception2fail_json(msg='Failed to list resource: {0}') + def list_resource(self, resource, search=None, params=None): + """ + Execute the ``index`` action on an resource. + + :param resource: Plural name of the api resource to show + :type resource: str + :param search: Search string as accepted by the API to limit the results + :type search: str, optional + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: Union[dict,None], optional + """ + + if params is None: + params = {} + else: + params = params.copy() + + if search is not None: + params['search'] = search + params['per_page'] = PER_PAGE + + params = self._resource_prepare_params(resource, 'index', params) + + return self._resource_call(resource, 'index', params)['results'] + + def find_resource(self, resource, search, params=None, failsafe=False, thin=None): + list_params = {} + if params is not None: + list_params.update(params) + if thin is None: + thin = self._thin_default + list_params['thin'] = thin + results = self.list_resource(resource, search, list_params) + if len(results) == 1: + result = results[0] + elif failsafe: + result = None + else: + if len(results) > 1: + error_msg = "too many ({0})".format(len(results)) + else: + error_msg = "no" + self.fail_json(msg="Found {0} results while searching for {1} with {2}".format(error_msg, resource, search)) + if result and not thin: + result = self.show_resource(resource, result['id'], params=params) + return result + + def find_resource_by(self, resource, search_field, value, **kwargs): + if not value: + return NoEntity + search = '{0}{1}"{2}"'.format(search_field, kwargs.pop('search_operator', '='), value) + return self.find_resource(resource, search, **kwargs) + + def find_resource_by_name(self, resource, name, **kwargs): + return self.find_resource_by(resource, 'name', name, **kwargs) + + def find_resource_by_title(self, resource, title, **kwargs): + return self.find_resource_by(resource, 'title', title, **kwargs) + + def find_resource_by_id(self, resource, obj_id, **kwargs): + return self.find_resource_by(resource, 'id', obj_id, **kwargs) + + def find_resources_by_name(self, resource, names, **kwargs): + return [self.find_resource_by_name(resource, name, **kwargs) for name in names] + + def find_operatingsystem(self, name, failsafe=False, **kwargs): + result = self.find_resource_by_title('operatingsystems', name, failsafe=True, **kwargs) + if not result: + result = self.find_resource_by('operatingsystems', 'title', name, search_operator='~', failsafe=failsafe, **kwargs) + return result + + def find_puppetclass(self, name, environment=None, params=None, failsafe=False, thin=None): + if thin is None: + thin = self._thin_default + if environment: + scope = {'environment_id': environment} + else: + scope = {} + if params is not None: + scope.update(params) + search = 'name="{0}"'.format(name) + results = self.list_resource('puppetclasses', search, params=scope) + + # verify that only one puppet module is returned with only one puppet class inside + # as provided search results have to be like "results": { "ntp": [{"id": 1, "name": "ntp" ...}]} + # and get the puppet class id + if len(results) == 1 and len(list(results.values())[0]) == 1: + result = list(results.values())[0][0] + if thin: + return {'id': result['id']} + else: + return result + + if failsafe: + return None + else: + self.fail_json(msg='No data found for name="%s"' % search) + + def find_puppetclasses(self, names, **kwargs): + return [self.find_puppetclass(name, **kwargs) for name in names] + + def find_cluster(self, name, compute_resource): + cluster = self.find_compute_resource_parts('clusters', name, compute_resource, None, ['ovirt', 'vmware']) + + # workaround for https://projects.theforeman.org/issues/31874 + if compute_resource['provider'].lower() == 'vmware': + cluster['_api_identifier'] = cluster['name'] + else: + cluster['_api_identifier'] = cluster['id'] + + return cluster + + def find_network(self, name, compute_resource, cluster=None): + return self.find_compute_resource_parts('networks', name, compute_resource, cluster, ['ovirt', 'vmware', 'google', 'azurerm']) + + def find_storage_domain(self, name, compute_resource, cluster=None): + return self.find_compute_resource_parts('storage_domains', name, compute_resource, cluster, ['ovirt', 'vmware']) + + def find_storage_pod(self, name, compute_resource, cluster=None): + return self.find_compute_resource_parts('storage_pods', name, compute_resource, cluster, ['vmware']) + + def find_compute_resource_parts(self, part_name, name, compute_resource, cluster=None, supported_crs=None): + if supported_crs is None: + supported_crs = [] + + if compute_resource['provider'].lower() not in supported_crs: + return {'id': name, 'name': name} + + additional_params = {'id': compute_resource['id']} + if cluster is not None: + additional_params['cluster_id'] = cluster['_api_identifier'] + api_name = 'available_{0}'.format(part_name) + available_parts = self.resource_action('compute_resources', api_name, params=additional_params, + ignore_check_mode=True, record_change=False)['results'] + part = next((part for part in available_parts if str(part['name']) == str(name) or str(part['id']) == str(name)), None) + if part is None: + err_msg = "Could not find {0} '{1}' on compute resource '{2}'.".format(part_name, name, compute_resource.get('name')) + self.fail_json(msg=err_msg) + return part + + def scope_for(self, key, scoped_resource=None): + # workaround for https://projects.theforeman.org/issues/31714 + if scoped_resource in ['content_views', 'repositories'] and key == 'lifecycle_environment': + scope_key = 'environment' + else: + scope_key = key + return {'{0}_id'.format(scope_key): self.lookup_entity(key)['id']} + + def set_entity(self, key, entity): + self.foreman_params[key] = entity + + def lookup_entity(self, key, params=None): + if key not in self.foreman_params: + return None + + entity_spec = self.foreman_spec[key] + if _is_resolved(entity_spec, self.foreman_params[key]): + # Already looked up or not an entity(_list) so nothing to do + return self.foreman_params[key] + + result = self._lookup_entity(self.foreman_params[key], entity_spec, params) + self.set_entity(key, result) + return result + + def _lookup_entity(self, identifier, entity_spec, params=None): + resource_type = entity_spec['resource_type'] + failsafe = entity_spec.get('failsafe', False) + thin = entity_spec.get('thin', True) + if params: + params = params.copy() + else: + params = {} + try: + for scope in entity_spec.get('scope', []): + params.update(self.scope_for(scope, resource_type)) + for optional_scope in entity_spec.get('optional_scope', []): + if optional_scope in self.foreman_params: + params.update(self.scope_for(optional_scope, resource_type)) + + except TypeError: + if failsafe: + if entity_spec.get('type') == 'entity': + result = None + else: + result = [None for value in identifier] + else: + self.fail_json(msg="Failed to lookup scope {0} while searching for {1}.".format(entity_spec['scope'], resource_type)) + else: + # No exception happend => scope is in place + if resource_type == 'operatingsystems': + if entity_spec.get('type') == 'entity': + result = self.find_operatingsystem(identifier, params=params, failsafe=failsafe, thin=thin) + else: + result = [self.find_operatingsystem(value, params=params, failsafe=failsafe, thin=thin) for value in identifier] + elif resource_type == 'puppetclasses': + if entity_spec.get('type') == 'entity': + result = self.find_puppetclass(identifier, params=params, failsafe=failsafe, thin=thin) + else: + result = [self.find_puppetclass(value, params=params, failsafe=failsafe, thin=thin) for value in identifier] + else: + if entity_spec.get('type') == 'entity': + result = self.find_resource_by( + resource=resource_type, + value=identifier, + search_field=entity_spec.get('search_by', ENTITY_KEYS.get(resource_type, 'name')), + search_operator=entity_spec.get('search_operator', '='), + failsafe=failsafe, thin=thin, params=params, + ) + else: + result = [self.find_resource_by( + resource=resource_type, + value=value, + search_field=entity_spec.get('search_by', ENTITY_KEYS.get(resource_type, 'name')), + search_operator=entity_spec.get('search_operator', '='), + failsafe=failsafe, thin=thin, params=params, + ) for value in identifier] + return result + + def auto_lookup_entities(self): + self.auto_lookup_nested_entities() + return [ + self.lookup_entity(key) + for key, entity_spec in self.foreman_spec.items() + if entity_spec.get('resolve', True) and entity_spec.get('type') in {'entity', 'entity_list'} + ] + + def auto_lookup_nested_entities(self): + for key, entity_spec in self.foreman_spec.items(): + if entity_spec.get('type') in {'nested_list'}: + for nested_key, nested_spec in entity_spec['foreman_spec'].items(): + for item in self.foreman_params.get(key, []): + if (nested_key in item and nested_spec.get('resolve', True) + and not _is_resolved(nested_spec, item[nested_key])): + item[nested_key] = self._lookup_entity(item[nested_key], nested_spec) + + def record_before(self, resource, entity): + if isinstance(entity, dict): + to_record = _recursive_dict_without_none(entity) + else: + to_record = entity + self._before[resource].append(to_record) + + def record_after(self, resource, entity): + if isinstance(entity, dict): + to_record = _recursive_dict_without_none(entity) + else: + to_record = entity + self._after[resource].append(to_record) + + def record_after_full(self, resource, entity): + self._after_full[resource].append(entity) + + @_exception2fail_json(msg='Failed to ensure entity state: {0}') + def ensure_entity(self, resource, desired_entity, current_entity, params=None, state=None, foreman_spec=None): + """ + Ensure that a given entity has a certain state + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param desired_entity: Desired properties of the entity + :type desired_entity: dict + :param current_entity: Current properties of the entity or None if nonexistent + :type current_entity: Union[dict,None] + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + :param state: Desired state of the entity (optionally taken from the module) + :type state: str, optional + :param foreman_spec: Description of the entity structure (optionally taken from module) + :type foreman_spec: dict, optional + + :return: The new current state of the entity + :rtype: Union[dict,None] + """ + if state is None: + state = self.state + if foreman_spec is None: + foreman_spec = self.foreman_spec + else: + foreman_spec, _dummy = _foreman_spec_helper(foreman_spec) + + updated_entity = None + + self.record_before(resource, _flatten_entity(current_entity, foreman_spec)) + + if state == 'present_with_defaults': + if current_entity is None: + updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec) + elif state == 'present': + if current_entity is None: + updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec) + else: + updated_entity = self._update_entity(resource, desired_entity, current_entity, params, foreman_spec) + elif state == 'copied': + if current_entity is not None: + updated_entity = self._copy_entity(resource, desired_entity, current_entity, params) + elif state == 'reverted': + if current_entity is not None: + updated_entity = self._revert_entity(resource, current_entity, params) + elif state == 'new_snapshot': + updated_entity = self._create_entity(resource, desired_entity, params, foreman_spec) + elif state == 'absent': + if current_entity is not None: + updated_entity = self._delete_entity(resource, current_entity, params) + else: + self.fail_json(msg='Not a valid state: {0}'.format(state)) + + self.record_after(resource, _flatten_entity(updated_entity, foreman_spec)) + self.record_after_full(resource, updated_entity) + + return updated_entity + + def _validate_supported_payload(self, resource, action, payload): + """ + Check whether the payload only contains supported keys. + Emits a warning for keys that are not part of the apidoc. + + :param resource: Plural name of the api resource to check + :type resource: str + :param action: Name of the action to check payload against + :type action: str + :param payload: API paylod to be checked + :type payload: dict + + :return: The payload as it can be submitted to the API + :rtype: dict + """ + filtered_payload = self._resource_prepare_params(resource, action, payload) + # On Python 2 dict.keys() is just a list, but we need a set here. + unsupported_parameters = set(payload.keys()) - set(_recursive_dict_keys(filtered_payload)) + if unsupported_parameters: + warn_msg = "The following parameters are not supported by your server when performing {0} on {1}: {2}. They were ignored." + self.warn(warn_msg.format(action, resource, unsupported_parameters)) + return filtered_payload + + def _create_entity(self, resource, desired_entity, params, foreman_spec): + """ + Create entity with given properties + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param desired_entity: Desired properties of the entity + :type desired_entity: dict + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + :param foreman_spec: Description of the entity structure + :type foreman_spec: dict + + :return: The new current state of the entity + :rtype: dict + """ + payload = _flatten_entity(desired_entity, foreman_spec) + self._validate_supported_payload(resource, 'create', payload) + if not self.check_mode: + if params: + payload.update(params) + return self.resource_action(resource, 'create', payload) + else: + fake_entity = desired_entity.copy() + fake_entity['id'] = -1 + self.set_changed() + return fake_entity + + def _update_entity(self, resource, desired_entity, current_entity, params, foreman_spec): + """ + Update a given entity with given properties if any diverge + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param desired_entity: Desired properties of the entity + :type desired_entity: dict + :param current_entity: Current properties of the entity + :type current_entity: dict + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + :param foreman_spec: Description of the entity structure + :type foreman_spec: dict + + :return: The new current state of the entity + :rtype: dict + """ + payload = {} + desired_entity = _flatten_entity(desired_entity, foreman_spec) + current_flat_entity = _flatten_entity(current_entity, foreman_spec) + for key, value in desired_entity.items(): + foreman_type = foreman_spec[key].get('type', 'str') + new_value = value + old_value = current_flat_entity.get(key) + # String comparison needs extra care in face of unicode + if foreman_type == 'str': + old_value = to_native(old_value) + new_value = to_native(new_value) + # ideally the type check would happen via foreman_spec.elements + # however this is not set for flattened entries and setting it + # confuses _flatten_entity + elif foreman_type == 'list' and value and isinstance(value[0], dict): + if 'name' in value[0]: + sort_key = 'name' + else: + sort_key = list(value[0].keys())[0] + new_value = sorted(new_value, key=operator.itemgetter(sort_key)) + old_value = sorted(old_value, key=operator.itemgetter(sort_key)) + if new_value != old_value: + payload[key] = value + if self._validate_supported_payload(resource, 'update', payload): + payload['id'] = current_flat_entity['id'] + if not self.check_mode: + if params: + payload.update(params) + return self.resource_action(resource, 'update', payload) + else: + # In check_mode we emulate the server updating the entity + fake_entity = current_flat_entity.copy() + fake_entity.update(payload) + self.set_changed() + return fake_entity + else: + # Nothing needs changing + return current_entity + + def _copy_entity(self, resource, desired_entity, current_entity, params): + """ + Copy a given entity + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param desired_entity: Desired properties of the entity + :type desired_entity: dict + :param current_entity: Current properties of the entity + :type current_entity: dict + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + + :return: The new current state of the entity + :rtype: dict + """ + payload = { + 'id': current_entity['id'], + 'new_name': desired_entity['new_name'], + } + if params: + payload.update(params) + return self.resource_action(resource, 'copy', payload) + + def _revert_entity(self, resource, current_entity, params): + """ + Revert a given entity + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param current_entity: Current properties of the entity + :type current_entity: dict + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + + :return: The new current state of the entity + :rtype: dict + """ + payload = {'id': current_entity['id']} + if params: + payload.update(params) + return self.resource_action(resource, 'revert', payload) + + def _delete_entity(self, resource, current_entity, params): + """ + Delete a given entity + + :param resource: Plural name of the api resource to manipulate + :type resource: str + :param current_entity: Current properties of the entity + :type current_entity: dict + :param params: Lookup parameters (i.e. parent_id for nested entities) + :type params: dict, optional + + :return: The new current state of the entity + :rtype: Union[dict,None] + """ + payload = {'id': current_entity['id']} + if params: + payload.update(params) + entity = self.resource_action(resource, 'destroy', payload) + + # this is a workaround for https://projects.theforeman.org/issues/26937 + if entity and isinstance(entity, dict) and 'error' in entity and 'message' in entity['error']: + self.fail_json(msg=entity['error']['message']) + + return None + + def resource_action(self, resource, action, params, options=None, data=None, files=None, + ignore_check_mode=False, record_change=True, ignore_task_errors=False): + resource_payload = self._resource_prepare_params(resource, action, params) + if options is None: + options = {} + try: + result = None + if ignore_check_mode or not self.check_mode: + result = self._resource_call(resource, action, resource_payload, options=options, data=data, files=files) + is_foreman_task = isinstance(result, dict) and 'action' in result and 'state' in result and 'started_at' in result + if is_foreman_task: + result = self.wait_for_task(result, ignore_errors=ignore_task_errors) + except Exception as e: + msg = 'Error while performing {0} on {1}: {2}'.format( + action, resource, to_native(e)) + self.fail_from_exception(e, msg) + if record_change and not ignore_check_mode: + # If we were supposed to ignore check_mode we can assume this action was not a changing one. + self.set_changed() + return result + + def wait_for_task(self, task, ignore_errors=False): + duration = self.task_timeout + while task['state'] not in ['paused', 'stopped']: + duration -= self.task_poll + if duration <= 0: + self.fail_json(msg="Timeout waiting for Task {0}".format(task['id'])) + time.sleep(self.task_poll) + + resource_payload = self._resource_prepare_params('foreman_tasks', 'show', {'id': task['id']}) + task = self._resource_call('foreman_tasks', 'show', resource_payload) + if not ignore_errors and task['result'] != 'success': + self.fail_json(msg='Task {0}({1}) did not succeed. Task information: {2}'.format(task['action'], task['id'], task['humanized']['errors'])) + return task + + def fail_from_exception(self, exc, msg): + fail = {'msg': msg} + if isinstance(exc, requests.exceptions.HTTPError): + try: + response = exc.response.json() + if 'error' in response: + fail['error'] = response['error'] + else: + fail['error'] = response + except Exception: + fail['error'] = exc.response.text + self.fail_json(**fail) + + def exit_json(self, changed=False, **kwargs): + kwargs['changed'] = changed or self.changed + if 'diff' not in kwargs and (self._before or self._after): + kwargs['diff'] = {'before': self._before, + 'after': self._after} + if 'entity' not in kwargs and self._after_full: + kwargs['entity'] = self._after_full + super(ForemanAnsibleModule, self).exit_json(**kwargs) + + def has_plugin(self, plugin_name): + try: + resource_name = _PLUGIN_RESOURCES[plugin_name] + except KeyError: + raise Exception("Unknown plugin: {0}".format(plugin_name)) + return resource_name in self.foremanapi.resources + + def check_required_plugins(self): + missing_plugins = [] + for (plugin, params) in self.required_plugins: + for param in params: + if (param in self.foreman_params or param == '*') and not self.has_plugin(plugin): + if param == '*': + param = 'the whole module' + missing_plugins.append("{0} (for {1})".format(plugin, param)) + if missing_plugins: + missing_msg = "The server is missing required plugins: {0}.".format(', '.join(missing_plugins)) + self.fail_json(msg=missing_msg) + + +class ForemanStatelessEntityAnsibleModule(ForemanAnsibleModule): + """ Base class for Foreman entities without a state. To use it, subclass it with the following convention: + To manage my_entity entity, create the following sub class:: + + class ForemanMyEntityModule(ForemanStatelessEntityAnsibleModule): + pass + + and use that class to instantiate module:: + + module = ForemanMyEntityModule( + argument_spec=dict( + [...] + ), + foreman_spec=dict( + [...] + ), + ) + + It adds the following attributes: + + * entity_key (str): field used to search current entity. Defaults to value provided by `ENTITY_KEYS` or 'name' if no value found. + * entity_name (str): name of the current entity. + By default deduce the entity name from the class name (eg: 'ForemanProvisioningTemplateModule' class will produce 'provisioning_template'). + * entity_opts (dict): Dict of options for base entity. Same options can be provided for subentities described in foreman_spec. + + The main entity is referenced with the key `entity` in the `foreman_spec`. + """ + + def __init__(self, **kwargs): + self.entity_key = kwargs.pop('entity_key', 'name') + self.entity_name = kwargs.pop('entity_name', self.entity_name_from_class) + entity_opts = kwargs.pop('entity_opts', {}) + + super(ForemanStatelessEntityAnsibleModule, self).__init__(**kwargs) + + if 'resource_type' not in entity_opts: + entity_opts['resource_type'] = inflector.pluralize(self.entity_name) + if 'thin' not in entity_opts: + # Explicit None to trigger the _thin_default mechanism lazily + entity_opts['thin'] = None + if 'failsafe' not in entity_opts: + entity_opts['failsafe'] = True + if 'search_operator' not in entity_opts: + entity_opts['search_operator'] = '=' + if 'search_by' not in entity_opts: + entity_opts['search_by'] = ENTITY_KEYS.get(entity_opts['resource_type'], 'name') + + self.foreman_spec.update(_foreman_spec_helper(dict( + entity=dict( + type='entity', + flat_name='id', + ensure=False, + **entity_opts + ), + ))[0]) + + if 'parent' in self.foreman_spec and self.foreman_spec['parent'].get('type') == 'entity': + if 'resouce_type' not in self.foreman_spec['parent']: + self.foreman_spec['parent']['resource_type'] = self.foreman_spec['entity']['resource_type'] + if 'failsafe' not in self.foreman_spec['parent']: + self.foreman_spec['parent']['failsafe'] = True + current, parent = split_fqn(self.foreman_params[self.entity_key]) + if isinstance(self.foreman_params.get('parent'), six.string_types): + if parent: + self.fail_json(msg="Please specify the parent either separately, or as part of the title.") + parent = self.foreman_params['parent'] + elif parent: + self.foreman_params['parent'] = parent + self.foreman_params[self.entity_key] = current + self.foreman_params['entity'] = build_fqn(current, parent) + else: + self.foreman_params['entity'] = self.foreman_params.get(self.entity_key) + + @property + def entity_name_from_class(self): + """ + The entity name derived from the class name. + + The class name must follow the following name convention: + + * It starts with ``Foreman`` or ``Katello``. + * It ends with ``Module``. + + This will convert the class name ``ForemanMyEntityModule`` to the entity name ``my_entity``. + + Examples: + + * ``ForemanArchitectureModule`` => ``architecture`` + * ``ForemanProvisioningTemplateModule`` => ``provisioning_template`` + * ``KatelloProductMudule`` => ``product`` + """ + # Convert current class name from CamelCase to snake_case + class_name = re.sub(r'(?<=[a-z])[A-Z]|[A-Z](?=[^A-Z])', r'_\g<0>', self.__class__.__name__).lower().strip('_') + # Get entity name from snake case class name + return '_'.join(class_name.split('_')[1:-1]) + + +class ForemanInfoAnsibleModule(ForemanStatelessEntityAnsibleModule): + """ + Base class for Foreman info modules that fetch information about entities + """ + def __init__(self, **kwargs): + self._resources = [] + foreman_spec = dict( + name=dict(), + search=dict(), + organization=dict(type='entity'), + location=dict(type='entity'), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + mutually_exclusive = kwargs.pop('mutually_exclusive', []) + if not foreman_spec['name'].get('invisible', False): + mutually_exclusive.extend([['name', 'search']]) + super(ForemanInfoAnsibleModule, self).__init__(foreman_spec=foreman_spec, mutually_exclusive=mutually_exclusive, **kwargs) + + def run(self, **kwargs): + """ + lookup entities + """ + self.auto_lookup_entities() + + resource = self.foreman_spec['entity']['resource_type'] + + if 'name' in self.foreman_params: + self._info_result = {self.entity_name: self.lookup_entity('entity')} + else: + _flat_entity = _flatten_entity(self.foreman_params, self.foreman_spec) + self._info_result = {resource: self.list_resource(resource, self.foreman_params.get('search'), _flat_entity)} + + def exit_json(self, **kwargs): + kwargs.update(self._info_result) + super(ForemanInfoAnsibleModule, self).exit_json(**kwargs) + + +class ForemanEntityAnsibleModule(ForemanStatelessEntityAnsibleModule): + """ Base class for Foreman entities. To use it, subclass it with the following convention: + To manage my_entity entity, create the following sub class:: + + class ForemanMyEntityModule(ForemanEntityAnsibleModule): + pass + + and use that class to instantiate module:: + + module = ForemanMyEntityModule( + argument_spec=dict( + [...] + ), + foreman_spec=dict( + [...] + ), + ) + + This adds a `state` parameter to the module and provides the `run` method for the most + common usecases. + """ + + def __init__(self, **kwargs): + argument_spec = dict( + state=dict(choices=['present', 'absent'], default='present'), + ) + argument_spec.update(kwargs.pop('argument_spec', {})) + super(ForemanEntityAnsibleModule, self).__init__(argument_spec=argument_spec, **kwargs) + + self.state = self.foreman_params.pop('state') + self.desired_absent = self.state == 'absent' + self._thin_default = self.desired_absent + + def run(self, **kwargs): + """ lookup entities, ensure entity, remove sensitive data, manage parameters. + """ + parent_name = self.foreman_params.get('parent') + if ('parent' in self.foreman_spec and self.foreman_spec['parent'].get('type') == 'entity' + and 'parent' in self.foreman_params and self.lookup_entity('parent') is None): + if self.desired_absent: + # Parent does not exist so just exit here + return None + else: + self.fail_json(msg="Couldn't find parent '{0}' for '{1}'.".format(parent_name, self.foreman_params['name'])) + if not self.desired_absent: + self.auto_lookup_entities() + entity = self.lookup_entity('entity') + + if not self.desired_absent: + updated_key = "updated_" + self.entity_key + if entity and updated_key in self.foreman_params: + self.foreman_params[self.entity_key] = self.foreman_params.pop(updated_key) + + params = kwargs.get('params', {}) + for scope in self.foreman_spec['entity'].get('scope', []): + params.update(self.scope_for(scope)) + for optional_scope in self.foreman_spec['entity'].get('optional_scope', []): + if optional_scope in self.foreman_params: + params.update(self.scope_for(optional_scope)) + new_entity = self.ensure_entity(self.foreman_spec['entity']['resource_type'], self.foreman_params, entity, params=params) + new_entity = self.remove_sensitive_fields(new_entity) + + return new_entity + + def remove_sensitive_fields(self, entity): + """ Set fields with 'no_log' option to None """ + if entity: + for blacklisted_field in self.blacklisted_fields: + entity[blacklisted_field] = None + return entity + + @property + def blacklisted_fields(self): + return [key for key, value in self.foreman_spec.items() if value.get('no_log', False)] + + +class ForemanTaxonomicAnsibleModule(TaxonomyMixin, ForemanAnsibleModule): + """ + Combine :class:`ForemanAnsibleModule` with the :class:`TaxonomyMixin` Mixin. + """ + + pass + + +class ForemanTaxonomicEntityAnsibleModule(TaxonomyMixin, ForemanEntityAnsibleModule): + """ + Combine :class:`ForemanEntityAnsibleModule` with the :class:`TaxonomyMixin` Mixin. + """ + + pass + + +class ForemanScapDataStreamModule(ForemanTaxonomicEntityAnsibleModule): + def __init__(self, **kwargs): + foreman_spec = dict( + original_filename=dict(type='str'), + scap_file=dict(type='path'), + ) + foreman_spec.update(kwargs.pop('foreman_spec', {})) + super(ForemanScapDataStreamModule, self).__init__(foreman_spec=foreman_spec, **kwargs) + + def run(self, **kwargs): + entity = self.lookup_entity('entity') + + if not self.desired_absent: + if not entity and 'scap_file' not in self.foreman_params: + self.fail_json(msg="Content of scap_file not provided. XML containing SCAP content is required.") + + if 'scap_file' in self.foreman_params and 'original_filename' not in self.foreman_params: + self.foreman_params['original_filename'] = os.path.basename(self.foreman_params['scap_file']) + + if 'scap_file' in self.foreman_params: + with open(self.foreman_params['scap_file']) as input_file: + self.foreman_params['scap_file'] = input_file.read() + + if entity and 'scap_file' in self.foreman_params: + digest = hashlib.sha256(self.foreman_params['scap_file'].encode("utf-8")).hexdigest() + # workaround for https://projects.theforeman.org/issues/29409 + digest_stripped = hashlib.sha256(self.foreman_params['scap_file'].strip().encode("utf-8")).hexdigest() + if entity['digest'] in [digest, digest_stripped]: + self.foreman_params.pop('scap_file') + + return super(ForemanScapDataStreamModule, self).run(**kwargs) + + +class KatelloAnsibleModule(KatelloMixin, ForemanAnsibleModule): + """ + Combine :class:`ForemanAnsibleModule` with the :class:`KatelloMixin` Mixin. + """ + + pass + + +class KatelloScopedMixin(KatelloMixin): + """ + Enhances :class:`KatelloMixin` with scoping by ``organization`` as required by Katello. + """ + + def __init__(self, **kwargs): + entity_opts = kwargs.pop('entity_opts', {}) + if 'scope' not in entity_opts: + entity_opts['scope'] = ['organization'] + elif 'organization' not in entity_opts['scope']: + entity_opts['scope'].append('organization') + super(KatelloScopedMixin, self).__init__(entity_opts=entity_opts, **kwargs) + + +class KatelloInfoAnsibleModule(KatelloScopedMixin, ForemanInfoAnsibleModule): + """ + Combine :class:`ForemanInfoAnsibleModule` with the :class:`KatelloScopedMixin` Mixin. + """ + + pass + + +class KatelloEntityAnsibleModule(KatelloScopedMixin, ForemanEntityAnsibleModule): + """ + Combine :class:`ForemanEntityAnsibleModule` with the :class:`KatelloScopedMixin` Mixin. + """ + + pass + + +def _foreman_spec_helper(spec): + """Extend an entity spec by adding entries for all flat_names. + Extract Ansible compatible argument_spec on the way. + """ + foreman_spec = {} + argument_spec = {} + + _FILTER_SPEC_KEYS = { + 'ensure', + 'failsafe', + 'flat_name', + 'foreman_spec', + 'invisible', + 'optional_scope', + 'resolve', + 'resource_type', + 'scope', + 'search_by', + 'search_operator', + 'thin', + 'type', + } + _VALUE_SPEC_KEYS = { + 'ensure', + 'type', + } + _ENTITY_SPEC_KEYS = { + 'failsafe', + 'optional_scope', + 'resolve', + 'resource_type', + 'scope', + 'search_by', + 'search_operator', + 'thin', + } + + # _foreman_spec_helper() is called before we call check_requirements() in the __init__ of ForemanAnsibleModule + # and thus before the if HAS APYPIE check happens. + # We have to ensure that apypie is available before using it. + # There is two cases where we can call _foreman_spec_helper() without apypie available: + # * When the user calls the module but doesn't have the right Python libraries installed. + # In this case nothing will works and the module will warn the user to install the required library. + # * When Ansible generates docs from the argument_spec. As the inflector is only used to build foreman_spec and not argument_spec, + # This is not a problem. + # + # So in conclusion, we only have to verify that apypie is available before using it. + # Lazy evaluation helps there. + for key, value in spec.items(): + foreman_value = {k: v for (k, v) in value.items() if k in _VALUE_SPEC_KEYS} + argument_value = {k: v for (k, v) in value.items() if k not in _FILTER_SPEC_KEYS} + + foreman_type = value.get('type') + ansible_invisible = value.get('invisible', False) + flat_name = value.get('flat_name') + + if foreman_type == 'entity': + if not flat_name: + flat_name = '{0}_id'.format(key) + foreman_value['resource_type'] = HAS_APYPIE and inflector.pluralize(key) + foreman_value.update({k: v for (k, v) in value.items() if k in _ENTITY_SPEC_KEYS}) + elif foreman_type == 'entity_list': + argument_value['type'] = 'list' + argument_value['elements'] = value.get('elements', 'str') + if not flat_name: + flat_name = '{0}_ids'.format(HAS_APYPIE and inflector.singularize(key)) + foreman_value['resource_type'] = key + foreman_value.update({k: v for (k, v) in value.items() if k in _ENTITY_SPEC_KEYS}) + elif foreman_type == 'nested_list': + argument_value['type'] = 'list' + argument_value['elements'] = 'dict' + foreman_value['foreman_spec'], argument_value['options'] = _foreman_spec_helper(value['foreman_spec']) + foreman_value['ensure'] = value.get('ensure', False) + elif foreman_type: + argument_value['type'] = foreman_type + + if flat_name: + foreman_value['flat_name'] = flat_name + foreman_spec[flat_name] = {} + # When translating to a flat name, the flattened entry should get the same "type" + # as Ansible expects so that comparison still works for non-strings + if argument_value.get('type') is not None: + foreman_spec[flat_name]['type'] = argument_value['type'] + + foreman_spec[key] = foreman_value + + if not ansible_invisible: + argument_spec[key] = argument_value + + return foreman_spec, argument_spec + + +def _flatten_entity(entity, foreman_spec): + """Flatten entity according to spec""" + result = {} + if entity is None: + entity = {} + for key, value in entity.items(): + if key in foreman_spec and foreman_spec[key].get('ensure', True) and value is not None: + spec = foreman_spec[key] + flat_name = spec.get('flat_name', key) + property_type = spec.get('type', 'str') + if property_type == 'entity': + if value is not NoEntity: + result[flat_name] = value['id'] + else: + result[flat_name] = None + elif property_type == 'entity_list': + result[flat_name] = sorted(val['id'] for val in value) + elif property_type == 'nested_list': + result[flat_name] = [_flatten_entity(ent, foreman_spec[key]['foreman_spec']) for ent in value] + else: + result[flat_name] = value + return result + + +def _recursive_dict_keys(a_dict): + """Find all keys of a nested dictionary""" + keys = set(a_dict.keys()) + for _k, v in a_dict.items(): + if isinstance(v, dict): + keys.update(_recursive_dict_keys(v)) + return keys + + +def _recursive_dict_without_none(a_dict, exclude=None): + """ + Remove all entries with `None` value from a dict, recursively. + Also drops all entries with keys in `exclude` in the top level. + """ + if exclude is None: + exclude = [] + + result = {} + + for (k, v) in a_dict.items(): + if v is not None and k not in exclude: + if isinstance(v, dict): + v = _recursive_dict_without_none(v) + elif isinstance(v, list) and v and isinstance(v[0], dict): + v = [_recursive_dict_without_none(element) for element in v] + result[k] = v + + return result + + +def _is_resolved(spec, what): + if spec.get('type') not in ('entity', 'entity_list'): + return True + + if spec.get('type') == 'entity' and (what is None or isinstance(what, dict)): + return True + + if spec.get('type') == 'entity_list' and isinstance(what, list) and what and (what[0] is None or isinstance(what[0], dict)): + return True + + return False + + +# Helper for (global, operatingsystem, ...) parameters +def parameter_value_to_str(value, parameter_type): + """Helper to convert the value of parameters to string according to their parameter_type.""" + if parameter_type in ['real', 'integer']: + parameter_string = str(value) + elif parameter_type in ['array', 'hash', 'yaml', 'json']: + parameter_string = json.dumps(value, sort_keys=True) + else: + parameter_string = value + return parameter_string + + +# Helper for converting lists of parameters +def parameters_list_to_str_list(parameters): + filtered_params = [] + for param in parameters: + new_param = {k: v for (k, v) in param.items() if k in parameter_ansible_spec.keys()} + new_param['value'] = parameter_value_to_str(new_param['value'], new_param['parameter_type']) + filtered_params.append(new_param) + return filtered_params + + +# Helper for templates +def parse_template(template_content, module): + if not HAS_PYYAML: + module.fail_json(msg=missing_required_lib("PyYAML"), exception=PYYAML_IMP_ERR) + + try: + template_dict = {} + data = re.search( + r'<%#([^%]*([^%]*%*[^>%])*%*)%>', template_content) + if data: + datalist = data.group(1) + if datalist[-1] == '-': + datalist = datalist[:-1] + template_dict = yaml.safe_load(datalist) + # No metadata, import template anyway + template_dict['template'] = template_content + except Exception as e: + module.fail_json(msg='Error while parsing template: ' + to_native(e)) + return template_dict + + +def parse_template_from_file(file_name, module): + try: + with open(file_name) as input_file: + template_content = input_file.read() + template_dict = parse_template(template_content, module) + except Exception as e: + module.fail_json(msg='Error while reading template file: ' + to_native(e)) + return template_dict + + +# Helper for titles +def split_fqn(title): + """ Split fully qualified name (title) in name and parent title """ + fqn = title.split('/') + if len(fqn) > 1: + name = fqn.pop() + return (name, '/'.join(fqn)) + else: + return (title, None) + + +def build_fqn(name, parent=None): + if parent: + return "%s/%s" % (parent, name) + else: + return name + + +# Helper for puppetclasses +def ensure_puppetclasses(module, entity_type, entity, expected_puppetclasses=None): + if expected_puppetclasses is not None: + puppetclasses_resource = '{0}_classes'.format(entity_type) + expected_puppetclasses = module.find_puppetclasses(expected_puppetclasses, environment=entity['environment_id'], thin=True) + current_puppetclasses = entity.get('puppetclasses', []) + current_puppetclass_ids = [pc['id'] for pc in current_puppetclasses] + previous_puppetclass_ids = current_puppetclass_ids[:] + for puppetclass in expected_puppetclasses: + if puppetclass['id'] in current_puppetclass_ids: + # Nothing to do, prevent removal + previous_puppetclass_ids.remove(puppetclass['id']) + else: + payload = {'{0}_id'.format(entity_type): entity['id'], 'puppetclass_id': puppetclass['id']} + module.ensure_entity(puppetclasses_resource, {}, None, params=payload, state='present', foreman_spec={}) + # Add to entity for reporting + current_puppetclass_ids.append(puppetclass['id']) + + for leftover_puppetclass in previous_puppetclass_ids: + payload = {'{0}_id'.format(entity_type): entity['id']} + module.ensure_entity( + puppetclasses_resource, {}, {'id': leftover_puppetclass}, + params=payload, state='absent', foreman_spec={}, + ) + current_puppetclass_ids.remove(leftover_puppetclass) + entity['puppetclass_ids'] = current_puppetclass_ids + + +# Helper constants +OS_LIST = ['AIX', + 'Altlinux', + 'Archlinux', + 'Coreos', + 'Debian', + 'Fcos', + 'Freebsd', + 'Gentoo', + 'Junos', + 'NXOS', + 'Rancheros', + 'Redhat', + 'Rhcos', + 'Solaris', + 'Suse', + 'VRP', + 'Windows', + 'Xenserver', + ] + +TEMPLATE_KIND_LIST = [ + 'Bootdisk', + 'cloud-init', + 'finish', + 'host_init_config', + 'iPXE', + 'job_template', + 'kexec', + 'POAP', + 'provision', + 'PXEGrub', + 'PXEGrub2', + 'PXELinux', + 'registration', + 'script', + 'user_data', + 'ZTP', +] + +# interface specs +interfaces_spec = dict( + id=dict(invisible=True), + mac=dict(), + ip=dict(), + ip6=dict(), + type=dict(choices=['interface', 'bmc', 'bond', 'bridge']), + name=dict(), + subnet=dict(type='entity'), + subnet6=dict(type='entity', resource_type='subnets'), + domain=dict(type='entity'), + identifier=dict(), + managed=dict(type='bool'), + primary=dict(type='bool'), + provision=dict(type='bool'), + username=dict(), + password=dict(no_log=True), + provider=dict(choices=['IPMI', 'Redfish', 'SSH']), + virtual=dict(type='bool'), + tag=dict(), + mtu=dict(type='int'), + attached_to=dict(), + mode=dict(choices=[ + 'balance-rr', + 'active-backup', + 'balance-xor', + 'broadcast', + '802.3ad', + 'balance-tlb', + 'balance-alb', + ]), + attached_devices=dict(type='list', elements='str'), + bond_options=dict(), + compute_attributes=dict(type='dict'), +) diff --git a/ansible_collections/theforeman/foreman/plugins/modules/activation_key.py b/ansible_collections/theforeman/foreman/plugins/modules/activation_key.py new file mode 100644 index 00000000..0684cc25 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/activation_key.py @@ -0,0 +1,398 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Andrew Kofink <ajkofink@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: activation_key +version_added: 1.0.0 +short_description: Manage Activation Keys +description: + - Create and manage activation keys +author: "Andrew Kofink (@akofink)" +options: + name: + description: + - Name of the activation key + required: true + type: str + description: + description: + - Description of the activation key + type: str + lifecycle_environment: + description: + - Name of the lifecycle environment + type: str + content_view: + description: + - Name of the content view + type: str + subscriptions: + description: + - List of subscriptions that include either Name, Pool ID, or Upstream Pool ID. + - Pool IDs are preferred since Names and Upstream Pool IDs are not guaranteed to be unique. The module will fail if it finds more than one match. + type: list + elements: dict + suboptions: + name: + description: + - Name of the Subscription to be added. + - Mutually exclusive with I(pool_id) and I(upstream_pool_id). + type: str + required: false + pool_id: + description: + - Pool ID of the Subscription to be added. + - Mutually exclusive with I(name) and I(upstream_pool_id). + - Also named C(Candlepin Id) in the CSV export of the subscriptions, + - it is as well the C(UUID) as output by C(hammer subscription list). + type: str + required: false + upstream_pool_id: + description: + - Upstream Pool ID of the Subscription to be added. + - Mutually exclusive with I(name) and I(pool_id). + - Also named C(Master Pools) in the Red Hat Portal. + type: str + required: false + host_collections: + description: + - List of host collections to add to activation key + type: list + elements: str + content_overrides: + description: + - List of content overrides that include label and override state + - Label refers to repository C(content_label), e.g. rhel-7-server-rpms + - Override state ('enabled', 'disabled', or 'default') sets initial state of repository for newly registered hosts + type: list + elements: dict + suboptions: + label: + description: + - Repository C(content_label) to override when registering hosts with the activation key + type: str + required: true + override: + description: + - Override value to use for the repository when registering hosts with the activation key + choices: + - enabled + - disabled + - default + type: str + required: true + auto_attach: + description: + - Set Auto-Attach on or off + type: bool + release_version: + description: + - Set the content release version + type: str + service_level: + description: + - Set the service level + choices: + - Self-Support + - Standard + - Premium + type: str + max_hosts: + description: + - Maximum number of registered content hosts. + - Required if I(unlimited_hosts=false) + type: int + unlimited_hosts: + description: + - Can the activation key have unlimited hosts + type: bool + purpose_usage: + description: + - Sets the system purpose usage + type: str + purpose_role: + description: + - Sets the system purpose role + type: str + purpose_addons: + description: + - Sets the system purpose add-ons + type: list + elements: str + state: + description: + - State of the Activation Key + - If C(copied) the key will be copied to a new one with I(new_name) as the name and all other fields left untouched + - C(present_with_defaults) will ensure the entity exists, but won't update existing ones + default: present + choices: + - present + - present_with_defaults + - absent + - copied + type: str + new_name: + description: + - Name of the new activation key when state == copied + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create client activation key" + theforeman.foreman.activation_key: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Clients" + organization: "Default Organization" + lifecycle_environment: "Library" + content_view: 'client content view' + host_collections: + - rhel7-servers + - rhel7-production + subscriptions: + - pool_id: "8a88e9826db22df5016dd018abdd029b" + - pool_id: "8a88e9826db22df5016dd01a23270344" + - name: "Red Hat Enterprise Linux" + content_overrides: + - label: rhel-7-server-optional-rpms + override: enabled + auto_attach: False + release_version: 7Server + service_level: Standard +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + activation_keys: + description: List of activation keys. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule, PER_PAGE + + +def override_to_boolnone(override): + value = None + if isinstance(override, bool): + value = override + else: + override = override.lower() + if override == 'enabled': + value = True + elif override == 'disabled': + value = False + elif override == 'default': + value = None + return value + + +class KatelloActivationKeyModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloActivationKeyModule( + foreman_spec=dict( + name=dict(required=True), + new_name=dict(), + description=dict(), + lifecycle_environment=dict(type='entity', flat_name='environment_id', scope=['organization']), + content_view=dict(type='entity', scope=['organization']), + host_collections=dict(type='entity_list', scope=['organization']), + auto_attach=dict(type='bool'), + release_version=dict(), + service_level=dict(choices=['Self-Support', 'Standard', 'Premium']), + max_hosts=dict(type='int'), + unlimited_hosts=dict(type='bool'), + purpose_usage=dict(), + purpose_role=dict(), + purpose_addons=dict(type='list', elements='str'), + ), + argument_spec=dict( + subscriptions=dict(type='list', elements='dict', options=dict( + name=dict(), + pool_id=dict(), + upstream_pool_id=dict(), + ), + required_one_of=[['name', 'pool_id', 'upstream_pool_id']], + mutually_exclusive=[['name', 'pool_id', 'upstream_pool_id']], + ), + content_overrides=dict(type='list', elements='dict', options=dict( + label=dict(required=True), + override=dict(required=True, choices=['enabled', 'disabled', 'default']), + )), + state=dict(default='present', choices=['present', 'present_with_defaults', 'absent', 'copied']), + ), + required_if=[ + ['state', 'copied', ['new_name']], + ['unlimited_hosts', False, ['max_hosts']], + ], + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + scope = module.scope_for('organization') + + if module.state == 'copied': + new_entity = module.find_resource_by_name('activation_keys', name=module.foreman_params['new_name'], params=scope, failsafe=True) + if new_entity is not None: + module.warn("Activation Key '{0}' already exists.".format(module.foreman_params['new_name'])) + module.exit_json() + + subscriptions = module.foreman_params.pop('subscriptions', None) + content_overrides = module.foreman_params.pop('content_overrides', None) + if not module.desired_absent: + module.lookup_entity('host_collections') + host_collections = module.foreman_params.pop('host_collections', None) + activation_key = module.run() + + # only update subscriptions of newly created or updated AKs + # copied keys inherit the subscriptions of the origin, so one would not have to specify them again + # deleted keys don't need subscriptions anymore either + if module.state == 'present' or (module.state == 'present_with_defaults' and module.changed): + # the auto_attach, release_version and service_level parameters can only be set on an existing AK with an update, + # not during create, so let's force an update. see https://projects.theforeman.org/issues/27632 for details + if any(key in module.foreman_params for key in ['auto_attach', 'release_version', 'service_level']) and module.changed: + activation_key = module.ensure_entity('activation_keys', module.foreman_params, activation_key, params=scope) + + ak_scope = {'activation_key_id': activation_key['id']} + ak_scope.update(scope) + if subscriptions is not None: + desired_subscriptions = [] + for subscription in subscriptions: + if subscription.get('name') is not None: + desired_subscriptions.append(module.find_resource_by_name('subscriptions', subscription['name'], params=scope, thin=True)) + if subscription.get('pool_id') is not None: + desired_subscriptions.append(module.find_resource_by_id('subscriptions', subscription['pool_id'], params=scope, thin=True)) + if subscription.get('upstream_pool_id') is not None: + desired_subscriptions.append( + module.find_resource_by('subscriptions', 'upstream_pool_id', subscription['upstream_pool_id'], params=scope, thin=True) + ) + desired_subscription_ids = set(item['id'] for item in desired_subscriptions) + current_subscriptions = module.list_resource('subscriptions', params=ak_scope) if entity else [] + current_subscription_ids = set(item['id'] for item in current_subscriptions) + + if desired_subscription_ids != current_subscription_ids: + module.record_before('activation_keys/subscriptions', {'id': activation_key['id'], 'subscriptions': current_subscription_ids}) + module.record_after('activation_keys/subscriptions', {'id': activation_key['id'], 'subscriptions': desired_subscription_ids}) + module.record_after_full('activation_keys/subscriptions', {'id': activation_key['id'], 'subscriptions': desired_subscription_ids}) + + ids_to_remove = current_subscription_ids - desired_subscription_ids + if ids_to_remove: + payload = { + 'id': activation_key['id'], + 'subscriptions': [{'id': item} for item in ids_to_remove], + } + payload.update(scope) + module.resource_action('activation_keys', 'remove_subscriptions', payload) + + ids_to_add = desired_subscription_ids - current_subscription_ids + if ids_to_add: + payload = { + 'id': activation_key['id'], + 'subscriptions': [{'id': item, 'quantity': 1} for item in ids_to_add], + } + payload.update(scope) + module.resource_action('activation_keys', 'add_subscriptions', payload) + + if content_overrides is not None: + if entity: + product_content = module.resource_action( + 'activation_keys', + 'product_content', + params={'id': activation_key['id'], + 'content_access_mode_all': True, + 'per_page': PER_PAGE}, + ignore_check_mode=True, + ) + else: + product_content = {'results': []} + current_content_overrides = { + product['content']['label']: product['enabled_content_override'] + for product in product_content['results'] + if product['enabled_content_override'] is not None + } + desired_content_overrides = { + product['label']: override_to_boolnone(product['override']) for product in content_overrides + } + changed_content_overrides = [] + + module.record_before('activation_keys/content_overrides', {'id': activation_key['id'], 'content_overrides': current_content_overrides.copy()}) + module.record_after('activation_keys/content_overrides', {'id': activation_key['id'], 'content_overrides': desired_content_overrides}) + module.record_after_full('activation_keys/content_overrides', {'id': activation_key['id'], 'content_overrides': desired_content_overrides}) + + for label, override in desired_content_overrides.items(): + if override is not None and override != current_content_overrides.pop(label, None): + changed_content_overrides.append({'content_label': label, 'value': override}) + for label in current_content_overrides.keys(): + changed_content_overrides.append({'content_label': label, 'remove': True}) + + if changed_content_overrides: + payload = { + 'id': activation_key['id'], + 'content_overrides': changed_content_overrides, + } + module.resource_action('activation_keys', 'content_override', payload) + + if host_collections is not None: + if not entity: + current_host_collection_ids = set() + elif 'host_collection_ids' in activation_key: + current_host_collection_ids = set(activation_key['host_collection_ids']) + else: + current_host_collection_ids = set(item['id'] for item in activation_key['host_collections']) + desired_host_collections = host_collections + desired_host_collection_ids = set(item['id'] for item in desired_host_collections) + + if desired_host_collection_ids != current_host_collection_ids: + module.record_before('activation_keys/host_collections', {'id': activation_key['id'], 'host_collections': current_host_collection_ids}) + module.record_after('activation_keys/host_collections', {'id': activation_key['id'], 'host_collections': desired_host_collection_ids}) + module.record_after_full('activation_keys/host_collections', {'id': activation_key['id'], 'host_collections': desired_host_collection_ids}) + + ids_to_remove = current_host_collection_ids - desired_host_collection_ids + if ids_to_remove: + payload = { + 'id': activation_key['id'], + 'host_collection_ids': list(ids_to_remove), + } + module.resource_action('activation_keys', 'remove_host_collections', payload) + + ids_to_add = desired_host_collection_ids - current_host_collection_ids + if ids_to_add: + payload = { + 'id': activation_key['id'], + 'host_collection_ids': list(ids_to_add), + } + module.resource_action('activation_keys', 'add_host_collections', payload) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/architecture.py b/ansible_collections/theforeman/foreman/plugins/modules/architecture.py new file mode 100644 index 00000000..c3fd5b68 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/architecture.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: architecture +version_added: 1.0.0 +short_description: Manage Architectures +description: + - Create, update, and delete Architectures +author: + - "Manisha Singhal (@Manisha15) ATIX AG" +options: + name: + description: Name of architecture + required: true + type: str + updated_name: + description: New architecture name. When this parameter is set, the module will not be idempotent. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.operatingsystems +''' + +EXAMPLES = ''' +- name: "Create an Architecture" + theforeman.foreman.architecture: + name: "i386" + operatingsystems: + - "TestOS1" + - "TestOS2" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: "Update an Architecture" + theforeman.foreman.architecture: + name: "i386" + operatingsystems: + - "TestOS3" + - "TestOS4" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: "Delete an Architecture" + theforeman.foreman.architecture: + name: "i386" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + architectures: + description: List of architectures. + type: list + elements: dict + contains: + id: + description: Database id of the architecture. + type: int + name: + description: Name of the architecture. + type: str + operatinsystem_ids: + description: Database ids of associated operatingsystems. + type: list + elements: int +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanArchitectureModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanArchitectureModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + operatingsystems=dict(type='entity_list'), + ), + ) + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/auth_source_ldap.py b/ansible_collections/theforeman/foreman/plugins/modules/auth_source_ldap.py new file mode 100644 index 00000000..466f76d9 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/auth_source_ldap.py @@ -0,0 +1,232 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Christoffer Reijer (Basalt AB) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: auth_source_ldap +version_added: 1.0.0 +short_description: Manage LDAP Authentication Sources +description: + - Create, update, and delete LDAP authentication sources +author: + - "Christoffer Reijer (@ephracis) Basalt AB" +options: + name: + description: The name of the LDAP authentication source + required: true + type: str + host: + description: The hostname of the LDAP server + required: true + type: str + port: + description: The port number of the LDAP server + required: false + type: int + default: 389 + account: + description: Account name to use when accessing the LDAP server. + required: false + type: str + account_password: + description: + - Account password to use when accessing the LDAP server. + - Required when using I(onthefly_register). + - When this parameter is set, the module will not be idempotent. + required: false + type: str + base_dn: + description: The base DN to use when searching. + required: false + type: str + attr_login: + description: + - Attribute containing login ID. + - Required when using I(onthefly_register). + required: false + type: str + attr_firstname: + description: + - Attribute containing first name. + - Required when using I(onthefly_register). + required: false + type: str + attr_lastname: + description: + - Attribute containing last name. + - Required when using I(onthefly_register). + required: false + type: str + attr_mail: + description: + - Attribute containing email address. + - Required when using I(onthefly_register). + required: false + type: str + attr_photo: + description: Attribute containing user photo + required: false + type: str + onthefly_register: + description: Whether or not to register users on the fly. + required: false + type: bool + usergroup_sync: + description: Whether or not to sync external user groups on login + required: false + type: bool + tls: + description: Whether or not to use TLS when contacting the LDAP server. + required: false + type: bool + groups_base: + description: Base DN where groups reside. + required: false + type: str + use_netgroups: + description: Whether to use NIS netgroups instead of posix groups, not valid for I(server_type=active_directory) + required: false + type: bool + server_type: + description: Type of the LDAP server + required: false + choices: ["free_ipa", "active_directory", "posix"] + type: str + ldap_filter: + description: Filter to apply to LDAP searches + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: Simple FreeIPA authentication source + theforeman.foreman.auth_source_ldap: + name: "Example LDAP" + host: "ldap.example.org" + server_url: "https://foreman.example.com" + locations: + - "Uppsala" + organizations: + - "Sweden" + username: "admin" + password: "changeme" + state: present + +- name: FreeIPA with automatic registration + theforeman.foreman.auth_source_ldap: + name: "Example LDAP" + host: "ldap.example.org" + onthefly_register: True + account: uid=ansible,cn=sysaccounts,cn=etc,dc=example,dc=com + account_password: secret + base_dn: dc=example,dc=com + groups_base: cn=groups,cn=accounts, dc=example,dc=com + server_type: free_ipa + attr_login: uid + attr_firstname: givenName + attr_lastname: sn + attr_mail: mail + attr_photo: jpegPhoto + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Active Directory with automatic registration + theforeman.foreman.auth_source_ldap: + name: "Example AD" + host: "ad.example.org" + onthefly_register: True + account: EXAMPLE\\ansible + account_password: secret + base_dn: cn=Users,dc=example,dc=com + groups_base: cn=Users,dc=example,dc=com + server_type: active_directory + attr_login: sAMAccountName + attr_firstname: givenName + attr_lastname: sn + attr_mail: mail + ldap_filter: (memberOf=CN=Domain Users,CN=Users,DC=example,DC=com) + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + auth_source_ldaps: + description: List of auth sources for LDAP. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +class ForemanAuthSourceLdapModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanAuthSourceLdapModule( + foreman_spec=dict( + name=dict(required=True), + host=dict(required=True), + port=dict(type='int', default=389), + account=dict(), + account_password=dict(no_log=True), + base_dn=dict(), + attr_login=dict(), + attr_firstname=dict(), + attr_lastname=dict(), + attr_mail=dict(), + attr_photo=dict(), + onthefly_register=dict(type='bool'), + usergroup_sync=dict(type='bool'), + tls=dict(type='bool'), + groups_base=dict(), + server_type=dict(choices=["free_ipa", "active_directory", "posix"]), + ldap_filter=dict(), + use_netgroups=dict(type='bool'), + ), + required_if=[['onthefly_register', True, ['attr_login', 'attr_firstname', 'attr_lastname', 'attr_mail']]], + ) + + # additional parameter checks + if 'use_netgroups' in module.foreman_params and module.foreman_params['server_type'] == 'active_directory': + module.fail_json(msg='use_netgroups cannot be used when server_type=active_directory') + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/bookmark.py b/ansible_collections/theforeman/foreman/plugins/modules/bookmark.py new file mode 100644 index 00000000..c34b7f55 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/bookmark.py @@ -0,0 +1,157 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: bookmark +version_added: 1.0.0 +short_description: Manage Bookmarks +description: + - "Manage Bookmark Entities" +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" + - "Christoffer Reijer (@ephracis) Basalt AB" +options: + name: + description: + - Name of the bookmark + required: true + type: str + controller: + description: + - Controller for the bookmark + required: true + type: str + public: + description: + - Make bookmark available for all users + required: false + default: true + type: bool + query: + description: + - Query of the bookmark + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults +''' + +EXAMPLES = ''' +- name: "Create a Bookmark" + theforeman.foreman.bookmark: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "recent" + controller: "job_invocations" + query: "started_at > '24 hours ago'" + state: present_with_defaults + +- name: "Update a Bookmark" + theforeman.foreman.bookmark: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "recent" + controller: "job_invocations" + query: "started_at > '12 hours ago'" + state: present + +- name: "Delete a Bookmark" + theforeman.foreman.bookmark: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "recent" + controller: "job_invocations" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + bookmarks: + description: List of bookmarks. + type: list + elements: dict + contains: + id: + description: Database id of the bookmark. + type: int + name: + description: Name of the bookmark. + type: str + controller: + description: Controller, the query is performed on. + type: str + query: + description: Query to be performed on the controller. + type: str + public: + description: Publicity of the bookmark. + type: bool + owner_type: + description: Class of the owner entity. + type: str + owner_id: + description: Database id of the owner entity. + type: int +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanBookmarkModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanBookmarkModule( + foreman_spec=dict( + name=dict(required=True), + controller=dict(required=True), + public=dict(default='true', type='bool'), + query=dict(), + ), + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + ), + required_if=( + ['state', 'present', ['query']], + ['state', 'present_with_defaults', ['query']], + ), + ) + + with module.api_connection(): + module.set_entity('entity', module.find_resource( + 'bookmarks', + search='name="{0}",controller="{1}"'.format(module.foreman_params['name'], module.foreman_params['controller']), + failsafe=True, + )) + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/compute_attribute.py b/ansible_collections/theforeman/foreman/plugins/modules/compute_attribute.py new file mode 100644 index 00000000..89894d9e --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/compute_attribute.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: compute_attribute +version_added: 1.0.0 +short_description: Manage Compute Attributes +description: + - "Manage Compute Attributes" + - "This beta version can create, and update compute attributes" +author: + - "Manisha Singhal (@Manisha15) ATIX AG" +options: + compute_resource: + description: + - Name of compute resource + required: true + type: str + compute_profile: + description: + - Name of compute profile + required: true + type: str + vm_attrs: + description: + - Hash containing the data of vm_attrs + required: false + aliases: + - vm_attributes + type: dict +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: "Create compute attribute" + theforeman.foreman.compute_attribute: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + compute_profile: "Test Compute Profile" + compute_resource: "Test Compute Resource" + vm_attrs: + memory_mb: '2048' + cpu: '2' + state: present + +- name: "Update compute attribute" + theforeman.foreman.compute_attribute: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + compute_profile: "Test Compute Profile" + compute_resource: "Test Compute Resource" + vm_attrs: + memory_mb: '1024' + cpu: '1' + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + compute_attributes: + description: List of compute attributes. + type: list + elements: dict + contains: + id: + description: Database id of the compute_attribute. + type: int + compute_profile_id: + description: Database id of the associated compute profile. + type: int + compute_profile_name: + description: Name of the associated compute profile. + type: str + compute_resource_id: + description: Database id of the associated compute resource. + type: int + compute_resource_name: + description: Name of the associated compute resource. + type: str + created_at: + description: Creation date of the compute attribute. + type: str + updated_at: + description: Date of last change to the compute attribute. + type: str + name: + description: Generated friendly name for the compute attribute. + type: str + provider_friendly_name: + description: Name of the provider type of the compute resource. + type: str + attributes: + description: Effective attributes for the given combination of compute profile and resource. + type: dict + vm_attrs: + description: Configured attributes. + type: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanComputeAttributeModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanComputeAttributeModule( + foreman_spec=dict( + compute_profile=dict(required=True, type='entity'), + compute_resource=dict(required=True, type='entity', thin=False), + vm_attrs=dict(type='dict', aliases=['vm_attributes']), + ), + entity_opts=dict(resolve=False), + ) + + with module.api_connection(): + compute_attributes = module.lookup_entity('compute_resource').get('compute_attributes') + compute_profile_id = module.lookup_entity('compute_profile').get('id') + entity = next((item for item in compute_attributes if item.get('compute_profile_id') == compute_profile_id), None) + module.set_entity('entity', entity) + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/compute_profile.py b/ansible_collections/theforeman/foreman/plugins/modules/compute_profile.py new file mode 100644 index 00000000..dc2f3667 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/compute_profile.py @@ -0,0 +1,228 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) Philipp Joos 2017 +# (c) Baptiste Agasse 2019 +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: compute_profile +version_added: 1.0.0 +short_description: Manage Compute Profiles +description: + - Create, update, and delete Compute Profiles +author: + - "Philipp Joos (@philippj)" + - "Baptiste Agasse (@bagasse)" +options: + name: + description: compute profile name + required: true + type: str + updated_name: + description: new compute profile name + required: false + type: str + compute_attributes: + description: Compute attributes related to this compute profile. Some of these attributes are specific to the underlying compute resource type + required: false + type: list + elements: dict + suboptions: + compute_resource: + description: + - Name of the compute resource the attribute should be for + type: str + vm_attrs: + description: + - Hash containing the data of vm_attrs + aliases: + - vm_attributes + type: dict +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: compute profile + theforeman.foreman.compute_profile: + name: example_compute_profile + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: another compute profile + theforeman.foreman.compute_profile: + name: another_example_compute_profile + compute_attributes: + - compute_resource: ovirt_compute_resource1 + vm_attrs: + cluster: 'a96d44a4-f14a-1015-82c6-f80354acdf01' + template: 'c88af4b7-a24a-453b-9ac2-bc647ca2ef99' + instance_type: 'cb8927e7-a404-40fb-a6c1-06cbfc92e077' + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: compute profile2 + theforeman.foreman.compute_profile: + name: example_compute_profile2 + compute_attributes: + - compute_resource: ovirt_compute_resource01 + vm_attrs: + cluster: a96d44a4-f14a-1015-82c6-f80354acdf01 + cores: 1 + sockets: 1 + memory: 1073741824 + ha: 0 + interfaces_attributes: + 0: + name: "" + network: 390666e1-dab3-4c99-9f96-006b2e2fd801 + interface: virtio + volumes_attributes: + 0: + size_gb: 16 + storage_domain: 19c50090-1ab4-4023-a63f-75ee1018ed5e + preallocate: '1' + wipe_after_delete: '0' + interface: virtio_scsi + bootable: 'true' + - compute_resource: libvirt_compute_resource03 + vm_attrs: + cpus: 1 + memory: 2147483648 + nics_attributes: + 0: + type: bridge + bridge: "" + model: virtio + volumes_attributes: + 0: + pool_name: default + capacity: 16G + allocation: 16G + format_type: raw + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Remove compute profile + theforeman.foreman.compute_profile: + name: example_compute_profile2 + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + compute_profiles: + description: List of compute profiles. + type: list + elements: dict + contains: + id: + description: Database id of the compute profile. + type: int + name: + description: Name of the compute profile. + type: str + compute_attributes: + description: Attributes for this compute profile. + type: list +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +compute_attribute_foreman_spec = { + 'id': {'invisible': True}, + 'compute_resource': {'type': 'entity'}, + 'vm_attrs': {'type': 'dict', 'aliases': ['vm_attributes']}, +} + + +class ForemanComputeProfileModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanComputeProfileModule( + foreman_spec=dict( + name=dict(required=True), + compute_attributes=dict(type='nested_list', foreman_spec=compute_attribute_foreman_spec), + ), + argument_spec=dict( + updated_name=dict(), + ), + ) + + compute_attributes = module.foreman_params.pop('compute_attributes', None) + + with module.api_connection(): + entity = module.run() + + # Apply changes on underlying compute attributes only when present + if entity and module.state == 'present' and compute_attributes is not None: + # Update or create compute attributes + scope = {'compute_profile_id': entity['id']} + for ca_module_params in compute_attributes: + ca_module_params['compute_resource'] = module.find_resource_by_name( + 'compute_resources', name=ca_module_params['compute_resource'], failsafe=False, thin=False) + compute_resource = ca_module_params['compute_resource'] + + if 'vm_attrs' in ca_module_params: + if 'cluster' in ca_module_params['vm_attrs']: + cluster = module.find_cluster(ca_module_params['vm_attrs']['cluster'], compute_resource) + ca_module_params['vm_attrs']['cluster'] = cluster['_api_identifier'] + else: + cluster = None + + if 'volumes_attributes' in ca_module_params['vm_attrs']: + for volume in ca_module_params['vm_attrs']['volumes_attributes'].values(): + if 'storage_pod' in volume: + storage_pod = module.find_storage_pod(volume['storage_pod'], compute_resource, cluster) + volume['storage_pod'] = storage_pod['id'] + if 'storage_domain' in volume: + storage_domain = module.find_storage_domain(volume['storage_domain'], compute_resource, cluster) + volume['storage_domain'] = storage_domain['id'] + + if 'interfaces_attributes' in ca_module_params['vm_attrs']: + for interface in ca_module_params['vm_attrs']['interfaces_attributes'].values(): + if 'network' in interface: + network = module.find_network(interface['network'], compute_resource, cluster) + interface['network'] = network['id'] + + ca_entities = ca_module_params['compute_resource'].get('compute_attributes', []) + ca_entity = next((item for item in ca_entities if item.get('compute_profile_id') == entity['id']), None) + module.ensure_entity('compute_attributes', ca_module_params, ca_entity, foreman_spec=compute_attribute_foreman_spec, params=scope) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/compute_resource.py b/ansible_collections/theforeman/foreman/plugins/modules/compute_resource.py new file mode 100644 index 00000000..8c0a03d2 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/compute_resource.py @@ -0,0 +1,485 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) Philipp Joos 2017 +# (c) Baptiste Agasse 2019 +# (c) Mark Hlawatschek 2020 +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: compute_resource +version_added: 1.0.0 +short_description: Manage Compute Resources +description: + - Create, update, and delete Compute Resources +author: + - "Philipp Joos (@philippj)" + - "Baptiste Agasse (@bagasse)" + - "Manisha Singhal (@Manisha15) ATIX AG" + - "Mark Hlawatschek (@hlawatschek) ATIX AG" +options: + name: + description: compute resource name + required: true + type: str + updated_name: + description: new compute resource name + required: false + type: str + description: + description: compute resource description + required: false + type: str + provider: + description: Compute resource provider. Required if I(state=present_with_defaults). + required: false + choices: ["vmware", "libvirt", "ovirt", "proxmox", "EC2", "AzureRm", "GCE"] + type: str + provider_params: + description: Parameter specific to compute resource provider. Required if I(state=present_with_defaults). + required: false + type: dict + suboptions: + url: + description: + - URL of the compute resource + type: str + user: + description: + - Username for the compute resource connection, not valid for I(provider=libvirt) + type: str + password: + description: + - Password for the compute resource connection, not valid for I(provider=libvirt) + type: str + region: + description: + - AWS region, AZURE region + type: str + tenant: + description: + - AzureRM tenant + type: str + app_ident: + description: + - AzureRM client id + type: str + datacenter: + description: + - Datacenter the compute resource is in, not valid for I(provider=libvirt) + type: str + display_type: + description: + - Display type to use for the remote console, only valid for I(provider=libvirt) + type: str + use_v4: + description: + - Use oVirt API v4, only valid for I(provider=ovirt) + type: bool + ovirt_quota: + description: + - oVirt quota ID, only valid for I(provider=ovirt) + type: str + project: + description: + - Project id for I(provider=GCE) + type: str + email: + description: + - Email for I(provider=GCE) + type: str + key_path: + description: + - Certificate path for I(provider=GCE) + type: str + zone: + description: + - zone for I(provider=GCE) + type: str + cloud: + description: + - cloud for I(provider=AzureRm) + type: str + choices: + - azure + - azureusgovernment + - azurechina + - azuregermancloud + version_added: 2.1.0 + sub_id: + description: + - Subscription ID for I(provider=AzureRm) + type: str + version_added: 2.1.0 + ssl_verify_peer: + description: + - verify ssl from provider I(provider=proxmox) + type: bool + caching_enabled: + description: + - enable caching for I(provider=vmware) + type: bool + set_console_password: + description: + - Set a randomly generated password on the display connection for I(provider=vmware) and I(provider=libvirt) + type: bool + version_added: 2.0.0 + keyboard_layout: + description: + - Default VNC Keyboard for I(provider=ovirt) + type: str + version_added: 2.0.0 + choices: + - 'ar' + - 'da' + - 'de' + - 'de-ch' + - 'en-gb' + - 'en-us' + - 'es' + - 'et' + - 'fi' + - 'fo' + - 'fr' + - 'fr-be' + - 'fr-ca' + - 'fr-ch' + - 'hr' + - 'hu' + - 'is' + - 'it' + - 'ja' + - 'lt' + - 'lv' + - 'mk' + - 'nl' + - 'nl-be' + - 'no' + - 'pl' + - 'pt' + - 'pt-br' + - 'ru' + - 'sl' + - 'sv' + - 'th' + - 'tr' + public_key: + description: + - X509 Certification Authorities, only valid for I(provider=ovirt) + type: str + version_added: 2.0.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: Create libvirt compute resource + theforeman.foreman.compute_resource: + name: example_compute_resource + locations: + - Munich + organizations: + - ACME + provider: libvirt + provider_params: + url: qemu+ssh://root@libvirt.example.com/system + display_type: spice + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Update libvirt compute resource + theforeman.foreman.compute_resource: + name: example_compute_resource + description: updated compute resource + locations: + - Munich + organizations: + - ACME + provider: libvirt + provider_params: + url: qemu+ssh://root@libvirt.example.com/system + display_type: spice + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Delete libvirt compute resource + theforeman.foreman.compute_resource: + name: example_compute_resource + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent + +- name: Create vmware compute resource + theforeman.foreman.compute_resource: + name: example_compute_resource + locations: + - Munich + organizations: + - ACME + provider: vmware + provider_params: + caching_enabled: false + url: vsphere.example.com + user: admin + password: secret + datacenter: ax01 + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Create ovirt compute resource + theforeman.foreman.compute_resource: + name: ovirt_compute_resource + locations: + - France/Toulouse + organizations: + - Example Org + provider: ovirt + provider_params: + url: ovirt.example.com + user: ovirt-admin@example.com + password: ovirtsecret + datacenter: aa92fb54-0736-4066-8fa8-b8b9e3bd75ac + ovirt_quota: 24868ab9-c2a1-47c3-87e7-706f17d215ac + use_v4: true + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Create proxmox compute resource + theforeman.foreman.compute_resource: + name: proxmox_compute_resource + locations: + - Munich + organizations: + - ACME + provider: proxmox + provider_params: + url: https://proxmox.example.com:8006/api2/json + user: root@pam + password: secretpassword + ssl_verify_peer: true + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: create EC2 compute resource + theforeman.foreman.compute_resource: + name: EC2_compute_resource + description: EC2 + locations: + - AWS + organizations: + - ACME + provider: EC2 + provider_params: + user: AWS_ACCESS_KEY + password: AWS_SECRET_KEY + region: eu-west-1 + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: create Azure compute resource + theforeman.foreman.compute_resource: + name: AzureRm_compute_resource + description: AzureRm + locations: + - Azure + organizations: + - ACME + provider: AzureRm + provider_params: + sub_id: SUBSCRIPTION_ID + tenant: TENANT_ID + app_ident: CLIENT_ID + password: CLIENT_SECRET + region: westeurope + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: create GCE compute resource + theforeman.foreman.compute_resource: + name: GCE compute resource + description: Google Cloud Engine + locations: + - GCE + organizations: + - ACME + provider: GCE + provider_params: + project: orcharhino + email: myname@atix.de + key_path: "/usr/share/foreman/gce_orcharhino_key.json" + zone: europe-west3-b + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + compute_resources: + description: List of compute resources. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +def get_provider_info(provider): + provider_name = provider.lower() + + if provider_name == 'libvirt': + return 'Libvirt', ['url', 'display_type', 'set_console_password'] + + elif provider_name == 'ovirt': + return 'Ovirt', ['url', 'user', 'password', 'datacenter', 'use_v4', 'ovirt_quota', 'keyboard_layout', 'public_key'] + + elif provider_name == 'proxmox': + return 'Proxmox', ['url', 'user', 'password', 'ssl_verify_peer'] + + elif provider_name == 'vmware': + return 'Vmware', ['url', 'user', 'password', 'datacenter', 'caching_enabled', 'set_console_password'] + + elif provider_name == 'ec2': + return 'EC2', ['user', 'password', 'region'] + + elif provider_name == 'azurerm': + return 'AzureRm', ['user', 'password', 'tenant', 'region', 'app_ident', 'cloud', 'sub_id'] + + elif provider_name == 'gce': + return 'GCE', ['project', 'email', 'key_path', 'zone'] + + else: + return '', [] + + +class ForemanComputeResourceModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanComputeResourceModule( + foreman_spec=dict( + name=dict(required=True), + updated_name=dict(), + description=dict(), + provider=dict(choices=['vmware', 'libvirt', 'ovirt', 'proxmox', 'EC2', 'AzureRm', 'GCE']), + display_type=dict(invisible=True), + datacenter=dict(invisible=True), + url=dict(invisible=True), + caching_enabled=dict(invisible=True), + user=dict(invisible=True), + password=dict(invisible=True), + region=dict(invisible=True), + tenant=dict(invisible=True), + app_ident=dict(invisible=True), + use_v4=dict(invisible=True), + ovirt_quota=dict(invisible=True), + project=dict(invisible=True), + email=dict(invisible=True), + key_path=dict(invisible=True), + zone=dict(invisible=True), + cloud=dict(invisible=True), + ssl_verify_peer=dict(invisible=True), + set_console_password=dict(invisible=True), + keyboard_layout=dict(invisible=True), + public_key=dict(invisible=True), + sub_id=dict(invisible=True), + ), + argument_spec=dict( + provider_params=dict(type='dict', options=dict( + url=dict(), + display_type=dict(), + user=dict(), + password=dict(no_log=True), + region=dict(), + tenant=dict(), + app_ident=dict(), + datacenter=dict(), + caching_enabled=dict(type='bool'), + use_v4=dict(type='bool'), + ovirt_quota=dict(), + project=dict(), + email=dict(), + key_path=dict(no_log=False), + zone=dict(), + cloud=dict(choices=['azure', 'azureusgovernment', 'azurechina', 'azuregermancloud']), + ssl_verify_peer=dict(type='bool'), + set_console_password=dict(type='bool'), + keyboard_layout=dict(choices=['ar', 'de-ch', 'es', 'fo', 'fr-ca', 'hu', 'ja', 'mk', 'no', 'pt-br', 'sv', 'da', 'en-gb', 'et', 'fr', 'fr-ch', + 'is', 'lt', 'nl', 'pl', 'ru', 'th', 'de', 'en-us', 'fi', 'fr-be', 'hr', 'it', 'lv', 'nl-be', 'pt', 'sl', 'tr']), + public_key=dict(), + sub_id=dict(), + ), + mutually_exclusive=[['user', 'sub_id']], + ), + state=dict(type='str', default='present', choices=['present', 'absent', 'present_with_defaults']), + ), + required_if=( + ['state', 'present_with_defaults', ['provider', 'provider_params']], + ), + ) + + if not module.desired_absent: + if 'provider' in module.foreman_params: + module.foreman_params['provider'], provider_param_keys = get_provider_info(provider=module.foreman_params['provider']) + provider_params = module.foreman_params.pop('provider_params', {}) + + if module.foreman_params['provider'] == 'AzureRm' and 'user' in provider_params: + provider_params['sub_id'] = provider_params.pop('user') + for key in provider_param_keys: + if key in provider_params: + module.foreman_params[key] = provider_params.pop(key) + if provider_params: + module.fail_json(msg="Provider {0} does not support the following given parameters: {1}".format( + module.foreman_params['provider'], list(provider_params.keys()))) + + with module.api_connection(): + entity = module.lookup_entity('entity') + if not module.desired_absent and 'provider' not in module.foreman_params and entity is None: + module.fail_json(msg='To create a compute resource a valid provider must be supplied') + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/config_group.py b/ansible_collections/theforeman/foreman/plugins/modules/config_group.py new file mode 100644 index 00000000..844b9894 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/config_group.py @@ -0,0 +1,97 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Baptiste Agasse +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: config_group +version_added: 1.0.0 +short_description: Manage (Puppet) Config Groups +description: + - Create, update, and delete (Puppet) config groups +author: + - "Baptiste Agasse (@bagasse)" +options: + name: + description: The config group name + required: true + type: str + updated_name: + description: New config group name. When this parameter is set, the module will not be idempotent. + type: str + puppetclasses: + description: List of puppet classes to include in this group + required: false + type: list + elements: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: create new config group + theforeman.foreman.config_group: + name: "My config group" + puppetclasses: + - ntp + - mymodule::myclass + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + config_groups: + description: List of config groups. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanConfigGroupModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanConfigGroupModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + puppetclasses=dict(type='entity_list'), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_credential.py b/ansible_collections/theforeman/foreman/plugins/modules/content_credential.py new file mode 100644 index 00000000..ae2b3230 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_credential.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018, Baptiste Agasse <baptiste.agagsse@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_credential +version_added: 1.0.0 +short_description: Manage Content Credentials +description: + - Create and manage content credentials +author: "Baptiste Agasse (@bagasse)" +options: + name: + description: + - Name of the content credential + required: true + type: str + content_type: + description: + - Type of credential + choices: + - gpg_key + - cert + required: true + type: str + content: + description: + - Content of the content credential + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create katello client GPG key" + theforeman.foreman.content_credential: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "RPM-GPG-KEY-my-repo" + content_type: gpg_key + organization: "Default Organization" + content: "{{ lookup('file', 'RPM-GPG-KEY-my-repo') }}" +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + content_credentials: + description: List of content credentials. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloContentCredentialModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloContentCredentialModule( + foreman_spec=dict( + name=dict(required=True), + content_type=dict(required=True, choices=['gpg_key', 'cert']), + content=dict(required=True), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_export_info.py b/ansible_collections/theforeman/foreman/plugins/modules/content_export_info.py new file mode 100644 index 00000000..84f9eefb --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_export_info.py @@ -0,0 +1,149 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Jeremy Lenz <jlenz@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_export_info +version_added: 3.5.0 +short_description: List content exports +description: + - List information about content exports. +author: + - "Jeremy Lenz (@jeremylenz)" +options: + id: + description: + - Export history identifier. + required: false + type: int + content_view_version: + description: + - Content view version. + required: false + type: str + content_view: + description: + - Content view name. + required: false + type: str + destination_server: + description: + - Destination server name + required: false + type: str + type: + description: + - Specify complete or incremental exports. + required: false + type: str + choices: + - complete + - incremental +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodulewithoutname +''' + +EXAMPLES = ''' +- name: "List all full exports in the organization" + theforeman.foreman.content_export_info: + organization: "Default Organization" + type: complete + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" +- name: "Get a specific export history and register the result for the next task" + vars: + organization_name: "Export Org" + theforeman.foreman.content_export_info: + id: 29 + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + register: result +- name: "Write metadata.json to disk using data from the previous task" + vars: + metadata: "{{ result['content_exports'][0]['metadata'] }}" + ansible.builtin.copy: + content: "{{ metadata }}" + dest: ./metadata.json +- name: "List all exports of a specific content view version" + theforeman.foreman.content_export_info: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" +- name: "List all exports marked for a specific destination server" + theforeman.foreman.content_export_info: + destination_server: "airgapped.example.com" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" +- name: "List incremental exports of a specific content view version marked for a specific destination server" + theforeman.foreman.content_export_info: + content_view: RHEL8 + destination_server: "airgapped.example.com" + type: incremental + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" +- name: "List all exports of a specific content view marked for a specific destination server" + theforeman.foreman.content_export_info: + content_view: RHEL8 + destination_server: "airgapped.example.com" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloInfoAnsibleModule + + +class KatelloContentExportInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloContentExportInfo( + foreman_spec=dict( + id=dict(required=False, type='int'), + content_view_version=dict(type='entity', scope=['content_view'], required=False), + content_view=dict(type='entity', scope=['organization'], required=False), + destination_server=dict(required=False, type='str'), + type=dict(required=False, type='str', choices=['complete', 'incremental']), + name=dict(invisible=True), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_export_library.py b/ansible_collections/theforeman/foreman/plugins/modules/content_export_library.py new file mode 100644 index 00000000..10342687 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_export_library.py @@ -0,0 +1,147 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Jeremy Lenz <jlenz@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_export_library +version_added: 3.5.0 +short_description: Manage library content exports +description: + - Export library content to a directory. +author: + - "Jeremy Lenz (@jeremylenz)" +options: + destination_server: + description: + - Destination server name; optional parameter to differentiate between exports + required: false + type: str + chunk_size_gb: + description: + - Split the exported content into archives no greater than the specified size in gigabytes. + required: false + type: int + fail_on_missing_content: + description: + - Fails if any of the repositories belonging to this organization are unexportable. + required: false + type: bool + incremental: + description: + - Export only the content that has changed since the last export. + required: false + type: bool + from_history_id: + description: + - Export history identifier used for incremental export. If not provided the most recent export history will be used. + required: false + type: int +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Export library content (full)" + theforeman.foreman.content_export_library: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + +- name: "Export library content (full) and fail if any repos are unexportable" + theforeman.foreman.content_export_library: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + fail_on_missing_content: true + +- name: "Export library content (full) in chunks of 10 GB" + theforeman.foreman.content_export_library: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + chunk_size_gb: 10 + organization: "Default Organization" + destination_server: "airgapped.example.com" + +- name: "Export library content (incremental) since the most recent export" + theforeman.foreman.content_export_library: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + incremental: true + +- name: "Export library content (incremental) since a specific export" + theforeman.foreman.content_export_library: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + incremental: true + from_history_id: 12345 +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule, _flatten_entity + + +class KatelloContentExportModule(KatelloAnsibleModule): + pass + + +def main(): + module = KatelloContentExportModule( + foreman_spec=dict( + destination_server=dict(required=False, type='str'), + chunk_size_gb=dict(required=False, type='int'), + fail_on_missing_content=dict(required=False, type='bool'), + from_history_id=dict(required=False, type='int'), + ), + argument_spec=dict( + incremental=dict(required=False, type='bool'), + ), + ) + + module.task_timeout = 12 * 60 * 60 + + with module.api_connection(): + module.auto_lookup_entities() + + incremental = module.params['incremental'] + endpoint = 'content_export_incrementals' if incremental else 'content_exports' + + if module.params.get('from_history_id') and incremental is not True: + module.fail_json(msg='from_history_id is only valid for incremental exports') + + payload = _flatten_entity(module.foreman_params, module.foreman_spec) + task = module.resource_action(endpoint, 'library', payload) + + module.exit_json(task=task) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_export_repository.py b/ansible_collections/theforeman/foreman/plugins/modules/content_export_repository.py new file mode 100644 index 00000000..ff16b248 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_export_repository.py @@ -0,0 +1,142 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Jeremy Lenz <jlenz@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_export_repository +version_added: 3.6.0 +short_description: Manage repository content exports +description: + - Export repository content to a directory. +author: + - "Jeremy Lenz (@jeremylenz)" +options: + repository: + description: + - Name of the repository to export. + required: true + type: str + product: + description: + - Name of the product that the repository belongs to. + required: true + type: str + chunk_size_gb: + description: + - Split the exported content into archives no greater than the specified size in gigabytes. + required: false + type: int + incremental: + description: + - Export only the content that has changed since the last export. + required: false + type: bool + from_history_id: + description: + - Export history identifier used for incremental export. If not provided the most recent export history will be used. + required: false + type: int +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Export repository (full)" + theforeman.foreman.content_export_repository: + product: "Example Product" + repository: "Example Repository" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + +- name: "Export repository (full) in chunks of 10 GB" + theforeman.foreman.content_export_repository: + product: "Example Product" + repository: "Example Repository" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + chunk_size_gb: 10 + +- name: "Export repository (incremental) since the most recent export" + theforeman.foreman.content_export_repository: + product: "Example Product" + repository: "Example Repository" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + incremental: true + +- name: "Export repository (incremental) since a specific export" + theforeman.foreman.content_export_repository: + product: "Example Product" + repository: "Example Repository" + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + incremental: true + from_history_id: 12345 +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule, _flatten_entity + + +class KatelloContentExportModule(KatelloAnsibleModule): + pass + + +def main(): + module = KatelloContentExportModule( + foreman_spec=dict( + repository=dict(type='entity', flat_name='id', scope=['product'], required=True), + product=dict(type='entity', scope=['organization'], required=True), + chunk_size_gb=dict(required=False, type='int'), + from_history_id=dict(required=False, type='int'), + ), + argument_spec=dict( + incremental=dict(required=False, type='bool'), + ), + ) + + module.task_timeout = 12 * 60 * 60 + + with module.api_connection(): + module.auto_lookup_entities() + + incremental = module.params['incremental'] + endpoint = 'content_export_incrementals' if incremental else 'content_exports' + + if module.params.get('from_history_id') and incremental is not True: + module.fail_json(msg='from_history_id is only valid for incremental exports') + + payload = _flatten_entity(module.foreman_params, module.foreman_spec) + task = module.resource_action(endpoint, 'repository', payload) + + module.exit_json(task=task) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_export_version.py b/ansible_collections/theforeman/foreman/plugins/modules/content_export_version.py new file mode 100644 index 00000000..662e9cdd --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_export_version.py @@ -0,0 +1,169 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Jeremy Lenz <jlenz@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_export_version +version_added: 3.6.0 +short_description: Manage content view version content exports +description: + - Export a content view version to a directory. +author: + - "Jeremy Lenz (@jeremylenz)" +options: + content_view_version: + description: + - Content view version, e.g. "7.0" + required: true + type: str + content_view: + description: + - Content view name. + required: true + type: str + destination_server: + description: + - Destination server name; optional parameter to differentiate between exports + required: false + type: str + chunk_size_gb: + description: + - Split the exported content into archives no greater than the specified size in gigabytes. + required: false + type: int + fail_on_missing_content: + description: + - Fails if any of the repositories belonging to this version are unexportable. + required: false + type: bool + incremental: + description: + - Export only the content that has changed since the last export. + required: false + type: bool + from_history_id: + description: + - Export history identifier used for incremental export. If not provided the most recent export history will be used. + required: false + type: int +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Export content view version (full)" + theforeman.foreman.content_export_version: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + +- name: "Export content view version (full) in chunks of 10 GB" + theforeman.foreman.content_export_version: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + chunk_size_gb: 10 + +- name: "Export content view version (full) and fail if any repos are unexportable" + theforeman.foreman.content_export_version: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + fail_on_missing_content: true + +- name: "Export content view version (incremental) since the most recent export" + theforeman.foreman.content_export_version: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + incremental: true + +- name: "Export content view version (incremental) since a specific export" + theforeman.foreman.content_export_version: + content_view: RHEL8 + content_view_version: '1.0' + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + destination_server: "airgapped.example.com" + incremental: true + from_history_id: 12345 +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule, _flatten_entity + + +class KatelloContentExportModule(KatelloAnsibleModule): + pass + + +def main(): + module = KatelloContentExportModule( + foreman_spec=dict( + content_view_version=dict(type='entity', scope=['content_view'], search_by='version', flat_name='id', required=True), + content_view=dict(type='entity', scope=['organization'], required=True), + destination_server=dict(required=False, type='str'), + chunk_size_gb=dict(required=False, type='int'), + fail_on_missing_content=dict(required=False, type='bool'), + from_history_id=dict(required=False, type='int'), + ), + argument_spec=dict( + incremental=dict(required=False, type='bool'), + ), + ) + + module.task_timeout = 12 * 60 * 60 + + with module.api_connection(): + module.auto_lookup_entities() + + incremental = module.params['incremental'] + endpoint = 'content_export_incrementals' if incremental else 'content_exports' + + if module.params.get('from_history_id') and incremental is not True: + module.fail_json(msg='from_history_id is only valid for incremental exports') + + payload = _flatten_entity(module.foreman_params, module.foreman_spec) + task = module.resource_action(endpoint, 'version', payload) + + module.exit_json(task=task) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_upload.py b/ansible_collections/theforeman/foreman/plugins/modules/content_upload.py new file mode 100644 index 00000000..80616f25 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_upload.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# (c) 2018, Sean O'Keeffe <seanokeeffe797@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_upload +version_added: 1.0.0 +short_description: Upload content to a repository +description: + - Allows the upload of content to a repository +author: "Eric D Helms (@ehelms)" +requirements: + - python-debian (For deb Package upload) + - rpm (For rpm upload) +options: + src: + description: + - File (on the remote/target machine) to upload + required: true + type: path + aliases: + - file + repository: + description: + - Repository to upload file in to + required: true + type: str + product: + description: + - Product to which the repository lives in + required: true + type: str + ostree_repository_name: + description: + - Name of repository within the OSTree archive. + - Required for OSTree uploads. + required: false + type: str +notes: + - Currently only uploading to deb, RPM, OSTree & file repositories is supported + - For anything but file repositories, a supporting library must be installed. See Requirements. + - OSTree content upload is not idempotent - running mutliple times will attempt to upload the content unit. +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Upload my.rpm" + theforeman.foreman.content_upload: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + src: "my.rpm" + repository: "Build RPMs" + product: "My Product" + organization: "Default Organization" + +- name: "Upload ostree-archive.tar" + theforeman.foreman.content_upload: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + src: "ostree_archive.tar" + repository: "My OStree Repository" + product: "My Product" + organization: "Default Organization" + ostree_repository_name: "small" +''' + +RETURN = ''' # ''' + +import os +import traceback + +from ansible.module_utils._text import to_bytes, to_native +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule, missing_required_lib + +try: + from debian import debfile + HAS_DEBFILE = True + DEBFILE_IMP_ERR = None +except ImportError: + HAS_DEBFILE = False + DEBFILE_IMP_ERR = traceback.format_exc() + +try: + import rpm + HAS_RPM = True + RPM_IMP_ERR = None +except ImportError: + HAS_RPM = False + RPM_IMP_ERR = traceback.format_exc() + +CONTENT_CHUNK_SIZE = 2 * 1024 * 1024 + + +def get_deb_info(path): + control = debfile.DebFile(path).debcontrol() + return control['package'], control['version'], control['architecture'] + + +def get_rpm_info(path): + ts = rpm.TransactionSet() + + # disable signature checks, we might not have the key or the file might be unsigned + # pre 4.15 RPM needs to use the old name of the bitmask + try: + vsflags = rpm.RPMVSF_MASK_NOSIGNATURES + except AttributeError: + vsflags = rpm._RPMVSF_NOSIGNATURES + ts.setVSFlags(vsflags) + + with open(path) as rpmfile: + rpmhdr = ts.hdrFromFdno(rpmfile) + + epoch = rpmhdr[rpm.RPMTAG_EPOCHNUM] + name = to_native(rpmhdr[rpm.RPMTAG_NAME]) + version = to_native(rpmhdr[rpm.RPMTAG_VERSION]) + release = to_native(rpmhdr[rpm.RPMTAG_RELEASE]) + arch = to_native(rpmhdr[rpm.RPMTAG_ARCH]) + if arch == 'noarch' and rpmhdr[rpm.RPMTAG_SOURCEPACKAGE] == 1: + arch = 'src' + + return (name, epoch, version, release, arch) + + +def main(): + module = KatelloAnsibleModule( + foreman_spec=dict( + src=dict(required=True, type='path', aliases=['file']), + repository=dict(required=True, type='entity', scope=['product'], thin=False), + product=dict(required=True, type='entity', scope=['organization']), + ostree_repository_name=dict(required=False, type='str'), + ), + ) + + with module.api_connection(): + repository_scope = module.scope_for('repository') + + b_src = to_bytes(module.foreman_params['src']) + filename = os.path.basename(module.foreman_params['src']) + + checksum = module.sha256(module.foreman_params['src']) + + content_unit = None + if module.foreman_params['repository']['content_type'] == 'deb': + if not HAS_DEBFILE: + module.fail_json(msg=missing_required_lib("python-debian"), exception=DEBFILE_IMP_ERR) + + name, version, architecture = get_deb_info(b_src) + query = 'name = "{0}" and version = "{1}" and architecture = "{2}"'.format(name, version, architecture) + content_unit = module.find_resource('debs', query, params=repository_scope, failsafe=True) + elif module.foreman_params['repository']['content_type'] == 'yum': + if not HAS_RPM: + module.fail_json(msg=missing_required_lib("rpm"), exception=RPM_IMP_ERR) + + name, epoch, version, release, arch = get_rpm_info(b_src) + query = 'name = "{0}" and epoch = "{1}" and version = "{2}" and release = "{3}" and arch = "{4}"'.format(name, epoch, version, release, arch) + content_unit = module.find_resource('packages', query, params=repository_scope, failsafe=True) + elif module.foreman_params['repository']['content_type'] == 'file': + query = 'name = "{0}" and checksum = "{1}"'.format(filename, checksum) + content_unit = module.find_resource('file_units', query, params=repository_scope, failsafe=True) + elif module.foreman_params['repository']['content_type'] == 'ostree': + try: + ostree_repository_name = module.foreman_params['ostree_repository_name'] + except KeyError: + module.fail_json(msg="The 'ostree_repository_name' parameter is required when uploading to OSTree repositories!") + else: + # possible types in 3.12: docker, ostree, yum, puppet, file, deb + module.fail_json(msg="Uploading to a {0} repository is not supported yet.".format(module.foreman_params['repository']['content_type'])) + + if not content_unit: + if not module.check_mode: + size = os.stat(module.foreman_params['src']).st_size + content_upload_payload = {'size': size} + content_upload_payload.update(repository_scope) + + content_upload = module.resource_action('content_uploads', 'create', content_upload_payload) + content_upload_scope = {'id': content_upload['upload_id']} + content_upload_scope.update(repository_scope) + + offset = 0 + + with open(b_src, 'rb') as contentfile: + for chunk in iter(lambda: contentfile.read(CONTENT_CHUNK_SIZE), b""): + data = {'content': chunk, 'offset': offset, 'size': size} + module.resource_action('content_uploads', 'update', params=content_upload_scope, data=data) + + offset += len(chunk) + + uploads = [{'id': content_upload['upload_id'], 'name': filename, + 'size': offset, 'checksum': checksum}] + import_params = {'id': module.foreman_params['repository']['id'], 'uploads': uploads} + if module.foreman_params['repository']['content_type'] == 'ostree': + ostree_parameters = {'ostree_repository_name': ostree_repository_name, 'content_type': 'ostree_ref'} + import_params.update(ostree_parameters) + + module.resource_action('repositories', 'import_uploads', import_params) + + module.resource_action('content_uploads', 'destroy', content_upload_scope) + else: + module.set_changed() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view.py new file mode 100644 index 00000000..509ac318 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view.py @@ -0,0 +1,285 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view +version_added: 1.0.0 +short_description: Manage Content Views +description: + - Create and manage content views +author: "Eric D Helms (@ehelms)" +options: + name: + description: + - Name of the Content View + required: true + type: str + description: + description: + - Description of the Content View + type: str + label: + description: + - Label of the Content View. This field cannot be updated. + type: str + repositories: + description: + - List of repositories that include name and product. + - Cannot be combined with I(composite=True). + type: list + elements: dict + suboptions: + name: + description: + - Name of the Repository to be added + type: str + required: true + product: + description: + - Product of the Repository to be added + type: str + required: true + auto_publish: + description: + - Auto publish composite view when a new version of a component content view is created. + - Also note auto publish will only happen when the component is marked "latest". + default: false + type: bool + solve_dependencies: + description: + - Solve RPM dependencies by default on Content View publish + type: bool + composite: + description: + - A composite view contains other content views. + default: false + type: bool + components: + description: + - List of content views to includes content_view and either version or latest. + - Ignored if I(composite=False). + type: list + elements: dict + suboptions: + content_view: + description: + - Content View name to be added to the Composite Content View + type: str + required: true + latest: + description: + - Always use the latest Content View Version + type: bool + default: False + content_view_version: + description: + - Version of the Content View to add + type: str + aliases: + - version +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create or update Fedora content view" + theforeman.foreman.content_view: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Fedora CV" + organization: "My Cool new Organization" + repositories: + - name: 'Fedora 26' + product: 'Fedora' + +- name: "Create a composite content view" + theforeman.foreman.content_view: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Fedora CCV" + organization: "My Cool new Organization" + composite: true + auto_publish: true + components: + - content_view: Fedora CV + content_view_version: 1.0 + - content_view: Internal CV + latest: true +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + content_views: + description: List of content views. + type: list + elements: dict +''' + +import copy +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +cvc_foreman_spec = { + 'id': {'invisible': True}, + 'content_view': {'type': 'entity', 'required': True}, + 'latest': {'type': 'bool', 'default': False}, + 'content_view_version': {'type': 'entity', 'aliases': ['version']}, +} + + +class KatelloContentViewModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloContentViewModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + label=dict(), + composite=dict(type='bool', default=False), + auto_publish=dict(type='bool', default=False), + solve_dependencies=dict(type='bool'), + components=dict(type='nested_list', foreman_spec=cvc_foreman_spec, resolve=False), + repositories=dict(type='entity_list', elements='dict', resolve=False, options=dict( + name=dict(required=True), + product=dict(required=True), + )), + ), + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + ), + mutually_exclusive=[['repositories', 'components']], + entity_opts=dict(thin=False), + ) + + # components is None when we're managing a CCV but don't want to adjust its components + components = module.foreman_params.pop('components', None) + if components: + for component in components: + if not component['latest'] and component.get('content_view_version') is None: + module.fail_json(msg="Content View Component must either have latest=True or provide a Content View Version.") + + with module.api_connection(): + entity = module.lookup_entity('entity') + scope = module.scope_for('organization') + + if not module.desired_absent: + if 'repositories' in module.foreman_params: + if module.foreman_params['composite']: + module.fail_json(msg="Repositories cannot be parts of a Composite Content View.") + else: + repositories = [] + for repository in module.foreman_params['repositories']: + product = module.find_resource_by_name('products', repository['product'], params=scope, thin=True) + repositories.append(module.find_resource_by_name('repositories', repository['name'], params={'product_id': product['id']}, thin=True)) + module.foreman_params['repositories'] = repositories + + if entity and module.desired_absent: + for lce in entity.get('environments', []): + module.resource_action('content_views', 'remove_from_environment', {'id': entity['id'], 'environment_id': lce['id']}) + + content_view_entity = module.run() + + # only update CVC's of newly created or updated CV's that are composite if components are specified + update_dependent_entities = (module.state == 'present' or (module.state == 'present_with_defaults' and module.changed)) + if update_dependent_entities and content_view_entity['composite'] and components is not None: + if not module.changed: + content_view_entity['content_view_components'] = entity['content_view_components'] + current_cvcs = content_view_entity.get('content_view_components', []) + + # only record a subset of data + current_cvcs_record = [] + for cvc in current_cvcs: + entry = {"id": cvc['id'], "content_view_id": cvc['content_view']['id'], "latest": cvc['latest']} + if 'content_view_version' in cvc and isinstance(cvc['content_view_version'], dict): + entry['content_view_version_id'] = cvc['content_view_version'].get('id') + current_cvcs_record.append(entry) + module.record_before('content_views/components', {'composite_content_view_id': content_view_entity['id'], + 'content_view_components': current_cvcs_record}) + final_cvcs_record = copy.deepcopy(current_cvcs_record) + + components_to_add = [] + ccv_scope = {'composite_content_view_id': content_view_entity['id']} + for component in components: + cvc = { + 'content_view': module.find_resource_by_name('content_views', name=component['content_view'], params=scope), + 'latest': component['latest'], + } + cvc_matched = next((item for item in current_cvcs if item['content_view']['id'] == cvc['content_view']['id']), None) + if not cvc['latest']: + search = "content_view_id={0},version={1}".format(cvc['content_view']['id'], component['content_view_version']) + cvc['content_view_version'] = module.find_resource('content_view_versions', search=search, thin=True) + cvc['latest'] = False + if cvc_matched and cvc_matched['latest']: + # When changing to latest=False & version is the latest we must send 'content_view_version' to the server + # Let's fake, it wasn't there... + cvc_matched.pop('content_view_version', None) + cvc_matched.pop('content_view_version_id', None) + if cvc_matched: + module.ensure_entity( + 'content_view_components', cvc, cvc_matched, state='present', foreman_spec=cvc_foreman_spec, params=ccv_scope) + current_cvcs.remove(cvc_matched) + else: + cvc['content_view_id'] = cvc.pop('content_view')['id'] + if 'content_view_version' in cvc: + cvc['content_view_version_id'] = cvc.pop('content_view_version')['id'] + components_to_add.append(cvc) + + if components_to_add: + payload = { + 'composite_content_view_id': content_view_entity['id'], + 'components': components_to_add, + } + module.resource_action('content_view_components', 'add_components', payload) + + final_cvcs_record.extend(components_to_add) + + # desired cvcs have already been updated and removed from `current_cvcs` + components_to_remove = [item['id'] for item in current_cvcs] + if components_to_remove: + payload = { + 'composite_content_view_id': content_view_entity['id'], + 'component_ids': components_to_remove, + } + module.resource_action('content_view_components', 'remove_components', payload) + + # some entries in "final" don't have an id yet, as it is only assigned on creation of a cv component, + # which didn't happen yet when we record the data + final_cvcs_record = [item for item in final_cvcs_record if item.get('id', 'NEW_ID') not in components_to_remove] + + module.record_after('content_views/components', {'composite_content_view_id': content_view_entity['id'], + 'content_view_components': final_cvcs_record}) + module.record_after_full('content_views/components', {'composite_content_view_id': content_view_entity['id'], + 'content_view_components': final_cvcs_record}) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter.py new file mode 100644 index 00000000..face7c28 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter.py @@ -0,0 +1,329 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018, Sean O'Keeffe <seanokeeffe797@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_filter +version_added: 1.0.0 +short_description: Manage Content View Filters +description: + - Create and manage content View filters +author: "Sean O'Keeffe (@sean797)" +options: + architecture: + description: + - package architecture + type: str + name: + description: + - Name of the Content View Filter + type: str + required: true + description: + description: + - Description of the Content View Filter + type: str + content_view: + description: + - Name of the content view + required: true + type: str + filter_state: + description: + - State of the content view filter + default: present + choices: + - present + - absent + type: str + repositories: + description: + - List of repositories that include name and product + - An empty Array means all current and future repositories + default: [] + type: list + elements: dict + rule_state: + description: + - State of the content view filter rule + default: present + choices: + - present + - absent + type: str + filter_type: + description: + - Content view filter type + required: true + choices: + - rpm + - package_group + - erratum + - docker + type: str + rule_name: + description: + - Content view filter rule name or package name + - If omitted, the value of I(name) will be used if necessary + aliases: + - package_name + - package_group + - tag + type: str + date_type: + description: + - Search using the 'Issued On' or 'Updated On' + - Only valid on I(filter_type=erratum). + default: updated + choices: + - issued + - updated + type: str + end_date: + description: + - erratum end date (YYYY-MM-DD) + type: str + start_date: + description: + - erratum start date (YYYY-MM-DD) + type: str + errata_id: + description: + - erratum id + type: str + max_version: + description: + - package maximum version + type: str + min_version: + description: + - package minimum version + type: str + types: + description: + - erratum types (enhancement, bugfix, security) + default: ["bugfix", "enhancement", "security"] + type: list + elements: str + version: + description: + - package version + type: str + inclusion: + description: + - Create an include filter + default: False + type: bool + original_packages: + description: + - Include all RPMs with no errata + type: bool +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: Exclude csh + theforeman.foreman.content_view_filter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "package filter 1" + organization: "Default Organization" + content_view: Web Servers + filter_type: "rpm" + package_name: tcsh + +- name: Include newer csh versions + theforeman.foreman.content_view_filter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "package filter 1" + organization: "Default Organization" + content_view: Web Servers + filter_type: "rpm" + package_name: tcsh + min_version: 6.20.00 + inclusion: True +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + content_view_filters: + description: List of content view filters. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloMixin, ForemanStatelessEntityAnsibleModule + +content_filter_spec = { + 'id': {}, + 'name': {}, + 'description': {}, + 'repositories': {'type': 'entity_list'}, + 'inclusion': {}, + 'content_view': {'type': 'entity'}, + 'filter_type': {'flat_name': 'type'}, + 'original_packages': {}, +} + +content_filter_rule_erratum_spec = { + 'id': {}, + 'date_type': {}, + 'end_date': {}, + 'start_date': {}, + 'types': {'type': 'list'}, +} + +content_filter_rule_erratum_id_spec = { + 'id': {}, + 'errata_id': {}, + 'date_type': {}, +} + +content_filter_rule_rpm_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, + 'end_date': {}, + 'max_version': {}, + 'min_version': {}, + 'version': {}, + 'architecture': {}, +} + +content_filter_rule_package_group_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, + 'uuid': {}, +} + +content_filter_rule_docker_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, +} + + +class KatelloContentViewFilterModule(KatelloMixin, ForemanStatelessEntityAnsibleModule): + pass + + +def main(): + module = KatelloContentViewFilterModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + repositories=dict(type='list', default=[], elements='dict'), + inclusion=dict(type='bool', default=False), + original_packages=dict(type='bool'), + content_view=dict(type='entity', scope=['organization'], required=True), + filter_type=dict(required=True, choices=['rpm', 'package_group', 'erratum', 'docker']), + filter_state=dict(default='present', choices=['present', 'absent']), + rule_state=dict(default='present', choices=['present', 'absent']), + rule_name=dict(aliases=['package_name', 'package_group', 'tag']), + date_type=dict(default='updated', choices=['issued', 'updated']), + end_date=dict(), + errata_id=dict(), + max_version=dict(), + min_version=dict(), + start_date=dict(), + types=dict(default=["bugfix", "enhancement", "security"], type='list', elements='str'), + version=dict(), + architecture=dict(), + ), + entity_opts=dict(scope=['content_view']), + ) + + filter_state = module.foreman_params.pop('filter_state') + rule_state = module.foreman_params.pop('rule_state') + + if module.foreman_params['filter_type'] == 'erratum': + module.foreman_params['rule_name'] = None + elif 'rule_name' not in module.foreman_params: + module.foreman_params['rule_name'] = module.foreman_params['name'] + + with module.api_connection(): + scope = module.scope_for('organization') + + cv_scope = module.scope_for('content_view') + if module.foreman_params['repositories']: + repositories = [] + for repo in module.foreman_params['repositories']: + product = module.find_resource_by_name('products', repo['product'], params=scope, thin=True) + product_scope = {'product_id': product['id']} + repositories.append(module.find_resource_by_name('repositories', repo['name'], params=product_scope, thin=True)) + module.foreman_params['repositories'] = repositories + + entity = module.lookup_entity('entity') + content_view_filter = module.ensure_entity( + 'content_view_filters', + module.foreman_params, + entity, + params=cv_scope, + state=filter_state, + foreman_spec=content_filter_spec, + ) + + if content_view_filter is not None: + cv_filter_scope = {'content_view_filter_id': content_view_filter['id']} + if 'errata_id' in module.foreman_params: + # should we try to find the errata the user is asking for? or just pass it blindly? + # errata = module.find_resource('errata', 'id={0}'.format(module.foreman_params['errata_id']), params=scope) + rule_spec = content_filter_rule_erratum_id_spec + search_scope = {'errata_id': module.foreman_params['errata_id']} + search_scope.update(cv_filter_scope) + search = None + else: + rule_spec = globals()['content_filter_rule_%s_spec' % (module.foreman_params['filter_type'])] + search_scope = cv_filter_scope + if module.foreman_params['rule_name'] is not None: + search = 'name="{0}"'.format(module.foreman_params['rule_name']) + else: + search = None + # not using find_resource_by_name here, because not all filters (errata) have names + content_view_filter_rule = module.find_resource('content_view_filter_rules', search, params=search_scope, failsafe=True) if entity else None + + if module.foreman_params['filter_type'] == 'package_group': + package_group = module.find_resource_by_name('package_groups', module.foreman_params['rule_name'], params=scope) + module.foreman_params['uuid'] = package_group['uuid'] + + # drop 'name' from the dict, as otherwise it might override 'rule_name' + rule_dict = module.foreman_params.copy() + rule_dict.pop('name', None) + + module.ensure_entity( + 'content_view_filter_rules', + rule_dict, + content_view_filter_rule, + params=cv_filter_scope, + state=rule_state, + foreman_spec=rule_spec, + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_info.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_info.py new file mode 100644 index 00000000..49bb4e2d --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_info.py @@ -0,0 +1,88 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Paul Armstrong +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_filter_info +version_added: 3.9.0 +short_description: Fetch information about a Content View Filter +description: + - Fetch information about a Content View Filter +author: + - "Paul Armstrong (@parmstro)" +options: + content_view: + description: + - the name of the content view that the filter applies to + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a content_view_filter" + theforeman.foreman.content_view_filter_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "SOE_RHEL9" + name: "AllRPMNoErrata" + +''' + +RETURN = ''' +content_view_filter: + description: Details about the found content view filter + returned: success and I(name) was passed + type: dict +content_view_filters: + description: Details about the found content view filters + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + KatelloInfoAnsibleModule, +) + + +class KatelloContentViewFilterInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloContentViewFilterInfo( + foreman_spec=dict( + content_view=dict(type='entity', scope=['organization'], required=True), + ), + entity_opts=dict(scope=['content_view']), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule.py new file mode 100644 index 00000000..5a63b221 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule.py @@ -0,0 +1,322 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Paul Armstrong <parmstro@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_filter_rule +version_added: 3.9.0 +short_description: Manage content view filter rules +description: + - Create, manage and remove content view filter rules +author: + - "Paul Armstrong (@parmstro)" +options: + architecture: + description: + - set package, module_stream, etc. architecture that the rule applies to + aliases: + - arch + type: str + content_view: + description: + - the name of the content view that the filter applies to + required: true + type: str + content_view_filter: + description: + - the name of the content view filter that the rule applies to + required: true + type: str + context: + description: + - the context for a module + - only valid in filter I(type=modulemd) + type: str + date_type: + description: + - set whether rule applied to erratum using the 'Issued On' or 'Updated On' date + - only valid on filter I(type=erratum). + default: updated + choices: + - issued + - updated + type: str + end_date: + description: + - the rule limit for erratum end date (YYYY-MM-DD) + - see date_type for the date the rule applies to + - Only valid on I(filter_type=erratum_by_date). + type: str + errata_id: + description: + - erratum id + type: str + max_version: + description: + - package maximum version + type: str + min_version: + description: + - package minimum version + type: str + name: + description: + - Content view filter rule name, package name, package_group name, module stream or docker tag + - If omitted, the value of I(name) will be used if necessary + - for module stream filters, this is the name of the module stream to search for + aliases: + - rule_name + - module_name + - package_name + - package_group + - tag + type: str + start_date: + description: + - the rule limit for erratum start date (YYYY-MM-DD) + - see date_type for the date the rule applies to + - Only valid on I(filter_type=erratum). + type: str + stream: + description: + - the context for a module + - only valid in filter I(type=modulemd) + type: str + types: + description: + - errata types the ruel applies to (enhancement, bugfix, security) + - Only valid on I(filter_type=erratum) + default: ["bugfix", "enhancement", "security"] + type: list + elements: str + version: + description: + - package or module version + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' + +- name: "Include errata by date" + theforeman.foreman.content_view_filter_rule: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + content_view: "Standard Operating Environment" + content_view_filter: "errata_by_date" + state: present + inclusion: true + date_type: updated + types: + - bugfix + - security + - enhancement + end_date: "2022-05-25" + +- name: "Exclude csh versions 6.20 and older" + theforeman.foreman.content_view_filter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + content_view: "Standard Operating Environment" + content_view_filter: "package filter 1" + name: "tcsh" + max_version: "6.20.00" + +- name: "Exclude csh version 6.23 due to example policy" + theforeman.foreman.content_view_filter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + content_view: "Standard Operating Environment" + content_view_filter: "package filter 1" + name: "tcsh" + version: "6.23.00" + +- name: "Content View Filter Rule for 389" + content_view_filter_rule: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + validate_certs: "true" + organization: "Default Organization" + content_view: "Standard Operating Environment" + content_view_filter: "modulemd filter" + name: "389-directory-server" + stream: "next" + version: "820220325123957" + context: "9edba152" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + content_view_filters_rules: + description: List of content view filter rule(s). + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + +content_filter_rule_erratum_spec = { + 'id': {}, + 'date_type': {}, + 'end_date': {}, + 'start_date': {}, + 'types': {'type': 'list'}, +} + +content_filter_rule_erratum_id_spec = { + 'id': {}, + 'errata_id': {}, +} + +content_filter_rule_rpm_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, + 'max_version': {}, + 'min_version': {}, + 'version': {}, + 'architecture': {}, +} + +content_filter_rule_modulemd_spec = { + 'id': {}, + 'module_stream_ids': {'type': 'list'}, +} + +content_filter_rule_package_group_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, + 'uuid': {}, +} + +content_filter_rule_docker_spec = { + 'id': {}, + 'rule_name': {'flat_name': 'name'}, +} + + +class KatelloContentViewFilterRuleModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloContentViewFilterRuleModule( + foreman_spec=dict( + content_view=dict(type='entity', scope=['organization'], required=True), + content_view_filter=dict(type='entity', scope=['content_view'], required=True), + name=dict(aliases=['rule_name', 'module_name', 'package_name', 'package_group', 'tag']), + errata_id=dict(), + types=dict(default=["bugfix", "enhancement", "security"], type='list', elements='str'), + date_type=dict(default='updated', choices=['issued', 'updated']), + start_date=dict(), + end_date=dict(), + architecture=dict(aliases=['arch']), + version=dict(), + max_version=dict(), + min_version=dict(), + stream=dict(), + context=dict(), + ), + entity_opts=dict(scope=['content_view_filter']), + ) + + with module.api_connection(): + + # A filter always exists before we create a rule + # Get a reference to the content filter that owns the rule we want to manage + cv_scope = module.scope_for('content_view') + cvf_scope = module.scope_for('content_view_filter') + cvf = module.lookup_entity('content_view_filter') + + # figure out what kind of filter we are working with + filter_type = cvf['type'] + rule_spec = globals()['content_filter_rule_%s_spec' % (filter_type)] + + # trying to find the existing rule is not simple... + search_scope = cvf_scope + content_view_filter_rule = None + + if filter_type != 'erratum' and module.foreman_params['name'] is None: + module.fail_json(msg="The 'name' parameter is required when creating a filter rule for rpm, container, package_group or modulemd filters.") + + if filter_type == 'erratum': + # this filter type supports many rules + # there are really 2 erratum filter types by_date and by_id + # however the table backing them is denormalized to support both, as is the api + # for an erratum filter rule == errata_by_date rule, there can be only one rule per filter. So that's easy, its the only one + if 'errata_id' in module.foreman_params: + # we need to search by errata_id, because it really doesn't have a name field. + rule_spec = content_filter_rule_erratum_id_spec + search_scope['errata_id'] = module.foreman_params['errata_id'] + content_view_filter_rule = module.find_resource('content_view_filter_rules', None, params=search_scope, failsafe=True) + + elif filter_type in ('rpm', 'docker', 'package_group'): + # these filter types support many rules + # the name is the key to finding the proper one and is required for these types + content_view_filter_rule = module.find_resource_by_name('content_view_filter_rules', module.foreman_params['name'], + params=search_scope, failsafe=True) + + if filter_type == 'package_group': + # uuid is also a required value creating, but is implementation specific and not easily knowable to the end user - we find it for them + package_group = module.find_resource_by_name('package_groups', module.foreman_params['name'], params=cv_scope) + module.foreman_params['uuid'] = package_group['uuid'] + + elif filter_type == 'modulemd': + # this filter type support many rules + # module_stream_ids are internal and non-searchable + # find the module_stream_id by NSVCA + search = ','.join('{0}="{1}"'.format(key, module.foreman_params.get(key, '')) for key in ('name', 'stream', 'version', 'context')) + module_stream = module.find_resource('module_streams', search, failsafe=True) + # determine if there is a rule for the module_stream + existing_rule = next((rule for rule in cvf['rules'] if rule['module_stream_id'] == module_stream['id']), None) + # if the rule exists, return it in a form ammenable to the API + if existing_rule: + content_view_filter_rule = module.find_resource_by_id('content_view_filter_rules', existing_rule['id'], params=search_scope, failsafe=True) + + # if the state is present and the module_id is NOT in the exising list, add module_stream_id. + if not module.desired_absent and not existing_rule: + module.foreman_params['module_stream_ids'] = [module_stream['id']] + + module.ensure_entity( + 'content_view_filter_rules', + module.foreman_params, + content_view_filter_rule, + params=cvf_scope, + foreman_spec=rule_spec, + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule_info.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule_info.py new file mode 100644 index 00000000..83e43b28 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule_info.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Paul Armstrong +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_filter_rule_info +version_added: 3.9.0 +short_description: Fetch information about a Content View Filter Rule +description: + - Fetch information about a Content View Filter Rule +author: + - "Paul Armstrong (@parmstro)" +options: + content_view: + description: + - the name of the content view that the filter applies to + required: true + type: str + content_view_filter: + description: + - the name of the content view filter that the rule applies to + type: str + required: true + errata_id: + description: + - for erratum fitlers using errata_by_id, the errata id to search for + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a content_view_filter_rule" + theforeman.foreman.content_view_filter_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "SOE_RHEL9" + content_view_filter: "NoFireFox" + name: firefox + +''' + +RETURN = ''' +content_view_filter_rule: + description: Details about the found content_view_filter_rule + returned: success and I(name) was passed + type: dict +content_view_filter_rules: + description: Details about the found content_view_filter_rules + returned: success and the filter type is erratum or modulemd + type: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + KatelloInfoAnsibleModule, +) + + +class KatelloContentViewFilterRuleInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloContentViewFilterRuleInfo( + foreman_spec=dict( + content_view=dict(type='entity', scope=['organization'], required=True), + content_view_filter=dict(type='entity', scope=['content_view'], required=True), + errata_id=dict(), + ), + entity_opts=dict(scope=['content_view_filter']), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_info.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_info.py new file mode 100644 index 00000000..a6bfc23e --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Eric Helms +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_info +version_added: 2.1.0 +short_description: Fetch information about Content Views +description: + - Fetch information about Content Views +author: + - "Eric Helms (@ehelms)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a content_view" + theforeman.foreman.content_view_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "CentOS 8" + +- name: "Show all content_views with name CentOS 8" + theforeman.foreman.content_view_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: 'name = "CentOS 8"' +''' + +RETURN = ''' +content_view: + description: Details about the found content_view + returned: success and I(name) was passed + type: dict +content_views: + description: List of all found content_views and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class KatelloContentViewInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = KatelloContentViewInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_version.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_version.py new file mode 100644 index 00000000..03739e70 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_version.py @@ -0,0 +1,265 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018, Sean O'Keeffe <seanokeeffe797@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_version +version_added: 1.0.0 +short_description: Manage Content View Versions +description: + - Publish, Promote or Remove a Content View Version +author: Sean O'Keeffe (@sean797) +notes: + - You cannot use this to remove a Content View Version from a Lifecycle environment, you should promote another version first. + - For idempotency you must specify either C(version) or C(current_lifecycle_environment). +options: + content_view: + description: + - Name of the content view + required: true + type: str + description: + description: + - Description of the Content View Version + type: str + version: + description: + - The content view version number (i.e. 1.0) + type: str + lifecycle_environments: + description: + - The lifecycle environments the Content View Version should be in. + type: list + elements: str + force_promote: + description: + - Force content view promotion and bypass lifecycle environment restriction + default: false + type: bool + aliases: + - force + force_yum_metadata_regeneration: + description: + - Force metadata regeneration when performing Publish and Promote tasks + type: bool + default: false + current_lifecycle_environment: + description: + - The lifecycle environment that is already associated with the content view version + - Helpful for promoting a content view version + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Ensure content view version 2.0 is in Test & Pre Prod" + theforeman.foreman.content_view_version: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CV 1" + organization: "Default Organization" + version: "2.0" + lifecycle_environments: + - Test + - Pre Prod + +- name: "Ensure content view version in Test is also in Pre Prod" + theforeman.foreman.content_view_version: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CV 1" + organization: "Default Organization" + current_lifecycle_environment: Test + lifecycle_environments: + - Pre Prod + +- name: "Publish a content view, not idempotent" + theforeman.foreman.content_view_version: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CV 1" + organization: "Default Organization" + +- name: "Publish a content view and promote that version to Library & Dev, not idempotent" + theforeman.foreman.content_view_version: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CV 1" + organization: "Default Organization" + lifecycle_environments: + - Library + - Dev + +- name: "Ensure content view version 1.0 doesn't exist" + theforeman.foreman.content_view_version: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "Web Servers" + organization: "Default Organization" + version: "1.0" + state: absent + +# Obtain information about a Content View and its versions +- name: find all CVs + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + resource: content_views + search: 'name="Example Content"' + register: example_content + +# Obtain more details about all versions of a specific Content View +- name: "find content view versions of {{ cv_id }}" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + resource: content_view_versions + params: + content_view_id: "{{ example_content.resources[0].id }}" + register: version_information +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + content_view_versions: + description: List of content view versions. + type: list + elements: dict +''' + + +import re +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +def promote_content_view_version(module, content_view_version, environments, force, force_yum_metadata_regeneration): + current_environment_ids = {environment['id'] for environment in content_view_version['environments']} + desired_environment_ids = {environment['id'] for environment in environments} + promote_to_environment_ids = list(desired_environment_ids - current_environment_ids) + + if promote_to_environment_ids: + payload = { + 'id': content_view_version['id'], + 'environment_ids': promote_to_environment_ids, + 'force': force, + 'force_yum_metadata_regeneration': force_yum_metadata_regeneration, + } + + module.record_before('content_view_versions', {'id': content_view_version['id'], 'environments': content_view_version['environments']}) + module.resource_action('content_view_versions', 'promote', params=payload) + module.record_after('content_view_versions', {'id': content_view_version['id'], 'environments': environments}) + module.record_after_full('content_view_versions', {'id': content_view_version['id'], 'environments': environments}) + + +class KatelloContentViewVersionModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloContentViewVersionModule( + foreman_spec=dict( + content_view=dict(type='entity', required=True, scope=['organization']), + description=dict(), + version=dict(), + lifecycle_environments=dict(type='entity_list', scope=['organization']), + force_promote=dict(type='bool', aliases=['force'], default=False), + force_yum_metadata_regeneration=dict(type='bool', default=False), + current_lifecycle_environment=dict(type='entity', resource_type='lifecycle_environments', scope=['organization']), + ), + mutually_exclusive=[['current_lifecycle_environment', 'version']], + ) + + module.task_timeout = 60 * 60 + + if 'version' in module.foreman_params and not re.match(r'^\d+\.\d+$', module.foreman_params['version']): + try: + major_version = int(module.foreman_params['version']) + module.foreman_params['version'] = "{0}.0".format(major_version) + except ValueError: + module.fail_json("The 'version' needs to be in the format 'X.Y', not '{0}'".format(module.foreman_params['version'])) + + with module.api_connection(): + scope = module.scope_for('organization') + content_view = module.lookup_entity('content_view') + + if 'current_lifecycle_environment' in module.foreman_params: + search_scope = {'content_view_id': content_view['id'], 'environment_id': module.lookup_entity('current_lifecycle_environment')['id']} + content_view_version = module.find_resource('content_view_versions', search=None, params=search_scope) + elif 'version' in module.foreman_params: + search = "content_view_id={0},version={1}".format(content_view['id'], module.foreman_params['version']) + content_view_version = module.find_resource('content_view_versions', search=search, failsafe=True) + else: + content_view_version = None + module.set_entity('entity', content_view_version) + + if module.desired_absent: + module.ensure_entity('content_view_versions', None, content_view_version, params=scope) + else: + module.auto_lookup_entities() + if content_view_version is None: + payload = { + 'id': content_view['id'], + } + if 'description' in module.foreman_params: + payload['description'] = module.foreman_params['description'] + if 'force_yum_metadata_regeneration' in module.foreman_params: + payload['force_yum_metadata_regeneration'] = module.foreman_params['force_yum_metadata_regeneration'] + if 'version' in module.foreman_params: + split_version = list(map(int, str(module.foreman_params['version']).split('.'))) + payload['major'] = split_version[0] + payload['minor'] = split_version[1] + + response = module.resource_action('content_views', 'publish', params=payload) + # workaround for https://projects.theforeman.org/issues/28138 + if not module.check_mode: + content_view_version_id = response['output'].get('content_view_version_id') or response['input'].get('content_view_version_id') + content_view_version = module.show_resource('content_view_versions', content_view_version_id) + else: + content_view_version = {'id': -1, 'environments': []} + + if 'lifecycle_environments' in module.foreman_params: + promote_content_view_version( + module, + content_view_version, + module.foreman_params['lifecycle_environments'], + force=module.foreman_params['force_promote'], + force_yum_metadata_regeneration=module.foreman_params['force_yum_metadata_regeneration'], + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/content_view_version_info.py b/ansible_collections/theforeman/foreman/plugins/modules/content_view_version_info.py new file mode 100644 index 00000000..832efc33 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/content_view_version_info.py @@ -0,0 +1,90 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Eric Helms +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: content_view_version_info +version_added: 2.1.0 +short_description: Fetch information about Content Views +description: + - Fetch information about Content Views +author: + - "Eric Helms (@ehelms)" +options: + content_view: + description: + - Content View to which the Version belongs + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodulewithoutname +''' + +EXAMPLES = ''' +- name: "Show a content view version" + theforeman.foreman.content_view_version_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CentOS 8 View" + search: 'version = "4.0"' + +- name: "Show all content view_versions for a content view" + theforeman.foreman.content_view_version_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + content_view: "CentOS 8 View" +''' + +RETURN = ''' +content_view_versions: + description: List of all found content_view_versions and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + KatelloInfoAnsibleModule, +) + + +class KatelloContentViewVersionInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloContentViewVersionInfo( + foreman_spec=dict( + content_view=dict(type='entity', scope=['organization'], required=True), + name=dict(invisible=True), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/discovery_rule.py b/ansible_collections/theforeman/foreman/plugins/modules/discovery_rule.py new file mode 100644 index 00000000..9b903bed --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/discovery_rule.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022, Jeffrey van Pelt <jeff@vanpelt.one> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: discovery_rule +version_added: 3.5.0 +short_description: Manage Host Discovery Rules +description: + - Manage Host Discovery Rules +author: + - "Jeffrey van Pelt (@Thulium-Drake)" +options: + name: + description: + - Name of the Discovery Rule + required: True + type: str + search: + description: + - Expression to match newly discovered hosts with + - Required if I(state=present) + type: str + hostgroup: + description: + - Hostgroup to assign hosts to + - Required if I(state=present) + type: str + hostname: + description: + - Hostname to assign to discovered host(s) + - When matching multiple hosts, must provide unique hostnames for each of the discovered hosts + type: str + enabled: + description: + - Enable or disable the rule + type: bool + priority: + description: + - Priority of the rule + type: int + max_count: + description: + - Maximum amount of hosts to provision with the rule + - 0 means no limit + type: int +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: 'Ensure Discovery Rule' + theforeman.foreman.discovery_rule: + username: 'admin' + password: 'secret_password' + server_url: 'https://foreman.example.com' + name: 'my-first-disco' + search: 'mac = bb:bb:bb:bb:bb:bb' + hostgroup: 'RedHat7-Base' + hostname: 'servera' + max_count: 1 + organizations: + - 'MyOrg' + locations: + - 'DC1' + +- name: 'Remove Discovery Rule' + theforeman.foreman.discovery_rule: + username: 'admin' + password: 'secret_password' + server_url: 'https://foreman.example.com' + name: 'my-first-disco' + state: 'absent' +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + discovery_rules: + description: List of discovery rules. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +class ForemanDiscoveryRuleModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanDiscoveryRuleModule( + foreman_spec=dict( + name=dict(required=True), + search=dict(), + hostgroup=dict(type='entity'), + hostname=dict(), + max_count=dict(type='int'), + hosts_limit=dict(type='int', invisible=True, flat_name='max_count'), + priority=dict(type='int'), + enabled=dict(type='bool'), + ), + required_if=[ + ['state', 'present', ['hostgroup', 'search']], + ], + required_plugins=[('discovery', ['*'])], + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + # workround the fact that the API expects `max_count` when modifying the entity + # but uses `hosts_limit` when showing one + if entity and 'hosts_limit' in entity: + entity['max_count'] = entity.pop('hosts_limit') + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/domain.py b/ansible_collections/theforeman/foreman/plugins/modules/domain.py new file mode 100644 index 00000000..ef05d5f5 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/domain.py @@ -0,0 +1,112 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Markus Bucher (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: domain +version_added: 1.0.0 +short_description: Manage Domains +description: + - Create, update, and delete Domains +author: + - "Markus Bucher (@m-bucher) ATIX AG" +options: + name: + description: The full DNS domain name + required: true + type: str + updated_name: + description: New domain name. When this parameter is set, the module will not be idempotent. + type: str + dns_proxy: + aliases: + - dns + description: DNS proxy to use within this domain for managing A records + required: false + type: str + description: + aliases: + - fullname + description: Full name describing the domain + required: false + type: str + parameters: + description: + - Domain specific host parameters +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.nested_parameters +''' + +EXAMPLES = ''' +- name: domain + theforeman.foreman.domain: + name: "example.org" + description: "Example Domain" + locations: + - "Munich" + organizations: + - "ACME" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + domains: + description: List of domains. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule, ParametersMixin + + +class ForemanDomainModule(ParametersMixin, ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanDomainModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + description=dict(aliases=['fullname'], flat_name='fullname'), + dns_proxy=dict(type='entity', flat_name='dns_id', aliases=['dns'], resource_type='smart_proxies'), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/domain_info.py b/ansible_collections/theforeman/foreman/plugins/modules/domain_info.py new file mode 100644 index 00000000..f5e5525f --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/domain_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Eric Helms +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: domain_info +version_added: 2.1.0 +short_description: Fetch information about Domains +description: + - Fetch information about Domains +author: + - "Eric Helms (@ehelms)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a domain" + theforeman.foreman.domain_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "example.com" + +- name: "Show all domains with domain example.com" + theforeman.foreman.domain_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "name = example.com" +''' + +RETURN = ''' +domain: + description: Details about the found domain + returned: success and I(name) was passed + type: dict +domains: + description: List of all found domains and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanDomainInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanDomainInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/external_usergroup.py b/ansible_collections/theforeman/foreman/plugins/modules/external_usergroup.py new file mode 100644 index 00000000..0ef6b525 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/external_usergroup.py @@ -0,0 +1,123 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Kirill Shirinkin (kirill@mkdev.me) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: external_usergroup +version_added: 1.0.0 +short_description: Manage External User Groups +description: + - Create, update, and delete external user groups +author: + - "Kirill Shirinkin (@Fodoj)" +options: + name: + description: + - Name of the group + required: true + type: str + usergroup: + description: + - Name of the linked usergroup + required: true + type: str + auth_source: + description: + - Name of the authentication source to be used for this group + required: true + type: str + aliases: + - auth_source_ldap +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: Create an external user group + theforeman.foreman.external_usergroup: + name: test + auth_source: "My LDAP server" + usergroup: "Internal Usergroup" + state: present +- name: Link a group from FreeIPA + theforeman.foreman.external_usergroup: + name: ipa_users + auth_source: "External" + usergroup: "Internal Usergroup" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + external_usergroups: + description: List of external usergroups. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanExternalUsergroupModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanExternalUsergroupModule( + foreman_spec=dict( + name=dict(required=True), + usergroup=dict(required=True, type='entity', ensure=False), + auth_source=dict(required=True, aliases=['auth_source_ldap'], type='entity', flat_name='auth_source_id', resource_type='auth_sources'), + auth_source_ldap=dict(type='entity', invisible=True, flat_name='auth_source_id'), + auth_source_external=dict(type='entity', invisible=True, flat_name='auth_source_id'), + ), + ) + + entity = None + + with module.api_connection(): + params = module.scope_for('usergroup') + # There is no way to find by name via API search, so we need + # to iterate over all external user groups of a given usergroup + for external_usergroup in module.list_resource("external_usergroups", params=params): + if external_usergroup['name'] == module.foreman_params['name']: + entity = external_usergroup + + module.set_entity('entity', entity) + + auth_source = module.lookup_entity('auth_source') + if auth_source.get('type') == 'AuthSourceExternal': + module.set_entity('auth_source_external', auth_source) + elif auth_source.get('type') == 'AuthSourceLdap': + module.set_entity('auth_source_ldap', auth_source) + else: + module.fail_json(msg="Unsupported authentication source type: {0}".format(auth_source.get('type'))) + + module.run(params=params) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/global_parameter.py b/ansible_collections/theforeman/foreman/plugins/modules/global_parameter.py new file mode 100644 index 00000000..0b2b969f --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/global_parameter.py @@ -0,0 +1,164 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017 Matthias Dellweg & Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +# pylint: disable=super-with-arguments + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: global_parameter +version_added: 1.0.0 +short_description: Manage Global Parameters +description: + - "Manage Global Parameter Entities" +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" + - "Matthias Dellweg (@mdellweg) ATIX AG" + - "Manisha Singhal (@manisha15) ATIX AG" +options: + name: + description: + - Name of the Global Parameter + required: true + type: str + updated_name: + description: + - New name of the Global Parameter. When this parameter is set, the module will not be idempotent. + type: str + value: + description: + - Value of the Global Parameter + required: false + type: raw + hidden_value: + description: + - Whether the value should be hidden in the GUI + required: false + type: bool + parameter_type: + description: + - Type of value + default: string + choices: + - string + - boolean + - integer + - real + - array + - hash + - yaml + - json + type: str +notes: + - The I(parameter_type) only has an effect on Foreman >= 1.22 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults +''' + +EXAMPLES = ''' +- name: "Create a Global Parameter" + theforeman.foreman.global_parameter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "TheAnswer" + value: "42" + state: present_with_defaults + +- name: "Update a Global Parameter" + theforeman.foreman.global_parameter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "TheAnswer" + value: "43" + state: present + +- name: "Delete a Global Parameter" + theforeman.foreman.global_parameter: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "TheAnswer" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + global_parameters: + description: List of global parameters. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule, parameter_value_to_str + + +class ForemanCommonParameterModule(ForemanEntityAnsibleModule): + def remove_sensitive_fields(self, entity): + if entity and 'hidden_value?' in entity: + entity['hidden_value'] = entity.pop('hidden_value?') + if entity['hidden_value']: + entity['value'] = None + return super(ForemanCommonParameterModule, self).remove_sensitive_fields(entity) + + +def main(): + module = ForemanCommonParameterModule( + foreman_spec=dict( + name=dict(required=True), + value=dict(type='raw'), + hidden_value=dict(type='bool'), + parameter_type=dict(default='string', choices=['string', 'boolean', 'integer', 'real', 'array', 'hash', 'yaml', 'json']), + ), + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + updated_name=dict(), + ), + required_if=( + ['state', 'present_with_defaults', ['value']], + ['state', 'present', ['value']], + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity', params={'show_hidden': True}) + + if not module.desired_absent: + # Convert values according to their corresponding parameter_type + if entity and 'parameter_type' not in entity: + entity['parameter_type'] = 'string' + module.foreman_params['value'] = parameter_value_to_str(module.foreman_params['value'], module.foreman_params['parameter_type']) + if entity and 'value' in entity: + entity['value'] = parameter_value_to_str(entity['value'], entity.get('parameter_type', 'string')) + if entity and 'hidden_value?' in entity: + entity['hidden_value'] = entity.pop('hidden_value?') + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/hardware_model.py b/ansible_collections/theforeman/foreman/plugins/modules/hardware_model.py new file mode 100644 index 00000000..27bf2944 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/hardware_model.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020, Evgeni Golov <evgeni@golov.de> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: hardware_model +version_added: 1.0.0 +short_description: Manage Hardware Models +description: + - Manage hardware models +author: + - "Evgeni Golov (@evgeni)" +options: + name: + description: + - Name of the hardware model + required: true + type: str + info: + description: + - General description of the hardware model + type: str + vendor_class: + description: + - The class of the machine as reported by the OpenBoot PROM. + - This is primarily used by Solaris SPARC builds and can be left blank for other architectures. + type: str + hardware_model: + description: + - The class of CPU supplied in this machine. + - This is primarily used by Sparc Solaris builds and can be left blank for other architectures. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: "Create ACME Laptop model" + theforeman.foreman.hardware_model: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "acme laptop" + info: "this is the acme laptop" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + hardware_models: + description: List of hardware models. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanModelModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanModelModule( + foreman_spec=dict( + name=dict(required=True), + info=dict(), + vendor_class=dict(), + hardware_model=dict(), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/host.py b/ansible_collections/theforeman/foreman/plugins/modules/host.py new file mode 100644 index 00000000..eaa46f18 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/host.py @@ -0,0 +1,535 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: host +version_added: 1.0.0 +short_description: Manage Hosts +description: + - Create, update, and delete Hosts +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" +options: + name: + description: + - Fully Qualified Domain Name of host + required: true + type: str + hostgroup: + description: + - Title of related hostgroup + - "Example: A child hostgroup I(bar) within a parent hostgroup I(foo) would have the title I(foo/bar)." + required: false + type: str + location: + description: + - Name of related location + required: false + type: str + organization: + description: + - Name of related organization + required: false + type: str + build: + description: + - Whether or not to setup build context for the host + type: bool + required: false + enabled: + description: + - Include this host within reporting + type: bool + required: false + managed: + description: + - Whether a host is managed or unmanaged. + - Forced to true when I(build=true) + type: bool + required: false + ip: + description: + - IP address of the primary interface of the host. + type: str + required: false + mac: + description: + - MAC address of the primary interface of the host. + - Please include leading zeros and separate nibbles by colons, otherwise the execution will not be idempotent. + - Example EE:BB:01:02:03:04 + type: str + required: false + comment: + description: + - Comment about the host. + type: str + required: false + owner: + description: + - Owner (user) of the host. + - Users are looked up by their C(login). + - Mutually exclusive with I(owner_group). + type: str + required: false + owner_group: + description: + - Owner (user group) of the host. + - Mutually exclusive with I(owner). + type: str + required: false + provision_method: + description: + - The method used to provision the host. + - I(provision_method=bootdisk) is only available if the bootdisk plugin is installed. + choices: + - 'build' + - 'image' + - 'bootdisk' + type: str + required: false + image: + description: + - The image to use when I(provision_method=image). + - The I(compute_resource) parameter is required to find the correct image. + type: str + required: false + compute_attributes: + description: + - Additional compute resource specific attributes. + - When this parameter is set, the module will not be idempotent. + - When you provide a I(cluster) here and I(compute_resource) is set, the cluster id will be automatically looked up. + type: dict + required: false + interfaces_attributes: + description: + - Additional interfaces specific attributes. + version_added: 1.5.0 + required: false + type: list + elements: dict + suboptions: + mac: + description: + - MAC address of interface. Required for managed interfaces on bare metal. + - Please include leading zeros and separate nibbles by colons, otherwise the execution will not be idempotent. + - Example EE:BB:01:02:03:04 + - You need to set one of I(identifier), I(name) or I(mac) to be able to update existing interfaces and make execution idempotent. + type: str + ip: + description: + - IPv4 address of interface + type: str + ip6: + description: + - IPv6 address of interface + type: str + type: + description: + - Interface type. + type: str + choices: + - 'interface' + - 'bmc' + - 'bond' + - 'bridge' + name: + description: + - Interface's DNS name + - You need to set one of I(identifier), I(name) or I(mac) to be able to update existing interfaces and make execution idempotent. + type: str + subnet: + description: + - IPv4 Subnet name + type: str + subnet6: + description: + - IPv6 Subnet name + type: str + domain: + description: + - Domain name + - Required for primary interfaces on managed hosts. + type: str + identifier: + description: + - Device identifier, e.g. eth0 or eth1.1 + - You need to set one of I(identifier), I(name) or I(mac) to be able to update existing interfaces and make execution idempotent. + type: str + managed: + description: + - Should this interface be managed via DHCP and DNS smart proxy and should it be configured during provisioning? + type: bool + primary: + description: + - Should this interface be used for constructing the FQDN of the host? + - Each managed hosts needs to have one primary interface. + type: bool + provision: + description: + - Should this interface be used for TFTP of PXELinux (or SSH for image-based hosts)? + - Each managed hosts needs to have one provision interface. + type: bool + username: + description: + - Username for BMC authentication. + - Only for BMC interfaces. + type: str + password: + description: + - Password for BMC authentication. + - Only for BMC interfaces. + type: str + provider: + description: + - Interface provider, e.g. IPMI. + - Only for BMC interfaces. + type: str + choices: + - 'IPMI' + - 'Redfish' + - 'SSH' + virtual: + description: + - Alias or VLAN device + type: bool + tag: + description: + - VLAN tag, this attribute has precedence over the subnet VLAN ID. + - Only for virtual interfaces. + type: str + mtu: + description: + - MTU, this attribute has precedence over the subnet MTU. + type: int + attached_to: + description: + - Identifier of the interface to which this interface belongs, e.g. eth1. + - Only for virtual interfaces. + type: str + mode: + description: + - Bond mode of the interface. + - Only for bond interfaces. + type: str + choices: + - 'balance-rr' + - 'active-backup' + - 'balance-xor' + - 'broadcast' + - '802.3ad' + - 'balance-tlb' + - 'balance-alb' + attached_devices: + description: + - Identifiers of attached interfaces, e.g. ['eth1', 'eth2']. + - For bond interfaces those are the slaves. + - Only for bond and bridges interfaces. + type: list + elements: str + bond_options: + description: + - Space separated options, e.g. miimon=100. + - Only for bond interfaces. + type: str + compute_attributes: + description: + - Additional compute resource specific attributes for the interface. + - When this parameter is set, the module will not be idempotent. + - When you provide a I(network) here and I(compute_resource) is set, the network id will be automatically looked up. + - On oVirt/RHV I(cluster) is required in the hosts I(compute_attributes) for the lookup to work. + type: dict +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.host_options + - theforeman.foreman.foreman.nested_parameters + - theforeman.foreman.foreman.operatingsystem +''' + +EXAMPLES = ''' +- name: "Create a host" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + hostgroup: my_hostgroup + state: present + +- name: "Create a host with build context" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + hostgroup: my_hostgroup + build: true + state: present + +- name: "Create an unmanaged host" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + managed: false + state: present + +- name: "Create a VM with 2 CPUs and 4GB RAM" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + compute_attributes: + cpus: 2 + memory_mb: 4096 + state: present + +- name: "Create a VM and start it after creation" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + compute_attributes: + start: "1" + state: present + +- name: "Create a VM on specific ovirt network" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + interfaces_attributes: + - type: "interface" + compute_attributes: + name: "nic1" + network: "969efbe6-f9e0-4383-a19a-a7ee65ad5007" + interface: "virtio" + state: present + +- name: "Create a VM with 2 NICs on specific ovirt networks" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + interfaces_attributes: + - type: "interface" + primary: true + compute_attributes: + name: "nic1" + network: "969efbe6-f9e0-4383-a19a-a7ee65ad5007" + interface: "virtio" + - type: "interface" + name: "new_host_nic2" + managed: true + compute_attributes: + name: "nic2" + network: "969efbe6-f9e0-4383-a19a-a7ee65ad5008" + interface: "e1000" + state: present + +- name: "Delete a host" + theforeman.foreman.host: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "new_host" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + hosts: + description: List of hosts. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ensure_puppetclasses, + interfaces_spec, + ForemanEntityAnsibleModule, + HostMixin, +) + + +def ensure_host_interfaces(module, entity, interfaces): + scope = {'host_id': entity['id']} + + current_interfaces = module.list_resource('interfaces', params=scope) + current_interfaces_ids = {x['id'] for x in current_interfaces} + expected_interfaces_ids = set() + + for interface in interfaces: + if 1 == len(current_interfaces) == len(interfaces): + existing_interface = current_interfaces[0] + else: + for possible_identifier in ['identifier', 'name', 'mac']: + if possible_identifier in interface: + unique_identifier = possible_identifier + break + else: + unique_identifier = None + warning_msg = "The provided interface definition has no unique identifier and thus cannot be matched against existing interfaces. " \ + "This will always create a new interface and might not be the desired behaviour." + module.warn(warning_msg) + + existing_interface = next((x for x in current_interfaces if unique_identifier and x.get(unique_identifier) == interface[unique_identifier]), None) + + if 'mac' in interface: + interface['mac'] = interface['mac'].lower() + + # workaround for https://projects.theforeman.org/issues/31390 + if existing_interface is not None and 'attached_devices' in existing_interface: + existing_interface['attached_devices'] = existing_interface['attached_devices'].split(',') + + updated_interface = (existing_interface or {}).copy() + updated_interface.update(interface) + + module.ensure_entity('interfaces', updated_interface, existing_interface, params=scope, state='present', + foreman_spec=module.foreman_spec['interfaces_attributes']['foreman_spec']) + + if 'id' in updated_interface: + expected_interfaces_ids.add(updated_interface['id']) + + for leftover_interface in current_interfaces_ids - expected_interfaces_ids: + module.ensure_entity('interfaces', {}, {'id': leftover_interface}, params=scope, state='absent', + foreman_spec=module.foreman_spec['interfaces_attributes']['foreman_spec']) + + +class ForemanHostModule(HostMixin, ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanHostModule( + foreman_spec=dict( + name=dict(required=True), + hostgroup=dict(type='entity'), + location=dict(type='entity'), + organization=dict(type='entity'), + enabled=dict(type='bool'), + managed=dict(type='bool'), + build=dict(type='bool'), + ip=dict(), + mac=dict(), + comment=dict(), + owner=dict(type='entity', resource_type='users', flat_name='owner_id'), + owner_group=dict(type='entity', resource_type='usergroups', flat_name='owner_id'), + owner_type=dict(invisible=True), + provision_method=dict(choices=['build', 'image', 'bootdisk']), + image=dict(type='entity', scope=['compute_resource']), + compute_attributes=dict(type='dict'), + interfaces_attributes=dict(type='nested_list', foreman_spec=interfaces_spec, ensure=True), + ), + mutually_exclusive=[ + ['owner', 'owner_group'] + ], + required_by=dict( + image=('compute_resource',), + ), + ) + + # additional param validation + if '.' not in module.foreman_params['name']: + module.fail_json(msg="The hostname must be FQDN") + + if not module.desired_absent: + if 'build' in module.foreman_params and module.foreman_params['build']: + # When 'build'=True, 'managed' has to be True. Assuming that user's priority is to build. + if 'managed' in module.foreman_params and not module.foreman_params['managed']: + module.warn("when 'build'=True, 'managed' is ignored and forced to True") + module.foreman_params['managed'] = True + elif 'build' not in module.foreman_params and 'managed' in module.foreman_params and not module.foreman_params['managed']: + # When 'build' is not given and 'managed'=False, have to clear 'build' context that might exist on the server. + module.foreman_params['build'] = False + + if 'mac' in module.foreman_params: + module.foreman_params['mac'] = module.foreman_params['mac'].lower() + + if 'owner' in module.foreman_params: + module.foreman_params['owner_type'] = 'User' + elif 'owner_group' in module.foreman_params: + module.foreman_params['owner_type'] = 'Usergroup' + + with module.api_connection(): + entity = module.lookup_entity('entity') + + if not module.desired_absent: + module.auto_lookup_entities() + + if 'image' in module.foreman_params: + if 'compute_attributes' not in module.foreman_params: + module.foreman_params['compute_attributes'] = {} + module.foreman_params['compute_attributes']['image_id'] = module.foreman_params['image']['uuid'] + + if 'compute_resource' in module.foreman_params: + compute_resource = module.foreman_params['compute_resource'] + cluster = None + if 'compute_attributes' in module.foreman_params: + if 'cluster' in module.foreman_params['compute_attributes']: + cluster = module.find_cluster(module.foreman_params['compute_attributes']['cluster'], compute_resource) + module.foreman_params['compute_attributes']['cluster'] = cluster['_api_identifier'] + + if 'volumes_attributes' in module.foreman_params['compute_attributes']: + for volume in module.foreman_params['compute_attributes']['volumes_attributes'].values(): + if 'storage_pod' in volume: + storage_pod = module.find_storage_pod(volume['storage_pod'], compute_resource, cluster) + volume['storage_pod'] = storage_pod['id'] + if 'storage_domain' in volume: + storage_domain = module.find_storage_domain(volume['storage_domain'], compute_resource, cluster) + volume['storage_domain'] = storage_domain['id'] + + if 'interfaces_attributes' in module.foreman_params: + for interface in module.foreman_params['interfaces_attributes']: + if 'compute_attributes' in interface and 'network' in interface['compute_attributes']: + network = module.find_network(interface['compute_attributes']['network'], compute_resource, cluster) + interface['compute_attributes']['network'] = network['id'] + + # We use different APIs for creating a host with interfaces + # and updating it, so let's differentiate based on entity being present or not + if entity and 'interfaces_attributes' in module.foreman_params: + interfaces = module.foreman_params.pop('interfaces_attributes') + else: + interfaces = None + + expected_puppetclasses = module.foreman_params.pop('puppetclasses', None) + + entity = module.run() + + if not module.desired_absent: + if 'environment_id' in entity: + ensure_puppetclasses(module, 'host', entity, expected_puppetclasses) + if interfaces is not None: + ensure_host_interfaces(module, entity, interfaces) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/host_collection.py b/ansible_collections/theforeman/foreman/plugins/modules/host_collection.py new file mode 100644 index 00000000..cfd2cae9 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/host_collection.py @@ -0,0 +1,100 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019, Maxim Burgerhout <maxim@wzzrd.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: host_collection +version_added: 1.0.0 +short_description: Manage Host Collections +description: + - Create and Manage host collections +author: + - "Maxim Burgerhout (@wzzrd)" + - "Christoffer Reijer (@ephracis)" +options: + description: + description: + - Description of the host collection + required: false + type: str + name: + description: + - Name of the host collection + required: true + type: str + updated_name: + description: + - New name of the host collection. When this parameter is set, the module will not be idempotent. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create Foo host collection" + theforeman.foreman.host_collection: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Foo" + description: "Foo host collection for Foo servers" + organization: "My Cool new Organization" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + host_collections: + description: List of host collections. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloHostCollectionModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloHostCollectionModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + description=dict(), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/host_errata_info.py b/ansible_collections/theforeman/foreman/plugins/modules/host_errata_info.py new file mode 100644 index 00000000..7d9c69ac --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/host_errata_info.py @@ -0,0 +1,117 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: host_errata_info +version_added: 2.1.0 +short_description: Fetch information about Host Errata +description: + - Fetch information about Host Errata +author: + - "Evgeni Golov (@evgeni)" +options: + host: + description: + - Name of the host to fetch errata for. + required: true + type: str + content_view: + description: + - Calculate Applicable Errata based on a particular Content View. + - Required together with I(lifecycle_environment). + - If this is set, I(organization) also needs to be set. + required: false + type: str + lifecycle_environment: + description: + - Calculate Applicable Errata based on a particular Lifecycle Environment. + - Required together with I(content_view). + - If this is set, I(organization) also needs to be set. + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodulewithoutname +''' + +EXAMPLES = ''' +- name: "List installable errata for host" + theforeman.foreman.host_errata_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + host: "host.example.com" + +- name: "List applicable errata for host" + theforeman.foreman.host_errata_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + host: "host.example.com" + lifecycle_environment: "Library" + content_view: "Default Organization View" +''' + +RETURN = ''' +host_errata: + description: List of all found errata for the host and their details + returned: success + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule +) + + +class ForemanHostErrataInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanHostErrataInfo( + foreman_spec=dict( + name=dict(invisible=True), + host=dict(type='entity', required=True), + content_view=dict(type='entity', scope=['organization']), + lifecycle_environment=dict(type='entity', flat_name='environment_id', scope=['organization']), + ), + entity_opts=dict( + resource_type='host_errata', + ), + required_together=[ + ('content_view', 'lifecycle_environment'), + ], + required_by={ + 'content_view': 'organization', + 'lifecycle_environment': 'organization', + }, + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/host_info.py b/ansible_collections/theforeman/foreman/plugins/modules/host_info.py new file mode 100644 index 00000000..73ca3256 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/host_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: host_info +version_added: 2.0.0 +short_description: Fetch information about Hosts +description: + - Fetch information about Hosts +author: + - "Evgeni Golov (@evgeni)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a host" + theforeman.foreman.host_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "host.example.com" + +- name: "Show all hosts with domain example.com" + theforeman.foreman.host_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "domain = example.com" +''' + +RETURN = ''' +host: + description: Details about the found host + returned: success and I(name) was passed + type: dict +hosts: + description: List of all found hosts and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanHostInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanHostInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/host_power.py b/ansible_collections/theforeman/foreman/plugins/modules/host_power.py new file mode 100644 index 00000000..c594f04f --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/host_power.py @@ -0,0 +1,137 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: host_power +version_added: 1.0.0 +short_description: Manage Power State of Hosts +description: + - "Manage power state of a host" + - "This beta version can start and stop an existing foreman host and question the current power state." +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" + - "Baptiste Agasse (@bagasse)" +options: + name: + description: Name (FQDN) of the host + required: true + aliases: + - hostname + type: str + state: + description: Desired power state + default: state + choices: + - 'on' + - 'start' + - 'off' + - 'stop' + - 'soft' + - 'reboot' + - 'cycle' + - 'reset' + - 'state' + - 'status' + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: "Switch a host on" + theforeman.foreman.host_power: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + hostname: "test-host.domain.test" + state: on + +- name: "Switch a host off" + theforeman.foreman.host_power: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + hostname: "test-host.domain.test" + state: off + +- name: "Query host power state" + theforeman.foreman.host_power: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + hostname: "test-host.domain.test" + state: state + register: result +- debug: + msg: "Host power state is {{ result.power_state }}" + + +''' + +RETURN = ''' +power_state: + description: current power state of host + returned: always + type: str + sample: "off" + ''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +def main(): + module = ForemanEntityAnsibleModule( + foreman_spec=dict( + name=dict(aliases=['hostname'], required=True), + ), + argument_spec=dict( + state=dict(default='state', choices=['on', 'start', 'off', 'stop', 'soft', 'reboot', 'cycle', 'reset', 'state', 'status']), + ) + ) + + module_params = module.foreman_params + + with module.api_connection(): + # power_status endpoint was only added in foreman 1.22.0 per https://projects.theforeman.org/issues/25436 + # Delete this piece when versions below 1.22 are off common use + # begin delete + if 'power_status' not in module.foremanapi.resource('hosts').actions: + params = {'id': module_params['name'], 'power_action': 'status'} + power_state = module.resource_action('hosts', 'power', params=params, ignore_check_mode=True) + power_state['state'] = 'on' if power_state['power'] == 'running' else 'off' + else: + # end delete (on delete un-indent the below two lines) + params = {'id': module_params['name']} + power_state = module.resource_action('hosts', 'power_status', params=params, ignore_check_mode=True) + + if module.state in ['state', 'status']: + module.exit_json(power_state=power_state['state']) + elif ((module.state in ['on', 'start'] and power_state['state'] == 'on') + or (module.state in ['off', 'stop'] and power_state['state'] == 'off')): + module.exit_json() + else: + params['power_action'] = module.state + module.resource_action('hosts', 'power', params=params) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/hostgroup.py b/ansible_collections/theforeman/foreman/plugins/modules/hostgroup.py new file mode 100644 index 00000000..73f0cae9 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/hostgroup.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: hostgroup +version_added: 1.0.0 +short_description: Manage Hostgroups +description: + - Create, update, and delete Hostgroups +author: + - "Manisha Singhal (@Manisha15) ATIX AG" + - "Baptiste Agasse (@bagasse)" +options: + name: + description: Name of hostgroup + required: true + type: str + updated_name: + description: New name of hostgroup. When this parameter is set, the module will not be idempotent. + type: str + description: + description: Description of hostgroup + required: false + type: str + parent: + description: Hostgroup parent name + required: false + type: str + organization: + description: + - Organization for scoped resources attached to the hostgroup. + - Only used for Katello installations. + - This organization will implicitly be added to the I(organizations) parameter if needed. + required: false + type: str + parameters: + description: + - Hostgroup specific host parameters + ansible_roles: + description: + - A list of ansible roles to associate with the hostgroup. + - The foreman-ansible plugin must be installed to use this parameter. + required: false + type: list + elements: str + version_added: 2.1.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.nested_parameters + - theforeman.foreman.foreman.host_options + - theforeman.foreman.foreman.operatingsystem +''' + +EXAMPLES = ''' +- name: "Create a Hostgroup" + theforeman.foreman.hostgroup: + name: "new_hostgroup" + architecture: "architecture_name" + operatingsystem: "operatingsystem_name" + medium: "media_name" + ptable: "Partition_table_name" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: "Update a Hostgroup" + theforeman.foreman.hostgroup: + name: "new_hostgroup" + architecture: "updated_architecture_name" + operatingsystem: "updated_operatingsystem_name" + organizations: + - Org One + - Org Two + locations: + - Loc One + - Loc Two + - Loc One/Nested loc + medium: "updated_media_name" + ptable: "updated_Partition_table_name" + root_pass: "password" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: "My nested hostgroup" + theforeman.foreman.hostgroup: + parent: "new_hostgroup" + name: "my nested hostgroup" + +- name: "My hostgroup with some proxies" + theforeman.foreman.hostgroup: + name: "my hostgroup" + environment: production + puppet_proxy: puppet-proxy.example.com + puppet_ca_proxy: puppet-proxy.example.com + openscap_proxy: openscap-proxy.example.com + +- name: "My katello related hostgroup" + theforeman.foreman.hostgroup: + organization: "My Org" + name: "kt hostgroup" + content_source: capsule.example.com + lifecycle_environment: "Production" + content_view: "My content view" + parameters: + - name: "kt_activation_keys" + value: "my_prod_ak" + +- name: "Delete a Hostgroup" + theforeman.foreman.hostgroup: + name: "new_hostgroup" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + hostgroups: + description: List of hostgroups. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ensure_puppetclasses, + HostMixin, + ForemanTaxonomicEntityAnsibleModule, +) + + +class ForemanHostgroupModule(HostMixin, ForemanTaxonomicEntityAnsibleModule): + PARAMETERS_FLAT_NAME = 'group_parameters_attributes' + + +def main(): + module = ForemanHostgroupModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + parent=dict(type='entity'), + ansible_roles=dict(type='entity_list', ensure=False), + organization=dict(type='entity', required=False, ensure=False), + ), + argument_spec=dict( + updated_name=dict(), + ), + required_by=dict( + content_source=('organization',), + content_view=('organization',), + lifecycle_environment=('organization',), + ), + required_plugins=[('ansible', ['ansible_roles'])], + ) + + module_params = module.foreman_params + with module.api_connection(): + old_entity = module.lookup_entity('entity') + if not module.desired_absent: + if 'organization' in module_params: + if 'organizations' in module_params: + if module_params['organization'] not in module_params['organizations']: + module_params['organizations'].append(module_params['organization']) + else: + module_params['organizations'] = [module_params['organization']] + expected_puppetclasses = module_params.pop('puppetclasses', None) + entity = module.run() + + if not module.desired_absent and 'environment_id' in entity: + ensure_puppetclasses(module, 'hostgroup', entity, expected_puppetclasses) + + ansible_roles = module_params.get('ansible_roles') + if not module.desired_absent and ansible_roles is not None: + desired_ansible_role_ids = [item['id'] for item in ansible_roles] + current_ansible_role_ids = [ + item['id'] for item in module.resource_action( + 'hostgroups', 'ansible_roles', {'id': entity['id']}, + ignore_check_mode=True, record_change=False, + ) + ] if old_entity else [] + if set(current_ansible_role_ids) != set(desired_ansible_role_ids): + module.resource_action( + 'hostgroups', 'assign_ansible_roles', + {'id': entity['id'], 'ansible_role_ids': desired_ansible_role_ids}, + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/hostgroup_info.py b/ansible_collections/theforeman/foreman/plugins/modules/hostgroup_info.py new file mode 100644 index 00000000..2bd9d84e --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/hostgroup_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2023 Louis Tiches HallasTech +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: hostgroup_info +version_added: 3.9.0 +short_description: Get information about hostgroup(s) +description: + - Get information about hostgroup(s) +author: + - "Louis Tiches (@TheRedGreek)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a hostgroup" + theforeman.foreman.hostgroup_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Default Hostgroup" + +- name: "Show all hostgroups with 'name ~ Default'" + theforeman.foreman.hostgroup_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "name ~ Default" +''' + +RETURN = ''' +hostgroup: + description: Details about the found hostgroup + returned: success and I(name) was passed + type: dict +hostgroups: + description: List of all found hostgroups and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanHostgroupInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanHostgroupInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/http_proxy.py b/ansible_collections/theforeman/foreman/plugins/modules/http_proxy.py new file mode 100644 index 00000000..0c467dfa --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/http_proxy.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: http_proxy +version_added: 1.1.0 +short_description: Manage HTTP Proxies +description: + - Create, update, and delete HTTP Proxies +author: + - "Evgeni Golov (@evgeni)" +options: + name: + description: + - The HTTP Proxy name + required: true + type: str + url: + description: + - URL of the HTTP Proxy + - Required when creating a new HTTP Proxy. + required: False + type: str + proxy_username: + description: + - Username used to authenticate with the HTTP Proxy + required: False + type: str + proxy_password: + description: + - Password used to authenticate with the HTTP Proxy + - When this parameter is set, the module will not be idempotent. + required: False + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: create example.org proxy + theforeman.foreman.http_proxy: + name: "example.org" + url: "http://example.org:3128" + locations: + - "Munich" + organizations: + - "ACME" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + http_proxies: + description: List of HTTP proxies. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +class ForemanHttpProxyModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanHttpProxyModule( + foreman_spec=dict( + name=dict(required=True), + url=dict(), + proxy_username=dict(flat_name='username'), + proxy_password=dict(no_log=True, flat_name='password'), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + if not module.desired_absent: + if 'url' not in module.foreman_params: + if not entity: + module.fail_json(msg="The 'url' parameter is required when creating a new HTTP Proxy.") + else: + module.foreman_params['url'] = entity['url'] + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/image.py b/ansible_collections/theforeman/foreman/plugins/modules/image.py new file mode 100644 index 00000000..ff9e41d3 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/image.py @@ -0,0 +1,135 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Mark Hlawatschek (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: image +version_added: 1.0.0 +short_description: Manage Images +description: + - Create, update, and delete Images +author: + - "Mark Hlawatschek (@hlawatschek) ATIX AG" +options: + name: + description: Image name + required: true + type: str + compute_resource: + description: Compute resource the image is assigned to + required: true + type: str + uuid: + aliases: + - image_uuid + description: UUID or Marketplace URN of the operatingsystem image + required: true + type: str + image_username: + description: Username that is used to login into the operating system + required: true + type: str + image_password: + description: Password that is used to login into the operating system + required: false + type: str + operatingsystem: + required: true + architecture: + description: architecture of the image + required: true + type: str + user_data: + description: Image supports user_data + required: false + type: bool +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.operatingsystem +''' + +EXAMPLES = ''' +- name: create Image for EC2 + theforeman.foreman.image: + name: CentOS + image_uuid: "ami-0ff760d16d9497662" + image_username: "centos" + operatingsystem: "CentOS 7" + compute_resource: "AWS" + architecture: "x86_64" +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + images: + description: List of images. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanImageModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanImageModule( + argument_spec=dict( + image_username=dict(required=True), + image_password=dict(no_log=True), + ), + foreman_spec=dict( + name=dict(required=True), + username=dict(invisible=True), + uuid=dict(required=True, aliases=['image_uuid']), + password=dict(invisible=True, no_log=True), + compute_resource=dict(type='entity', required=True), + architecture=dict(type='entity', required=True), + operatingsystem=dict(type='entity', required=True), + user_data=dict(type='bool') + ), + entity_opts={'scope': ['compute_resource']}, + ) + + module.foreman_params['username'] = module.foreman_params.pop('image_username') + if 'image_password' in module.foreman_params: + module.foreman_params['password'] = module.foreman_params.pop('image_password') + with module.api_connection(): + scope = module.scope_for('compute_resource') + operatingsystem_id = module.lookup_entity('operatingsystem')['id'] + module.set_entity('entity', module.find_resource( + 'images', + search='name="{0}",operatingsystem="{1}"'.format(module.foreman_params['name'], operatingsystem_id), + params=scope, + failsafe=True, + )) + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/installation_medium.py b/ansible_collections/theforeman/foreman/plugins/modules/installation_medium.py new file mode 100644 index 00000000..6a0930e4 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/installation_medium.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Manuel Bonk (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: installation_medium +version_added: 1.0.0 +short_description: Manage Installation Media +description: + - Create, update, and delete Installation Media +author: + - "Manuel Bonk(@manuelbonk) ATIX AG" +options: + name: + description: + - The full installation medium name. + - The special name "*" (only possible as parameter) is used to perform bulk actions (modify, delete) on all existing partition tables. + required: true + type: str + updated_name: + description: New full installation medium name. When this parameter is set, the module will not be idempotent. + type: str + os_family: + description: + - The OS family the template shall be assigned with. + - If no os_family is set but a operatingsystem, the value will be derived from it. + path: + description: Path to the installation medium + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.os_family + - theforeman.foreman.foreman.operatingsystems +''' + +EXAMPLES = ''' +- name: create new debian medium + theforeman.foreman.installation_medium: + name: "wheezy" + locations: + - "Munich" + organizations: + - "ACME" + operatingsystems: + - "Debian" + path: "http://debian.org/mirror/" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + media: + description: List of installation media. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule, OS_LIST + + +class ForemanInstallationMediumModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanInstallationMediumModule( + argument_spec=dict( + updated_name=dict(), + state=dict(default='present', choices=['present', 'present_with_defaults', 'absent']), + ), + foreman_spec=dict( + name=dict(required=True), + operatingsystems=dict(type='entity_list'), + os_family=dict(choices=OS_LIST), + path=dict(), + ), + entity_opts=dict( + resource_type='media', + ), + entity_name='medium', + ) + + module_params = module.foreman_params + entity = None + + name = module_params['name'] + + affects_multiple = name == '*' + # sanitize user input, filter unuseful configuration combinations with 'name: *' + if affects_multiple: + if module.state == 'present_with_defaults': + module.fail_json(msg="'state: present_with_defaults' and 'name: *' cannot be used together") + if module.params['updated_name']: + module.fail_json(msg="updated_name not allowed if 'name: *'!") + if module.desired_absent: + further_params = set(module_params.keys()) - {'name', 'entity'} + if further_params: + module.fail_json(msg='When deleting all installation media, there is no need to specify further parameters: %s ' % further_params) + + with module.api_connection(): + if affects_multiple: + module.set_entity('entity', None) # prevent lookup + entities = module.list_resource('media') + if not entities: + # Nothing to do shortcut to exit + module.exit_json() + if not module.desired_absent: # not 'thin' + entities = [module.show_resource('media', entity['id']) for entity in entities] + module.auto_lookup_entities() + module_params.pop('name') + for entity in entities: + module.ensure_entity('media', module_params, entity) + else: + entity = module.lookup_entity('entity') + if not module.desired_absent and 'operatingsystems' in module_params: + operatingsystems = module.lookup_entity('operatingsystems') + if len(operatingsystems) == 1 and 'os_family' not in module_params and entity is None: + module_params['os_family'] = module.show_resource('operatingsystems', operatingsystems[0]['id'])['family'] + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/job_invocation.py b/ansible_collections/theforeman/foreman/plugins/modules/job_invocation.py new file mode 100644 index 00000000..80ae2f84 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/job_invocation.py @@ -0,0 +1,225 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Peter Ondrejka <pondrejk@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: job_invocation +short_description: Invoke Remote Execution Jobs +version_added: 1.4.0 +description: + - "Invoke and schedule Remote Execution Jobs" +author: + - "Peter Ondrejka (@pondrejk)" +options: + search_query: + description: + - Search query to identify hosts + type: str + bookmark: + description: + - Bookmark to infer the search query from + type: str + job_template: + description: + - Job template to execute + required: true + type: str + targeting_type: + description: + - Dynamic query updates the search results before execution (useful for scheduled jobs) + choices: + - static_query + - dynamic_query + default: static_query + type: str + randomized_ordering: + description: + - Whether to order the selected hosts randomly + type: bool + execution_timeout_interval: + description: + - Override the timeout interval from the template for this invocation only + type: int + ssh: + description: + - ssh related options + type: dict + suboptions: + effective_user: + description: + - What user should be used to run the script (using sudo-like mechanisms) + - Defaults to a template parameter or global setting + type: str + command: + description: + - Command to be executed on host. Required for command templates + type: str + inputs: + description: + - Inputs to use + type: dict + recurrence: + description: + - Schedule a recurring job + type: dict + suboptions: + cron_line: + description: + - How often the job should occur, in the cron format + type: str + max_iteration: + description: + - Repeat a maximum of N times + type: int + end_time: + description: + - Perform no more executions after this time + type: str + scheduling: + description: + - Schedule the job to start at a later time + type: dict + suboptions: + start_at: + description: + - Schedule the job for a future time + type: str + start_before: + description: + - Indicates that the action should be cancelled if it cannot be started before this time. + type: str + concurrency_control: + description: + - Control concurrency level and distribution over time + type: dict + suboptions: + time_span: + description: + - Distribute tasks over given number of seconds + type: int + concurrency_level: + description: + - Maximum jobs to be executed at once + type: int +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' + +- name: "Run remote command on a single host once" + theforeman.foreman.job_invocation: + search_query: "name ^ (foreman.example.com)" + command: 'ls' + job_template: "Run Command - SSH Default" + ssh: + effective_user: "tester" + +- name: "Run ansible command on active hosts once a day" + theforeman.foreman.job_invocation: + bookmark: 'active' + command: 'pwd' + job_template: "Run Command - Ansible Default" + recurrence: + cron_line: "30 2 * * *" + concurrency_control: + concurrency_level: 2 +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + job_invocations: + description: List of job invocations + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanAnsibleModule, +) + +ssh_foreman_spec = { + 'effective_user': dict(), +} + +recurrence_foreman_spec = { + 'cron_line': dict(), + 'max_iteration': dict(type='int'), + 'end_time': dict(), +} + +scheduling_foreman_spec = { + 'start_at': dict(), + 'start_before': dict(), +} + +concurrency_control_foreman_spec = { + 'time_span': dict(type='int'), + 'concurrency_level': dict(type='int'), +} + + +class ForemanJobInvocationModule(ForemanAnsibleModule): + pass + + +def main(): + module = ForemanJobInvocationModule( + foreman_spec=dict( + search_query=dict(), + bookmark=dict(type='entity'), + job_template=dict(required=True, type='entity'), + targeting_type=dict(default='static_query', choices=['static_query', 'dynamic_query']), + randomized_ordering=dict(type='bool'), + command=dict(), + inputs=dict(type='dict'), + execution_timeout_interval=dict(type='int'), + ssh=dict(type='dict', options=ssh_foreman_spec), + recurrence=dict(type='dict', options=recurrence_foreman_spec), + scheduling=dict(type='dict', options=scheduling_foreman_spec), + concurrency_control=dict(type='dict', options=concurrency_control_foreman_spec), + ), + required_one_of=[['search_query', 'bookmark']], + required_if=[ + ['job_template', 'Run Command - SSH Default', ['command']], + ['job_template', 'Run Command - Ansible Default', ['command']], + ], + ) + + # command input required by api + if 'command' in module.foreman_params: + module.foreman_params['inputs'] = {"command": module.foreman_params.pop('command')} + + with module.api_connection(): + if 'bookmark' in module.foreman_params: + module.set_entity('bookmark', module.find_resource('bookmarks', search='name="{0}",controller="hosts"'.format( + module.foreman_params['bookmark']), + failsafe=False, + )) + module.auto_lookup_entities() + module.ensure_entity('job_invocations', module.foreman_params, None, state='present') + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/job_template.py b/ansible_collections/theforeman/foreman/plugins/modules/job_template.py new file mode 100644 index 00000000..4415d01c --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/job_template.py @@ -0,0 +1,476 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Manuel Bonk & Matthias Dellweg (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: job_template +version_added: 1.0.0 +short_description: Manage Job Templates +description: + - Manage Remote Execution Job Templates +author: + - "Manuel Bonk (@manuelbonk) ATIX AG" + - "Matthias Dellweg (@mdellweg) ATIX AG" +options: + audit_comment: + description: + - Content of the audit comment field + type: str + description_format: + description: + - description of the job template. Template inputs can be referenced. + type: str + file_name: + description: + - The path of a template file, that shall be imported. + - Either this or I(template) is required as a source for the Job Template "content". + type: path + job_category: + description: + - The category the template should be assigend to + type: str + locked: + description: + - Determines whether the template shall be locked + default: false + type: bool + name: + description: + - The name of the Job Template. + - If omited, will be determined from the C(name) header of the template or the filename (in that order). + - The special value "*" can be used to perform bulk actions (modify, delete) on all existing templates. + type: str + provider_type: + description: + - Determines via which provider the template shall be executed + required: false + type: str + snippet: + description: + - Determines whether the template shall be a snippet + type: bool + template: + description: + - The content of the Job Template. + - Either this or I(file_name) is required as a source for the Job Template "content". + type: str + template_inputs: + description: + - The template inputs used in the Job Template + type: list + elements: dict + suboptions: + advanced: + description: + - Template Input is advanced + type: bool + description: + description: + - description of the Template Input + type: str + fact_name: + description: + - Fact name to use. + - Required when I(input_type=fact). + type: str + input_type: + description: + - input type + required: true + choices: + - user + - fact + - variable + - puppet_parameter + type: str + name: + description: + - name of the Template Input + required: true + type: str + options: + description: + - Template values for user inputs. Must be an array of any type. + type: list + elements: raw + puppet_class_name: + description: + - Puppet class name. + - Required when I(input_type=puppet_parameter). + type: str + puppet_parameter_name: + description: + - Puppet parameter name. + - Required when I(input_type=puppet_parameter). + type: str + required: + description: + - Is the input required + type: bool + variable_name: + description: + - Variable name to use. + - Required when I(input_type=variable). + type: str + value_type: + description: + - Type of the value + choices: + - plain + - search + - date + - resource + type: str + resource_type: + description: + - Type of the resource + type: str + hidden_value: + description: + - The value contains sensitive information and should't be normally visible, useful e.g. for passwords + type: bool + default: + description: + - Default value for user input + type: str + version_added: 3.8.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' + +- name: "Create a Job Template inline" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: A New Job Template + state: present + template: | + <%# + name: A Job Template + %> + rm -rf <%= input("toDelete") %> + template_inputs: + - name: toDelete + input_type: user + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "Create a Job Template from a file" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: a new job template + file_name: timeywimey_template.erb + template_inputs: + - name: a new template input + input_type: user + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "remove a job template's template inputs" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: a new job template + template_inputs: [] + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "Delete a Job Template" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: timeywimey + state: absent + +- name: "Create a Job Template from a file and modify with parameter(s)" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: timeywimey_template.erb + name: Wibbly Wobbly Template + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +# Providing a name in this case wouldn't be very sensible. +# Alternatively make use of with_filetree to parse recursively with filter. +- name: Parsing a directory of Job templates + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: "{{ item }}" + state: present + locations: + - SKARO + organizations: + - DALEK INC + with_fileglob: + - "./arsenal_templates/*.erb" + +# If the templates are stored locally and the ansible module is executed on a remote host +- name: Ensure latest version of all your Job Templates + theforeman.foreman.job_template: + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + template: '{{ lookup("file", item.src) }}' + with_filetree: '/path/to/job/templates' + when: item.state == 'file' + + +# with name set to "*" bulk actions can be performed +- name: "Delete *ALL* Job Templates" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: absent + +- name: "Assign all Job Templates to the same organization(s)" + theforeman.foreman.job_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: present + organizations: + - DALEK INC + - sky.net + - Doc Brown's garage + +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + job_templates: + description: List of job templates. + type: list + elements: dict + template_inputs: + description: List of template inputs associated with the job template. + type: list + elements: dict +''' + +import os +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, + parse_template, + parse_template_from_file, +) + + +template_defaults = { + 'provider_type': 'SSH', + 'job_category': 'unknown', +} + + +template_input_foreman_spec = { + 'id': dict(invisible=True), + 'name': dict(required=True), + 'description': dict(), + 'required': dict(type='bool'), + 'advanced': dict(type='bool'), + 'input_type': dict(required=True, choices=[ + 'user', + 'fact', + 'variable', + 'puppet_parameter', + ]), + 'fact_name': dict(), + 'variable_name': dict(), + 'puppet_class_name': dict(), + 'puppet_parameter_name': dict(), + 'options': dict(type='list', elements='raw'), + 'value_type': dict(choices=[ + 'plain', + 'search', + 'date', + 'resource', + ]), + 'resource_type': dict(), + 'hidden_value': dict(type='bool'), + 'default': dict(), +} + + +class ForemanJobTemplateModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanJobTemplateModule( + foreman_spec=dict( + description_format=dict(), + job_category=dict(), + locked=dict(type='bool', default=False), + name=dict(), + provider_type=dict(), + snippet=dict(type='bool'), + template=dict(), + template_inputs=dict( + type='nested_list', + foreman_spec=template_input_foreman_spec, + required_if=( + ['input_type', 'fact', ('fact_name',)], + ['input_type', 'variable', ('variable_name',)], + ['input_type', 'puppet_parameter', ('puppet_class_name', 'puppet_parameter_name')], + ), + ), + ), + argument_spec=dict( + audit_comment=dict(), + file_name=dict(type='path'), + state=dict(default='present', choices=['absent', 'present_with_defaults', 'present']), + ), + mutually_exclusive=[ + ['file_name', 'template'], + ], + required_one_of=[ + ['name', 'file_name', 'template'], + ], + ) + + # We do not want a layout text for bulk operations + if module.foreman_params.get('name') == '*': + if module.foreman_params.get('file_name') or module.foreman_params.get('template'): + module.fail_json( + msg="Neither file_name nor template allowed if 'name: *'!") + + entity = None + file_name = module.foreman_params.pop('file_name', None) + + if file_name or 'template' in module.foreman_params: + if file_name: + parsed_dict = parse_template_from_file(file_name, module) + else: + parsed_dict = parse_template(module.foreman_params['template'], module) + # sanitize name from template data + # The following condition can actually be hit, when someone is trying to import a + # template with the name set to '*'. + # Besides not being sensible, this would go horribly wrong in this module. + if parsed_dict.get('name') == '*': + module.fail_json(msg="Cannot use '*' as a job template name!") + # module params are priorized + parsed_dict.update(module.foreman_params) + # make sure certain values are set + module.foreman_params = template_defaults.copy() + module.foreman_params.update(parsed_dict) + + # make sure, we have a name + if 'name' not in module.foreman_params: + if file_name: + module.foreman_params['name'] = os.path.splitext( + os.path.basename(file_name))[0] + else: + module.fail_json( + msg='No name specified and no filename to infer it.') + + affects_multiple = module.foreman_params['name'] == '*' + # sanitize user input, filter unuseful configuration combinations with 'name: *' + if affects_multiple: + if module.state == 'present_with_defaults': + module.fail_json(msg="'state: present_with_defaults' and 'name: *' cannot be used together") + if module.desired_absent: + further_params = set(module.foreman_params.keys()) - {'name', 'entity'} + if further_params: + module.fail_json(msg='When deleting all job templates, there is no need to specify further parameters: %s ' % further_params) + + with module.api_connection(): + if 'audit_comment' in module.foreman_params: + extra_params = {'audit_comment': module.foreman_params['audit_comment']} + else: + extra_params = {} + + if affects_multiple: + module.set_entity('entity', None) # prevent lookup + entities = module.list_resource('job_templates') + if not entities: + # Nothing to do; shortcut to exit + module.exit_json() + if not module.desired_absent: # not 'thin' + entities = [module.show_resource('job_templates', entity['id']) for entity in entities] + module.auto_lookup_entities() + module.foreman_params.pop('name') + for entity in entities: + module.ensure_entity('job_templates', module.foreman_params, entity, params=extra_params) + else: + # The name could have been determined to late, so copy it again + module.foreman_params['entity'] = module.foreman_params['name'] + entity = module.lookup_entity('entity') + # TemplateInputs need to be added as separate entities later + template_inputs = module.foreman_params.get('template_inputs') + + job_template = module.run(params=extra_params) + + update_dependent_entities = (module.state == 'present' or (module.state == 'present_with_defaults' and module.changed)) + if update_dependent_entities and template_inputs is not None: + scope = {'template_id': job_template['id']} + + # Manage TemplateInputs here + current_template_input_list = module.list_resource('template_inputs', params=scope) if entity else [] + current_template_inputs = {item['name']: item for item in current_template_input_list} + for template_input_dict in template_inputs: + template_input_entity = current_template_inputs.pop(template_input_dict['name'], None) + + module.ensure_entity( + 'template_inputs', template_input_dict, template_input_entity, + params=scope, foreman_spec=template_input_foreman_spec, + ) + + # At this point, desired template inputs have been removed from the dict. + for template_input_entity in current_template_inputs.values(): + module.ensure_entity( + 'template_inputs', None, template_input_entity, state="absent", + params=scope, foreman_spec=template_input_foreman_spec, + ) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/lifecycle_environment.py b/ansible_collections/theforeman/foreman/plugins/modules/lifecycle_environment.py new file mode 100644 index 00000000..a6ac287d --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/lifecycle_environment.py @@ -0,0 +1,118 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Andrew Kofink <ajkofink@gmail.com> +# (c) 2019, Baptiste Agasse <baptiste.agasse@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: lifecycle_environment +version_added: 1.0.0 +short_description: Manage Lifecycle Environments +description: + - Create and manage lifecycle environments +author: + - "Andrew Kofink (@akofink)" + - "Baptiste Agasse (@bagasse)" +options: + name: + description: + - Name of the lifecycle environment + required: true + type: str + label: + description: + - Label of the lifecycle environment. This field cannot be updated. + type: str + description: + description: + - Description of the lifecycle environment + type: str + prior: + description: + - Name of the parent lifecycle environment + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Add a production lifecycle environment" + theforeman.foreman.lifecycle_environment: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Production" + label: "production" + organization: "Default Organization" + prior: "Library" + description: "The production environment" + state: "present" +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + lifecycle_environments: + description: List of lifecycle environments. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloLifecycleEnvironmentModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloLifecycleEnvironmentModule( + foreman_spec=dict( + name=dict(required=True), + label=dict(), + description=dict(), + prior=dict(type='entity', resource_type='lifecycle_environments', scope=['organization']), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + # Default to 'Library' for new env with no 'prior' provided + if 'prior' not in module.foreman_params and not entity: + module.foreman_params['prior'] = 'Library' + + if entity and not module.desired_absent: + if 'label' in module.foreman_params and entity['label'] != module.foreman_params['label']: + module.fail_json(msg="Label cannot be updated on a lifecycle environment.") + + if 'prior' in module.foreman_params and entity['prior']['id'] != module.lookup_entity('prior')['id']: + module.fail_json(msg="Prior cannot be updated on a lifecycle environment.") + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/location.py b/ansible_collections/theforeman/foreman/plugins/modules/location.py new file mode 100644 index 00000000..e440c49e --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/location.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Matthias M Dellweg <dellweg@atix.de> (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: location +version_added: 1.0.0 +short_description: Manage Locations +description: + - Manage Locations +author: + - "Matthias M Dellweg (@mdellweg) ATIX AG" +options: + name: + description: + - Name of the Location + required: true + type: str + parent: + description: + - Title of a parent Location for nesting + type: str + organizations: + description: + - List of organizations the location should be assigned to + type: list + elements: str + ignore_types: + description: + - List of resources types that will be automatically associated + type: list + elements: str + required: false + aliases: + - select_all_types + version_added: 3.8.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.nested_parameters +''' + +EXAMPLES = ''' +# Create a simple location +- name: "Create CI Location" + theforeman.foreman.location: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My Cool New Location" + organizations: + - "Default Organization" + state: present + +# Create a nested location +- name: "Create Nested CI Location" + theforeman.foreman.location: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My Nested location" + parent: "My Cool New Location" + state: present + +# Create a new nested location with parent included in name +- name: "Create New Nested Location" + theforeman.foreman.location: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My Cool New Location/New nested location" + state: present + +# Move a nested location to another parent +- name: "Create Nested CI Location" + theforeman.foreman.location: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My Cool New Location/New nested location" + parent: "My Cool New Location/My Nested location" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + locations: + description: List of locations. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule, NestedParametersMixin + + +class ForemanLocationModule(NestedParametersMixin, ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanLocationModule( + foreman_spec=dict( + name=dict(required=True), + parent=dict(type='entity'), + organizations=dict(type='entity_list'), + ignore_types=dict(type='list', elements='str', required=False, aliases=['select_all_types']), + select_all_types=dict(type='list', invisible=True, flat_name='ignore_types'), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + # workround the fact that the API expects `ignore_types` when modifying the entity + # but uses `select_all_types` when showing one + if entity and 'select_all_types' in entity: + entity['ignore_types'] = entity.pop('select_all_types') + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/operatingsystem.py b/ansible_collections/theforeman/foreman/plugins/modules/operatingsystem.py new file mode 100644 index 00000000..0186f200 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/operatingsystem.py @@ -0,0 +1,233 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017 Matthias M Dellweg (ATIX AG) +# (c) 2017 Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: operatingsystem +version_added: 1.0.0 +short_description: Manage Operating Systems +description: + - Manage Operating Systems +author: + - "Matthias M Dellweg (@mdellweg) ATIX AG" + - "Bernhard Hopfenmüller (@Fobhep) ATIX AG" +options: + name: + description: + - Name of the Operating System + required: true + type: str + updated_name: + description: New operating system name. When this parameter is set, the module will not be idempotent. + type: str + release_name: + description: + - Release name of the operating system (recommended for debian) + type: str + description: + description: + - Description of the Operating System + required: false + type: str + os_family: + description: + - Distribution family of the Operating System + aliases: + - family + major: + description: + - major version of the Operating System + required: false + type: str + minor: + description: + - minor version of the Operating System + required: false + type: str + architectures: + description: + - architectures, the operating system can be installed on + required: false + type: list + elements: str + media: + description: + - list of installation media + required: false + type: list + elements: str + ptables: + description: + - list of partitioning tables + required: false + type: list + elements: str + provisioning_templates: + description: + - List of provisioning templates that are associated with the operating system. + - Specify the full list of template names you want to associate with your OS. + - For example ["Kickstart default", "Kickstart default finish", "Kickstart default iPXE", "custom"]. + - After specifying the template associations, you can set the default association in + - the M(theforeman.foreman.os_default_template) module. + required: false + type: list + elements: str + password_hash: + description: + - hashing algorithm for passwd + required: false + choices: + - MD5 + - SHA256 + - SHA512 + - Base64 + - Base64-Windows + type: str + parameters: + description: + - Operating System specific host parameters +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.nested_parameters + - theforeman.foreman.foreman.os_family +''' + +EXAMPLES = ''' +- name: "Create an Operating System" + theforeman.foreman.operatingsystem: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: Debian + release_name: stretch + family: Debian + major: 9 + parameters: + - name: additional-packages + value: python vim + state: present + +- name: "Ensure existence of an Operating System (provide default values)" + theforeman.foreman.operatingsystem: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: Centos + family: Redhat + major: 7 + password_hash: SHA256 + state: present_with_defaults + +- name: "Delete an Operating System" + theforeman.foreman.operatingsystem: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: Debian + family: Debian + major: 9 + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + operatinsystems: + description: List of operatinsystems. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanEntityAnsibleModule, + ParametersMixin, + OS_LIST, +) + + +class ForemanOperatingsystemModule(ParametersMixin, ForemanEntityAnsibleModule): + PARAMETERS_FLAT_NAME = 'os_parameters_attributes' + + +def main(): + module = ForemanOperatingsystemModule( + foreman_spec=dict( + name=dict(required=True), + release_name=dict(), + description=dict(), + os_family=dict(choices=OS_LIST, flat_name='family', aliases=['family']), + major=dict(), + minor=dict(), + architectures=dict(type='entity_list'), + media=dict(type='entity_list', flat_name='medium_ids', resource_type='media'), + ptables=dict(type='entity_list'), + provisioning_templates=dict(type='entity_list'), + password_hash=dict(choices=['MD5', 'SHA256', 'SHA512', 'Base64', 'Base64-Windows'], no_log=False), + ), + argument_spec=dict( + state=dict(default='present', choices=['present', 'present_with_defaults', 'absent']), + updated_name=dict(), + ), + required_if=[ + ['state', 'present', ['name', 'major', 'os_family']], + ['state', 'present_with_defaults', ['name', 'major', 'os_family']], + ], + required_one_of=[ + ['description', 'name'], + ['description', 'major'], + ], + ) + + module_params = module.foreman_params + + with module.api_connection(): + + # Try to find the Operating System to work on + # name is however not unique, but description is, as well as "<name> <major>[.<minor>]" + entity = None + # If we have a description, search for it + if 'description' in module_params and module_params['description'] != '': + search_string = 'description="{0}" or title="{0}"'.format(module_params['description']) + entity = module.find_resource('operatingsystems', search_string, failsafe=True) + # If we did not yet find a unique OS, search by name & version + # In case of state == absent, those information might be missing, we assume that we did not find an operatingsytem to delete then + if entity is None and 'name' in module_params and 'major' in module_params: + search_string = ','.join('{0}="{1}"'.format(key, module_params.get(key, '')) for key in ('name', 'major', 'minor')) + entity = module.find_resource('operatingsystems', search_string, failsafe=True) + + if not entity and (module.state == 'present' or module.state == 'present_with_defaults'): + # we actually attempt to create a new one... + for param_name in ['major', 'os_family', 'password_hash']: + if param_name not in module_params.keys(): + module.fail_json(msg='{0} is a required parameter to create a new operating system.'.format(param_name)) + + module.set_entity('entity', entity) + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/organization.py b/ansible_collections/theforeman/foreman/plugins/modules/organization.py new file mode 100644 index 00000000..c597c183 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/organization.py @@ -0,0 +1,116 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# (c) 2017, Matthias M Dellweg <dellweg@atix.de> (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: organization +version_added: 1.0.0 +short_description: Manage Organizations +description: + - Manage Organizations +author: + - "Eric D Helms (@ehelms)" + - "Matthias M Dellweg (@mdellweg) ATIX AG" +options: + name: + description: + - Name of the Organization + required: true + type: str + description: + description: + - Description of the Organization + required: false + type: str + label: + description: + - Label of the Organization + type: str + ignore_types: + description: + - List of resources types that will be automatically associated + type: list + elements: str + required: false + aliases: + - select_all_types + version_added: 3.8.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.nested_parameters +''' + +EXAMPLES = ''' +- name: "Create CI Organization" + theforeman.foreman.organization: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My Cool New Organization" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + organizations: + description: List of organizations. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule, NestedParametersMixin + + +class ForemanOrganizationModule(NestedParametersMixin, ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanOrganizationModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + label=dict(), + ignore_types=dict(type='list', elements='str', required=False, aliases=['select_all_types']), + select_all_types=dict(type='list', invisible=True, flat_name='ignore_types'), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + # workround the fact that the API expects `ignore_types` when modifying the entity + # but uses `select_all_types` when showing one + if entity and 'select_all_types' in entity: + entity['ignore_types'] = entity.pop('select_all_types') + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/organization_info.py b/ansible_collections/theforeman/foreman/plugins/modules/organization_info.py new file mode 100644 index 00000000..f9621445 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/organization_info.py @@ -0,0 +1,83 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Stejskal Leos (Red Hat) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: organization_info +version_added: 2.3.0 +short_description: Get information about organization(s) +description: + - Get information about organization(s) +author: + - "Stejskal Leos (@lstejska)" + +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a organization" + theforeman.foreman.organization_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Default Organization" + +- name: "Show all organizations with 'name ~ Default'" + theforeman.foreman.organization_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "name ~ Default" +''' + +RETURN = ''' +organization: + description: Details about the found organization + returned: success and I(name) was passed + type: dict +organizations: + description: List of all found organizations and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanOrganizationInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanOrganizationInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/os_default_template.py b/ansible_collections/theforeman/foreman/plugins/modules/os_default_template.py new file mode 100644 index 00000000..86e0422b --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/os_default_template.py @@ -0,0 +1,145 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017 Matthias M Dellweg (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: os_default_template +version_added: 1.0.0 +short_description: Manage Default Template Associations To Operating Systems +description: + - Manage OSDefaultTemplate Entities +author: + - "Matthias M Dellweg (@mdellweg) ATIX AG" +options: + operatingsystem: + required: true + template_kind: + description: + - name of the template kind + required: true + type: str + choices: + - Bootdisk + - cloud-init + - finish + - host_init_config + - iPXE + - job_template + - kexec + - POAP + - provision + - PXEGrub + - PXEGrub2 + - PXELinux + - registration + - script + - user_data + - ZTP + provisioning_template: + description: + - name of provisioning template + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.operatingsystem +''' + +EXAMPLES = ''' +- name: "Create an Association" + theforeman.foreman.os_default_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + operatingsystem: "CoolOS" + template_kind: "finish" + provisioning_template: "CoolOS finish" + state: present + +- name: "Delete an Association" + theforeman.foreman.os_default_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + operatingsystem: "CoolOS" + template_kind: "finish" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + os_default_templates: + description: List of operatingsystem default templates. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule, TEMPLATE_KIND_LIST + + +class ForemanOsDefaultTemplateModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanOsDefaultTemplateModule( + argument_spec=dict( + state=dict(default='present', choices=['present', 'present_with_defaults', 'absent']), + ), + foreman_spec=dict( + operatingsystem=dict(required=True, type='entity'), + template_kind=dict(required=True, choices=TEMPLATE_KIND_LIST, type='entity'), + provisioning_template=dict(type='entity', thin=False), + ), + required_if=( + ['state', 'present', ['provisioning_template']], + ['state', 'present_with_defaults', ['provisioning_template']], + ), + entity_opts={'scope': ['operatingsystem']}, + ) + + if 'provisioning_template' in module.foreman_params and module.desired_absent: + module.fail_json(msg='Provisioning template must not be specified for deletion.') + + with module.api_connection(): + template_kind_id = module.lookup_entity('template_kind')['id'] + if not module.desired_absent: + if module.lookup_entity('provisioning_template')['template_kind_id'] != template_kind_id: + module.fail_json(msg='Provisioning template kind mismatching.') + + scope = module.scope_for('operatingsystem') + # Default templates do not support a scoped search + # see: https://projects.theforeman.org/issues/27722 + entities = module.list_resource('os_default_templates', params=scope) + entity = next((item for item in entities if item['template_kind_id'] == template_kind_id), None) + module.set_entity('entity', entity) + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/partition_table.py b/ansible_collections/theforeman/foreman/plugins/modules/partition_table.py new file mode 100644 index 00000000..1ddccea7 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/partition_table.py @@ -0,0 +1,296 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017 Matthias Dellweg & Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: partition_table +version_added: 1.0.0 +short_description: Manage Partition Table Templates +description: + - Manage Partition Table Templates +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" + - "Matthias Dellweg (@mdellweg) ATIX AG" +options: + file_name: + description: + - The path of a template file, that shall be imported. + - Either this or I(layout) is required as a source for the Partition Template "content". + required: false + type: path + layout: + description: + - The content of the Partitioning Table Template + - Either this or I(file_name) is required as a source for the Partition Template "content". + required: false + type: str + locked: + description: + - Determines whether the template shall be locked + required: false + type: bool + name: + description: + - The name of the Partition Table. + - If omited, will be determined from the C(name) header of the template or the filename (in that order). + - The special value "*" can be used to perform bulk actions (modify, delete) on all existing Partition Tables. + required: false + type: str + updated_name: + description: New name of the template. When this parameter is set, the module will not be idempotent. + required: false + type: str + os_family: + description: + - The OS family the template shall be assigned with. +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.os_family +''' + +EXAMPLES = ''' + +# Keep in mind, that in this case, the inline parameters will be overwritten +- name: "Create a Partition Table inline" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: A New Partition Template + state: present + layout: | + <%# + name: A Partition Template + %> + zerombr + clearpart --all --initlabel + autopart + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "Create a Partition Template from a file" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: timeywimey_template.erb + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "Delete a Partition Template" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: timeywimey + layout: | + <%# + dummy: + %> + state: absent + +- name: "Create a Partition Template from a file and modify with parameter(s)" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: timeywimey_template.erb + name: Wibbly Wobbly Template + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +# Providing a name in this case wouldn't be very sensible. +# Alternatively make use of with_filetree to parse recursively with filter. +- name: "Parsing a directory of partition templates" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: "{{ item }}" + state: present + locations: + - SKARO + organizations: + - DALEK INC + with_fileglob: + - "./arsenal_templates/*.erb" + +# If the templates are stored locally and the ansible module is executed on a remote host +- name: Ensure latest version of all Ptable Community Templates + theforeman.foreman.partition_table: + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + layout: '{{ lookup("file", item.src) }}' + with_filetree: '/path/to/partition/tables' + when: item.state == 'file' + + +# with name set to "*" bulk actions can be performed +- name: "Delete *ALL* partition tables" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: absent + +- name: "Assign all partition tables to the same organization(s)" + theforeman.foreman.partition_table: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: present + organizations: + - DALEK INC + - sky.net + - Doc Brown's garage + +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + ptables: + description: List of partition tables. + type: list + elements: dict +''' + + +import os + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, + parse_template, + parse_template_from_file, + OS_LIST, +) + + +class ForemanPtableModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanPtableModule( + argument_spec=dict( + file_name=dict(type='path'), + state=dict(default='present', choices=['absent', 'present_with_defaults', 'present']), + updated_name=dict(), + ), + foreman_spec=dict( + layout=dict(), + locked=dict(type='bool'), + name=dict(), + os_family=dict(choices=OS_LIST), + ), + mutually_exclusive=[ + ['file_name', 'layout'], + ], + required_one_of=[ + ['name', 'file_name', 'layout'], + ], + ) + + # We do not want a layout text for bulk operations + if module.foreman_params.get('name') == '*': + if module.foreman_params.get('file_name') or module.foreman_params.get('layout') or module.foreman_params.get('updated_name'): + module.fail_json( + msg="Neither file_name nor layout nor updated_name allowed if 'name: *'!") + + entity = None + file_name = module.foreman_params.pop('file_name', None) + + if file_name or 'layout' in module.foreman_params: + if file_name: + parsed_dict = parse_template_from_file(file_name, module) + else: + parsed_dict = parse_template(module.foreman_params['layout'], module) + parsed_dict['layout'] = parsed_dict.pop('template') + if 'oses' in parsed_dict: + parsed_dict['os_family'] = parsed_dict.pop('oses') + # sanitize name from template data + # The following condition can actually be hit, when someone is trying to import a + # template with the name set to '*'. + # Besides not being sensible, this would go horribly wrong in this module. + if parsed_dict.get('name') == '*': + module.fail_json(msg="Cannot use '*' as a partition table name!") + # module params are priorized + parsed_dict.update(module.foreman_params) + module.foreman_params = parsed_dict + + # make sure, we have a name + if 'name' not in module.foreman_params: + if file_name: + module.foreman_params['name'] = os.path.splitext( + os.path.basename(file_name))[0] + else: + module.fail_json( + msg='No name specified and no filename to infer it.') + + affects_multiple = module.foreman_params['name'] == '*' + # sanitize user input, filter unuseful configuration combinations with 'name: *' + if affects_multiple: + if module.state == 'present_with_defaults': + module.fail_json(msg="'state: present_with_defaults' and 'name: *' cannot be used together") + if module.desired_absent: + further_params = set(module.foreman_params.keys()) - {'name', 'entity'} + if further_params: + module.fail_json(msg='When deleting all partition tables, there is no need to specify further parameters: %s ' % further_params) + + with module.api_connection(): + if affects_multiple: + module.set_entity('entity', None) # prevent lookup + entities = module.list_resource('ptables') + if not entities: + # Nothing to do; shortcut to exit + module.exit_json() + if not module.desired_absent: # not 'thin' + entities = [module.show_resource('ptables', entity['id']) for entity in entities] + module.auto_lookup_entities() + module.foreman_params.pop('name') + for entity in entities: + module.ensure_entity('ptables', module.foreman_params, entity) + else: + # The name could have been determined to late, so copy it again + module.foreman_params['entity'] = module.foreman_params['name'] + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/product.py b/ansible_collections/theforeman/foreman/plugins/modules/product.py new file mode 100644 index 00000000..9a99e089 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/product.py @@ -0,0 +1,144 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: product +version_added: 1.0.0 +short_description: Manage Products +description: + - Create and manage products +author: + - "Eric D Helms (@ehelms)" + - "Matthias Dellweg (@mdellweg) ATIX AG" +options: + name: + description: + - Name of the product + required: true + type: str + label: + description: + - Label to show the user + required: false + type: str + gpg_key: + description: + - Content GPG key name attached to this product + required: false + type: str + ssl_ca_cert: + description: + - Content SSL CA certificate name attached to this product + required: false + type: str + ssl_client_cert: + description: + - Content SSL client certificate name attached to this product + required: false + type: str + ssl_client_key: + description: + - Content SSL client private key name attached to this product + required: false + type: str + sync_plan: + description: + - Sync plan name attached to this product + required: false + type: str + description: + description: + - Possibly long description to show the user in detail view + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create Fedora product with a sync plan" + theforeman.foreman.product: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Fedora" + organization: "My Cool new Organization" + sync_plan: "Fedora repos sync" + state: present + +- name: "Create CentOS 7 product with content credentials" + theforeman.foreman.product: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "CentOS 7" + gpg_key: "RPM-GPG-KEY-CentOS7" + organization: "My Cool new Organization" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + products: + description: List of products. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloProductModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloProductModule( + entity_name='product', + foreman_spec=dict( + name=dict(required=True), + label=dict(), + gpg_key=dict(type='entity', resource_type='content_credentials', scope=['organization'], no_log=False), + ssl_ca_cert=dict(type='entity', resource_type='content_credentials', scope=['organization']), + ssl_client_cert=dict(type='entity', resource_type='content_credentials', scope=['organization']), + ssl_client_key=dict(type='entity', resource_type='content_credentials', scope=['organization'], no_log=False), + sync_plan=dict(type='entity', scope=['organization']), + description=dict(), + ), + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/provisioning_template.py b/ansible_collections/theforeman/foreman/plugins/modules/provisioning_template.py new file mode 100644 index 00000000..0a52f8de --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/provisioning_template.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017 Matthias Dellweg & Bernhard Hopfenmüller (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: provisioning_template +version_added: 1.0.0 +short_description: Manage Provisioning Templates +description: + - Manage Provisioning Templates +author: + - "Bernhard Hopfenmueller (@Fobhep) ATIX AG" + - "Matthias Dellweg (@mdellweg) ATIX AG" +options: + audit_comment: + description: + - Content of the audit comment field + required: false + type: str + kind: + description: + - The provisioning template kind + required: false + choices: + - Bootdisk + - cloud-init + - finish + - host_init_config + - iPXE + - job_template + - kexec + - POAP + - provision + - PXEGrub + - PXEGrub2 + - PXELinux + - registration + - script + - snippet + - user_data + - ZTP + type: str + template: + description: + - The content of the provisioning template. + - Either this or I(file_name) is required as a source for the Provisioning Template "content". + required: false + type: str + file_name: + description: + - The path of a template file, that shall be imported. + - Either this or I(template) is required as a source for the Provisioning Template "content". + required: false + type: path + locked: + description: + - Determines whether the template shall be locked + required: false + type: bool + name: + description: + - The name of the Provisioning Template. + - If omited, will be determined from the C(name) header of the template or the filename (in that order). + - The special value "*" can be used to perform bulk actions (modify, delete) on all existing templates. + required: false + type: str + updated_name: + description: New provisioning template name. When this parameter is set, the module will not be idempotent. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.operatingsystems +''' + +EXAMPLES = ''' + +# Keep in mind, that in this case, the inline parameters will be overwritten +- name: "Create a Provisioning Template inline" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: A New Finish Template + kind: finish + state: present + template: | + <%# + name: Finish timetravel + kind: finish + %> + cd / + rm -rf * + locations: + - Gallifrey + organizations: + - TARDIS INC + +- name: "Create a Provisioning Template from a file" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: timeywimey_template.erb + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +# Due to the module logic, deleting requires a template dummy, +# either inline or from a file. +- name: "Delete a Provisioning Template" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: timeywimey_template + template: | + <%# + dummy: + %> + state: absent + +- name: "Create a Provisioning Template from a file and modify with parameter" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: timeywimey_template.erb + name: Wibbly Wobbly Template + state: present + locations: + - Gallifrey + organizations: + - TARDIS INC + +# Providing a name in this case wouldn't be very sensible. +# Alternatively make use of with_filetree to parse recursively with filter. +- name: "Parsing a directory of provisioning templates" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + file_name: "{{ item }}" + state: present + locations: + - SKARO + organizations: + - DALEK INC + with_fileglob: + - "./arsenal_templates/*.erb" + +# If the templates are stored locally and the ansible module is executed on a remote host +- name: Ensure latest version of all Provisioning Community Templates + theforeman.foreman.provisioning_template: + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + template: '{{ lookup("file", item.src) }}' + with_filetree: '/path/to/provisioning/templates' + when: item.state == 'file' + + +# with name set to "*" bulk actions can be performed +- name: "Delete *ALL* provisioning templates" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: absent + +- name: "Assign all provisioning templates to the same organization(s)" + theforeman.foreman.provisioning_template: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "*" + state: present + organizations: + - DALEK INC + - sky.net + - Doc Brown's garage + +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + provisioning_templates: + description: List of provisioning templates. + type: list + elements: dict +''' + + +import os + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, + parse_template, + parse_template_from_file, + TEMPLATE_KIND_LIST, +) + + +def find_template_kind(module, module_params): + if 'kind' not in module_params: + return module_params + + module_params['snippet'] = (module_params['kind'] == 'snippet') + if module_params['snippet']: + module_params.pop('kind') + else: + module_params['kind'] = module.find_resource_by_name('template_kinds', module_params['kind'], thin=True) + return module_params + + +class ForemanProvisioningTemplateModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanProvisioningTemplateModule( + argument_spec=dict( + audit_comment=dict(), + file_name=dict(type='path'), + state=dict(default='present', choices=['absent', 'present_with_defaults', 'present']), + updated_name=dict(), + ), + foreman_spec=dict( + kind=dict(choices=TEMPLATE_KIND_LIST + ['snippet'], type='entity', flat_name='template_kind_id', resolve=False), + template=dict(), + locked=dict(type='bool'), + name=dict(), + operatingsystems=dict(type='entity_list'), + snippet=dict(invisible=True), + ), + mutually_exclusive=[ + ['file_name', 'template'], + ], + required_one_of=[ + ['name', 'file_name', 'template'], + ], + ) + + # We do not want a template text for bulk operations + if module.foreman_params.get('name') == '*': + if module.foreman_params.get('file_name') or module.foreman_params.get('template') or module.foreman_params.get('updated_name'): + module.fail_json( + msg="Neither file_name nor template nor updated_name allowed if 'name: *'!") + + entity = None + file_name = module.foreman_params.pop('file_name', None) + + if file_name or 'template' in module.foreman_params: + if file_name: + parsed_dict = parse_template_from_file(file_name, module) + else: + parsed_dict = parse_template(module.foreman_params['template'], module) + # sanitize name from template data + # The following condition can actually be hit, when someone is trying to import a + # template with the name set to '*'. + # Besides not being sensible, this would go horribly wrong in this module. + if parsed_dict.get('name') == '*': + module.fail_json(msg="Cannot use '*' as a template name!") + # module params are priorized + parsed_dict.update(module.foreman_params) + module.foreman_params = parsed_dict + + # make sure, we have a name + if 'name' not in module.foreman_params: + if file_name: + module.foreman_params['name'] = os.path.splitext( + os.path.basename(file_name))[0] + else: + module.fail_json( + msg='No name specified and no filename to infer it.') + + affects_multiple = module.foreman_params['name'] == '*' + # sanitize user input, filter unuseful configuration combinations with 'name: *' + if affects_multiple: + if module.foreman_params.get('updated_name'): + module.fail_json(msg="updated_name not allowed if 'name: *'!") + if module.state == 'present_with_defaults': + module.fail_json(msg="'state: present_with_defaults' and 'name: *' cannot be used together") + if module.desired_absent: + further_params = set(module.foreman_params.keys()) - {'name', 'entity'} + if further_params: + module.fail_json(msg='When deleting all templates, there is no need to specify further parameters: %s ' % further_params) + + with module.api_connection(): + if 'audit_comment' in module.foreman_params: + extra_params = {'audit_comment': module.foreman_params['audit_comment']} + else: + extra_params = {} + + if affects_multiple: + module.set_entity('entity', None) # prevent lookup + entities = module.list_resource('provisioning_templates') + if not entities: + # Nothing to do; shortcut to exit + module.exit_json() + if not module.desired_absent: # not 'thin' + entities = [module.show_resource('provisioning_templates', entity['id']) for entity in entities] + module.auto_lookup_entities() + module.foreman_params.pop('name') + for entity in entities: + module.ensure_entity('provisioning_templates', module.foreman_params, entity, params=extra_params) + else: + # The name could have been determined to late, so copy it again + module.foreman_params['entity'] = module.foreman_params['name'] + + module.foreman_params = find_template_kind(module, module.foreman_params) + + module.run(params=extra_params) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/puppet_environment.py b/ansible_collections/theforeman/foreman/plugins/modules/puppet_environment.py new file mode 100644 index 00000000..40db9243 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/puppet_environment.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Bernhard Suttner (ATIX AG) +# (c) 2019 Christoffer Reijer (Basalt AB) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: puppet_environment +version_added: 1.0.0 +short_description: Manage Puppet Environments +description: + - Create, update, and delete Puppet Environments +author: + - "Bernhard Suttner (@_sbernhard) ATIX AG" + - "Christoffer Reijer (@ephracis) Basalt AB" +options: + name: + description: The full environment name + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: create new environment + theforeman.foreman.puppet_environment: + name: "testing" + locations: + - "Munich" + organizations: + - "ACME" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + puppet_environments: + description: List of puppet environments. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, +) + + +class ForemanEnvironmentModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanEnvironmentModule( + foreman_spec=dict( + name=dict(required=True), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/puppetclasses_import.py b/ansible_collections/theforeman/foreman/plugins/modules/puppetclasses_import.py new file mode 100644 index 00000000..cf6a91af --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/puppetclasses_import.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: puppetclasses_import +version_added: 2.0.0 +short_description: Import Puppet Classes from a Proxy +description: + - Import Puppet Classes from a Proxy +author: + - "Evgeni Golov (@evgeni)" +options: + smart_proxy: + description: + - Smart Proxy to import Puppet Classes from + required: True + type: str + environment: + description: + - Puppet Environment to import Puppet Classes from + required: False + type: str + except: + description: + - Which types of Puppet Classes to exclude from the import. + choices: + - new + - updated + - obsolete + required: False + type: list + elements: str +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: Import Puppet Classes + theforeman.foreman.puppetclasses_import: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + smart_proxy: "foreman.example.com" +''' + +RETURN = ''' +result: + description: Details about the Puppet Class import + returned: success + type: dict + contains: + environments_with_new_puppetclasses: + description: + - Number of Puppet Environments with new Puppet Classes + type: int + returned: when I(environment) not specificed + environments_updated_puppetclasses: + description: + - Number of Puppet Environments with updated Puppet Classes + type: int + returned: when I(environment) not specificed + environments_obsolete: + description: + - Number of Puppet Environments with removed Puppet Classes + type: int + returned: when I(environment) not specificed + environments_ignored: + description: + - Number of ignored Puppet Environments + type: int + returned: when I(environment) not specificed + results: + description: + - List of Puppet Environments and the changes made to them + type: list + returned: success +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanAnsibleModule, _flatten_entity + + +def main(): + module = ForemanAnsibleModule( + foreman_spec={ + 'smart_proxy': dict(type='entity', required=True, flat_name='id'), + 'environment': dict(type='entity'), + 'except': dict(type='list', elements='str', choices=['new', 'updated', 'obsolete']), + }, + supports_check_mode=False, + ) + + with module.api_connection(): + module.auto_lookup_entities() + + if 'except' in module.foreman_params: + module.foreman_params['except'] = ','.join(module.foreman_params.get('except')) + + result = module.resource_action('smart_proxies', 'import_puppetclasses', record_change=False, + params=_flatten_entity(module.foreman_params, module.foreman_spec)) + if (result.get('environments_updated_puppetclasses', 0) + result.get('environments_with_new_puppetclasses', 0) + + result.get('environments_obsolete', 0) + result.get('environments_ignored', 0)): + module.set_changed() + + module.exit_json(result=result) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/realm.py b/ansible_collections/theforeman/foreman/plugins/modules/realm.py new file mode 100644 index 00000000..3382cf4c --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/realm.py @@ -0,0 +1,102 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Lester R Claudio <claudiol@redhat.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: realm +version_added: 1.0.0 +short_description: Manage Realms +description: + - Manage Realms +author: + - "Lester R Claudio (@claudiol1)" +options: + name: + description: + - Name of the realm + required: true + type: str + realm_proxy: + description: + - Proxy to use for this realm + required: true + type: str + realm_type: + description: + - Realm type + choices: + - Red Hat Identity Management + - FreeIPA + - Active Directory + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: "Create EXAMPLE.LOCAL Realm" + theforeman.foreman.realm: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "EXAMPLE.COM" + realm_proxy: "foreman.example.com" + realm_type: "Red Hat Identity Management" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + realms: + description: List of realms. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +class ForemanRealmModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanRealmModule( + foreman_spec=dict( + name=dict(required=True), + realm_proxy=dict(type='entity', required=True, resource_type='smart_proxies'), + realm_type=dict(required=True, choices=['Red Hat Identity Management', 'FreeIPA', 'Active Directory']), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/redhat_manifest.py b/ansible_collections/theforeman/foreman/plugins/modules/redhat_manifest.py new file mode 100644 index 00000000..58f779dc --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/redhat_manifest.py @@ -0,0 +1,344 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Sean O'Keeffe <seanokeeffe797@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: redhat_manifest +version_added: 1.0.0 +short_description: Interact with a Red Hat Satellite Subscription Manifest +description: + - Download and modify a Red Hat Satellite Subscription Manifest +author: + - "Sean O'Keeffe (@sean797)" +options: + name: + description: + - Manifest Name + type: str + uuid: + description: + - Manifest uuid + type: str + username: + description: + - Red Hat Portal username + required: true + type: str + password: + description: + - Red Hat Portal password + required: true + type: str + pool_id: + description: + - Subscription pool_id + type: str + quantity: + description: + - quantity of pool_id Subscriptions + type: int + default: 1 + pool_state: + description: + - Subscription state + default: present + choices: + - present + - absent + type: str + state: + description: + - Manifest state + default: present + choices: + - present + - absent + type: str + path: + description: + - path to export the manifest + type: path + validate_certs: + description: + - Validate Portal SSL + default: True + type: bool + portal: + description: + - Red Hat Portal subscription access address + default: https://subscription.rhsm.redhat.com + type: str + content_access_mode: + description: + - Content Access Mode of the Subscription Manifest. + - Setting I(content_access_mode=org_enviroment) enables Simple Content Access. + type: str + choices: + - org_environment + - entitlement + default: entitlement +''' + +EXAMPLES = ''' +- name: Create foreman.example.com Manifest and add 7 sub + theforeman.foreman.redhat_manifest: + name: "foreman.example.com" + username: "john-smith" + password: "changeme" + pool_id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + quantity: 7 + +- name: Ensure my manifest has 10 of one subs in it and export + theforeman.foreman.redhat_manifest: + uuid: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX + username: john-smith + password: changeme + pool_id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + quantity: 10 + path: /root/manifest.zip + +- name: Remove all of one subs from foreman.example.com + theforeman.foreman.redhat_manifest: + name: foreman.example.com + username: john-smith + password: changeme + pool_id: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + pool_state: absent +''' + +RETURN = ''' +uuid: + description: Manifest UUID + returned: success + type: str + sample: 5349d1d0-5bda-480a-b7bd-ff41e2c29e03 + version_added: 3.8.0 +''' + +import json +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url +from ansible.module_utils._text import to_text, to_native + + +REDHAT_UEP = '/etc/rhsm/ca/redhat-uep.pem' + + +def fetch_portal(module, path, method, data=None, accept_header='application/json'): + if data is None: + data = {} + url = module.params['portal'] + path + headers = {'accept': accept_header, + 'content-type': 'application/json'} + fetch_kwargs = {'timeout': 30} + if os.path.exists(REDHAT_UEP): + fetch_kwargs['ca_path'] = REDHAT_UEP + try: + resp, info = fetch_url(module, url, json.dumps(data), headers, method, **fetch_kwargs) + except TypeError: + # ca_path was added in Ansible 2.9 and backported to 2.8 in 2.8.6 + # older Ansible releases don't support that and we have to omit the CA cert here + if module.params['validate_certs']: + module.warn("Your Ansible version does not support providing custom CA certificates for HTTP requests. " + "Talking to the Red Hat portal might fail without validate_certs=False. Please update.") + del fetch_kwargs['ca_path'] + resp, info = fetch_url(module, url, json.dumps(data), headers, method, **fetch_kwargs) + if resp is None or info["status"] >= 400: + try: + error = json.loads(info['body'])['displayMessage'] + except Exception: + error = info['msg'] + module.fail_json(msg="%s to %s failed, got %s" % (method, url, error)) + return resp, info + + +def create_manifest(module): + path = "/subscription/consumers" + data = {'name': module.params['name'], + 'type': "satellite", + 'contentAccessMode': module.params['content_access_mode'], + # TODO: Make these 2 configurable, we need to work out which horribly + # undocumented API to use. + 'facts': {'distributor_version': 'sat-6.3', + 'system.certificate_version': '3.2'}} + resp, info = fetch_portal(module, path, 'POST', data) + return json.loads(to_text(resp.read())) + + +def delete_manifest(module, uuid): + path = "/subscription/consumers/%s" % uuid + resp, info = fetch_portal(module, path, 'DELETE') + if info['status'] != 204: + module.fail_json(msg="Got status %s attempting to delete manifest, expected 204" % (info['status'])) + + +def get_manifest(module): + path = "/subscription/owners/%s/consumers?type=satellite" % (module.params['rhsm_owner']) + if module.params['uuid']: + path += '&uuid={0}'.format(module.params['uuid']) + resp, info = fetch_portal(module, path, 'GET') + manifests = json.loads(to_text(resp.read())) + if module.params['name']: + attr = 'name' + if module.params['uuid']: + attr = 'uuid' + manifest = [m for m in manifests if m[attr] == module.params[attr]] + if manifest: + if module.params['state'] == 'present': + return manifest[0], False + if module.params['state'] == 'absent': + if not module.check_mode: + return delete_manifest(module, manifest[0]['uuid']), True + return None, True + elif module.params['state'] == 'present': + if not module.check_mode: + return create_manifest(module), True + return None, True + return None, False + + +def get_owner(module): + path = "/subscription/users/%s/owners" % (module.params['username']) + resp, info = fetch_portal(module, path, 'GET') + return json.loads(to_text(resp.read()))[0]['key'] + + +def get_subs(module, manifest): + path = "/subscription/consumers/%s/entitlements" % (manifest['uuid']) + resp, info = fetch_portal(module, path, 'GET') + all_subs = json.loads(to_text(resp.read())) + subs = [s for s in all_subs if s['pool']['id'] == module.params['pool_id']] + return subs + + +def get_remove_or_attach_sub(module, manifest): + changed = False + subs = get_subs(module, manifest) + if subs: + if module.params['pool_state'] == 'present': + sub_quantity = sum(s['quantity'] for s in subs) + while sub_quantity > module.params['quantity']: + if not module.check_mode: + remove_sub(module, manifest, subs[0]) + else: + changed = True + break + changed = True + subs = get_subs(module, manifest) + sub_quantity = sum(s['quantity'] for s in subs) + if sub_quantity < module.params['quantity']: + difference = module.params['quantity'] - sub_quantity + if not module.check_mode: + attach_sub(module, manifest, difference) + changed = True + elif module.params['pool_state'] == 'absent': + if not module.check_mode: + for sub in subs: + remove_sub(module, manifest, sub) + changed = True + elif module.params['pool_state'] == 'present': + if not module.check_mode: + attach_sub(module, manifest, module.params['quantity']) + changed = True + return changed + + +def remove_sub(module, manifest, sub): + path = "/subscription/consumers/%s/entitlements/%s" % (manifest['uuid'], sub['id']) + fetch_portal(module, path, 'DELETE') + + +def attach_sub(module, manifest, quantity): + path = "/subscription/consumers/%s/entitlements?pool=%s&quantity=%s" % (manifest['uuid'], module.params['pool_id'], quantity) + fetch_portal(module, path, 'POST') + + +def export_manifest(module, manifest): + path = "/subscription/consumers/%s/export" % (manifest['uuid']) + try: + resp, info = fetch_portal(module, path, 'GET', accept_header='application/zip') + if not module.check_mode: + with open(module.params['path'], 'wb') as f: + while True: + data = resp.read(65536) # 64K + if not data: + break + f.write(data) + except Exception as e: + module.fail_json(msg="Failure downloading manifest, {0}".format(to_native(e))) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str'), + uuid=dict(type='str'), + username=dict(required=True, no_log=True), + password=dict(required=True, no_log=True), + content_access_mode=dict(choices=['org_environment', 'entitlement'], default='entitlement'), + pool_id=dict(type='str'), + quantity=dict(type='int', default=1), + pool_state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent'], default='present'), + path=dict(type='path'), + validate_certs=dict(default=True, type='bool'), + portal=dict(default='https://subscription.rhsm.redhat.com'), + ), + required_one_of=[['name', 'uuid']], + supports_check_mode=True, + ) + + if module.params['validate_certs'] and not os.path.exists(REDHAT_UEP): + module.warn("Couldn't find the Red Hat Entitlement Platform CA certificate ({0}) on your system. " + "It's required to validate the certificate of {1}.".format(REDHAT_UEP, module.params['portal'])) + + username = module.params['username'] + password = module.params['password'] + + # Hack to add options the way fetch_url expects + module.params['url_username'] = username + module.params['url_password'] = password + module.params['force_basic_auth'] = True + + module.params['rhsm_owner'] = get_owner(module) + + manifest, man_changed = get_manifest(module) + if module.params['pool_id'] and manifest: + sub_changed = get_remove_or_attach_sub(module, manifest) + else: + sub_changed = False + + if module.params['path'] and manifest: + export_manifest(module, manifest) + + if manifest: + manifest_uuid = manifest.get('uuid') + else: + manifest_uuid = None + + changed = man_changed or sub_changed + module.exit_json(changed=changed, uuid=manifest_uuid) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/repository.py b/ansible_collections/theforeman/foreman/plugins/modules/repository.py new file mode 100644 index 00000000..898a22d0 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/repository.py @@ -0,0 +1,399 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: repository +version_added: 1.0.0 +short_description: Manage Repositories +description: + - Create and manage repositories +author: "Eric D Helms (@ehelms)" +notes: + - You can configure certain aspects of existing Red Hat Repositories (like I(download_policy)) using this module, + but you can't create (enable) or delete (disable) them. + - If you want to enable or disable Red Hat Repositories available through your subscription, + please use the M(theforeman.foreman.repository_set) module instead. +options: + name: + description: + - Name of the repository + required: true + type: str + description: + description: + - Description of the repository + required: false + type: str + product: + description: + - Product to which the repository lives in + required: true + type: str + label: + description: + - label of the repository + type: str + content_type: + description: + - The content type of the repository + required: true + choices: + - deb + - docker + - file + - ostree + - puppet + - yum + - ansible_collection + type: str + url: + description: + - Repository URL to sync from + required: false + type: str + ignore_global_proxy: + description: + - Whether content sync should use or ignore the global http proxy setting + - This is deprecated with Katello 3.13 + - It has been superseeded by I(http_proxy_policy) + required: false + type: bool + http_proxy_policy: + description: + - Which proxy to use for content synching + choices: + - global_default_http_proxy + - none + - use_selected_http_proxy + required: false + type: str + http_proxy: + description: + - Name of the http proxy to use for content synching + - Should be combined with I(http_proxy_policy='use_selected_http_proxy') + required: false + type: str + gpg_key: + description: + - Repository GPG key + required: false + type: str + ssl_ca_cert: + description: + - Repository SSL CA certificate + required: false + type: str + ssl_client_cert: + description: + - Repository SSL client certificate + required: false + type: str + ssl_client_key: + description: + - Repository SSL client private key + required: false + type: str + download_concurrency: + description: + - download concurrency for sync from upstream + - as the API does not return this value, this will break idempotence for this module + required: false + type: int + version_added: 3.0.0 + download_policy: + description: + - The download policy for sync from upstream. + - The download policy C(background) is deprecated and not available since Katello 4.3. + choices: + - background + - immediate + - on_demand + required: false + type: str + mirror_on_sync: + description: + - toggle "mirror on sync" where the state of the repository mirrors that of the upstream repository at sync time + - This is deprecated with Katello 4.3 + - It has been superseeded by I(mirroring_policy=mirror_content_only) + type: bool + required: false + mirroring_policy: + description: + - Policy to set for mirroring content + - Supported since Katello 4.3 + type: str + choices: + - additive + - mirror_content_only + - mirror_complete + verify_ssl_on_sync: + description: + - verify the upstream certifcates are signed by a trusted CA + type: bool + required: false + upstream_username: + description: + - username to access upstream repository + type: str + upstream_password: + description: + - Password to access upstream repository. + - When this parameter is set, the module will not be idempotent. + type: str + docker_upstream_name: + description: + - name of the upstream docker repository + - only available for I(content_type=docker) + type: str + docker_tags_whitelist: + description: + - list of tags to sync for Container Image repository + - only available for I(content_type=docker) + - Deprecated since Katello 4.4 + type: list + elements: str + deb_releases: + description: + - comma separated list of releases to be synced from deb-archive + - only available for I(content_type=deb) + type: str + deb_components: + description: + - comma separated list of repo components to be synced from deb-archive + - only available for I(content_type=deb) + type: str + deb_architectures: + description: + - comma separated list of architectures to be synced from deb-archive + - only available for I(content_type=deb) + type: str + deb_errata_url: + description: + - URL to sync Debian or Ubuntu errata information from + - only available on Orcharhino + - only available for I(content_type=deb) + type: str + required: false + unprotected: + description: + - publish the repository via HTTP + type: bool + required: false + checksum_type: + description: + - Checksum of the repository + type: str + required: false + choices: + - sha1 + - sha256 + ignorable_content: + description: + - List of content units to ignore while syncing a yum repository. + - Must be subset of rpm,drpm,srpm,distribution,erratum. + type: list + elements: str + required: false + ansible_collection_requirements: + description: + - Contents of requirement yaml file to sync from URL + type: str + required: false + auto_enabled: + description: + - repositories will be automatically enabled on a registered host subscribed to this product + type: bool + required: false + os_versions: + description: + - Identifies whether the repository should be disabled on a client with a non-matching OS version. + - A maximum of one OS version can be selected. + - Set to C([]) to disable filtering again. + type: list + elements: str + required: false + choices: + - rhel-6 + - rhel-7 + - rhel-8 + - rhel-9 + arch: + description: + - Architecture of content in the repository + - Set to C(noarch) to disable the architecture restriction again. + type: str + required: false + include_tags: + description: + - List of tags to sync for a container image repository. + type: list + elements: str + required: false + version_added: 3.7.0 + exclude_tags: + description: + - List of tags to exclude when syncing a container image repository. + type: list + elements: str + required: false + version_added: 3.7.0 +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create repository" + theforeman.foreman.repository: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My repository" + state: present + content_type: "yum" + product: "My Product" + organization: "Default Organization" + url: "http://yum.theforeman.org/plugins/latest/el7/x86_64/" + mirror_on_sync: true + download_policy: immediate + +- name: "Create repository with content credentials" + theforeman.foreman.repository: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My repository 2" + state: present + content_type: "yum" + product: "My Product" + organization: "Default Organization" + url: "http://yum.theforeman.org/releases/latest/el7/x86_64/" + download_policy: on_demand + mirror_on_sync: true + gpg_key: RPM-GPG-KEY-my-product2 +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + repositories: + description: List of repositories. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloRepositoryModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloRepositoryModule( + foreman_spec=dict( + product=dict(type='entity', scope=['organization'], required=True), + label=dict(), + name=dict(required=True), + content_type=dict(required=True, choices=['docker', 'ostree', 'yum', 'puppet', 'file', 'deb', 'ansible_collection']), + url=dict(), + ignore_global_proxy=dict(type='bool'), + http_proxy_policy=dict(choices=['global_default_http_proxy', 'none', 'use_selected_http_proxy']), + http_proxy=dict(type='entity'), + gpg_key=dict(type='entity', resource_type='content_credentials', scope=['organization'], no_log=False), + ssl_ca_cert=dict(type='entity', resource_type='content_credentials', scope=['organization']), + ssl_client_cert=dict(type='entity', resource_type='content_credentials', scope=['organization']), + ssl_client_key=dict(type='entity', resource_type='content_credentials', scope=['organization'], no_log=False), + download_policy=dict(choices=['background', 'immediate', 'on_demand']), + download_concurrency=dict(type='int'), + mirror_on_sync=dict(type='bool'), + mirroring_policy=dict(type='str', choices=['additive', 'mirror_content_only', 'mirror_complete']), + verify_ssl_on_sync=dict(type='bool'), + upstream_username=dict(), + upstream_password=dict(no_log=True), + docker_upstream_name=dict(), + docker_tags_whitelist=dict(type='list', elements='str'), + deb_errata_url=dict(), + deb_releases=dict(), + deb_components=dict(), + deb_architectures=dict(), + description=dict(), + unprotected=dict(type='bool'), + checksum_type=dict(choices=['sha1', 'sha256']), + ignorable_content=dict(type='list', elements='str'), + ansible_collection_requirements=dict(), + auto_enabled=dict(type='bool'), + os_versions=dict(type='list', elements='str', choices=['rhel-6', 'rhel-7', 'rhel-8', 'rhel-9']), + arch=dict(), + include_tags=dict(type='list', elements='str'), + exclude_tags=dict(type='list', elements='str'), + ), + mutually_exclusive=[ + ['mirror_on_sync', 'mirroring_policy'] + ], + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + ), + entity_opts={'scope': ['product']}, + ) + + # KatelloEntityAnsibleModule automatically adds organization to the entity scope + # but repositories are scoped by product (and these are org scoped) + module.foreman_spec['entity']['scope'].remove('organization') + + if module.foreman_params['content_type'] != 'docker': + invalid_list = [key for key in ['docker_upstream_name', 'docker_tags_whitelist', 'include_tags', 'exclude_tags'] if key in module.foreman_params] + if invalid_list: + module.fail_json(msg="({0}) can only be used with content_type 'docker'".format(",".join(invalid_list))) + + if module.foreman_params['content_type'] != 'deb': + invalid_list = [key for key in ['deb_errata_url', 'deb_releases', 'deb_components', 'deb_architectures'] if key in module.foreman_params] + if invalid_list: + module.fail_json(msg="({0}) can only be used with content_type 'deb'".format(",".join(invalid_list))) + + if module.foreman_params['content_type'] != 'ansible_collection': + invalid_list = [key for key in ['ansible_collection_requirements'] if key in module.foreman_params] + if invalid_list: + module.fail_json(msg="({0}) can only be used with content_type 'ansible_collection'".format(",".join(invalid_list))) + + if module.foreman_params['content_type'] != 'yum': + invalid_list = [key for key in ['ignorable_content', 'os_versions'] if key in module.foreman_params] + if invalid_list: + module.fail_json(msg="({0}) can only be used with content_type 'yum'".format(",".join(invalid_list))) + + if 'ignore_global_proxy' in module.foreman_params and 'http_proxy_policy' not in module.foreman_params: + module.foreman_params['http_proxy_policy'] = 'none' if module.foreman_params['ignore_global_proxy'] else 'global_default_http_proxy' + + with module.api_connection(): + if not module.desired_absent: + module.auto_lookup_entities() + module.foreman_params.pop('organization') + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/repository_info.py b/ansible_collections/theforeman/foreman/plugins/modules/repository_info.py new file mode 100644 index 00000000..a4e81cfb --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/repository_info.py @@ -0,0 +1,99 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: repository_info +version_added: 2.0.0 +short_description: Fetch information about Repositories +description: + - Fetch information about Repositories +author: "Evgeni Golov (@evgeni)" +options: + product: + description: + - Product to which the repository lives in + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Find repository by name" + theforeman.foreman.repository_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "My repository" + product: "My Product" + organization: "Default Organization" + +- name: "Find repository using a search" + theforeman.foreman.repository_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + product: "My Product" + organization: "Default Organization" + search: 'name = "My repository"' +''' + +RETURN = ''' +repository: + description: Details about the found repository + returned: success and I(name) was passed + type: dict +repositories: + description: List of all found repositories and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloInfoAnsibleModule + + +class KatelloRepositoryInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloRepositoryInfo( + foreman_spec=dict( + product=dict(type='entity', scope=['organization'], required=True), + ), + entity_opts={'scope': ['product']}, + ) + + # KatelloInfoAnsibleModule automatically adds organization to the entity scope + # but repositories are scoped by product (and these are org scoped) + module.foreman_spec['entity']['scope'].remove('organization') + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/repository_set.py b/ansible_collections/theforeman/foreman/plugins/modules/repository_set.py new file mode 100644 index 00000000..b442befe --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/repository_set.py @@ -0,0 +1,338 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Andrew Kofink <ajkofink@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: repository_set +version_added: 1.0.0 +short_description: Enable/disable Red Hat Repositories available through subscriptions +description: + - Enable/disable Red Hat Repositories that are available through subscriptions +author: "Andrew Kofink (@akofink)" +options: + name: + description: + - Name of the repository set + required: false + type: str + product: + description: + - Name of the parent product + required: false + type: str + label: + description: + - Label of the repository set, can be used in place of I(name) & I(product) + required: false + type: str + repositories: + description: + - Release version and base architecture of the repositories to enable. + - Some reposotory sets require only I(basearch) or only I(releasever) to be set. + - See the examples how you can obtain this information using M(theforeman.foreman.resource_info). + - Required when I(all_repositories) is unset or C(false). + required: false + type: list + elements: dict + suboptions: + basearch: + description: + - Basearch of the repository to enable. + type: str + releasever: + description: + - Releasever of the repository to enable. + type: str + all_repositories: + description: + - Affect all available repositories in the repository set instead of listing them in I(repositories). + - Required when I(repositories) is unset or an empty list. + required: false + type: bool + state: + description: + - Whether the repositories are enabled or not + required: false + choices: + - 'enabled' + - 'disabled' + default: enabled + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Enable RHEL 7 RPMs repositories" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Red Hat Enterprise Linux 7 Server (RPMs)" + organization: "Default Organization" + product: "Red Hat Enterprise Linux Server" + repositories: + - releasever: "7.0" + basearch: "x86_64" + - releasever: "7.1" + basearch: "x86_64" + - releasever: "7.2" + basearch: "x86_64" + - releasever: "7.3" + basearch: "x86_64" + state: enabled + +- name: "Enable RHEL 7 RPMs repositories with label" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + label: rhel-7-server-rpms + repositories: + - releasever: "7.0" + basearch: "x86_64" + - releasever: "7.1" + basearch: "x86_64" + - releasever: "7.2" + basearch: "x86_64" + - releasever: "7.3" + basearch: "x86_64" + state: enabled + +- name: "Disable RHEL 7 Extras RPMs repository" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: Red Hat Enterprise Linux 7 Server - Extras (RPMs) + organization: "Default Organization" + product: Red Hat Enterprise Linux Server + state: disabled + repositories: + - basearch: x86_64 + +- name: "Enable RHEL 8 BaseOS RPMs repository with label" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + label: rhel-8-for-x86_64-baseos-rpms + repositories: + - releasever: "8" + +- name: "Enable Red Hat Virtualization Manager RPMs repository with label" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + label: "rhel-7-server-rhv-4.2-manager-rpms" + repositories: + - basearch: x86_64 + state: enabled + +- name: "Enable Red Hat Virtualization Manager RPMs repository without specifying basearch" + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + label: "rhel-7-server-rhv-4.2-manager-rpms" + all_repositories: true + state: enabled + +- name: "Search for possible repository sets of a product" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + resource: repository_sets + search: product_name="Red Hat Virtualization Manager" + register: data +- name: "Output found repository sets, see the contentUrl section for possible repository substitutions" + debug: + var: data + +- name: "Search for possible repository sets by label" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + resource: repository_sets + search: label=rhel-7-server-rhv-4.2-manager-rpms + register: data +- name: "Output found repository sets, see the contentUrl section for possible repository substitutions" + debug: + var: data + +- name: Enable set with and without all_repositories at the same time + theforeman.foreman.repository_set: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + label: "{{ item.label }}" + repositories: "{{ item.repositories | default(omit) }}" + all_repositories: "{{ item.repositories is not defined }}" + state: enabled + loop: + - label: rhel-7-server-rpms + repositories: + - releasever: "7Server" + basearch: "x86_64" + - label: rhel-7-server-rhv-4.2-manager-rpms +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + repository_sets: + description: List of repository sets. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +def get_desired_repos(desired_substitutions, available_repos): + desired_repos = [] + for sub in desired_substitutions: + desired_repos += filter(lambda available: available['substitutions'] == sub, available_repos) + return desired_repos + + +def record_repository_set_state(module, record_data, repo, state_before, state_after): + repo_change_data = record_data.copy() + repo_change_data['repo_name'] = repo + repo_change_data['state'] = state_before + repo_change_data_after = repo_change_data.copy() + repo_change_data_after['state'] = state_after + module.record_before('repository_sets', repo_change_data) + module.record_after('repository_sets', repo_change_data_after) + module.record_after_full('repository_sets', repo_change_data_after) + + +class KatelloRepositorySetModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloRepositorySetModule( + foreman_spec=dict( + product=dict(type='entity', scope=['organization']), + name=dict(), + label=dict(), + repositories=dict(type='list', elements='dict', options=dict( + basearch=dict(), + releasever=dict(), + )), + all_repositories=dict(type='bool'), + ), + argument_spec=dict( + state=dict(default='enabled', choices=['disabled', 'enabled']), + ), + required_one_of=[ + ['label', 'name'], + ['repositories', 'all_repositories'], + ], + required_if=[ + ['all_repositories', False, ['repositories']], + ['repositories', [], ['all_repositories']], + ], + ) + + repositories = module.foreman_params.get('repositories', []) + + with module.api_connection(): + scope = module.scope_for('organization') + + record_data = {} + if 'product' in module.foreman_params: + record_data['product'] = module.foreman_params['product'] + scope.update(module.scope_for('product')) + + if 'label' in module.foreman_params: + search = 'label="{0}"'.format(module.foreman_params['label']) + repo_set = module.find_resource('repository_sets', search=search, params=scope) + record_data['label'] = module.foreman_params['label'] + else: + repo_set = module.find_resource_by_name('repository_sets', name=module.foreman_params['name'], params=scope) + record_data['name'] = module.foreman_params['name'] + module.set_entity('entity', repo_set) + + repo_set_scope = {'id': repo_set['id'], 'product_id': repo_set['product']['id']} + repo_set_scope.update(scope) + + available_repos = module.resource_action('repository_sets', 'available_repositories', params=repo_set_scope, ignore_check_mode=True) + available_repos = available_repos['results'] + current_repos = repo_set['repositories'] + if not module.foreman_params.get('all_repositories', False): + desired_repos = get_desired_repos(repositories, available_repos) + else: + desired_repos = available_repos[:] + + current_repo_names = set(map(lambda repo: repo['name'], current_repos)) + desired_repo_names = set(map(lambda repo: repo['repo_name'], desired_repos)) + + if not module.foreman_params.get('all_repositories', False) and len(repositories) != len(desired_repo_names): + repo_set_identification = ' '.join(['{0}: {1}'.format(k, v) for (k, v) in record_data.items()]) + + available_repo_details = [{'name': repo['repo_name'], 'repositories': repo['substitutions']} for repo in available_repos] + desired_repo_details = [{'name': repo['repo_name'], 'repositories': repo['substitutions']} for repo in desired_repos] + search_details = record_data.copy() + search_details['repositories'] = repositories + + error_msg = "Desired repositories are not available on the repository set {0}.\nSearched: {1}\nFound: {2}\nAvailable: {3}".format( + repo_set_identification, search_details, desired_repo_details, available_repo_details) + + module.fail_json(msg=error_msg) + + if module.state == 'enabled': + for repo in desired_repo_names - current_repo_names: + repo_to_enable = next((r for r in available_repos if r['repo_name'] == repo)) + repo_change_params = repo_to_enable['substitutions'].copy() + repo_change_params.update(repo_set_scope) + + record_repository_set_state(module, record_data, repo, 'disabled', 'enabled') + + module.resource_action('repository_sets', 'enable', params=repo_change_params) + elif module.state == 'disabled': + for repo in current_repo_names & desired_repo_names: + repo_to_disable = next((r for r in available_repos if r['repo_name'] == repo)) + repo_change_params = repo_to_disable['substitutions'].copy() + repo_change_params.update(repo_set_scope) + + record_repository_set_state(module, record_data, repo, 'enabled', 'disabled') + + module.resource_action('repository_sets', 'disable', params=repo_change_params) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/repository_set_info.py b/ansible_collections/theforeman/foreman/plugins/modules/repository_set_info.py new file mode 100644 index 00000000..95d4b8a1 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/repository_set_info.py @@ -0,0 +1,101 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 William Bradford Clark +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: repository_set_info +version_added: 2.1.0 +short_description: Fetch information about Red Hat Repositories +description: + - Fetch information about Red Hat Repositories +author: "William Bradford Clark (@wbclark)" +options: + product: + description: + - Name of the parent product + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Find repository set by name and product." + theforeman.foreman.repository_set_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + name: "Red Hat Enterprise Linux 7 Server (RPMs)" + product: "Red Hat Enterprise Linux Server" + +- name: "Find repository set by label." + theforeman.foreman.repository_set_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + search: 'label = "rhel-7-server-rpms"' +''' + +RETURN = ''' +repository_set: + description: Details about the found Red Hat Repository. + returned: success and I(name) was passed + type: dict +repository_sets: + description: List of all found Red Hat Repositories and their details. + returned: success and I(search) was passed + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloInfoAnsibleModule + + +class KatelloRepositorySetInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloRepositorySetInfo( + foreman_spec=dict( + product=dict(type='entity', scope=['organization']), + ), + entity_opts={'scope': ['product']}, + required_together=[ + ['name', 'product'], + ], + ) + + # KatelloInfoAnsibleModule automatically adds organization to the entity scope + # but repository sets are scoped by product (and these are org scoped) + module.foreman_spec['entity']['scope'].remove('organization') + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/repository_sync.py b/ansible_collections/theforeman/foreman/plugins/modules/repository_sync.py new file mode 100644 index 00000000..5c806bcc --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/repository_sync.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2016, Eric D Helms <ericdhelms@gmail.com> +# (c) 2019, Matthias M Dellweg <dellweg@atix.de> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: repository_sync +version_added: 1.0.0 +short_description: Sync a Repository or Product +description: + - Sync a repository or product +author: + - "Eric D Helms (@ehelms)" + - "Matthias M Dellweg (@mdellweg) ATIX AG" +options: + product: + description: Product to which the I(repository) lives in + required: true + type: str + repository: + description: | + Name of the repository to sync + If omitted, all repositories in I(product) are synched. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +... +''' + +EXAMPLES = ''' +- name: "Sync repository" + theforeman.foreman.repository_sync: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + repository: "My repository" + product: "My Product" + organization: "Default Organization" +''' + +RETURN = ''' # ''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule + + +def main(): + module = KatelloAnsibleModule( + foreman_spec=dict( + product=dict(type='entity', scope=['organization'], required=True), + repository=dict(type='entity', scope=['product']), + ), + ) + + module.task_timeout = 12 * 60 * 60 + + with module.api_connection(): + product = module.lookup_entity('product') + repository = module.lookup_entity('repository') + if repository: + task = module.resource_action('repositories', 'sync', {'id': repository['id']}) + else: + task = module.resource_action('products', 'sync', {'id': product['id']}) + + module.exit_json(task=task) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/resource_info.py b/ansible_collections/theforeman/foreman/plugins/modules/resource_info.py new file mode 100644 index 00000000..16aa977a --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/resource_info.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018, Sean O'Keeffe <seanokeeffe797@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: resource_info +version_added: 1.0.0 +short_description: Gather information about resources +description: + - Gather information about resources +author: + - "Sean O'Keeffe (@sean797)" +options: + resource: + description: + - Resource to search + - Set to an invalid choice like I(foo) see all available options. + required: true + type: str + search: + description: + - Search query to use + - If None, all resources are returned + type: str + params: + description: + - Add parameters to the API call if necessary + - If not specified, no additional parameters are passed + type: dict + organization: + description: + - Scope the searched resource by organization + type: str + full_details: + description: + - If C(True) all details about the found resources are returned + type: bool + default: false + aliases: [ info ] +notes: + - Some resources don't support scoping and will return errors when you pass I(organization) or unknown data in I(params). +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: "Read a Setting" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + resource: settings + search: name = foreman_url + register: result +- debug: + var: result.resources[0].value + +- name: "Read all Registries" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + resource: registries + register: result +- debug: + var: item.name + with_items: "{{ result.resources }}" + +- name: "Read all Organizations with full details" + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + resource: organizations + full_details: true + register: result +- debug: + var: result.resources + +- name: Get all existing subscriptions for organization with id 1 + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + resource: subscriptions + params: + organization_id: 1 + register: result +- debug: + var: result + +- name: Get all existing activation keys for organization ACME + theforeman.foreman.resource_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + resource: activation_keys + organization: ACME + register: result +- debug: + var: result +''' + +RETURN = ''' +resources: + description: Resource information + returned: always + type: list +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanAnsibleModule + + +def main(): + + module = ForemanAnsibleModule( + foreman_spec=dict( + resource=dict(type='str', required=True), + search=dict(), + full_details=dict(type='bool', aliases=['info'], default='false'), + params=dict(type='dict'), + organization=dict(), + ), + ) + + module_params = module.foreman_params + resource = module_params['resource'] + search = module_params.get('search') + params = module_params.get('params', {}) + + with module.api_connection(): + if resource not in module.foremanapi.resources: + msg = "Resource '{0}' does not exist in the API. Existing resources: {1}".format(resource, ', '.join(sorted(module.foremanapi.resources))) + module.fail_json(msg=msg) + if 'organization' in module_params: + params['organization_id'] = module.find_resource_by_name('organizations', module_params['organization'], thin=True)['id'] + + if 'id' not in params: + response = module.list_resource(resource, search, params) + + if module_params['full_details']: + resources = [] + for found_resource in response: + resources.append(module.show_resource(resource, found_resource['id'], params)) + else: + resources = response + else: + res_id = params.pop('id') + resources = [module.show_resource(resource, res_id, params)] + + module.exit_json(resources=resources) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/role.py b/ansible_collections/theforeman/foreman/plugins/modules/role.py new file mode 100644 index 00000000..94982e8c --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/role.py @@ -0,0 +1,146 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Christoffer Reijer (Basalt AB) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: role +version_added: 1.0.0 +short_description: Manage Roles +description: + - Create, update, and delete Roles +author: + - "Christoffer Reijer (@ephracis) Basalt AB" +options: + name: + description: The name of the role + required: true + type: str + description: + description: Description of the role + required: false + type: str + filters: + description: Filters with permissions for this role + required: false + type: list + elements: dict + suboptions: + permissions: + description: List of permissions + required: true + type: list + elements: str + search: + description: Filter condition for the resources + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: role + theforeman.foreman.role: + name: "Provisioner" + description: "Only provision on libvirt" + locations: + - "Uppsala" + organizations: + - "ACME" + filters: + - permissions: + - view_hosts + search: "owner_type = Usergroup and owner_id = 4" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + roles: + description: List of roles. + type: list + elements: dict +''' + +import copy + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +filter_foreman_spec = dict( + id=dict(invisible=True), + permissions=dict(type='entity_list', required=True, resolve=False), + search=dict(), +) + + +class ForemanRoleModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanRoleModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + filters=dict(type='nested_list', foreman_spec=filter_foreman_spec), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + new_entity = module.run() + + filters = module.foreman_params.get("filters") + if not module.desired_absent and filters is not None: + scope = {'role_id': new_entity['id']} + + if entity: + current_filters = [module.show_resource('filters', filter['id']) for filter in entity['filters']] + else: + current_filters = [] + desired_filters = copy.deepcopy(filters) + + for desired_filter in desired_filters: + # search for an existing filter + for current_filter in current_filters: + if desired_filter.get('search') == current_filter['search']: + if set(desired_filter.get('permissions', [])) == set(perm['name'] for perm in current_filter['permissions']): + current_filters.remove(current_filter) + break + else: + desired_filter['permissions'] = module.find_resources_by_name('permissions', desired_filter['permissions'], thin=True) + module.ensure_entity('filters', desired_filter, None, params=scope, state='present', foreman_spec=filter_foreman_spec) + for current_filter in current_filters: + module.ensure_entity('filters', None, {'id': current_filter['id']}, params=scope, state='absent', foreman_spec=filter_foreman_spec) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/scap_content.py b/ansible_collections/theforeman/foreman/plugins/modules/scap_content.py new file mode 100644 index 00000000..d43ad99f --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/scap_content.py @@ -0,0 +1,126 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Jameer Pathan <jameerpathan111@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: scap_content +version_added: 1.0.0 +short_description: Manage SCAP content +description: + - Create, update, and delete SCAP content +author: + - "Jameer Pathan (@jameerpathan111)" +options: + title: + description: + - Title of SCAP content. + required: true + type: str + updated_title: + description: + - New SCAP content title. + - When this parameter is set, the module will not be idempotent. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.scap_datastream +''' + +EXAMPLES = ''' +- name: Create SCAP content + theforeman.foreman.scap_content: + title: "Red Hat firefox default content" + scap_file: "/home/user/Downloads/ssg-firefox-ds.xml" + original_filename: "ssg-firefox-ds.xml" + organizations: + - "Default Organization" + locations: + - "Default Location" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Update SCAP content + theforeman.foreman.scap_content: + title: "Red Hat firefox default content" + updated_title: "Updated scap content title" + scap_file: "/home/user/Downloads/updated-ssg-firefox-ds.xml" + original_filename: "updated-ssg-firefox-ds.xml" + organizations: + - "Org One" + - "Org Two" + locations: + - "Loc One" + - "Loc Two" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Delete SCAP content + theforeman.foreman.scap_content: + title: "Red Hat firefox default content" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + scap_contents: + description: List of scap contents. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanScapDataStreamModule + + +class ForemanScapContentModule(ForemanScapDataStreamModule): + pass + + +def main(): + module = ForemanScapContentModule( + argument_spec=dict( + updated_title=dict(type='str'), + ), + foreman_spec=dict( + title=dict(type='str', required=True), + ), + entity_key='title', + required_plugins=[('openscap', ['*'])], + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/scap_tailoring_file.py b/ansible_collections/theforeman/foreman/plugins/modules/scap_tailoring_file.py new file mode 100644 index 00000000..c06c7495 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/scap_tailoring_file.py @@ -0,0 +1,125 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov <evgeni@golov.de> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: scap_tailoring_file +version_added: 1.0.0 +short_description: Manage SCAP Tailoring Files +description: + - Create, update, and delete SCAP Tailoring Files +author: + - "Evgeni Golov (@evgeni)" +options: + name: + description: + - Name of the tailoring file. + required: true + type: str + updated_name: + description: + - New name of the tailoring file. + - When this parameter is set, the module will not be idempotent. + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.scap_datastream +''' + +EXAMPLES = ''' +- name: Create SCAP tailoring file + theforeman.foreman.scap_tailoring_file: + name: "Red Hat firefox default content" + scap_file: "/home/user/Downloads/ssg-firefox-ds-tailoring.xml" + original_filename: "ssg-firefox-ds-tailoring.xml" + organizations: + - "Default Organization" + locations: + - "Default Location" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Update SCAP tailoring file + theforeman.foreman.scap_tailoring_file: + name: "Red Hat firefox default content" + updated_name: "Updated tailoring file name" + scap_file: "/home/user/Downloads/updated-ssg-firefox-ds-tailoring.xml" + original_filename: "updated-ssg-firefox-ds-tailoring.xml" + organizations: + - "Org One" + - "Org Two" + locations: + - "Loc One" + - "Loc Two" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: Delete SCAP tailoring file + theforeman.foreman.scap_tailoring_file: + name: "Red Hat firefox default content" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + scap_tailoring_files: + description: List of scap tailoring files. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanScapDataStreamModule + + +class ForemanTailoringFileModule(ForemanScapDataStreamModule): + pass + + +def main(): + module = ForemanTailoringFileModule( + argument_spec=dict( + updated_name=dict(type='str'), + ), + foreman_spec=dict( + name=dict(type='str', required=True), + ), + required_plugins=[('openscap', ['*'])], + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/scc_account.py b/ansible_collections/theforeman/foreman/plugins/modules/scc_account.py new file mode 100644 index 00000000..259e9a7d --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/scc_account.py @@ -0,0 +1,180 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: scc_account +version_added: 1.0.0 +short_description: Manage SUSE Customer Center Accounts +description: + - Manage SUSE Customer Center Accounts + - This module requires the foreman_scc_manager plugin set up in the server + - See U(https://github.com/ATIX-AG/foreman_scc_manager) +author: + - "Manisha Singhal (@manisha15) ATIX AG" +options: + name: + description: Name of the suse customer center account + required: true + type: str + login: + description: Login id of suse customer center account + required: false + type: str + scc_account_password: + description: Password of suse customer center account + required: false + type: str + base_url: + description: URL of SUSE for suse customer center account + required: false + type: str + interval: + description: Interval for syncing suse customer center account + required: false + type: str + choices: ["never", "daily", "weekly", "monthly"] + sync_date: + description: Last Sync time of suse customer center account + required: false + type: str + organization: + description: Name of related organization + type: str + required: true + test_connection: + description: Test suse customer center account credentials that connects to the server + required: false + default: false + type: bool + updated_name: + description: Name to be updated of suse customer center account + type: str + state: + description: State of the suse customer center account + default: present + choices: ["present", "absent", "synced"] + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create a suse customer center account" + theforeman.foreman.scc_account: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Test" + login: "abcde" + scc_account_password: "12345" + base_url: "https://scc.suse.com" + state: present + +- name: "Update a suse customer center account" + theforeman.foreman.scc_account: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Test1" + state: present + +- name: "Delete a suse customer center account" + theforeman.foreman.scc_account: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Test" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + scc_accounts: + description: List of scc accounts. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloSccAccountModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloSccAccountModule( + foreman_spec=dict( + name=dict(required=True), + updated_name=dict(), + login=dict(), + scc_account_password=dict(no_log=True, flat_name='password'), + base_url=dict(), + sync_date=dict(), + interval=dict(choices=['never', 'daily', 'weekly', 'monthly']), + ), + argument_spec=dict( + test_connection=dict(type='bool', default=False), + state=dict(default='present', choices=['present', 'absent', 'synced']), + ), + required_plugins=[('scc_manager', ['*'])], + ) + + module.task_timeout = 4 * 60 + + with module.api_connection(): + module.foreman_spec['entity']['failsafe'] = (module.state != 'synced') + entity = module.lookup_entity('entity') + + if not module.desired_absent: + if not entity: + if 'login' not in module.foreman_params: + module.fail_json(msg="scc account login not provided") + if 'scc_account_password' not in module.foreman_params: + module.fail_json(msg="Scc account password not provided") + + if module.foreman_params['test_connection']: + scc_account_credentials = {} + if entity: + scc_account_credentials['id'] = entity['id'] + if 'login' in module.foreman_params: + scc_account_credentials['login'] = module.foreman_params['login'] + if 'scc_account_password' in module.foreman_params: + scc_account_credentials['password'] = module.foreman_params['scc_account_password'] + if 'base_url' in module.foreman_params: + scc_account_credentials['base_url'] = module.foreman_params['base_url'] + module.resource_action('scc_accounts', 'test_connection', scc_account_credentials, ignore_check_mode=True) + + if module.state == 'synced': + module.resource_action('scc_accounts', 'sync', {'id': entity['id']}) + else: + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/scc_product.py b/ansible_collections/theforeman/foreman/plugins/modules/scc_product.py new file mode 100644 index 00000000..b8123a4f --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/scc_product.py @@ -0,0 +1,108 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: scc_product +version_added: 1.0.0 +short_description: Subscribe SUSE Customer Center Account Products +description: + - Manage SUSE Customer Center Products + - This module requires the foreman_scc_manager plugin set up in the server + - See U(https://github.com/ATIX-AG/foreman_scc_manager) +author: + - "Manisha Singhal (@manisha15) ATIX AG" +options: + scc_product: + description: + - Full name of the product of suse customer center account. + - The I(friendly_name) alias is deprecated as it refers to an attribute that does not + uniquely identify a product and not used for product lookups since SCC Manager 1.8.6. + required: true + type: str + aliases: + - friendly_name + scc_account: + description: Name of the suse customer center account associated with product + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Subscribe to suse customer center product" + theforeman.foreman.scc_product: + scc_product: "Product1" + scc_account: "Test" + organization: "Test Organization" +''' + +RETURN = ''' # ''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloAnsibleModule +from ansible_collections.theforeman.foreman.plugins.module_utils._version import LooseVersion + + +def main(): + module = KatelloAnsibleModule( + foreman_spec=dict( + scc_product=dict(required=True, type='entity', aliases=['friendly_name'], scope=['scc_account'], thin=False), + scc_account=dict(required=True, type='entity', scope=['organization']), + ), + required_plugins=[('scc_manager', ['*'])], + ) + + module.task_timeout = 4 * 60 + + with module.api_connection(): + scc_version = '1.0.0' # fallback + try: + statuses = module.foremanapi.resource('ping').call('statuses') + plugins = statuses['results']['foreman']['plugins'] + for plugin in plugins: + if isinstance(plugin, dict): + if plugin['name'] == 'foreman_scc_manager': + scc_version = plugin['version'] + else: + if 'foreman_scc_manager' in plugin: + scc_version = plugin.split(',')[1] + except Exception: + pass + + if LooseVersion(scc_version.strip()) < LooseVersion('1.8.6'): + scc_search_by = 'friendly_name' + else: + scc_search_by = 'name' + + module.foreman_spec['scc_product']['search_by'] = scc_search_by + scc_product = module.lookup_entity('scc_product') + + if not scc_product.get('product_id'): + payload = {'id': scc_product['id']} + payload.update(module.scope_for('scc_account')) + module.resource_action('scc_products', 'subscribe', payload) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/setting.py b/ansible_collections/theforeman/foreman/plugins/modules/setting.py new file mode 100644 index 00000000..7cf93421 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/setting.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Matthias M Dellweg (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: setting +version_added: 1.0.0 +short_description: Manage Settings +description: + - Manage Settings +author: + - "Matthias M Dellweg (@mdellweg) ATIX AG" +options: + name: + description: + - Name of the Setting + required: true + type: str + value: + description: + - value to set the Setting to + - if missing, reset to default + required: false + type: raw +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: "Set a Setting" + theforeman.foreman.setting: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "http_proxy" + value: "http://localhost:8088" + +- name: "Reset a Setting" + theforeman.foreman.setting: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "http_proxy" +''' + +RETURN = ''' +foreman_setting: + description: Created / Updated state of the setting (deprecated) + returned: success + type: dict +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + settings: + description: List of settings. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanStatelessEntityAnsibleModule, parameter_value_to_str + + +class ForemanSettingModule(ForemanStatelessEntityAnsibleModule): + pass + + +def main(): + module = ForemanSettingModule( + foreman_spec=dict( + name=dict(required=True), + value=dict(type='raw'), + ), + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + if 'value' not in module.foreman_params: + module.foreman_params['value'] = entity['default'] or '' + + settings_type = entity['settings_type'] + new_value = module.foreman_params['value'] + # Allow to pass integers as string + if settings_type == 'integer': + new_value = int(new_value) + module.foreman_params['value'] = parameter_value_to_str(new_value, settings_type) + old_value = entity['value'] + entity['value'] = parameter_value_to_str(old_value, settings_type) + + entity = module.ensure_entity('settings', module.foreman_params, entity, state='present') + + if entity: + # Fake the not serialized input value as output + entity['value'] = new_value + + module.exit_json(foreman_setting=entity) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/setting_info.py b/ansible_collections/theforeman/foreman/plugins/modules/setting_info.py new file mode 100644 index 00000000..9b506c2c --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/setting_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Eric D Helms +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: setting_info +version_added: 2.1.0 +short_description: Fetch information about Settings +description: + - Fetch information about Settings +author: + - "Eric Helms (@ehelms)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a setting" + theforeman.foreman.setting_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "http_proxy" + +- name: "Show all settings with proxy" + theforeman.foreman.setting_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "name = proxy" +''' + +RETURN = ''' +setting: + description: Details about the found setting + returned: success and I(name) was passed + type: dict +settings: + description: List of all found settings and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanSettingInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanSettingInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/smart_class_parameter.py b/ansible_collections/theforeman/foreman/plugins/modules/smart_class_parameter.py new file mode 100644 index 00000000..e8f34582 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/smart_class_parameter.py @@ -0,0 +1,273 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Baptiste Agasse (@bagasse) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: smart_class_parameter +version_added: 1.0.0 +short_description: Manage Smart Class Parameters +description: + - Update Smart Class Parameters. + - Smart Class Parameters are created/deleted for Puppet classes during import and cannot be created or deleted otherwise. +author: + - "Baptiste Agasse (@bagasse)" +options: + puppetclass_name: + description: Name of the puppetclass that own the parameter + required: true + type: str + parameter: + description: Name of the parameter + required: true + type: str + description: + description: Description of the Smart Class Parameter + type: str + override: + description: Whether the smart class parameter value is managed by Foreman + type: bool + default_value: + description: Value to use by default. + type: raw + hidden_value: + description: When enabled the parameter is hidden in the UI. + type: bool + omit: + description: + - Don't send this parameter in classification output. + - Puppet will use the value defined in the Puppet manifest for this parameter. + type: bool + override_value_order: + description: The order in which values are resolved. + type: list + elements: str + validator_type: + description: Types of validation values. + type: str + choices: + - regexp + - list + validator_rule: + description: Used to enforce certain values for the parameter values. + type: str + parameter_type: + description: Types of variable values. If C(none), set the parameter type to empty value. + type: str + choices: + - string + - boolean + - integer + - real + - array + - hash + - yaml + - json + - none + required: + description: If true, will raise an error if there is no default value and no matcher provide a value. + type: bool + merge_overrides: + description: Merge all matching values (only array/hash type). + type: bool + merge_default: + description: Include default value when merging all matching values. + type: bool + avoid_duplicates: + description: Remove duplicate values (only array type) + type: bool + override_values: + description: Value overrides + required: false + type: list + elements: dict + suboptions: + match: + description: Override match + required: true + type: str + value: + description: Override value, required if omit is false + type: raw + omit: + description: Don't send this parameter in classification output, replaces use_puppet_default. + type: bool + state: + description: State of the entity. + type: str + default: present + choices: + - present + - present_with_defaults +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: "Update prometheus::server alertmanagers_config param default value" + theforeman.foreman.smart_class_parameter: + puppetclass_name: "prometheus::server" + parameter: alertmanagers_config + override: true + required: true + default_value: /etc/prometheus/alert.yml + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present + +- name: "Update prometheus::server alertmanagers_config param default value" + theforeman.foreman.smart_class_parameter: + puppetclass_name: "prometheus::server" + parameter: alertmanagers_config + override: true + override_value_order: + - fqdn + - hostgroup + - domain + required: true + default_value: /etc/prometheus/alert.yml + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + override_values: + - match: domain=example.com + value: foo + - match: domain=foo.example.com + omit: true + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + smart_class_parameters: + description: List of smart class parameters. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule, parameter_value_to_str + +override_value_foreman_spec = dict( + id=dict(invisible=True), + match=dict(required=True), + value=dict(type='raw'), + omit=dict(type='bool'), +) + + +class ForemanSmartClassParameterModule(ForemanEntityAnsibleModule): + # TODO: greatly similar to how parameters are managed, dry it up ? + def ensure_override_values(self, entity, expected_override_values): + if expected_override_values is not None: + parameter_type = entity.get('parameter_type', 'string') + scope = {'smart_class_parameter_id': entity['id']} + if not self.desired_absent: + current_override_values = {override_value['match']: override_value for override_value in entity.get('override_values', [])} + desired_override_values = {override_value['match']: override_value for override_value in expected_override_values} + + for match in desired_override_values: + desired_override_value = desired_override_values[match] + if 'value' in desired_override_value: + desired_override_value['value'] = parameter_value_to_str(desired_override_value['value'], parameter_type) + current_override_value = current_override_values.pop(match, None) + if current_override_value: + current_override_value['value'] = parameter_value_to_str(current_override_value['value'], parameter_type) + self.ensure_entity( + 'override_values', desired_override_value, current_override_value, + state="present", foreman_spec=override_value_foreman_spec, params=scope) + for current_override_value in current_override_values.values(): + self.ensure_entity( + 'override_values', None, current_override_value, state="absent", foreman_spec=override_value_foreman_spec, params=scope) + + +def main(): + module = ForemanSmartClassParameterModule( + argument_spec=dict( + puppetclass_name=dict(required=True), + parameter=dict(required=True), + state=dict(default='present', choices=['present_with_defaults', 'present']), + ), + foreman_spec=dict( + parameter_type=dict(choices=['string', 'boolean', 'integer', 'real', 'array', 'hash', 'yaml', 'json', 'none']), + validator_type=dict(choices=['list', 'regexp']), + validator_rule=dict(), + description=dict(), + default_value=dict(type='raw'), + omit=dict(type='bool'), + override=dict(type='bool'), + merge_default=dict(type='bool'), + merge_overrides=dict(type='bool'), + avoid_duplicates=dict(type='bool'), + required=dict(type='bool'), + hidden_value=dict(type='bool'), + override_value_order=dict(type='list', elements='str'), + # tried nested_list here but, if using nested_list, override_values are not part of loaded entity. + # override_values=dict(type='nested_list', elements='dict', foreman_spec=override_value_foreman_spec), + override_values=dict(type='list', elements='dict'), + ), + # smart_class_parameters are created on puppetclass import and cannot be created/deleted from API, + # so if we don't find it, it's an error. + entity_opts=dict(failsafe=False), + ) + + module_params = module.foreman_params + if module_params.get('parameter_type', 'string') not in ['array', 'hash']: + if 'merge_default' in module_params or 'merge_overrides' in module_params: + module.fail_json(msg="merge_default or merge_overrides can be used only with array or hash parameter_type") + if module_params.get('parameter_type', 'string') != 'array' and 'avoid_duplicates' in module_params: + module.fail_json(msg="avoid_duplicates can be used only with array parameter_type") + + search = "puppetclass_name={0} and parameter={1}".format(module_params['puppetclass_name'], module_params['parameter']) + override_values = module_params.pop('override_values', None) + + if 'override_value_order' in module_params: + module_params['override_value_order'] = '\n'.join(module_params['override_value_order']) + if 'parameter_type' in module_params and module_params['parameter_type'] == 'none': + module_params['parameter_type'] = '' + + with module.api_connection(): + entity = module.find_resource('smart_class_parameters', search=search) + module.set_entity('entity', entity) + # When override is set to false, foreman API don't accept parameter_type and all 'override options' have to be set to false if present + if not module_params.get('override', False): + module_params['parameter_type'] = '' + for override_option in ['merge_default', 'merge_overrides', 'avoid_duplicates']: + if override_option in entity and entity[override_option]: + module_params[override_option] = False + + # Foreman API returns 'hidden_value?' instead of 'hidden_value' this is a bug ? + if 'hidden_value?' in entity: + entity['hidden_value'] = entity.pop('hidden_value?') + if 'default_value' in module_params: + module_params['default_value'] = parameter_value_to_str(module_params['default_value'], module_params.get('parameter_type', 'string')) + if 'default_value' in entity: + entity['default_value'] = parameter_value_to_str(entity['default_value'], entity.get('parameter_type', 'string')) + + entity = module.run() + module.ensure_override_values(entity, override_values) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/smart_proxy.py b/ansible_collections/theforeman/foreman/plugins/modules/smart_proxy.py new file mode 100644 index 00000000..4dc81bbd --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/smart_proxy.py @@ -0,0 +1,170 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: smart_proxy +version_added: 1.4.0 +short_description: Manage Smart Proxies +description: + - Create, update and delete Smart Proxies +author: + - "James Stuart (@jstuart)" + - "Matthias M Dellweg (@mdellweg)" + - "Jeffrey van Pelt (@Thulium-Drake)" +options: + name: + description: + - Name of the Smart Proxy + required: true + type: str + lifecycle_environments: + description: + - Lifecycle Environments synced to the Smart Proxy. + - Only available for Katello installations. + required: false + elements: str + type: list + url: + description: + - URL of the Smart Proxy + required: true + type: str + download_policy: + description: + - The download policy for the Smart Proxy + - Only available for Katello installations. + - The download policy C(background) is deprecated and not available since Katello 4.3. + - The download policy C(streamed) is available since Katello 4.5. + choices: + - background + - immediate + - on_demand + - streamed + - inherit + required: false + type: str +notes: + - Even with I(state=present) this module does not install a new Smart Proxy. + - It can only associate an existing Smart Proxy listening at the specified I(url). + - Consider using I(foreman-installer) to create Smart Proxies. +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +# Create a local Smart Proxy +- name: "Create Smart Proxy" + theforeman.foreman.smart_proxy: + username: "admin" + password: "changeme" + server_url: "https://{{ ansible_fqdn }}" + name: "{{ ansible_fqdn }}" + url: "https://{{ ansible_fqdn }}:9090" + download_policy: "immediate" + lifecycle_environments: + - "Development" + organizations: + - "Default Organization" + locations: + - "Default Location" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + smart_proxies: + description: List of smart_proxies. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicEntityAnsibleModule + + +class ForemanSmartProxyModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanSmartProxyModule( + foreman_spec=dict( + name=dict(required=True), + url=dict(required=True), + lifecycle_environments=dict(required=False, type='entity_list'), + download_policy=dict(required=False, choices=['background', 'immediate', 'on_demand', 'streamed', 'inherit']), + ), + required_plugins=[('katello', ['lifecycle_environments', 'download_policy'])], + ) + + with module.api_connection(): + handle_lifecycle_environments = not module.desired_absent and 'lifecycle_environments' in module.foreman_params + if handle_lifecycle_environments: + module.lookup_entity('lifecycle_environments') + lifecycle_environments = module.foreman_params.pop('lifecycle_environments', []) + + smart_proxy = module.lookup_entity('entity') + new_smart_proxy = module.run() + + if handle_lifecycle_environments: + + if smart_proxy: + payload = { + 'id': new_smart_proxy['id'], + } + current_lces = module.resource_action('capsule_content', 'lifecycle_environments', payload, ignore_check_mode=True, record_change=False) + else: + current_lces = {'results': []} + + desired_environment_ids = set(lifecycle_environment['id'] for lifecycle_environment in lifecycle_environments) + current_environment_ids = set(lifecycle_environment['id'] for lifecycle_environment in current_lces['results']) if current_lces else set() + + module.record_before('smart_proxy_content/lifecycle_environment_ids', current_environment_ids) + module.record_after('smart_proxy_content/lifecycle_environment_ids', desired_environment_ids) + module.record_after_full('smart_proxy_content/lifecycle_environment_ids', desired_environment_ids) + + if desired_environment_ids != current_environment_ids: + environment_ids_to_add = desired_environment_ids - current_environment_ids + if environment_ids_to_add: + for environment_id_to_add in environment_ids_to_add: + payload = { + 'id': new_smart_proxy['id'], + 'environment_id': environment_id_to_add, + } + module.resource_action('capsule_content', 'add_lifecycle_environment', payload) + environment_ids_to_remove = current_environment_ids - desired_environment_ids + if environment_ids_to_remove: + for environment_id_to_remove in environment_ids_to_remove: + payload = { + 'id': smart_proxy['id'], + 'environment_id': environment_id_to_remove, + } + module.resource_action('capsule_content', 'remove_lifecycle_environment', payload) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/snapshot.py b/ansible_collections/theforeman/foreman/plugins/modules/snapshot.py new file mode 100644 index 00000000..6ac77706 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/snapshot.py @@ -0,0 +1,178 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: snapshot +version_added: 1.0.0 +short_description: Manage Snapshots +description: + - "Manage Snapshots for Host Entities" + - "This module can create, update, revert and delete snapshots" + - "This module requires the foreman_snapshot_management plugin set up in the server" + - "See: U(https://github.com/ATIX-AG/foreman_snapshot_management)" +author: + - "Manisha Singhal (@Manisha15) ATIX AG" +options: + name: + description: + - Name of Snapshot + required: true + type: str + description: + description: + - Description of Snapshot + required: false + type: str + host: + description: + - Name of related Host + required: true + type: str + include_ram: + description: + - Option to add RAM (only available for VMWare compute-resource) + required: false + type: bool + state: + description: + - State of Snapshot + default: present + choices: ["present", "reverted", "absent", "new_snapshot"] + type: str + id: + description: + - Id of Snapshot + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: "Create a Snapshot" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + state: present + +- name: "Create Snapshots with same name" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + state: new_snapshot + +- name: "Update a Snapshot" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + description: "description of snapshot" + state: present + +- name: "Update a Snapshot with same name" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + description: "description of snapshot" + state: present + id: "snapshot-id" + +- name: "Revert a Snapshot" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + state: reverted + +- name: "Delete a Snapshot" + theforeman.foreman.snapshot: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "snapshot_before_software_upgrade" + host: "server.example.com" + state: absent +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + snapshots: + description: List of snapshots. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanSnapshotModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanSnapshotModule( + argument_spec=dict( + state=dict(default='present', choices=['present', 'absent', 'reverted', 'new_snapshot']), + ), + foreman_spec=dict( + host=dict(type='entity', required=True, ensure=False), + name=dict(required=True), + description=dict(), + include_ram=dict(type='bool'), + id=dict(), + ), + required_plugins=[('snapshot_management', ['*'])], + entity_opts={'scope': ['host']}, + ) + + with module.api_connection(): + host_val = module.lookup_entity('host') + params = {'host_id': host_val['id']} + if module.state == 'new_snapshot': + module.ensure_entity('snapshots', module.foreman_params, None, params=params) + elif module.state != 'new_snapshot' and module.foreman_params.get('id'): + snapshot = module.resource_action('snapshots', 'show', params={'id': module.params['id'], 'host_id': host_val['id']}) + module.ensure_entity('snapshots', module.foreman_params, snapshot, params=params, state=module.state) + else: + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/snapshot_info.py b/ansible_collections/theforeman/foreman/plugins/modules/snapshot_info.py new file mode 100644 index 00000000..622220d4 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/snapshot_info.py @@ -0,0 +1,95 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2022 Manisha Singhal (ATIX AG) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: snapshot_info +version_added: 3.8.0 +short_description: Fetch information about Foreman Snapshots +description: + - Fetch information about Foreman Snapshots +author: + - "Manisha Singhal (@Manisha15) ATIX AG" +options: + host: + description: + - Name of related Host + required: true + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show all snapshots for a host" + theforeman.foreman.snapshot_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + host: "server.example.com" + +- name: "Show a snapshot" + theforeman.foreman.snapshot_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + host: "server.example.com" + search: "name=ansible" +''' + +RETURN = ''' +snapshots: + description: List of all snapshots and their details for a host + returned: success and I(search) was passed + type: list + elements: dict +snapshot: + description: Details about the first found snapshot with searched name + returned: success and I(name) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanSnapshotInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanSnapshotInfo( + foreman_spec=dict( + host=dict(type='entity', required=True), + ), + entity_opts={'scope': ['host', 'snapshot']}, + required_plugins=[('snapshot_management', ['*'])], + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/status_info.py b/ansible_collections/theforeman/foreman/plugins/modules/status_info.py new file mode 100644 index 00000000..a83e4f98 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/status_info.py @@ -0,0 +1,76 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: status_info +version_added: 1.3.0 +short_description: Get status info +description: + - Get status information from the server +author: + - "Evgeni Golov (@evgeni)" +extends_documentation_fragment: + - theforeman.foreman.foreman +''' + +EXAMPLES = ''' +- name: status + theforeman.foreman.status_info: + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" +''' + +RETURN = ''' +status: + description: Basic status of the server. + returned: always + type: dict +ping: + description: Detailed service status. + returned: if supported by server + type: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanAnsibleModule + + +def main(): + module = ForemanAnsibleModule() + + with module.api_connection(): + status = module.status() + + if 'ping' in module.foremanapi.resources: + if 'ping' in module.foremanapi.resource('ping').actions: + ping_action = 'ping' + else: + ping_action = 'index' + ping = module.foremanapi.resource('ping').call(ping_action) + else: + ping = None + + module.exit_json(status=status, ping=ping) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/subnet.py b/ansible_collections/theforeman/foreman/plugins/modules/subnet.py new file mode 100644 index 00000000..a79c9e7b --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/subnet.py @@ -0,0 +1,292 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2018 Baptiste AGASSE (baptiste.agasse@gmail.com) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: subnet +version_added: 1.0.0 +short_description: Manage Subnets +description: + - Create, update, and delete Subnets +author: + - "Baptiste Agasse (@bagasse)" +requirements: + - ipaddress +options: + name: + description: Subnet name + required: true + type: str + description: + description: Description of the subnet + type: str + updated_name: + description: New subnet name. When this parameter is set, the module will not be idempotent. + type: str + network_type: + description: Subnet type + default: IPv4 + choices: ["IPv4", "IPv6"] + type: str + dns_primary: + description: Primary DNS server for this subnet + required: false + type: str + dns_secondary: + description: Secondary DNS server for this subnet + required: false + type: str + domains: + description: List of DNS domains the subnet should assigned to + required: false + type: list + elements: str + gateway: + description: Subnet gateway IP address + required: false + type: str + network: + description: Subnet IP address + required: true + type: str + cidr: + description: CIDR prefix length; Required if I(network_type=IPv4) and no I(mask) provided + type: int + mask: + description: Subnet netmask. Required if I(network_type=IPv4) and no I(cidr) prefix length provided + type: str + from_ip: + description: First IP address of the host IP allocation pool + required: false + type: str + to_ip: + description: Last IP address of the host IP allocation pool + required: false + type: str + boot_mode: + description: Boot mode used by hosts in this subnet + required: false + default: DHCP + choices: ["DHCP", "Static"] + type: str + ipam: + description: IPAM mode for this subnet + required: false + default: DHCP + choices: + - "DHCP" + - "Internal DB" + - "Random DB" + - "EUI-64" + - "External IPAM" + - "None" + type: str + dhcp_proxy: + description: DHCP Smart proxy for this subnet + required: false + type: str + httpboot_proxy: + description: HTTP Boot Smart proxy for this subnet + required: false + type: str + tftp_proxy: + description: TFTP Smart proxy for this subnet + required: false + type: str + discovery_proxy: + description: + - Discovery Smart proxy for this subnet + - This option is only available if the discovery plugin is installed. + required: false + type: str + dns_proxy: + description: Reverse DNS Smart proxy for this subnet + required: false + type: str + template_proxy: + description: Template Smart proxy for this subnet + required: false + type: str + bmc_proxy: + description: BMC Smart proxy for this subnet + required: false + type: str + version_added: 2.1.0 + remote_execution_proxies: + description: + - Remote execution Smart proxies for this subnet + - This option is only available if the remote_execution plugin is installed. + - This will always report I(changed=true) when used with I(remote_execution < 4.1.0), due to a bug in the plugin. + required: false + type: list + elements: str + externalipam_proxy: + description: + - External IPAM proxy for this subnet. + - Only relevant if I(ipam=External IPAM). + required: false + type: str + externalipam_group: + description: + - External IPAM group for this subnet. + - Only relevant if I(ipam=External IPAM). + version_added: 1.5.0 + required: false + type: str + vlanid: + description: VLAN ID + required: false + type: int + mtu: + description: MTU + required: false + type: int + parameters: + description: + - Subnet specific host parameters +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy + - theforeman.foreman.foreman.nested_parameters +''' + +EXAMPLES = ''' +- name: My subnet + theforeman.foreman.subnet: + name: "My subnet" + description: "My description" + network: "192.168.0.0" + mask: "255.255.255.192" + gateway: "192.168.0.1" + from_ip: "192.168.0.2" + to_ip: "192.168.0.42" + boot_mode: "Static" + dhcp_proxy: "smart-proxy1.foo.example.com" + tftp_proxy: "smart-proxy1.foo.example.com" + dns_proxy: "smart-proxy2.foo.example.com" + template_proxy: "smart-proxy2.foo.example.com" + vlanid: 452 + mtu: 9000 + domains: + - "foo.example.com" + - "bar.example.com" + organizations: + - "Example Org" + locations: + - "Toulouse" + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + subnets: + description: List of subnets. + type: list + elements: dict +''' + +import traceback +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, ParametersMixin, missing_required_lib +) +try: + import ipaddress + HAS_IPADDRESS = True + IPADDRESS_IMP_ERR = None +except ImportError: + HAS_IPADDRESS = False + IPADDRESS_IMP_ERR = traceback.format_exc() + + +class ForemanSubnetModule(ParametersMixin, ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanSubnetModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + description=dict(), + network_type=dict(choices=['IPv4', 'IPv6'], default='IPv4'), + dns_primary=dict(), + dns_secondary=dict(), + domains=dict(type='entity_list'), + gateway=dict(), + network=dict(required=True), + cidr=dict(type='int'), + mask=dict(), + from_ip=dict(flat_name='from'), + to_ip=dict(flat_name='to'), + boot_mode=dict(choices=['DHCP', 'Static'], default='DHCP'), + ipam=dict(choices=['DHCP', 'Internal DB', 'Random DB', 'EUI-64', 'External IPAM', 'None'], default='DHCP'), + dhcp_proxy=dict(type='entity', flat_name='dhcp_id', resource_type='smart_proxies'), + httpboot_proxy=dict(type='entity', flat_name='httpboot_id', resource_type='smart_proxies'), + tftp_proxy=dict(type='entity', flat_name='tftp_id', resource_type='smart_proxies'), + discovery_proxy=dict(type='entity', flat_name='discovery_id', resource_type='smart_proxies'), + dns_proxy=dict(type='entity', flat_name='dns_id', resource_type='smart_proxies'), + template_proxy=dict(type='entity', flat_name='template_id', resource_type='smart_proxies'), + bmc_proxy=dict(type='entity', flat_name='bmc_id', resource_type='smart_proxies'), + remote_execution_proxies=dict(type='entity_list', resource_type='smart_proxies'), + externalipam_proxy=dict(type='entity', flat_name='externalipam_id', resource_type='smart_proxies'), + externalipam_group=dict(), + vlanid=dict(type='int'), + mtu=dict(type='int'), + ), + required_plugins=[ + ('discovery', ['discovery_proxy']), + ('remote_execution', ['remote_execution_proxies']), + ], + ) + + if not HAS_IPADDRESS: + module.fail_json(msg=missing_required_lib("ipaddress"), exception=IPADDRESS_IMP_ERR) + + module_params = module.foreman_params + + if not module.desired_absent: + if module_params['network_type'] == 'IPv4': + if 'mask' not in module_params and 'cidr' not in module_params: + module.fail_json(msg='When specifying IPv4 networks, either "mask" or "cidr" is required.') + IPNetwork = ipaddress.IPv4Network + else: + IPNetwork = ipaddress.IPv6Network + if 'mask' in module_params and 'cidr' not in module_params: + module_params['cidr'] = IPNetwork(u'%s/%s' % (module_params['network'], module_params['mask'])).prefixlen + elif 'mask' not in module_params and 'cidr' in module_params: + module_params['mask'] = str(IPNetwork(u'%s/%s' % (module_params['network'], module_params['cidr'])).netmask) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/subnet_info.py b/ansible_collections/theforeman/foreman/plugins/modules/subnet_info.py new file mode 100644 index 00000000..911a1eb1 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/subnet_info.py @@ -0,0 +1,81 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: subnet_info +version_added: 2.1.0 +short_description: Fetch information about Subnets +description: + - Fetch information about Subnets +author: + - "Evgeni Golov (@evgeni)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a subnet" + theforeman.foreman.subnet_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "subnet.example.com" + +- name: "Show all subnets with domain example.com" + theforeman.foreman.subnet_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: "domain = example.com" +''' + +RETURN = ''' +subnet: + description: Details about the found subnet + returned: success and I(name) was passed + type: dict +subnets: + description: List of all found subnets and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanInfoAnsibleModule, +) + + +class ForemanSubnetInfo(ForemanInfoAnsibleModule): + pass + + +def main(): + module = ForemanSubnetInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/subscription_info.py b/ansible_collections/theforeman/foreman/plugins/modules/subscription_info.py new file mode 100644 index 00000000..9ac5851a --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/subscription_info.py @@ -0,0 +1,82 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2021 Evgeni Golov +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: subscription_info +version_added: 2.1.0 +short_description: Fetch information about Subscriptions +description: + - Fetch information about Subscriptions +author: + - "Evgeni Golov (@evgeni)" +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.katelloinfomodule + - theforeman.foreman.foreman.infomodule +''' + +EXAMPLES = ''' +- name: "Show a subscription" + theforeman.foreman.subscription_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Red Hat Satellite Infrastructure Subscription" + +- name: "Show all subscriptions with a certain name" + theforeman.foreman.subscription_info: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + search: name="Red Hat Satellite Infrastructure Subscription" +''' + +RETURN = ''' +subscription: + description: Details about the found subscription + returned: success and I(name) was passed + type: dict +subscriptions: + description: List of all found subscriptions and their details + returned: success and I(search) was passed + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + KatelloInfoAnsibleModule +) + + +class KatelloSubscriptionInfo(KatelloInfoAnsibleModule): + pass + + +def main(): + module = KatelloSubscriptionInfo() + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/subscription_manifest.py b/ansible_collections/theforeman/foreman/plugins/modules/subscription_manifest.py new file mode 100644 index 00000000..b93570e0 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/subscription_manifest.py @@ -0,0 +1,134 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Andrew Kofink <ajkofink@gmail.com> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: subscription_manifest +version_added: 1.0.0 +short_description: Manage Subscription Manifests +description: + - Upload, refresh and delete Subscription Manifests +author: "Andrew Kofink (@akofink)" +options: + manifest_path: + description: + - Path to the manifest zip file + - This parameter will be ignored if I(state=absent) or I(state=refreshed) + type: path + state: + description: + - The state of the manifest + default: present + choices: + - absent + - present + - refreshed + type: str + repository_url: + description: + - URL to retrieve content from + aliases: [ redhat_repository_url ] + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Upload the RHEL developer edition manifest" + theforeman.foreman.subscription_manifest: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + organization: "Default Organization" + state: present + manifest_path: "/tmp/manifest.zip" +''' + +RETURN = ''' # ''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +def main(): + module = KatelloEntityAnsibleModule( + argument_spec=dict( + manifest_path=dict(type='path'), + state=dict(default='present', choices=['absent', 'present', 'refreshed']), + repository_url=dict(aliases=['redhat_repository_url']), + ), + foreman_spec=dict( + organization=dict(type='entity', required=True, thin=False), + ), + required_if=[ + ['state', 'present', ['manifest_path']], + ], + supports_check_mode=False, + ) + + module.task_timeout = 10 * 60 + + with module.api_connection(): + organization = module.lookup_entity('organization') + scope = module.scope_for('organization') + + try: + existing_manifest = organization['owner_details']['upstreamConsumer'] + except KeyError: + existing_manifest = None + + if module.state == 'present': + if 'repository_url' in module.foreman_params: + payload = {'redhat_repository_url': module.foreman_params['repository_url']} + org_spec = dict(id=dict(), redhat_repository_url=dict()) + organization = module.ensure_entity('organizations', payload, organization, state='present', foreman_spec=org_spec) + + try: + with open(module.foreman_params['manifest_path'], 'rb') as manifest_file: + files = {'content': (module.foreman_params['manifest_path'], manifest_file, 'application/zip')} + params = {} + if 'repository_url' in module.foreman_params: + params['repository_url'] = module.foreman_params['repository_url'] + params.update(scope) + result = module.resource_action('subscriptions', 'upload', params, files=files, record_change=False, ignore_task_errors=True) + for error in result['humanized']['errors']: + if "same as existing data" in error: + # Nothing changed, but everything ok + break + if "older than existing data" in error: + module.fail_json(msg="Manifest is older than existing data.") + else: + module.fail_json(msg="Upload of the manifest failed: %s" % error) + else: + module.set_changed() + except IOError as e: + module.fail_json(msg="Unable to read the manifest file: %s" % e) + elif module.desired_absent and existing_manifest: + module.resource_action('subscriptions', 'delete_manifest', scope) + elif module.state == 'refreshed': + if existing_manifest: + module.resource_action('subscriptions', 'refresh_manifest', scope) + else: + module.fail_json(msg="No manifest found to refresh.") + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/sync_plan.py b/ansible_collections/theforeman/foreman/plugins/modules/sync_plan.py new file mode 100644 index 00000000..ee8ebece --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/sync_plan.py @@ -0,0 +1,180 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2017, Andrew Kofink <ajkofink@gmail.com> +# (c) 2019, Matthias Dellweg <dellweg@atix.de> +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: sync_plan +version_added: 1.0.0 +short_description: Manage Sync Plans +description: + - Manage sync plans +author: + - "Andrew Kofink (@akofink)" + - "Matthis Dellweg (@mdellweg) ATIX-AG" +options: + name: + description: + - Name of the sync plan + required: true + type: str + description: + description: + - Description of the sync plan + type: str + interval: + description: + - How often synchronization should run + choices: + - hourly + - daily + - weekly + - custom cron + required: true + type: str + enabled: + description: + - Whether the sync plan is active + required: true + type: bool + sync_date: + description: + - Start date and time of the first synchronization + required: true + type: str + cron_expression: + description: + - A cron expression as found in crontab files + - This must be provided together with I(interval='custom cron'). + type: str + products: + description: + - List of products to include in the sync plan + required: false + type: list + elements: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state_with_defaults + - theforeman.foreman.foreman.organization +''' + +EXAMPLES = ''' +- name: "Create or update weekly RHEL sync plan" + theforeman.foreman.sync_plan: + username: "admin" + password: "changeme" + server_url: "https://foreman.example.com" + name: "Weekly RHEL Sync" + organization: "Default Organization" + interval: "weekly" + enabled: false + sync_date: "2017-01-01 00:00:00 UTC" + products: + - 'Red Hat Enterprise Linux Server' + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + sync_plans: + description: List of sync plans. + type: list + elements: dict +''' + + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import KatelloEntityAnsibleModule + + +class KatelloSyncPlanModule(KatelloEntityAnsibleModule): + pass + + +def main(): + module = KatelloSyncPlanModule( + foreman_spec=dict( + name=dict(required=True), + description=dict(), + interval=dict(choices=['hourly', 'daily', 'weekly', 'custom cron'], required=True), + enabled=dict(type='bool', required=True), + sync_date=dict(required=True), + cron_expression=dict(), + products=dict(type='entity_list', scope=['organization'], resolve=False), + ), + argument_spec=dict( + state=dict(default='present', choices=['present_with_defaults', 'present', 'absent']), + ), + required_if=[ + ['interval', 'custom cron', ['cron_expression']], + ], + ) + + if (module.foreman_params['interval'] != 'custom cron') and ('cron_expression' in module.foreman_params): + module.fail_json(msg='"cron_expression" cannot be combined with "interval"!="custom cron".') + + with module.api_connection(): + entity = module.lookup_entity('entity') + scope = module.scope_for('organization') + + handle_products = not (module.desired_absent or module.state == 'present_with_defaults') and 'products' in module.foreman_params + if handle_products: + module.lookup_entity('products') + + products = module.foreman_params.pop('products', None) + sync_plan = module.run() + + if handle_products: + desired_product_ids = set(product['id'] for product in products) + current_product_ids = set(product['id'] for product in entity['products']) if entity else set() + + module.record_before('sync_plans/products', {'id': sync_plan['id'], 'product_ids': current_product_ids}) + module.record_after('sync_plans/products', {'id': sync_plan['id'], 'product_ids': desired_product_ids}) + module.record_after_full('sync_plans/products', {'id': sync_plan['id'], 'product_ids': desired_product_ids}) + + if desired_product_ids != current_product_ids: + if not module.check_mode: + product_ids_to_add = desired_product_ids - current_product_ids + if product_ids_to_add: + payload = { + 'id': sync_plan['id'], + 'product_ids': list(product_ids_to_add), + } + payload.update(scope) + module.resource_action('sync_plans', 'add_products', payload) + product_ids_to_remove = current_product_ids - desired_product_ids + if product_ids_to_remove: + payload = { + 'id': sync_plan['id'], + 'product_ids': list(product_ids_to_remove), + } + payload.update(scope) + module.resource_action('sync_plans', 'remove_products', payload) + else: + module.set_changed() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/templates_import.py b/ansible_collections/theforeman/foreman/plugins/modules/templates_import.py new file mode 100644 index 00000000..d531039c --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/templates_import.py @@ -0,0 +1,193 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2020 Anton Nesterov (@nesanton) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: templates_import +version_added: 1.0.0 +short_description: Sync Templates from a repository +description: + - Sync provisioning templates, report_templates, partition tables and job templates from external git repository or file system. + - Based on foreman_templates plugin U(https://github.com/theforeman/foreman_templates). +author: + - "Anton Nesterov (@nesanton)" +notes: + - Due to a bug in the foreman_templates plugin, this module won't report C(changed=true) + when the only change is the Organization/Location association of the imported templates. + Please see U(https://projects.theforeman.org/issues/29534) for details. + - Default values for all module options can be set using M(theforeman.foreman.setting) for TemplateSync category or on the settings page in WebUI. +options: + prefix: + description: + - Adds specified string to beginning of all imported templates that do not yet have that prefix. + required: false + type: str + associate: + description: + - Associate to Operatingsystems, Locations and Organizations based on metadata. + required: false + type: str + choices: + - always + - new + - never + verbose: + description: + - Add template reports to the output. + required: false + type: bool + force: + description: + - Update templates that are locked. + required: false + type: bool + lock: + description: + - Lock imported templates. + required: false + type: bool + branch: + description: + - Branch of the I(repo). Only for git-based repositories. + required: false + type: str + repo: + description: + - Filesystem path or repo (with protocol), for example /tmp/dir or git://example.com/repo.git or https://example.com/repo.git. + required: false + type: str + filter: + description: + - Sync only templates with name matching this regular expression, after I(prefix) was applied. + - Case-insensitive, snippets are not filtered. + required: false + type: str + negate: + description: + - Negate the filter condition. + required: false + type: bool + dirname: + description: + - The directory within Git repo containing the templates. + required: false + type: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: Sync templates from git repo + theforeman.foreman.templates_import: + repo: https://github.com/theforeman/community-templates.git + branch: 1.24-stable + associate: new + server_url: "https://foreman.example.com" + username: "admin" + password: "changeme" +''' + +RETURN = ''' +message: + description: Information about the import. + returned: success + type: dict + contains: + repo: + description: Repository, the templates were imported from. + type: str + branch: + description: Branch used in the repository. + type: str +report: + description: Report of the import. + returned: success + type: dict + contains: + changed: + description: List of templates that have been updated. + type: list + new: + description: List of templates that have been created. + type: list +templates: + description: Final state of the templates. + returned: success + type: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanTaxonomicAnsibleModule, _flatten_entity + + +def main(): + module = ForemanTaxonomicAnsibleModule( + foreman_spec=dict( + associate=dict(choices=['always', 'new', 'never']), + prefix=dict(), + branch=dict(), + repo=dict(), + filter=dict(), + dirname=dict(), + verbose=dict(type='bool'), + force=dict(type='bool'), + lock=dict(type='bool'), + negate=dict(type='bool'), + ), + supports_check_mode=False, + required_plugins=[('templates', ['*'])], + ) + + with module.api_connection(): + + module.auto_lookup_entities() + + # Build a list of all existing templates of all supported types to check if we are adding any new + template_report = [] + + template_types = ['provisioning_templates', 'report_templates', 'ptables'] + if 'job_templates' in module.foremanapi.resources: + template_types.append('job_templates') + + for template_type in template_types: + template_report += [(resource['name'], resource['id']) for resource in module.list_resource(template_type)] + + result = module.resource_action('templates', 'import', record_change=False, params=_flatten_entity(module.foreman_params, module.foreman_spec)) + msg_templates = result['message'].pop('templates', []) + + report = {'changed': [], 'new': []} + templates = {} + + for template in msg_templates: + if template['changed']: + report['changed'].append(template['name']) + module.set_changed() + elif template['imported']: + if (template['name'], template['id']) not in template_report: + report['new'].append(template['name']) + module.set_changed() + templates[template.pop('name')] = template + + module.exit_json(templates=templates, message=result['message'], report=report) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/user.py b/ansible_collections/theforeman/foreman/plugins/modules/user.py new file mode 100644 index 00000000..c4ef0170 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/user.py @@ -0,0 +1,548 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Christoffer Reijer (Basalt AB) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: user +version_added: 1.0.0 +short_description: Manage Users +description: + - Create, update, and delete users +author: + - "Christoffer Reijer (@ephracis) Basalt AB" +options: + login: + aliases: + - name + description: + - Name of the user + required: true + type: str + firstname: + description: + - First name of the user + required: false + type: str + lastname: + description: + - Last name of the user + required: false + type: str + mail: + description: + - Email address of the user + - Required when creating a new user + required: false + type: str + description: + description: + - Description of the user + required: false + type: str + admin: + description: + - Whether or not the user is an administrator + required: false + default: false + type: bool + user_password: + description: + - Password for the user. + - When this parameter is set, the module will not be idempotent. + required: false + type: str + default_location: + description: + - The location that the user uses by default + required: false + type: str + default_organization: + description: + - The organizxation that the user uses by default + required: false + type: str + auth_source: + description: + - Authentication source where the user exists + required: false + type: str + timezone: + description: + - Timezone for the user + - If blank it will use the browser timezone. + required: false + type: str + choices: + - 'International Date Line West' + - 'American Samoa' + - 'Midway Island' + - 'Hawaii' + - 'Alaska' + - 'Pacific Time (US & Canada)' + - 'Tijuana' + - 'Arizona' + - 'Chihuahua' + - 'Mazatlan' + - 'Mountain Time (US & Canada)' + - 'Central America' + - 'Central Time (US & Canada)' + - 'Guadalajara' + - 'Mexico City' + - 'Monterrey' + - 'Saskatchewan' + - 'Bogota' + - 'Eastern Time (US & Canada)' + - 'Indiana (East)' + - 'Lima' + - 'Quito' + - 'Atlantic Time (Canada)' + - 'Caracas' + - 'Georgetown' + - 'La Paz' + - 'Puerto Rico' + - 'Santiago' + - 'Newfoundland' + - 'Brasilia' + - 'Buenos Aires' + - 'Greenland' + - 'Montevideo' + - 'Mid-Atlantic' + - 'Azores' + - 'Cape Verde Is.' + - 'Dublin' + - 'Edinburgh' + - 'Lisbon' + - 'London' + - 'Monrovia' + - 'UTC' + - 'Amsterdam' + - 'Belgrade' + - 'Berlin' + - 'Bern' + - 'Bratislava' + - 'Brussels' + - 'Budapest' + - 'Casablanca' + - 'Copenhagen' + - 'Ljubljana' + - 'Madrid' + - 'Paris' + - 'Prague' + - 'Rome' + - 'Sarajevo' + - 'Skopje' + - 'Stockholm' + - 'Vienna' + - 'Warsaw' + - 'West Central Africa' + - 'Zagreb' + - 'Zurich' + - 'Athens' + - 'Bucharest' + - 'Cairo' + - 'Harare' + - 'Helsinki' + - 'Jerusalem' + - 'Kaliningrad' + - 'Kyiv' + - 'Pretoria' + - 'Riga' + - 'Sofia' + - 'Tallinn' + - 'Vilnius' + - 'Baghdad' + - 'Istanbul' + - 'Kuwait' + - 'Minsk' + - 'Moscow' + - 'Nairobi' + - 'Riyadh' + - 'St. Petersburg' + - 'Tehran' + - 'Abu Dhabi' + - 'Baku' + - 'Muscat' + - 'Samara' + - 'Tbilisi' + - 'Volgograd' + - 'Yerevan' + - 'Kabul' + - 'Ekaterinburg' + - 'Islamabad' + - 'Karachi' + - 'Tashkent' + - 'Chennai' + - 'Kolkata' + - 'Mumbai' + - 'New Delhi' + - 'Sri Jayawardenepura' + - 'Kathmandu' + - 'Almaty' + - 'Astana' + - 'Dhaka' + - 'Urumqi' + - 'Rangoon' + - 'Bangkok' + - 'Hanoi' + - 'Jakarta' + - 'Krasnoyarsk' + - 'Novosibirsk' + - 'Beijing' + - 'Chongqing' + - 'Hong Kong' + - 'Irkutsk' + - 'Kuala Lumpur' + - 'Perth' + - 'Singapore' + - 'Taipei' + - 'Ulaanbaatar' + - 'Osaka' + - 'Sapporo' + - 'Seoul' + - 'Tokyo' + - 'Yakutsk' + - 'Adelaide' + - 'Darwin' + - 'Brisbane' + - 'Canberra' + - 'Guam' + - 'Hobart' + - 'Melbourne' + - 'Port Moresby' + - 'Sydney' + - 'Vladivostok' + - 'Magadan' + - 'New Caledonia' + - 'Solomon Is.' + - 'Srednekolymsk' + - 'Auckland' + - 'Fiji' + - 'Kamchatka' + - 'Marshall Is.' + - 'Wellington' + - 'Chatham Is.' + - "Nuku'alofa" + - 'Samoa' + - 'Tokelau Is.' + locale: + description: + - The language locale for the user + required: false + type: str + choices: + - 'ca' + - 'de' + - 'en' + - 'en_GB' + - 'es' + - 'fr' + - 'gl' + - 'it' + - 'ja' + - 'ko' + - 'nl_NL' + - 'pl' + - 'pt_BR' + - 'ru' + - 'sv_SE' + - 'zh_CN' + - 'zh_TW' + roles: + description: + - List of roles assigned to the user + required: false + type: list + elements: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state + - theforeman.foreman.foreman.taxonomy +''' + +EXAMPLES = ''' +- name: Create a user + theforeman.foreman.user: + name: test + firstname: Test + lastname: Userson + mail: test.userson@example.com + description: Dr. Test Userson + admin: no + user_password: s3cret + default_location: Test Location + default_organization: Test Organization + auth_source: Internal + timezone: Stockholm + locale: sv_SE + roles: + - Manager + locations: + - Test Location + organizations: + - Test Organization + state: present + +- name: Update a user + theforeman.foreman.user: + name: test + firstname: Tester + state: present + +- name: Change password + theforeman.foreman.user: + name: test + user_password: newp@ss + +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + users: + description: List of users. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ( + ForemanTaxonomicEntityAnsibleModule, +) + + +# List of allowed timezones +timezone_list = [ + 'International Date Line West', + 'American Samoa', + 'Midway Island', + 'Hawaii', + 'Alaska', + 'Pacific Time (US & Canada)', + 'Tijuana', + 'Arizona', + 'Chihuahua', + 'Mazatlan', + 'Mountain Time (US & Canada)', + 'Central America', + 'Central Time (US & Canada)', + 'Guadalajara', + 'Mexico City', + 'Monterrey', + 'Saskatchewan', + 'Bogota', + 'Eastern Time (US & Canada)', + 'Indiana (East)', + 'Lima', + 'Quito', + 'Atlantic Time (Canada)', + 'Caracas', + 'Georgetown', + 'La Paz', + 'Puerto Rico', + 'Santiago', + 'Newfoundland', + 'Brasilia', + 'Buenos Aires', + 'Greenland', + 'Montevideo', + 'Mid-Atlantic', + 'Azores', + 'Cape Verde Is.', + 'Dublin', + 'Edinburgh', + 'Lisbon', + 'London', + 'Monrovia', + 'UTC', + 'Amsterdam', + 'Belgrade', + 'Berlin', + 'Bern', + 'Bratislava', + 'Brussels', + 'Budapest', + 'Casablanca', + 'Copenhagen', + 'Ljubljana', + 'Madrid', + 'Paris', + 'Prague', + 'Rome', + 'Sarajevo', + 'Skopje', + 'Stockholm', + 'Vienna', + 'Warsaw', + 'West Central Africa', + 'Zagreb', + 'Zurich', + 'Athens', + 'Bucharest', + 'Cairo', + 'Harare', + 'Helsinki', + 'Jerusalem', + 'Kaliningrad', + 'Kyiv', + 'Pretoria', + 'Riga', + 'Sofia', + 'Tallinn', + 'Vilnius', + 'Baghdad', + 'Istanbul', + 'Kuwait', + 'Minsk', + 'Moscow', + 'Nairobi', + 'Riyadh', + 'St. Petersburg', + 'Tehran', + 'Abu Dhabi', + 'Baku', + 'Muscat', + 'Samara', + 'Tbilisi', + 'Volgograd', + 'Yerevan', + 'Kabul', + 'Ekaterinburg', + 'Islamabad', + 'Karachi', + 'Tashkent', + 'Chennai', + 'Kolkata', + 'Mumbai', + 'New Delhi', + 'Sri Jayawardenepura', + 'Kathmandu', + 'Almaty', + 'Astana', + 'Dhaka', + 'Urumqi', + 'Rangoon', + 'Bangkok', + 'Hanoi', + 'Jakarta', + 'Krasnoyarsk', + 'Novosibirsk', + 'Beijing', + 'Chongqing', + 'Hong Kong', + 'Irkutsk', + 'Kuala Lumpur', + 'Perth', + 'Singapore', + 'Taipei', + 'Ulaanbaatar', + 'Osaka', + 'Sapporo', + 'Seoul', + 'Tokyo', + 'Yakutsk', + 'Adelaide', + 'Darwin', + 'Brisbane', + 'Canberra', + 'Guam', + 'Hobart', + 'Melbourne', + 'Port Moresby', + 'Sydney', + 'Vladivostok', + 'Magadan', + 'New Caledonia', + 'Solomon Is.', + 'Srednekolymsk', + 'Auckland', + 'Fiji', + 'Kamchatka', + 'Marshall Is.', + 'Wellington', + 'Chatham Is.', + "Nuku'alofa", + 'Samoa', + 'Tokelau Is.', +] + +# List of allowed locales +locale_list = [ + 'ca', + 'de', + 'en', + 'en_GB', + 'es', + 'fr', + 'gl', + 'it', + 'ja', + 'ko', + 'nl_NL', + 'pl', + 'pt_BR', + 'ru', + 'sv_SE', + 'zh_CN', + 'zh_TW', +] + + +class ForemanUserModule(ForemanTaxonomicEntityAnsibleModule): + pass + + +def main(): + module = ForemanUserModule( + foreman_spec=dict( + login=dict(required=True, aliases=['name']), + firstname=dict(required=False), + lastname=dict(required=False), + mail=dict(required=False), + description=dict(required=False), + admin=dict(required=False, type='bool', default=False), + user_password=dict(required=False, no_log=True, flat_name='password'), + default_location=dict(required=False, type='entity', resource_type='locations'), + default_organization=dict(required=False, type='entity', resource_type='organizations'), + auth_source=dict(required=False, type='entity'), + timezone=dict(required=False, choices=timezone_list), + locale=dict(required=False, choices=locale_list), + roles=dict(required=False, type='entity_list'), + ), + entity_key='login', + ) + + with module.api_connection(): + entity = module.lookup_entity('entity') + + if not module.desired_absent: + if 'mail' not in module.foreman_params: + if not entity: + module.fail_json(msg="The 'mail' parameter is required when creating a new user.") + else: + module.foreman_params['mail'] = entity['mail'] + + module.run() + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/theforeman/foreman/plugins/modules/usergroup.py b/ansible_collections/theforeman/foreman/plugins/modules/usergroup.py new file mode 100644 index 00000000..49013ae8 --- /dev/null +++ b/ansible_collections/theforeman/foreman/plugins/modules/usergroup.py @@ -0,0 +1,124 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# (c) 2019 Baptiste AGASSE (baptiste.agasse@gmail.com) +# +# 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 <http://www.gnu.org/licenses/>. + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: usergroup +version_added: 1.0.0 +short_description: Manage User Groups +description: + - Create, update, and delete user groups +author: + - "Baptiste Agasse (@bagasse)" +options: + name: + description: + - Name of the group + required: true + type: str + updated_name: + description: + - New user group name. When this parameter is set, the module will not be idempotent. + required: false + type: str + admin: + description: + - Whether or not the users in this group are administrators + required: false + default: false + type: bool + roles: + description: + - List of roles assigned to the group + required: false + type: list + elements: str + users: + description: + - List of users assigned to the group + required: false + type: list + elements: str + usergroups: + description: + - List of other groups assigned to the group + required: false + type: list + elements: str +extends_documentation_fragment: + - theforeman.foreman.foreman + - theforeman.foreman.foreman.entity_state +''' + +EXAMPLES = ''' +- name: Create a user group + theforeman.foreman.usergroup: + name: test + admin: no + roles: + - Manager + users: + - myuser1 + - myuser2 + usergroups: + - mynestedgroup + state: present +''' + +RETURN = ''' +entity: + description: Final state of the affected entities grouped by their type. + returned: success + type: dict + contains: + usergroups: + description: List of usergroups. + type: list + elements: dict +''' + +from ansible_collections.theforeman.foreman.plugins.module_utils.foreman_helper import ForemanEntityAnsibleModule + + +class ForemanUsergroupModule(ForemanEntityAnsibleModule): + pass + + +def main(): + module = ForemanUsergroupModule( + argument_spec=dict( + updated_name=dict(), + ), + foreman_spec=dict( + name=dict(required=True), + admin=dict(required=False, type='bool', default=False), + users=dict(required=False, type='entity_list'), + usergroups=dict(required=False, type='entity_list'), + roles=dict(required=False, type='entity_list'), + ), + ) + + with module.api_connection(): + module.run() + + +if __name__ == '__main__': + main() |