From 640932cc0db6da294c98ec83b0b2df2aa8e9b1fe Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 15:32:48 -0400 Subject: [PATCH 01/18] Add cloud-connector as a native foremanctl feature Re-implements the upstream satellite_operations.cloud_connector role natively in foremanctl so users can enable it via: foremanctl deploy --add-feature cloud-connector The new role installs rhc and yggdrasil-worker-forwarder, templates the worker config, starts the rhcd service, and sets rhc_instance_id via the Foreman API. Optional HTTP proxy support is included. Works with both foremanctl deploy and forge deploy-dev (with appropriate credential overrides for the dev environment). Enforces mutual exclusion with the iop feature at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../playbooks/deploy-dev/deploy-dev.yaml | 6 ++ .../playbooks/deploy-dev/metadata.obsah.yaml | 3 + src/features.yaml | 4 + src/playbooks/deploy/deploy.yaml | 3 + src/playbooks/deploy/metadata.obsah.yaml | 3 + src/roles/cloud_connector/defaults/main.yaml | 6 ++ src/roles/cloud_connector/handlers/main.yaml | 6 ++ .../cloud_connector/tasks/http_proxy.yaml | 17 +++++ src/roles/cloud_connector/tasks/main.yaml | 73 +++++++++++++++++++ .../templates/foreman_rh_cloud.toml.j2 | 8 ++ .../cloud_connector/templates/proxy.conf.j2 | 3 + src/vars/base.yaml | 4 + tests/cloud_connector_test.py | 38 ++++++++++ 13 files changed, 174 insertions(+) create mode 100644 src/roles/cloud_connector/defaults/main.yaml create mode 100644 src/roles/cloud_connector/handlers/main.yaml create mode 100644 src/roles/cloud_connector/tasks/http_proxy.yaml create mode 100644 src/roles/cloud_connector/tasks/main.yaml create mode 100644 src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 create mode 100644 src/roles/cloud_connector/templates/proxy.conf.j2 create mode 100644 tests/cloud_connector_test.py diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index a4214e10e..ccddb6846 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -58,6 +58,12 @@ vars: iop_core_foreman_oauth_consumer_key: "{{ foreman_oauth_consumer_key }}" iop_core_foreman_oauth_consumer_secret: "{{ foreman_oauth_consumer_secret }}" + - role: cloud_connector + when: + - "'cloud-connector' in enabled_features" + vars: + cloud_connector_user: "{{ foreman_development_admin_user }}" + cloud_connector_password: "{{ foreman_development_admin_password }}" post_tasks: - name: Stop Foreman development service ansible.builtin.include_role: diff --git a/development/playbooks/deploy-dev/metadata.obsah.yaml b/development/playbooks/deploy-dev/metadata.obsah.yaml index 5cf4a4fdb..cb522c807 100644 --- a/development/playbooks/deploy-dev/metadata.obsah.yaml +++ b/development/playbooks/deploy-dev/metadata.obsah.yaml @@ -33,6 +33,9 @@ variables: parameter: --manage-repos help: Manage git repositories for Foreman and plugins. Set to false to skip cloning and checking out repositories. type: Boolean + cloud_connector_http_proxy: + parameter: --cloud-connector-http-proxy + help: HTTP proxy URL for the cloud connector rhcd service. include: - _flavor_features diff --git a/src/features.yaml b/src/features.yaml index 4f82adcb4..cf1027288 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -58,6 +58,10 @@ rh-cloud: hammer: foreman_rh_cloud dependencies: - katello +cloud-connector: + description: Cloud Connector for Red Hat Hybrid Cloud Console + dependencies: + - rh-cloud iop: description: iop services dependencies: diff --git a/src/playbooks/deploy/deploy.yaml b/src/playbooks/deploy/deploy.yaml index bd3304cb7..46829a537 100644 --- a/src/playbooks/deploy/deploy.yaml +++ b/src/playbooks/deploy/deploy.yaml @@ -42,4 +42,7 @@ - role: hammer when: - "'hammer' in enabled_features" + - role: cloud_connector + when: + - "'cloud-connector' in enabled_features" - post_install diff --git a/src/playbooks/deploy/metadata.obsah.yaml b/src/playbooks/deploy/metadata.obsah.yaml index 81744b368..bb042b436 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -14,6 +14,9 @@ variables: - ipa_with_api external_authentication_pam_service: help: Name of the PAM service to use for IPA authentication + cloud_connector_http_proxy: + parameter: --cloud-connector-http-proxy + help: HTTP proxy URL for the cloud connector rhcd service. include: - _flavor_features diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml new file mode 100644 index 000000000..a82805663 --- /dev/null +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -0,0 +1,6 @@ +--- +cloud_connector_url: "{{ foreman_url }}" +cloud_connector_user: "{{ foreman_initial_admin_username }}" +cloud_connector_password: "{{ foreman_initial_admin_password }}" +cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml +cloud_connector_validate_certs: true diff --git a/src/roles/cloud_connector/handlers/main.yaml b/src/roles/cloud_connector/handlers/main.yaml new file mode 100644 index 000000000..e08bcb1e6 --- /dev/null +++ b/src/roles/cloud_connector/handlers/main.yaml @@ -0,0 +1,6 @@ +--- +- name: Restart rhcd + ansible.builtin.service: + name: rhcd + state: restarted + daemon_reload: true diff --git a/src/roles/cloud_connector/tasks/http_proxy.yaml b/src/roles/cloud_connector/tasks/http_proxy.yaml new file mode 100644 index 000000000..2c5826ae7 --- /dev/null +++ b/src/roles/cloud_connector/tasks/http_proxy.yaml @@ -0,0 +1,17 @@ +--- +- name: Create systemd drop-in directory for rhcd + ansible.builtin.file: + state: directory + path: /etc/systemd/system/rhcd.service.d + owner: root + group: root + mode: '0755' + +- name: Deploy HTTP proxy systemd drop-in for rhcd + ansible.builtin.template: + src: proxy.conf.j2 + dest: /etc/systemd/system/rhcd.service.d/proxy.conf + owner: root + group: root + mode: '0644' + notify: Restart rhcd diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml new file mode 100644 index 000000000..8a24abfb9 --- /dev/null +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -0,0 +1,73 @@ +--- +- name: Verify cloud-connector is not used with iop + ansible.builtin.assert: + that: + - "'iop' not in enabled_features" + fail_msg: >- + The cloud-connector feature cannot be used together with the iop feature. + Remove one of them with --remove-feature before deploying. + +- name: Install rhc and yggdrasil-worker-forwarder + ansible.builtin.package: + name: + - rhc + - yggdrasil-worker-forwarder + disable_plugin: foreman-protector + +- name: Create workers directory + ansible.builtin.file: + state: directory + path: /etc/rhc/workers + owner: root + group: root + mode: '0755' + +- name: Configure rhc-cloud-connector-worker + ansible.builtin.template: + src: foreman_rh_cloud.toml.j2 + dest: "{{ cloud_connector_config_file }}" + owner: root + group: root + mode: '0640' + notify: Restart rhcd + +- name: Create rhcd worker script + ansible.builtin.copy: + dest: /usr/libexec/rhc/foreman-rh-cloud-worker + content: | + #!/bin/bash + + CONFIG_FILE="{{ cloud_connector_config_file }}" exec /usr/libexec/yggdrasil-worker-forwarder + owner: root + group: root + mode: '0755' + +- name: Ensure rhcd started and enabled + ansible.builtin.service: + name: rhcd + state: started + enabled: true + +- name: Read client ID from CN of consumer certificate + ansible.builtin.command: openssl x509 -in /etc/pki/consumer/cert.pem -subject -noout + register: __cloud_connector_cert_output + changed_when: false + +- name: Set rhc_instance_id in Foreman + ansible.builtin.uri: + url: "{{ cloud_connector_url }}/api/settings/rhc_instance_id" + user: "{{ cloud_connector_user }}" + password: "{{ cloud_connector_password }}" + body: + setting: + value: "{{ __cloud_connector_client_id }}" + method: PUT + validate_certs: "{{ cloud_connector_validate_certs }}" + force_basic_auth: true + body_format: json + vars: + __cloud_connector_client_id: "{{ __cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" + +- name: Configure HTTP proxy for rhcd + ansible.builtin.include_tasks: http_proxy.yaml + when: cloud_connector_http_proxy is defined diff --git a/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 b/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 new file mode 100644 index 000000000..0ed508931 --- /dev/null +++ b/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 @@ -0,0 +1,8 @@ +exec = "/usr/libexec/yggdrasil-worker-forwarder" +protocol = "grpc" +env = [ + "FORWARDER_USER={{ cloud_connector_user }}", + "FORWARDER_PASSWORD={{ cloud_connector_password }}", + "FORWARDER_URL={{ cloud_connector_url }}/api/v2/rh_cloud/cloud_request", + "FORWARDER_HANDLER=foreman_rh_cloud" +] diff --git a/src/roles/cloud_connector/templates/proxy.conf.j2 b/src/roles/cloud_connector/templates/proxy.conf.j2 new file mode 100644 index 000000000..3b59ade78 --- /dev/null +++ b/src/roles/cloud_connector/templates/proxy.conf.j2 @@ -0,0 +1,3 @@ +[Service] +Environment=HTTPS_PROXY={{ cloud_connector_http_proxy }} +Environment=NO_PROXY={{ cloud_connector_url | ansible.builtin.urlsplit('hostname') }} diff --git a/src/vars/base.yaml b/src/vars/base.yaml index ed3023aa9..0ef6ae395 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -63,3 +63,7 @@ backup_foreman_oauth_consumer_secret: "{{ foreman_oauth_consumer_secret }}" backup_foreman_ca_certificate: "{{ foreman_ca_certificate }}" backup_foreman_client_certificate: "{{ foreman_client_certificate }}" backup_foreman_client_key: "{{ foreman_client_key }}" + +cloud_connector_url: "{{ foreman_url }}" +cloud_connector_user: "{{ foreman_initial_admin_username }}" +cloud_connector_password: "{{ foreman_initial_admin_password }}" diff --git a/tests/cloud_connector_test.py b/tests/cloud_connector_test.py new file mode 100644 index 000000000..1869f80c4 --- /dev/null +++ b/tests/cloud_connector_test.py @@ -0,0 +1,38 @@ +import pytest + +pytestmark = pytest.mark.feature("cloud-connector") + + +def test_rhc_package_installed(server): + assert server.package("rhc").is_installed + + +def test_yggdrasil_worker_forwarder_package_installed(server): + assert server.package("yggdrasil-worker-forwarder").is_installed + + +def test_workers_directory_exists(server): + workers_dir = server.file("/etc/rhc/workers") + assert workers_dir.is_directory + assert workers_dir.mode == 0o755 + + +def test_worker_config_exists(server): + config = server.file("/etc/rhc/workers/foreman_rh_cloud.toml") + assert config.is_file + assert config.mode == 0o640 + assert config.contains("FORWARDER_HANDLER=foreman_rh_cloud") + assert config.contains("/api/v2/rh_cloud/cloud_request") + + +def test_worker_script_exists(server): + script = server.file("/usr/libexec/rhc/foreman-rh-cloud-worker") + assert script.is_file + assert script.mode == 0o755 + assert script.contains("yggdrasil-worker-forwarder") + + +def test_rhcd_service_running(server): + rhcd = server.service("rhcd") + assert rhcd.is_running + assert rhcd.is_enabled From 40ae75b491fc16a36fffe8364c923a46209e6bd5 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 16:07:31 -0400 Subject: [PATCH 02/18] Add early pre-checks for cloud-connector feature Move iop mutual exclusion and package availability checks into a new check_cloud_connector role that runs in the checks phase, before any services are deployed. This avoids a long deploy-dev run failing late when it reaches the cloud_connector role. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../playbooks/deploy-dev/deploy-dev.yaml | 5 ++++ .../check_cloud_connector/tasks/main.yaml | 25 +++++++++++++++++++ src/roles/checks/tasks/main.yml | 5 ++++ src/roles/cloud_connector/tasks/main.yaml | 8 ------ 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 src/roles/check_cloud_connector/tasks/main.yaml diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index ccddb6846..9ff011a16 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -14,6 +14,11 @@ httpd_foreman_backend: "http://localhost:3000" pulp_register_foreman_proxy: false pre_tasks: + - name: Check cloud-connector prerequisites + ansible.builtin.include_role: + name: check_cloud_connector + when: "'cloud-connector' in enabled_features" + - name: Set development postgresql databases ansible.builtin.set_fact: postgresql_databases: >- diff --git a/src/roles/check_cloud_connector/tasks/main.yaml b/src/roles/check_cloud_connector/tasks/main.yaml new file mode 100644 index 000000000..ae8c0388b --- /dev/null +++ b/src/roles/check_cloud_connector/tasks/main.yaml @@ -0,0 +1,25 @@ +--- +- name: Check cloud-connector prerequisites + when: "'cloud-connector' in enabled_features" + block: + - name: Verify cloud-connector is not used with iop + ansible.builtin.assert: + that: + - "'iop' not in enabled_features" + fail_msg: >- + The cloud-connector feature cannot be used together with the iop feature. + Remove one of them with --remove-feature before deploying. + + - name: Check that yggdrasil-worker-forwarder package is available + ansible.builtin.command: dnf info yggdrasil-worker-forwarder + changed_when: false + failed_when: false + register: __cloud_connector_pkg_check + + - name: Verify yggdrasil-worker-forwarder is available + ansible.builtin.assert: + that: + - __cloud_connector_pkg_check.rc == 0 + fail_msg: >- + The yggdrasil-worker-forwarder package is not available. + Ensure the appropriate repository is enabled. diff --git a/src/roles/checks/tasks/main.yml b/src/roles/checks/tasks/main.yml index 18765bfe3..2d281d51c 100644 --- a/src/roles/checks/tasks/main.yml +++ b/src/roles/checks/tasks/main.yml @@ -3,6 +3,11 @@ ansible.builtin.include_tasks: execute_check.yml loop: "{{ checks_to_execute }}" +- name: Run cloud connector checks + ansible.builtin.include_role: + name: check_cloud_connector + when: enabled_features | has_feature('cloud-connector') + - name: Run database index integrity checks ansible.builtin.include_role: name: check_database_index diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 8a24abfb9..80e95edd3 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -1,12 +1,4 @@ --- -- name: Verify cloud-connector is not used with iop - ansible.builtin.assert: - that: - - "'iop' not in enabled_features" - fail_msg: >- - The cloud-connector feature cannot be used together with the iop feature. - Remove one of them with --remove-feature before deploying. - - name: Install rhc and yggdrasil-worker-forwarder ansible.builtin.package: name: From fd1892ec638b8722569dacc14161085243a32fee Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 16:14:49 -0400 Subject: [PATCH 03/18] Fix SSL cert verification for Foreman API call Use ca_path with the Foreman CA certificate instead of validate_certs, matching the pattern used by other roles (foreman, check_foreman_api). The self-signed CA cert is always available in the deploy context. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/defaults/main.yaml | 1 - src/roles/cloud_connector/tasks/main.yaml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index a82805663..ccb65b40a 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -3,4 +3,3 @@ cloud_connector_url: "{{ foreman_url }}" cloud_connector_user: "{{ foreman_initial_admin_username }}" cloud_connector_password: "{{ foreman_initial_admin_password }}" cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml -cloud_connector_validate_certs: true diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 80e95edd3..8fd723219 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -54,7 +54,7 @@ setting: value: "{{ __cloud_connector_client_id }}" method: PUT - validate_certs: "{{ cloud_connector_validate_certs }}" + ca_path: "{{ foreman_ca_certificate }}" force_basic_auth: true body_format: json vars: From 4f1c3bb592517d02fd872aac229408a2cd41b7fe Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 12:12:04 -0400 Subject: [PATCH 04/18] Call announce_to_sources after setting rhc_instance_id After setting the rhc_instance_id, POST to the new /api/v2/rh_cloud/announce_to_sources endpoint to register the Satellite in Sources on console.redhat.com. This replaces the Ruby-side CloudConnectorAnnounceTask that previously triggered on REX job completion. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/tasks/main.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 8fd723219..b6af05cce 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -60,6 +60,16 @@ vars: __cloud_connector_client_id: "{{ __cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" +- name: Announce Satellite to Sources + ansible.builtin.uri: + url: "{{ cloud_connector_url }}/api/v2/rh_cloud/announce_to_sources" + user: "{{ cloud_connector_user }}" + password: "{{ cloud_connector_password }}" + method: POST + ca_path: "{{ foreman_ca_certificate }}" + force_basic_auth: true + status_code: [200, 201] + - name: Configure HTTP proxy for rhcd ansible.builtin.include_tasks: http_proxy.yaml when: cloud_connector_http_proxy is defined From b1f41f1ec284dc37c64c802e820053db66606fda Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:11:59 -0400 Subject: [PATCH 05/18] Fix TLS verification for cloud connector worker The yggdrasil-worker-forwarder binary uses the OS trust store and doesn't accept a CA path argument. Add the Foreman CA certificate to the system trust store so the worker can verify Foreman's self-signed certificate when forwarding cloud requests. Also fix Content-Type header on the announce_to_sources POST. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/tasks/main.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index b6af05cce..4558cfbef 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -34,6 +34,20 @@ group: root mode: '0755' +- name: Add Foreman CA to system trust store + ansible.builtin.copy: + src: "{{ foreman_ca_certificate }}" + dest: /etc/pki/ca-trust/source/anchors/foreman-ca.pem + remote_src: true + owner: root + group: root + mode: '0644' + notify: Restart rhcd + +- name: Update system CA trust + ansible.builtin.command: update-ca-trust + changed_when: true + - name: Ensure rhcd started and enabled ansible.builtin.service: name: rhcd @@ -68,6 +82,7 @@ method: POST ca_path: "{{ foreman_ca_certificate }}" force_basic_auth: true + body_format: json status_code: [200, 201] - name: Configure HTTP proxy for rhcd From fd45777b9d363421b7eb83f1623192c99f50e8d8 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:22:33 -0400 Subject: [PATCH 06/18] Enable automatic inventory upload during cloud-connector setup Set allow_auto_inventory_upload to true via the Foreman API, matching the previous cloud connector setup behavior. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/tasks/main.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 4558cfbef..86cc6eb41 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -74,6 +74,19 @@ vars: __cloud_connector_client_id: "{{ __cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" +- name: Enable automatic inventory upload + ansible.builtin.uri: + url: "{{ cloud_connector_url }}/api/settings/allow_auto_inventory_upload" + user: "{{ cloud_connector_user }}" + password: "{{ cloud_connector_password }}" + body: + setting: + value: true + method: PUT + ca_path: "{{ foreman_ca_certificate }}" + force_basic_auth: true + body_format: json + - name: Announce Satellite to Sources ansible.builtin.uri: url: "{{ cloud_connector_url }}/api/v2/rh_cloud/announce_to_sources" From 8be79b55181d2a9a7b3dbd0cb320bc8b90320001 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:30:35 -0400 Subject: [PATCH 07/18] Add consumer certificate pre-check for cloud-connector Verify /etc/pki/consumer/cert.pem exists early in the checks phase, since the cloud_connector role needs it to derive the rhc_instance_id from the certificate CN. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/check_cloud_connector/tasks/main.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/roles/check_cloud_connector/tasks/main.yaml b/src/roles/check_cloud_connector/tasks/main.yaml index ae8c0388b..42848a5fe 100644 --- a/src/roles/check_cloud_connector/tasks/main.yaml +++ b/src/roles/check_cloud_connector/tasks/main.yaml @@ -10,6 +10,19 @@ The cloud-connector feature cannot be used together with the iop feature. Remove one of them with --remove-feature before deploying. + - name: Check that consumer certificate exists + ansible.builtin.stat: + path: /etc/pki/consumer/cert.pem + register: __cloud_connector_consumer_cert + + - name: Verify consumer certificate exists + ansible.builtin.assert: + that: + - __cloud_connector_consumer_cert.stat.exists + fail_msg: >- + /etc/pki/consumer/cert.pem not found. + The system must be registered with subscription-manager. + - name: Check that yggdrasil-worker-forwarder package is available ansible.builtin.command: dnf info yggdrasil-worker-forwarder changed_when: false From f679cdc8cf88403ce31e6fe3c3f4e32ef9bb287c Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 15:47:41 -0400 Subject: [PATCH 08/18] Address code review feedback - Remove cross-role variable references from defaults (use standalone fallback values; base.yaml provides the real overrides) - Rename task "Configure rhc-cloud-connector-worker" for consistency - Rename "Announce Satellite to Sources" to "Announce to Sources" - Fix var-naming lint: use role-prefixed variable names instead of double-underscore prefix Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/check_cloud_connector/tasks/main.yaml | 8 ++++---- src/roles/cloud_connector/defaults/main.yaml | 6 +++--- src/roles/cloud_connector/tasks/main.yaml | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/roles/check_cloud_connector/tasks/main.yaml b/src/roles/check_cloud_connector/tasks/main.yaml index 42848a5fe..e661437ba 100644 --- a/src/roles/check_cloud_connector/tasks/main.yaml +++ b/src/roles/check_cloud_connector/tasks/main.yaml @@ -13,12 +13,12 @@ - name: Check that consumer certificate exists ansible.builtin.stat: path: /etc/pki/consumer/cert.pem - register: __cloud_connector_consumer_cert + register: check_cloud_connector_consumer_cert - name: Verify consumer certificate exists ansible.builtin.assert: that: - - __cloud_connector_consumer_cert.stat.exists + - check_cloud_connector_consumer_cert.stat.exists fail_msg: >- /etc/pki/consumer/cert.pem not found. The system must be registered with subscription-manager. @@ -27,12 +27,12 @@ ansible.builtin.command: dnf info yggdrasil-worker-forwarder changed_when: false failed_when: false - register: __cloud_connector_pkg_check + register: check_cloud_connector_pkg_check - name: Verify yggdrasil-worker-forwarder is available ansible.builtin.assert: that: - - __cloud_connector_pkg_check.rc == 0 + - check_cloud_connector_pkg_check.rc == 0 fail_msg: >- The yggdrasil-worker-forwarder package is not available. Ensure the appropriate repository is enabled. diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index ccb65b40a..cd4987c11 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -1,5 +1,5 @@ --- -cloud_connector_url: "{{ foreman_url }}" -cloud_connector_user: "{{ foreman_initial_admin_username }}" -cloud_connector_password: "{{ foreman_initial_admin_password }}" +cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" +cloud_connector_user: admin +cloud_connector_password: changeme cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 86cc6eb41..93b56dd01 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -14,7 +14,7 @@ group: root mode: '0755' -- name: Configure rhc-cloud-connector-worker +- name: Configure foreman-rh-cloud worker ansible.builtin.template: src: foreman_rh_cloud.toml.j2 dest: "{{ cloud_connector_config_file }}" @@ -56,7 +56,7 @@ - name: Read client ID from CN of consumer certificate ansible.builtin.command: openssl x509 -in /etc/pki/consumer/cert.pem -subject -noout - register: __cloud_connector_cert_output + register: cloud_connector_cert_output changed_when: false - name: Set rhc_instance_id in Foreman @@ -66,13 +66,13 @@ password: "{{ cloud_connector_password }}" body: setting: - value: "{{ __cloud_connector_client_id }}" + value: "{{ cloud_connector_client_id }}" method: PUT ca_path: "{{ foreman_ca_certificate }}" force_basic_auth: true body_format: json vars: - __cloud_connector_client_id: "{{ __cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" + cloud_connector_client_id: "{{ cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" - name: Enable automatic inventory upload ansible.builtin.uri: @@ -87,7 +87,7 @@ force_basic_auth: true body_format: json -- name: Announce Satellite to Sources +- name: Announce to Sources ansible.builtin.uri: url: "{{ cloud_connector_url }}/api/v2/rh_cloud/announce_to_sources" user: "{{ cloud_connector_user }}" From 02d2c5165bb6afc763ced68b153389bc8ded5fdf Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 15 Jun 2026 13:04:26 -0400 Subject: [PATCH 09/18] Address code review feedback - Use ansible.builtin.systemd_service instead of service for handler - Remove redundant workers directory task (rhc package creates it) - Use theforeman.foreman.setting module instead of raw uri for settings - Add noqa for static secret in role defaults Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/defaults/main.yaml | 2 +- src/roles/cloud_connector/handlers/main.yaml | 2 +- src/roles/cloud_connector/tasks/main.yaml | 36 ++++++-------------- 3 files changed, 12 insertions(+), 28 deletions(-) diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index cd4987c11..0683ad84b 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -1,5 +1,5 @@ --- cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" cloud_connector_user: admin -cloud_connector_password: changeme +cloud_connector_password: changeme # noqa: no-static-secrets cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml diff --git a/src/roles/cloud_connector/handlers/main.yaml b/src/roles/cloud_connector/handlers/main.yaml index e08bcb1e6..ef076b5ed 100644 --- a/src/roles/cloud_connector/handlers/main.yaml +++ b/src/roles/cloud_connector/handlers/main.yaml @@ -1,6 +1,6 @@ --- - name: Restart rhcd - ansible.builtin.service: + ansible.builtin.systemd_service: name: rhcd state: restarted daemon_reload: true diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 93b56dd01..d029a3570 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -6,14 +6,6 @@ - yggdrasil-worker-forwarder disable_plugin: foreman-protector -- name: Create workers directory - ansible.builtin.file: - state: directory - path: /etc/rhc/workers - owner: root - group: root - mode: '0755' - - name: Configure foreman-rh-cloud worker ansible.builtin.template: src: foreman_rh_cloud.toml.j2 @@ -60,32 +52,24 @@ changed_when: false - name: Set rhc_instance_id in Foreman - ansible.builtin.uri: - url: "{{ cloud_connector_url }}/api/settings/rhc_instance_id" - user: "{{ cloud_connector_user }}" + theforeman.foreman.setting: + name: rhc_instance_id + value: "{{ cloud_connector_client_id }}" + server_url: "{{ cloud_connector_url }}" + username: "{{ cloud_connector_user }}" password: "{{ cloud_connector_password }}" - body: - setting: - value: "{{ cloud_connector_client_id }}" - method: PUT ca_path: "{{ foreman_ca_certificate }}" - force_basic_auth: true - body_format: json vars: cloud_connector_client_id: "{{ cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" - name: Enable automatic inventory upload - ansible.builtin.uri: - url: "{{ cloud_connector_url }}/api/settings/allow_auto_inventory_upload" - user: "{{ cloud_connector_user }}" + theforeman.foreman.setting: + name: allow_auto_inventory_upload + value: true + server_url: "{{ cloud_connector_url }}" + username: "{{ cloud_connector_user }}" password: "{{ cloud_connector_password }}" - body: - setting: - value: true - method: PUT ca_path: "{{ foreman_ca_certificate }}" - force_basic_auth: true - body_format: json - name: Announce to Sources ansible.builtin.uri: From 5e332f3dc7c2299bfcf45c1953afc93ff0ff0a31 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 15 Jun 2026 14:00:02 -0400 Subject: [PATCH 10/18] Use dedicated service user for cloud connector worker Instead of storing admin credentials in the worker config, create a dedicated cloud_connector_user with a limited role that only grants dispatch_cloud_requests permission. The service user password is generated and persisted like other foremanctl secrets. Admin credentials are still used for the FAM calls to create the user/role and manage settings, but they no longer end up on disk in the worker config file. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/defaults/main.yaml | 2 ++ src/roles/cloud_connector/tasks/main.yaml | 27 +++++++++++++++++++ .../templates/foreman_rh_cloud.toml.j2 | 4 +-- src/vars/base.yaml | 5 ++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index 0683ad84b..40dc41b4b 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -2,4 +2,6 @@ cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" cloud_connector_user: admin cloud_connector_password: changeme # noqa: no-static-secrets +cloud_connector_service_user: cloud_connector_user +cloud_connector_service_password: changeme # noqa: no-static-secrets cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index d029a3570..ea05e6f14 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -6,6 +6,33 @@ - yggdrasil-worker-forwarder disable_plugin: foreman-protector +- name: Create cloud connector role + theforeman.foreman.role: + name: Cloud Connector + filters: + - permissions: + - dispatch_cloud_requests + server_url: "{{ cloud_connector_url }}" + username: "{{ cloud_connector_user }}" + password: "{{ cloud_connector_password }}" + ca_path: "{{ foreman_ca_certificate }}" + state: present + +- name: Create cloud connector service user + theforeman.foreman.user: + login: "{{ cloud_connector_service_user }}" + user_password: "{{ cloud_connector_service_password }}" + mail: "{{ cloud_connector_service_user }}@localhost" + auth_source: Internal + roles: + - Cloud Connector + admin: false + server_url: "{{ cloud_connector_url }}" + username: "{{ cloud_connector_user }}" + password: "{{ cloud_connector_password }}" + ca_path: "{{ foreman_ca_certificate }}" + state: present + - name: Configure foreman-rh-cloud worker ansible.builtin.template: src: foreman_rh_cloud.toml.j2 diff --git a/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 b/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 index 0ed508931..aab32dd7e 100644 --- a/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 +++ b/src/roles/cloud_connector/templates/foreman_rh_cloud.toml.j2 @@ -1,8 +1,8 @@ exec = "/usr/libexec/yggdrasil-worker-forwarder" protocol = "grpc" env = [ - "FORWARDER_USER={{ cloud_connector_user }}", - "FORWARDER_PASSWORD={{ cloud_connector_password }}", + "FORWARDER_USER={{ cloud_connector_service_user }}", + "FORWARDER_PASSWORD={{ cloud_connector_service_password }}", "FORWARDER_URL={{ cloud_connector_url }}/api/v2/rh_cloud/cloud_request", "FORWARDER_HANDLER=foreman_rh_cloud" ] diff --git a/src/vars/base.yaml b/src/vars/base.yaml index 0ef6ae395..8b2b4b4ce 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -67,3 +67,8 @@ backup_foreman_client_key: "{{ foreman_client_key }}" cloud_connector_url: "{{ foreman_url }}" cloud_connector_user: "{{ foreman_initial_admin_username }}" cloud_connector_password: "{{ foreman_initial_admin_password }}" +cloud_connector_service_user: cloud_connector_user +cloud_connector_service_password_file: "{{ obsah_state_path }}/cloud-connector-service-password" +cloud_connector_service_password: >- + {{ lookup('ansible.builtin.password', cloud_connector_service_password_file, + chars=['ascii_letters', 'digits'], length=32) }} From 07f6e93ff419aeae46e292588af94bb3bcdafc65 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 15 Jun 2026 15:44:32 -0400 Subject: [PATCH 11/18] Rename cloud_connector_user/password to cloud_connector_admin_* Clarifies that these are the admin credentials used for Foreman API calls during setup, distinct from cloud_connector_service_user/password which are the limited-permission credentials baked into the worker config. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../playbooks/deploy-dev/deploy-dev.yaml | 4 ++-- src/roles/cloud_connector/defaults/main.yaml | 6 +++--- src/roles/cloud_connector/tasks/main.yaml | 20 +++++++++---------- src/vars/base.yaml | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index 9ff011a16..a060dde3b 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -67,8 +67,8 @@ when: - "'cloud-connector' in enabled_features" vars: - cloud_connector_user: "{{ foreman_development_admin_user }}" - cloud_connector_password: "{{ foreman_development_admin_password }}" + cloud_connector_admin_user: "{{ foreman_development_admin_user }}" + cloud_connector_admin_password: "{{ foreman_development_admin_password }}" post_tasks: - name: Stop Foreman development service ansible.builtin.include_role: diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index 40dc41b4b..69784f5f2 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -1,7 +1,7 @@ --- cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" -cloud_connector_user: admin -cloud_connector_password: changeme # noqa: no-static-secrets -cloud_connector_service_user: cloud_connector_user +cloud_connector_admin_user: admin +cloud_connector_admin_password: changeme # noqa: no-static-secrets +cloud_connector_service_user: cloud_connector_admin_user cloud_connector_service_password: changeme # noqa: no-static-secrets cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index ea05e6f14..3bbf86516 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -13,8 +13,8 @@ - permissions: - dispatch_cloud_requests server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_user }}" - password: "{{ cloud_connector_password }}" + username: "{{ cloud_connector_admin_user }}" + password: "{{ cloud_connector_admin_password }}" ca_path: "{{ foreman_ca_certificate }}" state: present @@ -28,8 +28,8 @@ - Cloud Connector admin: false server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_user }}" - password: "{{ cloud_connector_password }}" + username: "{{ cloud_connector_admin_user }}" + password: "{{ cloud_connector_admin_password }}" ca_path: "{{ foreman_ca_certificate }}" state: present @@ -83,8 +83,8 @@ name: rhc_instance_id value: "{{ cloud_connector_client_id }}" server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_user }}" - password: "{{ cloud_connector_password }}" + username: "{{ cloud_connector_admin_user }}" + password: "{{ cloud_connector_admin_password }}" ca_path: "{{ foreman_ca_certificate }}" vars: cloud_connector_client_id: "{{ cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" @@ -94,15 +94,15 @@ name: allow_auto_inventory_upload value: true server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_user }}" - password: "{{ cloud_connector_password }}" + username: "{{ cloud_connector_admin_user }}" + password: "{{ cloud_connector_admin_password }}" ca_path: "{{ foreman_ca_certificate }}" - name: Announce to Sources ansible.builtin.uri: url: "{{ cloud_connector_url }}/api/v2/rh_cloud/announce_to_sources" - user: "{{ cloud_connector_user }}" - password: "{{ cloud_connector_password }}" + user: "{{ cloud_connector_admin_user }}" + password: "{{ cloud_connector_admin_password }}" method: POST ca_path: "{{ foreman_ca_certificate }}" force_basic_auth: true diff --git a/src/vars/base.yaml b/src/vars/base.yaml index 8b2b4b4ce..0c8f00b9a 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -65,8 +65,8 @@ backup_foreman_client_certificate: "{{ foreman_client_certificate }}" backup_foreman_client_key: "{{ foreman_client_key }}" cloud_connector_url: "{{ foreman_url }}" -cloud_connector_user: "{{ foreman_initial_admin_username }}" -cloud_connector_password: "{{ foreman_initial_admin_password }}" +cloud_connector_admin_user: "{{ foreman_initial_admin_username }}" +cloud_connector_admin_password: "{{ foreman_initial_admin_password }}" cloud_connector_service_user: cloud_connector_user cloud_connector_service_password_file: "{{ obsah_state_path }}/cloud-connector-service-password" cloud_connector_service_password: >- From 7e0a611abaebdb3159a33d5ede06bd5a83f8f1a5 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 24 Jun 2026 13:15:15 -0400 Subject: [PATCH 12/18] Address code review feedback - Make update-ca-trust a handler triggered by CA file changes instead of an always-changed task, fixing idempotency - Move check_cloud_connector out of the checks loop and call it separately with a when clause, matching the check_database_index pattern Co-Authored-By: Claude Opus 4.6 (1M context) --- .../check_cloud_connector/tasks/main.yaml | 63 +++++++++---------- src/roles/cloud_connector/handlers/main.yaml | 6 ++ src/roles/cloud_connector/tasks/main.yaml | 6 +- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/roles/check_cloud_connector/tasks/main.yaml b/src/roles/check_cloud_connector/tasks/main.yaml index e661437ba..7a3286c9e 100644 --- a/src/roles/check_cloud_connector/tasks/main.yaml +++ b/src/roles/check_cloud_connector/tasks/main.yaml @@ -1,38 +1,35 @@ --- -- name: Check cloud-connector prerequisites - when: "'cloud-connector' in enabled_features" - block: - - name: Verify cloud-connector is not used with iop - ansible.builtin.assert: - that: - - "'iop' not in enabled_features" - fail_msg: >- - The cloud-connector feature cannot be used together with the iop feature. - Remove one of them with --remove-feature before deploying. +- name: Verify cloud-connector is not used with iop + ansible.builtin.assert: + that: + - "'iop' not in enabled_features" + fail_msg: >- + The cloud-connector feature cannot be used together with the iop feature. + Remove one of them with --remove-feature before deploying. - - name: Check that consumer certificate exists - ansible.builtin.stat: - path: /etc/pki/consumer/cert.pem - register: check_cloud_connector_consumer_cert +- name: Check that consumer certificate exists + ansible.builtin.stat: + path: /etc/pki/consumer/cert.pem + register: check_cloud_connector_consumer_cert - - name: Verify consumer certificate exists - ansible.builtin.assert: - that: - - check_cloud_connector_consumer_cert.stat.exists - fail_msg: >- - /etc/pki/consumer/cert.pem not found. - The system must be registered with subscription-manager. +- name: Verify consumer certificate exists + ansible.builtin.assert: + that: + - check_cloud_connector_consumer_cert.stat.exists + fail_msg: >- + /etc/pki/consumer/cert.pem not found. + The system must be registered with subscription-manager. - - name: Check that yggdrasil-worker-forwarder package is available - ansible.builtin.command: dnf info yggdrasil-worker-forwarder - changed_when: false - failed_when: false - register: check_cloud_connector_pkg_check +- name: Check that yggdrasil-worker-forwarder package is available + ansible.builtin.command: dnf info yggdrasil-worker-forwarder + changed_when: false + failed_when: false + register: check_cloud_connector_pkg_check - - name: Verify yggdrasil-worker-forwarder is available - ansible.builtin.assert: - that: - - check_cloud_connector_pkg_check.rc == 0 - fail_msg: >- - The yggdrasil-worker-forwarder package is not available. - Ensure the appropriate repository is enabled. +- name: Verify yggdrasil-worker-forwarder is available + ansible.builtin.assert: + that: + - check_cloud_connector_pkg_check.rc == 0 + fail_msg: >- + The yggdrasil-worker-forwarder package is not available. + Ensure the appropriate repository is enabled. diff --git a/src/roles/cloud_connector/handlers/main.yaml b/src/roles/cloud_connector/handlers/main.yaml index ef076b5ed..310a2dd98 100644 --- a/src/roles/cloud_connector/handlers/main.yaml +++ b/src/roles/cloud_connector/handlers/main.yaml @@ -1,6 +1,12 @@ --- +- name: Update system CA trust + ansible.builtin.command: update-ca-trust + changed_when: true # noqa: no-changed-when + listen: Foreman CA changed + - name: Restart rhcd ansible.builtin.systemd_service: name: rhcd state: restarted daemon_reload: true + listen: Foreman CA changed diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 3bbf86516..49b465c16 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -61,11 +61,7 @@ owner: root group: root mode: '0644' - notify: Restart rhcd - -- name: Update system CA trust - ansible.builtin.command: update-ca-trust - changed_when: true + notify: Foreman CA changed - name: Ensure rhcd started and enabled ansible.builtin.service: From 5b6891cb0a13e3a3e82cd572430ff4cd90f05783 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Thu, 25 Jun 2026 14:44:56 -0400 Subject: [PATCH 13/18] Fix service user login name in defaults The default was accidentally set to 'cloud_connector_admin_user' (a leftover from the rename) instead of the intended login name 'cloud_connector_user'. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/defaults/main.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index 69784f5f2..c98186357 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -2,6 +2,6 @@ cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" cloud_connector_admin_user: admin cloud_connector_admin_password: changeme # noqa: no-static-secrets -cloud_connector_service_user: cloud_connector_admin_user +cloud_connector_service_user: cloud_connector_user cloud_connector_service_password: changeme # noqa: no-static-secrets cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml From 93edc704994c0c86204368fd8063cc9c4adcefa9 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 26 Jun 2026 15:08:21 -0400 Subject: [PATCH 14/18] Make CA certificate path optional in cloud_connector role Use default(omit) for ca_path so the role works in environments where foreman_ca_certificate is not defined (e.g. HTTP-only dev setups). Also skip the CA trust store copy when undefined. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/tasks/main.yaml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 49b465c16..f5cbd6e80 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -15,7 +15,7 @@ server_url: "{{ cloud_connector_url }}" username: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" - ca_path: "{{ foreman_ca_certificate }}" + ca_path: "{{ foreman_ca_certificate | default(omit) }}" state: present - name: Create cloud connector service user @@ -30,7 +30,7 @@ server_url: "{{ cloud_connector_url }}" username: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" - ca_path: "{{ foreman_ca_certificate }}" + ca_path: "{{ foreman_ca_certificate | default(omit) }}" state: present - name: Configure foreman-rh-cloud worker @@ -62,6 +62,7 @@ group: root mode: '0644' notify: Foreman CA changed + when: foreman_ca_certificate is defined - name: Ensure rhcd started and enabled ansible.builtin.service: @@ -81,7 +82,7 @@ server_url: "{{ cloud_connector_url }}" username: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" - ca_path: "{{ foreman_ca_certificate }}" + ca_path: "{{ foreman_ca_certificate | default(omit) }}" vars: cloud_connector_client_id: "{{ cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" @@ -92,7 +93,7 @@ server_url: "{{ cloud_connector_url }}" username: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" - ca_path: "{{ foreman_ca_certificate }}" + ca_path: "{{ foreman_ca_certificate | default(omit) }}" - name: Announce to Sources ansible.builtin.uri: @@ -100,7 +101,7 @@ user: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" method: POST - ca_path: "{{ foreman_ca_certificate }}" + ca_path: "{{ foreman_ca_certificate | default(omit) }}" force_basic_auth: true body_format: json status_code: [200, 201] From 34cd0dfd4f04f45eb56eca711766b47fd1500a64 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 26 Jun 2026 15:11:29 -0400 Subject: [PATCH 15/18] Remove foreman-protector disable_plugin from package install foreman-protector is not used in foremanctl deployments. The disable_plugin parameter was carried over from the upstream role where it was needed for installer-based deployments. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/tasks/main.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index f5cbd6e80..adf217fe5 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -4,7 +4,6 @@ name: - rhc - yggdrasil-worker-forwarder - disable_plugin: foreman-protector - name: Create cloud connector role theforeman.foreman.role: From 014cc7ed772a10a6c79efc35af9bbdaa18a43bcf Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Tue, 30 Jun 2026 16:36:50 -0400 Subject: [PATCH 16/18] Address code review feedback - Remove redundant yggdrasil package pre-check (let dnf fail on its own) - Use has_feature filter for cloud-connector check - Tighten proxy drop-in to 0640 and worker script to 0750 - Use community.crypto.x509_certificate_info instead of shelling out to openssl for reading the consumer certificate CN - Enable foreman_rh_cloud plugin in deploy-dev when cloud-connector is enabled - Remove proxy systemd drop-in when cloud_connector_http_proxy is unset Co-Authored-By: Claude Opus 4.6 (1M context) --- .../playbooks/deploy-dev/deploy-dev.yaml | 8 +++++ .../check_cloud_connector/tasks/main.yaml | 14 --------- .../cloud_connector/tasks/http_proxy.yaml | 2 +- src/roles/cloud_connector/tasks/main.yaml | 29 ++++++++++++++----- 4 files changed, 30 insertions(+), 23 deletions(-) diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index a060dde3b..151db5522 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -44,6 +44,14 @@ - name: Enable foreman_ansible plugin for iop ansible.builtin.set_fact: foreman_development_enabled_plugins: "{{ foreman_development_enabled_plugins + ['foreman_ansible'] }}" + + - name: Setup cloud-connector requirements + when: + - "'cloud-connector' in enabled_features" + block: + - name: Enable foreman_rh_cloud plugin for cloud-connector + ansible.builtin.set_fact: + foreman_development_enabled_plugins: "{{ foreman_development_enabled_plugins + ['foreman_rh_cloud'] }}" roles: - role: pre_install - role: certificates diff --git a/src/roles/check_cloud_connector/tasks/main.yaml b/src/roles/check_cloud_connector/tasks/main.yaml index 7a3286c9e..c1828ec53 100644 --- a/src/roles/check_cloud_connector/tasks/main.yaml +++ b/src/roles/check_cloud_connector/tasks/main.yaml @@ -19,17 +19,3 @@ fail_msg: >- /etc/pki/consumer/cert.pem not found. The system must be registered with subscription-manager. - -- name: Check that yggdrasil-worker-forwarder package is available - ansible.builtin.command: dnf info yggdrasil-worker-forwarder - changed_when: false - failed_when: false - register: check_cloud_connector_pkg_check - -- name: Verify yggdrasil-worker-forwarder is available - ansible.builtin.assert: - that: - - check_cloud_connector_pkg_check.rc == 0 - fail_msg: >- - The yggdrasil-worker-forwarder package is not available. - Ensure the appropriate repository is enabled. diff --git a/src/roles/cloud_connector/tasks/http_proxy.yaml b/src/roles/cloud_connector/tasks/http_proxy.yaml index 2c5826ae7..7a8d7d511 100644 --- a/src/roles/cloud_connector/tasks/http_proxy.yaml +++ b/src/roles/cloud_connector/tasks/http_proxy.yaml @@ -13,5 +13,5 @@ dest: /etc/systemd/system/rhcd.service.d/proxy.conf owner: root group: root - mode: '0644' + mode: '0640' notify: Restart rhcd diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index adf217fe5..72c067c19 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -50,7 +50,7 @@ CONFIG_FILE="{{ cloud_connector_config_file }}" exec /usr/libexec/yggdrasil-worker-forwarder owner: root group: root - mode: '0755' + mode: '0750' - name: Add Foreman CA to system trust store ansible.builtin.copy: @@ -69,21 +69,19 @@ state: started enabled: true -- name: Read client ID from CN of consumer certificate - ansible.builtin.command: openssl x509 -in /etc/pki/consumer/cert.pem -subject -noout - register: cloud_connector_cert_output - changed_when: false +- name: Read client ID from consumer certificate + community.crypto.x509_certificate_info: + path: /etc/pki/consumer/cert.pem + register: cloud_connector_cert_info - name: Set rhc_instance_id in Foreman theforeman.foreman.setting: name: rhc_instance_id - value: "{{ cloud_connector_client_id }}" + value: "{{ cloud_connector_cert_info.subject.commonName }}" server_url: "{{ cloud_connector_url }}" username: "{{ cloud_connector_admin_user }}" password: "{{ cloud_connector_admin_password }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" - vars: - cloud_connector_client_id: "{{ cloud_connector_cert_output.stdout | regex_search('CN\\s?=\\s?([a-z0-9-]+)', '\\1') | first }}" - name: Enable automatic inventory upload theforeman.foreman.setting: @@ -108,3 +106,18 @@ - name: Configure HTTP proxy for rhcd ansible.builtin.include_tasks: http_proxy.yaml when: cloud_connector_http_proxy is defined + +- name: Remove HTTP proxy configuration for rhcd + when: cloud_connector_http_proxy is not defined + block: + - name: Remove HTTP proxy systemd drop-in for rhcd + ansible.builtin.file: + path: /etc/systemd/system/rhcd.service.d/proxy.conf + state: absent + notify: Restart rhcd + + - name: Remove systemd drop-in directory if empty + ansible.builtin.file: + path: /etc/systemd/system/rhcd.service.d + state: absent + notify: Restart rhcd From 9b8b99d71726bc1eefc850cadc6ae9b59e05548f Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 6 Jul 2026 14:05:41 -0400 Subject: [PATCH 17/18] Switch cloud connector API auth from admin credentials to OAuth Replace username/password authentication with oauth1_consumer_key/secret on all Foreman API calls during cloud connector setup, matching the pattern used by foreman_proxy, iop_core, pulp, and backup roles. Add a reusable foremanctl_api custom module for OAuth-authenticated API calls where no dedicated FAM module exists (e.g. announce_to_sources). Admin credentials are no longer needed or stored. The service user (with limited dispatch_cloud_requests permission) remains for runtime worker authentication only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../playbooks/deploy-dev/deploy-dev.yaml | 3 - src/plugins/modules/foremanctl_api.py | 140 ++++++++++++++++++ src/roles/cloud_connector/defaults/main.yaml | 2 - src/roles/cloud_connector/tasks/main.yaml | 29 ++-- src/vars/base.yaml | 2 - 5 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 src/plugins/modules/foremanctl_api.py diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index 151db5522..c8fdd75ab 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -74,9 +74,6 @@ - role: cloud_connector when: - "'cloud-connector' in enabled_features" - vars: - cloud_connector_admin_user: "{{ foreman_development_admin_user }}" - cloud_connector_admin_password: "{{ foreman_development_admin_password }}" post_tasks: - name: Stop Foreman development service ansible.builtin.include_role: diff --git a/src/plugins/modules/foremanctl_api.py b/src/plugins/modules/foremanctl_api.py new file mode 100644 index 000000000..8c355ea47 --- /dev/null +++ b/src/plugins/modules/foremanctl_api.py @@ -0,0 +1,140 @@ +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +--- +module: foremanctl_api +short_description: Make authenticated Foreman API calls using OAuth1 +description: + - Make HTTP requests to the Foreman API using OAuth1 authentication. + - Useful for one-off API calls where no dedicated Foreman Ansible Module exists. +options: + server_url: + description: Foreman server URL + required: true + type: str + oauth1_consumer_key: + description: OAuth1 consumer key + required: true + type: str + oauth1_consumer_secret: + description: OAuth1 consumer secret + required: true + type: str + no_log: true + endpoint: + description: API endpoint path (e.g. /api/v2/rh_cloud/announce_to_sources) + required: true + type: str + method: + description: HTTP method + default: GET + type: str + choices: [GET, POST, PUT, DELETE, PATCH] + body: + description: Request body (sent as JSON) + type: dict + ca_path: + description: Path to CA certificate for SSL verification + type: str + status_code: + description: List of acceptable HTTP status codes + default: [200] + type: list + elements: int +''' + +EXAMPLES = ''' +- name: Announce to Sources + foremanctl_api: + server_url: "https://foreman.example.com" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" + endpoint: /api/v2/rh_cloud/announce_to_sources + method: POST + status_code: [200, 201] +''' + +import json + +from ansible.module_utils.basic import AnsibleModule + +try: + import requests + from requests_oauthlib import OAuth1 + HAS_DEPS = True +except ImportError: + HAS_DEPS = False + + +def run_module(): + module = AnsibleModule( + argument_spec=dict( + server_url=dict(required=True, type='str'), + oauth1_consumer_key=dict(required=True, type='str'), + oauth1_consumer_secret=dict(required=True, type='str', no_log=True), + endpoint=dict(required=True, type='str'), + method=dict(default='GET', type='str', choices=['GET', 'POST', 'PUT', 'DELETE', 'PATCH']), + body=dict(type='dict'), + ca_path=dict(type='str'), + status_code=dict(default=[200], type='list', elements='int'), + ), + supports_check_mode=True, + ) + + if not HAS_DEPS: + module.fail_json(msg='requests and requests-oauthlib are required for this module') + + url = module.params['server_url'].rstrip('/') + module.params['endpoint'] + method = module.params['method'] + body = module.params['body'] + ca_path = module.params['ca_path'] + expected_status = module.params['status_code'] + + auth = OAuth1( + module.params['oauth1_consumer_key'], + client_secret=module.params['oauth1_consumer_secret'], + ) + + headers = {'Content-Type': 'application/json'} + + try: + response = requests.request( + method=method, + url=url, + auth=auth, + headers=headers, + json=body, + verify=ca_path if ca_path else True, + ) + except requests.exceptions.RequestException as e: + module.fail_json(msg=f'Request failed: {e}', url=url) + + response_body = None + try: + response_body = response.json() + except (json.JSONDecodeError, ValueError): + response_body = response.text + + if response.status_code not in expected_status: + module.fail_json( + msg=f'Unexpected status code {response.status_code} (expected {expected_status})', + url=url, + status_code=response.status_code, + body=response_body, + ) + + module.exit_json( + changed=method != 'GET', + url=url, + status_code=response.status_code, + body=response_body, + ) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/src/roles/cloud_connector/defaults/main.yaml b/src/roles/cloud_connector/defaults/main.yaml index c98186357..a0bb9be6d 100644 --- a/src/roles/cloud_connector/defaults/main.yaml +++ b/src/roles/cloud_connector/defaults/main.yaml @@ -1,7 +1,5 @@ --- cloud_connector_url: "https://{{ ansible_facts['fqdn'] }}" -cloud_connector_admin_user: admin -cloud_connector_admin_password: changeme # noqa: no-static-secrets cloud_connector_service_user: cloud_connector_user cloud_connector_service_password: changeme # noqa: no-static-secrets cloud_connector_config_file: /etc/rhc/workers/foreman_rh_cloud.toml diff --git a/src/roles/cloud_connector/tasks/main.yaml b/src/roles/cloud_connector/tasks/main.yaml index 72c067c19..02fa0fabb 100644 --- a/src/roles/cloud_connector/tasks/main.yaml +++ b/src/roles/cloud_connector/tasks/main.yaml @@ -12,8 +12,8 @@ - permissions: - dispatch_cloud_requests server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_admin_user }}" - password: "{{ cloud_connector_admin_password }}" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" state: present @@ -27,8 +27,8 @@ - Cloud Connector admin: false server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_admin_user }}" - password: "{{ cloud_connector_admin_password }}" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" state: present @@ -79,8 +79,8 @@ name: rhc_instance_id value: "{{ cloud_connector_cert_info.subject.commonName }}" server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_admin_user }}" - password: "{{ cloud_connector_admin_password }}" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" - name: Enable automatic inventory upload @@ -88,19 +88,18 @@ name: allow_auto_inventory_upload value: true server_url: "{{ cloud_connector_url }}" - username: "{{ cloud_connector_admin_user }}" - password: "{{ cloud_connector_admin_password }}" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" - name: Announce to Sources - ansible.builtin.uri: - url: "{{ cloud_connector_url }}/api/v2/rh_cloud/announce_to_sources" - user: "{{ cloud_connector_admin_user }}" - password: "{{ cloud_connector_admin_password }}" - method: POST + foremanctl_api: + server_url: "{{ cloud_connector_url }}" + oauth1_consumer_key: "{{ foreman_oauth_consumer_key }}" + oauth1_consumer_secret: "{{ foreman_oauth_consumer_secret }}" ca_path: "{{ foreman_ca_certificate | default(omit) }}" - force_basic_auth: true - body_format: json + method: POST + endpoint: /api/v2/rh_cloud/announce_to_sources status_code: [200, 201] - name: Configure HTTP proxy for rhcd diff --git a/src/vars/base.yaml b/src/vars/base.yaml index 0c8f00b9a..e22e2959c 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -65,8 +65,6 @@ backup_foreman_client_certificate: "{{ foreman_client_certificate }}" backup_foreman_client_key: "{{ foreman_client_key }}" cloud_connector_url: "{{ foreman_url }}" -cloud_connector_admin_user: "{{ foreman_initial_admin_username }}" -cloud_connector_admin_password: "{{ foreman_initial_admin_password }}" cloud_connector_service_user: cloud_connector_user cloud_connector_service_password_file: "{{ obsah_state_path }}/cloud-connector-service-password" cloud_connector_service_password: >- From 4eb0858a2aac86bed8c3434986ee6df665d71385 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Mon, 6 Jul 2026 14:24:35 -0400 Subject: [PATCH 18/18] Add library symlink so ansible-lint resolves foremanctl_api module Ansible resolves modules for roles from role/library/ first. CI's ansible-lint couldn't find foremanctl_api when linting the role in isolation. Symlink to src/plugins/modules/ keeps a single source. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/roles/cloud_connector/library | 1 + 1 file changed, 1 insertion(+) create mode 120000 src/roles/cloud_connector/library diff --git a/src/roles/cloud_connector/library b/src/roles/cloud_connector/library new file mode 120000 index 000000000..eab9f8664 --- /dev/null +++ b/src/roles/cloud_connector/library @@ -0,0 +1 @@ +../../plugins/modules \ No newline at end of file