From 063fb5a5a7fea079c58820ac3e2edf9ea3f64dac Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 15:32:48 -0400 Subject: [PATCH 01/11] 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 caf89c1d9..efd72fc61 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -72,6 +72,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 964d6bf5c..5bfbcb445 100644 --- a/development/playbooks/deploy-dev/metadata.obsah.yaml +++ b/development/playbooks/deploy-dev/metadata.obsah.yaml @@ -29,6 +29,9 @@ variables: foreman_development_github_username: help: GitHub username to add as additional remote for git checkouts action: store + 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 daf451bdc..adc11edb3 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -50,6 +50,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 837d36c98..d7fb41c63 100644 --- a/src/playbooks/deploy/deploy.yaml +++ b/src/playbooks/deploy/deploy.yaml @@ -55,4 +55,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 c1a32b484..c86496ba9 100644 --- a/src/playbooks/deploy/metadata.obsah.yaml +++ b/src/playbooks/deploy/metadata.obsah.yaml @@ -56,6 +56,9 @@ variables: parameter: --bmc-redfish-verify-ssl help: Verify SSL certificates for Redfish BMC connections. type: Boolean + cloud_connector_http_proxy: + parameter: --cloud-connector-http-proxy + help: HTTP proxy URL for the cloud connector rhcd service. constraints: required_together: 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 b85b9d02f..830c8fd74 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -48,3 +48,7 @@ foreman_proxy_oauth_consumer_secret: "{{ foreman_oauth_consumer_secret }}" iop_core_foreman_url: "{{ foreman_url }}" iop_core_foreman_oauth_consumer_key: "{{ foreman_oauth_consumer_key }}" iop_core_foreman_oauth_consumer_secret: "{{ foreman_oauth_consumer_secret }}" + +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 1539d08ecdb10e76408d2a459ad8e11339ccd27f Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 16:07:31 -0400 Subject: [PATCH 02/11] 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 | 1 + src/roles/cloud_connector/tasks/main.yaml | 8 ------ 4 files changed, 31 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 efd72fc61..b92f8134c 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -13,6 +13,11 @@ vars: httpd_foreman_backend: "http://localhost:3000" 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 90dbf9e1d..0cc796d47 100644 --- a/src/roles/checks/tasks/main.yml +++ b/src/roles/checks/tasks/main.yml @@ -3,6 +3,7 @@ ansible.builtin.include_tasks: execute_check.yml loop: - check_features + - check_cloud_connector - check_hostname - check_database_connection - check_system_requirements 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 124398daf3e84180e03b1d35e27b6a554581154e Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 16:14:49 -0400 Subject: [PATCH 03/11] 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 308822dcbeca8e346f6c154d3cc0581c7e3bb92d Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 12:12:04 -0400 Subject: [PATCH 04/11] 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 e9bdb43b17ce0ac4f31ac5fc52bad1dc46293dfb Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:11:59 -0400 Subject: [PATCH 05/11] 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 230ff566e8c6244c6a7e74b9deab65b7dac2df84 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:22:33 -0400 Subject: [PATCH 06/11] 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 9cd7ef174584e1913edba778c70d33861f357a8b Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 14:30:35 -0400 Subject: [PATCH 07/11] 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 342bcdcb94860eec0de75e0daf8a341ce5d17829 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 15:47:41 -0400 Subject: [PATCH 08/11] 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 46f78f36ea304e7a4e9795cebe77fd812e0cfe3e Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 15:47:45 -0400 Subject: [PATCH 09/11] Add conflicts support to the feature system Adds a 'conflicts' key to features.yaml that declares mutually exclusive features. A new conflicting_features filter function validates enabled features and the check_features role fails early with a clear error message when conflicts are detected. Uses cloud-connector / iop as the first conflict pair. Also adds check_features role to deploy-dev so the validation runs in both production and development deploy paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- development/ansible.cfg | 1 + .../playbooks/deploy-dev/deploy-dev.yaml | 1 + src/features.yaml | 2 + src/filter_plugins/foremanctl.py | 16 +++++ src/roles/check_features/tasks/main.yaml | 11 ++++ tests/unit/features_filter_test.py | 60 +++++++++++++++++++ 6 files changed, 91 insertions(+) create mode 100644 tests/unit/features_filter_test.py diff --git a/development/ansible.cfg b/development/ansible.cfg index 15225a2c6..40fc13d79 100644 --- a/development/ansible.cfg +++ b/development/ansible.cfg @@ -3,4 +3,5 @@ host_key_checking = False stdout_callback=debug stderr_callback=debug roles_path = ./roles:../src/roles +filter_plugins = ../src/filter_plugins display_skipped_hosts = no diff --git a/development/playbooks/deploy-dev/deploy-dev.yaml b/development/playbooks/deploy-dev/deploy-dev.yaml index b92f8134c..892ee0a74 100644 --- a/development/playbooks/deploy-dev/deploy-dev.yaml +++ b/development/playbooks/deploy-dev/deploy-dev.yaml @@ -60,6 +60,7 @@ foreman_development_enabled_plugins: "{{ foreman_development_enabled_plugins + ['foreman_ansible'] }}" roles: - role: pre_install + - role: check_features - role: certificates - role: postgresql - role: redis diff --git a/src/features.yaml b/src/features.yaml index adc11edb3..4cdf3db4b 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -54,6 +54,8 @@ cloud-connector: description: Cloud Connector for Red Hat Hybrid Cloud Console dependencies: - rh-cloud + conflicts: + - iop iop: description: iop services dependencies: diff --git a/src/filter_plugins/foremanctl.py b/src/filter_plugins/foremanctl.py index 25c9ebc1c..73de53500 100644 --- a/src/filter_plugins/foremanctl.py +++ b/src/filter_plugins/foremanctl.py @@ -93,6 +93,21 @@ def invalid_features(features): return [feature for feature in features if feature not in FEATURE_MAP] +def conflicting_features(enabled_features): + """Return list of conflict descriptions for any mutually exclusive features.""" + conflicts = [] + seen = set() + enabled_set = set(enabled_features) + for feature in enabled_features: + for conflict in FEATURE_MAP.get(feature, {}).get('conflicts', []): + if conflict in enabled_set: + pair = tuple(sorted([feature, conflict])) + if pair not in seen: + seen.add(pair) + conflicts.append(f"{pair[0]} conflicts with {pair[1]}") + return conflicts + + def hammer_plugins(value): dependencies = list(get_dependencies(filter_features(value))) plugins = [FEATURE_MAP.get(feature, {}).get('hammer') for feature in filter_features(value + dependencies)] @@ -127,5 +142,6 @@ def filters(self): 'available_foreman_proxy_plugins': available_foreman_proxy_plugins, 'list_all_features': list_all_features, 'invalid_features': invalid_features, + 'conflicting_features': conflicting_features, 'has_feature': has_feature, } diff --git a/src/roles/check_features/tasks/main.yaml b/src/roles/check_features/tasks/main.yaml index 2e5d1a7a8..ff232fee5 100644 --- a/src/roles/check_features/tasks/main.yaml +++ b/src/roles/check_features/tasks/main.yaml @@ -13,3 +13,14 @@ vars: found_invalid_features: "{{ features | invalid_features }}" when: features | length > 0 + +- name: Validate no conflicting features + ansible.builtin.assert: + that: + - found_conflicting_features | length == 0 + fail_msg: | + ERROR: Conflicting features enabled: {{ found_conflicting_features | join(', ') }} + + Remove one of the conflicting features with --remove-feature before deploying. + vars: + found_conflicting_features: "{{ enabled_features | conflicting_features }}" diff --git a/tests/unit/features_filter_test.py b/tests/unit/features_filter_test.py new file mode 100644 index 000000000..e1677e700 --- /dev/null +++ b/tests/unit/features_filter_test.py @@ -0,0 +1,60 @@ +from unittest.mock import patch + +from filter_plugins.foremanctl import conflicting_features + + +MOCK_FEATURES = { + 'alpha': {'description': 'Alpha feature'}, + 'beta': { + 'description': 'Beta feature', + 'conflicts': ['gamma'], + }, + 'gamma': {'description': 'Gamma feature'}, + 'delta': { + 'description': 'Delta feature', + 'conflicts': ['gamma'], + }, +} + + +@patch('filter_plugins.foremanctl.FEATURE_MAP', MOCK_FEATURES) +class TestConflictingFeatures: + def test_no_conflicts(self): + assert conflicting_features(['alpha', 'gamma']) == [] + + def test_detects_conflict(self): + result = conflicting_features(['beta', 'gamma']) + assert len(result) == 1 + assert 'beta conflicts with gamma' in result + + def test_no_conflict_when_only_one_side_enabled(self): + assert conflicting_features(['beta']) == [] + + def test_deduplicates_when_declared_on_both_sides(self): + both_declare = { + 'a': {'conflicts': ['b']}, + 'b': {'conflicts': ['a']}, + } + with patch('filter_plugins.foremanctl.FEATURE_MAP', both_declare): + result = conflicting_features(['a', 'b']) + assert len(result) == 1 + assert 'a conflicts with b' in result + + def test_multiple_conflicts(self): + result = conflicting_features(['beta', 'delta', 'gamma']) + assert len(result) == 2 + + def test_empty_features(self): + assert conflicting_features([]) == [] + + def test_features_without_conflicts_key(self): + assert conflicting_features(['alpha']) == [] + + def test_conflict_detected_when_added_separately(self): + """Simulates a previously persisted feature conflicting with a newly added one.""" + previously_persisted = ['beta'] + newly_added = ['gamma'] + enabled = previously_persisted + newly_added + result = conflicting_features(enabled) + assert len(result) == 1 + assert 'beta conflicts with gamma' in result From c4511d786b8f37a2c5f57e77e6161e58e5cffdb1 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Wed, 10 Jun 2026 17:24:23 -0400 Subject: [PATCH 10/11] Enable parameter persistence for forge Adds OBSAH_PERSIST_PARAMS=true to the forge wrapper, matching foremanctl's behavior. This ensures features added via forge deploy-dev --add-feature are remembered across runs, which is required for conflict detection to work when features are added in separate deploys. Co-Authored-By: Claude Opus 4.6 (1M context) --- forge | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forge b/forge index 76503996d..25c5c146d 100755 --- a/forge +++ b/forge @@ -5,7 +5,8 @@ OBSAH_BASE=$(dirname $(readlink -f $0)) OBSAH_DATA=${OBSAH_BASE}/development OBSAH_INVENTORY=${OBSAH_BASE}/inventories OBSAH_STATE=${OBSAH_BASE}/.var/lib/foremanctl -export OBSAH_NAME OBSAH_DATA OBSAH_INVENTORY OBSAH_STATE +OBSAH_PERSIST_PARAMS=true +export OBSAH_NAME OBSAH_DATA OBSAH_INVENTORY OBSAH_STATE OBSAH_PERSIST_PARAMS ANSIBLE_COLLECTIONS_PATH=${OBSAH_BASE}/build/collections/forge ANSIBLE_COLLECTIONS_SCAN_SYS_PATH=false From 2547caf5893f41894461371980be6ce852c430e4 Mon Sep 17 00:00:00 2001 From: Jeremy Lenz Date: Fri, 12 Jun 2026 15:51:47 -0400 Subject: [PATCH 11/11] Add --preserve-plugin-branches parameter for forge deploy-dev Co-Authored-By: Claude Opus 4.6 (1M context) --- development/playbooks/deploy-dev/metadata.obsah.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/development/playbooks/deploy-dev/metadata.obsah.yaml b/development/playbooks/deploy-dev/metadata.obsah.yaml index 5bfbcb445..0e95871b3 100644 --- a/development/playbooks/deploy-dev/metadata.obsah.yaml +++ b/development/playbooks/deploy-dev/metadata.obsah.yaml @@ -32,6 +32,10 @@ variables: cloud_connector_http_proxy: parameter: --cloud-connector-http-proxy help: HTTP proxy URL for the cloud connector rhcd service. + foreman_development_preserve_plugin_branches: + parameter: --preserve-plugin-branches + help: Skip git checkout for plugins, preserving local branches and changes. + type: Boolean include: - _flavor_features