├── requirements.txt ├── plugins ├── modules │ ├── __init__.py │ ├── reboot_host.py │ ├── is_reboot_required.py │ ├── apply_highstate.py │ ├── openscap_run.py │ ├── apply_states.py │ ├── full_pkg_update.py │ ├── install_patches.py │ └── install_upgrades.py ├── module_utils │ ├── __init__.py │ ├── utilities.py │ ├── exceptions.py │ └── helper_functions.py ├── doc_fragments │ └── uyuni_auth.py └── inventory │ └── inventory.py ├── roles ├── client │ ├── tests │ │ ├── inventory │ │ ├── .gitignore │ │ └── test.yml │ ├── .config │ │ └── ansible-lint.yml │ ├── .markdownlint.yml │ ├── defaults │ │ └── main.yml │ ├── molecule │ │ ├── default │ │ │ ├── converge.yml │ │ │ ├── molecule.yml │ │ │ └── tests │ │ │ │ └── test_default.py │ │ └── README.md │ ├── vars │ │ ├── redhat.yml │ │ ├── debian.yml │ │ └── suse.yml │ ├── tasks │ │ ├── main.yml │ │ ├── removal.yml │ │ └── bootstrap.yml │ ├── meta │ │ └── main.yml │ └── README.md ├── server │ ├── tests │ │ ├── inventory │ │ ├── .gitignore │ │ └── test.yml │ ├── molecule │ │ ├── requirements.yml │ │ ├── mlm │ │ │ ├── tasks │ │ │ │ └── create-fail.yml │ │ │ ├── vars │ │ │ │ └── main.yml │ │ │ ├── molecule.yml │ │ │ └── converge.yml │ │ ├── default │ │ │ ├── tasks │ │ │ │ └── create-fail.yml │ │ │ ├── molecule.yml │ │ │ ├── vars │ │ │ │ └── main.yml │ │ │ ├── converge.yml │ │ │ ├── destroy.yml │ │ │ ├── create.yml │ │ │ └── tests │ │ │ │ └── test_default.py │ │ └── README.md │ ├── tasks │ │ ├── prepare.yml │ │ ├── check.yml │ │ ├── monitoring.yml │ │ ├── content.yml │ │ ├── check_opensuse.yml │ │ ├── prepare_opensuse.yml │ │ ├── prepare_opensuse_leap_micro.yml │ │ ├── check_sles.yml │ │ ├── check_sle_micro.yml │ │ ├── prepare_sles.yml │ │ ├── main.yml │ │ ├── prepare_sle_micro.yml │ │ └── install.yml │ ├── .markdownlint.yml │ ├── Containerfile.tumbleweed │ ├── Containerfile.sles │ ├── vars │ │ ├── sles.yml │ │ ├── sle_micro.yml │ │ └── opensuse.yml │ ├── meta │ │ └── main.yml │ ├── handlers │ │ └── main.yml │ ├── templates │ │ └── uyuni.yml.j2 │ ├── defaults │ │ └── main.yml │ └── README.md └── proxy │ ├── tests │ ├── inventory │ └── test.yml │ ├── molecule │ ├── requirements.yml │ ├── mlm │ │ ├── vars │ │ │ └── main.yml │ │ ├── tasks │ │ │ └── create-fail.yml │ │ ├── converge.yml │ │ └── molecule.yml │ ├── default │ │ ├── vars │ │ │ └── main.yml │ │ ├── converge.yml │ │ ├── tasks │ │ │ └── create-fail.yml │ │ ├── molecule.yml │ │ ├── destroy.yml │ │ ├── tests │ │ │ └── test_default.py │ │ └── create.yml │ └── README.md │ ├── tasks │ ├── prepare.yml │ ├── check.yml │ ├── check_opensuse.yml │ ├── prepare_opensuse.yml │ ├── check_sles.yml │ ├── prepare_opensuse_leap_micro.yml │ ├── check_sle_micro.yml │ ├── install.yml │ ├── prepare_sles.yml │ ├── main.yml │ └── prepare_sle_micro.yml │ ├── Containerfile.tumbleweed │ ├── Containerfile.sles │ ├── handlers │ └── main.yml │ ├── vars │ ├── sles.yml │ ├── sle_micro.yml │ └── opensuse.yml │ ├── defaults │ └── main.yml │ ├── meta │ └── main.yml │ └── README.md ├── .yamllint ├── meta ├── extensions.yml ├── execution-environment.yml └── runtime.yml ├── .ansible-lint ├── .gitignore ├── playbooks └── reboot.yml ├── galaxy.yml ├── extensions └── eda │ ├── rulebooks │ └── show_required_reboots.yml │ └── plugins │ └── event_source │ └── requires_reboot.py ├── .github └── workflows │ └── ansible-test.yml ├── README.md ├── CHANGELOG.md └── LICENSE /requirements.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/module_utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /roles/client/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /roles/server/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | -------------------------------------------------------------------------------- /roles/proxy/tests/inventory: -------------------------------------------------------------------------------- 1 | localhost 2 | 3 | -------------------------------------------------------------------------------- /roles/client/tests/.gitignore: -------------------------------------------------------------------------------- 1 | Vagrantfile 2 | .vagrant 3 | -------------------------------------------------------------------------------- /roles/server/tests/.gitignore: -------------------------------------------------------------------------------- 1 | Vagrantfile 2 | .vagrant 3 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | rules: 4 | line-length: disable 5 | -------------------------------------------------------------------------------- /roles/proxy/molecule/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - containers.podman 4 | -------------------------------------------------------------------------------- /roles/server/molecule/requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | collections: 3 | - containers.podman 4 | -------------------------------------------------------------------------------- /roles/client/.config/ansible-lint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | skip_list: 3 | - '303' 4 | - experimental 5 | -------------------------------------------------------------------------------- /meta/extensions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extensions: 3 | - args: 4 | ext_dir: eda/plugins/event_source 5 | -------------------------------------------------------------------------------- /.ansible-lint: -------------------------------------------------------------------------------- 1 | --- 2 | warn_list: 3 | - no-changed-when 4 | - line-length 5 | - yaml[line-length] 6 | -------------------------------------------------------------------------------- /meta/execution-environment.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | 4 | dependencies: 5 | python: requirements.txt 6 | -------------------------------------------------------------------------------- /roles/proxy/molecule/mlm/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # packages 3 | proxy_pkgs: 4 | - mgrpxy 5 | - mgrpxy-bash-completion 6 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # packages 3 | proxy_pkgs: 4 | - mgrpxy 5 | - uyuni-storage-setup-proxy 6 | -------------------------------------------------------------------------------- /roles/proxy/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run role 3 | hosts: localhost 4 | remote_user: root 5 | roles: 6 | - proxy 7 | -------------------------------------------------------------------------------- /roles/client/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run role 3 | hosts: localhost 4 | remote_user: root 5 | roles: 6 | - role: stdevel.uyuni.client 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | TODO* 2 | *.pyc 3 | credentials*.yml 4 | *.uyuni.yml 5 | ansible.cfg 6 | *.tar.gz 7 | __pycache__ 8 | demo_*.* 9 | .ansible 10 | .vscode 11 | -------------------------------------------------------------------------------- /roles/proxy/tasks/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install requirements 3 | community.general.zypper: 4 | name: "{{ proxy_requirements }}" 5 | become: true 6 | -------------------------------------------------------------------------------- /roles/server/tasks/prepare.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install requirements 3 | community.general.zypper: 4 | name: "{{ server_requirements }}" 5 | become: true 6 | -------------------------------------------------------------------------------- /roles/server/tests/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Run role 3 | hosts: localhost 4 | remote_user: root 5 | roles: 6 | - role: stdevel.uyuni.server 7 | use_lvm: false 8 | -------------------------------------------------------------------------------- /roles/client/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "MD013": false # line-length 3 | "MD014": false # show commands output 4 | "MD041": false # first line should be heading (build status image) 5 | -------------------------------------------------------------------------------- /roles/server/.markdownlint.yml: -------------------------------------------------------------------------------- 1 | --- 2 | "MD013": false # line-length 3 | "MD014": false # show commands output 4 | "MD041": false # first line should be heading (build status image) 5 | -------------------------------------------------------------------------------- /roles/proxy/Containerfile.tumbleweed: -------------------------------------------------------------------------------- 1 | FROM opensuse/tumbleweed AS tumbleweed 2 | RUN zypper in -y systemd sudo python3 python312 podman iproute 3 | RUN echo 'root:root' | chpasswd 4 | CMD exec /sbin/init 5 | -------------------------------------------------------------------------------- /roles/server/Containerfile.tumbleweed: -------------------------------------------------------------------------------- 1 | FROM opensuse/tumbleweed AS tumbleweed 2 | RUN zypper in -y systemd sudo python3 python312 podman iproute 3 | RUN echo 'root:root' | chpasswd 4 | CMD exec /sbin/init 5 | -------------------------------------------------------------------------------- /roles/proxy/Containerfile.sles: -------------------------------------------------------------------------------- 1 | FROM registry.suse.com/bci/bci-base:15.7 AS sles157 2 | RUN zypper in -y suseconnect-ng systemd sudo python3 python313 podman iproute 3 | RUN echo 'root:root' | chpasswd 4 | CMD exec /sbin/init 5 | -------------------------------------------------------------------------------- /roles/server/Containerfile.sles: -------------------------------------------------------------------------------- 1 | FROM registry.suse.com/bci/bci-base:15.7 AS sles157 2 | RUN zypper in -y suseconnect-ng systemd sudo python3 python313 podman iproute 3 | RUN echo 'root:root' | chpasswd 4 | CMD exec /sbin/init 5 | -------------------------------------------------------------------------------- /roles/client/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | client_bootstrap_filename: "bootstrap-{{ ansible_distribution | regex_replace(' ', '_') | lower }}{{ ansible_distribution_version }}.sh" 3 | client_bootstrap_folder: /opt 4 | client_state: present 5 | -------------------------------------------------------------------------------- /roles/client/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge machines 3 | hosts: all 4 | become: true 5 | 6 | roles: 7 | - role: stdevel.uyuni.client 8 | client_uyuni_server: CHANGEME 9 | # client_state: absent 10 | -------------------------------------------------------------------------------- /roles/client/vars/redhat.yml: -------------------------------------------------------------------------------- 1 | --- 2 | client_repo_file: /etc/yum.repos.d/susemanager:channels.repo 3 | 4 | client_packages: 5 | - venv-salt-minion 6 | client_minion_service: venv-salt-minion 7 | client_directories: 8 | - /etc/venv-salt-minion 9 | - /var/cache/venv-salt-minion 10 | -------------------------------------------------------------------------------- /roles/client/vars/debian.yml: -------------------------------------------------------------------------------- 1 | --- 2 | client_repo_file: /etc/apt/sources.list.d/susemanager:channels.list 3 | 4 | client_packages: 5 | - venv-salt-minion 6 | client_minion_service: venv-salt-minion 7 | client_directories: 8 | - /etc/venv-salt-minion 9 | - /var/cache/venv-salt-minion 10 | -------------------------------------------------------------------------------- /roles/proxy/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot system (if immutable) 3 | ansible.builtin.reboot: 4 | reboot_timeout: 3600 5 | changed_when: false 6 | when: ansible_distribution | lower in ['opensuse microos', 'opensuse leap micro', 'opensuse slowroll', 'sle micro'] 7 | become: true 8 | -------------------------------------------------------------------------------- /roles/client/vars/suse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | client_repo_file: /etc/zypp/repos.d/susemanager:channels.repo 3 | 4 | client_packages: 5 | - salt-minion 6 | client_minion_service: salt-minion 7 | client_directories: 8 | - /etc/salt-minion 9 | - /etc/salt 10 | - /var/cache/salt/minion 11 | - /var/cache/salt 12 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge machines 3 | hosts: all 4 | become: true 5 | pre_tasks: 6 | - name: Fix FQDN 7 | ansible.builtin.set_fact: 8 | ansible_fqdn: uyuni-proxy.podman.loc 9 | 10 | roles: 11 | - role: stdevel.uyuni.proxy 12 | proxy_config_file: /root/foo.bar 13 | -------------------------------------------------------------------------------- /roles/client/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | driver: 5 | name: vagrant 6 | lint: | 7 | yamllint . 8 | ansible-lint 9 | flake8 10 | platforms: 11 | - name: uyuni-client-opensuse-leap15 12 | box: opensuse/Leap-15.4.x86_64 13 | provisioner: 14 | name: ansible 15 | verifier: 16 | name: testinfra 17 | -------------------------------------------------------------------------------- /roles/proxy/vars/sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | proxy_requirements: 3 | - podman 4 | 5 | proxy_pkgs: 6 | - mgrpxy 7 | - mgrpxy-bash-completion 8 | 9 | proxy_suma_modules: 10 | - name: "SUSE Manager Server Proxy Extension {{ proxy_suma_release }} {{ ansible_architecture }}" 11 | identifier: "SUSE-Manager-Proxy/{{ proxy_suma_release }}/{{ ansible_architecture }}" 12 | search: SUSE-Manager-Proxy 13 | -------------------------------------------------------------------------------- /roles/proxy/vars/sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | proxy_requirements: 3 | - podman 4 | 5 | proxy_pkgs: 6 | - mgrpxy 7 | - mgrpxy-bash-completion 8 | 9 | proxy_suma_modules: 10 | - name: "SUSE Manager Server Proxy Extension {{ proxy_suma_release }} {{ ansible_architecture }}" 11 | identifier: "SUSE-Manager-Proxy/{{ proxy_suma_release }}/{{ ansible_architecture }}" 12 | search: SUSE-Manager-Proxy 13 | -------------------------------------------------------------------------------- /roles/proxy/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # check requirements 3 | proxy_check_requirements: true 4 | 5 | # required core packages 6 | proxy_core_packages: 7 | - man 8 | - firewalld 9 | 10 | # SCC settings 11 | proxy_scc_url: https://scc.suse.com 12 | proxy_scc_check_registration: true 13 | proxy_scc_check_modules: true 14 | 15 | # SUMA-related settings 16 | proxy_suma_release: 5.0 17 | proxy_suma_airgapped: false 18 | -------------------------------------------------------------------------------- /roles/proxy/tasks/check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check CPU 3 | ansible.builtin.fail: 4 | msg: "Please ensure having at least 2 CPUs" 5 | when: 6 | - ansible_processor_vcpus < 2 7 | - proxy_check_requirements 8 | 9 | - name: Check memory 10 | ansible.builtin.fail: 11 | msg: "Please ensure having at least 8 GB of memory" 12 | when: 13 | - ansible_memtotal_mb < 7900 14 | - proxy_check_requirements 15 | -------------------------------------------------------------------------------- /roles/server/tasks/check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check CPU 3 | ansible.builtin.fail: 4 | msg: "Please ensure having at least 4 CPUs" 5 | when: 6 | - ansible_processor_vcpus < 4 7 | - server_check_requirements 8 | 9 | - name: Check memory 10 | ansible.builtin.fail: 11 | msg: "Please ensure having at least 16 GB of memory" 12 | when: 13 | - ansible_memtotal_mb < 15900 14 | - server_check_requirements 15 | -------------------------------------------------------------------------------- /roles/proxy/molecule/mlm/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Retrieve container log 3 | ansible.builtin.command: 4 | cmd: >- 5 | {% raw %} 6 | podman logs 7 | {% endraw %} 8 | {{ item.stdout_lines[0] }} 9 | # podman inspect --format='{{.HostConfig.LogConfig.Path}}' 10 | changed_when: false 11 | register: logfile_cmd 12 | 13 | - name: Display container log 14 | ansible.builtin.fail: 15 | msg: "{{ logfile_cmd.stderr }}" 16 | -------------------------------------------------------------------------------- /roles/server/molecule/mlm/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Retrieve container log 3 | ansible.builtin.command: 4 | cmd: >- 5 | {% raw %} 6 | podman logs 7 | {% endraw %} 8 | {{ item.stdout_lines[0] }} 9 | # podman inspect --format='{{.HostConfig.LogConfig.Path}}' 10 | changed_when: false 11 | register: logfile_cmd 12 | 13 | - name: Display container log 14 | ansible.builtin.fail: 15 | msg: "{{ logfile_cmd.stderr }}" 16 | -------------------------------------------------------------------------------- /roles/server/tasks/monitoring.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Get monitoring status 3 | become: true 4 | ansible.builtin.command: mgrctl exec '/usr/sbin/mgr-monitoring-ctl status' 5 | register: server_monitoring_state 6 | changed_when: false 7 | 8 | - name: Enable monitoring 9 | become: true 10 | ansible.builtin.command: mgrctl exec '/usr/sbin/mgr-monitoring-ctl enable' 11 | when: "'error' in server_monitoring_state.stderr|lower" 12 | notify: Restart Uyuni 13 | -------------------------------------------------------------------------------- /roles/server/vars/sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_requirements: 3 | - podman 4 | 5 | server_pkgs: 6 | - mgradm 7 | - mgrctl 8 | - mgradm-bash-completion 9 | - mgrctl-bash-completion 10 | - netavark 11 | 12 | server_suma_modules: 13 | - name: "SUSE Manager Server Extension {{ server_suma_release }} {{ ansible_architecture }}" 14 | identifier: "SUSE-Manager-Server/{{ server_suma_release }}/{{ ansible_architecture }}" 15 | search: SUSE-Manager-Server 16 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Retrieve container log 3 | ansible.builtin.command: 4 | cmd: >- 5 | {% raw %} 6 | podman logs 7 | {% endraw %} 8 | {{ item.stdout_lines[0] }} 9 | # podman inspect --format='{{.HostConfig.LogConfig.Path}}' 10 | changed_when: false 11 | register: logfile_cmd 12 | 13 | - name: Display container log 14 | ansible.builtin.fail: 15 | msg: "{{ logfile_cmd.stderr }}" 16 | -------------------------------------------------------------------------------- /roles/server/molecule/default/tasks/create-fail.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Retrieve container log 3 | ansible.builtin.command: 4 | cmd: >- 5 | {% raw %} 6 | podman logs 7 | {% endraw %} 8 | {{ item.stdout_lines[0] }} 9 | # podman inspect --format='{{.HostConfig.LogConfig.Path}}' 10 | changed_when: false 11 | register: logfile_cmd 12 | 13 | - name: Display container log 14 | ansible.builtin.fail: 15 | msg: "{{ logfile_cmd.stderr }}" 16 | -------------------------------------------------------------------------------- /roles/server/vars/sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_requirements: 3 | - podman 4 | 5 | server_pkgs: 6 | - mgradm 7 | - mgrctl 8 | - mgradm-bash-completion 9 | - mgrctl-bash-completion 10 | - netavark 11 | 12 | server_suma_modules: 13 | - name: "SUSE Manager Server Extension {{ server_suma_release }} {{ ansible_architecture }}" 14 | identifier: "SUSE-Manager-Server/{{ server_suma_release }}/{{ ansible_architecture }}" 15 | search: SUSE-Manager-Server 16 | -------------------------------------------------------------------------------- /roles/proxy/molecule/mlm/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge machines 3 | hosts: all 4 | become: true 5 | pre_tasks: 6 | - name: Fix FQDN 7 | ansible.builtin.set_fact: 8 | ansible_fqdn: mlm-proxy.podman.loc 9 | 10 | roles: 11 | - role: stdevel.uyuni.proxy 12 | proxy_scc_reg_code_os: "" 13 | proxy_scc_reg_code_mlm: "" 14 | proxy_scc_mail: "" 15 | proxy_config_file: /root/foo.bar 16 | -------------------------------------------------------------------------------- /roles/server/tasks/content.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create software channels 3 | ansible.builtin.command: mgrctl exec -- /usr/bin/spacewalk-common-channels -u {{ server_org_login }} -p {{ server_org_password }} -a {{ item.arch }} {{ item.name }} 4 | loop: "{{ server_channels }}" 5 | register: server_create_result 6 | changed_when: 7 | - '"exists" not in server_create_result.stdout | lower' 8 | - '"already in use" not in server_create_result.stderr | lower' 9 | become: true 10 | when: server_channels | length > 0 11 | -------------------------------------------------------------------------------- /roles/server/tasks/check_opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "Uyuni only supports x86_64 and aarch64" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64'] 7 | 8 | - name: Ensure having Uyuni release equal/greater than 2024.03 9 | ansible.builtin.fail: 10 | msg: "Uyuni releases older than 2024.03 are unsupported - use an older version of this collection" 11 | when: 12 | - server_release is defined 13 | - "server_release is version('2024.03', '<=')" 14 | -------------------------------------------------------------------------------- /roles/proxy/tasks/check_opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "Uyuni only supports x86_64 and aarch64" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64'] 7 | 8 | - name: Ensure having Uyuni release equal/greater than 2024.03 9 | ansible.builtin.fail: 10 | msg: "Uyuni releases older than 2024.03 are unsupported - use an older version of this collection" 11 | when: 12 | - proxy_uyuni_release is defined 13 | - "proxy_uyuni_release is version('2024.03', '<=')" 14 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | options: 8 | managed: false 9 | login_cmd_template: "podman exec -ti {instance} bash" 10 | ansible_connection_options: 11 | ansible_connection: podman 12 | lint: | 13 | yamllint . 14 | ansible-lint 15 | flake8 16 | platforms: 17 | - name: opensuse-tumbleweed 18 | image: opensuse-tumbleweed-uyuni 19 | privileged: true 20 | command: /sbin/init 21 | verifier: 22 | name: testinfra 23 | -------------------------------------------------------------------------------- /roles/proxy/vars/opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | proxy_requirements: 3 | - podman 4 | 5 | proxy_pkgs: 6 | - mgrpxy 7 | - uyuni-storage-setup-proxy 8 | 9 | proxy_gpg: 10 | - "https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/images/repo/Uyuni-Proxy-POOL-{{ ansible_architecture }}-Media1/repodata/repomd.xml.key" 11 | 12 | proxy_repos: 13 | - url: "https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/images/repo/Uyuni-Proxy-POOL-{{ ansible_architecture }}-Media1/" 14 | name: systemsmanagement_proxy_Stable_ContainerUtils 15 | -------------------------------------------------------------------------------- /roles/server/molecule/default/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | options: 8 | managed: false 9 | login_cmd_template: "podman exec -ti {instance} bash" 10 | ansible_connection_options: 11 | ansible_connection: podman 12 | lint: | 13 | yamllint . 14 | ansible-lint 15 | flake8 16 | platforms: 17 | - name: opensuse-tumbleweed 18 | image: opensuse-tumbleweed-uyuni 19 | privileged: true 20 | command: /sbin/init 21 | verifier: 22 | name: testinfra 23 | -------------------------------------------------------------------------------- /roles/server/molecule/mlm/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # packages 3 | server_pkgs: 4 | - mgradm 5 | - mgrctl 6 | - netavark 7 | - uyuni-storage-setup-server 8 | 9 | # Uyuni configuration 10 | server_org_name: "Demo" 11 | server_org_login: "admin" 12 | server_org_password: "admin" 13 | 14 | # monitoring settings 15 | server_enable_monitoring: true 16 | 17 | # content settings 18 | # server_channels: 19 | # - name: almalinux9 20 | # arch: x86_64 21 | # - name: almalinux9-appstream 22 | # arch: x86_64 23 | # - name: almalinux9-uyuni-client 24 | # arch: x86_64 25 | -------------------------------------------------------------------------------- /roles/server/molecule/default/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # packages 3 | server_pkgs: 4 | - mgradm 5 | - mgrctl 6 | - netavark 7 | - uyuni-storage-setup-server 8 | 9 | # Uyuni configuration 10 | server_org_name: "Demo" 11 | server_org_login: "admin" 12 | server_org_password: "admin" 13 | 14 | # monitoring settings 15 | server_enable_monitoring: true 16 | 17 | # content settings 18 | # server_channels: 19 | # - name: almalinux9 20 | # arch: x86_64 21 | # - name: almalinux9-appstream 22 | # arch: x86_64 23 | # - name: almalinux9-uyuni-client 24 | # arch: x86_64 25 | -------------------------------------------------------------------------------- /roles/client/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include distribution-specific variables 3 | ansible.builtin.include_vars: "{{ ansible_os_family | lower }}.yml" 4 | 5 | - name: Check client state 6 | ansible.builtin.fail: 7 | msg: Set client_state to either 'present' or 'absent' 8 | when: client_state not in ['present', 'absent'] 9 | 10 | - name: Import bootstrap tasks 11 | ansible.builtin.include_tasks: bootstrap.yml 12 | when: client_state == 'present' 13 | 14 | - name: Import removal tasks 15 | ansible.builtin.include_tasks: removal.yml 16 | when: client_state == 'absent' 17 | -------------------------------------------------------------------------------- /roles/client/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: client 4 | author: Christian Stankowic 5 | namespace: stdevel 6 | description: Bootstraps Uyuni or SUSE Manager clients 7 | issue_tracker_url: https://github.com/stdevel/ansible-collection-uyuni/issues 8 | license: GPL-3.0-only 9 | 10 | min_ansible_version: "2.4" 11 | 12 | platforms: 13 | - name: opensuse 14 | - name: SLES 15 | - name: Debian 16 | - name: Ubuntu 17 | - name: EL 18 | 19 | galaxy_tags: 20 | - systemsmanagement 21 | - uyuni 22 | - suse 23 | - manager 24 | 25 | dependencies: [] 26 | -------------------------------------------------------------------------------- /roles/server/vars/opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | server_requirements: 3 | - podman 4 | 5 | server_pkgs: 6 | - mgradm 7 | - mgrctl 8 | - netavark 9 | - uyuni-storage-setup-server 10 | 11 | server_gpg: 12 | - "https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/images/repo/Uyuni-Server-POOL-{{ ansible_architecture }}-Media1/repodata/repomd.xml.key" 13 | 14 | server_repos: 15 | - url: "https://download.opensuse.org/repositories/systemsmanagement:/Uyuni:/Stable/images/repo/Uyuni-Server-POOL-{{ ansible_architecture }}-Media1/" 16 | name: systemsmanagement_Uyuni_Stable_ContainerUtils 17 | -------------------------------------------------------------------------------- /roles/proxy/molecule/mlm/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: default 8 | options: 9 | managed: false 10 | login_cmd_template: 'ssh root@{instance}' 11 | ansible_connection_options: 12 | ansible_connection: ssh 13 | ansible_user: root 14 | lint: | 15 | yamllint . 16 | ansible-lint 17 | flake8 18 | platforms: 19 | - name: instance 20 | hostname: 21 | address: 22 | user: root 23 | verifier: 24 | name: testinfra 25 | directory: ../default/tests 26 | -------------------------------------------------------------------------------- /roles/server/molecule/mlm/molecule.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependency: 3 | name: galaxy 4 | options: 5 | requirements-file: requirements.yml 6 | driver: 7 | name: default 8 | options: 9 | managed: false 10 | login_cmd_template: 'ssh root@{instance}' 11 | ansible_connection_options: 12 | ansible_connection: ssh 13 | ansible_user: root 14 | lint: | 15 | yamllint . 16 | ansible-lint 17 | flake8 18 | platforms: 19 | - name: instance 20 | hostname: 21 | address: 22 | user: root 23 | verifier: 24 | name: testinfra 25 | directory: ../default/tests 26 | -------------------------------------------------------------------------------- /roles/server/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: server 4 | author: Christian Stankowic 5 | namespace: stdevel 6 | description: Prepares, installs and configures Uyuni or SUSE Manager 7 | issue_tracker_url: https://github.com/stdevel/ansible-collection-uyuni/issues 8 | license: GPL-3.0-only 9 | 10 | min_ansible_version: "2.4" 11 | 12 | platforms: 13 | - name: opensuse 14 | versions: 15 | - "15.3" 16 | - name: SLES 17 | versions: 18 | - "15" 19 | 20 | galaxy_tags: 21 | - systemsmanagement 22 | - uyuni 23 | - suse 24 | - manager 25 | 26 | dependencies: [] 27 | -------------------------------------------------------------------------------- /roles/proxy/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | role_name: proxy 4 | author: Christian Stankowic 5 | namespace: stdevel 6 | description: Prepares, installs and configures Uyuni or SUSE Manager proxy server 7 | issue_tracker_url: https://github.com/stdevel/ansible-collection-uyuni/issues 8 | license: GPL-3.0-only 9 | 10 | min_ansible_version: "2.4" 11 | 12 | platforms: 13 | - name: opensuse 14 | versions: 15 | - "15.3" 16 | - name: SLES 17 | versions: 18 | - "15" 19 | 20 | galaxy_tags: 21 | - systemsmanagement 22 | - uyuni 23 | - suse 24 | - manager 25 | 26 | dependencies: [] 27 | -------------------------------------------------------------------------------- /roles/server/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot system (if immutable) 3 | ansible.builtin.reboot: 4 | reboot_timeout: 3600 5 | changed_when: false 6 | when: ansible_distribution | lower in ['opensuse microos', 'opensuse leap micro', 'opensuse slowroll', 'sle micro'] 7 | become: true 8 | 9 | - name: Create initialization file 10 | ansible.builtin.file: 11 | path: /root/.MANAGER_INITIALIZATION_COMPLETE 12 | owner: root 13 | group: root 14 | mode: '0644' 15 | state: touch 16 | become: true 17 | 18 | - name: Restart Uyuni 19 | ansible.builtin.command: mgradm restart 20 | changed_when: false 21 | become: true 22 | -------------------------------------------------------------------------------- /roles/server/molecule/default/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge machines 3 | hosts: all 4 | become: true 5 | pre_tasks: 6 | - name: Fix FQDN 7 | ansible.builtin.set_fact: 8 | ansible_fqdn: uyuni.podman.loc 9 | 10 | roles: 11 | - role: stdevel.uyuni.server 12 | server_enable_monitoring: true 13 | # server_channels: 14 | # - name: almalinux9 15 | # arch: x86_64 16 | # - name: almalinux9-appstream 17 | # arch: x86_64 18 | # - name: almalinux9-uyuni-client 19 | # arch: x86_64 20 | # # server_release: '2023.03' 21 | # server_firewall_config: true 22 | -------------------------------------------------------------------------------- /playbooks/reboot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Rebooting hosts 3 | hosts: localhost 4 | gather_facts: false 5 | tasks: 6 | # - name: Print all defined variables 7 | # ansible.builtin.debug: 8 | # msg: "{{ hostvars[inventory_hostname] }}" 9 | 10 | - name: Show system that will be rebooted 11 | ansible.builtin.debug: 12 | msg: "Host to be rebooted: {{ ansible_eda.event.host }}" 13 | 14 | - name: Reboot system 15 | stdevel.uyuni.reboot_host: 16 | uyuni_host: 192.168.1.10 17 | uyuni_user: admin 18 | uyuni_password: admin 19 | uyuni_verify_ssl: false 20 | name: "{{ ansible_eda.event.host }}" 21 | -------------------------------------------------------------------------------- /roles/client/tasks/removal.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Stop services 3 | ansible.builtin.service: 4 | name: "{{ client_minion_service }}" 5 | state: stopped 6 | enabled: false 7 | register: client_service_stop 8 | failed_when: 9 | - client_service_stop.failed 10 | - "'not find' not in client_service_stop.msg" 11 | become: true 12 | 13 | - name: Remove packages 14 | ansible.builtin.package: 15 | name: "{{ client_packages }}" 16 | state: absent 17 | become: true 18 | 19 | - name: Remove directories 20 | ansible.builtin.file: 21 | path: "{{ item }}" 22 | state: absent 23 | become: true 24 | loop: "{{ client_directories }}" 25 | -------------------------------------------------------------------------------- /roles/server/templates/uyuni.yml.j2: -------------------------------------------------------------------------------- 1 | # Image settings 2 | image: {{ server_image_name }} 3 | tag: latest 4 | 5 | # SSL settings 6 | ssl: 7 | city: {{ server_cert_city }} 8 | country: {{ server_cert_country }} 9 | org: {{ server_cert_o }} 10 | ou: {{ server_cert_ou }} 11 | password: {{ server_cert_pass }} 12 | state: {{ server_cert_state }} 13 | 14 | # Organization name 15 | organization: {{ server_org_name }} 16 | 17 | # Email address sending the notifications 18 | emailFrom: {{ server_mail }} 19 | 20 | # Administrators account details 21 | admin: 22 | password: {{ server_org_password }} 23 | login: {{ server_org_login }} 24 | firstName: {{ server_org_first_name }} 25 | lastName: {{ server_org_last_name }} 26 | email: {{ server_org_mail }} 27 | -------------------------------------------------------------------------------- /roles/server/tasks/prepare_opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add GPG key 3 | ansible.builtin.rpm_key: 4 | key: "{{ item }}" 5 | loop: "{{ server_gpg }}" 6 | become: true 7 | 8 | - name: Add Uyuni repositories 9 | community.general.zypper_repository: 10 | repo: "{{ item.url }}" 11 | name: "{{ item.name }}" 12 | loop: "{{ server_repos }}" 13 | become: true 14 | 15 | - name: Set default image name 16 | ansible.builtin.set_fact: 17 | server_image_name: registry.opensuse.org/uyuni/server 18 | when: server_release is not defined 19 | 20 | - name: Set default image name (release-specific) 21 | ansible.builtin.set_fact: 22 | server_image_name: "registry.opensuse.org/uyuni/server:{{ server_release }}" 23 | when: server_release is defined 24 | -------------------------------------------------------------------------------- /roles/proxy/tasks/prepare_opensuse.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add GPG key 3 | ansible.builtin.rpm_key: 4 | key: "{{ item }}" 5 | loop: "{{ proxy_gpg }}" 6 | become: true 7 | 8 | - name: Add Uyuni repositories 9 | community.general.zypper_repository: 10 | repo: "{{ item.url }}" 11 | name: "{{ item.name }}" 12 | loop: "{{ proxy_repos }}" 13 | become: true 14 | 15 | - name: Set default image name 16 | ansible.builtin.set_fact: 17 | proxy_image_name: registry.opensuse.org/uyuni/proxy 18 | when: proxy_release is not defined 19 | 20 | - name: Set default image name (release-specific) 21 | ansible.builtin.set_fact: 22 | proxy_image_name: "registry.opensuse.org/systemsmanagement/uyuni/snapshots/{{ proxy_release }}/containers/uyuni/proxy" 23 | when: proxy_release is defined 24 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: stdevel 3 | name: uyuni 4 | version: 0.3.6 5 | readme: README.md 6 | authors: 7 | - Christian Stankowic 8 | description: Collection of modules to manage Uyuni / SUSE Manager 9 | license: 10 | - GPL-3.0-only 11 | 12 | tags: 13 | - infrastructure 14 | - eda 15 | - linux 16 | - systemsmanagement 17 | - uyuni 18 | - suse 19 | - manager 20 | 21 | dependencies: 22 | "community.general": "*" 23 | "containers.podman": "*" 24 | "ansible.posix": "*" 25 | 26 | repository: https://github.com/stdevel/ansible-collection-uyuni 27 | documentation: https://github.com/stdevel/ansible-collection-uyuni/blob/main/README.md 28 | homepage: https://github.com/stdevel/ansible-collection-uyuni 29 | issues: https://github.com/stdevel/ansible-collection-uyuni/issues 30 | -------------------------------------------------------------------------------- /plugins/doc_fragments/uyuni_auth.py: -------------------------------------------------------------------------------- 1 | """ 2 | Uyuni authentication module snippet 3 | """ 4 | from __future__ import (absolute_import, division, print_function) 5 | 6 | __metaclass__ = type 7 | 8 | 9 | class ModuleDocFragment(object): 10 | DOCUMENTATION = r''' 11 | options: 12 | uyuni_host: 13 | description: Uyuni API endpoint URL 14 | required: True 15 | type: str 16 | uyuni_port: 17 | description: Uyuni API endpoint port 18 | default: 443 19 | type: int 20 | uyuni_verify_ssl: 21 | description: Verify SSL certificate 22 | default: True 23 | type: bool 24 | uyuni_user: 25 | description: Uyuni login user 26 | required: True 27 | type: str 28 | uyuni_password: 29 | description: Uyuni login password 30 | required: True 31 | type: str 32 | ''' 33 | -------------------------------------------------------------------------------- /roles/server/molecule/mlm/converge.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Converge machines 3 | hosts: all 4 | become: true 5 | pre_tasks: 6 | - name: Fix FQDN 7 | ansible.builtin.set_fact: 8 | ansible_fqdn: mlm.podman.loc 9 | 10 | roles: 11 | - role: stdevel.uyuni.server 12 | uyuni_scc_reg_code_os: "" 13 | uyuni_scc_reg_code_mlm: "" 15 | uyuni_enable_monitoring: true 16 | # uyuni_channels: 17 | # - name: almalinux9 18 | # arch: x86_64 19 | # - name: almalinux9-appstream 20 | # arch: x86_64 21 | # - name: almalinux9-uyuni-client 22 | # arch: x86_64 23 | # # uyuni_release: '2023.03' 24 | # uyuni_firewall_config: true 25 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy molecule containers 3 | hosts: molecule 4 | gather_facts: false 5 | tasks: 6 | - name: Stop and remove container 7 | delegate_to: localhost 8 | containers.podman.podman_container: 9 | name: "{{ inventory_hostname }}" 10 | state: absent 11 | rm: true 12 | - name: Remove potentially stopped container 13 | delegate_to: localhost 14 | ansible.builtin.command: 15 | cmd: podman container rm --ignore {{ inventory_hostname }} 16 | changed_when: false 17 | 18 | - name: Remove dynamic molecule inventory 19 | hosts: localhost 20 | gather_facts: false 21 | tasks: 22 | - name: Remove dynamic inventory file 23 | ansible.builtin.file: 24 | path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 25 | state: absent 26 | -------------------------------------------------------------------------------- /roles/server/molecule/default/destroy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Destroy molecule containers 3 | hosts: molecule 4 | gather_facts: false 5 | tasks: 6 | - name: Stop and remove container 7 | delegate_to: localhost 8 | containers.podman.podman_container: 9 | name: "{{ inventory_hostname }}" 10 | state: absent 11 | rm: true 12 | - name: Remove potentially stopped container 13 | delegate_to: localhost 14 | ansible.builtin.command: 15 | cmd: podman container rm --ignore {{ inventory_hostname }} 16 | changed_when: false 17 | 18 | - name: Remove dynamic molecule inventory 19 | hosts: localhost 20 | gather_facts: false 21 | tasks: 22 | - name: Remove dynamic inventory file 23 | ansible.builtin.file: 24 | path: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 25 | state: absent 26 | -------------------------------------------------------------------------------- /roles/proxy/tasks/check_sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "SUSE MLM Proxy only supports x86_64 and aarch64" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64'] 7 | 8 | - name: Ensure having SUSE MLM release equal/greater than 5.0 9 | ansible.builtin.fail: 10 | msg: "SUSE MLM releases older than 5.0 are unsupported" 11 | when: 12 | - proxy_suma_release is defined 13 | - "proxy_suma_release is version('5.0', '<')" 14 | 15 | - name: Ensure having supported SP release for SUSE MLM 5.1 16 | ansible.builtin.fail: 17 | msg: "Please upgrade to SLES 15 SP7" 18 | when: 19 | - proxy_suma_release == 5.1 20 | - ansible_distribution_version != '15.7' 21 | 22 | - name: Ensure having supported SP release for SUMA 5.0 23 | ansible.builtin.fail: 24 | msg: "SLES 15 is not supported for SUMA 5.0" 25 | when: 26 | - proxy_suma_release == 5.0 27 | -------------------------------------------------------------------------------- /roles/server/tasks/prepare_opensuse_leap_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add GPG key 3 | ansible.builtin.command: "transactional-update --continue run rpm --import {{ item }}" 4 | loop: "{{ server_gpg }}" 5 | notify: Reboot system (if immutable) 6 | become: true 7 | 8 | - name: Add Uyuni repositories 9 | ansible.builtin.command: "transactional-update --continue run zypper ar {{ item.url }}" 10 | loop: "{{ server_repos }}" 11 | notify: Reboot system (if immutable) 12 | become: true 13 | 14 | - name: Trigger reboot (if necessary) 15 | ansible.builtin.meta: flush_handlers 16 | 17 | - name: Set default image name 18 | ansible.builtin.set_fact: 19 | server_image_name: registry.opensuse.org/uyuni/server 20 | when: server_release is not defined 21 | 22 | - name: Set default image name (release-specific) 23 | ansible.builtin.set_fact: 24 | server_image_name: "registry.opensuse.org/uyuni/server:{{ server_release }}" 25 | when: server_release is defined 26 | -------------------------------------------------------------------------------- /roles/proxy/tasks/prepare_opensuse_leap_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Add GPG key 3 | ansible.builtin.command: "transactional-update --continue run rpm --import {{ item }}" 4 | loop: "{{ proxy_gpg }}" 5 | notify: Reboot system (if immutable) 6 | become: true 7 | 8 | - name: Add Uyuni repositories 9 | ansible.builtin.command: "transactional-update --continue run zypper ar {{ item.url }}" 10 | loop: "{{ proxy_repos }}" 11 | notify: Reboot system (if immutable) 12 | become: true 13 | 14 | - name: Trigger reboot (if necessary) 15 | ansible.builtin.meta: flush_handlers 16 | 17 | - name: Set default image name 18 | ansible.builtin.set_fact: 19 | proxy_image_name: registry.opensuse.org/uyuni/proxy 20 | when: proxy_release is not defined 21 | 22 | - name: Set default image name (release-specific) 23 | ansible.builtin.set_fact: 24 | proxy_image_name: "registry.opensuse.org/systemsmanagement/uyuni/snapshots/{{ proxy_release }}/containers/uyuni/proxy" 25 | when: proxy_release is defined 26 | -------------------------------------------------------------------------------- /roles/proxy/tasks/check_sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "SUSE MLM Proxy only supports x86_64 and aarch64" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64'] 7 | 8 | - name: Ensure having SUSE MLM release equal/greater than 5.0 9 | ansible.builtin.fail: 10 | msg: "SUSE MLM releases older than 5.0 are unsupported" 11 | when: 12 | - proxy_suma_release is defined 13 | - "proxy_suma_release is version('5.0', '<')" 14 | 15 | - name: Ensure having supported SL Micro release for SUSE MLM 5.1 16 | ansible.builtin.fail: 17 | msg: "Please upgrade to SL Micro 6.1" 18 | when: 19 | - proxy_suma_release == 5.1 20 | - ansible_distribution_version != '6.1' 21 | 22 | - name: Ensure having supported SLE Micro release for SUMA 5.0 23 | ansible.builtin.fail: 24 | msg: "Please upgrade to SLE Micro 5.5" 25 | when: 26 | - proxy_suma_release == 5.0 27 | - ansible_distribution_version != '5.5' 28 | -------------------------------------------------------------------------------- /roles/server/tasks/check_sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "SUSE MLM only supports x86_64, aarch64, ppc64l and s390x" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64', 'ppc64l', 's390x'] 7 | 8 | - name: Ensure having SUSE MLM release equal/greater than 5.0 9 | ansible.builtin.fail: 10 | msg: "SUSE MLM releases older than 5.0 are unsupported - use an older version of this collection" 11 | when: 12 | - server_suma_release is defined 13 | - "server_suma_release is version('5.0', '<')" 14 | 15 | - name: Ensure having supported SP release for SUSE MLM 5.1 16 | ansible.builtin.fail: 17 | msg: "Please upgrade to SLES 15 SP7" 18 | when: 19 | - server_suma_release == 5.1 20 | - ansible_distribution_version != '15.7' 21 | 22 | - name: Ensure having supported SP release for SUMA 5.0 23 | ansible.builtin.fail: 24 | msg: "Please upgrade to SLES 15 SP6" 25 | when: 26 | - server_suma_release == 5.0 27 | - ansible_distribution_version != '15.6' 28 | -------------------------------------------------------------------------------- /roles/server/tasks/check_sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure having proper architecture 3 | ansible.builtin.fail: 4 | msg: "SUSE MLM only supports x86_64, aarch64, ppc64l and s390x" 5 | when: 6 | - ansible_architecture not in ['x86_64', 'aarch64', 'ppc64l', 's390x'] 7 | 8 | - name: Ensure having SUSE MLM release equal/greater than 5.0 9 | ansible.builtin.fail: 10 | msg: "SUSE MLM releases older than 5.0 are unsupported - use an older version of this collection" 11 | when: 12 | - server_suma_release is defined 13 | - "server_suma_release is version('5.0', '<')" 14 | 15 | - name: Ensure having supported SL Micro release for SUSE MLM 5.1 16 | ansible.builtin.fail: 17 | msg: "Please upgrade to SL Micro 6.1" 18 | when: 19 | - server_suma_release == 5.1 20 | - ansible_distribution_version != '6.1' 21 | 22 | - name: Ensure having supported SLE Micro release for SUMA 5.0 23 | ansible.builtin.fail: 24 | msg: "Please upgrade to SLE Micro 5.5" 25 | when: 26 | - server_suma_release == 5.0 27 | - ansible_distribution_version != '5.5' 28 | -------------------------------------------------------------------------------- /roles/client/tasks/bootstrap.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if Uyuni server was set 3 | ansible.builtin.fail: 4 | msg: Set client_uyuni_server to a valid server hostname/FQDN 5 | when: client_uyuni_server is undefined 6 | 7 | - name: Check if the client_repo_file exists 8 | ansible.builtin.stat: 9 | path: "{{ client_repo_file }}" 10 | register: client_stat_repo_file 11 | 12 | - name: Download Uyuni bootstrap script 13 | when: not client_stat_repo_file.stat.exists 14 | ansible.builtin.get_url: 15 | url: "http://{{ client_uyuni_server }}/pub/bootstrap/{{ client_bootstrap_filename }}" 16 | dest: "{{ client_bootstrap_folder }}/bootstrap.sh" 17 | owner: root 18 | group: root 19 | mode: '0755' 20 | force: true 21 | become: true 22 | 23 | - name: Register with Uyuni 24 | ansible.builtin.command: "{{ client_bootstrap_folder }}/bootstrap.sh" 25 | args: 26 | creates: "{{ client_repo_file }}" 27 | become: true 28 | 29 | - name: Remove downloaded bootstrap script 30 | ansible.builtin.file: 31 | path: "{{ client_bootstrap_folder }}/bootstrap.sh" 32 | state: absent 33 | become: true 34 | -------------------------------------------------------------------------------- /extensions/eda/rulebooks/show_required_reboots.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Reboot hosts requiring reboots 3 | hosts: all 4 | sources: 5 | - name: Match all messages 6 | stdevel.uyuni.requires_reboot: 7 | hostname: 192.168.1.10 8 | username: admin 9 | password: admin 10 | delay: 5 11 | hosts: 12 | - myserver.giertz.loc 13 | 14 | rules: 15 | - name: Show if hosts require a reboot (true) 16 | condition: event.requires_reboot == True 17 | action: 18 | run_module: 19 | name: ansible.builtin.debug 20 | module_args: 21 | msg: "Hosts requires reboot" 22 | 23 | # - name: Reboot hosts if required 24 | # condition: event.requires_reboot == True 25 | # action: 26 | # run_playbook: 27 | # name: stdevel.uyuni.reboot 28 | # post_events: true 29 | 30 | - name: Show if hosts require a reboot (false) 31 | condition: event.requires_reboot == False 32 | action: 33 | run_module: 34 | name: ansible.builtin.debug 35 | module_args: 36 | msg: "Hosts does NOT require a reboot" 37 | -------------------------------------------------------------------------------- /roles/client/molecule/README.md: -------------------------------------------------------------------------------- 1 | # molecule 2 | 3 | This folder contains molecule configuration and tests. 4 | 5 | ## Preparation 6 | 7 | Ensure to the following installed: 8 | 9 | - [Vagrant](https://vagrantup.com) 10 | - [Oracle VirtualBox](https://virtualbox.org) 11 | - Python modules 12 | - [`molecule`](https://pypi.org/project/molecule/) 13 | - [`molecule-vagrant`](https://pypi.org/project/molecule-vagrant/) 14 | - [`python-vagrant`](https://pypi.org/project/python-vagrant/) 15 | 16 | ## Environment 17 | 18 | The test environment consists of one test scenario: 19 | 20 | - `default` - default scenario with VM running openSUSE Leap 15.x 21 | 22 | ## Usage 23 | 24 | In order to create the test environment execute the following command: 25 | 26 | ```shell 27 | $ molecule create 28 | ``` 29 | 30 | Edit [`default/converge.yml`](default/converge.yml) and enter a valid Uyuni server: 31 | 32 | ```yaml 33 | ... 34 | roles: 35 | - role: stdevel.uyuni.client 36 | client_uyuni_server: uyuni.evilcorp.lan 37 | ``` 38 | 39 | Run the Ansible role: 40 | 41 | ```shell 42 | $ molecule converge 43 | ``` 44 | 45 | Finally, run the tests: 46 | 47 | ```shell 48 | $ molecule verify 49 | ``` 50 | -------------------------------------------------------------------------------- /.github/workflows/ansible-test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Sanity checks 3 | 4 | concurrency: 5 | group: ${{ github.head_ref || github.run_id }} 6 | cancel-in-progress: true 7 | 8 | on: 9 | # Run CI against all pushes (direct commits, also merged PRs), Pull Requests 10 | push: 11 | pull_request: 12 | # Run CI once per day (at 06:00 UTC) 13 | # This ensures that even if there haven't been commits that we are still testing against latest version of ansible-test for each ansible-base version 14 | schedule: 15 | - cron: '0 6 * * *' 16 | env: 17 | NAMESPACE: stdevel 18 | COLLECTION_NAME: uyuni 19 | # ANSIBLE_TEST_PREFER_PODMAN: 1 20 | 21 | jobs: 22 | ansible-lint: 23 | uses: ansible/ansible-content-actions/.github/workflows/ansible_lint.yaml@main 24 | with: 25 | args: "--exclude .ansible/" 26 | sanity: 27 | uses: ansible/ansible-content-actions/.github/workflows/sanity.yaml@main 28 | all_green: 29 | if: ${{ always() }} 30 | needs: 31 | - ansible-lint 32 | - sanity 33 | runs-on: ubuntu-latest 34 | steps: 35 | - run: >- 36 | python -c "assert 'failure' not in 37 | set([ 38 | '${{ needs.sanity.result }}', 39 | '${{ needs.ansible-lint.result }}' 40 | ])" 41 | -------------------------------------------------------------------------------- /roles/client/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | """ 2 | Molecule unit tests 3 | """ 4 | import os 5 | import testinfra.utils.ansible_runner 6 | 7 | TESTINFRA_HOSTS = testinfra.utils.ansible_runner.AnsibleRunner( 8 | os.environ['MOLECULE_INVENTORY_FILE'] 9 | ).get_hosts('all') 10 | 11 | 12 | def test_salt(host): 13 | """ 14 | Test if salt-minion is running 15 | """ 16 | minion = host.process.filter(comm="salt-minion") 17 | minion_venv = host.process.filter( 18 | comm="/usr/lib/venv-salt-minion/bin/python.original" 19 | ) 20 | assert minion or minion_venv 21 | 22 | 23 | def test_repo(host): 24 | """ 25 | Test if repository was created 26 | """ 27 | # get OS family 28 | os = host.ansible("setup")["ansible_facts"]["ansible_os_family"].lower() 29 | if os == "debian": 30 | repo_file = host.file( 31 | "/etc/apt/sources.list.d/susemanager:channels.list" 32 | ) 33 | elif os == "redhat": 34 | repo_file = host.file( 35 | "/etc/yum.repos.d/susemanager:channels.repo" 36 | ) 37 | elif os == "suse": 38 | repo_file = host.file( 39 | "/etc/zypp/repos.d/susemanager:channels.repo" 40 | ) 41 | assert repo_file.exists 42 | assert repo_file.size > 100 43 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | """ 2 | Molecule unit tests 3 | """ 4 | 5 | import os 6 | import testinfra.utils.ansible_runner 7 | 8 | TESTINFRA_HOSTS = testinfra.utils.ansible_runner.AnsibleRunner( 9 | os.environ["MOLECULE_INVENTORY_FILE"] 10 | ).get_hosts("all") 11 | 12 | 13 | def test_packages(host): 14 | """ 15 | check if packages are installed 16 | """ 17 | # get variables from file 18 | ansible_vars = host.ansible("include_vars", "file=main.yml") 19 | # check dependencies and proxy packages 20 | for pkg in ansible_vars["ansible_facts"]["proxy_pkgs"]: 21 | assert host.package(pkg).is_installed 22 | 23 | 24 | def test_setup_complete(host): 25 | """ 26 | check if installation files exist 27 | """ 28 | with host.sudo(): 29 | for state_file in [ 30 | "/var/lib/containers/storage/volumes/uyuni-proxy-squid-cache/_data", 31 | "/var/lib/containers/storage/volumes/uyuni-proxy-rhn-cache/_data", 32 | "/var/lib/containers/storage/volumes/uyuni-proxy-tftpboot/_data" 33 | ]: 34 | assert host.file(state_file).exists 35 | 36 | 37 | def test_ports_listen(host): 38 | """ 39 | check if ports are listening 40 | """ 41 | for port in [443, 4505, 4506]: 42 | assert host.socket("tcp://0.0.0.0:%s" % port).is_listening 43 | -------------------------------------------------------------------------------- /plugins/module_utils/utilities.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shared functions 3 | """ 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | from collections import namedtuple 7 | __metaclass__ = type 8 | 9 | NVREA = namedtuple("NVREA", "name version release epoch architecture") 10 | 11 | 12 | def split_rpm_filename(filename: str): 13 | """ 14 | Splits a standard style RPM file name into NVREA. 15 | It returns a name, version, release, epoch, arch, e.g.: 16 | foo-1.0-1.i386.rpm returns foo, 1.0, 1, i386 17 | 1:bar-9-123a.ia64.rpm returns bar, 9, 123a, 1, ia64 18 | 19 | Proudly taken from: 20 | https://github.com/rpm-software-management/ 21 | yum/blob/master/rpmUtils/miscutils.py 22 | 23 | :param filename: RPM file name 24 | :type filename: str 25 | :rtype: NVREA 26 | """ 27 | 28 | if filename[-4:] == ".rpm": 29 | filename = filename[:-4] 30 | 31 | arch_index = filename.rfind(".") 32 | arch = filename[arch_index + 1:] 33 | 34 | rel_index = filename[:arch_index].rfind("-") 35 | rel = filename[rel_index + 1:arch_index] 36 | 37 | ver_index = filename[:rel_index].rfind("-") 38 | ver = filename[ver_index + 1:rel_index] 39 | 40 | epoch_index = filename.find(":") 41 | if epoch_index == -1: 42 | epoch = "" 43 | else: 44 | epoch = filename[:epoch_index] 45 | 46 | name = filename[epoch_index + 1:ver_index] 47 | return NVREA(name, ver, rel, epoch, arch) 48 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | --- 2 | requires_ansible: '>=2.15.0' 3 | 4 | # Content that Ansible needs to load from another location or that has 5 | # been deprecated/removed 6 | # plugin_routing: 7 | # action: 8 | # redirected_plugin_name: 9 | # redirect: ns.col.new_location 10 | # deprecated_plugin_name: 11 | # deprecation: 12 | # removal_version: "4.0.0" 13 | # warning_text: | 14 | # See the porting guide on how to update your playbook to 15 | # use ns.col.another_plugin instead. 16 | # removed_plugin_name: 17 | # tombstone: 18 | # removal_version: "2.0.0" 19 | # warning_text: | 20 | # See the porting guide on how to update your playbook to 21 | # use ns.col.another_plugin instead. 22 | # become: 23 | # cache: 24 | # callback: 25 | # cliconf: 26 | # connection: 27 | # doc_fragments: 28 | # filter: 29 | # httpapi: 30 | # inventory: 31 | # lookup: 32 | # module_utils: 33 | # modules: 34 | # netconf: 35 | # shell: 36 | # strategy: 37 | # terminal: 38 | # test: 39 | # vars: 40 | 41 | # Python import statements that Ansible needs to load from another location 42 | # import_redirection: 43 | # ansible_collections.ns.col.plugins.module_utils.old_location: 44 | # redirect: ansible_collections.ns.col.plugins.module_utils.new_location 45 | 46 | # Groups of actions/modules that take a common set of options 47 | # action_groups: 48 | # group_name: 49 | # - module1 50 | # - module2 51 | -------------------------------------------------------------------------------- /roles/proxy/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install packages 3 | community.general.zypper: 4 | name: "{{ proxy_pkgs }}" 5 | notify: Reboot system (if immutable) 6 | become: true 7 | 8 | - name: Ensure that Podman volumes work as expected 9 | containers.podman.podman_volume: 10 | name: test_volume 11 | become: true 12 | 13 | - name: Prepare storage for volumes and database 14 | ansible.builtin.command: 15 | cmd: "mgr-storage-proxy {{ proxy_disk_volumes }}" 16 | when: 17 | - proxy_disk is defined 18 | become: true 19 | 20 | - name: Download air-gapped container image 21 | community.general.zypper: 22 | name: suse-manager-{{ proxy_suma_release }}-{{ ansible_architecture }}-proxy-image 23 | notify: Reboot system (if immutable) 24 | when: proxy_suma_airgapped 25 | become: true 26 | 27 | - name: Trigger reboot (if necessary) 28 | ansible.builtin.meta: flush_handlers 29 | 30 | - name: Run installation 31 | ansible.builtin.command: "mgrpxy install podman {{ proxy_config_file }}" 32 | args: 33 | creates: /var/lib/containers/storage/volumes/uyuni-proxy-squid-cache/_data 34 | become: true 35 | 36 | - name: Enable netavark workaround 37 | community.general.ini_file: 38 | path: /etc/systemd/system/uyuni-proxy-pod.service.d/netavark.conf 39 | owner: root 40 | group: root 41 | mode: '0640' 42 | section: Service 43 | option: Environment 44 | value: NETAVARK_FW=iptables 45 | no_extra_spaces: true 46 | backup: true 47 | when: ansible_distribution | lower in ['opensuse microos', 'opensuse tumbleweed'] 48 | become: true 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ansible-collection-uyuni 2 | 3 | Ansible Collection for managing Uyuni / SUSE Manager installations and ressources. 4 | 5 | ## Roles 6 | 7 | - [`server`](roles/server) - Prepares, installs and configures Uyuni or SUSE Manager 8 | - [`client`](roles/client) - Bootstraps Uyuni or SUSE Manager clients 9 | 10 | ## Plugins 11 | 12 | - [`apply_highstate`](plugins/modules/apply_highstate.py) - Apply a host's highstate 13 | - [`apply_states`](plugins/modules/apply_states.py) - Apply states for a host 14 | - [`install_patches`](plugins/modules/install_patches.py) - Installs patches on managed hosts 15 | - [`install_upgrades`](plugins/modules/install_upgrades.py) - Installs package upgrades on managed hosts 16 | - [`inventory`](plugins/inventory/inventory.py) - Dynamic inventory 17 | - [`openscap_run`](plugins/modules/openscap_run.py) - Schedules OpenSCAP runson managed hosts 18 | - [`reboot_host`](plugins/modules/reboot_host.py) - Reboots a managed hosts 19 | 20 | ### Event-driven Ansible 21 | 22 | - [`requires_reboot`](extensions/eda/plugins/event_source/requires_reboot.py) - Checks whether a particular system requires a reboot 23 | 24 | Check-out [issues](https://github.com/stdevel/ansible-collection-uyuni/issues) for known issues, missing and upcoming functionality. 25 | 26 | ## Notes 27 | 28 | When using SLES or SL(E) Micro for using this collection you will most likely have to install an additional Python interpreter - the system-wide installation (3.6) is way too old. 29 | 30 | ## Demonstration 31 | 32 | See [the following GitHub repository](https://github.com/stdevel/susecon-suma-aap-demo) for a demonstration of using this collection with AWX. 33 | -------------------------------------------------------------------------------- /roles/server/defaults/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # check requirements 3 | server_check_requirements: true 4 | 5 | # required core packages 6 | server_core_packages: 7 | - man 8 | - firewalld 9 | 10 | server_packages: [] 11 | # server_packages: 12 | # - spacewalk-utils 13 | # - spacecmd 14 | 15 | server_suma_release: 5.0 16 | 17 | server_scc_url: https://scc.suse.com 18 | server_scc_check_registration: true 19 | server_scc_check_modules: true 20 | 21 | # Uyuni configuration 22 | server_mail: "root@localhost" 23 | server_db_name: "uyuni" 24 | server_db_user: "uyuni" 25 | server_db_pass: "Uyuni1337" 26 | server_cert_city: "Darmstadt" 27 | server_cert_country: "DE" 28 | server_cert_mail: "root@localhost" 29 | server_cert_o: "Darmstadt" 30 | server_cert_ou: "Darmstadt" 31 | server_cert_state: "Hessen" 32 | server_cert_pass: "uyuni" 33 | server_org_name: "Demo" 34 | server_org_login: "admin" 35 | server_org_password: "admin" 36 | server_org_mail: "root@localhost" 37 | server_org_first_name: "Anton" 38 | server_org_last_name: "Administrator" 39 | 40 | # additional settings 41 | server_use_repo: true 42 | server_suma_airgapped: false 43 | 44 | # firewall settings 45 | server_firewall_config: true 46 | server_firewall_default_zone: internal 47 | server_firewall_services: 48 | - suse-manager-server 49 | server_firewall_ports: [] 50 | server_firewall_ports_monitoring: 51 | - 5556/tcp 52 | - 5557/tcp 53 | - 9100/tcp 54 | - 9187/tcp 55 | - 9800/tcp 56 | 57 | # content settings 58 | server_channels: [] 59 | # create dict items like this: 60 | # - {"name": "centos7", "arch": "x86_64"} 61 | # - {"name": "centos7-updates", "arch": "x86_64"} 62 | server_sync_channels: false 63 | server_bootstrap_repos: false 64 | 65 | # monitoring settings 66 | server_enable_monitoring: false 67 | -------------------------------------------------------------------------------- /roles/proxy/molecule/README.md: -------------------------------------------------------------------------------- 1 | # molecule 2 | 3 | This folder contains molecule configuration and tests. 4 | 5 | ## Preparation 6 | 7 | Ensure to the following installed: 8 | 9 | - Python modules 10 | - [`molecule`](https://pypi.org/project/molecule/) 11 | - Podman and Molecule Podman plugin 12 | 13 | ## Environment 14 | 15 | The test environment consists of two test scenarios: 16 | 17 | - `default` - default scenario with a container running openSUSE Tumbleweed 18 | - `mlm` - SUSE Manager 5.0 scenario with VM running SLE Micro 5.5 19 | 20 | ### SUSE hints 21 | 22 | In order to run tests against SUSE Manager 5.x you will either require a valid subscription or a trial license. 23 | You can request a [60-day trial on the SUSE website.](https://www.suse.com/products/suse-manager/download/) 24 | For this, you will need to create a [SUSE Customer Center](https://scc.suse.com) account - you will **not** be able to request an additional trial for the same release after the 60 days have expired. 25 | 26 | **NOTE:** You will need to setup this VM manually, set the IP address in [`molecule.yml`](molecule.yml) and add an `/etc/hosts` entry with `instance`. This will change once Multi-Linux Manager 5.1 is released. 27 | 28 | ## Usage 29 | 30 | In order to create the test environment execute the following command: 31 | 32 | ```shell 33 | $ molecule create 34 | ``` 35 | 36 | Run the Ansible role: 37 | 38 | ```shell 39 | $ molecule converge 40 | ``` 41 | 42 | Finally, run the tests: 43 | 44 | ```shell 45 | $ molecule verify 46 | ... 47 | collected 8 items 48 | 49 | tests/test_default.py ........ [100%] 50 | 51 | ========================== 8 passed in 14.09 seconds =========================== 52 | Verifier completed successfully. 53 | ``` 54 | 55 | For running tests in the `mlm` scenario context, run the commands above with the `-s mlm` parameter. 56 | -------------------------------------------------------------------------------- /roles/server/molecule/README.md: -------------------------------------------------------------------------------- 1 | # molecule 2 | 3 | This folder contains molecule configuration and tests. 4 | 5 | ## Preparation 6 | 7 | Ensure to the following installed: 8 | 9 | - Python modules 10 | - [`molecule`](https://pypi.org/project/molecule/) 11 | - Podman and Molecule Podman plugin 12 | 13 | ## Environment 14 | 15 | The test environment consists of two test scenarios: 16 | 17 | - `default` - default scenario with a container running openSUSE Tumbleweed 18 | - `mlm` - SUSE Manager 5.0 scenario with VM running SLE Micro 5.5 19 | 20 | ### SUSE hints 21 | 22 | In order to run tests against SUSE Manager 5.x you will either require a valid subscription or a trial license. 23 | You can request a [60-day trial on the SUSE website.](https://www.suse.com/products/suse-manager/download/) 24 | For this, you will need to create a [SUSE Customer Center](https://scc.suse.com) account - you will **not** be able to request an additional trial for the same release after the 60 days have expired. 25 | 26 | **NOTE:** You will need to setup this VM manually, set the IP address in [`molecule.yml`](molecule.yml) and add an `/etc/hosts` entry with `instance`. This will change once Multi-Linux Manager 5.1 is released. 27 | 28 | ## Usage 29 | 30 | In order to create the test environment execute the following command: 31 | 32 | ```shell 33 | $ molecule create 34 | ``` 35 | 36 | Run the Ansible role: 37 | 38 | ```shell 39 | $ molecule converge 40 | ``` 41 | 42 | Finally, run the tests: 43 | 44 | ```shell 45 | $ molecule verify 46 | ... 47 | collected 8 items 48 | 49 | tests/test_default.py ........ [100%] 50 | 51 | ========================== 8 passed in 14.09 seconds =========================== 52 | Verifier completed successfully. 53 | ``` 54 | 55 | For running tests in the `mlm` scenario context, run the commands above with the `-s mlm` parameter. 56 | -------------------------------------------------------------------------------- /roles/client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | This role bootstraps [Uyuni](https://uyuni-project.org) and [SUSE Manager](https://www.suse.com/products/suse-manager/) clients. 4 | 5 | It requires that you have a **valid bootstrap script** placed at `/srv/www/htdocs/pub/bootstrap` containing an **activation key**. By default, this role searches for a bootstrap script including the appropriate Linux distribution and version, e.g. `bootstrap-debian11.sh` or `bootstrap-opensuse_leap15.4.sh`. 6 | 7 | ## Requirements 8 | 9 | No requirements. 10 | 11 | ## Role Variables 12 | 13 | | Variable | Default | Description | 14 | | -------- | ------- | ----------- | 15 | | `client_uyuni_server` | **empty** | Uyuni server hostname or FQDN | 16 | | `client_bootstrap_filename` | `(distro)(version).sh` | Bootstrap file to download | 17 | | `client_bootstrap_folder` | `/opt` | Bootstrap file download folder | 18 | | `client_state` | `present` | Bootstrap (`present`) or remove (`absent`) client | 19 | 20 | ## Dependencies 21 | 22 | No dependencies. 23 | 24 | ## Example Playbook 25 | 26 | Refer to the following example: 27 | 28 | ```yaml 29 | - hosts: clients 30 | roles: 31 | - role: stdevel.uyuni.client 32 | client_uyuni_server: uyuni01.evilcorp.lan 33 | ``` 34 | 35 | Set variables if required, e.g.: 36 | 37 | ```yaml 38 | --- 39 | - hosts: clients 40 | roles: 41 | - role: stdevel.uyuni.client 42 | client_uyuni_server: uyuni01.evilcorp.lan 43 | client_bootstrap_filename: bootstrap-dummy.sh 44 | client_bootstrap_folder: /tmp 45 | ``` 46 | 47 | To remove `salt-minion` and managed software repositories, set `client_state` to `absent`: 48 | 49 | ```yaml 50 | --- 51 | - hosts: clients 52 | roles: 53 | - role: stdevel.uyuni.client 54 | client_state: absent 55 | ``` 56 | 57 | **NOTE**: This will **not** remove the appropriate system profile from Uyuni/SUSE Manager. 58 | 59 | ## License 60 | 61 | GPL 3.0 62 | 63 | ## Author Information 64 | 65 | Christian Stankowic 66 | -------------------------------------------------------------------------------- /roles/proxy/tasks/prepare_sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check system registration 3 | ansible.builtin.command: SUSEConnect --status 4 | changed_when: false 5 | register: proxy_scc_registration 6 | when: proxy_scc_check_registration or proxy_scc_check_modules 7 | become: true 8 | 9 | - name: Register system 10 | ansible.builtin.command: "suseconnect --url {{ proxy_scc_url }} -r {{ proxy_scc_reg_code_os | upper }} -e {{ proxy_scc_mail }}" 11 | when: 12 | - proxy_scc_check_registration 13 | - proxy_scc_reg_code_os 14 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 15 | vars: 16 | query_filter: "[?identifier == 'SLES'].status" 17 | become: true 18 | 19 | - name: Register MLM 20 | ansible.builtin.command: "suseconnect --url {{ proxy_scc_url }} -p SUSE-Manager-Proxy/{{ proxy_suma_release }}/{{ ansible_architecture }} -r {{ proxy_scc_reg_code_mlm | upper }} -e {{ proxy_scc_mail }}" 21 | when: 22 | - proxy_scc_check_registration 23 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 24 | vars: 25 | query_filter: "[?identifier == 'SUSE-Manager-Proxy'].status" 26 | become: true 27 | 28 | - name: Enable modules 29 | ansible.builtin.command: "suseconnect --url {{ proxy_scc_url }} -p {{ item.identifier }}" 30 | loop: "{{ proxy_suma_modules }}" 31 | when: 32 | - proxy_scc_check_modules 33 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 34 | vars: 35 | query_filter: "[?identifier == '{{ item.name }}'].status" 36 | become: true 37 | 38 | - name: Set default image name 39 | ansible.builtin.set_fact: 40 | proxy_image_name: registry.suse.com/suse/manager/5.0/{{ ansible_architecture }}/proxy 41 | when: proxy_suma_release is not defined 42 | 43 | - name: Set default image name (release-specific) 44 | ansible.builtin.set_fact: 45 | proxy_image_name: registry.suse.com/suse/manager/{{ proxy_suma_release }}/{{ ansible_architecture }}/proxy 46 | when: proxy_suma_release is defined 47 | -------------------------------------------------------------------------------- /roles/server/tasks/prepare_sles.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check system registration 3 | ansible.builtin.command: SUSEConnect --status 4 | changed_when: false 5 | register: server_scc_registration 6 | when: server_scc_check_registration or server_scc_check_modules 7 | become: true 8 | 9 | - name: Register system 10 | ansible.builtin.command: "suseconnect --url {{ server_scc_url }} -r {{ server_scc_reg_code_os | upper }} -e {{ server_scc_mail }}" 11 | when: 12 | - server_scc_check_registration 13 | - server_scc_reg_code_os 14 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 15 | vars: 16 | query_filter: "[?identifier == 'SLES'].status" 17 | become: true 18 | 19 | - name: Register MLM 20 | ansible.builtin.command: "suseconnect --url {{ server_scc_url }} -p SUSE-Manager-Server/{{ server_suma_release }}/{{ ansible_architecture }} -r {{ server_scc_reg_code_mlm | upper }} -e {{ server_scc_mail }}" 21 | when: 22 | - server_scc_check_registration 23 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 24 | vars: 25 | query_filter: "[?identifier == 'SUSE-Manager-Server'].status" 26 | become: true 27 | 28 | - name: Enable modules 29 | ansible.builtin.command: "suseconnect --url {{ server_scc_url }} -p {{ item.identifier }}" 30 | loop: "{{ server_suma_modules }}" 31 | when: 32 | - server_scc_check_modules 33 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 34 | vars: 35 | query_filter: "[?identifier == '{{ item.name }}'].status" 36 | become: true 37 | 38 | - name: Set default image name 39 | ansible.builtin.set_fact: 40 | server_image_name: registry.suse.com/suse/manager/5.0/{{ ansible_architecture }}/server 41 | when: server_suma_release is not defined 42 | 43 | - name: Set default image name (release-specific) 44 | ansible.builtin.set_fact: 45 | server_image_name: registry.suse.com/suse/manager/{{ server_suma_release }}/{{ ansible_architecture }}/server 46 | when: server_suma_release is defined 47 | -------------------------------------------------------------------------------- /roles/proxy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include variables 3 | ansible.builtin.include_vars: "{{ lookup('ansible.builtin.first_found', params) }}" 4 | vars: 5 | params: 6 | files: 7 | - "{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 8 | - "{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 9 | - "{{ ansible_distribution | split(' ') | first | lower }}.yml" 10 | - "{{ ansible_os_family | lower }}.yml" 11 | paths: 12 | - vars 13 | 14 | - name: Include check tasks (distribution-specific) 15 | ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', params) }}" 16 | vars: 17 | params: 18 | files: 19 | - "check_{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 20 | - "check_{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 21 | - "check_{{ ansible_distribution | split(' ') | first | lower }}.yml" 22 | - "check_{{ ansible_os_family | lower }}.yml" 23 | skip: true 24 | paths: 25 | - tasks 26 | tags: prepare 27 | 28 | - name: Include check tasks (generic) 29 | ansible.builtin.import_tasks: check.yml 30 | tags: prepare 31 | 32 | - name: Include preparation tasks (distribution-specific) 33 | ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', params) }}" 34 | vars: 35 | params: 36 | files: 37 | - "prepare_{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 38 | - "prepare_{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 39 | - "prepare_{{ ansible_distribution | split(' ') | first | lower }}.yml" 40 | - "prepare_{{ ansible_os_family | lower }}.yml" 41 | skip: true 42 | paths: 43 | - tasks 44 | tags: prepare 45 | 46 | - name: Include preparation tasks (generic) 47 | ansible.builtin.include_tasks: "prepare.yml" 48 | tags: prepare 49 | 50 | - name: Include install tasks 51 | ansible.builtin.import_tasks: install.yml 52 | tags: install 53 | -------------------------------------------------------------------------------- /plugins/module_utils/exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Exceptions used by the management classes 3 | """ 4 | from __future__ import (absolute_import, division, print_function) 5 | __metaclass__ = type 6 | 7 | 8 | class SessionException(Exception): 9 | """ 10 | Exception for session errors 11 | 12 | .. class:: SessionException 13 | """ 14 | 15 | 16 | class InvalidCredentialsException(Exception): 17 | """ 18 | Exception for invalid credentials 19 | 20 | .. class:: InvalidCredentialsException 21 | """ 22 | 23 | 24 | class APILevelNotSupportedException(Exception): 25 | """ 26 | Exception for unsupported API levels 27 | 28 | .. class:: APILevelNotSupportedException 29 | """ 30 | 31 | 32 | class UnsupportedRequestException(Exception): 33 | """ 34 | Exception for unsupported requests 35 | 36 | .. class:: UnsupportedRequest 37 | """ 38 | 39 | 40 | class InvalidHostnameFormatException(Exception): 41 | """ 42 | Exception for invalid hostname formats (non-FQDN) 43 | 44 | .. class:: InvalidHostnameFormatException 45 | """ 46 | 47 | 48 | class UnsupportedFilterException(Exception): 49 | """ 50 | Exception for unsupported filters 51 | 52 | .. class:: UnsupportedFilterException 53 | """ 54 | 55 | 56 | class EmptySetException(Exception): 57 | """ 58 | Exception for empty result sets 59 | 60 | .. class:: EmptySetException 61 | """ 62 | 63 | 64 | class CustomVariableExistsException(Exception): 65 | """ 66 | Exception for already existing custom variables 67 | 68 | .. class:: CustomVariableExistsException 69 | """ 70 | 71 | 72 | class SnapshotExistsException(Exception): 73 | """ 74 | Exception for already existing snapshots 75 | 76 | .. class:: SnapshotExistsException 77 | """ 78 | 79 | 80 | class UnauthenticatedError(RuntimeError): 81 | """ 82 | Exception for showing that a client wasn't able to authenticate itself 83 | 84 | .. class:: UnauthenticatedError 85 | """ 86 | 87 | 88 | class SSLCertVerificationError(Exception): 89 | """ 90 | Exception for invalid SSL certificates 91 | 92 | .. class:: SSLCertVerificationError 93 | """ 94 | -------------------------------------------------------------------------------- /extensions/eda/plugins/event_source/requires_reboot.py: -------------------------------------------------------------------------------- 1 | """ 2 | requires_reboot.py 3 | 4 | ansible-rulebook event source plugin that lists all hosts that require a reboot. 5 | 6 | Arguments: 7 | - hostname: SUSE Manager/Uyuni hostname or IP address 8 | - username: API username 9 | - password: API password 10 | - delay: seconds to wait between events 11 | - hosts: list of hosts to check for reboots 12 | 13 | Examples: 14 | sources: 15 | - stdevel.uyuni.requires_reboot: 16 | hostname: uiuiuiuyuni.local.loc 17 | username: admin 18 | password: admin 19 | delay: 10 20 | hosts: 21 | - uyuni-client.pinkepank.loc 22 | 23 | """ 24 | import asyncio 25 | from typing import Any, Dict 26 | from pyuyuni.management import JSONHTTPClient 27 | from pyuyuni.hosts import list_systems_requiring_reboot 28 | import logging 29 | 30 | 31 | async def main(queue: asyncio.Queue, args: Dict[str, Any]): 32 | """ 33 | Main function that queries the Uyuni and returns whether hosts require reboots 34 | """ 35 | delay = args.get("delay", 60) 36 | hosts = args.get("hosts", []) 37 | hostname = args.get("hostname") 38 | username = args.get("username") 39 | password = args.get("password") 40 | port = args.get("port", 443) 41 | verify = args.get("verify", False) 42 | 43 | # access the Uyuni API 44 | api_client = JSONHTTPClient( 45 | logging.ERROR, 46 | hostname, 47 | username, 48 | password, 49 | port, 50 | verify 51 | ) 52 | 53 | while True: 54 | _systems = list_systems_requiring_reboot(api_client) 55 | for host in hosts: 56 | print(f"checking host {host}") 57 | _flag = True if host in _systems else False 58 | await queue.put( 59 | { 60 | "host": host, 61 | "requires_reboot": _flag 62 | } 63 | ) 64 | await asyncio.sleep(delay) 65 | 66 | 67 | if __name__ == "__main__": 68 | 69 | class MockQueue: 70 | """ 71 | Mock queue class 72 | """ 73 | 74 | async def put(self, event): 75 | """ 76 | Function that simply prints the event 77 | """ 78 | print(event) 79 | 80 | mock_arguments = {} 81 | asyncio.run(main(MockQueue(), mock_arguments)) 82 | -------------------------------------------------------------------------------- /roles/proxy/tasks/prepare_sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check system registration 3 | ansible.builtin.command: SUSEConnect --status 4 | changed_when: false 5 | register: proxy_scc_registration 6 | when: proxy_scc_check_registration or proxy_scc_check_modules 7 | become: true 8 | 9 | - name: Register system 10 | ansible.builtin.command: "transactional-update --continue register --url {{ proxy_scc_url }} -r {{ proxy_scc_reg_code_os | upper }} -e {{ proxy_scc_mail }}" 11 | when: 12 | - proxy_scc_check_registration 13 | - proxy_scc_reg_code_os 14 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 15 | vars: 16 | query_filter: "[?identifier == 'SLE-Micro'].status" 17 | notify: Reboot system (if immutable) 18 | become: true 19 | 20 | - name: Register MLM 21 | ansible.builtin.command: "transactional-update --continue register --url {{ proxy_scc_url }} -p SUSE-Manager-Proxy/{{ proxy_suma_release }}/{{ ansible_architecture }} -r {{ proxy_scc_reg_code_mlm | upper }} -e {{ proxy_scc_mail }}" 22 | when: 23 | - proxy_scc_check_registration 24 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 25 | vars: 26 | query_filter: "[?identifier == 'SUSE-Manager-Proxy'].status" 27 | notify: Reboot system (if immutable) 28 | become: true 29 | 30 | - name: Enable modules 31 | ansible.builtin.command: "transactional-update --continue register --url {{ proxy_scc_url }} -p {{ item.identifier }}" 32 | loop: "{{ proxy_suma_modules }}" 33 | when: 34 | - proxy_scc_check_modules 35 | - 'proxy_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 36 | vars: 37 | query_filter: "[?identifier == '{{ item.search }}'].status" 38 | notify: Reboot system (if immutable) 39 | become: true 40 | 41 | - name: Set default image name 42 | ansible.builtin.set_fact: 43 | proxy_image_name: registry.suse.com/suse/manager/5.0/{{ ansible_architecture }}/proxy 44 | when: proxy_suma_release is not defined 45 | 46 | - name: Set default image name (release-specific) 47 | ansible.builtin.set_fact: 48 | proxy_image_name: registry.suse.com/suse/manager/{{ proxy_suma_release }}/{{ ansible_architecture }}/proxy 49 | when: proxy_suma_release is defined 50 | 51 | - name: Trigger reboot (if necessary) 52 | ansible.builtin.meta: flush_handlers 53 | -------------------------------------------------------------------------------- /roles/server/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Include variables 3 | ansible.builtin.include_vars: "{{ lookup('ansible.builtin.first_found', params) }}" 4 | vars: 5 | params: 6 | files: 7 | - "{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 8 | - "{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 9 | - "{{ ansible_distribution | split(' ') | first | lower }}.yml" 10 | - "{{ ansible_os_family | lower }}.yml" 11 | paths: 12 | - vars 13 | 14 | - name: Include check tasks (distribution-specific) 15 | ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', params) }}" 16 | vars: 17 | params: 18 | files: 19 | - "check_{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 20 | - "check_{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 21 | - "check_{{ ansible_distribution | split(' ') | first | lower }}.yml" 22 | - "check_{{ ansible_os_family | lower }}.yml" 23 | skip: true 24 | paths: 25 | - tasks 26 | tags: prepare 27 | 28 | - name: Include check tasks (generic) 29 | ansible.builtin.import_tasks: check.yml 30 | tags: prepare 31 | 32 | - name: Include preparation tasks (distribution-specific) 33 | ansible.builtin.include_tasks: "{{ lookup('ansible.builtin.first_found', params) }}" 34 | vars: 35 | params: 36 | files: 37 | - "prepare_{{ ansible_distribution | regex_replace(' ', '_') | lower }}-{{ ansible_distribution_version }}.yml" 38 | - "prepare_{{ ansible_distribution | regex_replace(' ', '_') | lower }}.yml" 39 | - "prepare_{{ ansible_distribution | split(' ') | first | lower }}.yml" 40 | - "prepare_{{ ansible_os_family | lower }}.yml" 41 | skip: true 42 | paths: 43 | - tasks 44 | tags: prepare 45 | 46 | - name: Include preparation tasks (generic) 47 | ansible.builtin.include_tasks: "prepare.yml" 48 | tags: prepare 49 | 50 | - name: Include install tasks 51 | ansible.builtin.import_tasks: install.yml 52 | tags: install 53 | 54 | - name: Include content tasks 55 | ansible.builtin.import_tasks: content.yml 56 | tags: content 57 | 58 | - name: Include monitoring tasks 59 | ansible.builtin.include_tasks: monitoring.yml 60 | tags: monitoring 61 | when: server_enable_monitoring | bool 62 | -------------------------------------------------------------------------------- /roles/server/tasks/prepare_sle_micro.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check system registration 3 | ansible.builtin.command: SUSEConnect --status 4 | changed_when: false 5 | register: server_scc_registration 6 | when: server_scc_check_registration or server_scc_check_modules 7 | become: true 8 | 9 | - name: Register system 10 | ansible.builtin.command: "transactional-update --continue register --url {{ server_scc_url }} -r {{ server_scc_reg_code_os | upper }} -e {{ server_scc_mail }}" 11 | when: 12 | - server_scc_check_registration 13 | - server_scc_reg_code_os 14 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 15 | vars: 16 | query_filter: "[?identifier == 'SLE-Micro'].status" 17 | notify: Reboot system (if immutable) 18 | become: true 19 | 20 | - name: Register MLM 21 | ansible.builtin.command: "transactional-update --continue register --url {{ server_scc_url }} -p SUSE-Manager-Server/{{ server_suma_release }}/{{ ansible_architecture }} -r {{ server_scc_reg_code_mlm | upper }} -e {{ server_scc_mail }}" 22 | when: 23 | - server_scc_check_registration 24 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 25 | vars: 26 | query_filter: "[?identifier == 'SUSE-Manager-Server'].status" 27 | notify: Reboot system (if immutable) 28 | become: true 29 | 30 | - name: Enable modules 31 | ansible.builtin.command: "transactional-update --continue register --url {{ server_scc_url }} -p {{ item.identifier }}" 32 | loop: "{{ server_suma_modules }}" 33 | when: 34 | - server_scc_check_modules 35 | - 'server_scc_registration.stdout | from_json | json_query(query_filter) | join | lower != "registered"' 36 | vars: 37 | query_filter: "[?identifier == '{{ item.search }}'].status" 38 | notify: Reboot system (if immutable) 39 | become: true 40 | 41 | - name: Set default image name 42 | ansible.builtin.set_fact: 43 | server_image_name: registry.suse.com/suse/manager/5.0/{{ ansible_architecture }}/server 44 | when: server_suma_release is not defined 45 | 46 | - name: Set default image name (release-specific) 47 | ansible.builtin.set_fact: 48 | server_image_name: registry.suse.com/suse/manager/{{ server_suma_release }}/{{ ansible_architecture }}/server 49 | when: server_suma_release is defined 50 | 51 | - name: Trigger reboot (if necessary) 52 | ansible.builtin.meta: flush_handlers 53 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3.6 (27.08.2025) 4 | 5 | - `server` - added variable `server_fqdn` to set a custom FQDN if `ansible_fqdn` doesn't work for you 6 | 7 | ## 0.3.5 (14.08.2025) 8 | 9 | - fixed mgrctl parameter (added missing --) 10 | - added `apply_highstate` and `apply_states` modules 11 | - **Breaking change**: `server` role variable names have changed 12 | - `uyuni_*` --> `server_*` 13 | - **Breaking change**: `client` role variable names have changed 14 | - `uyuni_*` --> `client_*` 15 | - `uyuni_server` --> `client_uyuni_server` 16 | - fixed linting, refactored code 17 | 18 | ## 0.3.4 (04.06.2025) 19 | 20 | - fix a bug where local timezone was preferred over UTC timezone when scheduling actions 21 | 22 | ## 0.3.3 (28.05.2025) 23 | 24 | - fixed a bug where installing SUSE Manager 5.0 wasn't supported on SLES 15 SP6 25 | 26 | ## 0.3.2 (16.05.2025) 27 | 28 | - added `proxy` role for installing containerized proxy servers 29 | - added note about using this collection on SLES or SL(E) Micro with outdated Python versions 30 | - `server` - ensure that registration codes are always uppercase 31 | 32 | ## 0.3.0 (08.05.2025) 33 | 34 | - added **support for containerized setups** (SUMA 5.0+ and Uyuni 2024.10+) 35 | - openSUSE Tumbleweed, Slowroll, Leap, Leap Micro, SUSE Linux Enterprise Server and SUSE Linux Enterprise Micro/SUSE Linux Micro are supported installation targets 36 | - **Breaking change**: removed support for legacy installations (SUMA 4.x and Uyuni up to 2024.08) 37 | - **Breaking change**: removed Ansible role `stdevel.uyuni.storage` as it's not needed anymore for containerized setups 38 | - see variables `uyuni_disk_volumes` and `uyuni_disk_database` 39 | - `server`: added CPU architecture prerequisite check 40 | - `server`: removed CEFS support as CentOS 7 reached EOL 41 | - `server`: enabling SL(E) modules is now idempotent 42 | - `server`: instead of VMs, containers are now used for development and testing 43 | - `client`: client registration task now idempotent 44 | - added `is_reboot_required` und `full_pkg_update` modules 45 | 46 | ## 0.2.1 (02.05.2024) 47 | 48 | - modules will now report the appropriate action ID 49 | 50 | ## 0.2.0 (07.03.2024) 51 | 52 | - inventory plugin: add environment variable passthrough 53 | 54 | ## 0.1.0 (15.04.2023) 55 | 56 | - initial release 57 | - added dynamic inventory, `install_patches`, `install_upgrades`, `openscap_run` and `reboot_host` modules 58 | - splitted former `stdevel.uyuni` Ansible role in `stdevel.uyuni.storage` and `stdevel.uyuni.server` roles 59 | -------------------------------------------------------------------------------- /roles/server/tasks/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install packages 3 | community.general.zypper: 4 | name: "{{ server_pkgs }}" 5 | notify: Reboot system (if immutable) 6 | become: true 7 | 8 | - name: Ensure that Podman volumes work as expected 9 | containers.podman.podman_volume: 10 | name: test_volume 11 | become: true 12 | 13 | - name: Prepare storage for volumes and database 14 | ansible.builtin.command: 15 | cmd: "mgr-storage-server {{ server_disk_volumes }} {{ server_disk_database }}" 16 | when: 17 | - server_disk_volumes is defined 18 | - server_disk_database is defined 19 | become: true 20 | 21 | - name: Prepare storage for volumes 22 | ansible.builtin.command: 23 | cmd: "mgr-storage-server {{ server_disk_volumes }}" 24 | when: server_disk_volumes is defined 25 | become: true 26 | 27 | - name: Mount volumes volume 28 | ansible.posix.mount: 29 | path: /var/lib/containers/storage/volumes 30 | state: remounted 31 | when: server_disk_volumes is defined 32 | become: true 33 | 34 | - name: Mount database volume 35 | ansible.posix.mount: 36 | path: /var/lib/containers/storage/volumes/var-pgsql 37 | state: remounted 38 | when: server_disk_database is defined 39 | become: true 40 | 41 | - name: Stage deploy configuration 42 | ansible.builtin.template: 43 | src: uyuni.yml.j2 44 | dest: "/root/uyuni.yml" 45 | mode: "0644" 46 | owner: root 47 | group: root 48 | become: true 49 | 50 | - name: Download air-gapped container image 51 | community.general.zypper: 52 | name: suse-manager-{{ server_suma_release }}-{{ ansible_architecture }}-server-image 53 | notify: Reboot system (if immutable) 54 | when: server_suma_airgapped 55 | become: true 56 | 57 | - name: Enable netavark workaround 58 | community.general.ini_file: 59 | path: /etc/systemd/system/uyuni-server.service.d/netavark.conf 60 | owner: root 61 | group: root 62 | mode: '0640' 63 | section: Service 64 | option: Environment 65 | value: NETAVARK_FW=iptables 66 | no_extra_spaces: true 67 | backup: true 68 | when: ansible_distribution | lower in ['opensuse microos', 'opensuse tumbleweed'] 69 | become: true 70 | 71 | - name: Trigger reboot (if necessary) 72 | ansible.builtin.meta: flush_handlers 73 | 74 | - name: Set default FQDN 75 | ansible.builtin.set_fact: 76 | server_fqdn: "{{ ansible_fqdn }}" 77 | when: server_fqdn is not defined 78 | 79 | - name: Run installation 80 | ansible.builtin.command: "mgradm -c /root/uyuni.yml install podman {{ server_fqdn }}" 81 | args: 82 | creates: /root/.MANAGER_INITIALIZATION_COMPLETE 83 | become: true 84 | notify: Create initialization file 85 | -------------------------------------------------------------------------------- /roles/proxy/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | gather_facts: false 5 | vars: 6 | molecule_inventory: 7 | all: 8 | hosts: {} 9 | children: 10 | molecule: 11 | hosts: {} 12 | 13 | tasks: 14 | - name: Create a container 15 | containers.podman.podman_container: 16 | name: "{{ item.name }}" 17 | hostname: uyuni.podman.loc 18 | image: "{{ item.image }}" 19 | privileged: "{{ item.privileged | default(omit) }}" 20 | volumes: "{{ item.volumes | default(omit) }}" 21 | capabilities: "{{ item.capabilities | default(omit) }}" 22 | systemd: "{{ item.systemd | default(omit) }}" 23 | state: started 24 | command: "{{ item.command | default('sleep 1d') }}" 25 | # bash -c "while true; do sleep 10000; done" 26 | log_driver: json-file 27 | ports: 28 | - '8443:443' 29 | - '8080:80' 30 | register: result 31 | loop: "{{ molecule_yml.platforms }}" 32 | 33 | - name: Print some info 34 | ansible.builtin.debug: 35 | msg: "{{ result.results }}" 36 | 37 | - name: Fail if container is not running 38 | when: > 39 | item.container.State.ExitCode != 0 or 40 | not item.container.State.Running 41 | ansible.builtin.include_tasks: 42 | file: tasks/create-fail.yml 43 | loop: "{{ result.results }}" 44 | loop_control: 45 | label: "{{ item.container.Name }}" 46 | 47 | - name: Add container to molecule_inventory 48 | vars: 49 | inventory_partial_yaml: | 50 | all: 51 | children: 52 | molecule: 53 | hosts: 54 | "{{ item.name }}": 55 | ansible_connection: containers.podman.podman 56 | ansible.builtin.set_fact: 57 | molecule_inventory: > 58 | {{ molecule_inventory | combine(inventory_partial_yaml | from_yaml, recursive=true) }} 59 | loop: "{{ molecule_yml.platforms }}" 60 | loop_control: 61 | label: "{{ item.name }}" 62 | 63 | - name: Dump molecule_inventory 64 | ansible.builtin.copy: 65 | content: | 66 | {{ molecule_inventory | to_yaml }} 67 | dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 68 | mode: "0600" 69 | 70 | - name: Force inventory refresh 71 | ansible.builtin.meta: refresh_inventory 72 | 73 | - name: Fail if molecule group is missing 74 | ansible.builtin.assert: 75 | that: "'molecule' in groups" 76 | fail_msg: | 77 | molecule group was not found inside inventory groups: {{ groups }} 78 | run_once: true # noqa: run-once[task] 79 | 80 | # we want to avoid errors like "Failed to create temporary directory" 81 | - name: Validate that inventory was refreshed 82 | hosts: molecule 83 | gather_facts: false 84 | tasks: 85 | - name: Check uname 86 | ansible.builtin.raw: uname -a 87 | register: result 88 | changed_when: false 89 | 90 | - name: Display uname info 91 | ansible.builtin.debug: 92 | msg: "{{ result.stdout }}" 93 | -------------------------------------------------------------------------------- /roles/server/molecule/default/create.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create 3 | hosts: localhost 4 | gather_facts: false 5 | vars: 6 | molecule_inventory: 7 | all: 8 | hosts: {} 9 | children: 10 | molecule: 11 | hosts: {} 12 | 13 | tasks: 14 | - name: Create a container 15 | containers.podman.podman_container: 16 | name: "{{ item.name }}" 17 | hostname: uyuni.podman.loc 18 | image: "{{ item.image }}" 19 | privileged: "{{ item.privileged | default(omit) }}" 20 | volumes: "{{ item.volumes | default(omit) }}" 21 | capabilities: "{{ item.capabilities | default(omit) }}" 22 | systemd: "{{ item.systemd | default(omit) }}" 23 | state: started 24 | command: "{{ item.command | default('sleep 1d') }}" 25 | # bash -c "while true; do sleep 10000; done" 26 | log_driver: json-file 27 | ports: 28 | - '8443:443' 29 | - '8080:80' 30 | register: result 31 | loop: "{{ molecule_yml.platforms }}" 32 | 33 | - name: Print some info 34 | ansible.builtin.debug: 35 | msg: "{{ result.results }}" 36 | 37 | - name: Fail if container is not running 38 | when: > 39 | item.container.State.ExitCode != 0 or 40 | not item.container.State.Running 41 | ansible.builtin.include_tasks: 42 | file: tasks/create-fail.yml 43 | loop: "{{ result.results }}" 44 | loop_control: 45 | label: "{{ item.container.Name }}" 46 | 47 | - name: Add container to molecule_inventory 48 | vars: 49 | inventory_partial_yaml: | 50 | all: 51 | children: 52 | molecule: 53 | hosts: 54 | "{{ item.name }}": 55 | ansible_connection: containers.podman.podman 56 | ansible.builtin.set_fact: 57 | molecule_inventory: > 58 | {{ molecule_inventory | combine(inventory_partial_yaml | from_yaml, recursive=true) }} 59 | loop: "{{ molecule_yml.platforms }}" 60 | loop_control: 61 | label: "{{ item.name }}" 62 | 63 | - name: Dump molecule_inventory 64 | ansible.builtin.copy: 65 | content: | 66 | {{ molecule_inventory | to_yaml }} 67 | dest: "{{ molecule_ephemeral_directory }}/inventory/molecule_inventory.yml" 68 | mode: "0600" 69 | 70 | - name: Force inventory refresh 71 | ansible.builtin.meta: refresh_inventory 72 | 73 | - name: Fail if molecule group is missing 74 | ansible.builtin.assert: 75 | that: "'molecule' in groups" 76 | fail_msg: | 77 | molecule group was not found inside inventory groups: {{ groups }} 78 | run_once: true # noqa: run-once[task] 79 | 80 | # we want to avoid errors like "Failed to create temporary directory" 81 | - name: Validate that inventory was refreshed 82 | hosts: molecule 83 | gather_facts: false 84 | tasks: 85 | - name: Check uname 86 | ansible.builtin.raw: uname -a 87 | register: result 88 | changed_when: false 89 | 90 | - name: Display uname info 91 | ansible.builtin.debug: 92 | msg: "{{ result.stdout }}" 93 | -------------------------------------------------------------------------------- /roles/proxy/README.md: -------------------------------------------------------------------------------- 1 | # proxy 2 | 3 | This role prepares, installs and configures [Uyuni](https://uyuni-project.org) and [SUSE Multi-Linux Manager](https://www.suse.com/products/multi-linux-manager/) proxy server. 4 | 5 | ## Requirements 6 | 7 | Make sure to install the `jmespath` and `xml` Python modules. 8 | 9 | The system needs access to the internet. Also, you will need one of the following distributions: 10 | 11 | | Product | Distributions | 12 | | ------- | ------------- | 13 | | Uyuni | openSUSE Tumbleweed, Leap 15.x, Leap Micro 6.x | 14 | | Multi-Linux Manager | SL Micro 5.5, SLES 15 SP7 | 15 | 16 | ## Role Variables 17 | 18 | | Variable | Default | Description | 19 | | -------- | ------- | ----------- | 20 | | `proxy_disk` | - | Dedicated disk for volume | 21 | | `proxy_config_file` | - | Proxy configuration tarball (**required**) | 22 | | `proxy_uyuni_release`, `proxy_suma_release` | - | Specific release | 23 | | `proxy_scc_url` | `https://scc.suse.com` | [SUSE Customer Center](https://scc.suse.com) URL to use (*may be different for some hyperscalers*) | 24 | | `proxy_scc_reg_code_os` | - | [SUSE Customer Center](https://scc.suse.com) registration code for the OS (optional) | 25 | | `proxy_scc_reg_code_mlm` | - | [SUSE Customer Center](https://scc.suse.com) registration code (*received after trial registration or purchase*) | 26 | | `proxy_scc_mail` | - | SUSE Customer Center mail address | 27 | | `proxy_scc_check_registration` | `true` | Register system if unregistered | 28 | | `proxy_scc_check_modules` | `true` | Activate required modules if not already enabled | 29 | 30 | ## Dependencies 31 | 32 | No dependencies. 33 | 34 | ## Example Playbook 35 | 36 | Refer to the following example: 37 | 38 | ```yaml 39 | --- 40 | - hosts: prawwxy.giertz.loc 41 | roles: 42 | - role: stdevel.uyuni.proxy 43 | proxy_config_file: myproxy.tar.gz 44 | ``` 45 | 46 | Use a dedicated disk for the proxy cache: 47 | 48 | ```yaml 49 | --- 50 | - hosts: darmstadt.hessen.loc 51 | roles: 52 | - role: stdevel.uyuni.proxy 53 | proxy_config_file: eigude.tar.gz 54 | proxy_disk: /dev/sdb 55 | proxy_uyuni_release: '2024.12' 56 | ``` 57 | 58 | Set SCC-related variables when installing a MLM proxy: 59 | 60 | ```yaml 61 | - hosts: enterprise.lega.cy 62 | roles: 63 | - role: stdevel.uyuni.proxy 64 | proxy_config_file: lvdg.mybiz.loc 65 | proxy_scc_reg_code_os: DERP1337LULZ 66 | proxy_scc_reg_code_mlm: RFL0815CPTR 67 | ``` 68 | 69 | ## Development 70 | 71 | You'll need an customized openSUSE Tumbleweed Podman container (with systemd and other utilities) for testing Uyuni: 72 | 73 | ```command 74 | $ podman build -t opensuse-tumbleweed-uyuni -f Containerfile.tumbleweed 75 | ``` 76 | 77 | Use `molecule` for running the code: 78 | 79 | ```command 80 | $ molecule create [--scenario-name mlm] 81 | $ molecule converge [--scenario-name mlm] 82 | $ molecule verify [--scenario-name mlm] 83 | ``` 84 | 85 | SUSE Multi-Linux Manager requires a dedicated container image: 86 | 87 | ```command 88 | $ podman build -t sles-157-mlm -f Containerfile.sles 89 | ``` 90 | 91 | ## License 92 | 93 | GPL 3.0 94 | 95 | ## Author information 96 | 97 | Christian Stankowic (info@cstan.io) 98 | -------------------------------------------------------------------------------- /plugins/module_utils/helper_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Uyuni helper functions 3 | """ 4 | 5 | from __future__ import (absolute_import, division, print_function) 6 | import logging 7 | from .uyuni import UyuniAPIClient 8 | from .exceptions import SSLCertVerificationError 9 | __metaclass__ = type 10 | 11 | 12 | def get_host_id(target, api_client): 13 | """ 14 | Ensures that a host ID is returned 15 | """ 16 | if isinstance(target, int) or target.isdigit(): 17 | return int(target) 18 | return api_client.get_host_id(target) 19 | 20 | 21 | def get_patch_id(patch, api_client): 22 | """ 23 | Ensure that a patch ID is returned 24 | """ 25 | if isinstance(patch, int) or patch.isdigit(): 26 | return int(patch) 27 | return api_client.get_patch_by_name(patch) 28 | 29 | 30 | def patch_already_installed(system_id, patches, api_client): 31 | """ 32 | Checks whether specific patches are already installed 33 | """ 34 | # get recently installed patches 35 | _installed = [x['id'] for x in get_recently_installed_patches(system_id, api_client)] 36 | # check if patches aren't installed 37 | for patch in patches: 38 | if patch not in _installed: 39 | return False 40 | return True 41 | 42 | 43 | def get_recently_installed_patches(system_id, api_client): 44 | """ 45 | Get all recently installed patches 46 | """ 47 | # find already installed errata by searching actions 48 | actions = api_client.get_host_actions( 49 | system_id 50 | ) 51 | errata = [ 52 | x["additional_info"][0]["detail"].split(' ', 1)[0] for x in actions 53 | if ("name" in x 54 | and "patch update" in x["name"].lower() 55 | and x["successful_count"] == 1) 56 | ] 57 | # return errata IDs 58 | return [api_client.get_patch_by_name(x) for x in errata] 59 | 60 | 61 | def is_blocklisted(upgrade: str, blacklist: list): 62 | """ 63 | This function checks whether a patch is matched by an exclude pattern 64 | 65 | :param upgrade: Hostname 66 | :type upgrade: str 67 | :param blacklist: List of blacklisted terms 68 | :type blacklist: [str, ] 69 | """ 70 | return any(entry in upgrade for entry in blacklist) 71 | 72 | 73 | def _configure_connection(connection_params): 74 | """ 75 | Configures API connection 76 | """ 77 | # try to create API instance 78 | try: 79 | api_instance = UyuniAPIClient( 80 | logging.ERROR, 81 | connection_params.get('host'), 82 | connection_params.get('username'), 83 | connection_params.get('password'), 84 | port=connection_params.get('port'), 85 | verify=connection_params.get('verify_ssl') 86 | ) 87 | return api_instance 88 | except SSLCertVerificationError as err: 89 | raise BaseException("Failed to verify SSL certificate") from err 90 | except Exception as err: 91 | raise BaseException(f"Failed to create API connection: {err}") from err 92 | return api_instance 93 | 94 | 95 | def get_outdated_pkgs(target, api_client): 96 | """ 97 | Ensures that a number of outdated packages is returned 98 | """ 99 | if isinstance(target, int) or target.isdigit(): 100 | return int(target) 101 | return api_client.get_outdated_pkgs(target) 102 | -------------------------------------------------------------------------------- /plugins/modules/reboot_host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for rebooting a managed host 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: reboot_host 31 | short_description: Reboot a managed host 32 | description: 33 | - Reboot a managed host 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | ''' 44 | 45 | EXAMPLES = ''' 46 | - name: Reboot host 47 | stdevel.uyuni.reboot_host: 48 | uyuni_host: 192.168.1.1 49 | uyuni_user: admin 50 | uyuni_password: admin 51 | name: server.localdomain.loc 52 | ''' 53 | 54 | RETURN = ''' 55 | entity: 56 | description: State whether reboot was scheduled successfully 57 | returned: success 58 | type: bool 59 | ''' 60 | 61 | from ansible.module_utils.basic import AnsibleModule 62 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 63 | from ..module_utils.helper_functions import _configure_connection, get_host_id 64 | 65 | 66 | def _reboot_host(module, api_instance): 67 | """ 68 | Reboots the host 69 | """ 70 | try: 71 | action_id = api_instance.reboot_host( 72 | get_host_id( 73 | module.params.get('name'), 74 | api_instance 75 | ) 76 | ) 77 | module.exit_json(changed=True, action_id=action_id) 78 | except SSLCertVerificationError: 79 | module.fail_json(msg="Failed to verify SSL certificate") 80 | except EmptySetException as err: 81 | module.fail_json(msg=f"Exception when calling UyuniAPI->reboot_host: {err}") 82 | 83 | 84 | def main(): 85 | argument_spec = dict( 86 | uyuni_host=dict(required=True), 87 | uyuni_user=dict(required=True), 88 | uyuni_password=dict(required=True, no_log=True), 89 | uyuni_port=dict(default=443, type='int'), 90 | uyuni_verify_ssl=dict(default=True, type='bool'), 91 | name=dict(required=True) 92 | ) 93 | 94 | module = AnsibleModule(argument_spec=argument_spec) 95 | 96 | connection_params = dict( 97 | host=module.params.get('uyuni_host'), 98 | username=module.params.get('uyuni_user'), 99 | password=module.params.get('uyuni_password'), 100 | port=module.params.get('uyuni_port'), 101 | verify_ssl=module.params.get('uyuni_verify_ssl') 102 | ) 103 | 104 | api_instance = _configure_connection(connection_params) 105 | _reboot_host(module, api_instance) 106 | 107 | 108 | if __name__ == '__main__': 109 | main() 110 | -------------------------------------------------------------------------------- /plugins/modules/is_reboot_required.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module tp perform full package update on a managed host 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: is_reboot_required 31 | short_description: Check if system requires reboot 32 | description: 33 | - Check if a managed host needs to be rebooted! 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | ''' 44 | 45 | EXAMPLES = ''' 46 | - name: Check if system requires reboot 47 | stdevel.uyuni.is_reboot_required: 48 | uyuni_host: 192.168.1.1 49 | uyuni_user: admin 50 | uyuni_password: admin 51 | name: server.localdomain.loc 52 | ''' 53 | 54 | RETURN = ''' 55 | entity: 56 | description: State whether package host needs reboot or not 57 | returned: success 58 | type: bool 59 | ''' 60 | 61 | from ansible.module_utils.basic import AnsibleModule 62 | from ..module_utils.exceptions import SSLCertVerificationError 63 | from ..module_utils.helper_functions import _configure_connection, get_host_id 64 | 65 | 66 | def _is_reboot_required(module, api_instance): 67 | """ 68 | Check if host requires a reboot 69 | """ 70 | try: 71 | # is reboot required 72 | reboot_required = api_instance.is_reboot_required( 73 | get_host_id( 74 | module.params.get('name'), 75 | api_instance 76 | ) 77 | ) 78 | if reboot_required is True: 79 | module.exit_json(changed=True, reboot_required=reboot_required) 80 | if reboot_required is False: 81 | module.exit_json(changed=False, reboot_required=reboot_required) 82 | except SSLCertVerificationError: 83 | module.fail_json(msg="Failed to verify SSL certificate") 84 | 85 | 86 | def main(): 87 | """ 88 | Main functions 89 | """ 90 | argument_spec = dict( 91 | uyuni_host=dict(required=True), 92 | uyuni_user=dict(required=True), 93 | uyuni_password=dict(required=True, no_log=True), 94 | uyuni_port=dict(default=443, type='int'), 95 | uyuni_verify_ssl=dict(default=True, type='bool'), 96 | name=dict(required=True) 97 | ) 98 | 99 | module = AnsibleModule(argument_spec=argument_spec) 100 | 101 | connection_params = dict( 102 | host=module.params.get('uyuni_host'), 103 | username=module.params.get('uyuni_user'), 104 | password=module.params.get('uyuni_password'), 105 | port=module.params.get('uyuni_port'), 106 | verify_ssl=module.params.get('uyuni_verify_ssl') 107 | ) 108 | 109 | api_instance = _configure_connection(connection_params) 110 | _is_reboot_required(module, api_instance) 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /roles/server/molecule/default/tests/test_default.py: -------------------------------------------------------------------------------- 1 | """ 2 | Molecule unit tests 3 | """ 4 | 5 | import os 6 | import configparser 7 | import testinfra.utils.ansible_runner 8 | 9 | TESTINFRA_HOSTS = testinfra.utils.ansible_runner.AnsibleRunner( 10 | os.environ["MOLECULE_INVENTORY_FILE"] 11 | ).get_hosts("all") 12 | 13 | 14 | def test_packages(host): 15 | """ 16 | check if packages are installed 17 | """ 18 | # get variables from file 19 | ansible_vars = host.ansible("include_vars", "file=main.yml") 20 | # check dependencies and Uyuni packages 21 | for pkg in ansible_vars["ansible_facts"]["server_pkgs"]: 22 | assert host.package(pkg).is_installed 23 | 24 | 25 | def test_setup_complete(host): 26 | """ 27 | check if installation files exist 28 | """ 29 | with host.sudo(): 30 | for state_file in [ 31 | "/var/lib/containers/storage/volumes/var-pgsql/_data", 32 | "/root/.MANAGER_INITIALIZATION_COMPLETE", 33 | ]: 34 | assert host.file(state_file).exists 35 | 36 | 37 | def test_ports_listen(host): 38 | """ 39 | check if ports are listening 40 | """ 41 | for port in [80, 443, 4505, 4506]: 42 | assert host.socket("tcp://0.0.0.0:%s" % port).is_listening 43 | 44 | 45 | def test_org(host): 46 | """ 47 | check if organization is accessible 48 | """ 49 | # get variables from file 50 | ansible_vars = host.ansible("include_vars", "file=main.yml") 51 | # check if organization exists 52 | cmd_org = host.run( 53 | "mgrctl exec 'spacecmd -q -u %s -p %s org_list'", 54 | ansible_vars["ansible_facts"]["server_org_login"], 55 | ansible_vars["ansible_facts"]["server_org_password"], 56 | ) 57 | assert ( 58 | cmd_org.stdout.strip() == ansible_vars["ansible_facts"]["server_org_name"] 59 | ) # noqa: 204 60 | 61 | 62 | def test_channels(host): 63 | """ 64 | check if supplied channels were created 65 | """ 66 | # get variables from file 67 | ansible_vars = host.ansible("include_vars", "file=main.yml") 68 | # check channels if defined 69 | if ( 70 | "server_channels" in ansible_vars["ansible_facts"] 71 | and len(ansible_vars["ansible_facts"]["server_channels"]) > 0 72 | ): 73 | # get spacewalk-common-channels definitions from client 74 | with host.sudo(): 75 | definition_file = host.file( 76 | "/var/lib/containers/storage/volumes/etc-rhn/_data/spacewalk-common-channels.ini" 77 | ).content_string 78 | definitions = configparser.RawConfigParser(allow_no_value=True) 79 | definitions.read_string(definition_file) 80 | 81 | # get all repositories 82 | with host.sudo(): 83 | cmd_channels = host.run( 84 | "mgrctl exec 'spacecmd -q -u %s -p %s repo_list'", 85 | ansible_vars["ansible_facts"]["server_org_login"], 86 | ansible_vars["ansible_facts"]["server_org_password"], 87 | ) 88 | for channel in ansible_vars["ansible_facts"]["server_channels"]: 89 | # get repository name (it ain't nice, but it's honest work) 90 | repo_name = definitions[channel["name"]]["name"] 91 | repo_name = "External - %s" % repo_name.replace("%(arch)s", channel["arch"]) 92 | # ensure that repository exists 93 | assert repo_name in cmd_channels.stdout.strip().split("\n") 94 | 95 | 96 | def test_monitoring_enabled(host): 97 | """ 98 | check if monitoring is enabled 99 | """ 100 | # get variables from file 101 | ansible_vars = host.ansible("include_vars", "file=main.yml") 102 | # check configuration 103 | if ansible_vars["ansible_facts"]["server_enable_monitoring"]: 104 | with host.sudo(): 105 | rhn_cfg = host.file( 106 | "/var/lib/containers/storage/volumes/etc-rhn/_data/rhn.conf" 107 | ) 108 | assert rhn_cfg.contains("prometheus_monitoring_enabled") 109 | # check status 110 | with host.sudo(): 111 | mon_status = host.run("mgrctl exec 'mgr-monitoring-ctl status'") 112 | assert "error" not in mon_status.stderr.strip().lower() 113 | -------------------------------------------------------------------------------- /plugins/modules/apply_highstate.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for applying a host's highstate 4 | 5 | 2025 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: apply_highstate 31 | short_description: Apply a host's highstate 32 | description: 33 | - Apply a host's highstate 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | test_mode: 44 | description: Only simulate applying the highstate 45 | required: False 46 | type: bool 47 | default: False 48 | ''' 49 | 50 | EXAMPLES = ''' 51 | - name: Apply highstate 52 | stdevel.uyuni.apply_highstate: 53 | uyuni_host: 192.168.1.1 54 | uyuni_user: admin 55 | uyuni_password: admin 56 | name: server.localdomain.loc 57 | 58 | - name: Simulate applying highstate 59 | stdevel.uyuni.apply_highstate: 60 | uyuni_host: 192.168.1.1 61 | uyuni_user: admin 62 | uyuni_password: admin 63 | name: server.localdomain.loc 64 | test_mode: true 65 | ''' 66 | 67 | RETURN = ''' 68 | entity: 69 | description: State whether highstate was scheduled successfully 70 | returned: success 71 | type: bool 72 | ''' 73 | 74 | from ansible.module_utils.basic import AnsibleModule 75 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 76 | from ..module_utils.helper_functions import _configure_connection, get_host_id 77 | 78 | 79 | def _apply_highstate(module, api_instance): 80 | """ 81 | Applies a host's highstate 82 | """ 83 | try: 84 | action_id = api_instance.apply_highstate( 85 | get_host_id( 86 | module.params.get('name'), 87 | api_instance 88 | ), 89 | module.params.get('test_mode') 90 | ) 91 | module.exit_json(changed=True, action_id=action_id) 92 | except SSLCertVerificationError: 93 | module.fail_json(msg="Failed to verify SSL certificate") 94 | except EmptySetException as err: 95 | module.fail_json(msg=f"Exception when calling UyuniAPI->apply_highstate: {err}") 96 | 97 | 98 | def main(): 99 | """ 100 | Default function, calls module 101 | """ 102 | argument_spec = dict( 103 | uyuni_host=dict(required=True), 104 | uyuni_user=dict(required=True), 105 | uyuni_password=dict(required=True, no_log=True), 106 | uyuni_port=dict(default=443, type='int'), 107 | uyuni_verify_ssl=dict(default=True, type='bool'), 108 | name=dict(required=True), 109 | test_mode=dict(default=False, type='bool') 110 | ) 111 | 112 | module = AnsibleModule(argument_spec=argument_spec) 113 | 114 | connection_params = dict( 115 | host=module.params.get('uyuni_host'), 116 | username=module.params.get('uyuni_user'), 117 | password=module.params.get('uyuni_password'), 118 | port=module.params.get('uyuni_port'), 119 | verify_ssl=module.params.get('uyuni_verify_ssl') 120 | ) 121 | 122 | api_instance = _configure_connection(connection_params) 123 | _apply_highstate(module, api_instance) 124 | 125 | 126 | if __name__ == '__main__': 127 | main() 128 | -------------------------------------------------------------------------------- /plugins/modules/openscap_run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for scheduling OpenSCAP runs 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: openscap_run 31 | short_description: Schedule OpenSCAP runs 32 | description: 33 | - Schedule OpenSCAP runs 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | document: 44 | description: XCCDF document path 45 | required: True 46 | type: str 47 | arguments: 48 | description: Command-line arguments 49 | type: str 50 | ''' 51 | 52 | EXAMPLES = ''' 53 | - name: Check compliance 54 | stdevel.uyuni.openscap_run: 55 | uyuni_host: 192.168.1.1 56 | uyuni_user: admin 57 | uyuni_password: admin 58 | document: /usr/share/openscap/scap-yast2sec-xccdf.xml 59 | arguments: --profile Default 60 | ''' 61 | 62 | RETURN = ''' 63 | entity: 64 | description: State whether project was scheduled successfully 65 | returned: success 66 | type: bool 67 | ''' 68 | 69 | from ansible.module_utils.basic import AnsibleModule 70 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 71 | from ..module_utils.helper_functions import _configure_connection, get_host_id 72 | 73 | 74 | def _schedule_openscap_run(module, api_instance): 75 | """ 76 | Schedules an OpenSCAP run 77 | """ 78 | try: 79 | # module.fail_json(msg="About to schedule OpenSCAP run") 80 | action_id = api_instance.schedule_openscap_run( 81 | get_host_id( 82 | module.params.get('name'), 83 | api_instance 84 | ), 85 | module.params.get('document'), 86 | module.params.get('arguments') 87 | ) 88 | module.exit_json(changed=True, action_id=action_id) 89 | except SSLCertVerificationError: 90 | module.fail_json(msg="Failed to verify SSL certificate") 91 | except EmptySetException as err: 92 | module.fail_json(msg=f"Exception when calling UyuniAPI->schedule_openscap_run: {err}") 93 | 94 | 95 | def main(): 96 | """ 97 | Main function 98 | """ 99 | argument_spec = dict( 100 | uyuni_host=dict(required=True), 101 | uyuni_user=dict(required=True), 102 | uyuni_password=dict(required=True, no_log=True), 103 | uyuni_port=dict(default=443, type='int'), 104 | uyuni_verify_ssl=dict(default=True, type='bool'), 105 | name=dict(required=True), 106 | document=dict(type='str', required=True), 107 | arguments=dict(type='str') 108 | ) 109 | 110 | module = AnsibleModule(argument_spec=argument_spec) 111 | 112 | connection_params = dict( 113 | host=module.params.get('uyuni_host'), 114 | username=module.params.get('uyuni_user'), 115 | password=module.params.get('uyuni_password'), 116 | port=module.params.get('uyuni_port'), 117 | verify_ssl=module.params.get('uyuni_verify_ssl') 118 | ) 119 | 120 | api_instance = _configure_connection(connection_params) 121 | _schedule_openscap_run(module, api_instance) 122 | 123 | 124 | if __name__ == '__main__': 125 | main() 126 | -------------------------------------------------------------------------------- /plugins/modules/apply_states.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for applying states for a host 4 | 5 | 2025 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: apply_states 31 | short_description: Apply states for a host 32 | description: 33 | - Apply states for a host 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | states: 44 | description: Name of states to apply 45 | required: True 46 | type: list 47 | elements: str 48 | test_mode: 49 | description: Only simulate applying the states 50 | required: False 51 | type: bool 52 | default: False 53 | ''' 54 | 55 | EXAMPLES = ''' 56 | - name: Apply states 57 | stdevel.uyuni.apply_states: 58 | uyuni_host: 192.168.1.1 59 | uyuni_user: admin 60 | uyuni_password: admin 61 | name: server.localdomain.loc 62 | states: 63 | - backup-agent 64 | - monitoring-agent 65 | - lolcat 66 | 67 | - name: Simulate applying states 68 | stdevel.uyuni.apply_states: 69 | uyuni_host: 192.168.1.1 70 | uyuni_user: admin 71 | uyuni_password: admin 72 | name: server.localdomain.loc 73 | states: 74 | - backup-agent 75 | - monitoring-agent 76 | test_mode: true 77 | ''' 78 | 79 | RETURN = ''' 80 | entity: 81 | description: State whether states were scheduled successfully 82 | returned: success 83 | type: bool 84 | ''' 85 | 86 | from ansible.module_utils.basic import AnsibleModule 87 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 88 | from ..module_utils.helper_functions import _configure_connection, get_host_id 89 | 90 | 91 | def _apply_states(module, api_instance): 92 | """ 93 | Apply states for a host 94 | """ 95 | try: 96 | action_id = api_instance.apply_states( 97 | get_host_id( 98 | module.params.get('name'), 99 | api_instance 100 | ), 101 | module.params.get('states'), 102 | module.params.get('test_mode') 103 | ) 104 | module.exit_json(changed=True, action_id=action_id) 105 | except SSLCertVerificationError: 106 | module.fail_json(msg="Failed to verify SSL certificate") 107 | except EmptySetException as err: 108 | module.fail_json(msg=f"Exception when calling UyuniAPI->apply_states: {err}") 109 | 110 | 111 | def main(): 112 | """ 113 | Default function, calls module 114 | """ 115 | argument_spec = dict( 116 | uyuni_host=dict(required=True), 117 | uyuni_user=dict(required=True), 118 | uyuni_password=dict(required=True, no_log=True), 119 | uyuni_port=dict(default=443, type='int'), 120 | uyuni_verify_ssl=dict(default=True, type='bool'), 121 | name=dict(required=True), 122 | states=dict(required=True, type='list', elements='str'), 123 | test_mode=dict(default=False, type='bool') 124 | ) 125 | 126 | module = AnsibleModule(argument_spec=argument_spec) 127 | 128 | connection_params = dict( 129 | host=module.params.get('uyuni_host'), 130 | username=module.params.get('uyuni_user'), 131 | password=module.params.get('uyuni_password'), 132 | port=module.params.get('uyuni_port'), 133 | verify_ssl=module.params.get('uyuni_verify_ssl') 134 | ) 135 | 136 | api_instance = _configure_connection(connection_params) 137 | _apply_states(module, api_instance) 138 | 139 | 140 | if __name__ == '__main__': 141 | main() 142 | -------------------------------------------------------------------------------- /plugins/modules/full_pkg_update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module to perform a full synchronous package update on a managed host 4 | 5 | 2025 Luca Kinzel 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: full_pkg_update 31 | short_description: Perform full package update 32 | description: 33 | - Perform full synchronous package update on a managed host 34 | author: 35 | - "Luca Kinzel (@KinzelL)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | ''' 44 | 45 | EXAMPLES = ''' 46 | - name: Perform full package update 47 | stdevel.uyuni.full_pkg_update: 48 | uyuni_host: 192.168.1.1 49 | uyuni_user: admin 50 | uyuni_password: admin 51 | name: server.localdomain.loc 52 | ''' 53 | 54 | RETURN = ''' 55 | entity: 56 | description: State whether package installation was scheduled successfully 57 | returned: success 58 | type: bool 59 | ''' 60 | 61 | from ansible.module_utils.basic import AnsibleModule 62 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 63 | from ..module_utils.helper_functions import _configure_connection, get_host_id, get_outdated_pkgs 64 | 65 | 66 | def _full_pkg_update(module, api_instance): 67 | """ 68 | Performs full package update on host 69 | """ 70 | # get host id 71 | host = get_host_id(module.params.get('name'), api_instance) 72 | # is reboot required 73 | reboot_req = api_instance.is_reboot_required(host) 74 | if reboot_req is True: 75 | module.fail_json(msg="Cannot install updates. Host must be rebooted first.") 76 | # get number of outdated packages 77 | upgrades = get_outdated_pkgs(module.params.get('name'), api_instance) 78 | if upgrades == 0: 79 | module.exit_json(changed=False) 80 | try: 81 | # install upgrades 82 | action_id = api_instance.full_pkg_update( 83 | get_host_id( 84 | module.params.get('name'), 85 | api_instance 86 | ) 87 | ) 88 | # wait for all packages to be updated 89 | api_instance.wait_for_action(action_id, host) 90 | module.exit_json(changed=True, installed_updates=upgrades) 91 | except EmptySetException as err: 92 | # exit if no upgrades available 93 | if not upgrades: 94 | module.exit_json(changed=False) 95 | # exit if invalid upgrade 96 | module.fail_json(msg=f"Upgrade(s) not found or applicable: {err}") 97 | except SSLCertVerificationError: 98 | module.fail_json(msg="Failed to verify SSL certificate") 99 | 100 | 101 | def main(): 102 | """ 103 | Main functions 104 | """ 105 | argument_spec = dict( 106 | uyuni_host=dict(required=True), 107 | uyuni_user=dict(required=True), 108 | uyuni_password=dict(required=True, no_log=True), 109 | uyuni_port=dict(default=443, type='int'), 110 | uyuni_verify_ssl=dict(default=True, type='bool'), 111 | name=dict(required=True) 112 | ) 113 | 114 | module = AnsibleModule( 115 | argument_spec=argument_spec, 116 | supports_check_mode=False 117 | ) 118 | 119 | module_params = dict( 120 | host=module.params.get('uyuni_host'), 121 | username=module.params.get('uyuni_user'), 122 | password=module.params.get('uyuni_password'), 123 | port=module.params.get('uyuni_port'), 124 | verify_ssl=module.params.get('uyuni_verify_ssl') 125 | ) 126 | 127 | api_instance = _configure_connection(module_params) 128 | _full_pkg_update(module, api_instance) 129 | 130 | 131 | if __name__ == '__main__': 132 | main() 133 | -------------------------------------------------------------------------------- /plugins/modules/install_patches.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for installing patches on a managed host 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: install_patches 31 | short_description: Install patches 32 | description: 33 | - Install patches on a managed host 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | include_patches: 44 | description: List of patch names or IDs to install 45 | type: list 46 | elements: str 47 | exclude_patches: 48 | description: List of patch names or IDs to exclude from installation 49 | type: list 50 | elements: str 51 | ''' 52 | 53 | EXAMPLES = ''' 54 | - name: Install patches 55 | stdevel.uyuni.install_patches: 56 | uyuni_host: 192.168.1.1 57 | uyuni_user: admin 58 | uyuni_password: admin 59 | name: server.localdomain.loc 60 | exclude_patches: 61 | - openSUSE-2022-10013 62 | - openSUSE-SLE-15.3-2022-2118 63 | ''' 64 | 65 | RETURN = ''' 66 | entity: 67 | description: State whether patch installation was scheduled successfully 68 | returned: success 69 | type: bool 70 | ''' 71 | 72 | from ansible.module_utils.basic import AnsibleModule 73 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 74 | from ..module_utils.helper_functions import _configure_connection, get_host_id, get_patch_id, patch_already_installed 75 | 76 | 77 | def _install_patches(module, api_instance): 78 | """ 79 | Installs patches on the host 80 | """ 81 | # get parameters 82 | host = get_host_id(module.params.get('name'), api_instance) 83 | try: 84 | include_patches = [get_patch_id(x, api_instance)["id"] for x in module.params.get('include_patches')] 85 | except (UnboundLocalError, TypeError): 86 | include_patches = None 87 | except EmptySetException: 88 | module.fail_json(msg="Patch not found or applicable") 89 | 90 | try: 91 | exclude_patches = [get_patch_id(x, api_instance)["id"] for x in module.params.get('exclude_patches')] 92 | except (UnboundLocalError, TypeError): 93 | exclude_patches = None 94 | except EmptySetException: 95 | module.fail_json(msg="Patch not found or applicable") 96 | 97 | try: 98 | # get _all_ the patches 99 | all_patches = api_instance.get_host_patches(host) 100 | # exclude or include patches if defined 101 | if exclude_patches: 102 | patches = [x["id"] for x in all_patches if x["id"] not in exclude_patches] 103 | elif include_patches: 104 | patches = [x["id"] for x in all_patches if x["id"] in include_patches] 105 | else: 106 | patches = [x["id"] for x in all_patches] 107 | 108 | # install patches 109 | action_id = api_instance.install_patches( 110 | get_host_id( 111 | module.params.get('name'), 112 | api_instance 113 | ), 114 | patches 115 | ) 116 | module.exit_json(changed=True, action_id=action_id) 117 | except EmptySetException: 118 | # check if already installed 119 | if patch_already_installed( 120 | get_host_id( 121 | module.params.get('name'), 122 | api_instance 123 | ), 124 | patches, 125 | api_instance 126 | ): 127 | module.exit_json(changed=False) 128 | module.fail_json(msg="Patch(es) not found or applicable") 129 | except SSLCertVerificationError: 130 | module.fail_json(msg="Failed to verify SSL certificate") 131 | 132 | 133 | def main(): 134 | """ 135 | Main function 136 | """ 137 | argument_spec = dict( 138 | uyuni_host=dict(required=True), 139 | uyuni_user=dict(required=True), 140 | uyuni_password=dict(required=True, no_log=True), 141 | uyuni_port=dict(default=443, type='int'), 142 | uyuni_verify_ssl=dict(default=True, type='bool'), 143 | name=dict(required=True), 144 | include_patches=dict(type='list', elements='str', required=False), 145 | exclude_patches=dict(type='list', elements='str', required=False) 146 | ) 147 | 148 | module = AnsibleModule( 149 | argument_spec=argument_spec, 150 | mutually_exclusive=[('include_patches', 'exclude_patches')], 151 | supports_check_mode=False 152 | ) 153 | 154 | module_params = dict( 155 | host=module.params.get('uyuni_host'), 156 | username=module.params.get('uyuni_user'), 157 | password=module.params.get('uyuni_password'), 158 | port=module.params.get('uyuni_port'), 159 | verify_ssl=module.params.get('uyuni_verify_ssl'), 160 | include_patches=module.params.get('include_patches'), 161 | exclude_patches=module.params.get('exclude_patches') 162 | ) 163 | 164 | api_instance = _configure_connection(module_params) 165 | _install_patches(module, api_instance) 166 | 167 | 168 | if __name__ == '__main__': 169 | main() 170 | -------------------------------------------------------------------------------- /roles/server/README.md: -------------------------------------------------------------------------------- 1 | # server 2 | 3 | This role prepares, installs and configures [Uyuni](https://uyuni-project.org) and [SUSE Multi-Linux Manager](https://www.suse.com/products/multi-linux-manager/). 4 | 5 | ## Requirements 6 | 7 | Make sure to install the `jmespath` and `xml` Python modules. 8 | 9 | The system needs access to the internet. Also, you will need one of the following distributions: 10 | 11 | | Product | Distributions | 12 | | ------- | ------------- | 13 | | Uyuni | openSUSE Tumbleweed, Leap 15.x, Leap Micro 6.x | 14 | | SUSE Manager 5.0 | SLE Micro 5.5, SLES 15 SP6 | 15 | | SUSE Multi-Linux Manager 5.1 | SL Micro 5.5, SLES 15 SP7 | 16 | 17 | ## Role Variables 18 | 19 | | Variable | Default | Description | 20 | | -------- | ------- | ----------- | 21 | | `server_check_requirements` | `true` | Check for hardware requirements | 22 | | `server_suma_release` | `5.0` | SUSE Multi-Linux Manager release to install | 23 | | `server_disk_volumes` | - | Dedicated disk for container volumes | 24 | | `server_disk_database` | - | Dedicated disk for database container volume | 25 | | `server_suma_airgapped` | `false` | Whether to get container image from RPM instead of online registry | 26 | | `server_release` | *empty* | Uyuni release to install (*e.g. `2024.12`*) | 27 | | `server_scc_url` | `https://scc.suse.com` | [SUSE Customer Center](https://scc.suse.com) URL to use (*may be different for some hyperscalers*) | 28 | | `server_scc_reg_code_os` | - | [SUSE Customer Center](https://scc.suse.com) registration code for the OS (optional) | 29 | | `server_scc_reg_code_mlm` | - | [SUSE Customer Center](https://scc.suse.com) registration code (*received after trial registration or purchase*) | 30 | | `server_scc_mail` | - | SUSE Customer Center mail address | 31 | | `server_scc_check_registration` | `true` | Register system if unregistered | 32 | | `server_scc_check_modules` | `true` | Activate required modules if not already enabled | 33 | | `server_slm_modules` | (*Modules required for SUSE Multi-Linux Manager 5.x*) | Modules to enable before installation | 34 | | `server_mail` | `root@localhost` | Web server administrator mail | 35 | | `server_cert_city` | `Darmstadt` | Certificate city | 36 | | `server_cert_country` | `DE` | Certificate country | 37 | | `server_cert_mail` | `root@localhost` | Certificate mail | 38 | | `server_cert_o` | `Darmstadt` | Certificate organization | 39 | | `server_cert_ou` | `Darmstadt` | Certificate organization unit | 40 | | `server_cert_state` | `Hessen` | Certificate state | 41 | | `server_cert_pass` | `uyuni` | Certificate password | 42 | | `server_org_name` | `Demo` | Organization name | 43 | | `server_org_login` | `admin` | Organization administrator username | 44 | | `server_org_password` | `admin` | Organization administrator password | 45 | | `server_org_mail` | `root@localhost` | Organization administrator mail | 46 | | `server_org_first_name`| `Anton` | Organization administrator first name | 47 | | `server_org_last_name`| `Administrator` | Organization administrator last name | 48 | | `server_channels`| *empty* | Common channels to synchronize (*e.g. `almalinux9` and `epel9`*) | 49 | | `server_enable_monitoring` | `false` | Flag whether integrated monitoring stack should be enabled | 50 | | `server_fqdn` | - | Set custom FQDN if `ansible_fqdn` doesn't work for you | 51 | 52 | When supplying channels to create in `channels`, ensure passing a list with dicts like this: 53 | 54 | ```yaml 55 | - name: almalinux9 56 | arch: x86_64 57 | - name: almalinux9-appstream 58 | arch: x86_64 59 | - name: almalinux9-uyuni-client 60 | arch: x86_64 61 | ``` 62 | 63 | For available channels and architectures, see the `spacewalk-common-channels.ini` installed by the `spacewalk-utils` package. There is also [an online version](https://github.com/uyuni-project/uyuni/blob/master/utils/spacewalk-common-channels.ini) on GitHub. 64 | 65 | ## Dependencies 66 | 67 | No dependencies. 68 | 69 | ## Example Playbook 70 | 71 | Refer to the following example: 72 | 73 | ```yaml 74 | - hosts: servers 75 | roles: 76 | - stdevel.uyuni.server 77 | ``` 78 | 79 | Set variables if required, e.g.: 80 | 81 | ```yaml 82 | --- 83 | - hosts: uyuni.giertz.loc 84 | remote_user: root 85 | roles: 86 | - role: stdevel.uyuni.server 87 | server_channels: 88 | - name: almalinux9 89 | arch: x86_64 90 | - name: almalinux9-appstream 91 | arch: x86_64 92 | - name: almalinux9-uyuni-client 93 | arch: x86_64 94 | ``` 95 | 96 | Don't forget setting SUSE-related variables when deploying SUSE Multi-Linux Manager: 97 | 98 | ```yaml 99 | - hosts: servers 100 | roles: 101 | - role: stdevel.uyuni.server 102 | server_scc_reg_code: 103 | - DERP1337LULZ 104 | server_scc_mail: bla@foo.bar 105 | ``` 106 | 107 | Installing Multi-Linux Manager on SLES requires an additional registration code as the MLM subscription only includes SL Micro: 108 | 109 | ```yaml 110 | - hosts: servers 111 | roles: 112 | - role: stdevel.uyuni.server 113 | server_scc_reg_code_os: DERP1337LULZ 114 | server_scc_reg_code_mlm: RFL0815CPTR 115 | server_scc_mail: meh@foo.baz 116 | ``` 117 | 118 | If you plan to bootstrap older Uyuni versions, set the Uyuni release: 119 | 120 | ```yaml 121 | --- 122 | - hosts: retro.giertz.loc 123 | remote_user: root 124 | roles: 125 | - role: stdevel.uyuni.server 126 | server_release: '2024.07' 127 | ``` 128 | 129 | ## Development 130 | 131 | You'll need an customized openSUSE Tumbleweed Podman container (with systemd and other utilities) for testing Uyuni: 132 | 133 | ```command 134 | $ podman build -t opensuse-tumbleweed-uyuni -f Containerfile.tumbleweed 135 | ``` 136 | 137 | Use `molecule` for running the code: 138 | 139 | ```command 140 | $ molecule create [--scenario-name mlm] 141 | $ molecule converge [--scenario-name mlm] 142 | $ molecule verify [--scenario-name mlm] 143 | ``` 144 | 145 | SUSE Multi-Linux Manager requires a dedicated container image: 146 | 147 | ```command 148 | $ podman build -t sles-157-mlm -f Containerfile.sles 149 | ``` 150 | 151 | ## License 152 | 153 | GPL 3.0 154 | 155 | ## Author information 156 | 157 | Christian Stankowic 158 | -------------------------------------------------------------------------------- /plugins/modules/install_upgrades.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Ansible Module for installing upgrades on a managed host 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | ANSIBLE_METADATA = {'metadata_version': '1.1', 25 | 'status': ['preview'], 26 | 'supported_by': 'community'} 27 | 28 | DOCUMENTATION = ''' 29 | --- 30 | module: install_upgrades 31 | short_description: Install upgrades 32 | description: 33 | - Install upgrades (that aren't part of an patch) on a managed host 34 | author: 35 | - "Christian Stankowic (@stdevel)" 36 | extends_documentation_fragment: 37 | - stdevel.uyuni.uyuni_auth 38 | options: 39 | name: 40 | description: Name or profile ID of the managed host 41 | required: True 42 | type: str 43 | include_upgrades: 44 | description: List of package names to install 45 | type: list 46 | elements: str 47 | exclude_upgrades: 48 | description: List of package names to exclude from installation 49 | type: list 50 | elements: str 51 | ''' 52 | 53 | EXAMPLES = ''' 54 | - name: Install upgrades 55 | stdevel.uyuni.install_upgrades: 56 | uyuni_host: 192.168.1.1 57 | uyuni_user: admin 58 | uyuni_password: admin 59 | name: server.localdomain.loc 60 | exclude_upgrades: 61 | - kernel-default 62 | ''' 63 | 64 | RETURN = ''' 65 | entity: 66 | description: State whether package installation was scheduled successfully 67 | returned: success 68 | type: bool 69 | ''' 70 | 71 | from ansible.module_utils.basic import AnsibleModule 72 | from ..module_utils.exceptions import EmptySetException, SSLCertVerificationError 73 | from ..module_utils.helper_functions import _configure_connection, get_host_id, is_blocklisted 74 | 75 | 76 | def _install_upgrades(module, api_instance): 77 | """ 78 | Installs upgrades on the host 79 | """ 80 | # get parameters 81 | host = get_host_id(module.params.get('name'), api_instance) 82 | include_upgrades = module.params.get('include_upgrades') 83 | exclude_upgrades = module.params.get('exclude_upgrades') 84 | 85 | upgrades = [] 86 | try: 87 | # get _all_ the upgrades 88 | all_upgrades = api_instance.get_host_upgrades(host) 89 | 90 | # exclude or include upgrades if defined 91 | if exclude_upgrades: 92 | for upgrade in all_upgrades: 93 | if not is_blocklisted(upgrade["name"], exclude_upgrades): 94 | try: 95 | upgrades.append(upgrade["package_id"]) 96 | except KeyError: 97 | upgrades.append(upgrade["to_package_id"]) 98 | 99 | elif include_upgrades: 100 | for upgrade in all_upgrades: 101 | # ignore the misleading function name here pls 102 | if is_blocklisted(upgrade["name"], include_upgrades): 103 | try: 104 | upgrades.append(upgrade["package_id"]) 105 | except KeyError: 106 | upgrades.append(upgrade["to_package_id"]) 107 | else: 108 | try: 109 | upgrades = [x["package_id"] for x in all_upgrades] 110 | except KeyError: 111 | upgrades = [x["to_package_id"] for x in all_upgrades] 112 | 113 | # install upgrades 114 | action_id = api_instance.install_upgrades( 115 | get_host_id( 116 | module.params.get('name'), 117 | api_instance 118 | ), 119 | upgrades 120 | ) 121 | module.exit_json(changed=True, action_id=action_id) 122 | except EmptySetException as err: 123 | # exit if no upgrades available 124 | if not upgrades: 125 | module.exit_json(changed=False) 126 | # exit if invalid upgrade 127 | module.fail_json(msg=f"Upgrade(s) not found or applicable: {err}") 128 | except SSLCertVerificationError: 129 | module.fail_json(msg="Failed to verify SSL certificate") 130 | 131 | 132 | def main(): 133 | """ 134 | Main functions 135 | """ 136 | argument_spec = dict( 137 | uyuni_host=dict(required=True), 138 | uyuni_user=dict(required=True), 139 | uyuni_password=dict(required=True, no_log=True), 140 | uyuni_port=dict(default=443, type='int'), 141 | uyuni_verify_ssl=dict(default=True, type='bool'), 142 | name=dict(required=True), 143 | include_upgrades=dict(type='list', elements='str', required=False), 144 | exclude_upgrades=dict(type='list', elements='str', required=False) 145 | ) 146 | 147 | module = AnsibleModule( 148 | argument_spec=argument_spec, 149 | mutually_exclusive=[('include_upgrades', 'exclude_upgrades')], 150 | supports_check_mode=False 151 | ) 152 | 153 | module_params = dict( 154 | host=module.params.get('uyuni_host'), 155 | username=module.params.get('uyuni_user'), 156 | password=module.params.get('uyuni_password'), 157 | port=module.params.get('uyuni_port'), 158 | verify_ssl=module.params.get('uyuni_verify_ssl'), 159 | include_upgrades=module.params.get('include_upgrades'), 160 | exclude_upgrades=module.params.get('exclude_upgrades') 161 | ) 162 | 163 | api_instance = _configure_connection(module_params) 164 | _install_upgrades(module, api_instance) 165 | 166 | 167 | if __name__ == '__main__': 168 | main() 169 | -------------------------------------------------------------------------------- /plugins/inventory/inventory.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Ansible inventory module for Uyuni 4 | 5 | 2022 Christian Stankowic 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | """ 20 | 21 | from __future__ import (absolute_import, division, print_function) 22 | __metaclass__ = type 23 | 24 | DOCUMENTATION = ''' 25 | name: inventory 26 | short_description: Uyuni inventory source 27 | author: 28 | - Christian Stankowic (@stdevel) 29 | description: 30 | - Get inventory hosts from the Uyuni API. 31 | - "Uses a configuration file as an inventory source, it must end in 32 | C(.uyuni.yml) or C(.uyuni.yaml)." 33 | options: 34 | plugin: 35 | description: Name of the plugin. 36 | required: true 37 | type: string 38 | choices: ['stdevel.uyuni.inventory'] 39 | host: 40 | description: 41 | - Hostname/IP address of the Uyuni server. 42 | - If the value is not specified in the inventory configuration, the value of environment variable C(UYUNI_HOST) will be used instead. 43 | type: string 44 | required: true 45 | env: 46 | - name: UYUNI_HOST 47 | version_added: 0.2.0 48 | user: 49 | description: 50 | - Username to query the API with. 51 | - If the value is not specified in the inventory configuration, the value of environment variable C(UYUNI_USER) will be used instead. 52 | type: string 53 | required: true 54 | env: 55 | - name: UYUNI_USER 56 | version_added: 0.2.0 57 | port: 58 | description: API port 59 | type: int 60 | default: 443 61 | password: 62 | description: 63 | - Password to query the API with. 64 | - If the value is not specified in the inventory configuration, the value of environment variable C(UYUNI_PASSWORD) will be used instead. 65 | type: string 66 | required: true 67 | env: 68 | - name: UYUNI_PASSWORD 69 | version_added: 0.2.0 70 | verify_ssl: 71 | description: Enables or disables SSL certificate verification. 72 | type: boolean 73 | default: true 74 | only_powered_on: 75 | description: Only shows powered-on hosts. 76 | type: boolean 77 | default: true 78 | ipv6_only: 79 | description: Use IPv6 addresses only 80 | type: boolean 81 | default: false 82 | show_custom_values: 83 | description: Lists defined custom parameters and values 84 | type: boolean 85 | default: false 86 | groups: 87 | description: Limits to specific names groups 88 | type: list 89 | elements: str 90 | required: false 91 | pending_reboot_only: 92 | description: Limits to systems requiring a reboot only 93 | type: boolean 94 | default: false 95 | ''' 96 | 97 | EXAMPLES = r''' 98 | --- 99 | # my.uyuni.yml 100 | plugin: stdevel.uyuni.inventory 101 | host: 192.168.180.1 102 | user: admin 103 | password: admin 104 | verify_ssl: false 105 | show_custom_values: true 106 | ipv6_only: true 107 | groups: 108 | - dev 109 | - demo 110 | ... 111 | 112 | --- 113 | # for use in AWX / AAP (Inventory Source "Sourced from a Project"), 114 | # together with a custom credential that injects environment variables UYUNI_HOST, UYUNI_USER, UYUNI_PASSWORD 115 | plugin: stdevel.uyuni.inventory 116 | show_custom_values: true 117 | ... 118 | ''' 119 | 120 | from ansible.plugins.inventory import ( 121 | BaseInventoryPlugin, Constructable, Cacheable 122 | ) 123 | from ..module_utils.helper_functions import _configure_connection 124 | 125 | 126 | class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable): 127 | """ 128 | Host inventory parser for ansible using Uyuni 129 | """ 130 | 131 | NAME = 'stdevel.uyuni.inventory' 132 | 133 | def __init__(self): 134 | """ 135 | Initializes the inventory plugin 136 | """ 137 | super(InventoryModule, self).__init__() 138 | 139 | # clear config 140 | self.api_instance = None 141 | self.host = None 142 | self.user = None 143 | self.password = None 144 | self.port = None 145 | self.verify_ssl = None 146 | 147 | def verify_file(self, path): 148 | """ 149 | Verifies the configuration file 150 | """ 151 | valid = False 152 | if super(InventoryModule, self).verify_file(path): 153 | if path.endswith(('uyuni.yaml', 'uyuni.yml')): 154 | valid = True 155 | else: 156 | self.display.vvv( 157 | 'Skipping due to inventory source not ending in "uyuni.yml" nor "uyuni.yaml"' # noqa: E501 158 | ) 159 | return valid 160 | 161 | def _api_connect(self): 162 | """ 163 | Connects to the Uyuni API 164 | """ 165 | self.api_instance = _configure_connection( 166 | dict( 167 | host=str(self.get_option('host')), 168 | username=str(self.get_option('user')), 169 | password=str(self.get_option('password')), 170 | port=str(self.get_option('port')), 171 | verify_ssl=self.get_option('verify_ssl') 172 | ) 173 | ) 174 | 175 | def _populate(self): 176 | # get groups and hosts 177 | all_groups = self.api_instance.get_all_hostgroups() 178 | hosts = self.api_instance.get_all_hosts() 179 | 180 | if self.get_option('groups'): 181 | # limit to group selection 182 | groups = [x for x in all_groups if x in self.get_option('groups')] 183 | else: 184 | # all groups 185 | groups = all_groups 186 | 187 | for group in groups: 188 | # add selected/all groups 189 | self.inventory.add_group(group) 190 | 191 | # get systems requiring reboot 192 | _reboot = self.api_instance.get_hosts_by_required_reboot() 193 | 194 | # add _all_ the hosts 195 | for host in hosts: 196 | # get host groups 197 | _groups = self.api_instance.get_hostgroups_by_host(int(host['id'])) 198 | 199 | if self.get_option('groups'): 200 | # only add if host is filtered groups 201 | if not any(x in _groups for x in self.get_option('groups')): 202 | continue 203 | 204 | # check if reboot required 205 | if self.get_option('pending_reboot_only'): 206 | try: 207 | if host['name'] not in _reboot: 208 | continue 209 | except TypeError: 210 | continue 211 | 212 | # add host 213 | self.inventory.add_host(host['name']) 214 | 215 | # get IP address 216 | _network = self.api_instance.get_host_network(int(host['id'])) 217 | if self.get_option('ipv6_only'): 218 | self.inventory.set_variable( 219 | host['name'], 'ansible_host', _network['ip6'] 220 | ) 221 | else: 222 | self.inventory.set_variable( 223 | host['name'], 'ansible_host', _network['ip'] 224 | ) 225 | 226 | # add parameters 227 | if self.get_option('show_custom_values'): 228 | _params = self.api_instance.get_host_params(int(host['id'])) 229 | for param in _params: 230 | self.inventory.set_variable( 231 | host['name'], param, _params[param] 232 | ) 233 | 234 | # add hostgroups 235 | for _group in _groups: 236 | if _group in groups: 237 | self.inventory.add_child(_group, host['name']) 238 | 239 | def parse(self, inventory, loader, path, cache=True): 240 | """ 241 | Parses the inventory 242 | """ 243 | super(InventoryModule, self).parse(inventory, loader, path) 244 | 245 | # read config from file, this sets 'options' 246 | self._read_config_data(path) 247 | 248 | # create API instance 249 | self._api_connect() 250 | 251 | # create inventory 252 | self._populate() 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | {one line to give the program's name and a brief idea of what it does.} 635 | Copyright (C) {year} {name of author} 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | {project} Copyright (C) {year} {fullname} 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------