Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ jobs:
- name: Add optional feature - foreman-proxy
run: |
./foremanctl deploy --add-feature foreman-proxy
- name: Add optional feature - foreman_azure_rm and foreman_google
- name: Add optional features - azure_rm, google and remote_execution
run: |
./foremanctl deploy --add-feature foreman_azure_rm --add-feature foreman_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 }}"
Expand Down Expand Up @@ -214,9 +214,9 @@ jobs:
- name: Add optional feature - foreman-proxy
run: |
./foremanctl deploy --add-feature foreman-proxy
- name: Add optional feature - foreman_azure_rm and foreman_google
- name: Add optional features - azure_rm, google and remote_execution
run: |
./foremanctl deploy --add-feature foreman_azure_rm --add-feature foreman_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
Expand Down
1 change: 1 addition & 0 deletions src/ansible.cfg
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[defaults]
host_key_checking = False
roles_path = ./roles
filter_plugins = ./filter_plugins
callback_result_format = yaml
66 changes: 66 additions & 0 deletions src/filter_plugins/foremanctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

BASE_FEATURES = ['hammer', 'foreman-proxy', 'foreman']

FEATURE_MAP = {
'katello': {
'foreman': 'katello',
'foreman_proxy': None
},
'remote_execution': {
'foreman': 'foreman_remote_execution',
'foreman_proxy': 'remote_execution_ssh',
'dependencies': ['dynflow']
},
'dynflow': {
'foreman_proxy': 'dynflow'
},
'google': {
'foreman': 'foreman_google',
'foreman_proxy': None
},
'azure_rm': {
'foreman': 'foreman_azure_rm',
'foreman_proxy': None
}
}


def compact_list(items):
return [item for item in items if item is not None]


def foreman_plugins(value):
dependencies = [FEATURE_MAP.get(feature, {}).get('dependencies', []) for feature in 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') for feature in (value + dependencies) if feature not in BASE_FEATURES]
return compact_list(plugins)


def known_foreman_plugins(_value):
plugins = [FEATURE_MAP.get(feature).get('foreman') for feature in FEATURE_MAP.keys()]
return compact_list(plugins)

def foreman_proxy_plugins(value):
dependencies = [FEATURE_MAP.get(feature, {}).get('dependencies', []) for feature in 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') 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') 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,
'features_to_foreman_proxy_plugins': foreman_proxy_plugins,
'known_foreman_proxy_plugins': known_foreman_proxy_plugins,
}
1 change: 1 addition & 0 deletions src/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ collections:
- name: containers.podman
version: ">=1.16.4"
- name: theforeman.foreman
version: ">=5.9.0"
2 changes: 2 additions & 0 deletions src/roles/foreman/defaults/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ foreman_recurring_tasks:
- instance: ldap-refresh_usergroups
rake: "ldap:refresh_usergroups"
schedule: "*-*-* *:00,30:00"

foreman_plugins: []
7 changes: 7 additions & 0 deletions src/roles/foreman_proxy/defaults/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ foreman_proxy_url: "https://{{ foreman_proxy_name }}:{{ foreman_proxy_https_port
# Settings
foreman_proxy_trusted_hosts:
- "{{ foreman_proxy_name }}"

foreman_proxy_known_feautures: "{{ [] | known_foreman_proxy_plugins }}"
foreman_proxy_base_feautures:
- logs
Comment on lines +11 to +12
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ekohl can you remind me why we're force-enabling logs?
This is there since #279 but no explanation why we enable it.

foreman_proxy_plugins: []
foreman_proxy_features: "{{ foreman_proxy_base_feautures + foreman_proxy_plugins }}"
foreman_proxy_disabled_features: "{{ foreman_proxy_known_feautures | difference(foreman_proxy_features) }}"
8 changes: 8 additions & 0 deletions src/roles/foreman_proxy/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@
ansible.builtin.systemd:
name: foreman-proxy
state: restarted

- name: Refresh Foreman Proxy
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets executed too early on fresh installs. Damn.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now i wonder why is refreshing feature needed at the first place?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Foreman stores the list of features a Proxy has in its database, when the configuration of a proxy changes, you need to "refresh" that data.
And as we're deploying without REX first in the tests, we need to refresh once we enabled REX.

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
8 changes: 0 additions & 8 deletions src/roles/foreman_proxy/tasks/configs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
31 changes: 31 additions & 0 deletions src/roles/foreman_proxy/tasks/feature.yaml
Original file line number Diff line number Diff line change
@@ -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') }}"
36 changes: 36 additions & 0 deletions src/roles/foreman_proxy/tasks/feature/remote_execution_ssh.yaml
Original file line number Diff line number Diff line change
@@ -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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
name: foreman-proxy-remote_execution_ssh-ssh-key
name: foreman-proxy-remote_execution_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
Comment on lines +9 to +19
Copy link

@Gauravtalreja1 Gauravtalreja1 Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two tasks can be consolidated into a loop to create the SSH key/pub secrets, which will also ensure the handler runs only once, OR just run the handler when SSH secrets are mounted to the container

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handlers run only once, no matter how often they are notified.

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
31 changes: 27 additions & 4 deletions src/roles/foreman_proxy/tasks/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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'
Expand All @@ -35,13 +34,34 @@
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we limiting this feature_enabled to true and false, as i see it can have http, https etc as well?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can, but given that we only configure a https port right now, it's kinda pointless to differentiate.

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
Expand All @@ -55,3 +75,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
10 changes: 10 additions & 0 deletions src/roles/foreman_proxy/templates/settings.d/dynflow.yml.j2
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/roles/foreman_proxy/templates/settings.d/logs.yml.j2
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
---
:enabled: https
:enabled: {{ feature_enabled }}
Original file line number Diff line number Diff line change
@@ -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 public key>
# :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
4 changes: 3 additions & 1 deletion src/vars/base.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ foreman_client_key: "{{ client_key }}"
foreman_client_certificate: "{{ client_certificate }}"
foreman_oauth_consumer_key: abcdefghijklmnopqrstuvwxyz123456
foreman_oauth_consumer_secret: abcdefghijklmnopqrstuvwxyz123456
foreman_plugins: "{{ enabled_features | reject('contains', 'content/') | difference(['hammer', 'foreman-proxy', 'foreman']) }}"
foreman_plugins: "{{ enabled_features | reject('contains', 'content/') | features_to_foreman_plugins }}"
foreman_url: "https://{{ ansible_facts['fqdn'] }}"

httpd_server_ca_certificate: "{{ server_ca_certificate }}"
Expand All @@ -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 | reject('contains', 'content/') | features_to_foreman_proxy_plugins }}"
11 changes: 10 additions & 1 deletion tests/client_test.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
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')
assert client.package('bear').is_installed
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})
2 changes: 2 additions & 0 deletions tests/foreman_proxy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading