From 725f247aba678bda48aef55ce029e166a810f9c6 Mon Sep 17 00:00:00 2001 From: Markus Frei Date: Thu, 29 Oct 2026 10:29:06 +0100 Subject: [PATCH] feat(roles/curaline): install the client from the Linuxfabrik repository The role installs the swisscom-schweiz-ag-cdr-client package and only renders application-customer.yaml, creates the missing connector folders (2770, for the group of the account) and adds them as ReadWritePaths in a drop-in. Download, version marker, chown, SELinux file context, unit and logback templates go, as do curaline__user, curaline__group and curaline__version together with the hnet scenario: the client runs under the account of the package. --- CHANGELOG.md | 2 +- .../molecule/curaline/hnet/converge.yml | 2 - .../group_vars/systems_under_test.yml | 28 --- .../curaline/hnet/inventory/hosts.yml | 21 -- .../molecule/curaline/hnet/molecule.yml | 11 - extensions/molecule/curaline/hnet/prepare.yml | 69 ------ extensions/molecule/curaline/hnet/verify.yml | 64 ----- .../group_vars/systems_under_test.yml | 2 - .../curaline/install/inventory/hosts.yml | 6 +- .../molecule/curaline/install/molecule.yml | 6 +- .../molecule/curaline/install/verify.yml | 52 +++- playbooks/README.md | 1 + playbooks/curaline.yml | 4 + roles/curaline/README.md | 88 ++----- roles/curaline/defaults/main.yml | 4 +- roles/curaline/meta/argument_specs.yml | 19 +- roles/curaline/tasks/main.yml | 223 ++---------------- .../cdr-client/logback-service.xml.j2 | 38 --- .../read-write-paths.conf.j2 | 9 + .../swisscom-schweiz-ag-cdr-client.service.j2 | 39 --- roles/curaline/vars/Debian.yml | 2 - roles/curaline/vars/RedHat.yml | 3 - roles/curaline/vars/Ubuntu.yml | 2 - roles/curaline/vars/main.yml | 3 - 24 files changed, 112 insertions(+), 586 deletions(-) delete mode 100644 extensions/molecule/curaline/hnet/converge.yml delete mode 100644 extensions/molecule/curaline/hnet/inventory/group_vars/systems_under_test.yml delete mode 100644 extensions/molecule/curaline/hnet/inventory/hosts.yml delete mode 100644 extensions/molecule/curaline/hnet/molecule.yml delete mode 100644 extensions/molecule/curaline/hnet/prepare.yml delete mode 100644 extensions/molecule/curaline/hnet/verify.yml delete mode 100644 roles/curaline/templates/etc/swisscom-schweiz-ag/cdr-client/logback-service.xml.j2 create mode 100644 roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf.j2 delete mode 100644 roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.j2 delete mode 100644 roles/curaline/vars/Debian.yml delete mode 100644 roles/curaline/vars/RedHat.yml delete mode 100644 roles/curaline/vars/Ubuntu.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 38c7e62b6..a84e7ab8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,7 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * **role:exoscale_vm**: Security group rules support every protocol Exoscale offers, ICMP with `icmp_type` and `icmp_code`, private networks can be detached with `state: 'absent'`, and an existing instance is attached to a security group added later. * **role:collabora**: Add support for Collabora Online Enterprise 25.04.14. * **execution-environment**: Add the `telekom_mms.icinga_director` collection, so inventories using its Icinga Director inventory plugin work inside the EE. -* **role:curaline, playbook:curaline**: Add a role and playbook that install and configure the curaLINE Client of Swisscom, which exchanges files with the curaLINE Document Routing service. +* **role:curaline, playbook:curaline**: Add a role and playbook that install the curaLINE Client of Swisscom from the Linuxfabrik repository and configure its connectors and folders; the client exchanges files with the curaLINE Document Routing service. * **role:gitlab_ce**: Behind a reverse proxy, GitLab can show and log the real client IP address instead of the address of the proxy, for example under "Current sign-in IP". Set `gitlab_ce__rb_nginx_real_ip_trusted_addresses` to the address of the proxy. * **role:apache_tomcat**: With `lfops__remove_rpmnew_rpmsave`, the role also removes the `.rpmnew` and `.rpmsave` files next to the Tomcat configuration files it deploys. * **role:apache_tomcat**: Add support for Tomcat 10.1 on RHEL 10. diff --git a/extensions/molecule/curaline/hnet/converge.yml b/extensions/molecule/curaline/hnet/converge.yml deleted file mode 100644 index 543ddfa7a..000000000 --- a/extensions/molecule/curaline/hnet/converge.yml +++ /dev/null @@ -1,2 +0,0 @@ -- name: 'Converge curaline playbook' - ansible.builtin.import_playbook: 'linuxfabrik.lfops.curaline' diff --git a/extensions/molecule/curaline/hnet/inventory/group_vars/systems_under_test.yml b/extensions/molecule/curaline/hnet/inventory/group_vars/systems_under_test.yml deleted file mode 100644 index 3fb5d5148..000000000 --- a/extensions/molecule/curaline/hnet/inventory/group_vars/systems_under_test.yml +++ /dev/null @@ -1,28 +0,0 @@ -curaline__version: '6.2.1' - -# the credentials are not valid: the client keeps running and reports the denied login in its -# health endpoint, which is all this scenario needs to see that it reads its configuration. -curaline__client_id: 'linuxfabrik' -curaline__client_secret: 'linuxfabrik' - -# a non-default value, so verify.yml can see that the running client uses what was configured. -curaline__schedule_delay: 'PT15M' - -curaline__connectors__group_var: - - connector-id: 40000 - mode: 'test' - content-type: 'application/forumdatenaustausch+xml;charset=UTF-8' - source-folder: '/data/xml/outbox-test/XMLDatenforum' - target-folder: '/data/xml/inbox-test/XMLDatenforum' - source-archive-enabled: true - source-archive-folder: '/data/xml/archive-test' - # a connector that is set to absent must neither end up in the configuration nor get folders. - - connector-id: 40000 - mode: 'production' - source-folder: '/data/xml/outbox-absent/XMLDatenforum' - target-folder: '/data/xml/inbox-absent/XMLDatenforum' - state: 'absent' - -# take over the account and the data folders of the H-Net SecureClient that prepare.yml set up. -curaline__group: 'hnet' -curaline__user: 'hnet' diff --git a/extensions/molecule/curaline/hnet/inventory/hosts.yml b/extensions/molecule/curaline/hnet/inventory/hosts.yml deleted file mode 100644 index 0fa5a5c06..000000000 --- a/extensions/molecule/curaline/hnet/inventory/hosts.yml +++ /dev/null @@ -1,21 +0,0 @@ -# yamllint disable rule:empty-values - -# curaline targets 'lfops_curaline' (see playbooks/curaline.yml: hosts). -# -# All platforms of COMPATIBILITY.md are covered: the client is a self-contained download -# with its own Java runtime, so the role differs between the families only in the packages -# and in SELinux, which the RHEL family has enforcing. -lfops_curaline: - children: - systems_under_test: - -systems_under_test: - hosts: - debian12-vm: - debian13-vm: - rocky8-vm: - rocky9-vm: - rocky10-vm: - ubuntu2204-vm: - ubuntu2404-vm: - ubuntu2604-vm: diff --git a/extensions/molecule/curaline/hnet/molecule.yml b/extensions/molecule/curaline/hnet/molecule.yml deleted file mode 100644 index c56bcd86f..000000000 --- a/extensions/molecule/curaline/hnet/molecule.yml +++ /dev/null @@ -1,11 +0,0 @@ -# Molecule scenario marker for the 'hnet' sub-scenario, run with -# `molecule test --scenario-name curaline/hnet`. -# -# The prepare playbook is overridden so the host looks like one with an H-Net SecureClient -# installation before converge runs: the regular `hnet` account with its home in -# /home/hnet and the /data/xml tree owned by it, labelled `user_home_t` on the RHEL -# family. The scenario-local prepare.yml imports the shared vm-prepare.yml first, so the -# systems under test are still waited for and their facts still gathered. -provisioner: - playbooks: - prepare: '${MOLECULE_SCENARIO_DIRECTORY}/prepare.yml' diff --git a/extensions/molecule/curaline/hnet/prepare.yml b/extensions/molecule/curaline/hnet/prepare.yml deleted file mode 100644 index 7945915a7..000000000 --- a/extensions/molecule/curaline/hnet/prepare.yml +++ /dev/null @@ -1,69 +0,0 @@ -# Wait for the systems under test and gather their facts, as every other scenario does. -- name: 'Prepare VMs for Ansible' - ansible.builtin.import_playbook: '../../playbooks/vm-prepare.yml' - - -# Set the host up the way the Linuxfabrik installation guide of the H-Net SecureClient -# does, without the SecureClient itself: the role has to take over this account and these -# folders without changing them. -- name: 'Set up the account and the data folders of an H-Net SecureClient' - hosts: 'systems_under_test' - - tasks: - - - name: 'useradd --comment "User for H-Net Secure Client" hnet' - ansible.builtin.user: - name: 'hnet' - comment: 'User for H-Net Secure Client' - shell: '/bin/bash' - state: 'present' - - - name: 'mkdir -p /data/xml/{inbox,inbox-test,outbox,outbox-test,archive,archive-test} /var/hnet' - ansible.builtin.file: - path: '{{ item }}' - state: 'directory' - owner: 'hnet' - group: 'hnet' - mode: 0o755 - loop: - - '/data/xml' - - '/data/xml/archive' - - '/data/xml/archive-test' - - '/data/xml/inbox' - - '/data/xml/inbox-test' - - '/data/xml/outbox' - - '/data/xml/outbox-test' - - '/var/hnet' - - # an existing connector folder with a mode the role would not set, so verify.yml can - # see that the role leaves it alone. - - name: 'mkdir -m 0770 /data/xml/outbox-test/XMLDatenforum' - ansible.builtin.file: - path: '/data/xml/outbox-test/XMLDatenforum' - state: 'directory' - owner: 'hnet' - group: 'hnet' - mode: 0o770 - - - name: 'Label /data/xml as user_home_t' - when: - - 'ansible_facts["os_family"] == "RedHat"' - block: - - - name: 'dnf install policycoreutils-python-utils' - ansible.builtin.package: - name: 'policycoreutils-python-utils' - state: 'present' - retries: 3 - delay: 10 - - - name: 'semanage fcontext --add --type user_home_t "/data/xml(/.*)?"' - community.general.sefcontext: - target: '/data/xml(/.*)?' - setype: 'user_home_t' - state: 'present' - - - name: 'restorecon -Fvr /data/xml' - ansible.builtin.command: 'restorecon -Fvr /data/xml' - register: '__molecule__curaline_restorecon_result' - changed_when: '__molecule__curaline_restorecon_result["stdout"] | length > 0' diff --git a/extensions/molecule/curaline/hnet/verify.yml b/extensions/molecule/curaline/hnet/verify.yml deleted file mode 100644 index 677b469c0..000000000 --- a/extensions/molecule/curaline/hnet/verify.yml +++ /dev/null @@ -1,64 +0,0 @@ -# The client has to work under the `hnet` account exactly as under its own one, so the -# checks of the install scenario run first. -- name: 'Verify curaline' - ansible.builtin.import_playbook: '../install/verify.yml' - - -# What the takeover must not change is the account and the folders of the H-Net -# SecureClient, and the client must not write into /home/hnet, which the SecureClient -# lives in. -- name: 'Verify that the H-Net SecureClient installation is untouched' - hosts: 'systems_under_test' - gather_facts: false - tasks: - - - name: 'getent passwd hnet' - ansible.builtin.getent: - database: 'passwd' - key: 'hnet' - - # getent_passwd values: password, UID, GID, comment, home, shell - - name: 'Assert that the hnet account keeps its comment, home and shell' - ansible.builtin.assert: - that: - - 'ansible_facts["getent_passwd"]["hnet"][3] == "User for H-Net Secure Client"' - - 'ansible_facts["getent_passwd"]["hnet"][4] == "/home/hnet"' - - 'ansible_facts["getent_passwd"]["hnet"][5] == "/bin/bash"' - - - name: 'stat /data/xml/outbox-test/XMLDatenforum' - ansible.builtin.stat: - path: '/data/xml/outbox-test/XMLDatenforum' - register: '__molecule__curaline_outbox_result' - - - name: 'Assert that the existing connector folder keeps its mode' - ansible.builtin.assert: - that: - - '__molecule__curaline_outbox_result["stat"]["mode"] == "0770"' - - '__molecule__curaline_outbox_result["stat"]["pw_name"] == "hnet"' - - # the client gets its $HOME from the unit, so its `.cache` lands below - # /var/lib/swisscom-schweiz-ag/cdr-client instead. - - name: 'stat /home/hnet/.cache' - ansible.builtin.stat: - path: '/home/hnet/.cache' - register: '__molecule__curaline_home_cache_result' - - - name: 'Assert that the client did not write into the home of hnet' - ansible.builtin.assert: - that: 'not __molecule__curaline_home_cache_result["stat"]["exists"]' - - - name: 'Verify the SELinux context of the data folders' - when: - - 'inventory_hostname is match("rocky")' - block: - - - name: 'stat --format=%C /data/xml/inbox-test/XMLDatenforum' - ansible.builtin.command: 'stat --format=%C /data/xml/inbox-test/XMLDatenforum' - changed_when: false - register: '__molecule__curaline_inbox_result' - - # the role created this folder, the file context of the SecureClient setup has to - # apply to it as well. - - name: 'Assert that the folders the role created get the user_home_t type' - ansible.builtin.assert: - that: '":user_home_t:" in __molecule__curaline_inbox_result["stdout"]' diff --git a/extensions/molecule/curaline/install/inventory/group_vars/systems_under_test.yml b/extensions/molecule/curaline/install/inventory/group_vars/systems_under_test.yml index 0516bb6d5..704adea52 100644 --- a/extensions/molecule/curaline/install/inventory/group_vars/systems_under_test.yml +++ b/extensions/molecule/curaline/install/inventory/group_vars/systems_under_test.yml @@ -1,5 +1,3 @@ -curaline__version: '6.2.1' - # the credentials are not valid: the client keeps running and reports the denied login in its # health endpoint, which is all this scenario needs to see that it reads its configuration. curaline__client_id: 'linuxfabrik' diff --git a/extensions/molecule/curaline/install/inventory/hosts.yml b/extensions/molecule/curaline/install/inventory/hosts.yml index 0fa5a5c06..ae06df35a 100644 --- a/extensions/molecule/curaline/install/inventory/hosts.yml +++ b/extensions/molecule/curaline/install/inventory/hosts.yml @@ -2,9 +2,9 @@ # curaline targets 'lfops_curaline' (see playbooks/curaline.yml: hosts). # -# All platforms of COMPATIBILITY.md are covered: the client is a self-contained download -# with its own Java runtime, so the role differs between the families only in the packages -# and in SELinux, which the RHEL family has enforcing. +# All platforms of COMPATIBILITY.md are covered: the package of the client brings its own +# Java runtime, so the role is the same on all of them, and only the RHEL family has SELinux +# enforcing. lfops_curaline: children: systems_under_test: diff --git a/extensions/molecule/curaline/install/molecule.yml b/extensions/molecule/curaline/install/molecule.yml index 0e5759258..8b807a741 100644 --- a/extensions/molecule/curaline/install/molecule.yml +++ b/extensions/molecule/curaline/install/molecule.yml @@ -2,6 +2,6 @@ # `molecule test --scenario-name curaline/install`. # # The shared config.yml already provisions libvirt/KVM VMs, so this -# scenario needs no driver override: the role sets the SELinux file context that lets systemd -# execute the client from /usr/lib, which only a VM with an enforcing SELinux proves, and the -# Debian family needs systemd, which the Debian and Ubuntu container images do not ship. +# scenario needs no driver override: whether systemd may execute the packaged client from +# /usr/lib only a VM with an enforcing SELinux proves, and the client runs as a systemd +# service, which the container images do not ship. diff --git a/extensions/molecule/curaline/install/verify.yml b/extensions/molecule/curaline/install/verify.yml index e74b35630..976ae3abc 100644 --- a/extensions/molecule/curaline/install/verify.yml +++ b/extensions/molecule/curaline/install/verify.yml @@ -1,7 +1,8 @@ -# What only the running system can confirm is that the client starts from the unpacked -# download under the configured account, that SELinux lets systemd execute it, and that it reads the -# configuration the role wrote: the client is asked for its health and its version, and it -# reports the values it parsed in its log. The configuration files themselves are not checked. +# What only the running system can confirm is that the client starts from the package under +# its account, that SELinux lets systemd execute it, that the drop-in lets it write to the +# folders of its connectors, and that it reads the configuration the role wrote: the client is +# asked for its health and its version, and it reports the values it parsed in its log. The +# configuration files themselves are not checked. - name: 'Verify curaline' hosts: 'systems_under_test' gather_facts: false @@ -43,9 +44,12 @@ return_content: true register: '__molecule__curaline_info_result' - - name: 'Assert that the client is the installed version' + - name: 'Gather the package facts' + ansible.builtin.package_facts: # yamllint disable-line rule:empty-values + + - name: 'Assert that the client is the version of the installed package' ansible.builtin.assert: - that: '__molecule__curaline_info_result["json"]["build"]["version"] == curaline__version' + that: '__molecule__curaline_info_result["json"]["build"]["version"] == ansible_facts["packages"]["swisscom-schweiz-ag-cdr-client"][0]["version"]' - name: 'systemctl show swisscom-schweiz-ag-cdr-client.service --property=MainPID --value' # noqa command-instead-of-module (read-only property query) ansible.builtin.command: 'systemctl show swisscom-schweiz-ag-cdr-client.service --property=MainPID --value' @@ -57,11 +61,9 @@ path: '/proc/{{ __molecule__curaline_mainpid_result["stdout"] }}' register: '__molecule__curaline_proc_result' - # the hnet scenario imports this playbook and sets curaline__user, the install scenario - # relies on the role default. - - name: 'Assert that the client runs as {{ curaline__user | d("swisscom-schweiz-ag-cdr-client") }}' + - name: 'Assert that the client runs as swisscom-schweiz-ag-cdr-client' ansible.builtin.assert: - that: '__molecule__curaline_proc_result["stat"]["pw_name"] == curaline__user | d("swisscom-schweiz-ag-cdr-client")' + that: '__molecule__curaline_proc_result["stat"]["pw_name"] == "swisscom-schweiz-ag-cdr-client"' # the client logs the schedule delay and the folders it watches after it has parsed them. - name: 'Wait for the client to log the polling of the source folder' @@ -87,9 +89,33 @@ ansible.builtin.assert: that: 'not __molecule__curaline_absent_result["stat"]["exists"]' - # without the bin_t file context systemd cannot execute the client from /usr/lib, and the - # service would not run at all. a host that happens to run permissive would pass that - # assertion without proving anything, so the mode is asserted too. + # the sandbox of the packaged unit only lets the client write to the folders of the + # drop-in, so the folders of the connector must be in it, created for the group of the + # account. + - name: 'systemctl show swisscom-schweiz-ag-cdr-client.service --property=ReadWritePaths --value' # noqa command-instead-of-module (read-only property query) + ansible.builtin.command: 'systemctl show swisscom-schweiz-ag-cdr-client.service --property=ReadWritePaths --value' + changed_when: false + register: '__molecule__curaline_readwritepaths_result' + + - name: 'stat /data/xml/outbox-test/XMLDatenforum' + ansible.builtin.stat: + path: '/data/xml/outbox-test/XMLDatenforum' + register: '__molecule__curaline_outbox_result' + + - name: 'Assert that the client may write to the folders of its connector' + ansible.builtin.assert: + that: + - '"/data/xml/archive-test" in __molecule__curaline_readwritepaths_result["stdout"]' + - '"/data/xml/inbox-test/XMLDatenforum" in __molecule__curaline_readwritepaths_result["stdout"]' + - '"/data/xml/outbox-test/XMLDatenforum" in __molecule__curaline_readwritepaths_result["stdout"]' + - '__molecule__curaline_outbox_result["stat"]["pw_name"] == "swisscom-schweiz-ag-cdr-client"' + - '__molecule__curaline_outbox_result["stat"]["mode"] == "2770"' + + # the packaged unit starts the client through /usr/bin/env (bin_t), so that systemd + # transitions to unconfined_service_t; the client itself below /usr/lib is lib_t, which + # systemd could not execute directly, and the service would not run at all. a host that + # happens to run permissive would pass that assertion without proving anything, so the + # mode is asserted too. - name: 'Verify the SELinux context of the client' when: - 'inventory_hostname is match("rocky")' diff --git a/playbooks/README.md b/playbooks/README.md index 63872cd94..4ed52fc05 100644 --- a/playbooks/README.md +++ b/playbooks/README.md @@ -215,6 +215,7 @@ Calls the following roles (in order): Calls the following roles (in order): +* [repo_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_linuxfabrik): `curaline__skip_repo_linuxfabrik` * [curaline](https://github.com/Linuxfabrik/lfops/tree/main/roles/curaline) diff --git a/playbooks/curaline.yml b/playbooks/curaline.yml index 98d8a08f4..ad24c8825 100644 --- a/playbooks/curaline.yml +++ b/playbooks/curaline.yml @@ -19,6 +19,10 @@ roles: + - role: 'linuxfabrik.lfops.repo_linuxfabrik' + when: + - 'not curaline__skip_repo_linuxfabrik | d(false)' + - role: 'linuxfabrik.lfops.curaline' diff --git a/roles/curaline/README.md b/roles/curaline/README.md index 328dfb552..624e2ce51 100644 --- a/roles/curaline/README.md +++ b/roles/curaline/README.md @@ -1,6 +1,6 @@ # Ansible Role linuxfabrik.lfops.curaline -This role installs and configures the curaLINE Client (`cdr-client`) of Swisscom, which exchanges files with the curaLINE Document Routing (CDR) service of the Swiss healthcare system. It unpacks the client from the Swisscom download page, configures its connectors and runs it as the systemd service `swisscom-schweiz-ag-cdr-client.service`, either under a system account of its own or under an existing account such as `hnet` of an H-Net SecureClient installation. +This role installs the curaLINE Client (`cdr-client`) of Swisscom, which exchanges files with the curaLINE Document Routing (CDR) service of the Swiss healthcare system, from the Linuxfabrik repository, and configures its connectors and folders. The client runs as the systemd service `swisscom-schweiz-ag-cdr-client.service` under the account `swisscom-schweiz-ag-cdr-client`, which the package creates. *Available in the next LFOps release.* @@ -8,47 +8,41 @@ This role installs and configures the curaLINE Client (`cdr-client`) of Swisscom ## How the Role Behaves -* The client is downloaded on the Ansible controller (`delegate_to: 'localhost'`) into `/tmp` and uploaded to the target, so targets without Internet access can still be provisioned. The controller needs outbound HTTPS access to `cdr.health.swisscom.ch`. The target needs it only for the client to do its work. -* The client comes with its own Java runtime and is only published for `linux-amd64`. The role aborts on other architectures. -* The installed version is recorded in `/usr/lib/swisscom-schweiz-ag/cdr-client/.ansible-version`. When it differs from `curaline__version`, the role removes `/usr/lib/swisscom-schweiz-ag/cdr-client`, unpacks the new version and restarts the service. The configuration lives in `/etc/swisscom-schweiz-ag/cdr-client` and survives the update. -* The service runs as `curaline__user` and `curaline__group`. If they do not exist, the role creates them as a system user and group. An existing user is taken over as it is: the role changes neither its UID, home, shell nor comment, so other software using the account keeps working. -* The installation directory belongs to `curaline__user`, as with the Debian package of the client. The role hands it over recursively when `curaline__user` or `curaline__group` change. -* The systemd unit is modelled on the one of the Debian package, with the same name, user and group names and directories, but with a static user instead of a dynamic one. The client gets `/var/lib/swisscom-schweiz-ag/cdr-client` as its `$HOME`, whatever the home of `curaline__user` is, and writes its log to `/var/log/swisscom-schweiz-ag/cdr-client`. The unit runs the client sandboxed: it may only write to its installation, configuration, state, cache and log directories and to the folders of its connectors. -* The folders of the connectors (`source-folder`, `target-folder`, `source-archive-folder`) and `curaline__local_folder` are created for `curaline__user` if they do not exist yet. Existing folders keep their owner, mode and content, so they must already be writable by `curaline__user`. Parent directories are created owned by `root`. The client creates the subfolders below them, such as `error`, itself. -* Files must be written to a source folder under a temporary name and renamed to `*.xml` when complete, and must be readable by `curaline__user`. The client uploads a file that is still being written in fragments, and retries an unreadable file forever as `*.upload`. -* The files in `/etc/swisscom-schweiz-ag/cdr-client` are re-rendered on every run (a timestamped backup is kept), so manual edits are overwritten. This includes the client secret in `application-customer.yaml`. +* The package `swisscom-schweiz-ag-cdr-client` brings the client with its own Java runtime, the account, the default configuration and the systemd unit. Updates of the client come through the package manager. +* The role renders `/etc/swisscom-schweiz-ag/cdr-client/application-customer.yaml` on every run (a timestamped backup is kept), so manual edits are overwritten. This includes the client secret. The file belongs to `swisscom-schweiz-ag-cdr-client`, since the client refuses to start with a configuration it does not own. * With `curaline__renew_credential: true` the client renews the secret and rewrites `application-customer.yaml` itself. The next run of the role restores the old secret and renewal time from the inventory, so the client immediately tries to renew again with a secret that is no longer valid, and its login is denied. Keep it at `false` and renew the secret by hand. +* The packaged unit runs the client sandboxed (`ProtectSystem=strict`): it may only write to its own directories below `/etc`, `/var/lib`, `/var/cache` and `/var/log`. The role adds the folders of the connectors (`source-folder`, `target-folder`, `source-archive-folder`) and `curaline__local_folder` to the drop-in `/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf`. +* Missing folders are created for `swisscom-schweiz-ag-cdr-client:swisscom-schweiz-ag-cdr-client` with mode `2770`, so the accounts of the scripts that fill the source folders and empty the target folders get access by being added to the group `swisscom-schweiz-ag-cdr-client`. Existing folders keep their owner, mode and content, so they must already be writable by the account. Parent directories are created owned by `root`. The client creates the subfolders below them, such as `error`, itself. +* Files must be written to a source folder under a temporary name and renamed to `*.xml` when complete, and must be readable by `swisscom-schweiz-ag-cdr-client`. The client uploads a file that is still being written in fragments, and retries an unreadable file forever as `*.upload`. * The role does not manage the firewall. The client only makes outgoing connections, and its management ports `8191` and `8193` listen on `127.0.0.1`. -* On the Red Hat family with SELinux, the role sets the `bin_t` file context on `/usr/lib/swisscom-schweiz-ag/cdr-client/bin`, so that systemd may execute the client from `/usr/lib`. -* The client downloads a document into `curaline__local_folder` and then moves it to the target folder, so the file keeps the SELinux label of `curaline__local_folder`. If a confined service reads the target folder, for example the `/data/xml` tree of an H-Net SecureClient installation labelled `user_home_t`, put `curaline__local_folder` below the same tree, such as `/data/xml/cdr-inflight`. +* The client downloads a document into `curaline__local_folder` and then moves it to the target folder, so on the Red Hat family the file keeps the SELinux label of `curaline__local_folder` (`var_lib_t` by default). If a confined service such as Samba or httpd reads the target folder, put `curaline__local_folder` below the same tree as the target folder. ## Known Limitations * Only `x86_64` hosts are supported. -* Hosts with the Debian package of the client are not supported. Remove the package before running the role. * The role does not configure the proxy of the client. * The role manages one client per host. +## Dependent Roles + +Any [LFOps playbook](https://github.com/Linuxfabrik/lfops/blob/main/playbooks/README.md) that installs this role runs these for you. Optional ones can be disabled via the playbook's skip variables. + +* The Linuxfabrik repository must be enabled (role: [linuxfabrik.lfops.repo_linuxfabrik](https://github.com/Linuxfabrik/lfops/tree/main/roles/repo_linuxfabrik)). The package of the client is served from it. + + ## Requirements * Credentials for the curaLINE Document Routing service: the client ID and the client secret of the application registered for this client, and the connector ID(s) of the practice or institution. -* Outbound HTTPS access from the Ansible controller to `cdr.health.swisscom.ch`, and from the target to `cdr.health.swisscom.ch` and `login.microsoftonline.com`. +* Outbound HTTPS access from the target to `cdr.health.swisscom.ch` and `login.microsoftonline.com`. ## Replacing the H-Net SecureClient -To take over the data folders of an H-Net SecureClient installation (typically `/data/xml/{inbox,outbox,archive}` and their `-test` counterparts, owned by `hnet:hnet`), run the client under the same account: +The SecureClient keeps its data in `/data/xml/{inbox,outbox,archive}` and their `-test` counterparts, owned by `hnet:hnet`. The curaLINE Client writes exactly into the configured folder, while the SecureClient appends the transaction type, so the folders of a connector end in `XMLDatenforum`, for example `/data/xml/outbox/XMLDatenforum`. Give `swisscom-schweiz-ag-cdr-client` access to the existing folders, for example by handing them to its group, or let the role create new ones. -```yaml -curaline__group: 'hnet' -curaline__user: 'hnet' -``` - -The role leaves the `hnet` account, `/home/hnet`, `/var/hnet` and the SELinux setup of the SecureClient (`user_home_t` on `/data/xml`, the `HnetSecureService` policy module) untouched, and does not manage `hnet-securesvc.service`. - -**Warning:** Do not run both clients on the same folders. The SecureClient and the curaLINE Client would both pick up the files in the outbox, and documents could be sent twice or get lost. Stop and disable the SecureClient (`systemctl disable --now hnet-securesvc.service`) before the curaLINE Client is started for the first time, or deploy the role with `curaline__service_enabled: false` and `curaline__service_state: 'stopped'` until the switch-over. +**Warning:** Do not run both clients on the same folders. Both would pick up the files in the outbox, and documents could be sent twice or get lost. Stop and disable the SecureClient (`systemctl disable --now hnet-securesvc.service`) before the curaLINE Client is started for the first time, or deploy the role with `curaline__service_enabled: false` and `curaline__service_state: 'stopped'` until the switch-over. ## Post-Installation Steps @@ -60,18 +54,15 @@ The role leaves the `hnet` account, `/home/hnet`, `/var/hnet` and the SELinux se `curaline` -* Installs the required packages. -* Creates the user and group of the service if they do not exist, and the missing folders of the connectors. * Installs the client. -* Deploys the configuration files and the systemd unit. -* Sets the SELinux file context of the client (Red Hat family with SELinux). +* Creates the missing folders of the connectors. +* Deploys the configuration file and the systemd drop-in. * Ensures the service is in the desired state. * Triggers: swisscom-schweiz-ag-cdr-client.service restart. `curaline:configure` -* Deploys the configuration files and the systemd unit. -* Sets the SELinux file context of the client (Red Hat family with SELinux). +* Deploys the configuration file and the systemd drop-in. * Triggers: swisscom-schweiz-ag-cdr-client.service restart. `curaline:state` @@ -79,12 +70,6 @@ The role leaves the `hnet` account, `/home/hnet`, `/var/hnet` and the SELinux se * Manages the service state (start, stop, enable, disable). * Triggers: none. -`curaline:update` - -* Installs the client in the version of `curaline__version`, if a different version or none is installed, and hands the installation directory to `curaline__user`. -* Sets the SELinux file context of the client (Red Hat family with SELinux). -* Triggers: swisscom-schweiz-ag-cdr-client.service restart. - ## Mandatory Role Variables @@ -148,11 +133,6 @@ The role leaves the `hnet` account, `/home/hnet`, `/var/hnet` and the SELinux se * Any other key the client knows is written to the configuration as it is. -`curaline__version` - -* The version of the curaLINE Client to install. -* Type: String. - Example: ```yaml # mandatory @@ -166,7 +146,6 @@ curaline__connectors__host_var: target-folder: '/data/xml/inbox-test/XMLDatenforum' source-archive-enabled: true source-archive-folder: '/data/xml/archive-test' -curaline__version: '6.2.1' ``` @@ -232,12 +211,6 @@ curaline__version: '6.2.1' * Type: Bool. * Default: `true` -`curaline__group` - -* Group of the service. Created as a system group if it does not exist. Set it to `'hnet'` to take over an H-Net SecureClient installation, see "Replacing the H-Net SecureClient". -* Type: String. -* Default: `'swisscom-schweiz-ag-cdr-client'` - `curaline__idp_endpoint` * The OAuth2 token endpoint of the identity provider. @@ -252,10 +225,9 @@ curaline__version: '6.2.1' `curaline__local_folder` -* Folder for temporary downloads. +* Folder for temporary downloads. Do not point it below `/tmp`: the unit runs the client with `PrivateTmp=yes`, which empties `/tmp` on every stop and restart and would lose the documents being downloaded. * Type: String. -* Default: `'/data/cdr-inflight'` -* Deviates from the upstream default `${java.io.tmpdir}`: the unit runs the client with `PrivateTmp=yes`, so its `/tmp` is private to the service and is emptied on every stop and restart, which would lose the documents that are being downloaded. +* Default: `'/var/lib/swisscom-schweiz-ag/cdr-client'` `curaline__renew_credential` @@ -294,12 +266,6 @@ curaline__version: '6.2.1' * Type: String. * Default: `'70b434db-eccb-4280-95dc-1e59220aca55'` -`curaline__user` - -* User of the service. Created as a system user without a login shell if it does not exist. An existing user is not modified. Set it to `'hnet'` to take over an H-Net SecureClient installation, see "Replacing the H-Net SecureClient". -* Type: String. -* Default: `'swisscom-schweiz-ag-cdr-client'` - Example: ```yaml # optional @@ -310,13 +276,11 @@ curaline__cdr_api_scheme: 'https' curaline__credential_api_base_path: 'api/client-credentials' curaline__file_busy_test_strategy: 'file_size_changed' curaline__file_synchronization_enabled: true -curaline__group: 'hnet' -curaline__local_folder: '/data/cdr-inflight' +curaline__local_folder: '/var/lib/swisscom-schweiz-ag/cdr-client' curaline__renew_credential: false curaline__schedule_delay: 'PT10M' curaline__service_enabled: true curaline__service_state: 'started' -curaline__user: 'hnet' ``` @@ -328,11 +292,7 @@ curaline__user: 'hnet' **The service fails with `status=226/NAMESPACE`** -* A path in `ReadWritePaths=` of the unit does not exist, typically a folder of a connector that was removed by hand, or one on a network share that is not mounted. Run the role again to recreate missing folders, or mount the share before the service starts. - -**The run aborts with `curaline__version must look like "6.2.1"`** - -* Set `curaline__version` to a release listed on the curaLINE download page. +* A path in `ReadWritePaths=` of the drop-in does not exist, typically a folder of a connector that was removed by hand, or one on a network share that is not mounted. Run the role again to recreate missing folders, or mount the share before the service starts. **The run aborts with `At least one connector must be present ...` or `Connector ... must set content-type, source-folder and target-folder`** diff --git a/roles/curaline/defaults/main.yml b/roles/curaline/defaults/main.yml index ae1884cfb..1250c1471 100644 --- a/roles/curaline/defaults/main.yml +++ b/roles/curaline/defaults/main.yml @@ -23,12 +23,11 @@ curaline__credential_api_port: '{{ curaline__cdr_api_port }}' curaline__credential_api_scheme: '{{ curaline__cdr_api_scheme }}' curaline__file_busy_test_strategy: 'file_size_changed' curaline__file_synchronization_enabled: true -curaline__group: 'swisscom-schweiz-ag-cdr-client' curaline__idp_endpoint: 'https://login.microsoftonline.com/{{ curaline__tenant_id }}/oauth2/v2.0/token' # a time in the past makes the client renew its credential immediately on start, if # curaline__renew_credential is enabled curaline__last_credential_renewal_time: '1970-01-01T00:00:00Z' -curaline__local_folder: '/data/cdr-inflight' # upstream default: ${java.io.tmpdir} +curaline__local_folder: '/var/lib/swisscom-schweiz-ag/cdr-client' curaline__renew_credential: false # upstream default: true curaline__schedule_delay: 'PT10M' curaline__scope: 'https://identity.health.swisscom.ch/CdrApi/.default' @@ -36,4 +35,3 @@ curaline__service_enabled: true curaline__service_state: 'started' # the Entra ID tenant of Swisscom, which the client ships with curaline__tenant_id: '70b434db-eccb-4280-95dc-1e59220aca55' -curaline__user: 'swisscom-schweiz-ag-cdr-client' diff --git a/roles/curaline/meta/argument_specs.yml b/roles/curaline/meta/argument_specs.yml index 0d4bce7f2..084dd9ade 100644 --- a/roles/curaline/meta/argument_specs.yml +++ b/roles/curaline/meta/argument_specs.yml @@ -101,12 +101,6 @@ argument_specs: default: true description: 'Whether the client uploads and downloads files.' - curaline__group: - type: 'str' - required: false - default: 'swisscom-schweiz-ag-cdr-client' - description: 'Group of the service, created as a system group if it does not exist.' - curaline__idp_endpoint: type: 'str' required: false @@ -121,7 +115,7 @@ argument_specs: curaline__local_folder: type: 'str' required: false - default: '/data/cdr-inflight' + default: '/var/lib/swisscom-schweiz-ag/cdr-client' description: 'Folder for temporary downloads.' curaline__renew_credential: @@ -163,14 +157,3 @@ argument_specs: required: false default: '70b434db-eccb-4280-95dc-1e59220aca55' description: 'The Entra ID tenant that issues the tokens.' - - curaline__user: - type: 'str' - required: false - default: 'swisscom-schweiz-ag-cdr-client' - description: 'User of the service, created as a system user if it does not exist. An existing user is not modified.' - - curaline__version: - type: 'str' - required: true - description: 'The version of the curaLINE client to install.' diff --git a/roles/curaline/tasks/main.yml b/roles/curaline/tasks/main.yml index 16925aa5d..2f0e1348a 100644 --- a/roles/curaline/tasks/main.yml +++ b/roles/curaline/tasks/main.yml @@ -1,26 +1,8 @@ -- block: - - - name: 'Set platform/version specific variables' - ansible.builtin.import_role: - name: 'shared' - tasks_from: 'platform-variables.yml' - - tags: - - 'always' - - - block: # simple checks (required, type, choices) live in meta/argument_specs.yml. a connector # without `connector-id` or `mode` is already rejected by `combine_lod`, which names the # item. - - name: 'Check variable constraints' - ansible.builtin.assert: - that: - - 'curaline__version is regex("^[0-9]+\.[0-9]+\.[0-9]+$")' - quiet: true - fail_msg: 'curaline__version must look like "6.2.1", got "{{ curaline__version }}".' - - name: 'Check that at least one connector is present' ansible.builtin.assert: that: @@ -53,66 +35,31 @@ - block: - - name: 'Install required packages' + # the package comes from the Linuxfabrik repository. it creates the account the client runs + # as, the default configuration and the systemd unit. + - name: 'Install swisscom-schweiz-ag-cdr-client' ansible.builtin.package: - name: '{{ __curaline__required_packages }}' + name: 'swisscom-schweiz-ag-cdr-client' state: 'present' retries: 3 # ride out a brief outage of the package repository delay: 10 - # an existing group keeps its GID, `ansible.builtin.group` only sets `system` on creation. - - name: 'groupadd --system {{ curaline__group }}' - ansible.builtin.group: - name: '{{ curaline__group }}' - state: 'present' - system: true - - # an existing account, such as the `hnet` account of an H-Net SecureClient installation, is - # taken over as it is. `ansible.builtin.user` would rewrite its home, shell and comment and - # break the other software using it. - - name: 'getent passwd {{ curaline__user }}' - ansible.builtin.command: 'getent passwd {{ curaline__user }}' - register: '__curaline__getent_passwd_result' - changed_when: false - failed_when: '__curaline__getent_passwd_result["rc"] not in [0, 2]' - check_mode: false - - - name: 'useradd --system --gid {{ curaline__group }} --home-dir {{ __curaline__state_dir }} --no-create-home --shell /usr/sbin/nologin {{ curaline__user }}' - ansible.builtin.user: - name: '{{ curaline__user }}' - comment: 'curaLINE Service Account' - group: '{{ curaline__group }}' - home: '{{ __curaline__state_dir }}' - create_home: false - shell: '/usr/sbin/nologin' - system: true - state: 'present' - when: '__curaline__getent_passwd_result["rc"] == 2' - - - name: 'mkdir -p {{ __curaline__config_dir }}' - ansible.builtin.file: - path: '{{ __curaline__config_dir }}' - state: 'directory' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o750 - - name: 'stat {{ __curaline__folders | join(" ") }}' ansible.builtin.stat: path: '{{ item }}' loop: '{{ __curaline__folders }}' register: '__curaline__folders_stat_result' - # only missing folders are created. existing ones, such as the `/data/xml` tree of an H-Net - # SecureClient installation, keep their owner and mode, and the files in them belong to - # whatever delivers or collects them. + # only missing folders are created, writable for the group of the account, so that the + # accounts of the scripts that fill the source folders and empty the target folders can be + # given access through it. existing folders keep their owner, mode and content. - name: 'mkdir -p {{ __curaline__folders | join(" ") }}' ansible.builtin.file: path: '{{ item["item"] }}' state: 'directory' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o755 + owner: 'swisscom-schweiz-ag-cdr-client' + group: 'swisscom-schweiz-ag-cdr-client' + mode: 0o2770 loop: '{{ __curaline__folders_stat_result["results"] | rejectattr("stat.exists") | list }}' loop_control: label: '{{ item["item"] }}' @@ -123,163 +70,46 @@ - block: - - name: 'mkdir -p {{ __curaline__install_dir }}' - ansible.builtin.file: - path: '{{ __curaline__install_dir }}' - state: 'directory' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o755 - - - name: 'cat {{ __curaline__install_dir }}/.ansible-version' - ansible.builtin.command: 'cat {{ __curaline__install_dir }}/.ansible-version' - register: '__curaline__installed_version_result' - changed_when: false - failed_when: false - check_mode: false - - # the tarball ships its own Java runtime. the whole directory belongs to the service account, - # as the Debian package of the client does it on every start of the service. - - name: 'Install cdr-client {{ curaline__version }}' - when: - - '__curaline__installed_version_result["stdout"] | trim != curaline__version | string' - block: - - # download on the controller and let `unarchive` upload it, so that targets without - # internet access can be provisioned. see "How the Role Behaves" in the README. - - name: 'curl --output /tmp/ansible.cdr-client-{{ curaline__version }}-linux-amd64.tar.gz https://cdr.health.swisscom.ch/share/downloads/cdr-client-{{ curaline__version }}-linux-amd64.tar.gz' # noqa risky-file-permissions (temporary file) - ansible.builtin.get_url: - url: 'https://cdr.health.swisscom.ch/share/downloads/cdr-client-{{ curaline__version }}-linux-amd64.tar.gz' - dest: '/tmp/ansible.cdr-client-{{ curaline__version }}-linux-amd64.tar.gz' - mode: 0o644 - retries: 3 # ride out a brief outage of the download source - delay: 10 - delegate_to: 'localhost' - become: false - vars: - ansible_become: false # noqa var-naming[pattern] - changed_when: false # not an actual change on the target - check_mode: false # run task even if `--check` is specified - - # a stale file of the previous version (a renamed jar, for example) would end up on the - # class path, so start from an empty directory. - - name: 'rm -rf {{ __curaline__install_dir }}' - ansible.builtin.file: - path: '{{ __curaline__install_dir }}' - state: 'absent' - - - name: 'mkdir {{ __curaline__install_dir }}' - ansible.builtin.file: - path: '{{ __curaline__install_dir }}' - state: 'directory' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o755 - - - name: 'tar --extract --gzip --strip-components=1 --directory {{ __curaline__install_dir }}' - ansible.builtin.unarchive: - src: '/tmp/ansible.cdr-client-{{ curaline__version }}-linux-amd64.tar.gz' - dest: '{{ __curaline__install_dir }}' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - extra_opts: - - '--strip-components=1' - notify: 'curaline: restart swisscom-schweiz-ag-cdr-client' - - # written last: an aborted installation is repeated by the next run. a state marker is not - # a config file, so `copy` is fine here. - - name: 'echo {{ curaline__version }} > {{ __curaline__install_dir }}/.ansible-version' - ansible.builtin.copy: - dest: '{{ __curaline__install_dir }}/.ansible-version' - content: '{{ curaline__version }}' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o644 - - # hands an existing installation over after a change of `curaline__user` or `curaline__group`. - - name: 'chown -R --changes {{ curaline__user }}:{{ curaline__group }} {{ __curaline__install_dir }}' - ansible.builtin.command: 'chown --recursive --no-dereference --changes {{ curaline__user }}:{{ curaline__group }} {{ __curaline__install_dir }}' - register: '__curaline__chown_result' - changed_when: '__curaline__chown_result["stdout"] | length > 0' - notify: 'curaline: restart swisscom-schweiz-ag-cdr-client' - - tags: - - 'curaline' - - 'curaline:update' - - -- block: - + # the client refuses to start with a configuration file it does not own, and rewrites it when + # it renews its credential. - name: 'Deploy {{ __curaline__config_dir }}/application-customer.yaml' ansible.builtin.template: backup: true src: 'etc/swisscom-schweiz-ag/cdr-client/application-customer.yaml.j2' dest: '{{ __curaline__config_dir }}/application-customer.yaml' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' + owner: 'swisscom-schweiz-ag-cdr-client' + group: 'swisscom-schweiz-ag-cdr-client' mode: 0o640 # contains the client secret diff: false # keep the client secret out of `--diff` output notify: 'curaline: restart swisscom-schweiz-ag-cdr-client' - - name: 'Deploy {{ __curaline__config_dir }}/logback-service.xml' - ansible.builtin.template: - backup: true - src: 'etc/swisscom-schweiz-ag/cdr-client/logback-service.xml.j2' - dest: '{{ __curaline__config_dir }}/logback-service.xml' - owner: '{{ curaline__user }}' - group: '{{ curaline__group }}' - mode: 0o640 - notify: 'curaline: restart swisscom-schweiz-ag-cdr-client' + - name: 'mkdir -p /etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d' + ansible.builtin.file: + path: '/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d' + state: 'directory' + owner: 'root' + group: 'root' + mode: 0o755 - # the unit is templated whole, not as a drop-in: the role does not install the Debian - # package, so there is no unit to override. it is modelled on the one of the package, but - # with a static account instead of a dynamic user, since the account owns the installation - # and data folders. - - name: 'Deploy /etc/systemd/system/swisscom-schweiz-ag-cdr-client.service' + - name: 'Deploy /etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf' ansible.builtin.template: backup: true - src: 'etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.j2' - dest: '/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service' + src: 'etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf.j2' + dest: '/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf' owner: 'root' group: 'root' mode: 0o644 - register: '__curaline__unit_result' + register: '__curaline__dropin_result' notify: 'curaline: restart swisscom-schweiz-ag-cdr-client' - name: 'systemctl daemon-reload' # noqa no-handler would require flush_handlers here anyway ansible.builtin.systemd: daemon_reload: true - when: '__curaline__unit_result is changed' - - tags: - - 'curaline' - - 'curaline:configure' - - -# its own block, because an update unpacks new files into bin/ that need the file context as well. -- block: - - # systemd starts the client from /usr/lib, where it needs the bin_t type to be allowed to - # execute it. - - name: 'semanage fcontext --add --type bin_t "{{ __curaline__install_dir }}/bin(/.*)?"' - community.general.sefcontext: - target: '{{ __curaline__install_dir }}/bin(/.*)?' - setype: 'bin_t' - state: 'present' - - - name: 'restorecon -RFv {{ __curaline__install_dir }}/bin' - ansible.builtin.command: 'restorecon -RFv {{ __curaline__install_dir }}/bin' - register: '__curaline__restorecon_result' - changed_when: '__curaline__restorecon_result["stdout"] | length > 0' - - when: - - 'ansible_facts["os_family"] == "RedHat"' - - 'ansible_facts["selinux"]["status"] != "disabled"' + when: '__curaline__dropin_result is changed' tags: - 'curaline' - 'curaline:configure' - - 'curaline:update' - block: @@ -310,4 +140,3 @@ - 'curaline' - 'curaline:configure' - 'curaline:state' - - 'curaline:update' diff --git a/roles/curaline/templates/etc/swisscom-schweiz-ag/cdr-client/logback-service.xml.j2 b/roles/curaline/templates/etc/swisscom-schweiz-ag/cdr-client/logback-service.xml.j2 deleted file mode 100644 index 3e3b04766..000000000 --- a/roles/curaline/templates/etc/swisscom-schweiz-ag/cdr-client/logback-service.xml.j2 +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - %d{ISO8601} [%thread] %-5level %logger{36} -%kvp- %msg%n - - - - - ${LOG_DIR}/cdr-client-service.log - - ${LOG_DIR}/cdr-client-service-%d{yyyy-MM-dd}.%i.log.gz - 100MB - 15 - 1GB - - - %d{ISO8601} [%thread] %-5level %logger{36} -%kvp- %msg%n - - - - - - - - - - - - - diff --git a/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf.j2 b/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf.j2 new file mode 100644 index 000000000..5842b5dc2 --- /dev/null +++ b/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.d/read-write-paths.conf.j2 @@ -0,0 +1,9 @@ +# {{ ansible_managed }} +# 2026102901 + +# the packaged unit runs the client with `ProtectSystem=strict`, so it may only write to its own +# directories and to the folders of its connectors listed here +[Service] +{% if __curaline__folders | length > 0 %} +ReadWritePaths="{{ __curaline__folders | join('" "') }}" +{% endif %} diff --git a/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.j2 b/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.j2 deleted file mode 100644 index 13d740cbc..000000000 --- a/roles/curaline/templates/etc/systemd/system/swisscom-schweiz-ag-cdr-client.service.j2 +++ /dev/null @@ -1,39 +0,0 @@ -# {{ ansible_managed }} -# 2026101602 - -# Based on the unit of the Debian package of the client, but with a static user instead of `DynamicUser=yes`, so that the client can run under an existing account such as `hnet`. The sandboxing that `DynamicUser=yes` implies is set explicitly. - -[Unit] -After=network-online.target -Description=curaLINE Client -Wants=network-online.target - -[Service] -CacheDirectory=swisscom-schweiz-ag/cdr-client -ConfigurationDirectory=swisscom-schweiz-ag/cdr-client -ConfigurationDirectoryMode=0750 -# independent of the home of the user, so the client never writes into an existing home such as /home/hnet -Environment=HOME={{ __curaline__state_dir }} -ExecStart={{ __curaline__install_dir }}/bin/cdr-client-service -Group={{ curaline__group }} -LogsDirectory=swisscom-schweiz-ag/cdr-client -LogsDirectoryMode=0750 -NoNewPrivileges=yes -PrivateTmp=yes -ProtectHome=read-only -ProtectSystem=strict -# the configuration directory is writable for `curaline__renew_credential` -ReadWritePaths="{{ ([__curaline__install_dir, __curaline__config_dir] + __curaline__folders) | join('" "') }}" -Restart=always -RestartSec=10s -RestrictSUIDSGID=yes -StateDirectory=swisscom-schweiz-ag/cdr-client -# the JVM exits with 143 on SIGTERM -SuccessExitStatus=143 -SyslogIdentifier=cdr-client -Type=exec -User={{ curaline__user }} -WorkingDirectory={{ __curaline__install_dir }} - -[Install] -WantedBy=multi-user.target diff --git a/roles/curaline/vars/Debian.yml b/roles/curaline/vars/Debian.yml deleted file mode 100644 index 919da92e6..000000000 --- a/roles/curaline/vars/Debian.yml +++ /dev/null @@ -1,2 +0,0 @@ -__curaline__required_packages: - - 'tar' diff --git a/roles/curaline/vars/RedHat.yml b/roles/curaline/vars/RedHat.yml deleted file mode 100644 index 831ebd17e..000000000 --- a/roles/curaline/vars/RedHat.yml +++ /dev/null @@ -1,3 +0,0 @@ -__curaline__required_packages: - - 'policycoreutils-python-utils' # semanage - - 'tar' diff --git a/roles/curaline/vars/Ubuntu.yml b/roles/curaline/vars/Ubuntu.yml deleted file mode 100644 index 919da92e6..000000000 --- a/roles/curaline/vars/Ubuntu.yml +++ /dev/null @@ -1,2 +0,0 @@ -__curaline__required_packages: - - 'tar' diff --git a/roles/curaline/vars/main.yml b/roles/curaline/vars/main.yml index dff30d953..571dbc8ae 100644 --- a/roles/curaline/vars/main.yml +++ b/roles/curaline/vars/main.yml @@ -1,7 +1,4 @@ __curaline__config_dir: '/etc/swisscom-schweiz-ag/cdr-client' -__curaline__install_dir: '/usr/lib/swisscom-schweiz-ag/cdr-client' -# created by systemd (`StateDirectory=`) and set as $HOME of the client, see the unit -__curaline__state_dir: '/var/lib/swisscom-schweiz-ag/cdr-client' # the connectors that are not removed. the ones without a `state` come first, since `rejectattr` # cannot test an attribute that is not there.