diff --git a/hieradata/role/tma.yaml b/hieradata/role/tma.yaml new file mode 100644 index 0000000000..0b4a2008aa --- /dev/null +++ b/hieradata/role/tma.yaml @@ -0,0 +1,26 @@ +--- +classes: + - "profile::core::common" + - "profile::core::debugutils" + - "profile::core::docker" + - "profile::ts::tma" + +packages: + - "git" + - "git-lfs" + - "perl-File-Copy" + - "unzip" + +profile::ts::tma::tma_db_repo: "git@github.com:lsst-ts/ts_tma_mariadb-docker.git" +profile::ts::tma::tma_db_path: "/opt/tma/mariadb-docker" +profile::ts::tma::opman_path: "/opt/tma/operation-manager" +profile::ts::tma::pxi_0_ip: "10.0.0.10" +profile::ts::tma::pxi_1_ip: "10.0.0.11" + +profile::ts::tma::labview_rpm_url: "https://repo-nexus.lsst.org/nexus/repository/ts_yum/releases/ni-labview-2024-pro-24.3.2.49152-0%2Bf0-rhel9.noarch.rpm" +profile::ts::tma::vipm_url: "https://repo-nexus.lsst.org/nexus/repository/tma_artifacts/labview/vipm-22.1.2354-linux.zip" +profile::ts::tma::vipm_root: "/usr/local/JKI/VIPM" +profile::ts::tma::vipc_path: "/usr/local/JKI/VIPM/LSST_tma_dependencies.vipc" +profile::ts::tma::vipc_url: "https://github.com/lsst-ts/ts_tma_vipm_dependency/raw/develop/LSST_tma_dependencies.vipc" + +profile::ts::tma::enable_graphical: true diff --git a/site/profile/manifests/ts/tma.pp b/site/profile/manifests/ts/tma.pp new file mode 100644 index 0000000000..685553b09b --- /dev/null +++ b/site/profile/manifests/ts/tma.pp @@ -0,0 +1,232 @@ +# @summary +# TMA multi-user workstation: XFCE, Docker services, LabVIEW, VIPM +# +# @param tma_db_repo TMA MariaDB repository +# @param tma_db_path Path for TMA DB (shared service) +# @param github_token GitHub Personal Access Token for private repos (optional) +# @param pxi_0_ip PXI 0 IP address +# @param pxi_1_ip PXI 1 IP address +# @param opman_path Operation Manager path (shared service) +# @param labview_rpm_url Full URL to NI LabVIEW RPM +# @param compose_cmd Docker compose command (docker-compose or docker compose) +# @param ghcr_username GitHub Container Registry username +# @param ghcr_token GitHub Container Registry token (Sensitive) +# @param vipm_url VIPM ZIP download URL +# @param vipc_url VIPC dependencies URL +# @param vipm_root VIPM root installation path +# @param vipc_path VIPC file destination path +# @param enable_graphical Enable graphical mode (XFCE + GDM) +# @param tma_group LDAP group name for TMA users (must exist in FreeIPA) +class profile::ts::tma ( + String[1] $tma_db_repo, + Stdlib::Absolutepath $tma_db_path, + String[1] $pxi_0_ip, + String[1] $pxi_1_ip, + Optional[Sensitive[String[1]]] $github_token = undef, + Stdlib::Absolutepath $opman_path = '/opt/tma/operation-manager', + String[1] $labview_rpm_url = 'https://repo-nexus.lsst.org/nexus/repository/ts_yum/releases/ni-labview-2024-pro-24.3.2.49152-0%2Bf0-rhel9.noarch.rpm', + String[1] $compose_cmd = 'docker-compose', + Optional[String[1]] $ghcr_username = undef, + Optional[Sensitive[String[1]]] $ghcr_token = undef, + Optional[String[1]] $vipm_url = undef, + String[1] $vipc_url = 'https://github.com/lsst-ts/ts_tma_vipm_dependency/raw/develop/LSST_tma_dependencies.vipc', + Stdlib::Absolutepath $vipm_root = '/usr/local/JKI/VIPM', + Stdlib::Absolutepath $vipc_path = '/usr/local/JKI/VIPM/LSST_tma_dependencies.vipc', + Boolean $enable_graphical = true, + String[1] $tma_group = 'tma', +) { + if $enable_graphical { + ensure_packages(['@base-x', '@xfce-desktop']) + + exec { 'set-graphical-target': + command => '/bin/systemctl set-default graphical.target', + unless => '/bin/systemctl get-default | grep -q graphical.target', + onlyif => '/bin/systemctl is-active sssd', + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin'], + require => [Package['@base-x'], Package['@xfce-desktop']], + } + + file { '/etc/gdm/custom.conf': + ensure => file, + owner => 'root', + group => 'root', + mode => '0644', + content => epp('profile/ts/tma/gdm-custom.conf.epp'), + notify => Service['gdm'], + } + + service { 'gdm': + ensure => running, + enable => true, + require => [Exec['set-graphical-target'], Package['@xfce-desktop']], + } + } + + if $ghcr_username != undef and $ghcr_token != undef { + exec { 'docker-login-ghcr': + command => "bash -lc 'printf %s ${ghcr_token.unwrap} | docker login ghcr.io -u ${ghcr_username} --password-stdin'", + unless => "bash -lc 'docker info 2>/dev/null | grep -q ghcr.io'", + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin'], + } + } + + file { '/opt/tma': + ensure => directory, + owner => 'root', + group => $tma_group, + mode => '0775', + } + + if $github_token != undef { + file { $tma_db_path: + ensure => directory, + owner => 'root', + group => $tma_group, + mode => '2775', + require => File['/opt/tma'], + } + + $tma_db_source = regsubst($tma_db_repo, '^git@github\.com:', "https://${github_token.unwrap}@github.com/") + + vcsrepo { $tma_db_path: + ensure => present, + provider => git, + source => $tma_db_source, + require => File[$tma_db_path], + } + + file { "${tma_db_path}/backup": + ensure => directory, + owner => 'root', + group => $tma_group, + mode => '2775', + } + + exec { 'tma-db-up': + command => "${compose_cmd} up -d", + cwd => $tma_db_path, + refreshonly => true, + subscribe => Vcsrepo[$tma_db_path], + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin', '/usr/local/bin'], + } + + cron { 'tma-db-createbackup': + ensure => present, + command => "${tma_db_path}/createbackup.pl", + minute => '5', + hour => '12', + user => 'root', + } + + cron { 'tma-db-python-backup': + ensure => present, + command => "docker run --rm -v ${tma_db_path}/python:/script -v ${tma_db_path}/backup:/backup python:3.7 python /script/main.py", + minute => '5', + hour => '13', + user => 'root', + } + } else { + notify { 'tma-db-skip': + message => 'TMA DB skip: no GitHub token', + } + } + + file { $opman_path: + ensure => directory, + owner => 'root', + group => $tma_group, + mode => '2775', + require => File['/opt/tma'], + } + + $compose_content = @("COMPOSE") + version: '3' + services: + mt-mount-manager: + image: ghcr.io/lsst-ts/ts_tma_operation-manager_mt-mount-operation-manager:latest + container_name: mt-mount-manager + ports: + - "60005:60005" + - "40005:40005" + - "30005:30005" + volumes: + - /var/log/mtmount_operation_manager/:/var/log/mtmount_operation_manager + environment: + - PXI_0_IP=${pxi_0_ip} + - PXI_1_IP=${pxi_1_ip} + restart: unless-stopped + | COMPOSE + + file { "${opman_path}/docker-compose.yml": + ensure => file, + owner => 'root', + group => $tma_group, + mode => '0664', + content => $compose_content, + notify => Exec['opman-up'], + } + + exec { 'opman-up': + command => "${compose_cmd} up -d", + cwd => $opman_path, + refreshonly => true, + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin', '/usr/local/bin'], + } + + yumrepo { 'ni-labview-2024-el9-pro': + ensure => present, + baseurl => 'https://download.ni.com/ni-linux-desktop/LabVIEW/2024/Q3/f2/pro/rpm/ni-labview-2024/el9', + enabled => 1, + gpgcheck => 0, + repo_gpgcheck => 0, + before => Package['ni-labview-2024-pro'], + } + + package { 'ni-labview-2024-pro': + ensure => '24.3.2.49152-0+f0', + } + + file { ['/usr/local/JKI', '/etc/JKI']: + ensure => directory, + owner => 0, + group => 0, + mode => '0755', + } + + file { $vipm_root: + ensure => directory, + owner => 0, + group => 0, + mode => '0755', + require => File['/usr/local/JKI'], + } + + if $vipm_url != undef { + exec { 'vipm-download': + command => "curl -fsSL -o /tmp/vipm.zip ${vipm_url}", + unless => "test -x ${vipm_root}/vipm", + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin'], + require => File[$vipm_root], + } + + exec { 'vipm-unzip': + command => "unzip -o /tmp/vipm.zip -d ${vipm_root} && rm -f /tmp/vipm.zip", + unless => "test -x ${vipm_root}/vipm", + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin'], + require => Exec['vipm-download'], + } + } + + if $github_token != undef { + exec { 'vipc-fetch': + command => "bash -lc 'curl -fsSL -H \"Authorization: token ${github_token.unwrap}\" -o ${vipc_path} ${vipc_url}'", + creates => $vipc_path, + path => ['/bin', '/usr/bin', '/sbin', '/usr/sbin'], + require => File[$vipm_root], + } + } else { + notify { 'vipc-skip': + message => 'VIPC skip: no GitHub token', + } + } +} diff --git a/site/profile/templates/ts/tma/gdm-custom.conf.epp b/site/profile/templates/ts/tma/gdm-custom.conf.epp new file mode 100644 index 0000000000..5eb1e665c8 --- /dev/null +++ b/site/profile/templates/ts/tma/gdm-custom.conf.epp @@ -0,0 +1,12 @@ +[daemon] +WaylandEnable=false +DefaultSession=xfce.desktop + +[security] + +[xdmcp] + +[chooser] + +[debug] + diff --git a/spec/classes/ts/tma_spec.rb b/spec/classes/ts/tma_spec.rb new file mode 100644 index 0000000000..15352868cd --- /dev/null +++ b/spec/classes/ts/tma_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'profile::ts::tma' do + on_supported_os.each do |os, os_facts| + next unless os =~ %r{almalinux-9} + + context "on #{os}" do + let(:facts) { os_facts } + + let(:params) do + { + tma_db_repo: 'git@github.com:lsst-ts/ts_tma_mariadb-docker.git', + tma_db_path: '/opt/tma/mariadb-docker', + pxi_0_ip: '10.0.0.10', + pxi_1_ip: '10.0.0.11', + } + end + + it { is_expected.to compile.with_all_deps } + + it 'enables graphical mode by default' do + is_expected.to contain_exec('set-graphical-target') + .with_command('/bin/systemctl set-default graphical.target') + .with_unless('/bin/systemctl get-default | grep -q graphical.target') + .with_onlyif('/bin/systemctl is-active sssd') + .that_requires('Package[@base-x]') + .that_requires('Package[@xfce-desktop]') + + is_expected.to contain_file('/etc/gdm/custom.conf') + .with(ensure: 'file', owner: 'root', group: 'root', mode: '0644') + .with_content(%r{WaylandEnable=false}) + .with_content(%r{DefaultSession=xfce\.desktop}) + .that_notifies('Service[gdm]') + + is_expected.to contain_service('gdm') + .with_ensure('running') + .with_enable(true) + .that_requires('Exec[set-graphical-target]') + .that_requires('Package[@xfce-desktop]') + end + + it 'creates base TMA directory with group ownership' do + is_expected.to contain_file('/opt/tma') + .with(ensure: 'directory', owner: 'root', group: 'tma', mode: '0775') + end + + it 'creates op-manager directory with group ownership' do + is_expected.to contain_file('/opt/tma/operation-manager') + .with(ensure: 'directory', owner: 'root', group: 'tma', mode: '2775') + end + + it 'writes op-manager compose file and up exec' do + is_expected.to contain_file('/opt/tma/operation-manager/docker-compose.yml') + .with(ensure: 'file', owner: 'root', group: 'tma', mode: '0664') + .with_content(%r{PXI_0_IP=10\.0\.0\.10}) + .with_content(%r{PXI_1_IP=10\.0\.0\.11}) + + is_expected.to contain_exec('opman-up') + .with_command(%r{docker-compose up -d}) + .with_cwd('/opt/tma/operation-manager') + .that_subscribes_to('File[/opt/tma/operation-manager/docker-compose.yml]') + end + + context 'when github_token is not provided' do + it 'skips TMA DB setup' do + is_expected.not_to contain_vcsrepo('/opt/tma/mariadb-docker') + is_expected.to contain_notify('tma-db-skip') + .with_message('TMA DB skip: no GitHub token') + end + + it 'skips VIPC download' do + is_expected.not_to contain_exec('vipc-fetch') + is_expected.to contain_notify('vipc-skip') + .with_message('VIPC skip: no GitHub token') + end + end + + context 'when github_token is provided' do + let(:params) do + super().merge( + github_token: sensitive('ghp_test_token_12345') + ) + end + + it 'manages the DB repo and backup dir with group ownership' do + is_expected.to contain_file('/opt/tma/mariadb-docker') + .with(ensure: 'directory', owner: 'root', group: 'tma', mode: '2775') + + is_expected.to contain_vcsrepo('/opt/tma/mariadb-docker') + .with( + ensure: 'present', + provider: 'git' + ) + .with_source(%r{^https://.*@github\.com/lsst-ts/ts_tma_mariadb-docker\.git$}) + + is_expected.to contain_file('/opt/tma/mariadb-docker/backup') + .with(ensure: 'directory', owner: 'root', group: 'tma', mode: '2775') + end + + it 'configures backup cron jobs to run as root' do + is_expected.to contain_cron('tma-db-createbackup') + .with_user('root') + .with_command(%r{/opt/tma/mariadb-docker/createbackup\.pl}) + + is_expected.to contain_cron('tma-db-python-backup') + .with_user('root') + end + + it 'downloads VIPC file using GitHub token' do + is_expected.to contain_exec('vipc-fetch') + .with_command(%r{Authorization: token}) + .with_creates('/usr/local/JKI/VIPM/LSST_tma_dependencies.vipc') + end + end + + it 'creates LabVIEW yum repository' do + is_expected.to contain_yumrepo('ni-labview-2024-el9-pro') + .with( + ensure: 'present', + baseurl: 'https://download.ni.com/ni-linux-desktop/LabVIEW/2024/Q3/f2/pro/rpm/ni-labview-2024/el9', + enabled: 1, + gpgcheck: 0, + repo_gpgcheck: 0 + ) + .that_comes_before('Package[ni-labview-2024-pro]') + end + + it 'installs LabVIEW Pro package from the repository' do + is_expected.to contain_package('ni-labview-2024-pro') + .with_ensure('24.3.2.49152-0+f0') + end + + it 'creates JKI directories for VIPM installation' do + ['/usr/local/JKI', '/etc/JKI'].each do |dir| + is_expected.to contain_file(dir) + .with(ensure: 'directory', owner: 0, group: 0, mode: '0755') + end + + is_expected.to contain_file('/usr/local/JKI/VIPM') + .with(ensure: 'directory', owner: 0, group: 0, mode: '0755') + .that_requires('File[/usr/local/JKI]') + end + + context 'when enable_graphical => false' do + let(:params) do + super().merge( + enable_graphical: false + ) + end + + it 'does not configure graphical mode' do + is_expected.not_to contain_exec('set-graphical-target') + is_expected.not_to contain_file('/etc/gdm/custom.conf') + is_expected.not_to contain_service('gdm') + end + end + + context 'when vipm_url => undef' do + let(:params) do + super().merge( + vipm_url: :undef + ) + end + + it 'does not manage VIPM download or unzip' do + is_expected.not_to contain_exec('vipm-download') + is_expected.not_to contain_exec('vipm-unzip') + end + end + + context 'when vipm_url is provided' do + let(:params) do + super().merge( + vipm_url: 'https://repo-nexus.lsst.org/nexus/repository/tma_artifacts/labview/vipm-22.1.2354-linux.zip' + ) + end + + it 'downloads and unpacks VIPM into vipm_root' do + is_expected.to contain_exec('vipm-download') + .with_command(%r{curl -fsSL -o /tmp/vipm\.zip https://repo-nexus\.lsst\.org/}) + .with_unless(%r{test -x /usr/local/JKI/VIPM/vipm}) + .that_requires('File[/usr/local/JKI/VIPM]') + + is_expected.to contain_exec('vipm-unzip') + .with_command(%r{unzip -o /tmp/vipm\.zip -d /usr/local/JKI/VIPM}) + .with_unless(%r{test -x /usr/local/JKI/VIPM/vipm}) + .that_requires('Exec[vipm-download]') + end + end + end + end +end diff --git a/spec/hosts/roles/tma_spec.rb b/spec/hosts/roles/tma_spec.rb new file mode 100644 index 0000000000..c83eda825a --- /dev/null +++ b/spec/hosts/roles/tma_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' + +role = 'tma' + +describe "#{role} role" do + on_supported_os.each do |os, os_facts| + next unless os =~ %r{almalinux-9-x86_64} + + context "on #{os}" do + lsst_sites.each do |site| + describe "#{role}.#{site}.lsst.org", :sitepp do + let(:node_params) { { role:, site: } } + let(:facts) { lsst_override_facts(os_facts) } + + it { is_expected.to compile.with_all_deps } + + include_examples 'debugutils' + include_examples('common', os_facts:, site:) + + it { is_expected.to contain_class('profile::core::docker') } + it { is_expected.to contain_class('profile::ts::tma') } + + %w[@base-x @xfce-desktop git git-lfs perl-File-Copy unzip].each do |pkg| + it { is_expected.to contain_package(pkg) } + end + end + end + end + end +end