diff --git a/README.md b/README.md index 9842431..d96a9d6 100644 --- a/README.md +++ b/README.md @@ -1,179 +1,182 @@ # Unattended Upgrades module for Puppet [![Build Status](https://travis-ci.org/voxpupuli/puppet-unattended_upgrades.png?branch=master)](https://travis-ci.org/voxpupuli/puppet-unattended_upgrades) [![Puppet Forge](https://img.shields.io/puppetforge/v/puppet/unattended_upgrades.svg)](https://forge.puppetlabs.com/puppet/unattended_upgrades) [![Puppet Forge - downloads](https://img.shields.io/puppetforge/dt/puppet/unattended_upgrades.svg)](https://forge.puppetlabs.com/puppet/unattended_upgrades) [![Puppet Forge - endorsement](https://img.shields.io/puppetforge/e/puppet/unattended_upgrades.svg)](https://forge.puppetlabs.com/puppet/unattended_upgrades) [![Puppet Forge - scores](https://img.shields.io/puppetforge/f/puppet/unattended_upgrades.svg)](https://forge.puppetlabs.com/puppet/unattended_upgrades) #### Table of Contents 1. [Overview](#overview) 1. [Module Description](#module-description) 1. [Setup](#setup) 1. [Usage](#usage) 1. [Reference](#reference) * [Classes](#classes) * [Parameters](#parameters) 1. [Limitations - OS compatibility, etc.](#limitations) 1. [License](#license) ## Overview The unattended\_upgrades module allows for the installation and configuration of automatic security (and other) updates through apt. This functionality used to be part of the puppetlabs-apt module but was split off into its own module. ## Module Description The unattended\_upgrades module automates the configuration of apt package updates. ## Setup ### What unattended\_upgrades affects * Package/configuration for unattended\_upgrades ### Beginning with unattended\_upgrades All you need to do is include the apt module, `include apt`, and this module, `include unattended_upgrades` for it to work. This module relies on the [apt](https://forge.puppetlabs.com/puppetlabs/apt) module and will not work without it. ## Usage Using unattended\_upgrades simply consists of including the module and if needed altering some of the default settings. ## Reference ### Classes * `unattended_upgrades`: Main class, installs the necessary packages and writes the configuration. ### Parameters #### unattended\_upgrades * `age` (`{}`): A hash of settings with two possible keys: * `min` (`2`): Minimum age of a cache package file. File younger than `min` will not be deleted. * `max` (`0`): Maximum allowed age of a cache package file. File older than `max` will be deleted. Any of these keys can be specified and will be merged into the defaults: ```puppet class { 'unattended_upgrades': age => { 'max' => 10 }, } ``` * `auto` (`{}`): A hash of settings with these possible keys: - * `clean`(`0`): Remove packages that can no longer be downloaded from cache every - X days (`0` = disabled). + * `clean`(`0`): Remove packages that can no longer be downloaded from cache every X days (`0` = disabled). + Also supports 'always' as value to execute this operation every time the script is executed. * `fix_interrupted_dpkg`(`true`): Try to fix package installation state. * `reboot`(`false`): Reboot system after package update installation. * `reboot_time`(`now`): If automatic reboot is enabled and needed, reboot at the specific time (instead of immediately). Expects a string in the format "HH:MM", using the 24 hour clock with leading zeros. Examples: "16:37" for 37 minutes past 4PM, or "02:03" for 3 minutes past 2AM. * `remove`(`true`): Remove unneeded dependencies after update installation. Any of these keys can be specified and will be merged into the defaults: ```puppet class { 'unattended_upgrades': auto => { 'reboot' => true }, } ``` * `backup` (`{}`): A hash with two possible keys: * `archive_interval` (`0`): Backup after n-days if archive contents changed. * `level` (`3`): Backup level. Any of these keys can be specified and will be merged into the defaults: ```puppet class { 'unattended_upgrades': backup => { 'level' => 5 }, } ``` * `blacklist`(`[]`): A list of packages to **not** automatically upgrade. * `dl_limit`(`undef`): Use a bandwidth limit for downloading, specified in kb/sec. * `enable` (`1`): Enable the automatic installation of updates. * `install_on_shutdown` (`false`): Install updates on shutdown instead of in the background. * `mail`: A hash to configure email behaviour with the following possible keys: * `report` (`undef`): Possible values are "always", "only-on-error" or "on-change". Defaults to "on-change". Note that "never" is achieved by not setting any `to` address. * `only_on_error` (`true`): Only send mail when something went wrong. Deprecated in unattended-upgrades 1.13 and newer in favor of `report`. * `to` (`undef`): Email address to send email too If the default for `to` is kept you will not receive any mail at all. You'll likely want to set this parameter. Any of these keys can be specified and will be merged into the defaults: ```puppet class { 'unattended_upgrades': mail => { 'to' => 'admin@domain.tld', }, } ``` * `minimal_steps` (`true`): Split the upgrade process into sections to allow shutdown during upgrade. * `origins`: The repositories from which to automatically upgrade included packages. * `extra_origins`: Additional repositories from which upgrades should be included. Can be used, if the default `origins` should be kept. * `package_ensure` (`installed`): The ensure state for the 'unattended-upgrades' package. * `random_sleep` (`undef`): Maximum amount of time (in seconds) that the apt cron job can sleep before the execution. The exact amount of time will be random but up to the value specified. The purpose is to avoid that servers/mirrors get hammered at exactly the same time when a lot of machines are switched on, e.g. 9:00 in the morning. Note: If this is left unset, the default value in the apt cron job applies, which is 1800 seconds. * `size` (`0`): Maximum size of the cache in MB. * `update` (`1`): Do "apt-get update" automatically every n-days. + Also supports 'always' as value to execute this operation every time the script is executed. * `upgrade` (`1`): Run the "unattended-upgrade" security upgrade script every n-days. + Also supports 'always' as value to execute this operation every time the script is executed. * `days` (`[]`): Set the days of the week that updates should be applied. The days can be specified as localized abbreviated or full names. Or as integers where "0" is Sunday, "1" is Monday etc. * `upgradeable_packages` (`{}`): A hash with two possible keys: * `download_only` (`0`): Do "apt-get upgrade --download-only" every n-days. + Also supports 'always' as value to execute this operation every time the script is executed. * `debdelta` (`1`): Use debdelta-upgrade to download updates if available. Any of these keys can be specified and will be merged into the defaults: ```puppet class { 'unattended_upgrades': upgradeable_packages => { 'debdelta' => 1, }, } ``` * `verbose` (`0`): Send report mail to root. * `options` (`{}`): A hash of settings with these possible keys: * `force_confdef` (`true`) : Use the default option for new config files if one is available, don't prompt. If no default can be found, you will be prompted unless one of the confold or confnew options is also given * `force_confold` (`true`): Always use the old config files, don't prompt * `force_confnew` (`false`): Always use the new config files, don't prompt * `force_confmiss` (`false`): Always install missing config files * `remove_new_unused_deps` (`undef`): Automatic removal of newly unused dependencies after the upgrade. * `remove_unused_kernel` (`undef`): Remove unused automatically installed kernel-related packages. * `syslog_enable` (`undef`): Enable logging to syslog. Default is False. * `syslog_facility` (`undef`): Specify syslog facility. Default is `daemon`. ## Limitations This module should work across all versions of Debian, Ubuntu, and Linux Mint. ## License The original code for this module comes from Evolving Web and was licensed under the MIT license. Code added since the fork of that module into puppetlabs-apt is covered under the Apache License version 2 as is any code added since it was split off into this separate unattended\_upgrades module. The LICENSE contains both licenses. diff --git a/manifests/init.pp b/manifests/init.pp index ab4d2b4..2d30dcd 100644 --- a/manifests/init.pp +++ b/manifests/init.pp @@ -1,84 +1,84 @@ class unattended_upgrades ( Unattended_upgrades::Age $age = {}, Unattended_upgrades::Auto $auto = {}, Unattended_upgrades::Backup $backup = {}, Array[String[1]] $blacklist = [], Array[String[1]] $whitelist = [], Optional[Integer[0]] $dl_limit = undef, Integer[0, 1] $enable = 1, Boolean $install_on_shutdown = false, Unattended_upgrades::Mail $mail = {}, Boolean $minimal_steps = true, Array[Unattended_upgrades::Origin] $origins = $unattended_upgrades::params::origins, String[1] $package_ensure = installed, Array[String[1]] $extra_origins = [], Optional[Integer[0]] $random_sleep = undef, Optional[String] $sender = undef, Integer[0] $size = 0, - Integer[0] $update = 1, - Integer[0] $upgrade = 1, + Variant[Integer[0], Enum['always']] $update = 1, + Variant[Integer[0], Enum['always']] $upgrade = 1, Unattended_upgrades::Upgradeable_packages $upgradeable_packages = {}, Integer[0] $verbose = 0, Boolean $notify_update = false, Unattended_upgrades::Options $options = {}, Array[String[1]] $days = [], Optional[Boolean] $remove_unused_kernel = undef, Optional[Boolean] $remove_new_unused_deps = undef, Optional[Boolean] $syslog_enable = undef, Optional[String] $syslog_facility = undef, ) inherits unattended_upgrades::params { # apt::conf settings require the apt class to work include apt $_age = merge($unattended_upgrades::default_age, $age) assert_type(Unattended_upgrades::Age, $_age) $_auto = merge($unattended_upgrades::default_auto, $auto) assert_type(Unattended_upgrades::Auto, $_auto) $_backup = merge($unattended_upgrades::default_backup, $backup) assert_type(Unattended_upgrades::Backup, $_backup) $_mail = merge($unattended_upgrades::default_mail, $mail) assert_type(Unattended_upgrades::Mail, $_mail) $_upgradeable_packages = merge($unattended_upgrades::default_upgradeable_packages, $upgradeable_packages) assert_type(Unattended_upgrades::Upgradeable_packages, $_upgradeable_packages) if $options != {} { warning('passing "options" is deprecated, use apt::conf directly instead') } $_options = merge($unattended_upgrades::default_options, $options) assert_type(Unattended_upgrades::Options, $_options) package { 'unattended-upgrades': ensure => $package_ensure, } apt::conf { 'unattended-upgrades': priority => 50, content => template("${module_name}/unattended-upgrades.erb"), require => Package['unattended-upgrades'], notify_update => $notify_update, } apt::conf { 'periodic': priority => 10, content => template("${module_name}/periodic.erb"), require => Package['unattended-upgrades'], notify_update => $notify_update, } apt::conf { 'auto-upgrades': ensure => absent, priority => 20, require => Package['unattended-upgrades'], notify_update => $notify_update, } apt::conf { 'options': priority => 10, content => template("${module_name}/options.erb"), require => Package['unattended-upgrades'], notify_update => $notify_update, } } diff --git a/spec/classes/unattended_upgrades_spec.rb b/spec/classes/unattended_upgrades_spec.rb index 8930ab0..627a626 100644 --- a/spec/classes/unattended_upgrades_spec.rb +++ b/spec/classes/unattended_upgrades_spec.rb @@ -1,384 +1,439 @@ require 'spec_helper' # rubocop:disable Style/RegexpLiteral describe 'unattended_upgrades' do let(:file_unattended) { '/etc/apt/apt.conf.d/50unattended-upgrades' } let(:file_periodic) { '/etc/apt/apt.conf.d/10periodic' } let(:file_options) { '/etc/apt/apt.conf.d/10options' } shared_examples 'basic specs' do let(:params) { {} } context 'baseline specs' do it { is_expected.to compile.with_all_deps } it do is_expected.to contain_package('unattended-upgrades') is_expected.to compile.with_all_deps is_expected.to contain_class('unattended_upgrades::params') is_expected.to contain_class('unattended_upgrades') is_expected.to contain_class('apt') end it do is_expected.to contain_apt__conf('unattended-upgrades').with( require: 'Package[unattended-upgrades]', notify_update: false ) end it do is_expected.to contain_apt__conf('periodic').with( require: 'Package[unattended-upgrades]', notify_update: false ) end it do is_expected.to contain_apt__conf('options').with( require: 'Package[unattended-upgrades]', notify_update: false ) end it { is_expected.to create_file(file_unattended).without_content(/Unattended-Upgrade::Sender/) } end context 'set all the things' do let :params do { age: { 'min' => 1, 'max' => 20 }, size: 1000, update: 5, upgradeable_packages: { 'download_only' => 5, 'debdelta' => 5 }, upgrade: 5, days: %w[tuesday Thursday 5], auto: { 'clean' => 5, 'fix_interrupted_dpkg' => false, 'remove' => false, 'reboot' => true, 'reboot_time' => '03:00' }, verbose: 1, origins: %w[codename=bananas], blacklist: %w[foo bar], whitelist: %w[foo bar], minimal_steps: false, install_on_shutdown: true, mail: { 'to' => 'root@localhost', 'only_on_error' => true, 'report' => 'on-change' }, sender: 'root@server.example.com', dl_limit: 70, random_sleep: 300, notify_update: true, options: { 'force_confdef' => false, 'force_confold' => false, 'force_confnew' => true, 'force_confmiss' => true }, remove_new_unused_deps: false, syslog_enable: true, syslog_facility: 'daemon' } end it { is_expected.to contain_package('unattended-upgrades') } it do is_expected.to contain_apt__conf('unattended-upgrades').with( require: 'Package[unattended-upgrades]', notify_update: true ) end it do is_expected.to contain_apt__conf('periodic').with( require: 'Package[unattended-upgrades]', notify_update: true ) end it do is_expected.to contain_apt__conf('options').with( require: 'Package[unattended-upgrades]', notify_update: true ) end it do is_expected.to create_file(file_unattended).with( owner: 'root', group: 'root' ).with_content( /Unattended-Upgrade::Origins-Pattern {\n\t"codename=bananas";\n};/ ).with_content( /Unattended-Upgrade::Package-Blacklist {\n\t"foo";\n\t"bar";\n};/ ).with_content( /Unattended-Upgrade::Package-Whitelist {\n\t"foo";\n\t"bar";\n};\n/ ).with_content( /Unattended-Upgrade::Update-Days {\n\t"Tuesday";\n\t"Thursday";\n\t"5";\n};/ ).with_content( /Unattended-Upgrade::AutoFixInterruptedDpkg "false";/ ).with_content( /Unattended-Upgrade::MinimalSteps "false";/ ).with_content( /Unattended-Upgrade::InstallOnShutdown "true";/ ).with_content( /Unattended-Upgrade::Remove-Unused-Dependencies "false";/ ).with_content( /Unattended-Upgrade::Automatic-Reboot "true";/ ).with_content( /Unattended-Upgrade::Automatic-Reboot-Time "03:00";/ ).with_content( /Unattended-Upgrade::Mail "root@localhost";/ ).with_content( /Unattended-Upgrade::Sender "root@server.example.com";/ ).with_content( /Unattended-Upgrade::MailOnlyOnError "true";/ ).with_content( /Unattended-Upgrade::MailReport "on-change";/ ).with_content( /Acquire::http::Dl-Limit "70";/ ).with_content( /Unattended-Upgrade::Remove-New-Unused-Dependencies "false";/ ).without_content( /Unattended-Upgrade::Remove-Unused-Kernel-Packages/ ).with_content( /Unattended-Upgrade::SyslogEnable "true";/ ).with_content( /Unattended-Upgrade::SyslogFacility "daemon";/ ) end it do is_expected.to create_file(file_periodic).with( owner: 'root', group: 'root' ).with_content( /APT::Periodic::Enable "1";/ ).with_content( /APT::Periodic::BackupArchiveInterval "0";/ ).with_content( /APT::Periodic::BackupLevel "3";/ ).with_content( /APT::Periodic::MaxAge "20";/ ).with_content( /APT::Periodic::MinAge "1";/ ).with_content( /APT::Periodic::MaxSize "1000";/ ).with_content( /APT::Periodic::Update-Package-Lists "5";/ ).with_content( /APT::Periodic::Download-Upgradeable-Packages "5";/ ).with_content( /APT::Periodic::Download-Upgradeable-Packages-Debdelta "5";/ ).with_content( /APT::Periodic::Unattended-Upgrade "5";/ ).with_content( /APT::Periodic::AutocleanInterval "5";/ ).with_content( /APT::Periodic::Verbose "1";/ ).with_content( /APT::Periodic::RandomSleep "300";/ ) end it do is_expected.to create_file(file_options).with( owner: 'root', group: 'root' ).with_content( /^Dpkg::Options\s{/ ).without_content( /"--force-confdef";/ ).without_content( /"--force-confold";/ ).with_content( /^\s+"--force-confnew";/ ).with_content( /^\s+"--force-confmiss";/ ) end it do is_expected.to contain_apt__conf('auto-upgrades').with( ensure: 'absent' ) end end describe 'validation tests' do context 'bad install_on_shutdown' do let :params do { install_on_shutdown: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad days' do let :params do { days: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad minimal_steps' do let :params do { minimal_steps: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad blacklist' do let :params do { blacklist: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad origins' do let :params do { origins: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad auto' do let :params do { auto: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad mail' do let :params do { mail: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad backup' do let :params do { backup: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad age' do let :params do { age: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad size' do let :params do { size: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad upgradeable_packages' do let :params do { upgradeable_packages: 'foo' } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad mail[\'only_on_error\']' do let :params do { mail: { 'only_on_error' => 'foo' } } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad options[\'force_confdef\']' do let :params do { options: { 'force_confdef' => 'foo' } } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad options[\'force_confold\']' do let :params do { options: { 'force_confold' => 'foo' } } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad options[\'force_confnew\']' do let :params do { options: { 'force_confnew' => 'foo' } } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad options[\'force_confmiss\']' do let :params do { options: { 'force_confmiss' => 'foo' } } end it { is_expected.to compile.and_raise_error(/got String/) } end context 'bad options[\'invalid_key\']' do let :params do { options: { 'invalid_key' => true } } end it { is_expected.to compile.and_raise_error(/unrecognized key 'invalid_key'/) } end end + + describe 'n-days settings tests' do + context 'update supports always' do + let :params do + { + update: 'always' + } + end + + it do + is_expected.to create_file(file_periodic).with_content( + /APT::Periodic::Update-Package-Lists "always";/ + ) + end + end + context 'ugrade supports always' do + let :params do + { + upgrade: 'always' + } + end + + it do + is_expected.to create_file(file_periodic).with_content( + /APT::Periodic::Unattended-Upgrade "always";/ + ) + end + end + context 'count supports always' do + let :params do + { + auto: { 'clean' => 'always' } + } + end + + it do + is_expected.to create_file(file_periodic).with_content( + /APT::Periodic::AutocleanInterval "always";/ + ) + end + end + context 'download-only supports always' do + let :params do + { + upgradeable_packages: { 'download_only' => 'always' } + } + end + + it do + is_expected.to create_file(file_periodic).with_content( + /APT::Periodic::Download-Upgradeable-Packages "always";/ + ) + end + end + end end on_supported_os.each do |os, facts| context "on #{os}" do let(:facts) do facts.merge(fqdn: 'unattended-upgrades.example.com', path: '/usr/lib64/qt-3.3/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/puppetlabs/bin:/root/bin') end it_behaves_like 'basic specs' end end end diff --git a/types/auto.pp b/types/auto.pp index bc3a896..b37539e 100644 --- a/types/auto.pp +++ b/types/auto.pp @@ -1,9 +1,9 @@ type Unattended_upgrades::Auto = Struct[ { - Optional['clean'] => Integer[0], + Optional['clean'] => Variant[Integer[0], Enum['always']], Optional['fix_interrupted_dpkg'] => Boolean, Optional['reboot'] => Boolean, Optional['reboot_time'] => String, Optional['remove'] => Boolean, } ] diff --git a/types/upgradeable_packages.pp b/types/upgradeable_packages.pp index ce41a94..9fcd87e 100644 --- a/types/upgradeable_packages.pp +++ b/types/upgradeable_packages.pp @@ -1,6 +1,6 @@ type Unattended_upgrades::Upgradeable_packages = Struct[ { - Optional['download_only'] => Integer[0], + Optional['download_only'] => Variant[Integer[0], Enum['always']], Optional['debdelta'] => Integer[0], } ]