From 01719f2ec8ea21d754ddf58a86407bf3fcd74ec6 Mon Sep 17 00:00:00 2001 From: Christoph Maser Date: Sat, 2 Mar 2024 13:47:27 +0100 Subject: [PATCH] feat: add a cron like wrapper for timers This helper will allow to define a timer in a cron like way, and will setup a corresponding timer and service. In basic usage it only requires a command, user, timerspec and an ensure parameter. For additional features the parameter `service_overrides`, `timer_overrides`, `service_unit_overrides` and `timer_unit_overrides` can be used to define additional aspects of the create units. The correct order of the units is also ensured. fixes #374 related to https://github.com/voxpupuli/puppet-systemd/pull/287 --- REFERENCE.md | 151 +++++++++++++++++++++++- manifests/timer_wrapper.pp | 92 +++++++++++++++ spec/defines/timer_wrapper_spec.rb | 177 +++++++++++++++++++++++++++++ types/unit/timer.pp | 12 +- types/unit/timer/timerspec.pp | 4 + 5 files changed, 424 insertions(+), 12 deletions(-) create mode 100644 manifests/timer_wrapper.pp create mode 100644 spec/defines/timer_wrapper_spec.rb create mode 100644 types/unit/timer/timerspec.pp diff --git a/REFERENCE.md b/REFERENCE.md index d38a01ba..0a568227 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -36,6 +36,7 @@ * [`systemd::network`](#systemd--network): Creates network config for systemd-networkd * [`systemd::service_limits`](#systemd--service_limits): Adds a set of custom limits to the service * [`systemd::timer`](#systemd--timer): Create a timer and optionally a service unit to execute with the timer unit +* [`systemd::timer_wrapper`](#systemd--timer_wrapper): Helper to define timer and accompanying services for a given task (cron like interface). * [`systemd::tmpfile`](#systemd--tmpfile): Creates a systemd tmpfile * [`systemd::udev::rule`](#systemd--udev--rule): Adds a custom udev rule * [`systemd::unit_file`](#systemd--unit_file): Creates a systemd unit file @@ -67,6 +68,7 @@ * [`Systemd::Unit::Service::Exec`](#Systemd--Unit--Service--Exec): Possible strings for ExecStart, ExecStartPrep, ... * [`Systemd::Unit::Socket`](#Systemd--Unit--Socket): Possible keys for the [Socket] section of a unit file * [`Systemd::Unit::Timer`](#Systemd--Unit--Timer): Possible keys for the [Timer] section of a unit file +* [`Systemd::Unit::Timer::TimerSpec`](#Systemd--Unit--Timer--TimerSpec): Timer specification for systemd timers * [`Systemd::Unit::Unit`](#Systemd--Unit--Unit): Possible keys for the [Unit] section of a unit file ## Classes @@ -1677,6 +1679,134 @@ Call `systemd::daemon_reload` Default value: `true` +### `systemd::timer_wrapper` + +Helper to define timer and accompanying services for a given task (cron like interface). + +#### Parameters + +The following parameters are available in the `systemd::timer_wrapper` defined type: + +* [`ensure`](#-systemd--timer_wrapper--ensure) +* [`command`](#-systemd--timer_wrapper--command) +* [`user`](#-systemd--timer_wrapper--user) +* [`on_active_sec`](#-systemd--timer_wrapper--on_active_sec) +* [`on_boot_sec`](#-systemd--timer_wrapper--on_boot_sec) +* [`on_start_up_sec`](#-systemd--timer_wrapper--on_start_up_sec) +* [`on_unit_active_sec`](#-systemd--timer_wrapper--on_unit_active_sec) +* [`on_unit_inactive_sec`](#-systemd--timer_wrapper--on_unit_inactive_sec) +* [`on_calendar`](#-systemd--timer_wrapper--on_calendar) +* [`service_overrides`](#-systemd--timer_wrapper--service_overrides) +* [`timer_overrides`](#-systemd--timer_wrapper--timer_overrides) +* [`service_unit_overrides`](#-systemd--timer_wrapper--service_unit_overrides) +* [`timer_unit_overrides`](#-systemd--timer_wrapper--timer_unit_overrides) + +##### `ensure` + +Data type: `Enum['present', 'absent']` + +whether the timer and service should be present or absent + +##### `command` + +Data type: `Optional[String[1]]` + +the command for the systemd servie to execute + +Default value: `undef` + +##### `user` + +Data type: `Optional[String[1]]` + +the user to run the command as + +Default value: `undef` + +##### `on_active_sec` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +run service relative to the time when the timer was activated + +Default value: `undef` + +##### `on_boot_sec` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +run service relative to when the machine was booted + +Default value: `undef` + +##### `on_start_up_sec` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +run service relative to when the service manager was started + +Default value: `undef` + +##### `on_unit_active_sec` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +run service relative to when the unit was last activated + +Default value: `undef` + +##### `on_unit_inactive_sec` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +run service relative to when the unit was last deactivated + +Default value: `undef` + +##### `on_calendar` + +Data type: `Optional[Systemd::Unit::Timer::TimerSpec]` + +the calendar event expressions time to run the service + +Default value: `undef` + +##### `service_overrides` + +Data type: `Systemd::Unit::Service` + +override for the`[Service]` section of the service +example: `'service_overrides' => { 'Wants' => 'network-online.target' }` + +Default value: `{}` + +##### `timer_overrides` + +Data type: `Systemd::Unit::Timer` + +override for the`[Timer]` section of the timer +example: `'timer_overrides' => { 'OnBootSec' => '10' }` + +Default value: `{}` + +##### `service_unit_overrides` + +Data type: `Systemd::Unit::Unit` + +override for the`[Unit]` section of the service +example: `'service_unit_overrides' => { 'Wants' => 'network-online.target' }` + +Default value: `{}` + +##### `timer_unit_overrides` + +Data type: `Systemd::Unit::Unit` + +override for the `[Unit]` section of the timer +can not be used in combination with `additional_timer_params` + +Default value: `{}` + ### `systemd::tmpfile` Creates a systemd tmpfile @@ -2578,12 +2708,12 @@ Alias of ```puppet Struct[{ - Optional['OnActiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnBootSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnStartUpSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnUnitActiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnUnitInactiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnCalendar'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], + Optional['OnActiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnBootSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnStartUpSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnUnitActiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnUnitInactiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnCalendar'] => Systemd::Unit::Timer::TimerSpec, Optional['AccuracySec'] => Variant[Integer[0],String], Optional['RandomizedDelaySec'] => Variant[Integer[0],String], Optional['FixedRandomDelay'] => Boolean, @@ -2596,6 +2726,15 @@ Struct[{ }] ``` +### `Systemd::Unit::Timer::TimerSpec` + +Timer specification for systemd timers + +* **See also** + * https://www.freedesktop.org/software/systemd/man/systemd.timer.html + +Alias of `Variant[Integer[0], String, Array[Variant[Integer[0],String]]]` + ### `Systemd::Unit::Unit` Possible keys for the [Unit] section of a unit file diff --git a/manifests/timer_wrapper.pp b/manifests/timer_wrapper.pp new file mode 100644 index 00000000..81a761bc --- /dev/null +++ b/manifests/timer_wrapper.pp @@ -0,0 +1,92 @@ +# @summary +# Helper to define timer and accompanying services for a given task (cron like interface). +# @param ensure whether the timer and service should be present or absent +# @param command the command for the systemd servie to execute +# @param user the user to run the command as +# @param on_active_sec run service relative to the time when the timer was activated +# @param on_boot_sec run service relative to when the machine was booted +# @param on_start_up_sec run service relative to when the service manager was started +# @param on_unit_active_sec run service relative to when the unit was last activated +# @param on_unit_inactive_sec run service relative to when the unit was last deactivated +# @param on_calendar the calendar event expressions time to run the service +# @param service_overrides override for the`[Service]` section of the service +# example: `'service_overrides' => { 'Wants' => 'network-online.target' }` +# @param timer_overrides override for the`[Timer]` section of the timer +# example: `'timer_overrides' => { 'OnBootSec' => '10' }` +# @param service_unit_overrides override for the`[Unit]` section of the service +# example: `'service_unit_overrides' => { 'Wants' => 'network-online.target' }` +# @param timer_unit_overrides override for the `[Unit]` section of the timer +# can not be used in combination with `additional_timer_params` +define systemd::timer_wrapper ( + Enum['present', 'absent'] $ensure, + Optional[Systemd::Unit::Service::Exec] $command = undef, + Optional[String[1]] $user = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_active_sec = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_boot_sec = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_start_up_sec = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_unit_active_sec = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_unit_inactive_sec = undef, + Optional[Systemd::Unit::Timer::TimerSpec] $on_calendar = undef, + Systemd::Unit::Service $service_overrides = {}, + Systemd::Unit::Timer $timer_overrides = {}, + Systemd::Unit::Unit $timer_unit_overrides = {}, + Systemd::Unit::Unit $service_unit_overrides = {}, +) { + $_timer_spec = { + 'OnActiveSec' => $on_active_sec, + 'OnBootSec' => $on_boot_sec, + 'OnStartUpSec' => $on_start_up_sec, + 'OnUnitActiveSec' => $on_unit_active_sec, + 'OnUnitInactiveSec' => $on_unit_inactive_sec, + 'OnCalendar' => $on_calendar, + }.filter |$k, $v| { $v =~ NotUndef } + + if $ensure == 'present' { + if $_timer_spec == {} { + fail('At least one of on_active_sec, + on_boot_sec, + on_start_up_sec, + on_unit_active_sec, + on_unit_inactive_sec, + or on_calendar must be set' + ) + } + if ! $command { + fail('command must be set') + } + } + + $service_ensure = $ensure ? { 'absent' => false, default => true, } + $unit_name = systemd::escape($title) + + systemd::manage_unit { "${unit_name}.service": + ensure => $ensure, + unit_entry => $service_unit_overrides, + service_entry => { + 'ExecStart' => $command, # if ensure present command is defined is checked above + 'User' => $user, # defaults apply + 'Type' => 'oneshot', + }.filter |$key, $val| { $val =~ NotUndef } + $service_overrides, + } + systemd::manage_unit { "${unit_name}.timer": + ensure => $ensure, + unit_entry => $timer_unit_overrides, + timer_entry => $_timer_spec + $timer_overrides, + } + + service { "${unit_name}.timer": + ensure => $service_ensure, + enable => $service_ensure, + } + + if $ensure == 'present' { + Systemd::Manage_unit["${unit_name}.service"] + -> Systemd::Manage_unit["${unit_name}.timer"] + -> Service["${unit_name}.timer"] + } else { + # Ensure the timer is stopped and disabled before the service + Service["${unit_name}.timer"] + -> Systemd::Manage_unit["${unit_name}.timer"] + -> Systemd::Manage_unit["${unit_name}.service"] + } +} diff --git a/spec/defines/timer_wrapper_spec.rb b/spec/defines/timer_wrapper_spec.rb new file mode 100644 index 00000000..2c70c877 --- /dev/null +++ b/spec/defines/timer_wrapper_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'systemd::timer_wrapper' do + context 'supported operating systems' do + on_supported_os.each do |os, facts| + context "on #{os}" do + let(:facts) { facts } + let(:title) { 'timer_name' } + + context 'simple usage' do + let :params do + { + ensure: 'present', + on_calendar: '*:0/10', + command: '/bin/date', + user: 'root', + } + end + + it do + is_expected.to compile.with_all_deps + is_expected.to contain_file("/etc/systemd/system/#{title}.service"). + with_content(%r{# Deployed with puppet}). + with_content(%r{Type=oneshot}). + with_content(%r{ExecStart=/bin/date}). + with_content(%r{Type=oneshot}). + with_content(%r{User=root}) + is_expected.to contain_file("/etc/systemd/system/#{title}.timer"). + with_content(%r{OnCalendar=\*:0/10}) + is_expected.to contain_Systemd__Unit_file("#{title}.service"). + that_comes_before("Systemd::Unit_file[#{title}.timer]") + is_expected.to contain_Systemd__Unit_file("#{title}.service"). + that_comes_before("Systemd::Unit_file[#{title}.timer]") + is_expected.to contain_Exec("systemd-#{title}.service-systemctl-daemon-reload") + is_expected.to contain_Exec("systemd-#{title}.timer-systemctl-daemon-reload") + is_expected.to contain_Service("#{title}.timer") + is_expected.to contain_Systemd__Daemon_reload("#{title}.service") + is_expected.to contain_Systemd__Daemon_reload("#{title}.timer") + is_expected.to contain_Systemd__Manage_unit("#{title}.service") + is_expected.to contain_Systemd__Manage_unit("#{title}.timer") + is_expected.to contain_Systemd__Unit_file("#{title}.timer") + end + end + + context 'failue when not passing calendar spec' do + let :params do + { + ensure: 'present', + command: '/bin/date', + user: 'root', + } + end + + it do + is_expected.to compile.and_raise_error(%r{At least one of on_active_sec}) + end + end + + context 'with / in title' do + let :title do + 't/i/t/l/e' + end + let :params do + { + ensure: 'present', + on_calendar: '*:0/10', + command: '/bin/true', + user: 'root', + } + end + + it { + is_expected.to compile + is_expected.to contain_file('/etc/systemd/system/t-i-t-l-e.timer') + is_expected.to contain_systemd__manage_unit('t-i-t-l-e.timer') + is_expected.to contain_systemd__unit_file('t-i-t-l-e.timer') + is_expected.to contain_file('/etc/systemd/system/t-i-t-l-e.service') + is_expected.to contain_systemd__manage_unit('t-i-t-l-e.service') + is_expected.to contain_systemd__unit_file('t-i-t-l-e.service') + is_expected.to contain_service('t-i-t-l-e.timer') + is_expected.to contain_exec('systemd-t-i-t-l-e.service-systemctl-daemon-reload') + is_expected.to contain_exec('systemd-t-i-t-l-e.timer-systemctl-daemon-reload') + is_expected.to contain_Systemd__Daemon_reload('t-i-t-l-e.service') + is_expected.to contain_Systemd__Daemon_reload('t-i-t-l-e.timer') + } + end + + context 'ensure absent' do + let :params do + { + ensure: 'absent', + } + end + + it { + is_expected.to contain_Systemd__Manage_unit("#{title}.timer") + is_expected.to contain_Systemd__Manage_unit("#{title}.service") + is_expected.to contain_Service("#{title}.timer"). + that_comes_before("Systemd::Unit_file[#{title}.timer]") + is_expected.to contain_Systemd__Unit_file("#{title}.timer"). + that_comes_before("Systemd::Unit_file[#{title}.service]") + } + end + + context 'applies service_overrides' do + let :params do + { + ensure: 'present', + command: 'date', + service_overrides: { 'Group' => 'bob' }, + on_boot_sec: 100, + user: 'root', + } + end + + it { + is_expected.to contain_file("/etc/systemd/system/#{title}.service"). + with_content(%r{Group=bob}) + } + end + + context 'applies service_unit_overrides' do + let :params do + { + ensure: 'present', + command: 'date', + service_unit_overrides: { 'Wants' => 'network-online.target' }, + on_boot_sec: 100, + user: 'root', + } + end + + it { + is_expected.to contain_file("/etc/systemd/system/#{title}.service"). + with_content(%r{Wants=network-online.target}) + } + end + + context 'applies timer_overrides' do + let :params do + { + ensure: 'present', + command: 'date', + timer_overrides: { 'OnBootSec' => '200' }, + on_boot_sec: 100, + user: 'root', + } + end + + it { + is_expected.to contain_file("/etc/systemd/system/#{title}.timer"). + with_content(%r{OnBootSec=200}) + } + end + + context 'applies timer_unit_overrides' do + let :params do + { + ensure: 'present', + command: 'date', + timer_unit_overrides: { 'Wants' => 'network-online.target' }, + on_boot_sec: 100, + user: 'root', + } + end + + it { + is_expected.to contain_file("/etc/systemd/system/#{title}.timer"). + with_content(%r{Wants=network-online.target}) + } + end + end + end + end +end diff --git a/types/unit/timer.pp b/types/unit/timer.pp index d3c927d8..e0412af1 100644 --- a/types/unit/timer.pp +++ b/types/unit/timer.pp @@ -3,12 +3,12 @@ # type Systemd::Unit::Timer = Struct[ { - Optional['OnActiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnBootSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnStartUpSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnUnitActiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnUnitInactiveSec'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], - Optional['OnCalendar'] => Variant[Integer[0],String,Array[Variant[Integer[0],String]]], + Optional['OnActiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnBootSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnStartUpSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnUnitActiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnUnitInactiveSec'] => Systemd::Unit::Timer::TimerSpec, + Optional['OnCalendar'] => Systemd::Unit::Timer::TimerSpec, Optional['AccuracySec'] => Variant[Integer[0],String], Optional['RandomizedDelaySec'] => Variant[Integer[0],String], Optional['FixedRandomDelay'] => Boolean, diff --git a/types/unit/timer/timerspec.pp b/types/unit/timer/timerspec.pp new file mode 100644 index 00000000..3ea8fc04 --- /dev/null +++ b/types/unit/timer/timerspec.pp @@ -0,0 +1,4 @@ +# @summary Timer specification for systemd timers +# @see https://www.freedesktop.org/software/systemd/man/systemd.timer.html +# +type Systemd::Unit::Timer::TimerSpec = Variant[Integer[0],String,Array[Variant[Integer[0],String]]]