summaryrefslogtreecommitdiffstats
path: root/ansible_collections/theforeman/foreman/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/theforeman/foreman/plugins
parentInitial commit. (diff)
downloadansible-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')
-rw-r--r--ansible_collections/theforeman/foreman/plugins/callback/foreman.py395
-rw-r--r--ansible_collections/theforeman/foreman/plugins/doc_fragments/foreman.py381
-rw-r--r--ansible_collections/theforeman/foreman/plugins/filter/cp_label.yml21
-rw-r--r--ansible_collections/theforeman/foreman/plugins/filter/foreman.py24
-rw-r--r--ansible_collections/theforeman/foreman/plugins/inventory/foreman.py671
-rw-r--r--ansible_collections/theforeman/foreman/plugins/module_utils/_apypie.py907
-rw-r--r--ansible_collections/theforeman/foreman/plugins/module_utils/_version.py335
-rw-r--r--ansible_collections/theforeman/foreman/plugins/module_utils/foreman_helper.py1864
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/activation_key.py398
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/architecture.py122
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/auth_source_ldap.py232
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/bookmark.py157
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/compute_attribute.py153
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/compute_profile.py228
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/compute_resource.py485
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/config_group.py97
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_credential.py100
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_export_info.py149
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_export_library.py147
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_export_repository.py142
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_export_version.py169
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_upload.py226
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view.py285
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_filter.py329
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_info.py88
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule.py322
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_filter_rule_info.py99
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_version.py265
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/content_view_version_info.py90
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/discovery_rule.py145
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/domain.py112
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/domain_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/external_usergroup.py123
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/global_parameter.py164
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/hardware_model.py102
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/host.py535
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/host_collection.py100
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/host_errata_info.py117
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/host_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/host_power.py137
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/hostgroup.py217
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/hostgroup_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/http_proxy.py118
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/image.py135
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/installation_medium.py151
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/job_invocation.py225
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/job_template.py476
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/lifecycle_environment.py118
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/location.py145
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/operatingsystem.py233
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/organization.py116
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/organization_info.py83
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/os_default_template.py145
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/partition_table.py296
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/product.py144
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/provisioning_template.py344
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/puppet_environment.py91
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/puppetclasses_import.py127
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/realm.py102
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/redhat_manifest.py344
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/repository.py399
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/repository_info.py99
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/repository_set.py338
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/repository_set_info.py101
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/repository_sync.py87
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/resource_info.py173
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/role.py146
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/scap_content.py126
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/scap_tailoring_file.py125
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/scc_account.py180
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/scc_product.py108
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/setting.py121
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/setting_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/smart_class_parameter.py273
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/smart_proxy.py170
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/snapshot.py178
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/snapshot_info.py95
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/status_info.py76
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/subnet.py292
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/subnet_info.py81
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/subscription_info.py82
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/subscription_manifest.py134
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/sync_plan.py180
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/templates_import.py193
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/user.py548
-rw-r--r--ansible_collections/theforeman/foreman/plugins/modules/usergroup.py124
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()