├── .gitignore ├── CHANGELOG.md ├── Gemfile ├── README.md ├── Rakefile ├── lib └── puppet │ ├── application │ └── package.rb │ ├── face │ └── package.rb │ ├── indirector │ ├── catalog │ │ └── package_updates.rb │ └── patches │ │ ├── hiera.rb │ │ └── yaml.rb │ ├── node │ └── patches.rb │ └── package.rb ├── manifests ├── init.pp └── pe_master.pp └── metadata.json /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | .pe_build 3 | .vagrant 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Release 1.0.0 2 | 3 | * Add ability to apply patches 4 | 5 | ## Release 0.1.0 6 | ### Summary 7 | 8 | * Add the missing Puppet application so the CLI functions 9 | * Ensure Facter's external facts directory exists 10 | * Improve the README documentation 11 | * Improve the metadata to point to the project's github 12 | 13 | ## Release 0.0.1 14 | ### Summary 15 | 16 | First Release. See README for description. 17 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | group :development do 4 | gem 'puppet-blacksmith' 5 | gem 'cucumber' 6 | gem 'beaker-rspec' 7 | gem 'pry' 8 | gem 'minitest', '4.7.5' 9 | end 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # package_updates 2 | 3 | #### Table of Contents 4 | 5 | 1. [Module Description](#module-description) 6 | 2. [Setup](#setup) 7 | * [What package_updates affects](#what-package_updates-affects) 8 | * [Setup requirements](#setup-requirements) 9 | * [Beginning with package_updates](#beginning-with-package_updates) 10 | 3. [Usage](#usage) 11 | * [Setting up a scan schedule](#setting-a-scan-schedule) 12 | * [Using the custom fact](#using-the-custom-fact) 13 | * [Querying infrastructure patch state](#querying-infrastructure-patch-state) 14 | * [Patch deployment](#patch-deployment) 15 | 4. [Reference](#reference) 16 | 5. [Limitations - OS and Puppet compatibility](#limitations-and-support) 17 | 18 | ## Module Description 19 | 20 | This is an experimental module that aims to enable continuous delivery of all 21 | package updates within an infrastructure across any package manager that has a 22 | Puppet provider that supports the upgradeable feature. Package information is 23 | stored in PuppetDB is inventory information and package update versions are 24 | specified in Hiera as part of a r10k change management process. 25 | 26 | The module provides a `puppet package updates` subcommand to query available 27 | package updates from all package providers available on the system. The 28 | subcommand is able to query from almost all package managers out of the box and 29 | more can be added by downloading modules from the Forge that include additional 30 | package providers, such as the [chocolatey/chocolatey](https://forge.puppetlabs.com/chocolatey/chocolatey) 31 | module for Windows. 32 | 33 | The provided **package_updates** class manages a cron job to scan the system 34 | for available package updates on a regular schedule. The cron job takes the 35 | output from the included `puppet package updates` plugin and generates a 36 | Facter fact so the package update status is always up to date in PuppetDB. 37 | Keeping the data in PuppetDB provides an easy interface to query for 38 | available updates and generate custom reports. 39 | 40 | This module also includes a catalog terminus that searches for package update 41 | information in Hiera, and injects that information into a normally compiled 42 | catalog. This way, packages can be managed as usual as package resources in 43 | Puppet code, while the updates to those packages, and all system packages with 44 | updates NOT managed by Puppet code, can be managed as Puppet resources. 45 | Updates are continuously enforced each Puppet run, show up in the Puppet 46 | reports, and are fully auditable. 47 | 48 | 49 | ## Setup 50 | 51 | ### What package_updates affects 52 | 53 | * A cron job in the root user's crontab 54 | * A custom Facter fact with package update information 55 | * A custom catalog compiler 56 | 57 | ### Setup requirements 58 | 59 | * Add the package_updates class to all node groups you want to monitor updates on 60 | 61 | ### Beginning with package_updates 62 | 63 | To have nodes scan for updates on a regular cadence and report the result as a custom fact, 64 | declare the ***package_updates*** class to any node or node group you'd like to monitor for updates. 65 | 66 | To deploy package updates, declare the **package_updates::pe_master** class to 67 | the **PE Master** group in the Puppet Enterprise Console. See [Patch 68 | deployment](#patch-deployment) for how to define and deploy patches. 69 | 70 | ### Usage 71 | 72 | #### Setting a scan schedule 73 | 74 | The module contains a single class: **package_updates**. This class sets up a 75 | cron job to run the puppet face and caches the result in an external fact. By 76 | default, the cron job runs every day at 3:00am. You can change that with the 77 | available class parameters. 78 | The class will also allow to specify a list of command to execute before the 79 | cron command. You can specify a proxy server and it's exclution as part of 80 | the cron job. 81 | 82 | * minute - The minute at which to run the scan. Default: undef 83 | * hour - The hour at which to run the scan. Default: 3 84 | * month - The month of the year. Default: undef 85 | * monthday - The day of the month to run the scan. Default: undef 86 | * weekday - The day of th week to run the scan. Default: undef 87 | * precommand - Array containing command to execute first. Default: Empty Array 88 | 89 | **Example** 90 | 91 | You can define a $proxy_command Array and pass it to the class this way: 92 | 93 | $proxy_command=["export http_proxy=${::proxy_server}", 94 | "export https_proxy=${::proxy_server}", 95 | "export no_proxy=${::no_proxy}" 96 | ] 97 | class {'::package_updates' : 98 | precommand => $proxy_command, 99 | schedule => 'daily', 100 | minute => 0, 101 | hour => 3, 102 | month => 'all', 103 | monthday => 'all', 104 | weekday => 'all', 105 | } 106 | 107 | Proxy defined in this way should work, with the correct exclusion, with all the package provider. 108 | 109 | #### Using the Puppet Command Line Interface 110 | 111 | After installing the module on the Puppet master, each Puppet agent will pluginsync the libraries 112 | to their local file systems. Once the sync happens, you can use the following command to get a list of 113 | all the packages that have updates available. 114 | 115 | $ puppet package updates 116 | 117 | You can also request the output be in JSON serialized format 118 | 119 | $ puppet package updates --render-as json 120 | 121 | #### Using the custom fact 122 | 123 | The available package updates on the system can be retrieved as a structured custom fact. Since it 124 | can take several seconds to scan the system for updates, it's preferable to scan for updates at a 125 | regular cadence and cache the results for Facter to retrieve. 126 | 127 | The package_updates class provides a way to set a schedule for the system to scan for package updates 128 | and caches the results for Facter. 129 | 130 | #### Querying infrastructure patch state 131 | 132 | You can use the `puppet query` command to query the patch state for different parts of the infrastructure. 133 | For example, the following command will return all package updates for the production environment. 134 | 135 | puppet query 'facts { name = "package_updates" and environment = "production" }' 136 | 137 | To learn more about using PQL, go [here](https://docs.puppet.com/puppetdb/4.2/api/query/tutorial-pql.html). 138 | 139 | #### Patch deployment 140 | 141 | This module provides a catalog terminus called **package_updates**. The 142 | catalog terminus injects patch information into a node's commpiled catalog. To 143 | set the terminus, set the **catalog_terminus ** setting in the **master** 144 | section of the /etc/puppetlabs/puppet/puppet.conf file to the value of 145 | **package_updates** by running the folllowing comand. Restart the puppetserver 146 | service once complete. 147 | 148 | puppet config set catalog_terminus package_updates 149 | 150 | For Puppet Enterprise installations, simply declare the 151 | **package_updates::pe_master** class in the **PE Master** node group in the 152 | Puppet Enterprise Console. 153 | 154 | 155 | To apply patches to systems, a hash of package versions to be applied must be 156 | generated and added to your r10k control repository. By specifying patch 157 | information in the control repo, patches can be defined, tested, and promoted 158 | through the delivery process you already use for all other code. 159 | 160 | The hash follows the following example yaml format: 161 | 162 | package_updates: 163 | classes: 164 | role::webserver 165 | apache: 166 | version: '2.9.3.el7' 167 | provider: 'yum' 168 | gcc: 169 | version: '4.8.5-4.el7' 170 | provider: 'yum' 171 | 172 | The **classes** key in the package_updates hash contains a hash where each key 173 | is the name of a Puppet class that should have the patches specified applied to 174 | any system with that class. Any packages specified outside the **classes** key 175 | are assumed global and will apply to any system at all. 176 | 177 | **Using Hiera** 178 | 179 | The default terminus for retrieving patches is from Hiera. Hiera enables users 180 | to break the package_updates hash into hierarchies such as patch information 181 | for Red Hat systems vs Ubuntu or specifying patches assigned to geographical location. 182 | 183 | The Puppet::Node::Patches indirector finds all instances of the package_updates 184 | hash in any hierarchy that applies to the node, merging all found instances of 185 | package_updates. 186 | 187 | #### Report generation 188 | 189 | Since the PuppetDB query outputs standard JSON, existing tools can be used to generate spreadsheet 190 | reports or custom interfaces can be built that renders the serialized data. 191 | 192 | Suggested tools: 193 | 194 | * Ruby - [json2csv](https://github.com/ngmaloney/json2csv) 195 | * NodJS - [json2csv](https://github.com/zemirco/json2csv) 196 | 197 | 198 | ### Limitations and support 199 | 200 | This module is compatible with Puppet 4.x+ only. It makes use of the Puppet 4 parameter data type 201 | validation which is incompatible with Puppet 3.x 202 | 203 | Setting a schedule to scan for updates on a regular schedule currently only 204 | works with non-Windows systems. Once the **package_updates** interface can 205 | handle both cron and scheduled_task resources, Windows support for package 206 | management systems like Chocolatey can easily be added. 207 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'cucumber' 3 | require 'cucumber/rake/task' 4 | require 'puppetlabs_spec_helper/rake_tasks' # needed for some module packaging tasks 5 | require 'puppet_blacksmith/rake_tasks' 6 | 7 | Cucumber::Rake::Task.new(:features) do |t| 8 | t.cucumber_opts = "features --format pretty" 9 | end 10 | -------------------------------------------------------------------------------- /lib/puppet/application/package.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/application/face_base' 2 | class Puppet::Application::Package < Puppet::Application::FaceBase 3 | end 4 | -------------------------------------------------------------------------------- /lib/puppet/face/package.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/indirector/face' 2 | require 'puppet/package' 3 | 4 | Puppet::Indirector::Face.define(:package, '0.0.1') do 5 | copyright "Puppet Labs", 2011 6 | license "Apache 2 license; see COPYING" 7 | 8 | summary "View and manage packages on a node." 9 | description <<-'EOT' 10 | This subcommand interacts with package objects, using the default provider. 11 | Upgrading multiple packages and viewing what updates are available for any 12 | package type requires providers that supports 'versionable' and 'upgradeable'. 13 | EOT 14 | 15 | save = get_action(:save).summary "Invalid for this face. Use the resource subcommand" 16 | destroy = get_action(:destroy).summary "Invalid for this face. Use the resource subcommand" 17 | 18 | action :updates do 19 | summary "List all packages with updates available from packaging system" 20 | returns <<-'EOT' 21 | A hash where the key is the package and the value is a hash containing `update`, 22 | `current`, and `provider` 23 | EOT 24 | 25 | when_invoked do |options| 26 | #We don't want the catalog.apply to print the logs to stdout 27 | Puppet::Util::Log.close_all 28 | 29 | Puppet::Package.find_updates 30 | end 31 | 32 | when_rendering :console do |package_updates| 33 | packages = package_updates['package_updates'] 34 | output = Array.new 35 | 36 | #Provide pretty output 37 | layout = "\033[31m%-40s\033[32m%-30s\033[0m \033[32m%-30s\033[0m%s" 38 | 39 | output << layout % ["PACKAGE NAME", "CURRENT VERSION", "UPDATE AVAILABLE", "PROVIDER"] 40 | output << '-'*120 41 | 42 | output << packages.map do |name,package| 43 | layout % [name, package['current'], package['update'], package['provider'] ] 44 | end 45 | 46 | output.flatten.join("\n") 47 | end 48 | 49 | when_rendering :json do |package_updates| 50 | puts package_updates.to_json 51 | end 52 | end 53 | 54 | action :update do 55 | summary "Perform an update on a package" 56 | arguments " | " 57 | examples <<-EOT 58 | When invoked from the command line: 59 | 60 | $ puppet package update apr [--provider rpm] [--level 1.2.7-11.el5_6.5] 61 | 62 | Or you can use in API form by passing in a hash of packages where each package value 63 | contains a hash with keys `update` and `provider`: 64 | 65 | updates = { 66 | 'apr' => { :update => '1.2.7-11.el5_6.5', :provider => :rpm } 67 | 'stomp' => { :update => '1.1.6', :provider => :gem } 68 | } 69 | 70 | Puppet::Face[:package, '0.0.1'].update updates 71 | EOT 72 | 73 | Puppet::Package.add_update_options(self) 74 | 75 | when_invoked do |package, options| 76 | if package.class == Hash 77 | Puppet::Package.apply_updates package 78 | elsif package.class == String 79 | Puppet::Package.update_package(package, options) 80 | else 81 | raise "Must give a Hash for String to update action" 82 | end 83 | nil #Don't render anything 84 | end 85 | end 86 | 87 | action :list do 88 | default 89 | summary "List installed packages" 90 | 91 | when_invoked do |options| 92 | Puppet::Package.system_packages 93 | end 94 | 95 | when_rendering :console do |packages| 96 | packages.map do |package| 97 | package.name 98 | end.join("\n") 99 | end 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/puppet/indirector/catalog/package_updates.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/node' 2 | require 'puppet/node/patches' 3 | require 'puppet/resource/catalog' 4 | require 'puppet/indirector/catalog/compiler' 5 | 6 | # Implement a skeleton for a compiler catalog terminus that adds package patch resources 7 | class Puppet::Resource::Catalog::PackageUpdates < Puppet::Resource::Catalog::Compiler 8 | 9 | def find(request) 10 | catalog = super(request) 11 | node = node_from_request(request) 12 | 13 | Puppet.notice "Adding patches for #{node.name} to catalog" 14 | 15 | managed_packages = catalog.resources.find_all { |resource| resource.type == 'Package' } 16 | 17 | package_updates = Array.new 18 | retrieve_package_updates(node).each do |name,parameters| 19 | package_parameters = { :name => name, 20 | :ensure => parameters['version'], 21 | :provider => parameters['provider'] 22 | } 23 | 24 | package_updates << create_package_object(name, package_parameters) 25 | end 26 | 27 | package_updates.each do |package_update| 28 | if catalog_package = managed_packages.find { |r| 29 | r[:name] == package_update[:name] and r[:provider] == package_update[:provider] 30 | } 31 | # Make sure not to override the version if the version is managed by Puppet code 32 | if [nil,'installed','present','absent','purged'].include? catalog_package[:ensure] 33 | catalog_package[:ensure] = package_update[:ensure] 34 | else 35 | # If we're here, the version is being specified in Puppet code 36 | unless catalog_package[:ensure] == package_update[:ensure] 37 | # The specified update doesn't match what's specified in Puppet code 38 | Puppet.warn "Not overriding version #{catalog_package[:ensure]} with specified update #{package_update[:ensure]} for package #{package_update[:name]}" 39 | end 40 | end 41 | else 42 | catalog.add_resource package_update 43 | end 44 | end 45 | 46 | catalog 47 | end 48 | 49 | private 50 | 51 | def create_package_object(title, parameters) 52 | Puppet::Resource.new(Puppet::Type::Package, title, {:parameters => parameters}) 53 | end 54 | 55 | def retrieve_package_updates(node) 56 | begin 57 | Puppet::Node::Patches.find(node) 58 | rescue Puppet::Node::Patches::NoPatchFile => e 59 | Puppet.warning e 60 | Puppet.warning "Continuing with compilation without managing patches" 61 | return Hash.new 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/puppet/indirector/patches/hiera.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/indirector' 2 | require 'puppet/indirector/hiera' 3 | require 'puppet/node/patches' 4 | 5 | class Puppet::Node::Patches::Hiera < Puppet::Indirector::Hiera 6 | desc "Retrieve patch state from Hiera." 7 | 8 | def find(request) 9 | node = request.key 10 | 11 | hiera.lookup('package_updates', {}, node.parameters, {}, convert_merge('deep')) 12 | 13 | rescue *DataBindingExceptions => detail 14 | error = Puppet::DataBinding::LookupError.new("DataBinding 'hiera': #{detail.message}") 15 | error.set_backtrace(detail.backtrace) 16 | raise error 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/puppet/indirector/patches/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/indirector' 2 | require 'puppet/indirector/yaml' 3 | require 'puppet/node/patches' 4 | 5 | class Puppet::Node::Patches::Yaml < Puppet::Indirector::Yaml 6 | desc "Retrieve patch state from a yaml file for all roles on a node." 7 | 8 | def find(request) 9 | node = request.key 10 | file = request.options[:file] || 'patches.yaml' 11 | 12 | path = File.join( Puppet[:environmentpath], node.environment.to_s, 'patches', file ) 13 | 14 | unless File.exists?(path) 15 | raise Puppet::Node::Patches::NoPatchFile, "Puppet patch file #{path} doesn't exist" 16 | end 17 | 18 | YAML::load(IO.read(path)) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/puppet/node/patches.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/node' 2 | require 'puppet/indirector' 3 | 4 | class Puppet::Node::Patches 5 | Puppet::ResourceType = self 6 | 7 | class Puppet::Node::Patches::NoPatchFile < Exception; end 8 | 9 | extend Puppet::Indirector 10 | 11 | indirects :patches, :terminus_class => :hiera 12 | 13 | def self.find(node) 14 | all_patches = self.indirection.find(node) 15 | 16 | patches_to_be_applied = Hash.new 17 | 18 | # If the patch hash has classes with updates, find 19 | # the updates for classes on this node 20 | if all_patches.has_key?('classes') 21 | classes_with_patches = node.classes.select { |c| all_patches['classes'].has_key?(c) } 22 | classes_with_patches.each do |klass| 23 | role = klass.first 24 | patches_to_be_applied.merge! all_patches['classes'][role] 25 | end 26 | 27 | # This key isn't needed anymore. 28 | # All that's left should be global patches 29 | all_patches.delete('classes') 30 | end 31 | 32 | # Add in the global patches 33 | patches_to_be_applied.merge! all_patches 34 | 35 | patches_to_be_applied 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/puppet/package.rb: -------------------------------------------------------------------------------- 1 | require 'puppet/indirector' 2 | require 'puppet/face' 3 | 4 | class Puppet::Package 5 | 6 | # Set up indirection 7 | extend Puppet::Indirector 8 | 9 | indirects :package, :terminus_setting => :package_terminus 10 | 11 | def self.[](name) 12 | system_packages.each do |package| 13 | if package.name == name 14 | return package 15 | end 16 | end 17 | end 18 | 19 | def self.add_update_options(action) 20 | action.option '--level=', '-l=' do 21 | summary "The version level to patch to. Default is latest" 22 | end 23 | 24 | action.option '--provider=', '-p=' do 25 | summary "The provider for the package. Defaults to system default provider" 26 | end 27 | end 28 | 29 | def self.system_packages 30 | #Can't use Puppet::Resource face here 31 | #Since it doesn't return package objects 32 | Puppet::Type.type(:package).instances 33 | end 34 | 35 | 36 | #Takes a hash in the form 37 | # package_name = { 38 | # :update => 39 | # :provider => 40 | # } 41 | def self.apply_updates(packages) 42 | packages.each do |name, package| 43 | unless system_packages.map{ |p| p.name }.include? name 44 | raise "#{name} is not installed on the system" 45 | end 46 | 47 | package_resource = Puppet::Resource.new( :package, "Package[#{name}]", 48 | :parameters => {:ensure => package[:update], :provider => package[:provider]} 49 | ) 50 | 51 | #Apply the update 52 | begin 53 | Puppet::Face[:resource,'0.0.1'].save package_resource 54 | rescue => e 55 | raise "Could not update \033[31m#{name}\033[0m: #{e}" 56 | end 57 | end 58 | end 59 | 60 | def self.update_package(package, options) 61 | #The other methods expect the package name to be in the following format 62 | level = options.has_key?(:level) ? options[:level] : :latest 63 | provider = options.has_key?(:provider) ? options[:provider] : Puppet::Type.type(:package).defaultprovider.name 64 | 65 | #unless system_packages.map{ |p| p.name }.include? package 66 | #raise ArgumentError, "#{package} is not installed on the system" 67 | #end 68 | 69 | #Perform the update 70 | apply_updates package => { :update => level, :provider => provider } 71 | end 72 | 73 | def self.find_updates 74 | packages = system_packages 75 | package_updates = Hash.new 76 | 77 | prefetch_updates(packages) 78 | 79 | packages.each do |p| 80 | provider = String(p[:provider]) 81 | 82 | unless package_updates[provider] 83 | package_updates[provider] = Hash.new 84 | end 85 | 86 | # Some providers can't determine latest versions 87 | next unless p.provider.class.method_defined?(:latest) 88 | 89 | # Need to access with Array because sometimes puppet hands back 90 | # strings, sometimes it hands back arrays containing a string. Oy. 91 | # Using confusing [*] syntax for speed. 92 | latest = [*p.provider.latest][0] 93 | current = [*p.provider.properties[:ensure]][0] 94 | 95 | # filter out packages that apt reports as "installed" but not "purged" 96 | # as well as packages with no upgrade 97 | next unless latest && (current != "absent") 98 | 99 | if latest != current 100 | package_updates[p.title] = { 101 | 'name' => p.title, 102 | 'current' => current, 103 | 'update' => latest, 104 | 'provider' => String(p[:provider]) 105 | } 106 | end 107 | end 108 | 109 | {'package_updates' => package_updates } 110 | end 111 | 112 | private 113 | 114 | # Some providers require prefetching, while others don't even implement it 115 | # This method collects all of the providers for a given set of packages, 116 | # then calls prefetch on those that implement a prefetch method. 117 | def self.prefetch_updates(packages) 118 | providers = packages.map {|p| p.provider.class }.uniq 119 | 120 | providers.each do |provider| 121 | next unless provider.methods.include? "prefetch" 122 | to_prefetch = packages.select { |p| p.provider.class == provider } 123 | 124 | # We have to submit packages to prefetch methods in title-keyed hash 125 | prefetch_hash = Hash.new 126 | to_prefetch.each { |p| prefetch_hash[p.title] = p } 127 | 128 | # At least one package must be ensure => latest, or else lazy loading 129 | # mechanisms will "helpfully" prevent prefetching 130 | prefetch_hash[prefetch_hash.keys.first][:ensure] = :latest 131 | 132 | provider.prefetch(prefetch_hash) 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /manifests/init.pp: -------------------------------------------------------------------------------- 1 | class package_updates ( 2 | Array[String] $precommand= [], 3 | 4 | Enum['daily','weekly','monthly','once'] $schedule = 'daily', 5 | 6 | Variant[ 7 | Integer[0,59], 8 | Array[ 9 | Integer[0,59] 10 | ], 11 | Enum['all'] 12 | ] $minute = 0, 13 | 14 | Variant[ 15 | Integer[0,23], 16 | Array[ 17 | Integer[0,23] 18 | ], 19 | Enum['all'] 20 | ] $hour = 3, 21 | 22 | Variant[ 23 | Integer[1,12], 24 | Array[Integer[1,12]], 25 | Array[ 26 | Enum[ 27 | 'january', 'february', 'march','april','may','june', 28 | 'july','august','september','october','november','december' 29 | ] 30 | ], 31 | Enum[ 32 | 'january', 'february', 'march','april','may','june', 33 | 'july','august','september','october','november','december','all' 34 | ] 35 | ] $month = 'all', 36 | 37 | Variant[ 38 | Integer[1,31], 39 | Array[Integer[1,31]], 40 | Enum['all'] 41 | ] $monthday = 'all', 42 | 43 | Variant[ 44 | Integer[0,7], 45 | Array[Integer[0,7]], 46 | Array[ 47 | Enum[ 48 | 'sunday','monday','tuesday','wednesday', 49 | 'thursday','friday','saturday','sunday' 50 | ] 51 | ], 52 | Enum[ 53 | 'sunday','monday','tuesday','wednesday', 54 | 'thursday','friday','saturday','sunday','all' 55 | ] 56 | ] $weekday = 'all', 57 | ) { 58 | 59 | # If all is specified, just build an 60 | # array of every minute 61 | $_hour = $hour ? { 62 | 'all' => range('0','23'), 63 | default => $hour 64 | } 65 | 66 | # If all is specified, just build an 67 | # array of every minute 68 | $_minute = $minute ? { 69 | 'all' => range('0','59'), 70 | default => $minute 71 | } 72 | 73 | # If all is specified, just build an 74 | # array of every month day number 75 | $_monthday = $monthday ? { 76 | 'all' => range('1','31'), 77 | default => $monthday, 78 | } 79 | 80 | # If all is specified, just build an 81 | # array of every week day number 82 | $_weekday = $weekday ? { 83 | 'all' => range('1','7'), 84 | default => $weekday, 85 | } 86 | 87 | # If all is specified, just build an 88 | # array of every month number 89 | $_month = $month ? { 90 | 'all' => range('1','12'), 91 | default => $month 92 | } 93 | 94 | $updates_subcommand = "package updates --render-as json" 95 | 96 | if $::kernel != 'windows' { 97 | $puppet_path = '/opt/puppetlabs/bin/puppet' 98 | $facts_d_directory = '/opt/puppetlabs/facter/facts.d' 99 | $tmp_path = '/tmp/package_updates.json' 100 | 101 | if $precommand != [] { 102 | $precmd="${join($precommand,';')};" 103 | } else { 104 | $precmd='' 105 | } 106 | # The `package updates` command takes a long time to run. Since the command is using shell 107 | # redirection, the target file is truncated prior to the `package updates` command being run. 108 | # Thus Facter will throw an error while looking up the package_updates fact if Facter is run 109 | # while the cron job is executing. So instead we'll output to a tmp file and mv the 110 | # file into place when the `package_updates` command is done executing. 111 | $command = "${precmd}${puppet_path} ${updates_subcommand} > ${tmp_path} && mv -f ${tmp_path} ${facts_d_directory}/" 112 | 113 | cron { 'package_updates': 114 | command => $command, 115 | minute => $_minute, 116 | hour => $_hour, 117 | month => $_month, 118 | monthday => $_monthday, 119 | weekday => $_weekday, 120 | } 121 | } else { 122 | notice('The package_updates class only supports non-Windows systems currently') 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /manifests/pe_master.pp: -------------------------------------------------------------------------------- 1 | class package_updates::pe_master { 2 | pe_ini_setting { 'set package update catalog terminus': 3 | section => 'master', 4 | setting => 'catalog_terminus', 5 | path => '/etc/puppetlabs/puppet/puppet.conf', 6 | value => 'package_updates', 7 | notify => Service['pe-puppetserver'], 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppetlabs-package_updates", 3 | "version": "1.0.0", 4 | "author": "Puppet Labs", 5 | "summary": "A module for monitoring and applying package updates", 6 | "license": "Apache-2.0", 7 | "source": "https://github.com/puppetlabs/puppetlabs-package_updates", 8 | "project_page": "https://github.com/puppetlabs/puppetlabs-package_updates", 9 | "issues_url": "https://github.com/puppetlabs/puppetlabs-package_updates/issues", 10 | "dependencies": [ 11 | 12 | ], 13 | "tags": [ 14 | "patch", 15 | "updates", 16 | "packages" 17 | ], 18 | "operatingsystem_support": [ 19 | { 20 | "operatingsystem": "RedHat", 21 | "operatingsystemrelease": [ 22 | "6.0", 23 | "7.0" 24 | ] 25 | }, 26 | { 27 | "operatingsystem": "Ubuntu", 28 | "operatingsystemrelease": [ 29 | "12.04", 30 | "14.04", 31 | "16.04" 32 | ] 33 | } 34 | ] 35 | } 36 | --------------------------------------------------------------------------------