How does puppet send commands to the OS? - puppet

I am new to Puppet, but understand the concepts quite well. Puppet Manifests call Puppet Modules and the Modules perform the actual task.
I am trying to understand what happens at the Puppet Module layer. How does the command actually execute? Taking the example of the following, what commands are actually passed on to the operating system? Also, where is that defined?
package { 'ntp':
ensure => installed,
}

Summary: Puppet determines the commands that need to be run, based on the facts of the system and the configuration within Puppet itself.
So, when Puppet compiles the catalog to run on it's system it looks like the following:
"I need to install a Pacakge resource, called ntp. I am CentOS system, of the RedHat family. By default on RedHat, I use the yum command. So I need to run yum install ntp"
Longer Explanation
The way that Puppet knows the commands to run and how to run them is known as the Resource Abstraction Layer.
When it's all boiled down, Puppet is not doing anything magical: the commands that are being run on the system are the same commands that would be run by a human operator.
Maybe Puppet has figured out a clever way to do it, and takes into account obscure bugs and gotchas for the platform you're on, or is raising an error because what you're trying to do contains a spelling mistake or similar.
But eventually, the action has to actually be performed using the systems actual applications and tooling.
That's where the RAL actually comes in. It's the biggest layer of abstraction in Puppet: turning all interactions with the base system into a consistent interface.
In the example you give, packages are fairly simple. The concept of installing a package is (mostly) the same to pretty much every operating system in at least the last two decades:
packagesystemtool keywordforinstall packagename
Generally, the install keyword is install, but there are a few exceptions. BSD's pkg which uses pkg add for example.
However: the actual attributes that can be managed in that package can vary a lot:
Can you specify the version?
Can you downgrade that version?
If the package is already installed, do you need to specify a different command to upgrade it?
A huge swath of other optional parameters such as proxy information, error logging level.
The RAL allows the user to define the characteristics of a resource regardless of the implementation in a consistent way:
type { 'title':
attribute => 'value',
}
Every resource follows the same syntax:
A resource type (eg. user, package, service, file)
Curly braces to define the resource block.
A title, separated from the body of the resource with a colon A body consisting of attributes and value pairs
So our package declaration looks like this:
package {'tree':
ensure => 'present',
}
The RAL can handle that behavior on every platform that has been defined, and support different package features where available, all in a well-defined way, hidden from the user by default.
The best metaphor I've heard for the RAL is it is the Swan gliding along on the lake on the Lake:
When you look at a swan on a body of water, it looks elegant and
graceful, gliding along. It barely looks like it's working at all.
What's hidden from the eye is the activity going on beneath the water’s surface. That swan is kicking it's webbed feet, way less gracefully that it looks up top: The actual command Puppet is running is the kicking legs under the water.
Ok, enough background, you're probably asking...
How does it actually work?
The RAL splits all resources on the system into two elements:
Types: High-level Models of the valid attributes for a resource
Providers: Platform-specific implementation of a type
This lets you describe resources in a way that can apply to any system. Each resource, regardless of what it is, has one or more providers. Providers are the interface between the underlying OS and the resource types.
Generally, there will be a default provider for a type, but you can specify a specific provider if required.
For a package, the default provider will be the default package provider for a system: yum for RHEL, apt for Debian, pkg for BSD etc. This is determined by a, which takes the facts from the system.
For example, the yum provider has the following:
defaultfor :osfamily => :redhat
But you might want to install a pip package, or a gem. For this you would specify the provider, so it would install it with a different command:
package {'tree':
ensure => 'present',
provider => 'pip',
}
This would mean we're saying to the RAL: "Hey, I know yum is the default to install a package, but this is a python package I need, so I'm telling you to use pip instead"
The most important resources of an attribute type are usually conceptually the same across operating systems, regardless of how the actual implementations differ.
Like we said, most packages will be installed with package installer install packagename
So, the description of a resource can be abstracted away from its implementation:
Puppet uses the RAL to both read and modify the state of resources on a system. Since it's a declarative system, Puppet starts with an understanding of what state a resource should have.
To sync the resource, it uses the RAL to query the current state, compare that against the desired state, and then use the RAL again to make any necessary changes. It uses the tooling to get the current state of the system and then figures out what it needs to do to change that state to the state defined by the resource.
When Puppet applies the catalog containing the resource, it will read the actual state of the resource on the target system, compare the actual state to the desired state, and, if necessary, change the system to enforce the desired state.
Let's look at how the RAL will manage this:
We've given the type as package.
The title/name of the package is ntp
I'm running this on a RHEL7 system, so the default provider is yum.
yum is a "child" provider of rpm: it uses the RPM command to check if the package is installed on the system. (This is a lot faster than running "yum info", as it doesn't make any internet calls, and won't fail if a yumrepo is failing)
The install command however, will be yum install
So previously we talked about how Puppet uses the RAL to both read and modify the state of resources on a system.
The "getter" of the RAL is the self.instances method in the provider.
Depending on the resource type, this is generally done in one of two ways:
Read a file on disk, iterate through the lines in a file and turn those into resources
Run a command on the terminal, break the stdout into lines, turn those into hashes which become resources
The rpm instances step goes with the latter. It runs rpm -qa with some given flags to check what packages are on the system:
def self.instances
packages = []
# list out all of the packages
begin
execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{self::NEVRA_FORMAT}'") { |process|
# now turn each returned line into a package object
process.each_line { |line|
hash = nevra_to_hash(line)
packages << new(hash) unless hash.empty?
}
}
rescue Puppet::ExecutionFailure
raise Puppet::Error, "Failed to list packages", $!.backtrace
end
packages
end
So it's running /usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n', then taking the stdout from that command, looping through each line of output from that, and using the nevra_to_hash method to turn the lines of STDOUT it into a hash.
self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+)$}
self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch]
private
# #param line [String] one line of rpm package query information
# #return [Hash] of NEVRA_FIELDS strings parsed from package info
# or an empty hash if we failed to parse
# #api private
def self.nevra_to_hash(line)
line.strip!
hash = {}
if match = self::NEVRA_REGEX.match(line)
self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v }
hash[:provider] = self.name
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
hash[:ensure].prepend("#{hash[:epoch]}:") if hash[:epoch] != '0'
else
Puppet.debug("Failed to match rpm line #{line}")
end
return hash
end
So basically it's a regex on the output, then turns those bits from the regex into the given fields.
These hashes become the current state of the resource.
We can run --debug to see this in action:
Debug: Prefetching yum resources for package
Debug: Executing: '/usr/bin/rpm --version'
Debug: Executing '/usr/bin/rpm -qa --nosignature --nodigest --qf '%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n''
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n'
Debug: Executing: '/usr/bin/rpm -q ntp --nosignature --nodigest --qf %{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n --whatprovides'
So it uses the RAL to fetch the current state. Puppet is doing the following:
Hmm, this is a package resource titled 'ntp' on a RHEL system, so I should use RPM
Let's get the current state of the RPM packages installed (eg. the instances method
ntp isn't here...
So we need ntp to be installed
the Yum provider then specifies the command required to install.
There's a lot of logic here:
def install
wanted = #resource[:name]
error_level = self.class.error_level
update_command = self.class.update_command
# If not allowing virtual packages, do a query to ensure a real package exists
unless #resource.allow_virtual?
execute([command(:cmd), '-d', '0', '-e', error_level, '-y', install_options, :list, wanted].compact)
end
should = #resource.should(:ensure)
self.debug "Ensuring => #{should}"
operation = :install
case should
when :latest
current_package = self.query
if current_package && !current_package[:ensure].to_s.empty?
operation = update_command
self.debug "Ensuring latest, so using #{operation}"
else
self.debug "Ensuring latest, but package is absent, so using #{:install}"
operation = :install
end
should = nil
when true, false, Symbol
# pass
should = nil
else
# Add the package version
wanted += "-#{should}"
if wanted.scan(ARCH_REGEX)
self.debug "Detected Arch argument in package! - Moving arch to end of version string"
wanted.gsub!(/(.+)(#{ARCH_REGEX})(.+)/,'\1\3\2')
end
current_package = self.query
if current_package
if rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) < 0
self.debug "Downgrading package #{#resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = :downgrade
elsif rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(current_package[:ensure])) > 0
self.debug "Upgrading package #{#resource[:name]} from version #{current_package[:ensure]} to #{should}"
operation = update_command
end
end
end
# Yum on el-4 and el-5 returns exit status 0 when trying to install a package it doesn't recognize;
# ensure we capture output to check for errors.
no_debug = if Facter.value(:operatingsystemmajrelease).to_i > 5 then ["-d", "0"] else [] end
command = [command(:cmd)] + no_debug + ["-e", error_level, "-y", install_options, operation, wanted].compact
output = execute(command)
if output =~ /^No package #{wanted} available\.$/
raise Puppet::Error, "Could not find package #{wanted}"
end
# If a version was specified, query again to see if it is a matching version
if should
is = self.query
raise Puppet::Error, "Could not find package #{self.name}" unless is
# FIXME: Should we raise an exception even if should == :latest
# and yum updated us to a version other than #param_hash[:ensure] ?
vercmp_result = rpm_compareEVR(rpm_parse_evr(should), rpm_parse_evr(is[:ensure]))
raise Puppet::Error, "Failed to update to version #{should}, got version #{is[:ensure]} instead" if vercmp_result != 0
end
end
This is some serious Swan leg kicking. There's a lot of logic here, for the more complex use case of a package on Yum, but making sure it works on the various versions of Yum avaliable, including RHEL 4 and 5.
The logic is broken down thusly: We haven't specified a version, so we don't need to check what version to install. Simply run yum install tree with the default options specified
Debug: Package[tree](provider=yum): Ensuring => present
Debug: Executing: '/usr/bin/yum -d 0 -e 0 -y install tree'
Notice: /Stage[main]/Main/Package[tree]/ensure: created
Ta-dah, installed.

It depends on your flavour of linux.
First is checked if the package ntp is installed.
If not it will be installed.
CentOS example:
yum list installed ntp
If not installed
yum install ntp
Debian example:
dpkg -s ntp
If not installed
apt-get install ntp
This is all handled by the package provider on your Linux of choice.
https://docs.puppet.com/puppet/latest/types/package.html

Related

How do I install package from amazon-linux-extras using Puppet?

I am trying to set up a Puppet module to install PHP 7.3 on Amazon Linux 2. It is available as a amazon-linux-extras package.
I could simply install it using CLI:
amazon-linux-extras install php7.3
But I would like to define it as a package and ensure it is installed, like this:
package { "php7.3":
ensure => installed,
provider => 'amazon-linux-extras'
}
Unfortunately I cannot set package provider to amazon-linux-extras as such provider doesn't exist.
What would be the correct way to install this package?
At this time, it appears that Puppet does not support the amazon-linux-extras utility.
Arguably, a new type/provider should be created to support amazon-linux-extras. It could live in Puppet Core, if you raised a feature request that is accepted. Or, you could write your own and release it as a module on the Puppet Forge, if you know how write custom types and providers.
In the mean time, it is easy to write a defined type to solve this problem using exec.
define al::amazon_linux_extras(
Enum['present'] $ensure = present,
) {
$pkg = $name
exec { "amazon-linux-extras install -y $pkg":
unless => "amazon-linux-extras list | grep -q '${pkg}=.*enabled'",
path => '/usr/bin',
}
}
Usage:
al::amazon_linux_extras { 'php7.3':
ensure => present,
}
Further explanation:
I assumed you would place your defined type in a module al. But it could be a profile etc. E.g. profile::amazon_linux_extras is another possibility.
I implemented ensure => present for readability only, i.e. it doesn't actually do anything, and also in case you decide to later implement ensure => absent or ensure => latest etc.

apt-puppetlabs do repos first

on my puppetserver i use the puppetlabs-apt module to configure the repos. And i use hiera to get the data for the repos. If i run it i get the message that dirmngr cant be installed cause it cant be found in the repos.
That error comes because puppet is trying to install dirmngr before hes doing the repos. And dirmngr is required in the module.
Is there a way to force the the module to do the repos first and then let it install dirmngr?
my code is like this
class {'apt':
purge =>{
"/etc/apt/sources.list =>true",
},
}
If I understand the problem, you should be able to do something like this:
$dirmngr_apt_source = ...
class { 'apt':
purge => {
"/etc/apt/sources.list" => true
}
}
Apt::Source[$dirmngr_apt_source] -> Package['dirmngr']
Further explanation:
The variable $dirmngr_apt_source is for you to fill in with the Apt source that the dirmngr package lives in. (Full disclaimer: I don't know much about Ubuntu.)
Although the Apt class declares the resources Apt::Source[$dirmngr_apt_source] and Package['dirmngr'], you can still declare relationships between those resources from outside the class, as I did there.
Also, this is a bit of a hack in my opinion, and it sounds like this is possibly a bug or a design flaw in the Apt module.
That is to say, considering that the Apt module manages a Linux node's Apt sources, and the dirmngr package depends on Apt sources, there shouldn't be an assumption in the module that the dirmngr package can be found prior to configuration of the Apt sources. (Or if it's a valid assumption, then perhaps it needs better documentation?)
So, you might consider raising a bug or checking if there's already a bug.

RPM fails to follow dependency order on install

I'm trying to force rpm to follow a given install order and it is not working as expected. The Requires clause I added is not being respected.
I am doing a bare-metal Linux installer (openSUSE 42.2-based). A whole system -- hundreds of packages -- are installed with one RPM command (using --root). I am having problems with three packages -- pam-config, pam-script, and openssh. The pam-config %post scriptlet tries to modify files contained in pam-script and openssh but is installed, by default, before them. It does not have dependencies by default, so, having the source, I rectified that by adding:
Requires: pam-script
Requires: openssh
to pam-config.spec. (I also tried Prereq: with same results.) As expected, with this change, it switches the ordering for pam-script and that error goes away. But it steadfastly refuses to change the order of installation for openssh, which is installed two packages after pam-config. [Openssh is dependent on coreutils and shadow (pwdutil), both of which are already installed at this point. It's also dependent (PreReq) on a mysterious macro, %{fillup_prereq}.]
Everything else installs (and runs) just fine, but I would like to understand better how rpm works. I thought if I used Required: to specify openssh in pam-config, that openssh would invariably be installed before pam-config. It worked for pam-script.
rpm -qp --requires on the .rpm file shows openssh. I repeated the install with the -vv option instead of -v. I can see the Requires: for openssh listed just the same as pam-script (YES (added provide)). I see a pam-config-0.91xxx -> openssh-7.2p2xxx listed under SCC #8: 11 members (100 external dependencies). I see the install of pam-config, which has no dependency information and nothing remarkable except for the %post scriptlet command that generates the error (pam-config --service sshd --delete --listfile). What other kind of things should I be looking at to debug this? What are these SCCs? Am I missing something about Requires? Or is there something obscure I may have overlooked, like circular, indirect, or hidden dependencies (I've checked for that, but ruled it out)? I've looked at several RPM tutorials and done a number of web searches and come up empty.
UPDATE: It appears that unlike pam-script, openssh is caught up in a mutual-dependency critical section. Here is the order of the packages actually being installed:
ruby2.1-rubygem-ruby-dbus-0.9.3-4.3.x86_64.rpm
pam-script-1.1.6-1.os42.gb01.x86_64.rpm
suse-module-tools-12.4-3.2.x86_64.rpm
kmod-17-6.2.x86_64.rpm
kmod-compat-17-6.2.x86_64.rpm
libcurl4-7.37.0-15.1.x86_64.rpm
pam-config-0.91-1.2.os42.gb01.x86_64.rpm
systemd-sysvinit-228-15.1.x86_64.rpm
krb5-1.12.5-5.13.x86_64.rpm
openssh-7.2p2-6.1.SBC.os42.gb01.x86_64.rpm
dracut-044-12.1.x86_64.rpm
systemd-228-15.1.x86_64.rpm
If I stage an installation on a production system and stop just before pam-config, it complains about being dependent on krb5, which is in the future! If I stop at ruby, it works. If I stop at pam-script, it works. If I stop at suse-module-tools, it complains about dependencies on dracut. So I'm wondering if RPM abandons its ordering principle within a mutual-dependency critical section, or if there is a dependency I haven't uncovered yet. I am using rpm -q --requires and rpm -q --provides to work this out. Stay tuned.
You can add more explicit sub-fields to the Requires tag, e.g. Requires(post): openssh-server or Requires(pre,post): openssh-server.
A single RPM transaction isn't really atomic, but is treated that way. Without this additional information, it just ensures that the packages are installed by the end of this transaction, which is "good enough" most of the time.
Another option is to put the required configuration into a %triggerin stanza, which I believe only executes once both packages are installed.

CentOS 7 and Puppet unable to install nc

I am having a weird issue with having puppet enforce the package nc.
I installed it manually in the end via: yum install nc
I see puppet does it via:
/usr/bin/yum -d 0 -e 0 -y list nc
Returns: Error: No matching Packages to list
I have tested this by command line as well:
yum list nc
Returns Error: No matching Packages to list
Yet, when I do:
yum install nc
Returns: Package 2:nmap-ncat-6.40-4.el7.x86_64 already installed and latest version
What am I missing?
Nc is a link to nmap-ncat.
It would be nice to use nmap-ncat in your puppet, because NC is a virtual name of nmap-ncat.
Puppet cannot understand the links/virtualnames
your puppet should be:
package {
'nmap-ncat':
ensure => installed;
}
yum install nmap-ncat.x86_64
resolved my problem
You can use a case in this case, to separate versions
one example is using FACT os (which returns the version etc of your system...
the command facter will return the details:
root#sytem# facter -p os
{"name"=>"CentOS", "family"=>"RedHat", "release"=>{"major"=>"7", "minor"=>"0", "full"=>"7.0.1406"}}
#we capture release hash
$curr_os = $os['release']
case $curr_os['major'] {
'7': { .... something }
*: {something}
}
That is an fast example, Might have typos, or not exactly working.
But using system facts you can see what happens.
The OS fact provides you 3 main variables: name, family, release... Under release you have a small dictionary with more information about your os! combining these you can create cases to meet your targets.

Installing several RPMs with custom install flags

I'm trying to do the initial work to get our dev shop to start using vagrant + puppet during development. At this stage in my puppet manifest development, I need to install several RPMs that are available via an internal http server (not a repo) with very specific flags ('--nodeps').
So, here's an example of what I need to install:
http://1.2.3.4/bar/package1.rpm
http://1.2.3.4/bar/package2.rpm
http://1.2.3.4/bar/package3.rpm
I would normally install them in this way:
rpm --install --nodeps ${rpm_uri}
I would like to be able to do something like this
$custom_rpms = [
'http://1.2.3.4/bar/package1.rpm',
'http://1.2.3.4/bar/package2.rpm',
'http://1.2.3.4/bar/package3.rpm',
]
# edit: just realized I was instantiating the parameterized
# class wrong. :)
class { 'custom_package': package_file => $custom_rpms }
With this module
# modules/company_packages/manifests/init.pp
define company_package($package_file) {
exec { "/bin/rpm --install --nodeps ${package_file} --nodeps" }
}
But, I'm not sure if that's right. Can some of you puppet masters (no pun intended) school me on how this should be done?
You may have already worked around this by now, but if not.
Using a repository is the preferred method as it will autoresolve all the dependancies, but it that's not available you can try the following. (I'm using epel as an example rpm)
package {"epel-release":
provider=>rpm,
ensure=>installed,
install_options => ['--nodeps'],
source=>"http://download.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm",
}
It used to be that 'install_options ' was only supported in windows.
It appears that it is now supported in linux.
If there is sequence that would be helpful, add "require=Package["package3.rpm"]," to sequence.
Answered by Randm over irc.freenode.net#puppet
Create or use an existing repo and install them with yum so that it resolves the dependencies for you.

Resources