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
43 changes: 43 additions & 0 deletions src/filter_plugins/foremanctl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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'
},
'google': {
'foreman': 'foreman_google',
'foreman_proxy': None
},
'azure_rm': {
'foreman': 'foreman_azure_rm',
'foreman_proxy': None
}
}


def foreman_plugins(value):
plugins = [FEATURE_MAP.get(feature, {}).get('foreman') for feature in value if feature not in BASE_FEATURES]
return [plugin for plugin in plugins if plugin is not None]


def foreman_proxy_plugins(value):
plugins = [FEATURE_MAP.get(feature, {}).get('foreman_proxy') for feature in value if feature not in BASE_FEATURES]
return [plugin for plugin in plugins if plugin is not None]


class FilterModule(object):
''' foremanctl filters'''

def filters(self):
return {
'features_to_foreman_plugins': foreman_plugins,
'features_to_foreman_proxy_plugins': foreman_proxy_plugins,
}
4 changes: 3 additions & 1 deletion src/requirements.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ collections:
- ansible.posix
- name: containers.podman
version: ">=1.16.4"
- name: theforeman.foreman
- name: https://github.com/theforeman/foreman-ansible-modules
type: git
version: smart_proxy_refresh
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: []
5 changes: 5 additions & 0 deletions src/roles/foreman/templates/settings.yaml.j2
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@
:oauth_consumer_key: {{ foreman_oauth_consumer_key }}
:oauth_consumer_secret: {{ foreman_oauth_consumer_secret }}
{% endif %}

# Configure reverse proxy headers
:ssl_client_dn_env: HTTP_SSL_CLIENT_S_DN
:ssl_client_verify_env: HTTP_SSL_CLIENT_VERIFY
:ssl_client_cert_env: HTTP_SSL_CLIENT_CERT
8 changes: 8 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,11 @@ foreman_proxy_url: "https://{{ foreman_proxy_name }}:{{ foreman_proxy_https_port
# Settings
foreman_proxy_trusted_hosts:
- "{{ foreman_proxy_name }}"

foreman_proxy_known_feautures:
- remote_execution_ssh
foreman_proxy_base_feautures:
- logs
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.

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', files=['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"
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
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 }}"
2 changes: 1 addition & 1 deletion src/vars/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ candlepin_container_tag: "foreman-{{ container_tag_stream }}"
foreman_container_image: quay.io/foreman/foreman
foreman_container_tag: "{{ container_tag_stream }}"
foreman_proxy_container_image: "quay.io/foreman/foreman-proxy"
foreman_proxy_container_tag: "{{ container_tag_stream }}"
foreman_proxy_container_tag: "{{ container_tag_stream }}-rex"

postgresql_container_image: quay.io/sclorg/postgresql-13-c9s
postgresql_container_tag: "latest"
Expand Down
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})
14 changes: 12 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ def server_fqdn(server_hostname):
return f'{server_hostname}.example.com'


@pytest.fixture(scope="module")
def client_hostname():
return 'client'


@pytest.fixture(scope="module")
def client_fqdn(client_hostname):
return f'{client_hostname}.example.com'


@pytest.fixture(scope="module")
def certificates(pytestconfig, server_fqdn):
source = pytestconfig.getoption("certificate_source")
Expand All @@ -53,8 +63,8 @@ def server(server_hostname):


@pytest.fixture(scope="module")
def client():
yield testinfra.get_host('paramiko://client', sudo=True, ssh_config=SSH_CONFIG)
def client(client_hostname):
yield testinfra.get_host(f'paramiko://{client_hostname}', sudo=True, ssh_config=SSH_CONFIG)


@pytest.fixture(scope="module")
Expand Down
1 change: 1 addition & 0 deletions tests/foreman_proxy_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def test_foreman_proxy_features(server, certificates, server_fqdn):
cmd = server.run(f"curl --cacert {certificates['ca_certificate']} --silent https://{server_fqdn}:{FOREMAN_PROXY_PORT}/features")
assert cmd.succeeded
assert "logs" in cmd.stdout
assert "script" in cmd.stdout

def test_foreman_proxy_service(server):
foreman_proxy = server.service("foreman-proxy")
Expand Down
Loading