diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8230f59ca..a6abb6a17 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/docs/feature-metadata.md b/docs/feature-metadata.md index 8a0e3d0b1..bab36df8e 100644 --- a/docs/feature-metadata.md +++ b/docs/feature-metadata.md @@ -2,7 +2,7 @@ Users want to enable abstract features, which means the deployment needs to know how to translate a feature name to a set of changes (configuration files, services, etc). -The metadata is a Hash with the feature name as the key and the feature definition as the value. +The metadata is a Hash with the feature (using dashes, not underscores if needed) name as the key and the feature definition as the value. The feature definition itself is again a Hash with the various properties of the feature. ```yaml diff --git a/src/features.yaml b/src/features.yaml index 5decdf6ec..dec084fbf 100644 --- a/src/features.yaml +++ b/src/features.yaml @@ -13,7 +13,19 @@ google: description: Google Compute Engine plugin for Foreman foreman: plugin_name: foreman_google -azure_rm: +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..964dab743 100644 --- a/src/filter_plugins/foremanctl.py +++ b/src/filter_plugins/foremanctl.py @@ -19,23 +19,61 @@ def filter_content(items): return filter(lambda x: not x.startswith('content/'), items) +def filter_base_features(items): + return filter(lambda x: x not in BASE_FEATURES, items) + + +def filter_features(items): + items = filter_content(items) + items = filter_base_features(items) + return items + + +def get_dependencies_for_feature(feature): + dependencies = set() + for dependency in FEATURE_MAP.get(feature, {}).get('dependencies', []): + if dependency not in dependencies: + dependencies.update(get_dependencies_for_feature(dependency)) + dependencies.add(dependency) + return dependencies + + +def get_dependencies(features): + dependencies = set() + for feature in features: + dependencies.update(get_dependencies_for_feature(feature)) + return dependencies + + def foreman_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', {}).get('plugin_name') for feature in (value + dependencies) if feature not in BASE_FEATURES] + dependencies = list(get_dependencies(filter_features(value))) + plugins = [FEATURE_MAP.get(feature, {}).get('foreman', {}).get('plugin_name') for feature in filter_features(value + dependencies)] return compact_list(plugins) -def known_foreman_plugins(_value): +def available_foreman_plugins(_value): plugins = [FEATURE_MAP.get(feature).get('foreman', {}).get('plugin_name') for feature in FEATURE_MAP.keys()] return compact_list(plugins) +def foreman_proxy_plugins(value): + dependencies = list(get_dependencies(filter_features(value))) + plugins = [FEATURE_MAP.get(feature, {}).get('foreman_proxy', {}).get('plugin_name') for feature in filter_features(value + dependencies)] + return compact_list(plugins) + + +def available_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''' def filters(self): return { 'features_to_foreman_plugins': foreman_plugins, - 'known_foreman_plugins': known_foreman_plugins, + 'available_foreman_plugins': available_foreman_plugins, + 'features_to_foreman_proxy_plugins': foreman_proxy_plugins, + 'available_foreman_proxy_plugins': available_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..7ac827a13 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_available_features: "{{ [] | available_foreman_proxy_plugins }}" +foreman_proxy_disabled_features: "{{ foreman_proxy_available_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..b8d4f9fd7 --- /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-id_rsa_foreman_proxy + 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-id_rsa_foreman_proxy-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-id_rsa_foreman_proxy,type=mount,target=/usr/share/foreman-proxy/.ssh/id_rsa_foreman_proxy + Secret=foreman_proxy-remote_execution_ssh-id_rsa_foreman_proxy-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")