diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8230f59ca..1b7aa3d7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -117,9 +117,9 @@ jobs: - name: Add optional feature - foreman-proxy run: | ./foremanctl deploy --add-feature foreman-proxy - - name: Add optional features - azure_rm, google + - name: Add optional features - azure_rm, google and remote_execution run: | - ./foremanctl deploy --add-feature azure_rm --add-feature google + ./foremanctl deploy --add-feature azure_rm --add-feature google --add-feature remote_execution - name: Run tests run: | ./forge test --pytest-args="--certificate-source=${{ matrix.certificate_source }} --database-mode=${{ matrix.database }}" @@ -226,9 +226,9 @@ jobs: - name: Add optional feature - foreman-proxy run: | ./foremanctl deploy --add-feature foreman-proxy - - name: Add optional features - azure_rm, google + - name: Add optional features - azure_rm, google and remote_execution run: | - ./foremanctl deploy --add-feature azure_rm --add-feature google + ./foremanctl deploy --add-feature azure_rm --add-feature google --add-feature remote_execution - name: Stop services run: vagrant ssh quadlet -- sudo systemctl stop foreman.target diff --git a/src/features.yaml b/src/features.yaml index 7e5903f90..22ec2433a 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -11,3 +11,15 @@ azure_rm: description: Azure Resource Manager plugin for Foreman foreman: plugin_name: foreman_azure_rm +remote_execution: + description: Remote Execution plugin for Foreman + foreman: + plugin_name: foreman_remote_execution + foreman_proxy: + plugin_name: remote_execution_ssh + dependencies: + - dynflow +dynflow: + internal: true + foreman_proxy: + plugin_name: dynflow diff --git a/src/filter_plugins/foremanctl.py b/src/filter_plugins/foremanctl.py index 9d5cd8bcc..75d9d1bd1 100644 --- a/src/filter_plugins/foremanctl.py +++ b/src/filter_plugins/foremanctl.py @@ -31,6 +31,18 @@ def known_foreman_plugins(_value): return compact_list(plugins) +def foreman_proxy_plugins(value): + dependencies = [FEATURE_MAP.get(feature, {}).get('dependencies', []) for feature in filter_content(value) if feature not in BASE_FEATURES] + dependencies = list(set([dep for deplist in dependencies for dep in deplist])) + plugins = [FEATURE_MAP.get(feature, {}).get('foreman_proxy', {}).get('plugin_name') for feature in (value + dependencies) if feature not in BASE_FEATURES] + return compact_list(plugins) + + +def known_foreman_proxy_plugins(_value): + plugins = [FEATURE_MAP.get(feature).get('foreman_proxy', {}).get('plugin_name') for feature in FEATURE_MAP.keys()] + return compact_list(plugins) + + class FilterModule(object): '''foremanctl filters''' @@ -38,4 +50,6 @@ def filters(self): return { 'features_to_foreman_plugins': foreman_plugins, 'known_foreman_plugins': known_foreman_plugins, + 'features_to_foreman_proxy_plugins': foreman_proxy_plugins, + 'known_foreman_proxy_plugins': known_foreman_proxy_plugins, } diff --git a/src/requirements.yml b/src/requirements.yml index 1587cb83d..977126086 100644 --- a/src/requirements.yml +++ b/src/requirements.yml @@ -6,3 +6,4 @@ collections: - name: containers.podman version: ">=1.16.4" - name: theforeman.foreman + version: ">=5.9.0" diff --git a/src/roles/foreman_proxy/defaults/main.yaml b/src/roles/foreman_proxy/defaults/main.yaml index 1d73e38b2..20136d3f7 100644 --- a/src/roles/foreman_proxy/defaults/main.yaml +++ b/src/roles/foreman_proxy/defaults/main.yaml @@ -10,3 +10,10 @@ foreman_proxy_url: "https://{{ foreman_proxy_name }}:{{ foreman_proxy_https_port # Settings foreman_proxy_trusted_hosts: - "{{ foreman_proxy_name }}" + +foreman_proxy_base_features: + - logs +foreman_proxy_plugins: [] +foreman_proxy_features: "{{ foreman_proxy_base_features + foreman_proxy_plugins }}" +foreman_proxy_known_features: "{{ [] | known_foreman_proxy_plugins }}" +foreman_proxy_disabled_features: "{{ foreman_proxy_known_features | difference(foreman_proxy_features) }}" diff --git a/src/roles/foreman_proxy/handlers/main.yml b/src/roles/foreman_proxy/handlers/main.yml index 6cea8a881..54befefd3 100644 --- a/src/roles/foreman_proxy/handlers/main.yml +++ b/src/roles/foreman_proxy/handlers/main.yml @@ -2,4 +2,12 @@ - name: Restart Foreman Proxy ansible.builtin.systemd: name: foreman-proxy - state: restarted + state: "{{ (_foreman_proxy_service is changed) | ternary('started', 'restarted') }}" + +- name: Refresh Foreman Proxy + theforeman.foreman.smart_proxy_refresh: + smart_proxy: "{{ foreman_proxy_name }}" + server_url: "{{ foreman_url }}" + username: "{{ foreman_initial_admin_username }}" + password: "{{ foreman_initial_admin_password }}" + validate_certs: false diff --git a/src/roles/foreman_proxy/tasks/configs.yaml b/src/roles/foreman_proxy/tasks/configs.yaml index 568cfd789..0ccffe020 100644 --- a/src/roles/foreman_proxy/tasks/configs.yaml +++ b/src/roles/foreman_proxy/tasks/configs.yaml @@ -6,11 +6,3 @@ data: "{{ lookup('ansible.builtin.template', 'settings.yml.j2') }}" notify: - Restart Foreman Proxy - -- name: Create logs config secret - containers.podman.podman_secret: - state: present - name: foreman-proxy-logs-yml - data: "{{ lookup('ansible.builtin.template', 'settings.d/logs.yml.j2') }}" - notify: - - Restart Foreman Proxy diff --git a/src/roles/foreman_proxy/tasks/feature.yaml b/src/roles/foreman_proxy/tasks/feature.yaml new file mode 100644 index 000000000..3d954a98c --- /dev/null +++ b/src/roles/foreman_proxy/tasks/feature.yaml @@ -0,0 +1,31 @@ +--- +- name: Create config secret for {{ feature_name }} + containers.podman.podman_secret: + state: present + name: foreman-proxy-{{ feature_name }}-yml + data: "{{ lookup('ansible.builtin.template', 'settings.d/' + feature_name + '.yml.j2') }}" + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + +- name: Mount config secret for {{ feature_name }} + ansible.builtin.copy: + dest: /etc/containers/systemd/foreman-proxy.container.d/{{ feature_name }}.conf + content: | + [Container] + Secret=foreman-proxy-{{ feature_name }}-yml,type=mount,target=/etc/foreman-proxy/settings.d/{{ feature_name }}.yml + mode: '0644' + owner: root + group: root + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + +- name: Include additional tasks for {{ feature_name }} + ansible.builtin.include_tasks: '{{ tasks_file }}' + when: + - feature_enabled != "false" + - tasks_file is not none + - tasks_file != "" + vars: + tasks_file: "{{ lookup('ansible.builtin.first_found', ['feature/' + feature_name + '.yaml'], errors='ignore') }}" diff --git a/src/roles/foreman_proxy/tasks/feature/remote_execution_ssh.yaml b/src/roles/foreman_proxy/tasks/feature/remote_execution_ssh.yaml new file mode 100644 index 000000000..0aa7cb0eb --- /dev/null +++ b/src/roles/foreman_proxy/tasks/feature/remote_execution_ssh.yaml @@ -0,0 +1,36 @@ +--- +- name: Create SSH Key + community.crypto.openssh_keypair: + path: "/root/foreman-proxy-ssh" + +- name: Create SSH Key podman secret + containers.podman.podman_secret: + state: present + name: foreman-proxy-remote_execution_ssh-ssh-key + path: "/root/foreman-proxy-ssh" + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + +- name: Create SSH Pub podman secret + containers.podman.podman_secret: + state: present + name: foreman-proxy-remote_execution_ssh-ssh-pub + path: "/root/foreman-proxy-ssh.pub" + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy + +- name: Mount SSH secrets + ansible.builtin.copy: + dest: /etc/containers/systemd/foreman-proxy.container.d/remote_execution_ssh-keys.conf + content: | + [Container] + Secret=foreman-proxy-remote_execution_ssh-ssh-key,type=mount,target=/usr/share/foreman-proxy/.ssh/id_rsa_foreman_proxy + Secret=foreman-proxy-remote_execution_ssh-ssh-pub,type=mount,target=/usr/share/foreman-proxy/.ssh/id_rsa_foreman_proxy.pub + mode: '0644' + owner: root + group: root + notify: + - Restart Foreman Proxy + - Refresh Foreman Proxy diff --git a/src/roles/foreman_proxy/tasks/main.yaml b/src/roles/foreman_proxy/tasks/main.yaml index 176f33505..7a7ec3319 100644 --- a/src/roles/foreman_proxy/tasks/main.yaml +++ b/src/roles/foreman_proxy/tasks/main.yaml @@ -22,7 +22,6 @@ hostname: "{{ ansible_facts['fqdn'] }}" secrets: - 'foreman-proxy-settings-yml,type=mount,target=/etc/foreman-proxy/settings.yml' - - 'foreman-proxy-logs-yml,type=mount,target=/etc/foreman-proxy/settings.d/logs.yml' - 'foreman-proxy-ssl-ca,type=mount,target=/etc/foreman-proxy/ssl_ca.pem' - 'foreman-proxy-ssl-cert,type=mount,target=/etc/foreman-proxy/ssl_cert.pem' - 'foreman-proxy-ssl-key,type=mount,target=/etc/foreman-proxy/ssl_key.pem' @@ -37,17 +36,39 @@ PartOf=foreman.target notify: Restart Foreman Proxy +- name: Create foreman-proxy.container.d folder + ansible.builtin.file: + path: /etc/containers/systemd/foreman-proxy.container.d + state: directory + mode: '0755' + owner: 'root' + group: 'root' + +- name: Configure features + ansible.builtin.include_tasks: feature.yaml + vars: + feature_enabled: "true" + loop: "{{ foreman_proxy_features }}" + loop_control: + loop_var: feature_name + +- name: Disable features + ansible.builtin.include_tasks: feature.yaml + vars: + feature_enabled: "false" + loop: "{{ foreman_proxy_disabled_features }}" + loop_control: + loop_var: feature_name + - name: Run daemon reload to make Quadlet create the service files ansible.builtin.systemd: daemon_reload: true -- name: Flush handlers to restart services - ansible.builtin.meta: flush_handlers - - name: Start the Foreman Proxy Service ansible.builtin.systemd: name: foreman-proxy state: started + register: _foreman_proxy_service - name: Register Foreman Proxy to Foreman theforeman.foreman.smart_proxy: @@ -57,3 +78,6 @@ username: "{{ foreman_initial_admin_username }}" password: "{{ foreman_initial_admin_password }}" validate_certs: false + +- name: Flush handlers to restart services + ansible.builtin.meta: flush_handlers diff --git a/src/roles/foreman_proxy/templates/settings.d/dynflow.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/dynflow.yml.j2 new file mode 100644 index 000000000..6bacaf20b --- /dev/null +++ b/src/roles/foreman_proxy/templates/settings.d/dynflow.yml.j2 @@ -0,0 +1,10 @@ +--- +:enabled: {{ feature_enabled }} +:database: + +# Require a valid cert to access Dynflow console +# :console_auth: true + +# Maximum age of execution plans to keep before having them cleaned +# by the execution plan cleaner (in seconds), defaults to 30 minutes +# :execution_plan_cleaner_age: 1800 diff --git a/src/roles/foreman_proxy/templates/settings.d/logs.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/logs.yml.j2 index cdbc714d6..dfcc456c2 100644 --- a/src/roles/foreman_proxy/templates/settings.d/logs.yml.j2 +++ b/src/roles/foreman_proxy/templates/settings.d/logs.yml.j2 @@ -1,2 +1,2 @@ --- -:enabled: https +:enabled: {{ feature_enabled }} diff --git a/src/roles/foreman_proxy/templates/settings.d/remote_execution_ssh.yml.j2 b/src/roles/foreman_proxy/templates/settings.d/remote_execution_ssh.yml.j2 new file mode 100644 index 000000000..76ad34456 --- /dev/null +++ b/src/roles/foreman_proxy/templates/settings.d/remote_execution_ssh.yml.j2 @@ -0,0 +1,46 @@ +--- +:enabled: {{ feature_enabled }} +:ssh_identity_key_file: '~/.ssh/id_rsa_foreman_proxy' +:local_working_dir: '/var/tmp' +:remote_working_dir: '/var/tmp' +:socket_working_dir: '/var/tmp' +# :kerberos_auth: false + +# :cockpit_integration: true + +# Mode of operation, one of ssh, pull, pull-mqtt +:mode: ssh + +# Enables the use of SSH certificate for smart proxy authentication +# The file should contain an SSH CA public key that the SSH public key of smart proxy is signed by +# :ssh_user_ca_public_key_file: + +# Enables the use of SSH host certificates for host authentication +# The file should contain a list of trusted SSH CA authorities that the host certs can be signed by +# Example file content: @cert-authority * +# :ssh_ca_known_hosts_file: + +# Defines how often (in seconds) should the runner check +# for new data leave empty to use the runner's default +# :runner_refresh_interval: 1 + +# Defines the verbosity of logging coming from ssh command +# one of :debug, :info, :error, :fatal +# must be lower than general log level +# :ssh_log_level: error + +# Remove working directories on job completion +# :cleanup_working_dirs: true + +# MQTT configuration, need to be set if mode is set to pull-mqtt +# :mqtt_broker: localhost +# :mqtt_port: 1883 + +# Use of SSL can be forced either way by explicitly setting mqtt_tls setting. If +# unset, SSL gets used if smart-proxy's foreman_ssl_cert, foreman_ssl_key and +# foreman_ssl_ca settings are set available. +# :mqtt_tls: + +# The notification is sent over mqtt every $mqtt_resend_interval seconds, until +# the job is picked up by the host or cancelled +# :mqtt_resend_interval: 900 diff --git a/src/vars/base.yaml b/src/vars/base.yaml index f7e01b9f7..e8d3161b0 100644 --- a/src/vars/base.yaml +++ b/src/vars/base.yaml @@ -34,3 +34,5 @@ pulp_plugins: "{{ enabled_features | select('contains', 'content/') | map('repla hammer_ca_certificate: "{{ server_ca_certificate }}" hammer_plugins: "{{ foreman_plugins | map('replace', 'foreman-tasks', 'foreman_tasks') | list }}" + +foreman_proxy_plugins: "{{ enabled_features | features_to_foreman_proxy_plugins }}" diff --git a/tests/client_test.py b/tests/client_test.py index 49b4d0da3..b753a6832 100644 --- a/tests/client_test.py +++ b/tests/client_test.py @@ -1,6 +1,6 @@ def test_foreman_content_view(client_environment, activation_key, organization, foremanapi, client): client.run('dnf install -y subscription-manager') - rcmd = foremanapi.create('registration_commands', {'organization_id': organization['id'], 'insecure': True, 'activation_keys': [activation_key['name']]}) + rcmd = foremanapi.create('registration_commands', {'organization_id': organization['id'], 'insecure': True, 'activation_keys': [activation_key['name']], 'force': True}) client.run_test(rcmd['registration_command']) client.run('subscription-manager repos --enable=*') client.run_test('dnf install -y bear') @@ -8,3 +8,12 @@ def test_foreman_content_view(client_environment, activation_key, organization, client.run('dnf remove -y bear') client.run('subscription-manager unregister') client.run('subscription-manager clean') + +def test_foreman_rex(client_environment, activation_key, organization, foremanapi, client, client_fqdn): + client.run('dnf install -y subscription-manager') + rcmd = foremanapi.create('registration_commands', {'organization_id': organization['id'], 'insecure': True, 'activation_keys': [activation_key['name']], 'force': True}) + client.run_test(rcmd['registration_command']) + job = foremanapi.create('job_invocations', {'feature': 'run_script', 'inputs': {'command': 'uptime'}, 'search_query': f'name = {client_fqdn}', 'targeting_type': 'static_query'}) + task = foremanapi.wait_for_task(job['task']) + assert task['result'] == 'success' + foremanapi.delete('hosts', {'id': client_fqdn}) diff --git a/tests/foreman_proxy_test.py b/tests/foreman_proxy_test.py index 840372ad9..de7ffeacc 100644 --- a/tests/foreman_proxy_test.py +++ b/tests/foreman_proxy_test.py @@ -7,6 +7,8 @@ def test_foreman_proxy_features(server, certificates, server_fqdn): assert cmd.succeeded features = json.loads(cmd.stdout) assert "logs" in features + assert "script" in features + assert "dynflow" in features def test_foreman_proxy_service(server): foreman_proxy = server.service("foreman-proxy")