summaryrefslogtreecommitdiffstats
path: root/ansible_collections/theforeman/foreman
diff options
context:
space:
mode:
Diffstat (limited to 'ansible_collections/theforeman/foreman')
-rw-r--r--ansible_collections/theforeman/foreman/CHANGELOG.rst510
-rw-r--r--ansible_collections/theforeman/foreman/FILES.json1440
-rw-r--r--ansible_collections/theforeman/foreman/LICENSE674
-rw-r--r--ansible_collections/theforeman/foreman/MANIFEST.json121
-rw-r--r--ansible_collections/theforeman/foreman/PSF-license.txt48
-rw-r--r--ansible_collections/theforeman/foreman/README.md146
-rw-r--r--ansible_collections/theforeman/foreman/bindep.txt3
-rw-r--r--ansible_collections/theforeman/foreman/changelogs/changelog.yaml725
-rw-r--r--ansible_collections/theforeman/foreman/meta/execution-environment.yml12
-rw-r--r--ansible_collections/theforeman/foreman/meta/runtime.yml188
-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
-rw-r--r--ansible_collections/theforeman/foreman/requirements.txt3
-rw-r--r--ansible_collections/theforeman/foreman/roles/activation_keys/README.md79
-rw-r--r--ansible_collections/theforeman/foreman/roles/activation_keys/tasks/main.yml26
-rw-r--r--ansible_collections/theforeman/foreman/roles/auth_sources_ldap/README.md77
-rw-r--r--ansible_collections/theforeman/foreman/roles/auth_sources_ldap/tasks/main.yml27
-rw-r--r--ansible_collections/theforeman/foreman/roles/compute_profiles/README.md54
-rw-r--r--ansible_collections/theforeman/foreman/roles/compute_profiles/tasks/main.yml11
-rw-r--r--ansible_collections/theforeman/foreman/roles/compute_resources/README.md53
-rw-r--r--ansible_collections/theforeman/foreman/roles/compute_resources/tasks/main.yml32
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_credentials/README.md107
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_credentials/tasks/main.yml12
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_rhel/README.md126
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_rhel/defaults/main.yml6
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_rhel/tasks/main.yml95
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_publish/README.md30
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_publish/tasks/main.yml12
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/README.md44
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/delete_cv_versions.yml14
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/find_and_delete_unused_cv_versions.yml20
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/main.yml37
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_views/README.md70
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_views/tasks/_create_content_view.yml44
-rw-r--r--ansible_collections/theforeman/foreman/roles/content_views/tasks/main.yml6
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/README.md44
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/defaults/main.yml32
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/activation_keys.yml111
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/content_views.yml52
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/host_groups.yml44
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/main.yml15
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/products_and_repos.yml88
-rw-r--r--ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/sync.yml44
-rw-r--r--ansible_collections/theforeman/foreman/roles/domains/README.md41
-rw-r--r--ansible_collections/theforeman/foreman/roles/domains/tasks/main.yml13
-rw-r--r--ansible_collections/theforeman/foreman/roles/hostgroups/README.md75
-rw-r--r--ansible_collections/theforeman/foreman/roles/hostgroups/tasks/main.yml42
-rw-r--r--ansible_collections/theforeman/foreman/roles/lifecycle_environments/README.md75
-rw-r--r--ansible_collections/theforeman/foreman/roles/lifecycle_environments/tasks/main.yml15
-rw-r--r--ansible_collections/theforeman/foreman/roles/manifest/README.md50
-rw-r--r--ansible_collections/theforeman/foreman/roles/manifest/defaults/main.yml2
-rw-r--r--ansible_collections/theforeman/foreman/roles/manifest/tasks/main.yml18
-rw-r--r--ansible_collections/theforeman/foreman/roles/operatingsystems/README.md43
-rw-r--r--ansible_collections/theforeman/foreman/roles/operatingsystems/tasks/main.yml35
-rw-r--r--ansible_collections/theforeman/foreman/roles/organizations/README.md44
-rw-r--r--ansible_collections/theforeman/foreman/roles/organizations/tasks/main.yml14
-rw-r--r--ansible_collections/theforeman/foreman/roles/provisioning_templates/README.md33
-rw-r--r--ansible_collections/theforeman/foreman/roles/provisioning_templates/tasks/main.yml18
-rw-r--r--ansible_collections/theforeman/foreman/roles/repositories/README.md117
-rw-r--r--ansible_collections/theforeman/foreman/roles/repositories/tasks/main.yml92
-rw-r--r--ansible_collections/theforeman/foreman/roles/settings/README.md31
-rw-r--r--ansible_collections/theforeman/foreman/roles/settings/tasks/main.yml10
-rw-r--r--ansible_collections/theforeman/foreman/roles/subnets/README.md53
-rw-r--r--ansible_collections/theforeman/foreman/roles/subnets/tasks/main.yml38
-rw-r--r--ansible_collections/theforeman/foreman/roles/sync_plans/README.md84
-rw-r--r--ansible_collections/theforeman/foreman/roles/sync_plans/tasks/main.yml16
151 files changed, 25101 insertions, 0 deletions
diff --git a/ansible_collections/theforeman/foreman/CHANGELOG.rst b/ansible_collections/theforeman/foreman/CHANGELOG.rst
new file mode 100644
index 00000000..e45f8de5
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/CHANGELOG.rst
@@ -0,0 +1,510 @@
+================================
+theforeman.foreman Release Notes
+================================
+
+.. contents:: Topics
+
+This changelog describes changes after version 0.8.1.
+
+v3.9.0
+======
+
+Bugfixes
+--------
+
+- content_export_* - increase task timeout to 12h as export tasks can be time intensive (https://bugzilla.redhat.com/show_bug.cgi?id=2162678)
+
+New Modules
+-----------
+
+- theforeman.foreman.content_view_filter_info - Fetch information about a Content View Filter
+- theforeman.foreman.content_view_filter_rule - Manage content view filter rules
+- theforeman.foreman.content_view_filter_rule_info - Fetch information about a Content View Filter Rule
+- theforeman.foreman.hostgroup_info - Get information about hostgroup(s)
+
+v3.8.0
+======
+
+Minor Changes
+-------------
+
+- job_template - add ``default`` option to the ``template_inputs`` parameter
+- location, organization - add ``ignore_types`` parameter to adjust automatic association of resources
+- redhat_manifest - Search by UUID on the server side if UUID is known. This is faster and allows fetching of manifest in big accounts (>1000 allocations).
+- redhat_manifest - return the UUID of the manifest so it can be reused later
+- redhat_manifest - set default ``quantity`` to 1 (https://github.com/theforeman/foreman-ansible-modules/pull/1499)
+
+Bugfixes
+--------
+
+- activation_key - properly fetch *all* repositories when managing content overrides (https://bugzilla.redhat.com/show_bug.cgi?id=2134605)
+- redhat_manifest - properly report http errors (https://github.com/theforeman/foreman-ansible-modules/issues/1497)
+- repository_sync - report an error instead of syncing the whole product when the repository could not be found
+
+New Modules
+-----------
+
+- theforeman.foreman.snapshot_info - Fetch information about Foreman Snapshots
+
+v3.7.0
+======
+
+Minor Changes
+-------------
+
+- repository - add support for ``include_tags`` and ``exclude_tags`` parameters for Katello 4.4+
+- subscription_manifest - increase the import timeout to 10 minutes (https://github.com/theforeman/foreman-ansible-modules/issues/1474)
+- sync_plans role - document the ``enabled`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1477)
+- sync_plans role - expose the ``state`` parameter of the underlying module, thus allowing to delete plans (https://github.com/theforeman/foreman-ansible-modules/issues/1477)
+
+Bugfixes
+--------
+
+- Properly use FQCN notation when redirecting the old ``foreman_*`` and ``katello_*`` module names. (https://github.com/theforeman/foreman-ansible-modules/issues/1484)
+- convert2rhel role - Content views for activation keys (https://bugzilla.redhat.com/2118790)
+
+v3.6.0
+======
+
+New Modules
+-----------
+
+- theforeman.foreman.content_export_repository - Manage repository content exports
+- theforeman.foreman.content_export_version - Manage content view version content exports
+
+v3.5.0
+======
+
+Minor Changes
+-------------
+
+- add execution environment metadata
+- installation_medium, operatingsystem, partition_table - add ``Fcos``, ``Rhcos``, ``VRP`` OS families
+- job_template - add ``hidden_value`` to ``template_inputs`` parameters
+- job_template - allow ``value_type`` to be ``resource``
+- operatingsystems role - make ``provisioning_template`` parameter optional
+- repositories role - add ``ansible_collection_requirements``
+- repositories role - add ``arch`` and ``os_versions`` parameters
+- repositories role - support ``mirroring_policy``
+- repository, smart_proxy - document deprecation/removal status of ``download_policy=background``
+- setting - the ``foreman_setting`` return entry is deprecated and kept for backwards compatibility, please use ``entity`` as with any other module
+- smart_proxy - add ``inherit`` to possible values of ``download_policy`` (https://github.com/theforeman/foreman-ansible-modules/issues/1438)
+- smart_proxy - add ``streamed`` download policy
+- snapshot - add include_ram option when creating VMWare snapshot
+
+New Modules
+-----------
+
+- theforeman.foreman.content_export_info - List pulp3 content exports
+- theforeman.foreman.content_export_library - Manage content exports
+- theforeman.foreman.discovery_rule - Manage Host Discovery Rules
+
+v3.4.0
+======
+
+Minor Changes
+-------------
+
+- add support for module defaults groups for Ansible core 2.12 (https://github.com/theforeman/foreman-ansible-modules/issues/1015)
+- all modules - report smaller diffs by dropping ``null`` values. This should result in not showing fields that were unset to begin with, and mark fields that were explicitly removed as "deleted" instead of "replaced by ``null``"
+- compute_resource - update libvirt examples (https://bugzilla.redhat.com/show_bug.cgi?id=1990119)
+- content_view - add support to set label during creation.
+- repository - add ``rhel-9`` to os version filter choices
+- repository - add support for ``mirroring_policy`` for Katello 4.4+ (https://github.com/theforeman/foreman-ansible-modules/issues/1388)
+
+Bugfixes
+--------
+
+- content_upload - properly detect SRPMs and ensure idempotency during uploads (https://github.com/theforeman/foreman-ansible-modules/issues/1274)
+- inventory plugin - fix caching for Report API (https://github.com/theforeman/foreman-ansible-modules/issues/1246)
+- operatingsystem - find operatingsystems by title or full (name,major,minor) tuple (https://github.com/theforeman/foreman-ansible-modules/issues/1401)
+- os_default_template, provisioning_template - don't document invalid template kind ``ptable`` (https://bugzilla.redhat.com/show_bug.cgi?id=1970132)
+
+v3.3.0
+======
+
+Minor Changes
+-------------
+
+- content_upload - add support for OSTree content uploads (https://github.com/theforeman/foreman-ansible-modules/issues/628, https://projects.theforeman.org/issues/33299)
+- os_default_template, provisioning_template - add ``host_init_config`` to list of possible template types
+
+v3.2.0
+======
+
+Minor Changes
+-------------
+
+- new ``auth_sources_ldap`` role to manage LDAP authentication sources
+
+Bugfixes
+--------
+
+- content_upload - clarify that ``src`` refers to a remote file (https://bugzilla.redhat.com/show_bug.cgi?id=2055416)
+
+v3.1.0
+======
+
+Minor Changes
+-------------
+
+- Warn if the user tries to use a plain HTTP server URL and fail if the URL is neither HTTPS nor HTTP.
+- new ``compute_profiles`` role to manage compute profiles
+- new ``compute_resources`` role to manage compute resources
+- new ``content_view_publish`` role to publish a list of content views (https://github.com/theforeman/foreman-ansible-modules/issues/1209)
+- new ``domains`` role to manage domains
+- new ``operatingsystems`` role to manage operating systems
+- new ``provisioning_templates`` role to manage provisioning templates
+- new ``settings`` role to manage settings
+- new ``subnets`` role to manage subnets
+- repository - new ``download_concurrency`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1273)
+
+Bugfixes
+--------
+
+- callback plugin - include timezone information in the callback reported data (https://github.com/theforeman/foreman-ansible-modules/issues/1171)
+- hostgroup, location - don't fail when trying to delete a Hostgroup or Location where the parent is already absent
+- inventory plugin - fetch *all* facts, not only the first 250, when using the old Hosts API
+
+v3.0.0
+======
+
+Minor Changes
+-------------
+
+- Add a role `convert2rhel` to perform setup for converting systems to RHEL
+- inventory plugin - enable certificate validation by default
+- repository - add ``arch`` parameter to limit architectures of the repository (https://github.com/theforeman/foreman-ansible-modules/issues/1265)
+
+Breaking Changes / Porting Guide
+--------------------------------
+
+- Set use_reports_api default value to true for the inventory plugin
+- Support for Ansible 2.8 is removed
+
+Bugfixes
+--------
+
+- host, hostgroup - fix updating puppetclasses while also updating description (or other string-like attributes) (https://github.com/theforeman/foreman-ansible-modules/issues/1231)
+
+v2.2.0
+======
+
+Minor Changes
+-------------
+
+- repository - add support for filtering repositories by OS version based on API feature apidoc/v2/repositories/create.html
+
+Bugfixes
+--------
+
+- host, hostgroup - don't accidentally duplicate ``kt_activation_keys`` param (https://github.com/theforeman/foreman-ansible-modules/issues/1268)
+
+v2.1.2
+======
+
+Bugfixes
+--------
+
+- activation_key - submit organization_id when querying subs, required for Katello 4.1
+- content_view_version_cleanup - sort content view versions before deleting (https://github.com/RedHatSatellite/satellite-ansible-collection/issues/30, https://bugzilla.redhat.com/show_bug.cgi?id=1980274)
+- content_view_version_cleanup role - properly clean up when users set keep=0 (https://bugzilla.redhat.com/show_bug.cgi?id=1974314)
+- host, compute_profile - when resolving cluster and other values in vm_attrs, compare them as strings (https://github.com/theforeman/foreman-ansible-modules/issues/1245)
+- subscription_info - mark ``organization`` parameter as required, to match Katello
+
+v2.1.1
+======
+
+Bugfixes
+--------
+
+- external_usergroup - always lookup the ID of the usergroup, instead of passing the name to the API (https://bugzilla.redhat.com/show_bug.cgi?id=1967649)
+- host, hostgroup - don't override already set parameters when passing an activation key only (and vice versa) (https://bugzilla.redhat.com/show_bug.cgi?id=1967904)
+
+v2.1.0
+======
+
+Minor Changes
+-------------
+
+- Add a domain_info module
+- Add a hostgroups role (https://github.com/theforeman/foreman-ansible-modules/issues/1116)
+- Add a role `content_rhel` to perform basic setup for registering and syncing RHEL content hosts
+- Add content credentials role
+- callback plugin - collect facts during the run, merge them correctly and upload them once at the end
+- compute_resource - add ``cloud`` param for the AzureRm provider, to select which Azure cloud to use
+- compute_resource - add ``sub_id`` parameter for handling the Azure Subscription ID instead of the ``user`` parameter
+- host - Add ``Redfish`` to list of possible BMC providers of an interface
+- host, compute_profile - look up the correct id for storage pods and domains given as part of ``volumes_attributes`` (https://bugzilla.redhat.com/show_bug.cgi?id=1885234)
+- hostgroup - add a ``ansible_roles`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1123)
+- new ``content_views`` role to manage content views (https://github.com/theforeman/foreman-ansible-modules/issues/1111)
+- new ``organizations`` role to manage organizations (https://github.com/theforeman/foreman-ansible-modules/issues/1109)
+- subnet - add ``bmc_proxy`` parameter to configure BMC proxies for subnets
+
+Bugfixes
+--------
+
+- host - pass the right image id to the compute resource when creating a host (https://github.com/theforeman/foreman-ansible-modules/issues/1160, https://bugzilla.redhat.com/show_bug.cgi?id=1911670)
+
+New Modules
+-----------
+
+- theforeman.foreman.content_view_info - Fetch information about Content Views
+- theforeman.foreman.content_view_version_info - Fetch information about Content Views
+- theforeman.foreman.domain_info - Fetch information about Domains
+- theforeman.foreman.host_errata_info - Fetch information about Host Errata
+- theforeman.foreman.repository_set_info - Fetch information about Red Hat Repositories
+- theforeman.foreman.setting_info - Fetch information about Settings
+- theforeman.foreman.subnet_info - Fetch information about Subnets
+- theforeman.foreman.subscription_info - Fetch information about Subscriptions
+
+v2.0.1
+======
+
+Bugfixes
+--------
+
+- host - don't filter ``false`` values for ``interfaces_attributes`` (https://github.com/theforeman/foreman-ansible-modules/issues/1148)
+- host_info, repository_info - correctly fetch all entities when neither ``name`` nor ``search`` is set
+- host_info, repository_info - enforce mutual exclusivity of ``name`` and ``search``
+
+v2.0.0
+======
+
+Minor Changes
+-------------
+
+- Add a role `activation_keys` to manage activation keys
+- Add a role `lifecycle_environments` to manage lifecycle environments
+- Add a role `repositories` to manage products, repositories, and repository_sets
+- Add a role `sync_plans` to manage sync plans
+- activation_key - add support for selecting subscriptions by ``upstream_pool_id``
+- compute_resource - add ``set_console_password``, ``keyboard_layout`` and ``public_key`` parameters (https://github.com/theforeman/foreman-ansible-modules/issues/1052)
+- host - clarify that ``owner`` refers to a users login, not their full name (https://github.com/theforeman/foreman-ansible-modules/issues/1045)
+- host - look up the correct network id for a network given as part of ``interfaces_attributes`` (https://github.com/theforeman/foreman-ansible-modules/issues/1104)
+- host, hostgroup - add ``activation_keys`` parameter to ease configuring activation keys for deploments
+
+Breaking Changes / Porting Guide
+--------------------------------
+
+- All role variables are now prefixed with ``foreman_`` to avoid clashes with similarly named variables from roles outside this collection.
+
+Bugfixes
+--------
+
+- content_view_version - make the ``version`` parameter not fail when the version was entered without a minor part (https://github.com/theforeman/foreman-ansible-modules/issues/1087)
+- host - allow moving hosts between Organizations and Locations (https://bugzilla.redhat.com/show_bug.cgi?id=1901716)
+- host - fix subnet/domain assignment when multiple interfaces are defined (https://github.com/theforeman/foreman-ansible-modules/issues/1095)
+- host, hostgroup - select kickstart_repository based on lifecycle_environment and content_view if those are set (https://github.com/theforeman/foreman-ansible-modules/issues/1090, https://bugzilla.redhat.com/1915872)
+- resource_info - correctly show the exact resource when passing ``id`` in ``params``
+
+New Modules
+-----------
+
+- theforeman.foreman.host_info - Fetch information about Hosts
+- theforeman.foreman.puppetclasses_import - Import Puppet Classes from a Proxy
+- theforeman.foreman.repository_info - Fetch information about Repositories
+
+v1.5.0
+======
+
+Minor Changes
+-------------
+
+- content_upload - use ``to_native`` to decode RPM headers if needed (RPM 4.15+ returns strings)
+- content_view_version - provide examples how to obtain detailed information about content view versions (https://bugzilla.redhat.com/show_bug.cgi?id=1868145)
+- content_view_version_cleanup - new role for cleaning up unused content view versions (https://github.com/theforeman/foreman-ansible-modules/issues/497)
+- host - allow management of interfaces (https://github.com/theforeman/foreman-ansible-modules/issues/757)
+- inventory plugin - add support for the Report API present in Foreman 1.24 and later
+- inventory plugin - allow to compose the ``inventory_hostname`` (https://github.com/theforeman/foreman-ansible-modules/issues/1070)
+- manifest - new role for easier handling of subscription manifest workflows
+- subnet - add new ``externalipam_group`` parameter
+- update vendored ``apypie`` to 0.3.2
+
+Bugfixes
+--------
+
+- content_upload - Fix upload of files bigger than 2MB in Pulp3-based setups (https://github.com/theforeman/foreman-ansible-modules/issues/1043)
+- job_invocation - properly submit ``ssh``, ``recurrence``, ``scheduling`` and ``concurrency_control`` to the server
+- repository - don't emit a false warning about ``organization_id`` not being supported by the server (https://github.com/theforeman/foreman-ansible-modules/issues/1055)
+- repository_set, repository - clarify documentation which module should be used for Red Hat Repositories (https://github.com/theforeman/foreman-ansible-modules/issues/1059)
+
+v1.4.0
+======
+
+Minor Changes
+-------------
+
+- global_parameter - allow to set hidden flag (https://github.com/theforeman/foreman-ansible-modules/issues/1024)
+- job_template - stricter validation of ``template_inputs`` sub-options
+- redhat_manifest - allow configuring content access mode (https://github.com/theforeman/foreman-ansible-modules/issues/820)
+- subnet - verify the server has the ``remote_execution`` plugin when specifying ``remote_execution_proxies``
+- the ``apypie`` library is vendored inside the collection, so users only have to install ``requests`` manually now.
+
+Bugfixes
+--------
+
+- Don't try to update an entity, if only parameters that aren't supported by the server are detected as changed. (https://github.com/theforeman/foreman-ansible-modules/issues/975)
+- allow to pass an empty string when refering to entities, thus unsetting the value (https://github.com/theforeman/foreman-ansible-modules/issues/969)
+- compute_profile - don't fail when trying to update compute attributes of a profile (https://github.com/theforeman/foreman-ansible-modules/issues/997)
+- host, hostgroup - support ``None`` as the ``pxe_loader`` (https://github.com/theforeman/foreman-ansible-modules/issues/971)
+- job_template - don't fail when trying to update template_inputs
+- os_default_template - document possible template kind choices (https://bugzilla.redhat.com/show_bug.cgi?id=1889952)
+- smart_class_parameters - don't fail when trying to update override_values
+
+New Modules
+-----------
+
+- theforeman.foreman.job_invocation - Invoke Remote Execution Jobs
+- theforeman.foreman.smart_proxy - Manage Smart Proxies
+
+v1.3.0
+======
+
+Minor Changes
+-------------
+
+- external_usergroup - rename the ``auth_source_ldap`` parameter to ``auth_source`` (``auth_source_ldap`` is still supported via an alias)
+- server URL and credentials can now also be specified using environment variables (https://github.com/theforeman/foreman-ansible-modules/issues/837)
+- subnet - add support for external IPAM (https://github.com/theforeman/foreman-ansible-modules/issues/966)
+
+Bugfixes
+--------
+
+- content_view - remove CVs from lifecycle environments before deleting them (https://bugzilla.redhat.com/show_bug.cgi?id=1875314)
+- external_usergroup - support non-LDAP external groups (https://github.com/theforeman/foreman-ansible-modules/issues/956)
+- host - properly scope image lookups by the compute resource (https://bugzilla.redhat.com/show_bug.cgi?id=1878693)
+- inventory plugin - include empty parent groups in the inventory (https://github.com/theforeman/foreman-ansible-modules/issues/919)
+
+New Modules
+-----------
+
+- theforeman.foreman.status_info - Get status info
+
+v1.2.0
+======
+
+Minor Changes
+-------------
+
+- compute_resource - added ``caching_enabled`` option for VMware compute resources
+- domain, host, hostgroup, operatingsystem, subnet - manage parameters in a single API call (https://bugzilla.redhat.com/show_bug.cgi?id=1855008)
+- host - add ``compute_attributes`` parameter to module (https://bugzilla.redhat.com/show_bug.cgi?id=1871815)
+- provisioning_template - update list of possible template kinds (https://bugzilla.redhat.com/show_bug.cgi?id=1871978)
+- repository - update supported parameters (https://github.com/theforeman/foreman-ansible-modules/issues/935)
+
+Bugfixes
+--------
+
+- image - fix quoting of search values (https://github.com/theforeman/foreman-ansible-modules/issues/927)
+
+v1.1.0
+======
+
+Minor Changes
+-------------
+
+- activation_key - add ``description`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/915)
+- callback plugin - add reporter to report logs sent to Foreman (https://github.com/theforeman/foreman-ansible-modules/issues/836)
+- document return values of modules (https://github.com/theforeman/foreman-ansible-modules/pull/901)
+- inventory plugin - allow to control batch size when pulling hosts from Foreman (https://github.com/theforeman/foreman-ansible-modules/pull/865)
+- subnet - Require mask/cidr only on ipv4 (https://github.com/theforeman/foreman-ansible-modules/issues/878)
+
+Bugfixes
+--------
+
+- inventory plugin - fix want_params handling (https://github.com/theforeman/foreman-ansible-modules/issues/847)
+
+New Modules
+-----------
+
+- theforeman.foreman.http_proxy - Manage HTTP Proxies
+
+v1.0.1
+======
+
+Release Summary
+---------------
+
+Documentation fixes to reflect the correct module names.
+
+
+v1.0.0
+======
+
+Release Summary
+---------------
+
+This is the first stable release of the ``theforeman.foreman`` collection.
+
+
+Breaking Changes / Porting Guide
+--------------------------------
+
+- All modules were renamed to drop the ``foreman_`` and ``katello_`` prefixes.
+ Additionally to the prefix removal, the following modules were further ranamed:
+
+ * katello_upload to content_upload
+ * katello_sync to repository_sync
+ * katello_manifest to subscription_manifest
+ * foreman_search_facts to resource_info
+ * foreman_ptable to partition_table
+ * foreman_model to hardware_model
+ * foreman_environment to puppet_environment
+
+New Modules
+-----------
+
+- theforeman.foreman.activation_key - Manage Activation Keys
+- theforeman.foreman.architecture - Manage Architectures
+- theforeman.foreman.auth_source_ldap - Manage LDAP Authentication Sources
+- theforeman.foreman.bookmark - Manage Bookmarks
+- theforeman.foreman.compute_attribute - Manage Compute Attributes
+- theforeman.foreman.compute_profile - Manage Compute Profiles
+- theforeman.foreman.compute_resource - Manage Compute Resources
+- theforeman.foreman.config_group - Manage (Puppet) Config Groups
+- theforeman.foreman.content_credential - Manage Content Credentials
+- theforeman.foreman.content_upload - Upload content to a repository
+- theforeman.foreman.content_view - Manage Content Views
+- theforeman.foreman.content_view_filter - Manage Content View Filters
+- theforeman.foreman.content_view_version - Manage Content View Versions
+- theforeman.foreman.domain - Manage Domains
+- theforeman.foreman.external_usergroup - Manage External User Groups
+- theforeman.foreman.global_parameter - Manage Global Parameters
+- theforeman.foreman.hardware_model - Manage Hardware Models
+- theforeman.foreman.host - Manage Hosts
+- theforeman.foreman.host_collection - Manage Host Collections
+- theforeman.foreman.host_power - Manage Power State of Hosts
+- theforeman.foreman.hostgroup - Manage Hostgroups
+- theforeman.foreman.image - Manage Images
+- theforeman.foreman.installation_medium - Manage Installation Media
+- theforeman.foreman.job_template - Manage Job Templates
+- theforeman.foreman.lifecycle_environment - Manage Lifecycle Environments
+- theforeman.foreman.location - Manage Locations
+- theforeman.foreman.operatingsystem - Manage Operating Systems
+- theforeman.foreman.organization - Manage Organizations
+- theforeman.foreman.os_default_template - Manage Default Template Associations To Operating Systems
+- theforeman.foreman.partition_table - Manage Partition Table Templates
+- theforeman.foreman.product - Manage Products
+- theforeman.foreman.provisioning_template - Manage Provisioning Templates
+- theforeman.foreman.puppet_environment - Manage Puppet Environments
+- theforeman.foreman.realm - Manage Realms
+- theforeman.foreman.redhat_manifest - Interact with a Red Hat Satellite Subscription Manifest
+- theforeman.foreman.repository - Manage Repositories
+- theforeman.foreman.repository_set - Enable/disable Repositories in Repository Sets
+- theforeman.foreman.repository_sync - Sync a Repository or Product
+- theforeman.foreman.resource_info - Gather information about resources
+- theforeman.foreman.role - Manage Roles
+- theforeman.foreman.scap_content - Manage SCAP content
+- theforeman.foreman.scap_tailoring_file - Manage SCAP Tailoring Files
+- theforeman.foreman.scc_account - Manage SUSE Customer Center Accounts
+- theforeman.foreman.scc_product - Subscribe SUSE Customer Center Account Products
+- theforeman.foreman.setting - Manage Settings
+- theforeman.foreman.smart_class_parameter - Manage Smart Class Parameters
+- theforeman.foreman.snapshot - Manage Snapshots
+- theforeman.foreman.subnet - Manage Subnets
+- theforeman.foreman.subscription_manifest - Manage Subscription Manifests
+- theforeman.foreman.sync_plan - Manage Sync Plans
+- theforeman.foreman.templates_import - Sync Templates from a repository
+- theforeman.foreman.user - Manage Users
+- theforeman.foreman.usergroup - Manage User Groups
diff --git a/ansible_collections/theforeman/foreman/FILES.json b/ansible_collections/theforeman/foreman/FILES.json
new file mode 100644
index 00000000..ba322821
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/FILES.json
@@ -0,0 +1,1440 @@
+{
+ "files": [
+ {
+ "name": ".",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b1bcc059b9ac5b937c523523da8727ee233326e7262b9e1e0f0e3850e401ab97",
+ "format": 1
+ },
+ {
+ "name": "changelogs",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "changelogs/changelog.yaml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d258dce1feca302f4b04eb1cdb17f7a8dd4a5ccfa1cb2be1684a7b02d80312d0",
+ "format": 1
+ },
+ {
+ "name": "requirements.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "876da4d933e6561c29737e2b9fa47f2df0b309c3ee1dba11d400ba443630cf2f",
+ "format": 1
+ },
+ {
+ "name": "LICENSE",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8ceb4b9ee5adedde47b31e975c1d90c73ad27b6b165a1dcd80c7c545eb65b903",
+ "format": 1
+ },
+ {
+ "name": "roles",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/activation_keys",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/activation_keys/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "bc661135c89fbe8c9bfa0a2e254cb6956d863788a5e7174fb43f4043db7572f9",
+ "format": 1
+ },
+ {
+ "name": "roles/activation_keys/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/activation_keys/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "74197f72ced5b8dc133ef35ebffb8ad2e282eb62cd59444b322c1dc6bd85a4a5",
+ "format": 1
+ },
+ {
+ "name": "roles/auth_sources_ldap",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/auth_sources_ldap/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5015ff70f66cd04b31db56c9240c3ba729007ef66d1ba2d4d066123a66b03cb3",
+ "format": 1
+ },
+ {
+ "name": "roles/auth_sources_ldap/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/auth_sources_ldap/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "44ade8315a6c20bd2b265c66dcf139a3f644e76d12e4d60c4abb9a3cfa8d1a12",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_publish",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_publish/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7bcadf4c448548ba4ebe514937fa809d172da2d53723f2da0c49cd116d19e15f",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_publish/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_publish/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d7657d75c93221f26201f53c731c580617b97939f69b409efccaca8029a38fce",
+ "format": 1
+ },
+ {
+ "name": "roles/compute_resources",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/compute_resources/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "54a520be744c2118cc7f69f5896471405eff341643d720e7763222780bb3d3b5",
+ "format": 1
+ },
+ {
+ "name": "roles/compute_resources/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/compute_resources/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c84706ef0ad1e270dd1c4b469630acbfcdc5aad5535e029df1c60dae5ce4677c",
+ "format": 1
+ },
+ {
+ "name": "roles/organizations",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/organizations/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "06b7099b4edf0a02e22c82dc3ec22e1a135784eae632dbd02af8208f8c1ae39b",
+ "format": 1
+ },
+ {
+ "name": "roles/organizations/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/organizations/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "87538fbb27a8072848899d6a1c5b16464f6e6d4bd36354ae9d0c6764a3eb36e2",
+ "format": 1
+ },
+ {
+ "name": "roles/content_credentials",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_credentials/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "059d2e2f0643f57d6d2b1eb863a3340cccf14444b3ea3140d03de7f944feef8d",
+ "format": 1
+ },
+ {
+ "name": "roles/content_credentials/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_credentials/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "28eb5381e020c9a66f82a73eaba5be86ace43825e7edb716e8b0632d51230c42",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4a74b7408c7c30c51661ad76d17be1548a4f3640a6bbe5441e4b8521c1ba9994",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup/tasks/delete_cv_versions.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "fea3216fff09ba58233b2b84dff7ab34f40203297f130e42347000c98c6ca0aa",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup/tasks/find_and_delete_unused_cv_versions.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4c7fb658dd89a32ecf68ed9a0e0b7f96d45be64540f8e93f7fbbde3fe1b3052b",
+ "format": 1
+ },
+ {
+ "name": "roles/content_view_version_cleanup/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2466702d75631214855614dd8d6658fc87a3df13915d4fdeefb05ac94ad3c610",
+ "format": 1
+ },
+ {
+ "name": "roles/operatingsystems",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/operatingsystems/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c9e99a91e985fc53fd2e96f8f6e60cc6350614c5c704b315e73c804c03d4c38f",
+ "format": 1
+ },
+ {
+ "name": "roles/operatingsystems/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/operatingsystems/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "46919f0dae0e3d2b2c58d7a411189b39cc6600959e833a356d9344ae6abe21c2",
+ "format": 1
+ },
+ {
+ "name": "roles/sync_plans",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/sync_plans/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "35f169e7b198c6b81cc531b172e875a3bc134c25c92998a7c5c9b7ceac49bb88",
+ "format": 1
+ },
+ {
+ "name": "roles/sync_plans/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/sync_plans/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a441f205359bbfa5288eac4e320703b8c2e98f53cf5e89cf2fcc434a989b872d",
+ "format": 1
+ },
+ {
+ "name": "roles/compute_profiles",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/compute_profiles/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "34ef57bf69ec6198878f4983cc6bac8d603895562acf97cf84d1fc2a165d735b",
+ "format": 1
+ },
+ {
+ "name": "roles/compute_profiles/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/compute_profiles/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b9889cf57d104750cd021cd4f40d3194b0e478e70534051ba66c3f5ad282fed8",
+ "format": 1
+ },
+ {
+ "name": "roles/settings",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/settings/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "25d68c67603e37dbebd6b9e8fdec716c37e824fb411bfea020daba8f670a6a4b",
+ "format": 1
+ },
+ {
+ "name": "roles/settings/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/settings/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5c7b62f0302510d3ce43ea04f7be81c55c413c091ce325f12026294206954299",
+ "format": 1
+ },
+ {
+ "name": "roles/content_views",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_views/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "88a78860dd279510249e85244f036735a0611ddf9460339f9681d58310a65eed",
+ "format": 1
+ },
+ {
+ "name": "roles/content_views/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_views/tasks/_create_content_view.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9182ad753478c5daea73b02e6f704915ea76fe07c692509ad8a6392fc5cc175c",
+ "format": 1
+ },
+ {
+ "name": "roles/content_views/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f181cc61783d9bacaf2df98c9bf3da65847c16db6ecdb5da4c2423209666f670",
+ "format": 1
+ },
+ {
+ "name": "roles/hostgroups",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/hostgroups/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b1dbb83dbc077b1defddeada36dbf723280d59a464d84cbd97cc2da8a260800f",
+ "format": 1
+ },
+ {
+ "name": "roles/hostgroups/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/hostgroups/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0e87f3fb8779838ee7bda9cf72a27099f1abbb16160e45ca062cb42dc25a0de4",
+ "format": 1
+ },
+ {
+ "name": "roles/domains",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/domains/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "bf11fbb2e9ed848b8eaeca539c1e1657ee906fc5b8bd0a53c082faeef525d106",
+ "format": 1
+ },
+ {
+ "name": "roles/domains/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/domains/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "adb39457f78b266cd70c34d969e0730e76f9d5f68030defdcb1c03078517bad0",
+ "format": 1
+ },
+ {
+ "name": "roles/subnets",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/subnets/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f2394e89001dd0c9973f16a80d5a00c2cef8f004130794ed15d47ff4d1f7e71d",
+ "format": 1
+ },
+ {
+ "name": "roles/subnets/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/subnets/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ddc3f6b6f12242c8fdc7dba58b9138266bcadb554407198de9f97a4c3f69150c",
+ "format": 1
+ },
+ {
+ "name": "roles/provisioning_templates",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/provisioning_templates/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cd580b020e7ddb2c1254d245a5ac52e89d11b1153e695842a1e7da3626279fcd",
+ "format": 1
+ },
+ {
+ "name": "roles/provisioning_templates/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/provisioning_templates/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1bf4a18fcb051009a48fcc3e13f8a27c04918998fad85772909c74ba861f4935",
+ "format": 1
+ },
+ {
+ "name": "roles/manifest",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/manifest/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c642ab85608467ce130432d957ad59c29154734d9a1046973a3c420ec854b7c4",
+ "format": 1
+ },
+ {
+ "name": "roles/manifest/defaults",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/manifest/defaults/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "83e030118cf5fee3dc34d1edd51f81f2af0065ced604a2cd115f87dbaa9bc001",
+ "format": 1
+ },
+ {
+ "name": "roles/manifest/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/manifest/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1c150072c0e56de63621695dd107dc5cf6d4855dbbc3ebbcf41b142c2726b1a1",
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b24ebaf52431f6505e35aac249c43e09b02f786e21ce233d52f57eb5dc4ee1f6",
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel/defaults",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel/defaults/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b0c0296be090b81fefb1f429c35f6bcc05dec65e4ae1d4df6cd610984d4d3338",
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/content_rhel/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c2ba9b6cca358949beebc66b2afb397a1acfa0a8d74f2c14b1f0f66aab07e4a9",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "093ca613396c822218a5770645787daaf7c9c1e81605b1f9f584ac1896ed7586",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/defaults",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/defaults/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6ee967f34631ee234d08cff740ff3a3adc6d6907dc07962803d6187cf131eca6",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/host_groups.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "570c5d3cf0974f3ed9f1cb826cff6d1f30138854dd280826251dac097e0c7262",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/products_and_repos.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0f489dc100e632918f9f9b8d6a4712f321ad3d6b3b57dbe46ef198c4db8501e7",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/content_views.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "638e8f94219086b108bd663f221813e76a3800125ce419c5c9ba22b77f6efe0a",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/sync.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "00f5bfa99074834cadc1ebe00b865e350822200290b65ecbedf14d24d3b42c8a",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/activation_keys.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "163ea2c9957018ca1a5a3e6f7432e8eaa2ea8fb01cd9ab2937a41541e98f839e",
+ "format": 1
+ },
+ {
+ "name": "roles/convert2rhel/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "eceaf987c0e8d28a8e67df11c5f758b51d7cece7c3ce7a64a919f7361aa61017",
+ "format": 1
+ },
+ {
+ "name": "roles/lifecycle_environments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/lifecycle_environments/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "93c2e33f4ef0ba798644cc183889fed37e8cb8e925bcecf935204ed039053473",
+ "format": 1
+ },
+ {
+ "name": "roles/lifecycle_environments/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/lifecycle_environments/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "fbd804133fa4fc8531ef27b38cad3f7d84acfa9084fd50bfd9e4f39aa6e0c9c9",
+ "format": 1
+ },
+ {
+ "name": "roles/repositories",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/repositories/README.md",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "952111411f4e32789ac002e3968f7d850fbc2b73b7d4290bc420e114bc18124d",
+ "format": 1
+ },
+ {
+ "name": "roles/repositories/tasks",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "roles/repositories/tasks/main.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7c5ca4096ac094fe99373914ca1b75b9366eb55c11f1b76a25c04231e382cff7",
+ "format": 1
+ },
+ {
+ "name": "plugins",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/callback",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/callback/foreman.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b3e0f113d9bd7006a47b509a2a7a6bed6cfabe155ebcf7f0bb7b9b76a8f9ed60",
+ "format": 1
+ },
+ {
+ "name": "plugins/inventory",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/inventory/foreman.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a1d96d9fd3d0223b2d6792e028e915233af1da8c16db754d79e6dc0fe6b16baa",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/lifecycle_environment.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f2c67c928966f90f6893b7c29effe82a97a462191449e74bd84999e05bc40378",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/host_errata_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5f5d4d6d6fce0e7bb4adbbb404eaaca5190709f419a82841aa330aeeb3f0e4d2",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/organization_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "9eae7478eb274de31e6761e986cc1cdd29aa921692fef4a9460aee652bebb75d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/external_usergroup.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ba031c1ca976233e64c54572c54c70350e5f1a7b3ecc93028e0eb191b3594696",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/subnet.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3f77f36fd227950601a79f7902dad37e6c28ddbb8c4272738b0bc83aa42dfc3c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/snapshot.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "abd29b85284655aedd32995de020b0c6e33cb3dc600dfef3c627f93cd5e8c96f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/http_proxy.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "582b6fc66821f769e7040bc38ac4d2a064112d5f2c0e9a9b69740ffd1104c983",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/status_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b40d8c6a67c098fefcc47b37add57e107033205b56cd5a0cc53fcbdce8bd74b5",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/setting.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "91f6f12c8e4aa30e3df5c8632c154884e8ebb68725c6085b476d7f8c576baa42",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/role.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "00414395f4d86fc05078a56801435087334719ce25d8e4f1676ffd0a31cd1ef0",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_export_version.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "df24f4599a356956ec95634256340d520f24475b4023a8d5deffc20051e120e2",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/repository_set.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e8468f2a452fe545438ddbbc3103399c36fbe1f687b3a4bd5edbce64b1b49104",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/setting_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "b4047c5726aeafc9324757637c68123b119e50b49bd2f52dae756b06b3e228d7",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/host_collection.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3394c4154396ac4912c89afed2797e66970d70fed584e4f1849551a44d130f5b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_filter_rule_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "884822275657260c6c26a06dbdbf931b06d33ad4672c2972e14fa993e20cc210",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/domain_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e76723994c1b38aa4c9be56feef37fb9b2fa44982dcd9dea7dc790a501014ca4",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/smart_proxy.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8bf3bd8b91b48b0cf0830fbe6711db02d5a9b98c2de7c0c7179dee4bad743071",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/image.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "31c0530b167b41e9a8bf3706a327fafda17b85bdeef5f3e394b3f4c469ca5d2c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/operatingsystem.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "dd74232a517741fa91adefc4a86bf088cb0bb55f4ce9174c80aaa59bfa8f20d3",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/repository.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f117d868eb7a6a4b22a511f0a2cda214c6b1095b4239fbd354baa0da5eca48d9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/auth_source_ldap.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "bc41c3feb5a69ba330491cb9a4b2755a8436efa5be923bda00069f3a25ce96a8",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/repository_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e5ca340d4152e75be0fbcf5de32af7ec880115ab74077f66aa6a0a7f08e07a0f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2c78b205d636a51cead40bedd09990871c450973d9a3e8fb5fbd3ce458be11a4",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/host.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "52f4cd1502ba2152e005697adfe8131d1a9eb216365214d4708c7bc6c69b4b6d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/sync_plan.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d7a5ca9d2e951ee698f8f84ee1f4f24fec2afc3eabc31b3af97856d0f13797b8",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/host_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3e88c7d2c3b365d55c98f182ac3fe2d79727235c68d39ae8d5cccd9719421577",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/compute_resource.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "f108e2afd910cd13e8c3ffb98568567b14e43600b31e701581bb2a6fde58e135",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/usergroup.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6a9d6df5f428d2a0865205ae22720901f14d28d4037a6f8f80ab7c228350e67d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/product.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "98d43acf90507636cba40cdfbda5ab844c8904da51e4fde6331f25d8125f03dc",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/hardware_model.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "db598a957fa07c32cb6ea465f79444a5d103de4c16b4eda575e2981359fc02d9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/activation_key.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "de6ebd81295a41e16e47084adaf2e0ce57a24a6ebbbd9312293ab901c70c6428",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/realm.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e5087b0c705cf6a608aecea02e5ea0dee9b9c2e139b755199b488898eea13abc",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0b6e98c8531a0e57c19d2d3dcdbf3073097a622b205fd124cff6f8090f65f3dd",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/organization.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5b2db55128c097773cde502b43d77f5d938ff7bc6c8ea9b1c82207ae125c3cf8",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_filter_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7991817116749b9f8432531169879da0ca9973c7017a46664ac3a5bd2d75c83c",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/redhat_manifest.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "caa3c7f9ada577a9f36ac242fffbe17bfe9de3740ae871b09bc0e0542e4928b9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/provisioning_template.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "58426b877b3844c6d5b162a35860fddac31365d684589c4c98f090c5c7e3748d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/compute_profile.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "7e75573a41a0ec7b1fd13d4e9763c17732a18ad5c4dd3124d0b3aa8ab44b6834",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/job_invocation.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "20a670b23d88661cd3db44f01207f9a06109ce0ad8e712b6753821b05a8bdb4a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_export_repository.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e0d6b95a349f0a9923f4497b25bacbde592a40ad62ad81381e4b6fc239567608",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/resource_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "dad6d110f034475244a03be83942b4347a8abd7223b24a31c2aaf8078b170dd7",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/compute_attribute.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "251360fdd219f87cd4947fe4d4f6e5600bfa8082ba31399451bd84d60406298e",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/subscription_manifest.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a39ae01e57f04f5e8bd806b3f98cf9952ad2b9db5953672310482a98a382c380",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/scc_account.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6f14b325fad9499fada3a2021493097e29cda439af8ba62ca3fadaf43d9d575d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/subnet_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1adba416abbe6a43e17d2833d22c43d0c58fd350e6f2a337cdfea104d5e49ca9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/scap_content.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ccb415652f59e34cf1f9fe4a4b0844c0d0503fb655239ed133f52c84dbda9ce3",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/hostgroup.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "87e72b29a6580d711b09de080c5b9a7020d3cb33481927226d45b6049ed530dd",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/scc_product.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "130323975b926aec4193cb2e88a64ac2131e3f63b124b568ec1606a7e712877a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/domain.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "6643d6b0fabfcfea13112df106a494e9d97f4989b0cad629b828b3373d65642b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_version_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4ffb312ffa1b891a940da677e20ac8eb5aa9b97e225bb1ce463468b13df358fd",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/puppetclasses_import.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ee16bbf8186eba7da9ed1d3013d4f48f790a0f35290232ccb25ae755298d7a57",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/templates_import.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "29387dd51b003d83a04c5f86b799d4f7bf637db00b424acd76320caba52278b6",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/os_default_template.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5979a3c2d084c2fa84a8fd86b45ff37e17fff375ecbcf5770901b1fb6f29504f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_export_library.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0421ecc71966793d480cbda0031db9d99e2e90602849367e74ac4f5b47f2c897",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/snapshot_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "dc9071e8b899cf9c805c16acebcc2114490f9c8c4da0c1fc318db5996fdc1622",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/repository_sync.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "37432a3e69a0a6f21b7a9d747404c217b86c678614ac5b722037a63ee42ef2f8",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_upload.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3ca817b0d1da05340445e0866bdb57d5bdf73a23ff1c6fec97964151af29915b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/location.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e6ed594e81190b017530d67a24c89ba7fa1b715d50db1a3b23219511683af806",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/host_power.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c3a1e191165d94b708546db6a3b0a5a38362d757158da64fe72406b5e151085e",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_version.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "2ed93cc365852f53011ea7bb2a7950f2534271c202cdf149f7318967c95a97b7",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/subscription_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "e1f49873856698bef0163c38496ef7f91abcbcdb3f0b347dfb0616a1c52d33a7",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/user.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8dc4bf5ed456c2da776c5ba43afe2f6527b41d75bf85a4b902037447d84b2728",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/installation_medium.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "508344224fd3d2702b919f75ef779b1f8366c72899c39823217bf06ed4c53b3a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/hostgroup_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "aa7ec220d077088bc1488c69524b49b7b18e682485a776787712d85b8995e68d",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/puppet_environment.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "d0dc12829c3bacfefc8a85d43c875c912058cb422742d0f2dcab064d8c9a5557",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/bookmark.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "76c52fe346b5b20ef4997b3f816d2b996c1c5a4f8cee7f73cb90c97fe0adc820",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/global_parameter.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0dc94762c660dc3773b51171d332a8cecef448e2f07868828ba532b4d8922782",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/config_group.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3da0833b6177a7a9b6c4c8a84db5a7209ea3c586a9e7b86c0a592a447108079e",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/discovery_rule.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "431d0c1b305c431cc9eaab26543fba0c5092ec97daa33abd3ac675f79c28c9f9",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_filter.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "933805fc3f2c62b1ef320a956c38d1386d04fdede979f8a46a50da29dbe5306f",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/scap_tailoring_file.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "8e0a89ec41681f48f104cccac1a22caf9f9267274ed99b1a441638718a445ae2",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_export_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "bf84524f3cfaa6ae2e6cf565ac24be8a188fdc42d55f8afd87de59a38c9fcd1a",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_credential.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "c74ae794aeaa5f4c7dee41580dbc03566ac62948a99fec7c5591d45fade1f6ca",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/architecture.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "26759610a81cb370d14544f34b5743a3d192f281da41ce0b44c4d9b43adc9570",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/content_view_filter_rule.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "3ed82348a69b67692390601af278c43590ac9f0af8a715bc80519575eec69f0b",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/job_template.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "a43fad291f92d3409ae04954f5a390b7617d7ce7ac5fa38fe5b0e29d203be478",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/partition_table.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "36dafbc1ad3fbc567c66da0c774f0f36c26bfbdf4ecfb1482d6bad43f6ea77d6",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/smart_class_parameter.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "79880750644d397c1ee6aaf996c27bcabc15e85273215a0f1b6820eb2d7b6c78",
+ "format": 1
+ },
+ {
+ "name": "plugins/modules/repository_set_info.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "21112984964503599daee1f8629dee78f29f3c4a4a60228e62c20ba6dbb8be28",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/foreman_helper.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "1d7a0208ebb2537fd14f496eb913375ed1dad1fe8d170f1a82d61c7d76e333bc",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/_apypie.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "64b1fb65d27a25332af56815aeba02226dc9b663c09aeb303b543e5dcc1510c5",
+ "format": 1
+ },
+ {
+ "name": "plugins/module_utils/_version.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "ae6c984a7e9dd51753ea7fcb5995d5655016dd5dc187cd9be7216ef7045f220b",
+ "format": 1
+ },
+ {
+ "name": "plugins/filter",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/filter/foreman.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "0f7c635e75c475f67afd1a19caecee672466d75057f40e91100a7c243beb1e45",
+ "format": 1
+ },
+ {
+ "name": "plugins/filter/cp_label.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "5e4e79480ba70ca309052ec11a02dbc386f542363f9fbd9a3d8451e67f071caa",
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "plugins/doc_fragments/foreman.py",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "71a250dc74ff0a43a7c89a15dd76d2d197beb83f542ff870fc778893ee906ffb",
+ "format": 1
+ },
+ {
+ "name": "PSF-license.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "83b042fc7d6aca0f10d68e45efa56b9bc0a1496608e7e7728fe09d1a534a054a",
+ "format": 1
+ },
+ {
+ "name": "bindep.txt",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "cb8ebfabb5af39c85d0d8eb9b734531aa2bf2ec44cfe4697810d517388fd9555",
+ "format": 1
+ },
+ {
+ "name": "CHANGELOG.rst",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4c9177f7e553aea7f1578540473ba5d06b12bc40c9b7d8fa0ffe1005b1eec79d",
+ "format": 1
+ },
+ {
+ "name": "meta",
+ "ftype": "dir",
+ "chksum_type": null,
+ "chksum_sha256": null,
+ "format": 1
+ },
+ {
+ "name": "meta/execution-environment.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "4ec43cfd0807568d41118d24ab243243ad74d8bd638f941ff671262c6f8d9293",
+ "format": 1
+ },
+ {
+ "name": "meta/runtime.yml",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "adabb170f71eff9cf3544888f64a8a1d598e54eaf8fcac6ef0527b0c36f2f864",
+ "format": 1
+ }
+ ],
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/theforeman/foreman/LICENSE b/ansible_collections/theforeman/foreman/LICENSE
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ansible_collections/theforeman/foreman/MANIFEST.json b/ansible_collections/theforeman/foreman/MANIFEST.json
new file mode 100644
index 00000000..e8e431ac
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/MANIFEST.json
@@ -0,0 +1,121 @@
+{
+ "collection_info": {
+ "namespace": "theforeman",
+ "name": "foreman",
+ "version": "3.9.0",
+ "authors": [
+ "@lessfoobar <59063885+lessfoobar@users.noreply.github.com>",
+ "Adam R\u016f\u017ei\u010dka <aruzicka@redhat.com>",
+ "AlanCoding <arominge@redhat.com>",
+ "Andrew Kofink <ajkofink@gmail.com>",
+ "Anthony Green <green@moxielogic.com>",
+ "Anton <nesanton@gmail.com>",
+ "Baptiste Agasse <baptiste.agasse@gmail.com>",
+ "Bernhard Hopfenm\u00fcller <hopfenmueller@atix.de>",
+ "Bernhard Suttner <sbernhard@users.noreply.github.com>",
+ "Bryan Kearney <bkearney@redhat.com>",
+ "Chris Forkner <gen2fish@gmail.com>",
+ "Chris Hindman <chindman@chindman.remote.csb>",
+ "Chris Snell <chsnell@users.noreply.github.com>",
+ "Christoffer Reijer <ephracis@gmail.com>",
+ "Dave Thomas <11580510+dthomastx@users.noreply.github.com>",
+ "Deric Crago <deric.crago@gmail.com>",
+ "Eric D. Helms <ericdhelms@gmail.com>",
+ "Eric L <ericzolf@users.noreply.github.com>",
+ "Ethan <smithe2413@gmail.com>",
+ "Evgeni Golov <evgeni@golov.de>",
+ "Ewoud Kohl van Wijngaarden <ewoud@kohlvanwijngaarden.nl>",
+ "Felix Fontein <felix@fontein.de>",
+ "Gerald Vogt <gvde@users.noreply.github.com>",
+ "Greg Swift <gregswift@gmail.com>",
+ "Griffin Sullivan <gsulliva@redhat.com>",
+ "Hideki Saito <saito@fgrep.org>",
+ "Ismael Puerto <ismaelpuerto@users.noreply.github.com>",
+ "Jameer Pathan <jpathan@redhat.com>",
+ "James Jeffers <jjeffers@redhat.com>",
+ "James Stuart <james@stuart.name>",
+ "Jeffrey van Pelt <jeff@vanpelt.one>",
+ "Jeremy Albinet <jalbinet@scaleway.com>",
+ "Jeremy Lenz <jlenz@redhat.com>",
+ "Jesper Reenberg <jesper.reenberg@gmail.com>",
+ "John Berninger <john.berninger@gmail.com>",
+ "Josh Swanson <jswanson@redhat.com>",
+ "Kenny Tordeurs <ktordeur@redhat.com>",
+ "Kirill Shirinkin <fodojyko@gmail.com>",
+ "Leos Stejskal <lstejska@redhat.com>",
+ "Lester Claudio <claudiol@redhat.com>",
+ "Lucas Bickel <hairmare@rabe.ch>",
+ "Luk\u00e1\u0161 Zapletal <lzap+git@redhat.com>",
+ "Manisha Singhal <manisha1595@gmail.com>",
+ "Manuel Bonk <githubMB@bonkii.com>",
+ "Marcelo Moreira de Mello <tchello.mello@gmail.com>",
+ "Marek Czernek <mczernek@redhat.com>",
+ "Mark Hlawatschek <hlawatschek@atix.de>",
+ "Markus Bucher <bucher@atix.de>",
+ "Martin Schlossarek <martin.schlossarek@gmail.com>",
+ "Matthias Dellweg <2500@gmx.de>",
+ "Miikka Joutsenvirta <miikka.joutsenvirta@dna.fi>",
+ "Nikhil Jain <jainnikhil30@gmail.com>",
+ "Olivier <oliverf1ca@yahoo.com>",
+ "Ondrej Prazak <oprazak@redhat.com>",
+ "Ond\u0159ej Ezr <oezr@redhat.com>",
+ "Ond\u0159ej Gajdu\u0161ek <ogajduse@redhat.com>",
+ "Patrick C. F. Ernzer <pcfe@pcfe.net>",
+ "Patrick Creech <pcreech@redhat.com>",
+ "Paul Armstrong <parmstro@redhat.com>",
+ "Paul Belanger <pabelanger@redhat.com>",
+ "Paul Gration <pmgration@gmail.com>",
+ "Peter Ondrejka <pondrejk@redhat.com>",
+ "Philipp <philipp98.joos@gmail.com>",
+ "Quirin Pamp <pamp@atix.de>",
+ "Richard Stempfl <richielatk@gmail.com>",
+ "Sam <samcalvert@me.com>",
+ "Samir Jha <sjha4@ncsu.edu>",
+ "Sean O'Keeffe <seanokeeffe797@gmail.com>",
+ "Sorin Sbarnea <sorin.sbarnea@gmail.com>",
+ "Stoned Elipot <stoned.elipot@gmail.com>",
+ "TTherouanne <thomas@therouanne.com>",
+ "Thomas Mueller <thomas@chaschperli.ch>",
+ "William Bradford Clark <wclark@redhat.com>",
+ "Yifat Makias <ymakias@redhat.com>",
+ "achevalet <anthony.chevalet@gmail.com>",
+ "bob <57952350+TheRedGreek@users.noreply.github.com>",
+ "calvingsmith <4283930+calvingsmith@users.noreply.github.com>",
+ "divialth <65872926+divialth@users.noreply.github.com>",
+ "furhouse <furhouse@users.noreply.github.com>",
+ "gardar <gardar@users.noreply.github.com>",
+ "igramic <36156377+igramic@users.noreply.github.com>",
+ "jerrejkw <43955357+jerrejkw@users.noreply.github.com>",
+ "marco <marco.markgraf@transporeon.com>",
+ "metalcated <mike.gomon@gmail.com>",
+ "russianguppie <46544650+russianguppie@users.noreply.github.com>",
+ "willtome <willtome@gmail.com>"
+ ],
+ "readme": "README.md",
+ "tags": [
+ "foreman",
+ "katello",
+ "satellite",
+ "orcharhino",
+ "infrastructure"
+ ],
+ "description": "Ansible Modules to manage Foreman and Katello installations",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "license_file": null,
+ "dependencies": {},
+ "repository": "https://github.com/theforeman/foreman-ansible-modules",
+ "documentation": "https://theforeman.org/plugins/foreman-ansible-modules/",
+ "homepage": "https://theforeman.org/",
+ "issues": "https://github.com/theforeman/foreman-ansible-modules/issues"
+ },
+ "file_manifest_file": {
+ "name": "FILES.json",
+ "ftype": "file",
+ "chksum_type": "sha256",
+ "chksum_sha256": "93de30b5e21080323a326f285f8c9658eacb4d0754a861d10f1b754c1ab4648c",
+ "format": 1
+ },
+ "format": 1
+} \ No newline at end of file
diff --git a/ansible_collections/theforeman/foreman/PSF-license.txt b/ansible_collections/theforeman/foreman/PSF-license.txt
new file mode 100644
index 00000000..35acd7fb
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/PSF-license.txt
@@ -0,0 +1,48 @@
+PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
+--------------------------------------------
+
+1. This LICENSE AGREEMENT is between the Python Software Foundation
+("PSF"), and the Individual or Organization ("Licensee") accessing and
+otherwise using this software ("Python") in source or binary form and
+its associated documentation.
+
+2. Subject to the terms and conditions of this License Agreement, PSF hereby
+grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
+analyze, test, perform and/or display publicly, prepare derivative works,
+distribute, and otherwise use Python alone or in any derivative version,
+provided, however, that PSF's License Agreement and PSF's notice of copyright,
+i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
+2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021 Python Software Foundation;
+All Rights Reserved" are retained in Python alone or in any derivative version
+prepared by Licensee.
+
+3. In the event Licensee prepares a derivative work that is based on
+or incorporates Python or any part thereof, and wants to make
+the derivative work available to others as provided herein, then
+Licensee hereby agrees to include in any such work a brief summary of
+the changes made to Python.
+
+4. PSF is making Python available to Licensee on an "AS IS"
+basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
+IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
+DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
+FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
+INFRINGE ANY THIRD PARTY RIGHTS.
+
+5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
+FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
+A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
+OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
+
+6. This License Agreement will automatically terminate upon a material
+breach of its terms and conditions.
+
+7. Nothing in this License Agreement shall be deemed to create any
+relationship of agency, partnership, or joint venture between PSF and
+Licensee. This License Agreement does not grant permission to use PSF
+trademarks or trade name in a trademark sense to endorse or promote
+products or services of Licensee, or any third party.
+
+8. By copying, installing or otherwise using Python, Licensee
+agrees to be bound by the terms and conditions of this License
+Agreement.
diff --git a/ansible_collections/theforeman/foreman/README.md b/ansible_collections/theforeman/foreman/README.md
new file mode 100644
index 00000000..e23c2d08
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/README.md
@@ -0,0 +1,146 @@
+# Foreman Ansible Modules ![Build Status](https://github.com/theforeman/foreman-ansible-modules/workflows/CI/badge.svg)
+
+Ansible modules for interacting with the Foreman API and various plugin APIs such as Katello.
+
+## Documentation
+
+A list of all modules and their documentation can be found at [theforeman.org/plugins/foreman-ansible-modules](https://theforeman.org/plugins/foreman-ansible-modules/).
+
+## Support
+
+### Supported Foreman and plugins versions
+
+Modules should support any currently stable Foreman release and the matching set of plugins.
+Some modules have additional features/arguments that are only applied when the corresponding plugin is installed.
+
+We actively test the modules against the latest stable Foreman release and the matching set of plugins.
+
+### Supported Ansible Versions
+
+The supported Ansible versions are aligned with currently maintained Ansible versions that support Collections (2.9+).
+You can find the list of maintained Ansible versions [here](https://docs.ansible.com/ansible/devel/reference_appendices/release_and_maintenance.html).
+
+### Supported Python Versions
+
+Ansible only supports Python 2.7 and 3.5 (and higher). These are also the only Python versions we develop and test the modules against.
+
+### Known issues
+
+* Some modules, e.g. `repository_sync` and `content_view_version`, trigger long running tasks on the server side. It might be beneficial to your playbook to wait for their completion in an asynchronous manner.
+ As Ansible has facilities to do so, the modules will wait unconditionally. See the [Ansible documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_async.html) for putting tasks in the background.
+ Please make sure to set a high enough `async` value, as otherwise Ansible might abort the execution of the module while there is still a task running on the server, making status reporting fail.
+
+* According to [Ansible documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html), using loop over Ansible resources can leak sensitive data. This applies to all modules, but especially those which require more secrets than the API credentials (`auth_source_ldap`, `compute_resource`, `host`, `hostgroup`, `http_proxy`, `image`, `repository`, `scc_account`, `user`). You can prevent this by using `no_log: yes` on the task.
+
+ eg:
+
+ ```yaml
+ - name: Create compute resources
+ theforeman.foreman.compute_resource:
+ server_url: https://foreman.example.com
+ username: admin
+ password: changeme
+ validate_certs: yes
+ name: "{{ item.name }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ locations: "{{ item.locations | default(omit) }}"
+ description: "{{ item.description | default(omit) }}"
+ provider: "{{ item.provider }}"
+ provider_params: "{{ item.provider_params | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ compute_resources }}"
+ no_log: yes
+ ```
+* Modules require write access to `~/.cache` (or wherever `$XDG_CACHE_HOME` points at). Otherwise the API documentation cannot be downloaded and you get errors like `[Errno 13] Permission denied: '/home/runner/.cache/apypie`. If on your system `~/.cache` is not writeable, please set the `$XDG_CACHE_HOME` environment variable to a directory Ansible can write to.
+
+## Installation
+
+There are currently two ways to use the modules in your setup: install directly from Ansible Galaxy or via packages.
+
+### Installation from Ansible Galaxy
+
+You can install the collection from [Ansible Galaxy](https://galaxy.ansible.com/theforeman/foreman) by running `ansible-galaxy collection install theforeman.foreman`.
+
+After the installation, the modules are available as `theforeman.foreman.<module_name>`. Please see the [Using Ansible collections documentation](https://docs.ansible.com/ansible/devel/user_guide/collections_using.html) for further details.
+
+### Installation via packages
+
+The collection is also available as `ansible-collection-theforeman-foreman` from the `plugins` repository on `yum.theforeman.org` for Enterprise Linux systems and from the `plugins` repository on `deb.theforeman.org` for Debian and Ubuntu systems.
+
+After installing the package, you can use the modules in the same way as when they are installed directly from Ansible Galaxy.
+
+## Installation From Source
+
+For development or testing purposes, you can install the collection from source git repository. For production usage, see the instructions above on installing the latest stable release.
+
+### Installation from Github Repository
+
+With Ansible >= 2.10, you can install from a Github repository (such as this one or your fork):
+
+```console
+$ ansible-galaxy collection install git+https://github.com/theforeman/foreman-ansible-modules.git
+```
+
+If you have configured GitHub to use SSH instead of HTTPS, you can do:
+
+```console
+$ ansible-galaxy collection install git@github.com/theforeman/foreman-ansible-modules.git
+```
+
+You can also specify a branch to use such as `devel` (below) or a feature branch that you are working with:
+
+```console
+$ ansible-galaxy collection install git+https://github.com/theforeman/foreman-ansible-modules.git,devel
+```
+
+To install from a `requirements.yml` file (useful when installing multiple collections) add a snippet to your `requirements.yml` like
+
+```yaml
+---
+collections:
+ - name: https://github.com/theforeman/foreman-ansible-modules.git
+ type: git
+ version: devel
+```
+
+And install all specified requirements with `ansible-galaxy install -r requirements.yml`
+
+### Building and Installing the Collection Locally
+
+For all currently supported versions of Ansible (i.e. Ansible >= 2.9, and particularly Ansible < 2.10 where the above approach is not yet supported), you can build the collection locally:
+
+```console
+$ make dist
+```
+
+And install it with:
+
+```console
+$ ansible-galaxy collection install ./theforeman-foreman-*.tar.gz
+```
+
+## Dependencies
+
+These dependencies are required for the Ansible controller, not the Foreman server.
+
+* [`PyYAML`](https://pypi.org/project/PyYAML/)
+* [`requests`](https://pypi.org/project/requests/)
+* [`ipaddress`](https://pypi.org/project/ipaddress/) for the `subnet` module on Python 2.7
+* `rpm` for the RPM support in the `content_upload` module
+* `debian` for the DEB support in the `content_upload` module
+
+# Foreman Ansible Roles
+
+Roles using the Foreman Ansible Modules to configure Foreman and its plugins.
+
+## Documentation
+
+For individual role documentation, check the README defined at `roles/rolename/README.md`.
+
+### Common Role Variables
+
+- `foreman_server_url`: URL of the Foreman server. If the variable is not specified, the value of environment variable `FOREMAN_SERVER_URL` will be used instead.
+- `foreman_username`: Username accessing the Foreman server. If the variable is not specified, the value of environment variable `FOREMAN_USERNAME` will be used instead.
+- `foreman_password`: Password of the user accessing the Foreman server. If the variable is not specified, the value of environment variable `FOREMAN_PASSWORD` will be used instead.
+- `foreman_validate_certs`: Whether or not to verify the TLS certificates of the Foreman server. If the variable is not specified, the value of environment variable `FOREMAN_VALIDATE_CERTS` will be used instead.
+- `foreman_organization`: Organization where configuration will be applied.
diff --git a/ansible_collections/theforeman/foreman/bindep.txt b/ansible_collections/theforeman/foreman/bindep.txt
new file mode 100644
index 00000000..1bfc31e5
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/bindep.txt
@@ -0,0 +1,3 @@
+python3-rpm [(platform:redhat platform:base-py3)]
+rpm-python [(platform:redhat platform:base-py2)]
+python38-requests [platform:centos-8 platform:rhel-8]
diff --git a/ansible_collections/theforeman/foreman/changelogs/changelog.yaml b/ansible_collections/theforeman/foreman/changelogs/changelog.yaml
new file mode 100644
index 00000000..578bb1b6
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/changelogs/changelog.yaml
@@ -0,0 +1,725 @@
+ancestor: 0.8.1
+releases:
+ 1.0.0:
+ changes:
+ breaking_changes:
+ - |
+ All modules were renamed to drop the ``foreman_`` and ``katello_`` prefixes.
+ Additionally to the prefix removal, the following modules were further ranamed:
+
+ * katello_upload to content_upload
+ * katello_sync to repository_sync
+ * katello_manifest to subscription_manifest
+ * foreman_search_facts to resource_info
+ * foreman_ptable to partition_table
+ * foreman_model to hardware_model
+ * foreman_environment to puppet_environment
+ release_summary: |
+ This is the first stable release of the ``theforeman.foreman`` collection.
+ modules:
+ - description: Manage Activation Keys
+ name: activation_key
+ namespace: ''
+ - description: Manage Architectures
+ name: architecture
+ namespace: ''
+ - description: Manage LDAP Authentication Sources
+ name: auth_source_ldap
+ namespace: ''
+ - description: Manage Bookmarks
+ name: bookmark
+ namespace: ''
+ - description: Manage Compute Attributes
+ name: compute_attribute
+ namespace: ''
+ - description: Manage Compute Profiles
+ name: compute_profile
+ namespace: ''
+ - description: Manage Compute Resources
+ name: compute_resource
+ namespace: ''
+ - description: Manage (Puppet) Config Groups
+ name: config_group
+ namespace: ''
+ - description: Manage Content Credentials
+ name: content_credential
+ namespace: ''
+ - description: Upload content to a repository
+ name: content_upload
+ namespace: ''
+ - description: Manage Content Views
+ name: content_view
+ namespace: ''
+ - description: Manage Content View Filters
+ name: content_view_filter
+ namespace: ''
+ - description: Manage Content View Versions
+ name: content_view_version
+ namespace: ''
+ - description: Manage Domains
+ name: domain
+ namespace: ''
+ - description: Manage External User Groups
+ name: external_usergroup
+ namespace: ''
+ - description: Manage Global Parameters
+ name: global_parameter
+ namespace: ''
+ - description: Manage Hardware Models
+ name: hardware_model
+ namespace: ''
+ - description: Manage Hosts
+ name: host
+ namespace: ''
+ - description: Manage Host Collections
+ name: host_collection
+ namespace: ''
+ - description: Manage Power State of Hosts
+ name: host_power
+ namespace: ''
+ - description: Manage Hostgroups
+ name: hostgroup
+ namespace: ''
+ - description: Manage Images
+ name: image
+ namespace: ''
+ - description: Manage Installation Media
+ name: installation_medium
+ namespace: ''
+ - description: Manage Job Templates
+ name: job_template
+ namespace: ''
+ - description: Manage Lifecycle Environments
+ name: lifecycle_environment
+ namespace: ''
+ - description: Manage Locations
+ name: location
+ namespace: ''
+ - description: Manage Operating Systems
+ name: operatingsystem
+ namespace: ''
+ - description: Manage Organizations
+ name: organization
+ namespace: ''
+ - description: Manage Default Template Associations To Operating Systems
+ name: os_default_template
+ namespace: ''
+ - description: Manage Partition Table Templates
+ name: partition_table
+ namespace: ''
+ - description: Manage Products
+ name: product
+ namespace: ''
+ - description: Manage Provisioning Templates
+ name: provisioning_template
+ namespace: ''
+ - description: Manage Puppet Environments
+ name: puppet_environment
+ namespace: ''
+ - description: Manage Realms
+ name: realm
+ namespace: ''
+ - description: Interact with a Red Hat Satellite Subscription Manifest
+ name: redhat_manifest
+ namespace: ''
+ - description: Manage Repositories
+ name: repository
+ namespace: ''
+ - description: Enable/disable Repositories in Repository Sets
+ name: repository_set
+ namespace: ''
+ - description: Sync a Repository or Product
+ name: repository_sync
+ namespace: ''
+ - description: Gather information about resources
+ name: resource_info
+ namespace: ''
+ - description: Manage Roles
+ name: role
+ namespace: ''
+ - description: Manage SCAP content
+ name: scap_content
+ namespace: ''
+ - description: Manage SCAP Tailoring Files
+ name: scap_tailoring_file
+ namespace: ''
+ - description: Manage SUSE Customer Center Accounts
+ name: scc_account
+ namespace: ''
+ - description: Subscribe SUSE Customer Center Account Products
+ name: scc_product
+ namespace: ''
+ - description: Manage Settings
+ name: setting
+ namespace: ''
+ - description: Manage Smart Class Parameters
+ name: smart_class_parameter
+ namespace: ''
+ - description: Manage Snapshots
+ name: snapshot
+ namespace: ''
+ - description: Manage Subnets
+ name: subnet
+ namespace: ''
+ - description: Manage Subscription Manifests
+ name: subscription_manifest
+ namespace: ''
+ - description: Manage Sync Plans
+ name: sync_plan
+ namespace: ''
+ - description: Sync Templates from a repository
+ name: templates_import
+ namespace: ''
+ - description: Manage Users
+ name: user
+ namespace: ''
+ - description: Manage User Groups
+ name: usergroup
+ namespace: ''
+ release_date: '2020-06-19'
+ 1.0.1:
+ changes:
+ release_summary: |
+ Documentation fixes to reflect the correct module names.
+ release_date: '2020-06-29'
+ 1.1.0:
+ changes:
+ bugfixes:
+ - inventory plugin - fix want_params handling (https://github.com/theforeman/foreman-ansible-modules/issues/847)
+ minor_changes:
+ - activation_key - add ``description`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/915)
+ - callback plugin - add reporter to report logs sent to Foreman (https://github.com/theforeman/foreman-ansible-modules/issues/836)
+ - document return values of modules (https://github.com/theforeman/foreman-ansible-modules/pull/901)
+ - inventory plugin - allow to control batch size when pulling hosts from Foreman
+ (https://github.com/theforeman/foreman-ansible-modules/pull/865)
+ - subnet - Require mask/cidr only on ipv4 (https://github.com/theforeman/foreman-ansible-modules/issues/878)
+ fragments:
+ - 836-add-reporter-to-callback.yaml
+ - 847-want_params-fix.yaml
+ - 865-limit-inventory-per-host.yaml
+ - 878-cidr-only-v4.yaml
+ - 901-document-return-values.yaml
+ - 915-activation_key-description.yaml
+ modules:
+ - description: Manage HTTP Proxies
+ name: http_proxy
+ namespace: ''
+ release_date: '2020-08-17'
+ 1.2.0:
+ changes:
+ bugfixes:
+ - image - fix quoting of search values (https://github.com/theforeman/foreman-ansible-modules/issues/927)
+ minor_changes:
+ - compute_resource - added ``caching_enabled`` option for VMware compute resources
+ - domain, host, hostgroup, operatingsystem, subnet - manage parameters in a
+ single API call (https://bugzilla.redhat.com/show_bug.cgi?id=1855008)
+ - host - add ``compute_attributes`` parameter to module (https://bugzilla.redhat.com/show_bug.cgi?id=1871815)
+ - provisioning_template - update list of possible template kinds (https://bugzilla.redhat.com/show_bug.cgi?id=1871978)
+ - repository - update supported parameters (https://github.com/theforeman/foreman-ansible-modules/issues/935)
+ fragments:
+ - 927-image-quoting.yaml
+ - 932-host-add-compute_attributes.yaml
+ - 935-repository-params.yaml
+ - 939-compute_resource-added_caching_enabled.yaml
+ - bz1855008-single_call_parameters.yaml
+ - bz1871978-template_kinds.yaml
+ release_date: '2020-09-03'
+ 1.3.0:
+ changes:
+ bugfixes:
+ - content_view - remove CVs from lifecycle environments before deleting them
+ (https://bugzilla.redhat.com/show_bug.cgi?id=1875314)
+ - external_usergroup - support non-LDAP external groups (https://github.com/theforeman/foreman-ansible-modules/issues/956)
+ - host - properly scope image lookups by the compute resource (https://bugzilla.redhat.com/show_bug.cgi?id=1878693)
+ - inventory plugin - include empty parent groups in the inventory (https://github.com/theforeman/foreman-ansible-modules/issues/919)
+ minor_changes:
+ - external_usergroup - rename the ``auth_source_ldap`` parameter to ``auth_source``
+ (``auth_source_ldap`` is still supported via an alias)
+ - server URL and credentials can now also be specified using environment variables
+ (https://github.com/theforeman/foreman-ansible-modules/issues/837)
+ - subnet - add support for external IPAM (https://github.com/theforeman/foreman-ansible-modules/issues/966)
+ fragments:
+ - 837-env-fallback.yaml
+ - 919-include-empty-parent-groups.yml
+ - 956-external_usergroup-non-ldap.yaml
+ - 966-subnet-external_ipam.yaml
+ - bz1875314-content_view-remove.yaml
+ - bz1878693-scope_image_lookups.yaml
+ modules:
+ - description: Get status info
+ name: status_info
+ namespace: ''
+ release_date: '2020-09-22'
+ 1.4.0:
+ changes:
+ bugfixes:
+ - Don't try to update an entity, if only parameters that aren't supported by
+ the server are detected as changed. (https://github.com/theforeman/foreman-ansible-modules/issues/975)
+ - allow to pass an empty string when refering to entities, thus unsetting the
+ value (https://github.com/theforeman/foreman-ansible-modules/issues/969)
+ - compute_profile - don't fail when trying to update compute attributes of a
+ profile (https://github.com/theforeman/foreman-ansible-modules/issues/997)
+ - host, hostgroup - support ``None`` as the ``pxe_loader`` (https://github.com/theforeman/foreman-ansible-modules/issues/971)
+ - job_template - don't fail when trying to update template_inputs
+ - os_default_template - document possible template kind choices (https://bugzilla.redhat.com/show_bug.cgi?id=1889952)
+ - smart_class_parameters - don't fail when trying to update override_values
+ minor_changes:
+ - global_parameter - allow to set hidden flag (https://github.com/theforeman/foreman-ansible-modules/issues/1024)
+ - job_template - stricter validation of ``template_inputs`` sub-options
+ - redhat_manifest - allow configuring content access mode (https://github.com/theforeman/foreman-ansible-modules/issues/820)
+ - subnet - verify the server has the ``remote_execution`` plugin when specifying
+ ``remote_execution_proxies``
+ - the ``apypie`` library is vendored inside the collection, so users only have
+ to install ``requests`` manually now.
+ fragments:
+ - 820-redhat_manifest-sca.yaml
+ - 969-allow-unset-entity.yml
+ - 971-pxe_loader-none.yaml
+ - 975-filter-update-payload.yml
+ - 997-compute_profile-update.yaml
+ - bz1889952-os_default_template-kind.yaml
+ - global_parameter-hidden_values.yaml
+ - job_template-template_inputs-validation.yaml
+ - rex-proxy-subnet-check.yaml
+ - venored_apypie.yaml
+ modules:
+ - description: Invoke Remote Execution Jobs
+ name: job_invocation
+ namespace: ''
+ - description: Manage Smart Proxies
+ name: smart_proxy
+ namespace: ''
+ release_date: '2020-10-21'
+ 1.5.0:
+ changes:
+ bugfixes:
+ - content_upload - Fix upload of files bigger than 2MB in Pulp3-based setups
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1043)
+ - job_invocation - properly submit ``ssh``, ``recurrence``, ``scheduling`` and
+ ``concurrency_control`` to the server
+ - repository - don't emit a false warning about ``organization_id`` not being
+ supported by the server (https://github.com/theforeman/foreman-ansible-modules/issues/1055)
+ - repository_set, repository - clarify documentation which module should be
+ used for Red Hat Repositories (https://github.com/theforeman/foreman-ansible-modules/issues/1059)
+ minor_changes:
+ - content_upload - use ``to_native`` to decode RPM headers if needed (RPM 4.15+
+ returns strings)
+ - content_view_version - provide examples how to obtain detailed information
+ about content view versions (https://bugzilla.redhat.com/show_bug.cgi?id=1868145)
+ - content_view_version_cleanup - new role for cleaning up unused content view
+ versions (https://github.com/theforeman/foreman-ansible-modules/issues/497)
+ - host - allow management of interfaces (https://github.com/theforeman/foreman-ansible-modules/issues/757)
+ - inventory plugin - add support for the Report API present in Foreman 1.24
+ and later
+ - inventory plugin - allow to compose the ``inventory_hostname`` (https://github.com/theforeman/foreman-ansible-modules/issues/1070)
+ - manifest - new role for easier handling of subscription manifest workflows
+ - subnet - add new ``externalipam_group`` parameter
+ - update vendored ``apypie`` to 0.3.2
+ fragments:
+ - 1043-chunked_content_upload.yml
+ - 1059-repository_set-docs.yml
+ - 1062-warnings.yml
+ - 1070-compose_inventory_hostname.yml
+ - 757-host_interfaces.yml
+ - bz1868145-cv_version_examples.yml
+ - content_upload-decode-old-rpm.yml
+ - inventory_plugin_report_api.yml
+ - subnet-externalipam_group.yml
+ - theforeman.foreman.content_view_version_cleanup_role.yml
+ - theforeman.foreman.manifest_role.yml
+ release_date: '2020-12-03'
+ 2.0.0:
+ changes:
+ breaking_changes:
+ - All role variables are now prefixed with ``foreman_`` to avoid clashes with
+ similarly named variables from roles outside this collection.
+ bugfixes:
+ - content_view_version - make the ``version`` parameter not fail when the version
+ was entered without a minor part (https://github.com/theforeman/foreman-ansible-modules/issues/1087)
+ - host - allow moving hosts between Organizations and Locations (https://bugzilla.redhat.com/show_bug.cgi?id=1901716)
+ - host - fix subnet/domain assignment when multiple interfaces are defined (https://github.com/theforeman/foreman-ansible-modules/issues/1095)
+ - host, hostgroup - select kickstart_repository based on lifecycle_environment
+ and content_view if those are set (https://github.com/theforeman/foreman-ansible-modules/issues/1090,
+ https://bugzilla.redhat.com/1915872)
+ - resource_info - correctly show the exact resource when passing ``id`` in ``params``
+ minor_changes:
+ - Add a role `activation_keys` to manage activation keys
+ - Add a role `lifecycle_environments` to manage lifecycle environments
+ - Add a role `repositories` to manage products, repositories, and repository_sets
+ - Add a role `sync_plans` to manage sync plans
+ - activation_key - add support for selecting subscriptions by ``upstream_pool_id``
+ - compute_resource - add ``set_console_password``, ``keyboard_layout`` and ``public_key``
+ parameters (https://github.com/theforeman/foreman-ansible-modules/issues/1052)
+ - host - clarify that ``owner`` refers to a users login, not their full name
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1045)
+ - host - look up the correct network id for a network given as part of ``interfaces_attributes``
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1104)
+ - host, hostgroup - add ``activation_keys`` parameter to ease configuring activation
+ keys for deploments
+ fragments:
+ - 1027-repositories_role.yml
+ - 1045-host-owner.yml
+ - 1052-compute_resource-params.yaml
+ - 1065-sync-plans_role.yml
+ - 1068-activation-keys_role.yml
+ - 1087-content_view_version-safenet.yml
+ - 1090-scope_kickstart_repository_tighter.yml
+ - 1095-nested_list-lookup-fix.yml
+ - 1096-lifecycle-environments-role.yml
+ - 1104-network_id-lookup.yml
+ - 1145-role_prefix.yml
+ - 541-activation-key-upstream-pool.yml
+ - bz1901716-dont_scope_requests.yml
+ - host-hostgroup-ak-parameter.yml
+ - resource_info-params-id.yml
+ modules:
+ - description: Fetch information about Hosts
+ name: host_info
+ namespace: ''
+ - description: Import Puppet Classes from a Proxy
+ name: puppetclasses_import
+ namespace: ''
+ - description: Fetch information about Repositories
+ name: repository_info
+ namespace: ''
+ release_date: '2021-02-22'
+ 2.0.1:
+ changes:
+ bugfixes:
+ - host - don't filter ``false`` values for ``interfaces_attributes`` (https://github.com/theforeman/foreman-ansible-modules/issues/1148)
+ - host_info, repository_info - correctly fetch all entities when neither ``name``
+ nor ``search`` is set
+ - host_info, repository_info - enforce mutual exclusivity of ``name`` and ``search``
+ fragments:
+ - 1148-dont-filter-false-params.yml
+ - 20210224-info_module_fixes.yml
+ release_date: '2021-03-02'
+ 2.1.0:
+ changes:
+ bugfixes:
+ - host - pass the right image id to the compute resource when creating a host
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1160, https://bugzilla.redhat.com/show_bug.cgi?id=1911670)
+ minor_changes:
+ - Add a domain_info module
+ - Add a hostgroups role (https://github.com/theforeman/foreman-ansible-modules/issues/1116)
+ - Add a role `content_rhel` to perform basic setup for registering and syncing
+ RHEL content hosts
+ - Add content credentials role
+ - callback plugin - collect facts during the run, merge them correctly and upload
+ them once at the end
+ - compute_resource - add ``cloud`` param for the AzureRm provider, to select
+ which Azure cloud to use
+ - compute_resource - add ``sub_id`` parameter for handling the Azure Subscription
+ ID instead of the ``user`` parameter
+ - host - Add ``Redfish`` to list of possible BMC providers of an interface
+ - host, compute_profile - look up the correct id for storage pods and domains
+ given as part of ``volumes_attributes`` (https://bugzilla.redhat.com/show_bug.cgi?id=1885234)
+ - hostgroup - add a ``ansible_roles`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1123)
+ - new ``content_views`` role to manage content views (https://github.com/theforeman/foreman-ansible-modules/issues/1111)
+ - new ``organizations`` role to manage organizations (https://github.com/theforeman/foreman-ansible-modules/issues/1109)
+ - subnet - add ``bmc_proxy`` parameter to configure BMC proxies for subnets
+ fragments:
+ - 1097-content-rhel-role.yml
+ - 1109-organizations-role.yml
+ - 1111-content_views_role.yml
+ - 1116-hostgroups-role.yml
+ - 1123-ansible-roles-for-hostgroups.yml
+ - 1160-pass-image-in-compute-attributes.yml
+ - 1196-content_credentials-role.yml
+ - azure-clouds.yml
+ - azure-subid.yml
+ - bz1885234-storage-lookup.yml
+ - domain-info-module.yml
+ - foreman-fact-upload.yml
+ - host-redfish-bmc.yml
+ - subnet-bmc-proxy-param.yml
+ modules:
+ - description: Fetch information about Content Views
+ name: content_view_info
+ namespace: ''
+ - description: Fetch information about Content Views
+ name: content_view_version_info
+ namespace: ''
+ - description: Fetch information about Domains
+ name: domain_info
+ namespace: ''
+ - description: Fetch information about Host Errata
+ name: host_errata_info
+ namespace: ''
+ - description: Fetch information about Red Hat Repositories
+ name: repository_set_info
+ namespace: ''
+ - description: Fetch information about Settings
+ name: setting_info
+ namespace: ''
+ - description: Fetch information about Subnets
+ name: subnet_info
+ namespace: ''
+ - description: Fetch information about Subscriptions
+ name: subscription_info
+ namespace: ''
+ release_date: '2021-05-20'
+ 2.1.1:
+ changes:
+ bugfixes:
+ - external_usergroup - always lookup the ID of the usergroup, instead of passing
+ the name to the API (https://bugzilla.redhat.com/show_bug.cgi?id=1967649)
+ - host, hostgroup - don't override already set parameters when passing an activation
+ key only (and vice versa) (https://bugzilla.redhat.com/show_bug.cgi?id=1967904)
+ fragments:
+ - bz1967649-usergroup_lookup.yml
+ - bz1967904-dont_override_params.yml
+ release_date: '2021-06-22'
+ 2.1.2:
+ changes:
+ bugfixes:
+ - activation_key - submit organization_id when querying subs, required for Katello
+ 4.1
+ - content_view_version_cleanup - sort content view versions before deleting
+ (https://github.com/RedHatSatellite/satellite-ansible-collection/issues/30,
+ https://bugzilla.redhat.com/show_bug.cgi?id=1980274)
+ - content_view_version_cleanup role - properly clean up when users set keep=0
+ (https://bugzilla.redhat.com/show_bug.cgi?id=1974314)
+ - host, compute_profile - when resolving cluster and other values in vm_attrs,
+ compare them as strings (https://github.com/theforeman/foreman-ansible-modules/issues/1245)
+ - subscription_info - mark ``organization`` parameter as required, to match
+ Katello
+ fragments:
+ - 1245-search_vm_attr_as_string.yml
+ - BZ1974314-cv_cleanup_keep_0.yml
+ - bz1980274.yml
+ - katello41-subscription-org-required.yml
+ release_date: '2021-07-13'
+ 2.2.0:
+ changes:
+ bugfixes:
+ - host, hostgroup - don't accidentally duplicate ``kt_activation_keys`` param
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1268)
+ minor_changes:
+ - repository - add support for filtering repositories by OS version based on
+ API feature apidoc/v2/repositories/create.html
+ fragments:
+ - 1268-ak_param_duplicate.yml
+ - repository.yml
+ release_date: '2021-08-24'
+ 3.0.0:
+ changes:
+ breaking_changes:
+ - Set use_reports_api default value to true for the inventory plugin
+ - Support for Ansible 2.8 is removed
+ bugfixes:
+ - host, hostgroup - fix updating puppetclasses while also updating description
+ (or other string-like attributes) (https://github.com/theforeman/foreman-ansible-modules/issues/1231)
+ minor_changes:
+ - Add a role `convert2rhel` to perform setup for converting systems to RHEL
+ - inventory plugin - enable certificate validation by default
+ - repository - add ``arch`` parameter to limit architectures of the repository
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1265)
+ fragments:
+ - 1231-puppetclasses-and-description-update.yml
+ - 1265-repository-arch.yml
+ - 1291-foreman-ansible-inventory-default.yml
+ - convert2rhel.yml
+ - drop-ansible28.yml
+ - validate-inventory-certs.yml
+ release_date: '2021-11-11'
+ 3.1.0:
+ changes:
+ bugfixes:
+ - callback plugin - include timezone information in the callback reported data
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1171)
+ - hostgroup, location - don't fail when trying to delete a Hostgroup or Location
+ where the parent is already absent
+ - inventory plugin - fetch *all* facts, not only the first 250, when using the
+ old Hosts API
+ minor_changes:
+ - Warn if the user tries to use a plain HTTP server URL and fail if the URL
+ is neither HTTPS nor HTTP.
+ - new ``compute_profiles`` role to manage compute profiles
+ - new ``compute_resources`` role to manage compute resources
+ - new ``content_view_publish`` role to publish a list of content views (https://github.com/theforeman/foreman-ansible-modules/issues/1209)
+ - new ``domains`` role to manage domains
+ - new ``operatingsystems`` role to manage operating systems
+ - new ``provisioning_templates`` role to manage provisioning templates
+ - new ``settings`` role to manage settings
+ - new ``subnets`` role to manage subnets
+ - repository - new ``download_concurrency`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1273)
+ fragments:
+ - 1171-timezone.yml
+ - 1209-content_views_role.yml
+ - 1301-failsafe-parents.yml
+ - 1303-foreman-repository-download-concurrency.yml
+ - add-compute-profile-role.yml
+ - add-compute-resource-role.yml
+ - add-domains-role.yml
+ - add-operatingsystems-role.yml
+ - add-provisioning-templates-role.yml
+ - add-settings-role.yml
+ - add-subnets-role.yml
+ - explicit-http-warning.yml
+ - inventory-facts-batch-size.yml
+ release_date: '2022-01-14'
+ 3.2.0:
+ changes:
+ bugfixes:
+ - content_upload - clarify that ``src`` refers to a remote file (https://bugzilla.redhat.com/show_bug.cgi?id=2055416)
+ minor_changes:
+ - new ``auth_sources_ldap`` role to manage LDAP authentication sources
+ fragments:
+ - add-auth-source-ldaps-role.yml
+ - bz2055416.yaml
+ release_date: '2022-03-01'
+ 3.3.0:
+ changes:
+ minor_changes:
+ - content_upload - add support for OSTree content uploads (https://github.com/theforeman/foreman-ansible-modules/issues/628,
+ https://projects.theforeman.org/issues/33299)
+ - os_default_template, provisioning_template - add ``host_init_config`` to list
+ of possible template types
+ fragments:
+ - 1297-host_init_config.yml
+ - 628-ostree.yml
+ release_date: '2022-04-04'
+ 3.4.0:
+ changes:
+ bugfixes:
+ - content_upload - properly detect SRPMs and ensure idempotency during uploads
+ (https://github.com/theforeman/foreman-ansible-modules/issues/1274)
+ - inventory plugin - fix caching for Report API (https://github.com/theforeman/foreman-ansible-modules/issues/1246)
+ - operatingsystem - find operatingsystems by title or full (name,major,minor)
+ tuple (https://github.com/theforeman/foreman-ansible-modules/issues/1401)
+ - os_default_template, provisioning_template - don't document invalid template
+ kind ``ptable`` (https://bugzilla.redhat.com/show_bug.cgi?id=1970132)
+ minor_changes:
+ - add support for module defaults groups for Ansible core 2.12 (https://github.com/theforeman/foreman-ansible-modules/issues/1015)
+ - all modules - report smaller diffs by dropping ``null`` values. This should
+ result in not showing fields that were unset to begin with, and mark fields
+ that were explicitly removed as "deleted" instead of "replaced by ``null``"
+ - compute_resource - update libvirt examples (https://bugzilla.redhat.com/show_bug.cgi?id=1990119)
+ - content_view - add support to set label during creation.
+ - repository - add ``rhel-9`` to os version filter choices
+ - repository - add support for ``mirroring_policy`` for Katello 4.4+ (https://github.com/theforeman/foreman-ansible-modules/issues/1388)
+ fragments:
+ - 1015-action_groups.yml
+ - 1246-inventory_reports_cache.yml
+ - 1274-content_upload-srpm.yml
+ - 1388-repository-mirroring_policy.yml
+ - 1397-content_view-label.yml
+ - 1401-os-search-title.yml
+ - 1970132-no-ptable-kind.yml
+ - 1990119-libvirt-examples.yml
+ - repository-rhel9.yml
+ - smaller-diffs.yml
+ release_date: '2022-05-17'
+ 3.5.0:
+ changes:
+ minor_changes:
+ - add execution environment metadata
+ - installation_medium, operatingsystem, partition_table - add ``Fcos``, ``Rhcos``,
+ ``VRP`` OS families
+ - job_template - add ``hidden_value`` to ``template_inputs`` parameters
+ - job_template - allow ``value_type`` to be ``resource``
+ - operatingsystems role - make ``provisioning_template`` parameter optional
+ - repositories role - add ``ansible_collection_requirements``
+ - repositories role - add ``arch`` and ``os_versions`` parameters
+ - repositories role - support ``mirroring_policy``
+ - repository, smart_proxy - document deprecation/removal status of ``download_policy=background``
+ - setting - the ``foreman_setting`` return entry is deprecated and kept for
+ backwards compatibility, please use ``entity`` as with any other module
+ - smart_proxy - add ``inherit`` to possible values of ``download_policy`` (https://github.com/theforeman/foreman-ansible-modules/issues/1438)
+ - smart_proxy - add ``streamed`` download policy
+ - snapshot - add include_ram option when creating VMWare snapshot
+ fragments:
+ - 1438-smart_proxy-download_policy-inherit.yml
+ - setting-return-deprecate.yml
+ modules:
+ - description: List pulp3 content exports
+ name: content_export_info
+ namespace: ''
+ - description: Manage content exports
+ name: content_export_library
+ namespace: ''
+ - description: Manage Host Discovery Rules
+ name: discovery_rule
+ namespace: ''
+ release_date: '2022-08-22'
+ 3.6.0:
+ modules:
+ - description: Manage repository content exports
+ name: content_export_repository
+ namespace: ''
+ - description: Manage content view version content exports
+ name: content_export_version
+ namespace: ''
+ release_date: '2022-09-01'
+ 3.7.0:
+ changes:
+ bugfixes:
+ - Properly use FQCN notation when redirecting the old ``foreman_*`` and ``katello_*``
+ module names. (https://github.com/theforeman/foreman-ansible-modules/issues/1484)
+ - convert2rhel role - Content views for activation keys (https://bugzilla.redhat.com/2118790)
+ minor_changes:
+ - repository - add support for ``include_tags`` and ``exclude_tags`` parameters
+ for Katello 4.4+
+ - subscription_manifest - increase the import timeout to 10 minutes (https://github.com/theforeman/foreman-ansible-modules/issues/1474)
+ - sync_plans role - document the ``enabled`` parameter (https://github.com/theforeman/foreman-ansible-modules/issues/1477)
+ - sync_plans role - expose the ``state`` parameter of the underlying module,
+ thus allowing to delete plans (https://github.com/theforeman/foreman-ansible-modules/issues/1477)
+ fragments:
+ - 1474-subscription_manifest-timeout.yaml
+ - 1477-sync_plans-enhancements.yml
+ - 1484-redirect-fqcn.yaml
+ - bz2118790.yml
+ - repository-include-exclude-tags.yaml
+ release_date: '2022-10-05'
+ 3.8.0:
+ changes:
+ bugfixes:
+ - activation_key - properly fetch *all* repositories when managing content overrides
+ (https://bugzilla.redhat.com/show_bug.cgi?id=2134605)
+ - redhat_manifest - properly report http errors (https://github.com/theforeman/foreman-ansible-modules/issues/1497)
+ - repository_sync - report an error instead of syncing the whole product when
+ the repository could not be found
+ minor_changes:
+ - job_template - add ``default`` option to the ``template_inputs`` parameter
+ - location, organization - add ``ignore_types`` parameter to adjust automatic
+ association of resources
+ - redhat_manifest - Search by UUID on the server side if UUID is known. This
+ is faster and allows fetching of manifest in big accounts (>1000 allocations).
+ - redhat_manifest - return the UUID of the manifest so it can be reused later
+ - redhat_manifest - set default ``quantity`` to 1 (https://github.com/theforeman/foreman-ansible-modules/pull/1499)
+ fragments:
+ - 1497-redhat_manifest-report-error.yml
+ - 1509-taxonomy-ignore_types.yaml
+ - 1519-job_template-input-default.yml
+ - 2134605-ak-product_contents-per_page.yml
+ - manifest-uuid-return.yml
+ - redhat_manifest-default_quantity.yml
+ - redhat_manifest-uuid-search.yml
+ - repository_sync-no-failsaife-repository.yml
+ modules:
+ - description: Fetch information about Foreman Snapshots
+ name: snapshot_info
+ namespace: ''
+ release_date: '2022-12-20'
+ 3.9.0:
+ changes:
+ bugfixes:
+ - content_export_* - increase task timeout to 12h as export tasks can be time
+ intensive (https://bugzilla.redhat.com/show_bug.cgi?id=2162678)
+ fragments:
+ - bz2162678-content_export-timeout.yaml
+ modules:
+ - description: Fetch information about a Content View Filter
+ name: content_view_filter_info
+ namespace: ''
+ - description: Manage content view filter rules
+ name: content_view_filter_rule
+ namespace: ''
+ - description: Fetch information about a Content View Filter Rule
+ name: content_view_filter_rule_info
+ namespace: ''
+ - description: Get information about hostgroup(s)
+ name: hostgroup_info
+ namespace: ''
+ release_date: '2023-02-20'
diff --git a/ansible_collections/theforeman/foreman/meta/execution-environment.yml b/ansible_collections/theforeman/foreman/meta/execution-environment.yml
new file mode 100644
index 00000000..07299e7b
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/meta/execution-environment.yml
@@ -0,0 +1,12 @@
+---
+version: 1
+
+build_arg_defaults:
+ EE_BASE_IMAGE: 'quay.io/ansible/ansible-runner:latest'
+
+dependencies:
+ # File paths are relative to collection root folder when collection is
+ # installed but relative to meta/ when it is built, so we need symlink magic
+ # See https://github.com/ansible/ansible-builder/issues/406#issuecomment-1195584188
+ python: requirements.txt
+ system: bindep.txt
diff --git a/ansible_collections/theforeman/foreman/meta/runtime.yml b/ansible_collections/theforeman/foreman/meta/runtime.yml
new file mode 100644
index 00000000..91fc657f
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/meta/runtime.yml
@@ -0,0 +1,188 @@
+---
+action_groups:
+ foreman:
+ - activation_key
+ - architecture
+ - auth_source_ldap
+ - bookmark
+ - compute_attribute
+ - compute_profile
+ - compute_resource
+ - config_group
+ - content_credential
+ - content_export_info
+ - content_export_library
+ - content_export_repository
+ - content_export_version
+ - content_upload
+ - content_view
+ - content_view_filter
+ - content_view_filter_info
+ - content_view_filter_rule
+ - content_view_filter_rule_info
+ - content_view_info
+ - content_view_version
+ - content_view_version_info
+ - discovery_rule
+ - domain
+ - domain_info
+ - external_usergroup
+ - global_parameter
+ - hardware_model
+ - host
+ - host_collection
+ - host_errata_info
+ - host_info
+ - host_power
+ - hostgroup
+ - hostgroup_info
+ - http_proxy
+ - image
+ - installation_medium
+ - job_invocation
+ - job_template
+ - lifecycle_environment
+ - location
+ - operatingsystem
+ - organization
+ - organization_info
+ - os_default_template
+ - partition_table
+ - product
+ - provisioning_template
+ - puppet_environment
+ - puppetclasses_import
+ - realm
+ - repository
+ - repository_info
+ - repository_set
+ - repository_set_info
+ - repository_sync
+ - resource_info
+ - role
+ - scap_content
+ - scap_tailoring_file
+ - scc_account
+ - scc_product
+ - setting
+ - setting_info
+ - smart_class_parameter
+ - smart_proxy
+ - snapshot
+ - snapshot_info
+ - status_info
+ - subnet
+ - subnet_info
+ - subscription_info
+ - subscription_manifest
+ - sync_plan
+ - templates_import
+ - user
+ - usergroup
+plugin_routing:
+ modules:
+ foreman_architecture:
+ redirect: theforeman.foreman.architecture
+ foreman_auth_source_ldap:
+ redirect: theforeman.foreman.auth_source_ldap
+ foreman_bookmark:
+ redirect: theforeman.foreman.bookmark
+ foreman_compute_attribute:
+ redirect: theforeman.foreman.compute_attribute
+ foreman_compute_profile:
+ redirect: theforeman.foreman.compute_profile
+ foreman_compute_resource:
+ redirect: theforeman.foreman.compute_resource
+ foreman_config_group:
+ redirect: theforeman.foreman.config_group
+ foreman_domain:
+ redirect: theforeman.foreman.domain
+ foreman_environment:
+ redirect: theforeman.foreman.puppet_environment
+ foreman_external_usergroup:
+ redirect: theforeman.foreman.external_usergroup
+ foreman_global_parameter:
+ redirect: theforeman.foreman.global_parameter
+ foreman_host:
+ redirect: theforeman.foreman.host
+ foreman_host_power:
+ redirect: theforeman.foreman.host_power
+ foreman_hostgroup:
+ redirect: theforeman.foreman.hostgroup
+ foreman_image:
+ redirect: theforeman.foreman.image
+ foreman_installation_medium:
+ redirect: theforeman.foreman.installation_medium
+ foreman_job_template:
+ redirect: theforeman.foreman.job_template
+ foreman_location:
+ redirect: theforeman.foreman.location
+ foreman_model:
+ redirect: theforeman.foreman.hardware_model
+ foreman_operatingsystem:
+ redirect: theforeman.foreman.operatingsystem
+ foreman_organization:
+ redirect: theforeman.foreman.organization
+ foreman_os_default_template:
+ redirect: theforeman.foreman.os_default_template
+ foreman_provisioning_template:
+ redirect: theforeman.foreman.provisioning_template
+ foreman_ptable:
+ redirect: theforeman.foreman.partition_table
+ foreman_realm:
+ redirect: theforeman.foreman.realm
+ foreman_role:
+ redirect: theforeman.foreman.role
+ foreman_scap_content:
+ redirect: theforeman.foreman.scap_content
+ foreman_scap_tailoring_file:
+ redirect: theforeman.foreman.scap_tailoring_file
+ foreman_scc_account:
+ redirect: theforeman.foreman.scc_account
+ foreman_scc_product:
+ redirect: theforeman.foreman.scc_product
+ foreman_search_facts:
+ redirect: theforeman.foreman.resource_info
+ foreman_setting:
+ redirect: theforeman.foreman.setting
+ foreman_smart_class_parameter:
+ redirect: theforeman.foreman.smart_class_parameter
+ foreman_snapshot:
+ redirect: theforeman.foreman.snapshot
+ foreman_subnet:
+ redirect: theforeman.foreman.subnet
+ foreman_templates_import:
+ redirect: theforeman.foreman.templates_import
+ foreman_user:
+ redirect: theforeman.foreman.user
+ foreman_usergroup:
+ redirect: theforeman.foreman.usergroup
+ katello_activation_key:
+ redirect: theforeman.foreman.activation_key
+ katello_content_credential:
+ redirect: theforeman.foreman.content_credential
+ katello_content_view:
+ redirect: theforeman.foreman.content_view
+ katello_content_view_filter:
+ redirect: theforeman.foreman.content_view_filter
+ katello_content_view_version:
+ redirect: theforeman.foreman.content_view_version
+ katello_host_collection:
+ redirect: theforeman.foreman.host_collection
+ katello_lifecycle_environment:
+ redirect: theforeman.foreman.lifecycle_environment
+ katello_manifest:
+ redirect: theforeman.foreman.subscription_manifest
+ katello_product:
+ redirect: theforeman.foreman.product
+ katello_repository:
+ redirect: theforeman.foreman.repository
+ katello_repository_set:
+ redirect: theforeman.foreman.repository_set
+ katello_sync:
+ redirect: theforeman.foreman.repository_sync
+ katello_sync_plan:
+ redirect: theforeman.foreman.sync_plan
+ katello_upload:
+ redirect: theforeman.foreman.content_upload
+requires_ansible: '>=2.9'
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()
diff --git a/ansible_collections/theforeman/foreman/requirements.txt b/ansible_collections/theforeman/foreman/requirements.txt
new file mode 100644
index 00000000..bcdcbab8
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/requirements.txt
@@ -0,0 +1,3 @@
+requests>=2.4.2
+ipaddress; python_version < '3.3'
+PyYAML
diff --git a/ansible_collections/theforeman/foreman/roles/activation_keys/README.md b/ansible_collections/theforeman/foreman/roles/activation_keys/README.md
new file mode 100644
index 00000000..4b04c593
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/activation_keys/README.md
@@ -0,0 +1,79 @@
+theforeman.foreman.activation_keys
+==================================
+
+This role creates and manages Activation Keys.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_activation_keys`. Each `activation_key` requires the following fields:
+
+- `name`: The name of the activation key.
+
+The following fields are required for an activation key but have defaults which make them optional for this role:
+
+- `lifecycle_environment`: Lifecycle Environment to assign to hosts registered with this activation key. Defaults to "Library".
+- `content_view`: Content View to assign to hosts registered with this activation key. Defaults to "Default Organization View".
+
+The following fields are optional in the sense that the server will use default values when they are omitted:
+
+- `auto_attach`: Auto Attach behavior for the activation key. When true, it will attempt to attach a minimum of subscriptions (from the subset of assigned subscriptions on the activation key; selects from all subscriptions in the organization if none are assigned) to cover any present products on the host. When false, it will attempt to attach all subscriptions assigned on the activation key to the host at registration time. server defaults to true.
+- `unlimited_hosts`: Allow an unlimited number of hosts to register with the activation key when true. When false, the `max_hosts` parameter which sets a numerical limit on the nnumber of hosts that can be registered becomes required. server defaults to true.
+
+The following fields are optional and will be omitted by default:
+
+- `description`: Description of the activation key. Helpful for other users to find which activation key to use.
+- `host_collections`: List of Host Collections to associate with the activation key.
+- `subscriptions`: List of Subscriptions to associate with the activation key. Each Subscription is required to have one of `name`, `pool_id`, or `upstream_pool_id`. Of these, only the `pool_id` is guaranteed to be unique. `upstream_pool_id` only exists for subscriptions imported from a 3rd party organization (e.g. on a Red Hat Subscription Manifest). When uniqueness is not an issue, `name` or `upstream_pool_id` can be easier to work with since the `pool_id` does not get determined until the subscription is imported or created and therefore may not yet be determined when you are writing playbooks.
+- `content_overrides`: List of Content Overrides for the activation key. Each Content Override is required to have a `label` which refers to a repository and `override` which refers to one of the states enabled, disabled, or default.
+- `release_version`: Release Version to set when registering hosts with the activation key.
+- `service_level`: Service Level to set when registering hosts with the activation key. Premium, Standard, or Self-Support. This will limit Subscriptions available to hosts to those matching this service level.
+- `purpose_usage`: System Purpose Usage to set when registering hosts with the activation key. Production, Development/Test, Disaster Recovery. When left unset this will not set System Purpose Usage on registering hosts. This should only be used when it is supported by the OS of registering hosts (RHEL 8 only at the time of writing).
+- `purpose_role`: System Purpose Role to set when registering hosts with the activation key. Red Hat Enterprise Linux Server, Red Hat Enterprise Linux Workstation, Red Hat Enterprise Linux Compute Node. When left unset this will not set System Purpose Role on registering hosts. This should only be used when it is supported by the OS of registering hosts (RHEL 8 only at the time of writing).
+- `purpose_addons`: List of System Purpose Addons (ELS, EUS) to set on registering hosts. This should only be used when it is supported by the OS of registering hosts (RHEL 8 only at the time of writing).
+
+A helpful behavior to keep in mind when creating activation keys is that a host can register with multiple activation keys; each activation key will attach subscriptions according to its own logic, in the order that the activation keys are listed. Host attributes like Lifecycle Environment, Content View, etc will be overwritten by later activation keys so that the last activation key listed wins. A common pattern is to first use an activation key which has auto-attach disabled and a list of subscriptions to attach for any applicable custom products, followed by a second activation key which has auto attach enabled to attach the best fitting subscription(s) for the OS and any remaining products which were not already covered, and also defines the LCE, Content View, and other host attributes as required.
+
+Example Playbooks
+-----------------
+
+Create a basic Activation Key that uses Library LCE, Default Organization View, and performs auto-attach from the set of all available Subscriptions (i.e. auto-attach=true and no Subscriptions are assigned to the Activation Key).
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.activation_keys
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_activation_keys:
+ - name: "Basic Activation Key"
+ description: "Registers hosts in Library/Default Organization View and tries to attach the best fitting subscription(s) from all available in the organization"
+```
+
+Define two Activation Keys. The first registers hosts in the "ACME" organization and attaches the Subscription for the custom product "ACME_App". The second assigns the "Test" LCE and "RHEL7_Base" Content View, and auto-attaches the best fitting subscription(s) from all which are available in the ACME Organization:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.activation_keys
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_activation_keys:
+ - name: "ACME_App_Key"
+ auto_attach: false
+ subscriptions:
+ - name: "ACME_App"
+ - name: "ACME_RHEL7_Base_Test"
+ lifecycle_environment: "Test"
+ content_view: "RHEL7_Base"
+```
+
+Following the second example, a Host which is registered using `subscription-manager register --activationkey ACME_App_Key,ACME_RHEL7_Base_Test` will get the ACME_App subscription, Test LCE, RHEL7_Base Content View, and auto-attach any additional necessary subscriptions from ACME Organization to cover the Base OS and any other products which require an entitlement certificate.
diff --git a/ansible_collections/theforeman/foreman/roles/activation_keys/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/activation_keys/tasks/main.yml
new file mode 100644
index 00000000..787c2ae8
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/activation_keys/tasks/main.yml
@@ -0,0 +1,26 @@
+---
+- name: 'Create Activation Keys' # noqa: args[module]
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ description: "{{ item.description | default(omit) }}"
+ lifecycle_environment: "{{ item.lifecycle_environment | default('Library') }}"
+ content_view: "{{ item.content_view | default('Default Organization View') }}"
+ host_collections: "{{ item.host_collections | default(omit) }}"
+ subscriptions: "{{ item.subscriptions | default(omit) }}"
+ content_overrides: "{{ item.content_overrides | default(omit) }}"
+ auto_attach: "{{ item.auto_attach | default(omit) }}"
+ unlimited_hosts: "{{ item.unlimited_hosts | default(omit) }}"
+ max_hosts: "{{ item.max_hosts | default(omit) }}"
+ release_version: "{{ item.release_version | default(omit) }}"
+ service_level: "{{ item.service_level | default(omit) }}"
+ purpose_usage: "{{ item.purpose_usage | default(omit) }}"
+ purpose_role: "{{ item.purpose_role | default(omit) }}"
+ purpose_addons: "{{ item.purpose_addons | default(omit) }}"
+ state: present
+ with_items:
+ - "{{ foreman_activation_keys }}"
diff --git a/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/README.md b/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/README.md
new file mode 100644
index 00000000..2a737c38
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/README.md
@@ -0,0 +1,77 @@
+theforeman.foreman.auth_sources_ldap
+====================================
+
+This role manages LDAP authentication sources, allowing users from an external source such as Active Directory or
+FreeIPA to authenticate to Foreman.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+Role specific variables should be documented as below:
+
+The main data structure for this role is the list of `foreman_auth_sources_ldap`. Each `auth_source_ldap` requires the following fields:
+
+- `name`: The name of the authentication source.
+
+For all other fields see the `auth_source_ldap` module.
+
+Example Playbooks
+-----------------
+
+Configure FreeIPA as an authentication source, with automatic registration:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.auth_sources_ldap
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_auth_sources_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
+ state: present
+```
+
+To instead integrate with Active Directory, only allowing users who are member of the "Domain Users" group:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.auth_sources_ldap
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_auth_sources_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)
+ state: present
+```
diff --git a/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/tasks/main.yml
new file mode 100644
index 00000000..855a15fe
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/auth_sources_ldap/tasks/main.yml
@@ -0,0 +1,27 @@
+---
+- name: 'Create LDAP Authentication Source'
+ theforeman.foreman.auth_source_ldap:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ account: "{{ item.account }}"
+ account_password: "{{ item.account_password }}"
+ base_dn: "{{ item.base_dn | default(omit) }}"
+ attr_login: "{{ item.attr_login | default(omit) }}"
+ attr_firstname: "{{ item.attr_firstname | default(omit) }}"
+ attr_lastname: "{{ item.attr_lastname | default(omit) }}"
+ attr_mail: "{{ item.attr_mail | default(omit) }}"
+ attr_photo: "{{ item.attr_photo | default(omit) }}"
+ onthefly_register: "{{ item.onthefly_register | default(omit) }}"
+ usergroup_sync: "{{ item.usergroup_sync | default(omit) }}"
+ tls: "{{ item.tls | default(omit) }}"
+ groups_base: "{{ item.groups_base | default(omit) }}"
+ host: "{{ item.host | default(omit) }}"
+ port: "{{ item.port | default(omit) }}"
+ server_type: "{{ item.server_type | default(omit) }}"
+ ldap_filter: "{{ item.ldap_filter | default(omit) }}"
+ use_netgroups: "{{ item.use_netgroups | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_auth_sources_ldap }}"
diff --git a/ansible_collections/theforeman/foreman/roles/compute_profiles/README.md b/ansible_collections/theforeman/foreman/roles/compute_profiles/README.md
new file mode 100644
index 00000000..bc8a1e59
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/compute_profiles/README.md
@@ -0,0 +1,54 @@
+theforeman.foreman.compute_profiles
+===================================
+
+This role creates and manages Compute Profiles.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_compute_profiles`. Each `compute_profile` requires the following fields:
+
+- `name`: The name of the compute profile.
+
+The following fields are optional and will be omitted by default:
+
+- `description`: Description of the compute profile
+- `compute_attributes`: List of attributes for the profile on specific compute resources.
+
+Example Playbooks
+-----------------
+
+Create a compute profile named `1-Small` with a VMware spec of 1 single core CPU, 2 GiB of memory, 15 GiB of disk, and a VMXNET3 network card connected to `VM Network`:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.compute_profiles
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_compute_profiles:
+ - name: "1-Small"
+ compute_attributes:
+ - compute_resource: "VMware"
+ vm_attrs:
+ cluster: "cluster01"
+ path: /Datacenters/ha-datacenter/vm/
+ memoryHotAddEnabled: true
+ cpuHotAddEnabled: true
+ cpus: 1
+ corespersocket: 1
+ memory_mb: 2048
+ volumes_attributes:
+ 0:
+ datastore: "datastore1"
+ size_gb: 15
+ interfaces_attributes:
+ 0:
+ type: "VirtualVmxnet3"
+ network: "VM Network"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/compute_profiles/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/compute_profiles/tasks/main.yml
new file mode 100644
index 00000000..6b51c75f
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/compute_profiles/tasks/main.yml
@@ -0,0 +1,11 @@
+---
+- name: 'Create Compute Profiles' # noqa: args[module]
+ theforeman.foreman.compute_profile:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ compute_attributes: "{{ item.compute_attributes | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_compute_profiles }}"
diff --git a/ansible_collections/theforeman/foreman/roles/compute_resources/README.md b/ansible_collections/theforeman/foreman/roles/compute_resources/README.md
new file mode 100644
index 00000000..cc12c82d
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/compute_resources/README.md
@@ -0,0 +1,53 @@
+theforeman.foreman.compute_resources
+====================================
+
+This role creates and manages Compute Resources.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_compute_resources`. Each `compute_resource` requires the following fields:
+
+- `name`: The name of the compute resource.
+
+The following fields are optional and will be omitted by default:
+
+- `description`: Description of the compute resource
+- `provider`: Compute resource provider. Required if *state=present_with_defaults*.
+- `provider_params`: Parameter specific to compute resource provider. Required if *state=present_with_defaults*.
+
+Each `compute_resource` can also list a number of `images` associated with the compute resource.
+
+Example Playbooks
+-----------------
+
+Create a compute resource for vSphere, with a single image for RHEL 8.4.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.compute_resources
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_compute_resources:
+ - name: "VMware"
+ provider: "vmware"
+ provider_params:
+ url: "vcenter.example.com"
+ user: "administrator@vsphere.local"
+ password: "changeme"
+ datacenter: "ha-datacenter"
+ images:
+ - name: "RHEL-8.4"
+ operatingsystem: "RedHat-8.4"
+ architecture: "x86_64"
+ user_data: true
+ image_username: "root"
+ image_password: "changeme"
+ uuid: "Templates/rhel-8.4-template"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/compute_resources/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/compute_resources/tasks/main.yml
new file mode 100644
index 00000000..9e3c067e
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/compute_resources/tasks/main.yml
@@ -0,0 +1,32 @@
+---
+- name: 'Create Compute Resources' # noqa: args[module]
+ theforeman.foreman.compute_resource:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ description: "{{ item.description | default(omit) }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ locations: "{{ item.locations | default(omit) }}"
+ provider: "{{ item.provider | default(omit) }}"
+ provider_params: "{{ item.provider_params | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_compute_resources }}"
+
+- name: 'Create Images'
+ theforeman.foreman.image:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.1.name }}"
+ architecture: "{{ item.1.architecture | default(omit) }}"
+ compute_resource: "{{ item.0.name | default(omit) }}"
+ image_password: "{{ item.1.image_password | default(omit) }}"
+ image_username: "{{ item.1.image_username | default(omit) }}"
+ operatingsystem: "{{ item.1.operatingsystem | default(omit) }}"
+ user_data: "{{ item.1.user_data | default(omit) }}"
+ uuid: "{{ item.1.uuid | default(omit) }}"
+ state: "{{ item.1.state | default('present') }}"
+ loop: "{{ foreman_compute_resources | subelements('images', {'skip_missing': True}) }}"
diff --git a/ansible_collections/theforeman/foreman/roles/content_credentials/README.md b/ansible_collections/theforeman/foreman/roles/content_credentials/README.md
new file mode 100644
index 00000000..6f2f68d4
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_credentials/README.md
@@ -0,0 +1,107 @@
+theforeman.foreman.content_credentials
+=======================================
+
+This role defines Content Credentials.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+- `foreman_content_credentials`: List of content credentials to create. Each content credential is represented as a dictionary which specifies the `name`, `content_type` (which can be `gpg_key` or `cert`) and `content` of the content credential.
+
+```yaml
+foreman_content_credentials:
+ - name: RPM-GPG-KEY-foreman
+ content_type: gpg_key
+ content: "{{ lookup('url', 'https://yum.theforeman.org/releases/latest/RPM-GPG-KEY-foreman', split_lines=False) }}"
+ - name: RPM-GPG-KEY-my-repo
+ content_type: gpg_key
+ content: "{{ lookup('file', '/etc/pki/rpm-gpg/RPM-GPG-KEY-my-repo') }}"
+ - name: RPM-GPG-KEY-my-repo2
+ content_type: gpg_key
+ content: |
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ mQINBGAX2bIBEADuTGNExTEST0hOcpJ13XS1BEwuhzo7r16QaI0hP1vRxZeLJgeC
+ b2KWRvHHfepr2jdAoAeOVhERrMz5EpMcgPEs7NUE+vbYr+K9LFzw5gmUC00CCuQ+
+ RCJRRXYNV8F41y4dTGOkE/ON52ljDvVyFb3DbUUYPH9ZfOE0Z6kMIcJo6eYsDAdK
+ EjoQ1jQkVaRa8I4+YZ9XEFkPqVUkY1+tMfipqqQuNbvN2xgQSk8dc6uEouyC8FBA
+ GPugplbCaEZNFWt48xQU9vP1JblQ6z9cynLKFxxWkgr9DKRRh1kw2pIQyGhl1RhI
+ uvedY9OeJlqxuBsBvko7JULcX622HcHUkhzQD+ss0L9nE3lZuO5ywpZdTYln296E
+ 7awNEr0ER9Xqx9pMp5JeXNSHjlleFN01vLG5Xa7WNc32fvDtn2JhkzTU/dlIA2F+
+ w5Tlg5ROY8olWc+jHKmvTQwxZ9s9XQuHmBpNbOijHg4Ekr0TGo6d3rjHZKiisZBG
+ mAbHe1pWLOmeRpjqc6xmIpDMrsx75U0WgkwjBtbfxUcDYEzzJOcO87Q3s8kH+ie3
+ 5eSClT7coImWUmVKIoFSvxj8JgUT6P81v7CW4AlVDpRjBtYmc82NsGuSEgAykuQo
+ VRguqU/w3QTU3rEcWfLVmyfyEKC4tBUCAhGShii/rLrtCspBT+uVpcDkQwARAQAB
+ tD1Gb3JlbWFuIEF1dG9tYXRpYyBTaWduaW5nIEtleSAoMi40KSA8cGFja2FnZXNA
+ dGhlZm9yZW1hbi5vcmc+iQJUBBMBCAA+FiEEZDJT9xuCsb6vLh1PpDm9Vawq2fEF
+ AmAX2bICGy8FCQHhM4AFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQpDm9Vawq
+ 2fHMBQ//ffbTmU0Bl9Im8dDCzebhO6/D3iyshoceAfUjJrwvuhzSlil2cjWiLdmP
+ CjBhUB8eDRhSQ+LlfJe9C0PKEyC72rzTsfUZj4NBKNQGT2P+peJ1l8PUAAlk7jZl
+ QZcDER9Nju7/d+VTqF6PXkcbnIo1GVD/EX+R9mKphIbu9qaxBqGhCVay1D7jNxzH
+ OBaMse5hf1hJ0WzcyK6pRLMU9JeuLEdhwJqSP0+/E8R31El92EO1+selLy6hD3ro
+ NX3iehtcQVKdQ/5rflP6K7ZbDDj76lgRBbOY+UT1tft1nvdgKIoRPMqlBc2tMLNT
+ jzJrw/AW7C9pRUTvox2uFKw0Eo/0pnSR4qllBCGE67VpJLXeMQFjwOLcaKX57civ
+ X1z7nGTg4K+Ye5BM33Pq0Df24M0qLeqD6vLhB0Ny2JFiivw4zWJu448RELb1Omai
+ aNipdHQDN8D345mjctUDcc/2T7q6bcu5ErrFT8GK/FPdwpgDIPN20gxEMR9vG83n
+ AMkzSNrMefNlJoyTdgthokPb99LmN6Foybk6VNoKy4u/mID6uprWGMIl1/LX2wu1
+ xRxRy1YznHnmtGqTYOikyAp0e+4tDfHMZ58yC9/XGztxJvj6vvwwf9n5ZO4MC4Kj
+ XQVHErcrTa8cZWW87pLrNvILegPA6v778BV0GLV5PqnWhl9Y1sY=
+ =SrzP
+ -----END PGP PUBLIC KEY BLOCK-----
+
+```
+
+Example Playbooks
+-----------------
+
+Create two content credentials:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_credentials
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_content_credentials:
+ - name: RPM-GPG-KEY-foreman
+ content_type: gpg_key
+ content: "{{ lookup('url', 'https://yum.theforeman.org/releases/latest/RPM-GPG-KEY-foreman', split_lines=False) }}"
+ - name: RPM-GPG-KEY-my-repo
+ content_type: gpg_key
+ content: "{{ lookup('file', '/etc/pki/rpm-gpg/RPM-GPG-KEY-my-repo') }}"
+ - name: RPM-GPG-KEY-my-repo2
+ content_type: gpg_key
+ content: |
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
+ mQINBGAX2bIBEADuTGNExTEST0hOcpJ13XS1BEwuhzo7r16QaI0hP1vRxZeLJgeC
+ b2KWRvHHfepr2jdAoAeOVhERrMz5EpMcgPEs7NUE+vbYr+K9LFzw5gmUC00CCuQ+
+ RCJRRXYNV8F41y4dTGOkE/ON52ljDvVyFb3DbUUYPH9ZfOE0Z6kMIcJo6eYsDAdK
+ EjoQ1jQkVaRa8I4+YZ9XEFkPqVUkY1+tMfipqqQuNbvN2xgQSk8dc6uEouyC8FBA
+ GPugplbCaEZNFWt48xQU9vP1JblQ6z9cynLKFxxWkgr9DKRRh1kw2pIQyGhl1RhI
+ uvedY9OeJlqxuBsBvko7JULcX622HcHUkhzQD+ss0L9nE3lZuO5ywpZdTYln296E
+ 7awNEr0ER9Xqx9pMp5JeXNSHjlleFN01vLG5Xa7WNc32fvDtn2JhkzTU/dlIA2F+
+ w5Tlg5ROY8olWc+jHKmvTQwxZ9s9XQuHmBpNbOijHg4Ekr0TGo6d3rjHZKiisZBG
+ mAbHe1pWLOmeRpjqc6xmIpDMrsx75U0WgkwjBtbfxUcDYEzzJOcO87Q3s8kH+ie3
+ 5eSClT7coImWUmVKIoFSvxj8JgUT6P81v7CW4AlVDpRjBtYmc82NsGuSEgAykuQo
+ VRguqU/w3QTU3rEcWfLVmyfyEKC4tBUCAhGShii/rLrtCspBT+uVpcDkQwARAQAB
+ tD1Gb3JlbWFuIEF1dG9tYXRpYyBTaWduaW5nIEtleSAoMi40KSA8cGFja2FnZXNA
+ dGhlZm9yZW1hbi5vcmc+iQJUBBMBCAA+FiEEZDJT9xuCsb6vLh1PpDm9Vawq2fEF
+ AmAX2bICGy8FCQHhM4AFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQpDm9Vawq
+ 2fHMBQ//ffbTmU0Bl9Im8dDCzebhO6/D3iyshoceAfUjJrwvuhzSlil2cjWiLdmP
+ CjBhUB8eDRhSQ+LlfJe9C0PKEyC72rzTsfUZj4NBKNQGT2P+peJ1l8PUAAlk7jZl
+ QZcDER9Nju7/d+VTqF6PXkcbnIo1GVD/EX+R9mKphIbu9qaxBqGhCVay1D7jNxzH
+ OBaMse5hf1hJ0WzcyK6pRLMU9JeuLEdhwJqSP0+/E8R31El92EO1+selLy6hD3ro
+ NX3iehtcQVKdQ/5rflP6K7ZbDDj76lgRBbOY+UT1tft1nvdgKIoRPMqlBc2tMLNT
+ jzJrw/AW7C9pRUTvox2uFKw0Eo/0pnSR4qllBCGE67VpJLXeMQFjwOLcaKX57civ
+ X1z7nGTg4K+Ye5BM33Pq0Df24M0qLeqD6vLhB0Ny2JFiivw4zWJu448RELb1Omai
+ aNipdHQDN8D345mjctUDcc/2T7q6bcu5ErrFT8GK/FPdwpgDIPN20gxEMR9vG83n
+ AMkzSNrMefNlJoyTdgthokPb99LmN6Foybk6VNoKy4u/mID6uprWGMIl1/LX2wu1
+ xRxRy1YznHnmtGqTYOikyAp0e+4tDfHMZ58yC9/XGztxJvj6vvwwf9n5ZO4MC4Kj
+ XQVHErcrTa8cZWW87pLrNvILegPA6v778BV0GLV5PqnWhl9Y1sY=
+ =SrzP
+ -----END PGP PUBLIC KEY BLOCK-----
+```
diff --git a/ansible_collections/theforeman/foreman/roles/content_credentials/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/content_credentials/tasks/main.yml
new file mode 100644
index 00000000..8cce34ae
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_credentials/tasks/main.yml
@@ -0,0 +1,12 @@
+---
+- name: 'Create Content Crendentials'
+ theforeman.foreman.content_credential:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ content_type: "{{ item.content_type }}"
+ content: "{{ item.content }}"
+ with_items: "{{ foreman_content_credentials | default([]) }}"
diff --git a/ansible_collections/theforeman/foreman/roles/content_rhel/README.md b/ansible_collections/theforeman/foreman/roles/content_rhel/README.md
new file mode 100644
index 00000000..f41514b9
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_rhel/README.md
@@ -0,0 +1,126 @@
+theforeman.foreman.content_rhel
+===============================
+
+This role is an opinionated reuse of other roles in the collection, which creates a basic configuration for everything needed to register and patch existing RHEL clients.
+
+That includes uploading a subscription manifest to an organization; enabling base RHEL7 and RHEL8 repositories (x86_64 architecture), syncing them immediately, and creating a sync plan for future syncs; and creating an activation key `base_rhel_key` to use when registering RHEL clients.
+
+The subscription manifest will be retrieved from the specified path on the Ansible target host; optionally, it can be fetched first from the RHSM portal using the provided login credentials and manifest UUID. It will be uploaded to the specified organization.
+
+By default, the role enables the rhel-7-server-rpms repository with the 7Server release and x86_64 architecture, as well as rhel-8-for-x86_64-baseos-rpms and rhel-8-for-x86_64-appstream-rpms. The manifest must provide access to all enabled content for the role to work properly.
+
+The role creates a sync plan using any of the sync plan intervals supported by the basic [Sync Plan Role](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/sync_plans/README.md).
+
+The role creates an activation key with the provided name. This activation key will register client systems in the "Library" lifecycle environment and "Default Organization View" content view, using the subscription auto-attach feature.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+This role supports the same variables used in the [Manifest Role](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/manifest/README.md#role-variables).
+
+It also supports customizing the included roles with:
+
+`foreman_sync_plan_name`: Name of the sync plan to create. Default 'RHEL Sync Plan'
+
+`foreman_sync_plan_interval`: 'hourly', 'daily', 'weekly', or 'custom cron'. See the [Sync Plan Role Documentation](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/manifest/README.md#role-variables) for more information. Default 'daily'
+
+`foreman_sync_plan_cron_expression`: Required when using the 'custom cron' `sync_plan_interval`.
+
+`foreman_sync_plan_sync_date`: Initial sync date for the sync plan, formatted as 'YYYY-MM-DD HH:MM:SS UTC'.
+
+`foreman_activation_key_name`: Name of the activation key to create. Default 'base_rhel_key'
+
+Repository behavior is controlled via the variables:
+
+`foreman_content_rhel_enable_rhel7`: Enable rhel-7-server-rpms repository (x86 architecture and 7Server release). Default true.
+
+`foreman_content_rhel_enable_rhel8`: Enable rhel-8-for-x86_64-baseos-rpms and rhel-8-for-x86_64-appstream-rpms (x86 architecture). Default true.
+
+`foreman_content_rhel_rhel8_releasever`: Version of RHEL 8 repositories. Default `8`.
+
+`foreman_content_rhel_sync_now`: Sync repositories immediately after enabling. Default true.
+
+`foreman_content_rhel_wait_for_syncs`: Monitor status of sync tasks. When false, the sync tasks will continue running in the background after the playbook has finished running. This option is most useful when other automation (for example, registering and patching a client) requires the repository syncs to have completed. Default true.
+
+Example Playbooks
+-----------------
+
+This minimal example assumes the manifest has already been downloaded to ~/manifest.zip on localhost (the Ansible control node) and uploads that manifest to the ACME organization. It enables RHEL7 and RHEL8 repositories, creates the role default sync plan for them, and also syncs the repositories immediately. It creates an activation key with the role default name `base_rhel_key`.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_rhel
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_manifest_download: False
+ foreman_manifest_path: "~/manifest.zip"
+```
+
+This example is identical to the above example, except instead of assuming the manifest is already downloaded at ~/manifest.zip, we first use the provided rhsm_{username,password} and manifest_uuid to download it from the Red Hat Customer Portal.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_rhel
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_manifest_download: True
+ foreman_rhsm_username: "happycustomer"
+ foreman_rhsm_password: "$ecur3p4$$w0rd"
+ foreman_manifest_uuid: "01234567-89ab-cdef-0123-456789abcdef"
+ foreman_manifest_path: "~/manifest.zip"
+```
+
+This example downloads a manifest with the provided UUID from the RHSM portal using the provided credentials and copies it to ~/manifest.zip before uploading it to "Default Organization". It then enables the RHEL7 and RHEL8 repositories without syncing them immediately, but creates a sync_plan which syncs the repositories at midnight each day. It creates an activation key "RHEL_Key" to register existing RHEL content hosts.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_rhel
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_manifest_download: True
+ foreman_rhsm_username: "happycustomer"
+ foreman_rhsm_password: "$ecur3p4$$w0rd"
+ foreman_manifest_uuid: "01234567-89ab-cdef-0123-456789abcdef"
+ foreman_manifest_path: "~/manifest.zip"
+ foreman_content_rhel_sync_now: false
+ foreman_sync_plan_name: "Daily RHEL Sync"
+ foreman_sync_plan_interval: daily
+ foreman_sync_plan_sync_date: 2021-02-02 00:00:00 UTC
+ foreman_activation_key_name: "RHEL_Key"
+ foreman_content_rhel_rhel8_releasever: 8.4
+```
+
+This example assumes the manifest has already been downloaded to ~/my_subscription_manifesst.zip on localhost and uploads that manifest to the ACME organization. It enables the rhel-7-server-rpms repository only, syncs it immediately, and also creates a custom cron sync plan for it. It creates an activation key "RHEL_Key" to register existing RHEL content hosts.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_rhel
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "ACME"
+ foreman_manifest_download: False
+ foreman_manifest_path: "~/my_subscription_manifest.zip"
+ foreman_content_rhel_enable_rhel8: false
+ foreman_sync_plan_name: "RHEL Sync Plan"
+ foreman_sync_plan_interval: custom cron
+ foreman_sync_plan_cron_expression: 0 6 8 * *
+ foreman_sync_plan_sync_date: 2021-02-02 00:00:00 UTC
+ foreman_activation_key_name: "RHEL_Key"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/content_rhel/defaults/main.yml b/ansible_collections/theforeman/foreman/roles/content_rhel/defaults/main.yml
new file mode 100644
index 00000000..1ed1023e
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_rhel/defaults/main.yml
@@ -0,0 +1,6 @@
+---
+foreman_content_rhel_enable_rhel7: true
+foreman_content_rhel_enable_rhel8: true
+foreman_content_rhel_sync_now: true
+foreman_content_rhel_wait_for_syncs: true
+foreman_content_rhel_rhel8_releasever: 8
diff --git a/ansible_collections/theforeman/foreman/roles/content_rhel/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/content_rhel/tasks/main.yml
new file mode 100644
index 00000000..9f857700
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_rhel/tasks/main.yml
@@ -0,0 +1,95 @@
+---
+- name: "Subscription Manifest"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.manifest
+
+- name: "Enable RHEL7 repository"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: Red Hat Enterprise Linux Server
+ repository_sets:
+ - name: Red Hat Enterprise Linux 7 Server (RPMs)
+ basearch: x86_64
+ releasever: 7Server
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Enable RHEL8 repositories"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: Red Hat Enterprise Linux for x86_64
+ repository_sets:
+ - name: Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)
+ releasever: "{{ foreman_content_rhel_rhel8_releasever }}"
+ - name: Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)
+ releasever: "{{ foreman_content_rhel_rhel8_releasever }}"
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Sync RHEL7 repository"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ organization: "{{ foreman_organization }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs }}"
+ product: Red Hat Enterprise Linux Server
+ async: 14400
+ poll: 0
+ register: rhel7_sync
+ when: foreman_content_rhel_enable_rhel7 and foreman_content_rhel_sync_now
+
+- name: "Sync RHEL8 repositories"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ organization: "{{ foreman_organization }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs }}"
+ product: Red Hat Enterprise Linux for x86_64
+ async: 14400
+ poll: 0
+ register: rhel8_sync
+ when: foreman_content_rhel_enable_rhel8 and foreman_content_rhel_sync_now
+
+- name: "Create Sync Plan"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.sync_plans
+ vars:
+ foreman_sync_plans:
+ - name: "{{ foreman_sync_plan_name | default('RHEL Sync Plan') }}"
+ interval: "{{ foreman_sync_plan_interval | default('daily') }}"
+ cron_expression: "{{ foreman_sync_plan_cron_expression | default(omit) }}"
+ sync_date: "{{ foreman_sync_plan_sync_date | default('2020-01-01 00:00:00 UTC') }}"
+ products:
+ "{{ [foreman_content_rhel_enable_rhel7 | ternary('Red Hat Enterprise Linux Server', ''),
+ foreman_content_rhel_enable_rhel8 | ternary('Red Hat Enterprise Linux for x86_64', '')]
+ | select() | list }}"
+
+- name: "Create Activation Key"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.activation_keys
+ vars:
+ foreman_activation_keys:
+ - name: "{{ foreman_activation_key_name | default('base_rhel_key') }}"
+ description: "Generated by ansible role theforeman.foreman.content_rhel"
+
+- name: "Wait for RHEL7 sync completion" # noqa: args[module]
+ ansible.builtin.async_status:
+ jid: "{{ rhel7_sync.ansible_job_id }}"
+ register: rhel7_job_result
+ until: rhel7_job_result.finished
+ retries: 99999
+ delay: 10
+ when: foreman_content_rhel_enable_rhel7 and foreman_content_rhel_sync_now and foreman_content_rhel_wait_for_syncs
+
+- name: "Wait for RHEL8 sync completion" # noqa: args[module]
+ ansible.builtin.async_status:
+ jid: "{{ rhel8_sync.ansible_job_id }}"
+ register: rhel8_job_result
+ until: rhel8_job_result.finished
+ retries: 99999
+ delay: 10
+ when: foreman_content_rhel_enable_rhel8 and foreman_content_rhel_sync_now and foreman_content_rhel_wait_for_syncs
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_publish/README.md b/ansible_collections/theforeman/foreman/roles/content_view_publish/README.md
new file mode 100644
index 00000000..47b38bbb
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_publish/README.md
@@ -0,0 +1,30 @@
+theforeman.foreman.content_view_publish
+=======================================
+
+Publish a list of Content Views.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+### Required
+
+- `foreman_content_views`: List of content views to publish
+
+Example Playbook
+----------------
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_view_publish
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_content_views:
+ - RHEL 7 View
+ - RHEL 8 View
+```
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_publish/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/content_view_publish/tasks/main.yml
new file mode 100644
index 00000000..55680abb
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_publish/tasks/main.yml
@@ -0,0 +1,12 @@
+---
+- name: "Publish content views"
+ theforeman.foreman.content_view_version:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ content_view: "{{ content_view }}"
+ organization: "{{ foreman_organization }}"
+ loop: "{{ foreman_content_views }}"
+ loop_control:
+ loop_var: "content_view"
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/README.md b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/README.md
new file mode 100644
index 00000000..02bfea49
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/README.md
@@ -0,0 +1,44 @@
+theforeman.foreman.content_view_version_cleanup
+===============================================
+
+Clean up unused Content View Versions.
+
+This role will remove any unused versions of your Content Views and
+Composite Content Views.
+
+Unused versions are those that match the following criteria:
+* not published to any Lifecycle Environment
+* not published as part of any Composite Content View
+* not part of any Composite Content View Version
+
+This role will first clean Composite Content Views, to avoid leaving
+unused versions of regular Content Views behind.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+### Required
+
+- `foreman_content_view_version_cleanup_keep`: How many unused versions to keep.
+
+### Optional
+
+- `foreman_content_view_version_cleanup_search`: Limit the cleaned content views using a search string (example: `name ~ SOE`).
+ When using Composite Content Views, both the composite and the non-composite ones need to match this search to be properly cleaned up by this role.
+
+Example Playbook
+----------------
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_view_version_cleanup
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_content_view_version_cleanup_keep: 10
+```
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/delete_cv_versions.yml b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/delete_cv_versions.yml
new file mode 100644
index 00000000..5654126a
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/delete_cv_versions.yml
@@ -0,0 +1,14 @@
+---
+- name: "Delete content view versions of {{ cv_name }}"
+ theforeman.foreman.content_view_version:
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ content_view: "{{ cv_name }}"
+ version: "{{ cv_version }}"
+ state: absent
+ loop: "{{ cv_versions }}"
+ loop_control:
+ loop_var: "cv_version"
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/find_and_delete_unused_cv_versions.yml b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/find_and_delete_unused_cv_versions.yml
new file mode 100644
index 00000000..b5b866b3
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/find_and_delete_unused_cv_versions.yml
@@ -0,0 +1,20 @@
+---
+- name: "Find content view versions of {{ cv.name }}"
+ theforeman.foreman.resource_info:
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ resource: content_view_versions
+ params:
+ content_view_id: "{{ cv.id }}"
+ register: versions
+
+- name: "Delete unused content view versions of {{ cv.name }}"
+ ansible.builtin.include_tasks: delete_cv_versions.yml
+ vars:
+ cv_name: "{{ cv.name }}"
+ cv_versions: "{{ (versions.resources | rejectattr('environments') | rejectattr('composite_content_view_ids') |
+ rejectattr('published_in_composite_content_view_ids') | map(attribute='version') | map('float') | sort |
+ map('string') | reverse | list)[foreman_content_view_version_cleanup_keep:] }}"
diff --git a/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/main.yml
new file mode 100644
index 00000000..bcaa6fff
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_view_version_cleanup/tasks/main.yml
@@ -0,0 +1,37 @@
+---
+- name: "Verify foreman_content_view_version_cleanup_keep is set"
+ ansible.builtin.assert:
+ that:
+ - foreman_content_view_version_cleanup_keep|int >= 0
+ fail_msg: "foreman_content_view_version_cleanup_keep needs to be set to >= 0"
+
+- name: "Find all content views"
+ theforeman.foreman.resource_info:
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ resource: content_views
+ search: "{{ foreman_content_view_version_cleanup_search | default(omit) }}"
+ register: all_cvs
+
+- name: "Delete unused composite content view versions"
+ ansible.builtin.include_tasks: delete_cv_versions.yml
+ vars:
+ cv_name: "{{ ccv.name }}"
+ cv_versions: "{{ (ccv.versions | rejectattr('environment_ids') | map(attribute='version') | map('float') | sort
+ | map('string') | reverse | list)[foreman_content_view_version_cleanup_keep:] }}"
+ loop: "{{ all_cvs.resources | selectattr('composite') | list }}"
+ loop_control:
+ label: "{{ ccv.label }}"
+ loop_var: "ccv"
+ when: (ccv.versions | rejectattr('environment_ids') | map(attribute='version') | reverse | list)[foreman_content_view_version_cleanup_keep:]
+
+- name: "Find and delete unused content view versions"
+ ansible.builtin.include_tasks: find_and_delete_unused_cv_versions.yml
+ loop: "{{ all_cvs.resources | rejectattr('composite') | list }}"
+ loop_control:
+ label: "{{ cv.label }}"
+ loop_var: "cv"
+ when: (cv.versions | rejectattr('environment_ids') | map(attribute='version') | reverse | list)[foreman_content_view_version_cleanup_keep:]
diff --git a/ansible_collections/theforeman/foreman/roles/content_views/README.md b/ansible_collections/theforeman/foreman/roles/content_views/README.md
new file mode 100644
index 00000000..67069882
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_views/README.md
@@ -0,0 +1,70 @@
+theforeman.foreman.content_views
+================================
+
+This role creates and manages Content Views.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_content_views`. Each Content View requires the following fields:
+
+- `name` - the name of the content view
+
+Each content view also requires either a list of repositories or components (for a composite content view):
+- `repositories` - List of repositories to add to the content view. Each repository requires the following fields:
+ - `name` - The name of the repository
+ - `product` - The product which the repository belongs to
+- `components` - List of content views to add to the composite content view. Each component requires the following fields:
+ - `content_view` - The name of the content view
+ - `content_view_version` - The version of the content view to add, *or*
+ - `latest` - If `true`, the latest version of the content view will be used
+
+Additionally you can pass any other parameters accepted by the `content_view` module.
+
+This role also allows you to create Content View Filters and add them to the Content View by passing a list of `filters`:
+
+- `filters` - List of filters to create and add to the content view. Each filter needs the following fields:
+ - `name` - Name of the content view filter
+ - `filter_type` - Content view filter type. The available types are `rpm`, `package_group`, `erratum`, or `docker`
+
+Additionally you can pass any other parameters accepted by the `content_view_filter` module.
+
+Example Playbooks
+-----------------
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.content_views
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_content_views:
+ - name: RHEL7
+ repositories:
+ - name: Red Hat Enterprise Linux 7 Server RPMs x86_64 7Server
+ product: 'Red Hat Enterprise Linux Server'
+ - name: Red Hat Enterprise Linux 7 Server - Extras RPMs x86_64
+ product: 'Red Hat Enterprise Linux Server'
+ - name: Red Hat Satellite Tools 6.8 (for RHEL 7 Server) (RPMs)
+ product: 'Red Hat Enterprise Linux Server'
+ - name: BearApp
+ repositories:
+ - name: MyApps
+ product: ACME
+ filters:
+ - name: "bear app"
+ filter_state: "present"
+ filter_type: "rpm"
+ rule_name: "bear"
+ - name: BearAppServer
+ components:
+ - content_view: RHEL7
+ latest: true
+ - content_view: BearApp
+ latest: true
+```
diff --git a/ansible_collections/theforeman/foreman/roles/content_views/tasks/_create_content_view.yml b/ansible_collections/theforeman/foreman/roles/content_views/tasks/_create_content_view.yml
new file mode 100644
index 00000000..5f075b1c
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_views/tasks/_create_content_view.yml
@@ -0,0 +1,44 @@
+---
+- name: Create content view # noqa: args[module]
+ theforeman.foreman.content_view:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ content_view.name }}"
+ auto_publish: "{{ content_view.auto_publish | default(omit) }}"
+ components: "{{ content_view.components | default(omit) }}"
+ composite: "{{ content_view.components | default(false) | ternary(true, false) }}"
+ description: "{{ content_view.description | default(omit) }}"
+ label: "{{ content_view.label | default(omit) }}"
+ repositories: "{{ content_view.repositories | default(omit) }}"
+ solve_dependencies: "{{ content_view.solve_dependencies | default(omit) }}"
+
+- name: Add content view filters
+ theforeman.foreman.content_view_filter:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ content_view: "{{ content_view.name }}"
+ repositories: "{{ item.repositories | default(omit) }}"
+ filter_type: "{{ item.filter_type }}"
+ start_date: "{{ item.start_date | default(omit) }}"
+ end_date: "{{ item.end_date | default(omit) }}"
+ types: "{{ item.types | default(omit) }}"
+ date_type: "{{ item.date_type | default(omit) }}"
+ inclusion: "{{ item.inclusion | default(omit) }}"
+ errata_id: "{{ item.errata_id | default(omit) }}"
+ max_version: "{{ item.max_version | default(omit) }}"
+ min_version: "{{ item.min_version | default(omit) }}"
+ rule_name: "{{ item.rule_name | default(omit) }}"
+ version: "{{ item.version | default(omit) }}"
+ description: "{{ item.description | default(omit) }}"
+ architecture: "{{ item.architecture | default(omit) }}"
+ filter_state: "{{ item.filter_state | default(omit) }}"
+ original_packages: "{{ item.original_packages | default(omit) }}"
+ rule_state: "{{ item.rule_state | default(omit) }}"
+ loop: "{{ content_view.filters | default([]) }}"
diff --git a/ansible_collections/theforeman/foreman/roles/content_views/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/content_views/tasks/main.yml
new file mode 100644
index 00000000..54b892d9
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/content_views/tasks/main.yml
@@ -0,0 +1,6 @@
+---
+- name: Create Content Views
+ ansible.builtin.include_tasks: '_create_content_view.yml'
+ with_items: "{{ foreman_content_views | default([]) }}"
+ loop_control:
+ loop_var: content_view
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/README.md b/ansible_collections/theforeman/foreman/roles/convert2rhel/README.md
new file mode 100644
index 00000000..ac17d670
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/README.md
@@ -0,0 +1,44 @@
+theforeman.foreman.convert2rhel
+===============================
+
+This role creates a basic configuration for everything needed to register and convert CentOS clients to Red hat Enterprise Linux.
+
+First step is upload of manifest and synchronization of RHEL repositories. For more detail see [content_rhel Role](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/content_rhel/README.md).
+
+Then the role creates Convert2RHEL products & repositories (and synchronizes them), activation keys and host groups for each OS.
+
+If simple content access is disabled, subscriptions and repositories for RHEL activation keys must be added manually.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables) and [Content RHEL variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/content_rhel/README.md)
+
+- `foreman_convert2rhel_manage_subscription`: Run [content_rhel Role](https://github.com/theforeman/foreman-ansible-modules/blob/develop/roles/content_rhel/README.md) role, default: `true`
+- `foreman_convert2rhel_lifecycle_env`: Lifecycle environment for activation keys, default: Library.
+- `foreman_convert2rhel_content_view`: Content view for activation keys, default: Default Organization View.
+- `foreman_convert2rhel_enable_oracle7`: Create data for Oracle Linux 7 conversion, default: `false`
+
+Example Playbooks
+-----------------
+
+Convert2RHEL
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.convert2rhel
+ vars:
+ foreman_server_url: "https://foreman.example.com"
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_manifest_path: "~/manifest.zip"
+ foreman_content_rhel_enable_rhel7: true
+ foreman_content_rhel_enable_rhel8: true
+ foreman_content_rhel_rhel8_releasever: 8.5
+ foreman_content_rhel_wait_for_syncs: false
+ foreman_convert2rhel_lifecycle_env: "Library"
+ foreman_convert2rhel_content_view: "Default Organization View"
+ foreman_convert2rhel_enable_oracle7: true
+```
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/defaults/main.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/defaults/main.yml
new file mode 100644
index 00000000..882802f1
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/defaults/main.yml
@@ -0,0 +1,32 @@
+---
+foreman_convert2rhel_manage_subscription: true
+
+# Convert2RHEL Products & repos
+foreman_convert2rhel_rhel7_product: "Convert2RHEL7"
+foreman_convert2rhel_rhel7_repo: "Convert2RHEL7 main"
+foreman_convert2rhel_rhel8_product: "Convert2RHEL8"
+foreman_convert2rhel_rhel8_repo: "Convert2RHEL8 main"
+foreman_convert2rhel_oracle7_product: "Oracle Linux 7 Convert2RHEL"
+foreman_convert2rhel_oracle7_repo: "Oracle Linux 7 Convert2RHEL main"
+foreman_convert2rhel_oracle8_product: "Oracle Linux 8 Convert2RHEL"
+foreman_convert2rhel_oracle8_repo: "Oracle Linux 8 Convert2RHEL main"
+
+# Activation keys
+foreman_convert2rhel_key_centos7: "convert2rhel_centos7"
+foreman_convert2rhel_key_centos8: "convert2rhel_centos8"
+foreman_convert2rhel_key_oracle7: "convert2rhel_oracle7"
+foreman_convert2rhel_key_oracle8: "convert2rhel_oracle8"
+foreman_convert2rhel_key_rhel7: "convert2rhel_rhel7"
+foreman_convert2rhel_key_rhel8: "convert2rhel_rhel8"
+foreman_convert2rhel_lifecycle_env: "Library"
+foreman_convert2rhel_content_view: "Default Organization View"
+
+# Host groups
+foreman_convert2rhel_hostgroup7: "CentOS 7 converting"
+foreman_convert2rhel_hostgroup8: "CentOS 8 converting"
+foreman_convert2rhel_hostgroup_oracle7: "Oracle Linux 7 converting"
+foreman_convert2rhel_hostgroup_oracle8: "Oracle Linux 8 converting"
+
+# Oracle Linux
+foreman_convert2rhel_enable_oracle7: false
+foreman_convert2rhel_enable_oracle8: false
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/activation_keys.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/activation_keys.yml
new file mode 100644
index 00000000..457fd0c6
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/activation_keys.yml
@@ -0,0 +1,111 @@
+---
+- name: "Get organization (SCA) info"
+ theforeman.foreman.organization_info:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ foreman_organization }}"
+ register: foreman_convert2rhel_org_info
+
+- name: "Set non SCA facts"
+ ansible.builtin.set_fact:
+ foreman_convert2rhel_centos7_subs:
+ - name: "Convert2RHEL7"
+ foreman_convert2rhel_centos8_subs:
+ - name: "Convert2RHEL8"
+ foreman_convert2rhel_ol7_subs:
+ - name: "Convert2RHEL7"
+ - name: "{{ foreman_convert2rhel_oracle7_product }}"
+ foreman_convert2rhel_ol8_subs:
+ - name: "Convert2RHEL8"
+ - name: "{{ foreman_convert2rhel_oracle8_product }}"
+ when: not foreman_convert2rhel_org_info['organization']['simple_content_access']
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_centos7 }}'"
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_centos7 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_key_centos7 }}"
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_centos8 }}'"
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_centos8 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_key_centos8 }}"
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_oracle7 }}'" # noqa: args[module]
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_oracle7 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_content_view }}"
+ subscriptions: "{{ foreman_convert2rhel_org_info['organization']['simple_content_access'] | ternary(omit, foreman_convert2rhel_ol7_subs) }}"
+ when: foreman_convert2rhel_enable_oracle7
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_oracle8 }}'" # noqa: args[module]
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_oracle8 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_content_view }}"
+ subscriptions: "{{ foreman_convert2rhel_org_info['organization']['simple_content_access'] | ternary(omit, foreman_convert2rhel_ol8_subs) }}"
+ when: foreman_convert2rhel_enable_oracle8
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_rhel7 }}'"
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_rhel7 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_content_view }}"
+ auto_attach: false
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Create activation key '{{ foreman_convert2rhel_key_rhel8 }}'"
+ theforeman.foreman.activation_key:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_rhel8 }}"
+ lifecycle_environment: "{{ foreman_convert2rhel_lifecycle_env }}"
+ content_view: "{{ foreman_convert2rhel_content_view }}"
+ auto_attach: false
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Add subscriptions to '{{ foreman_convert2rhel_key_rhel7 }}'"
+ ansible.builtin.debug:
+ msg:
+ - "Simple content access is disabled, please add subscriptions to '{{ foreman_convert2rhel_key_rhel7 }}' activation key manually"
+ when: not foreman_convert2rhel_org_info['organization']['simple_content_access'] and foreman_content_rhel_enable_rhel7
+
+- name: "Add subscriptions to '{{ foreman_convert2rhel_key_rhel8 }}'"
+ ansible.builtin.debug:
+ msg:
+ - "Simple content access is disabled, please add subscriptions to '{{ foreman_convert2rhel_key_rhel8 }}' activation key manually"
+ when: not foreman_convert2rhel_org_info['organization']['simple_content_access'] and foreman_content_rhel_enable_rhel8
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/content_views.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/content_views.yml
new file mode 100644
index 00000000..d638464b
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/content_views.yml
@@ -0,0 +1,52 @@
+---
+- name: "Create content view '{{ foreman_convert2rhel_key_centos7 }}'"
+ theforeman.foreman.content_view:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_centos7 }}"
+ repositories:
+ - name: '{{ foreman_convert2rhel_rhel7_repo }}'
+ product: '{{ foreman_convert2rhel_rhel7_product }}'
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Publish content view '{{ foreman_convert2rhel_key_centos7 }}'"
+ theforeman.foreman.content_view_version:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ content_view: "{{ foreman_convert2rhel_key_centos7 }}"
+ version: "1.0"
+ lifecycle_environments:
+ - "{{ foreman_convert2rhel_lifecycle_env }}"
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Create content view '{{ foreman_convert2rhel_key_centos8 }}'"
+ theforeman.foreman.content_view:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_key_centos8 }}"
+ repositories:
+ - name: '{{ foreman_convert2rhel_rhel8_repo }}'
+ product: '{{ foreman_convert2rhel_rhel8_product }}'
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Publish content view '{{ foreman_convert2rhel_key_centos8 }}'"
+ theforeman.foreman.content_view_version:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ content_view: "{{ foreman_convert2rhel_key_centos8 }}"
+ version: "1.0"
+ lifecycle_environments:
+ - "{{ foreman_convert2rhel_lifecycle_env }}"
+ when: foreman_content_rhel_enable_rhel8
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/host_groups.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/host_groups.yml
new file mode 100644
index 00000000..173ea36c
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/host_groups.yml
@@ -0,0 +1,44 @@
+---
+- name: "Create host group '{{ foreman_convert2rhel_hostgroup7 }}'"
+ theforeman.foreman.hostgroup:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_hostgroup7 }}"
+ activation_keys: "{{ foreman_convert2rhel_key_centos7 }}"
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Create host group '{{ foreman_convert2rhel_hostgroup8 }}'"
+ theforeman.foreman.hostgroup:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_hostgroup8 }}"
+ activation_keys: "{{ foreman_convert2rhel_key_centos8 }}"
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Create host group '{{ foreman_convert2rhel_hostgroup_oracle7 }}'"
+ theforeman.foreman.hostgroup:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_hostgroup_oracle7 }}"
+ activation_keys: "{{ foreman_convert2rhel_key_oracle7 }}"
+ when: foreman_convert2rhel_enable_oracle7
+
+- name: "Create host group '{{ foreman_convert2rhel_hostgroup_oracle8 }}'"
+ theforeman.foreman.hostgroup:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ foreman_convert2rhel_hostgroup_oracle8 }}"
+ activation_keys: "{{ foreman_convert2rhel_key_oracle8 }}"
+ when: foreman_convert2rhel_enable_oracle8
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/main.yml
new file mode 100644
index 00000000..c50dc777
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/main.yml
@@ -0,0 +1,15 @@
+---
+- name: "Upload Subscription Manifest and sync RHEL repository"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.content_rhel
+ when: foreman_convert2rhel_manage_subscription
+- name: "Create Producs and Repositories"
+ ansible.builtin.import_tasks: products_and_repos.yml
+- name: "Create Content Views"
+ ansible.builtin.import_tasks: content_views.yml
+- name: "Create Activation Keys"
+ ansible.builtin.import_tasks: activation_keys.yml
+- name: "Create Hostgroups"
+ ansible.builtin.import_tasks: host_groups.yml
+- name: "Sync Content"
+ ansible.builtin.import_tasks: sync.yml
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/products_and_repos.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/products_and_repos.yml
new file mode 100644
index 00000000..698b3964
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/products_and_repos.yml
@@ -0,0 +1,88 @@
+---
+- name: Check /etc/rhsm/ca/redhat-uep.pem
+ ansible.builtin.stat:
+ path: "/etc/rhsm/ca/redhat-uep.pem"
+ register: ct
+
+- name: "Create 'Convert2RHEL' credentials"
+ theforeman.foreman.content_credential:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "Convert2RHEL CA"
+ content_type: cert
+ content: "{{ ct.stat.exists | ternary(lookup('file', '/etc/rhsm/ca/redhat-uep.pem', errors='ignore', rstrip=False),
+ lookup('url', 'https://ftp.redhat.com/redhat/convert2rhel/redhat-uep.pem', split_lines=False)) }}"
+ state: present
+
+- name: "Create product and repositories '{{ foreman_convert2rhel_rhel7_product }}'"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: "{{ foreman_convert2rhel_rhel7_product }}"
+ repositories:
+ - name: "{{ foreman_convert2rhel_rhel7_repo }}"
+ content_type: "yum"
+ product: "{{ foreman_convert2rhel_rhel7_product }}"
+ url: "https://cdn.redhat.com/content/public/convert2rhel/7/x86_64/os/"
+ mirror_on_sync: true
+ verify_ssl_on_sync: true
+ download_policy: immediate
+ ssl_ca_cert: "Convert2RHEL CA"
+ state: present
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Create product and repositories '{{ foreman_convert2rhel_rhel8_product }}'"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: "{{ foreman_convert2rhel_rhel8_product }}"
+ repositories:
+ - name: "{{ foreman_convert2rhel_rhel8_repo }}"
+ content_type: "yum"
+ product: "{{ foreman_convert2rhel_rhel8_product }}"
+ url: "https://cdn.redhat.com/content/public/convert2rhel/8/x86_64/os/"
+ mirror_on_sync: true
+ verify_ssl_on_sync: true
+ download_policy: immediate
+ ssl_ca_cert: "Convert2RHEL CA"
+ state: present
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Create product and repositories '{{ foreman_convert2rhel_oracle7_product }}'"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: "{{ foreman_convert2rhel_oracle7_product }}"
+ repositories:
+ - name: "{{ foreman_convert2rhel_oracle7_repo }}"
+ content_type: "yum"
+ product: "{{ foreman_convert2rhel_oracle7_product }}"
+ url: "https://cdn-ubi.redhat.com/content/public/ubi/dist/ubi/server/7/7Server/x86_64/os"
+ mirror_on_sync: true
+ verify_ssl_on_sync: true
+ download_policy: immediate
+ state: present
+ when: foreman_convert2rhel_enable_oracle7
+
+- name: "Create product and repositories '{{ foreman_convert2rhel_oracle8_product }}'"
+ ansible.builtin.include_role:
+ name: theforeman.foreman.repositories
+ vars:
+ foreman_products:
+ - name: "{{ foreman_convert2rhel_oracle8_product }}"
+ repositories:
+ - name: "{{ foreman_convert2rhel_oracle8_repo }}"
+ content_type: "yum"
+ product: "{{ foreman_convert2rhel_oracle8_product }}"
+ url: "https://cdn-ubi.redhat.com/content/public/ubi/dist/ubi8/8/x86_64/baseos/os/"
+ mirror_on_sync: true
+ verify_ssl_on_sync: true
+ download_policy: immediate
+ state: present
+ when: foreman_convert2rhel_enable_oracle8
diff --git a/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/sync.yml b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/sync.yml
new file mode 100644
index 00000000..1dae6479
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/convert2rhel/tasks/sync.yml
@@ -0,0 +1,44 @@
+---
+- name: "Synchronize repository '{{ foreman_convert2rhel_rhel7_repo }}'"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ repository: "{{ foreman_convert2rhel_rhel7_repo }}"
+ product: "{{ foreman_convert2rhel_rhel7_product }}"
+ when: foreman_content_rhel_enable_rhel7
+
+- name: "Synchronize repository '{{ foreman_convert2rhel_rhel8_repo }}'"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ repository: "{{ foreman_convert2rhel_rhel8_repo }}"
+ product: "{{ foreman_convert2rhel_rhel8_product }}"
+ when: foreman_content_rhel_enable_rhel8
+
+- name: "Synchronize repository '{{ foreman_convert2rhel_oracle7_repo }}'"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ repository: "{{ foreman_convert2rhel_oracle7_repo }}"
+ product: "{{ foreman_convert2rhel_oracle7_product }}"
+ when: foreman_convert2rhel_enable_oracle7
+
+- name: "Synchronize repository '{{ foreman_convert2rhel_oracle8_repo }}'"
+ theforeman.foreman.repository_sync:
+ username: "{{ foreman_username }}"
+ password: "{{ foreman_password }}"
+ server_url: "{{ foreman_server_url }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ repository: "{{ foreman_convert2rhel_oracle8_repo }}"
+ product: "{{ foreman_convert2rhel_oracle8_product }}"
+ when: foreman_convert2rhel_enable_oracle8
diff --git a/ansible_collections/theforeman/foreman/roles/domains/README.md b/ansible_collections/theforeman/foreman/roles/domains/README.md
new file mode 100644
index 00000000..4998b37f
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/domains/README.md
@@ -0,0 +1,41 @@
+theforeman.foreman.domains
+==========================
+
+This role creates and manages Domains.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_domains`. Each `domain` requires the following fields:
+
+- `name`: The name of the domain.
+
+The following fields are optional and will be omitted by default:
+
+- `description`: Description of the domain.
+- `dns_proxy`: DNS proxy to use within this domain for managing A records.
+- `parameters`: Domain specific host parameters.
+
+Example Playbook
+----------------
+
+Create a domain `example.org`.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.domains
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_domains:
+ - name: "example.org"
+ description: "Example Domain"
+ locations:
+ - "Uppsala"
+ organizations:
+ - "ACME"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/domains/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/domains/tasks/main.yml
new file mode 100644
index 00000000..8957db86
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/domains/tasks/main.yml
@@ -0,0 +1,13 @@
+---
+- name: 'Create Domains' # noqa: args[module]
+ theforeman.foreman.domain:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ locations: "{{ item.locations | default(omit) }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ parameters: "{{ item.parameters | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_domains }}"
diff --git a/ansible_collections/theforeman/foreman/roles/hostgroups/README.md b/ansible_collections/theforeman/foreman/roles/hostgroups/README.md
new file mode 100644
index 00000000..3b1af611
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/hostgroups/README.md
@@ -0,0 +1,75 @@
+theforeman.foreman.hostgroups
+=============================
+
+This role creates and manages Hostgroups.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+- `foreman_hostgroups`: List of hostgroups to manage that are each represented as a dictionary. See module documentation for a list of available options for each hostgroup.
+ Hostgroups may have any set of fields defined on them and may optionally define a `parent` for nested hostgroups.
+ A variety of examples are demonstrated in the data structure below:
+
+```yaml
+foreman_hostgroups:
+ - name: "Basic example"
+ architecture: "x86_64"
+ operatingsystem: "CentOS"
+ medium: "media_name"
+ ptable: "partition_table_name"
+ - name: "Proxies hostgroup"
+ environment: production
+ puppet_proxy: puppet-proxy.example.com
+ puppet_ca_proxy: puppet-proxy.example.com
+ openscap_proxy: openscap-proxy.example.com
+ - name: "CentOS 7"
+ organization: "Default Organization"
+ lifecycle_environment: "Production"
+ content_view: "CentOS 7"
+ activation_keys: centos-7
+ - name: "Webserver"
+ parent: "CentOS 7"
+ environment: production
+ puppet_proxy: puppet-proxy.example.com
+ puppet_ca_proxy: puppet-proxy.example.com
+ openscap_proxy: openscap-proxy.example.com
+```
+
+Example Playbooks
+-----------------
+
+This example creates several hostgroups with some nested examples.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.hostgroups
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_hostgroups:
+ - name: "Basic example"
+ architecture: "x86_64"
+ operatingsystem: "CentOS"
+ medium: "media_name"
+ ptable: "partition_table_name"
+ - name: "Proxies hostgroup"
+ environment: production
+ puppet_proxy: puppet-proxy.example.com
+ puppet_ca_proxy: puppet-proxy.example.com
+ openscap_proxy: openscap-proxy.example.com
+ - name: "CentOS 7"
+ organization: "Default Organization"
+ lifecycle_environment: "Production"
+ content_view: "CentOS 7"
+ activation_keys: centos-7
+ - name: "Webserver"
+ parent: "CentOS 7"
+ environment: production
+ puppet_proxy: puppet-proxy.example.com
+ puppet_ca_proxy: puppet-proxy.example.com
+ openscap_proxy: openscap-proxy.example.com
+```
diff --git a/ansible_collections/theforeman/foreman/roles/hostgroups/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/hostgroups/tasks/main.yml
new file mode 100644
index 00000000..18627a4f
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/hostgroups/tasks/main.yml
@@ -0,0 +1,42 @@
+---
+- name: 'Create Hostgroups' # noqa: args[module]
+ theforeman.foreman.hostgroup:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ updated_name: "{{ item.updated_name | default(omit) }}"
+ description: "{{ item.description | default(omit) }}"
+ parent: "{{ item.parent | default(omit) }}"
+ organization: "{{ item.organization | default(omit) }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ locations: "{{ item.locations | default(omit) }}"
+ architecture: "{{ item.architecture | default(omit) }}"
+ operatingsystem: "{{ item.operatingsystem | default(omit) }}"
+ medium: "{{ item.medium | default(omit) }}"
+ ptable: "{{ item.ptable | default(omit) }}"
+ parameters: "{{ item.parameters | default(omit) }}"
+ ansible_roles: "{{ item.ansible_roles | default(omit) }}"
+ compute_resource: "{{ item.compute_resource | default(omit) }}"
+ compute_profile: "{{ item.compute_profile | default(omit) }}"
+ domain: "{{ item.domain | default(omit) }}"
+ subnet: "{{ item.subnet | default(omit) }}"
+ subnet6: "{{ item.subnet6 | default(omit) }}"
+ root_pass: "{{ item.root_pass | default(omit) }}"
+ realm: "{{ item.realm | default(omit) }}"
+ pxe_loader: "{{ item.pxe_loader | default(omit) }}"
+ environment: "{{ item.environment | default(omit) }}"
+ puppetclasses: "{{ item.puppetclasses | default(omit) }}"
+ config_groups: "{{ item.config_groups | default(omit) }}"
+ puppet_proxy: "{{ item.puppet_proxy | default(omit) }}"
+ puppet_ca_proxy: "{{ item.puppet_ca_proxy | default(omit) }}"
+ openscap_proxy: "{{ item.openscap_proxy | default(omit) }}"
+ content_source: "{{ item.content_source | default(omit) }}"
+ lifecycle_environment: "{{ item.lifecycle_environment | default(omit) }}"
+ kickstart_repository: "{{ item.kickstart_repository | default(omit) }}"
+ content_view: "{{ item.content_view | default(omit) }}"
+ activation_keys: "{{ item.activation_keys | default(omit) }}"
+ state: "{{ item.state | default(omit) }}"
+ with_items:
+ - "{{ foreman_hostgroups }}"
diff --git a/ansible_collections/theforeman/foreman/roles/lifecycle_environments/README.md b/ansible_collections/theforeman/foreman/roles/lifecycle_environments/README.md
new file mode 100644
index 00000000..f3424927
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/lifecycle_environments/README.md
@@ -0,0 +1,75 @@
+theforeman.foreman.lifecycle_environments
+=========================================
+
+This role creates and manages Lifecycle Environments.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_lifecycle_environments`. Each `lifecycle_environment` requires the following fields:
+
+- `name`: The name of the lifecycle environment.
+- `prior`: The name of the previous lifecycle environment to attach to in
+ sequence. For the first lifecycle environment in a new path, set the prior
+ lifecycle environment to Library. The order of definition matters, ensure that
+ the environments are listed in the order the path would exist. It can't be
+ changed after the lifecycle environment has been created.
+
+The following fields are optional and will be omitted by default:
+
+- `description`: Description of the lifecycle environment
+- `label`: A permanent label for identifying the lifecycle environment to tools
+ such as subscription-manager. This is created by the server if omitted. It
+ can't be changed after the lifecycle environment has been created.
+
+Example Playbooks
+-----------------
+
+Create a lifecycle environment path with three environments: Library -> Dev -> Test -> Prod
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.lifecycle_environments
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_lifecycle_environments:
+ - name: "Dev"
+ prior: "Library"
+ - name: "Test"
+ prior: "Dev"
+ - name: "Prod"
+ prior: "Test"
+```
+
+Create two lifecycle environment paths: Library -> Dev -> Test -> Prod and Library -> QA -> Stage -> Prod
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.lifecycle_environments
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_lifecycle_environments:
+ - name: "Dev"
+ prior: "Library"
+ - name: "Test"
+ prior: "Dev"
+ - name: "Prod"
+ prior: "Test"
+
+ - name: "QA"
+ prior: "Library"
+ - name: "Stage"
+ prior: "QA"
+ - name: "Prod"
+ prior: "Stage"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/lifecycle_environments/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/lifecycle_environments/tasks/main.yml
new file mode 100644
index 00000000..f6f0ea1b
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/lifecycle_environments/tasks/main.yml
@@ -0,0 +1,15 @@
+---
+- name: 'Create Lifecycle Environments'
+ theforeman.foreman.lifecycle_environment:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ description: "{{ item.description | default(omit) }}"
+ prior: "{{ item.prior }}"
+ label: "{{ item.label | default(omit) }}"
+ state: present
+ with_items:
+ - "{{ foreman_lifecycle_environments }}"
diff --git a/ansible_collections/theforeman/foreman/roles/manifest/README.md b/ansible_collections/theforeman/foreman/roles/manifest/README.md
new file mode 100644
index 00000000..ecda345e
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/manifest/README.md
@@ -0,0 +1,50 @@
+theforeman.foreman.manifest
+===========================
+
+Upload Subscription Manifest
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+- `foreman_manifest_path`: Path to subscription Manifest file on Ansible target host. When using `manifest_download`, it is first downloaded to this location from the Red Hat Customer Portal before being uploaded to the Foreman server.
+- `foreman_manifest_download`: Whether to first download the Manifest from the Red Hat Customer Portal. Defaults to `False`.
+- `foreman_manifest_uuid`: UUID of the Manifest to download, corresponding to a [Subscription Allocation](https://access.redhat.com/management/subscription_allocations) defined on your Red Hat account. Required when `manifest_download` is `True`.
+- `foreman_rhsm_username`: Your username for the Red Hat Customer Portal. Required when `foreman_manifest_download` is `true`.
+- `foreman_rhsm_password`: Your password for the Red Hat Customer Portal. Required when `foreman_manifest_download` is `true`.
+
+Example Playbooks
+-----------------
+
+Use a Subscription Manifest which has already been downloaded on localhost at `~/manifest.zip`:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.manifest
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_manifest_path: "~/manifest.zip"
+```
+
+Download the Subscription Manifest from the Red Hat Customer Portal to localhost before uploading to Foreman server:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.manifest
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_manifest_path: "~/manifest.zip"
+ foreman_manifest_download: True
+ foreman_rhsm_username: "happycustomer"
+ foreman_rhsm_password: "$ecur3p4$$w0rd"
+ foreman_manifest_uuid: "01234567-89ab-cdef-0123-456789abcdef"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/manifest/defaults/main.yml b/ansible_collections/theforeman/foreman/roles/manifest/defaults/main.yml
new file mode 100644
index 00000000..373f40b2
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/manifest/defaults/main.yml
@@ -0,0 +1,2 @@
+---
+foreman_manifest_download: false
diff --git a/ansible_collections/theforeman/foreman/roles/manifest/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/manifest/tasks/main.yml
new file mode 100644
index 00000000..01ab15cc
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/manifest/tasks/main.yml
@@ -0,0 +1,18 @@
+---
+- name: Download Subscription Manifest from Red Hat Customer Portal
+ theforeman.foreman.redhat_manifest:
+ uuid: "{{ foreman_manifest_uuid }}"
+ username: "{{ foreman_rhsm_username }}"
+ password: "{{ foreman_rhsm_password }}"
+ path: "{{ foreman_manifest_path }}"
+ when: foreman_manifest_download
+
+- name: Upload Subscription Manifest to Foreman
+ theforeman.foreman.subscription_manifest:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ manifest_path: "{{ foreman_manifest_path }}"
+ state: "{{ foreman_manifest_state | default('present') }}"
diff --git a/ansible_collections/theforeman/foreman/roles/operatingsystems/README.md b/ansible_collections/theforeman/foreman/roles/operatingsystems/README.md
new file mode 100644
index 00000000..1f4ff182
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/operatingsystems/README.md
@@ -0,0 +1,43 @@
+theforeman.foreman.operatingsystems
+===================================
+
+This role creates and manages Operatingsystems.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_operatingsystems`. Each `operatingsystem` requires the following fields:
+
+- `name`: The name of the operatingsystem.
+
+For all other fields see the `operatingsystem` module. The field `default_templates` can also be used to assign
+default provisioning templates for the operatingsystem where each `template` consists of the fields from the module
+`os_default_template`.
+
+Example Playbook
+----------------
+
+Create operating system `RedHat 8.5` and assign it templates for provisioning using `cloud-init` and `open-vm-tools`:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.operatingsystems
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_operatingsystems:
+ - name: "RedHat"
+ major: "8"
+ minor: "5"
+ os_family: "Redhat"
+ password_hash: "SHA256"
+ default_templates:
+ - template_kind: "cloud-init"
+ provisioning_template: "CloudInit default"
+ - template_kind: "user_data"
+ provisioning_template: "UserData open-vm-tools"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/operatingsystems/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/operatingsystems/tasks/main.yml
new file mode 100644
index 00000000..49b447b2
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/operatingsystems/tasks/main.yml
@@ -0,0 +1,35 @@
+---
+- name: 'Create Operatingsystems' # noqa: args[module]
+ theforeman.foreman.operatingsystem:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ architectures: "{{ item.architectures | default(omit) }}"
+ description: "{{ item.description | default(omit) }}"
+ major: "{{ item.major | default(omit) }}"
+ media: "{{ item.media | default(omit) }}"
+ minor: "{{ item.minor | default(omit) }}"
+ os_family: "{{ item.os_family | default(omit) }}"
+ parameters: "{{ item.parameters | default(omit) }}"
+ password_hash: "{{ item.password_hash | default(omit) }}"
+ provisioning_templates: "{{ item.provisioning_templates | default(omit) }}"
+ ptables: "{{ item.ptables | default(omit) }}"
+ release_name: "{{ item.release_name | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_operatingsystems }}"
+
+- name: 'Set default templates for Operatingsystems'
+ vars:
+ default_os_name: "{{ item.0.name }} {{ item.0.major }}.{{ item.0.minor | default('0') }}"
+ theforeman.foreman.os_default_template:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ operatingsystem: "{{ item.0.description | default(default_os_name) }}"
+ provisioning_template: "{{ item.1.provisioning_template | default(omit) }}"
+ template_kind: "{{ item.1.template_kind }}"
+ state: "{{ item.1.state | default('present') }}"
+ loop: "{{ foreman_operatingsystems | subelements('default_templates', {'skip_missing': True}) }}"
diff --git a/ansible_collections/theforeman/foreman/roles/organizations/README.md b/ansible_collections/theforeman/foreman/roles/organizations/README.md
new file mode 100644
index 00000000..e5d87ebf
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/organizations/README.md
@@ -0,0 +1,44 @@
+theforeman.foreman.organizations
+================================
+
+This role creates and manages organizations.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_organizations`. Each `organization` requires the following fields:
+
+- `name`: The name of the organization.
+
+The following fields are optional in the sense that the server will use default values when they are omitted:
+
+- `label`: The label of the organization.
+- `description`: The description of the organization.
+- `state`: The state of the organization. Can be `present` or `absent`.
+
+Example Playbooks
+-----------------
+
+```yaml
+---
+- name: add organizations to foreman
+ hosts: localhost
+ gather_facts: false
+ roles:
+ - role: theforeman.foreman.organizations
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: admin
+ foreman_password: changeme
+ foreman_organizations:
+ - name: raleigh
+ label: rdu
+ state: present
+ - name: default
+ label: boring
+ state: absent
+ - name: lanai
+ description: pacific datacenter
+```
diff --git a/ansible_collections/theforeman/foreman/roles/organizations/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/organizations/tasks/main.yml
new file mode 100644
index 00000000..c130b0d3
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/organizations/tasks/main.yml
@@ -0,0 +1,14 @@
+---
+- name: Add organizations
+ theforeman.foreman.organization:
+ name: "{{ foreman_organizations_item.name }}"
+ description: "{{ foreman_organizations_item.description | default(omit) }}"
+ label: "{{ foreman_organizations_item.label | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ state: "{{ foreman_organizations_item.state | default(omit) }}"
+ loop: "{{ foreman_organizations | default([]) }}"
+ loop_control:
+ loop_var: foreman_organizations_item
diff --git a/ansible_collections/theforeman/foreman/roles/provisioning_templates/README.md b/ansible_collections/theforeman/foreman/roles/provisioning_templates/README.md
new file mode 100644
index 00000000..1cdc9538
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/provisioning_templates/README.md
@@ -0,0 +1,33 @@
+theforeman.foreman.provisioning_templates
+=========================================
+
+This role creates and manages Provisioning Templates.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_provisioning_templates`. Each `provisioning_template` accepts fields according to the module `provisioning_template`.
+
+Example Playbook
+----------------
+
+Create a custom template `CloudInit vSphere` using the file `files/cloudinit_vsphere.erb` and assign it to the
+operating systems `RedHat 7.9` and `RedHat 8.5`:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.provisioning_templates
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_provisioning_templates:
+ - name: CloudInit vSphere
+ template: "{{ lookup('file', 'cloudinit_vsphere.erb') }}"
+ operatingsystems:
+ - RedHat 7.9
+ - RedHat 8.5
+```
diff --git a/ansible_collections/theforeman/foreman/roles/provisioning_templates/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/provisioning_templates/tasks/main.yml
new file mode 100644
index 00000000..0111a9bf
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/provisioning_templates/tasks/main.yml
@@ -0,0 +1,18 @@
+---
+- name: 'Create Provisioning Templates' # noqa: args[module]
+ theforeman.foreman.provisioning_template:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ audit_comment: "{{ item.audit_comment | default(omit) }}"
+ file_name: "{{ item.file_name | default(omit) }}"
+ kind: "{{ item.kind | default(omit) }}"
+ locations: "{{ item.locations | default(omit) }}"
+ locked: "{{ item.locked | default(omit) }}"
+ operatingsystems: "{{ item.operatingsystems | default(omit) }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ template: "{{ item.template | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_provisioning_templates }}"
diff --git a/ansible_collections/theforeman/foreman/roles/repositories/README.md b/ansible_collections/theforeman/foreman/roles/repositories/README.md
new file mode 100644
index 00000000..fd224e8d
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/repositories/README.md
@@ -0,0 +1,117 @@
+theforeman.foreman.repositories
+===============================
+
+This role defines Products and Custom Repositories and enables Red Hat Repositories.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+- `foreman_products`: List of products to manage.
+ Each product is represented as a dictionary and can include `repository_sets` which represent Red Hat Repositories and should be used when the product name matches an existing Red Hat Product.
+ Each element of `repository_sets` must have a `name` and should specify the `basearch` and/or `releasever` only when multiple versions are available for that Product.
+ All repository sets for a Red Hat Product can be enabled by omitting `repository_sets` and instead specifying that the Product has `all_repositories: True`. When using this option it is also necessary to specify a list of repository `label`s for the Product (e.g. rhel-7-server-rpms). Be wary that this option can result in enabling a large number of unused repositories that, if added to sync plans, can greatly increase sync times and rapidly fill disk space.
+ Custom (i.e. non Red Hat) Products can also be defined, with associated `repositories` which represent custom repositories, and are required to have a `name`, `url`, and `content_type`; they may require additional fields and can take any parameter supported by [theforeman.foreman.repository](https://theforeman.github.io/foreman-ansible-modules/develop/plugins/repository_module.html).
+ A variety of examples are demonstrated in the data structure below:
+
+```yaml
+foreman_products:
+ - name: Red Hat Enterprise Linux Server
+ repository_sets:
+ - name: Red Hat Enterprise Linux 7 Server (RPMs)
+ basearch: x86_64
+ releasever: 7Server
+ - name: Red Hat Enterprise Linux 6 Server (RPMs)
+ basearch: x86_64
+ releasever: 6Server
+ - name: Red Hat Enterprise Linux 7 Server - Extras (RPMs)
+ basearch: x86_64
+ - name: Red Hat Enterprise Linux 7 Server - Optional (RPMs)
+ basearch: x86_64
+ releasever: 7Server
+ - name: Red Hat Software Collections (for RHEL Server)
+ repository_sets:
+ - name: Red Hat Software Collections RPMs for Red Hat Enterprise Linux 7 Server
+ basearch: x86_64
+ releasever: 7Server
+ - name: Red Hat Software Collections RPMs for Red Hat Enterprise Linux 6 Server
+ basearch: x86_64
+ releasever: 6Server
+ - name: Red Hat Enterprise Linux for x86_64
+ repository_sets:
+ - name: Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)
+ releasever: 8
+ - name: Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)
+ releasever: 8
+ - name: Red Hat Software Collections (for RHEL Server)
+ all_repositories: True
+ labels:
+ - rhel-server-rhscl-7-rpms
+ - name: CentOS Stream 8
+ repositories:
+ - name: BaseOS x86_64
+ content_type: yum
+ url: http://mirror.centos.org/centos/8-stream/BaseOS/x86_64/os/
+ - name: AppStream x86_64
+ content_type: yum
+ url: http://mirror.centos.org/centos/8-stream/AppStream/x86_64/os/
+ - name: Debian 10
+ repositories:
+ - name: Debian 10 main
+ content_type: deb
+ url: http://deb.debian.org/debian
+ deb_components: main
+ deb_architectures: amd64
+ deb_releases: buster
+ - name: Foreman Client
+ repositories:
+ - name: Foreman Client Debian 10
+ url: https://apt.atix.de/debian
+ content_type: deb
+ deb_components: main
+ deb_architectures: amd64
+ deb_releases: stable
+ - name: Foreman Client CentOS 7
+ url: https://yum.theforeman.org/client/latest/el7/x86_64/
+ content_type: yum
+```
+
+Example Playbooks
+-----------------
+
+This example enables several Red Hat Repositories. There are a few important points to note about the structure of the data in the example:
+- RHEL 8 repos have a different product name than previous RHEL versions.
+- The RHEL 8 product already contains the `basearch` so it should not be specified on the RHEL 8 `repository_sets`, and the naming convention for `releasever` changed with RHEL 8 since system purpose removes the need for separate distributions like `Server` and `Workstation`.
+- The optional and extras repositories do not have point releases so `releasever` should be omitted.
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.repositories
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_products:
+ - name: Red Hat Enterprise Linux Server
+ repository_sets:
+ - name: Red Hat Enterprise Linux 7 Server (RPMs)
+ basearch: x86_64
+ releasever: 7Server
+ - name: Red Hat Enterprise Linux 6 Server (RPMs)
+ basearch: x86_64
+ releasever: 6Server
+ - name: Red Hat Enterprise Linux 7 Server - Extras (RPMs)
+ basearch: x86_64
+ - name: Red Hat Enterprise Linux 7 Server - Optional (RPMs)
+ basearch: x86_64
+ releasever: 7Server
+ - name: Red Hat Enterprise Linux for x86_64
+ repository_sets:
+ - name: Red Hat Enterprise Linux 8 for x86_64 - BaseOS (RPMs)
+ releasever: 8
+ - name: Red Hat Enterprise Linux 8 for x86_64 - AppStream (RPMs)
+ releasever: 8
+```
diff --git a/ansible_collections/theforeman/foreman/roles/repositories/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/repositories/tasks/main.yml
new file mode 100644
index 00000000..b73ca27c
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/repositories/tasks/main.yml
@@ -0,0 +1,92 @@
+---
+- name: 'Enable Red Hat Repositories'
+ theforeman.foreman.repository_set:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ product: "{{ item.0.name }}"
+ name: "{{ item.1.name }}"
+ all_repositories: false
+ state: enabled
+ repositories:
+ - releasever: "{{ item.1.releasever | default(omit) }}"
+ basearch: "{{ item.1.basearch | default(omit) }}"
+ with_subelements:
+ - "{{ foreman_products | selectattr('repository_sets', 'defined') | list }}"
+ - repository_sets
+
+- name: 'Enable Red Hat Repository Sets'
+ theforeman.foreman.repository_set:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ label: "{{ item.1 }}"
+ all_repositories: true
+ state: enabled
+ with_subelements:
+ - "{{ foreman_products | selectattr('all_repositories', 'defined') | selectattr('all_repositories', 'equalto', True) | list }}"
+ - labels
+
+- name: 'Create Products'
+ theforeman.foreman.product:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ label: "{{ item.label | default(omit) }}"
+ gpg_key: "{{ item.gpg_key | default(omit) }}"
+ state: present
+ with_items:
+ - "{{ foreman_products | selectattr('repositories', 'defined') | list }}"
+
+- name: 'Create Repositories' # noqa: args[module]
+ theforeman.foreman.repository:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.1.name }}"
+ url: "{{ item.1.url | default(omit) }}"
+ product: "{{ item.0.name }}"
+ arch: "{{ item.1.arch | default(omit) }}"
+ auto_enabled: "{{ item.1.auto_enabled | default(omit) }}"
+ checksum_type: "{{ item.1.checksum_type | default(omit) }}"
+ content_type: "{{ item.1.content_type }}"
+ deb_architectures: "{{ item.1.deb_architectures | default(omit) }}"
+ deb_components: "{{ item.1.deb_components | default(omit) }}"
+ deb_errata_url: "{{ item.1.deb_errata_url | default(omit) }}"
+ deb_releases: "{{ item.1.deb_releases | default(omit) }}"
+ description: "{{ item.1.description | default(omit) }}"
+ docker_tags_whitelist: "{{ item.1.docker_tags_whitelist | default(omit) }}"
+ docker_upstream_name: "{{ item.1.docker_upstream_name | default(omit) }}"
+ include_tags: "{{ item.1.include_tags | default(omit) }}"
+ exclude_tags: "{{ item.1.exclude_tags | default(omit) }}"
+ download_policy: "{{ item.1.download_policy | default(omit) }}"
+ gpg_key: "{{ item.1.gpg_key | default(omit) }}"
+ http_proxy: "{{ item.1.http_proxy | default(omit) }}"
+ http_proxy_policy: "{{ item.1.http_proxy_policy | default(omit) }}"
+ ignorable_content: "{{ item.1.ignorable_content | default(omit) }}"
+ ignore_global_proxy: "{{ item.1.ignore_global_proxy | default(omit) }}"
+ label: "{{ item.1.label | default(omit) }}"
+ mirror_on_sync: "{{ item.1.mirror_on_sync | default(omit) }}"
+ mirroring_policy: "{{ item.1.mirroring_policy | default(omit) }}"
+ os_versions: "{{ item.1.os_versions | default(omit) }}"
+ ssl_ca_cert: "{{ item.1.ssl_ca_cert | default(omit) }}"
+ ssl_client_cert: "{{ item.1.ssl_client_cert | default(omit) }}"
+ ssl_client_key: "{{ item.1.ssl_client_key | default(omit) }}"
+ state: present
+ unprotected: "{{ item.1.unprotected | default(omit) }}"
+ upstream_password: "{{ item.1.upstream_password | default(omit) }}"
+ upstream_username: "{{ item.1.upstream_username | default(omit) }}"
+ verify_ssl_on_sync: "{{ item.1.verify_ssl_on_sync | default(omit) }}"
+ ansible_collection_requirements: "{{ item.1.ansible_collection_requirements | default(omit) }}"
+ with_subelements:
+ - "{{ foreman_products | selectattr('repositories', 'defined') | list }}"
+ - repositories
diff --git a/ansible_collections/theforeman/foreman/roles/settings/README.md b/ansible_collections/theforeman/foreman/roles/settings/README.md
new file mode 100644
index 00000000..742804d7
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/settings/README.md
@@ -0,0 +1,31 @@
+theforeman.foreman.settings
+===========================
+
+This role creates and manages Settings.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_settings`. Each `setting` must contain the field `name` and may contain the optional field `value` which if empty will reset the setting to the default value.
+
+Example Playbook
+----------------
+
+Enable *Destroy associated VM on host delete* and disable *Clean up failed deployment*:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.settings
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_settings:
+ - name: destroy_vm_on_host_delete
+ value: true
+ - name: clean_up_failed_deployment
+ value: false
+```
diff --git a/ansible_collections/theforeman/foreman/roles/settings/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/settings/tasks/main.yml
new file mode 100644
index 00000000..f5a26bc9
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/settings/tasks/main.yml
@@ -0,0 +1,10 @@
+---
+- name: 'Create Settings'
+ theforeman.foreman.setting:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ value: "{{ item.value | default(omit) }}"
+ loop: "{{ foreman_settings }}"
diff --git a/ansible_collections/theforeman/foreman/roles/subnets/README.md b/ansible_collections/theforeman/foreman/roles/subnets/README.md
new file mode 100644
index 00000000..2a6f3fd1
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/subnets/README.md
@@ -0,0 +1,53 @@
+theforeman.foreman.subnets
+==========================
+
+This role creates and manages Subnets.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+The main data structure for this role is the list of `foreman_subnets`. Each `subnet` requires the following fields:
+
+- `name`: The name of the subnet.
+- `network`: Subnet IP address.
+
+For all other fields see the `subnet` module.
+
+Example Playbook
+----------------
+
+Create subnet `192.168.0.0/26`:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.subnets
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_subnets:
+ - 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:
+ - "Uppsala"
+```
diff --git a/ansible_collections/theforeman/foreman/roles/subnets/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/subnets/tasks/main.yml
new file mode 100644
index 00000000..7bb50ff5
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/subnets/tasks/main.yml
@@ -0,0 +1,38 @@
+---
+- name: 'Create Subnets' # noqa: args[module]
+ theforeman.foreman.subnet:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ name: "{{ item.name }}"
+ bmc_proxy: "{{ item.bmc_proxy | default(omit) }}"
+ boot_mode: "{{ item.boot_mode | default(omit) }}"
+ cidr: "{{ item.cidr | default(omit) }}"
+ description: "{{ item.description | default(omit) }}"
+ dhcp_proxy: "{{ item.dhcp_proxy | default(omit) }}"
+ discovery_proxy: "{{ item.discovery_proxy | default(omit) }}"
+ dns_primary: "{{ item.dns_primary | default(omit) }}"
+ dns_proxy: "{{ item.dns_proxy | default(omit) }}"
+ dns_secondary: "{{ item.dns_secondary | default(omit) }}"
+ domains: "{{ item.domains | default(omit) }}"
+ externalipam_group: "{{ item.externalipam_group | default(omit) }}"
+ externalipam_proxy: "{{ item.externalipam_proxy | default(omit) }}"
+ from_ip: "{{ item.from_ip | default(omit) }}"
+ gateway: "{{ item.gateway | default(omit) }}"
+ httpboot_proxy: "{{ item.httpboot_proxy | default(omit) }}"
+ ipam: "{{ item.ipam | default(omit) }}"
+ locations: "{{ item.locations | default(omit) }}"
+ mask: "{{ item.mask | default(omit) }}"
+ mtu: "{{ item.mtu | default(omit) }}"
+ network: "{{ item.network | default(omit) }}"
+ network_type: "{{ item.network_type | default(omit) }}"
+ organizations: "{{ item.organizations | default(omit) }}"
+ parameters: "{{ item.parameters | default(omit) }}"
+ remote_execution_proxies: "{{ item.remote_execution_proxies | default(omit) }}"
+ template_proxy: "{{ item.template_proxy | default(omit) }}"
+ tftp_proxy: "{{ item.tftp_proxy | default(omit) }}"
+ to_ip: "{{ item.to_ip | default(omit) }}"
+ vlanid: "{{ item.vlanid | default(omit) }}"
+ state: "{{ item.state | default('present') }}"
+ loop: "{{ foreman_subnets }}"
diff --git a/ansible_collections/theforeman/foreman/roles/sync_plans/README.md b/ansible_collections/theforeman/foreman/roles/sync_plans/README.md
new file mode 100644
index 00000000..179d8591
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/sync_plans/README.md
@@ -0,0 +1,84 @@
+theforeman.foreman.sync_plans
+=============================
+
+This role defines Sync Plans.
+
+Role Variables
+--------------
+
+This role supports the [Common Role Variables](https://github.com/theforeman/foreman-ansible-modules/blob/develop/README.md#common-role-variables).
+
+- `foreman_sync_plans`: List of sync plans to create. Each sync plan is represented as a dictionary which specifies the `name` of the sync plan and the `products` assigned to the sync plan. It also specifies the `interval` which can be 'hourly', 'daily', 'weekly', or 'custom cron'. In case the 'custom cron' `interval` is used, it should also specify the `cron_expression`. Finally the sync plan should have a `sync_date` which specifies the first time that the sync plan will run. Optionally the sync plan can be enabled and disabled using the `enabled` parameter, and its state can be managed using `state`.
+
+```yaml
+foreman_sync_plans:
+ - name: Weekly Sync
+ interval: weekly
+ sync_date: 2020-11-07 00:00:00 UTC
+ products:
+ - Red Hat Enterprise Linux Server
+ - Red Hat Software Collections (for RHEL Server)
+ - Red Hat Enterprise Linux for x86_64
+ - CentOS 8
+ - Debian 10
+ - name: Monthly Foreman Client Sync
+ interval: custom cron
+ cron_expression: 0 6 8 * *
+ sync_date: 2020-11-08 00:06:00 UTC
+ products:
+ - Foreman Client
+ - name: Weeky Ubuntu Sync (disabled)
+ interval: weekly
+ sync_date: 2020-11-07 00:00:00 UTC
+ products:
+ - Ubuntu 22.04
+ enabled: false
+```
+
+Example Playbooks
+-----------------
+
+Create two sync plans:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.sync_plans
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_sync_plans:
+ - name: Weekly Sync
+ interval: weekly
+ sync_date: 2020-11-07 00:00:00 UTC
+ products:
+ - Red Hat Enterprise Linux Server
+ - Red Hat Enterprise Linux for x86_64
+ - name: Daily Sync
+ interval: daily
+ sync_date: 2020-11-08 00:00:00 UTC
+ products:
+ - Red Hat Software Collections (for RHEL Server)
+```
+
+Create a single sync plan which has all defined products (those defined in the `foreman_products` dictionary in ansible vars, for example as defined in the role documentation for [theforeman.foreman.repositories](https://github.com/theforeman/foreman-ansible-modules/tree/develop/roles/repositories#role-variables)) assigned to it:
+
+```yaml
+- hosts: localhost
+ roles:
+ - role: theforeman.foreman.sync_plans
+ vars:
+ foreman_server_url: https://foreman.example.com
+ foreman_username: "admin"
+ foreman_password: "changeme"
+ foreman_organization: "Default Organization"
+ foreman_sync_plans:
+ - name: Weekly Sync
+ interval: weekly
+ sync_date: 2020-11-07 00:00:00 UTC
+ products: "{{ foreman_products | map(attribute='name') | list }}"
+```
+
+The above example assumes that a yaml dictionary `foreman_products` is already defined in Ansible variables. It uses yaml methods to select the name of each product from that dictionary, convert them all to a list, and pass that list to the definition of the sync plan.
diff --git a/ansible_collections/theforeman/foreman/roles/sync_plans/tasks/main.yml b/ansible_collections/theforeman/foreman/roles/sync_plans/tasks/main.yml
new file mode 100644
index 00000000..a98d613e
--- /dev/null
+++ b/ansible_collections/theforeman/foreman/roles/sync_plans/tasks/main.yml
@@ -0,0 +1,16 @@
+---
+- name: 'Create Sync Plans'
+ theforeman.foreman.sync_plan:
+ username: "{{ foreman_username | default(omit) }}"
+ password: "{{ foreman_password | default(omit) }}"
+ server_url: "{{ foreman_server_url | default(omit) }}"
+ validate_certs: "{{ foreman_validate_certs | default(omit) }}"
+ organization: "{{ foreman_organization }}"
+ name: "{{ item.name }}"
+ sync_date: "{{ item.sync_date }}"
+ interval: "{{ item.interval }}"
+ cron_expression: "{{ item.cron_expression | default(omit) }}"
+ enabled: "{{ item.enabled | default(true) }}"
+ products: "{{ item.products }}"
+ state: "{{ item.state | default(omit) }}"
+ with_items: "{{ foreman_sync_plans | default([]) }}"