I was chatting with someone recently about some security maintenance tasks and they were bemoaning that some software updates had turned into a yack shaving1. Updating this required updating that, required updating that on N hosts of varying platforms and flavors. So, they asked me how could they avoid updating a specific package and naturally I said, let’s just prototype some policy.
The incipiency of said yak shaving was updating packages via apt
, Debian flavored systems default package manager. apt upgrade
is being used to update all packages, but we need to exclude a specific package from the updates until the dependency chain has been resolved.
What we are talking about is simply package pinning. This can be done by creating a file /etc/apt/preferences.d/yourpackage
.
Package: yourpackage
Pin: version 1.2.3
Pin-Priority: 999
Iteration 0 - A very simple prototype
We can write a very simple policy to achieve this. The most simple solution that I could think of looked like this:
bundle agent __main__
{
files:
"/etc/apt/preferences.d/zpackage"
content => "Package: zpackage
Pin: version 1.2.3
Pin-Priority: 999
";
}
It’s not an ideal policy, it’s just a simple prototype. Let’s iterate on it a bit.
Iteration 1 - Specifying the proper file permissions
First, what about permissions? By default CFEngine will create files with mode of 600
(Owner read-write, group nothing, other nothing). I failed to find any canonical reference as to what the permissions for the file should be, but intuitively 644
(owner read-write, group read, other read) with user and group owners being root
seems appropriate and comports with the default permissions of a the directory on some spot checked Debian systems.2
bundle agent __main__
{
files:
"/etc/apt/preferences.d/zpackage"
perms => default:mog( "644", "root", "root" ),
content => "Package: zpackage
Pin: version 1.2.3
Pin-Priority: 999
";
}
I don’t like that way that looks, I find the string wrapping ugly.
Iteration 2 - Use concat() to make the policy more readable
We can use concat()
to make things align.
bundle agent __main__
{
files:
"/etc/apt/preferences.d/zpackage"
perms => default:mog( "644", "root", "root" ),
content => concat( "Package: zpackage", "$(const.n)",
"Pin: version 1.2.3", "$(const.n)",
"Pin-Priority: 999", "$(const.n)" );
}
We are repeating the package name, that’s not very DRY.3
Iteration 3 - Use with
to avoid repetition
We can use the with
attribute to avoid a case of repetition.
bundle agent __main__
{
files:
"/etc/apt/preferences.d/$(with)"
perms => default:mog( "644", "root", "root" ),
with => "zpackage",
content => concat( "Package: $(with)", "$(const.n)",
"Pin: version 1.2.3", "$(const.n)",
"Pin-Priority: 999", "$(const.n)" );
}
That’s about as far as I can get at the current level of abstraction. It’s still not reusable, let’s move it to a bundle so that it’s more reusable.
Iteration 4 - Use a parameterized bundle to make the policy reusable
Here we move the promise to it’s own agent bundle apt_package_version_pinned
taking parameters for the name of the package and the version we wish to pin.
bundle agent __main__
{
methods:
"Ensure apt package is pinned to version"
usebundle => apt_package_version_pinned( "zpackage", "1.2.3" );
}
bundle agent apt_package_version_pinned( name, version )
# @brief Ensure that `name` is pinned to `version` for the apt package manager
# @param `name` The name of the package to pin
# @param `version` The name of the package to pin
{
files:
"/etc/apt/preferences.d/$(name)"
perms => default:mog( "644", "root", "root" ),
content => concat( "Package: $(name)", "$(const.n)",
"Pin: version $(version)", "$(const.n)",
"Pin-Priority: 999", "$(const.n)" );
}
What about when I am ready to clear this pinned configuration? Let’s make it’s pinned state configurable.
Iteration 5 - Adding a parameter to specify the state of the pin
Here we have added the present_absent
parameter to the bundle which let’s us specify if we want the package pin configuration to be present or absent. This iteration also added a promise to ensure the configuration directory itself was present with appropriate permissions when we are requesting a present state and the configuration file also gets a header comment indicating CFEngine manages the file.
bundle agent __main__
{
methods:
"Ensure apt package is pinned to version"
usebundle => apt_package_version_pinned( "zpackage", "1.2.3", "present" );
}
bundle agent apt_package_version_pinned( name, version, present_absent )
# @brief Ensure that `name` is pinned to `version` for the apt package manager or not based on the desired state
# @param `name` The name of the package to pin, case matters.
# @param `version` The version of the package to pin
# @param `present_absent` The desired state of the package pin (present|absent), case does not matter. Note: the version does not matter when absent is specified.
{
files:
"/etc/apt/preferences.d/."
perms => default:mog( "755", "root", "root" ),
if => regcmp( "(?i)present", "$(present_absent)" ),
comment => concat( "If we are promising that a package",
" should be pinned, the configuration",
" directory should exist." );
"/etc/apt/preferences.d/$(name)"
perms => default:mog( "644", "root", "root" ),
content => concat( "# Managed by CFEngine", "$(const.n)",
"Package: $(name)", "$(const.n)",
"Pin: version $(version)", "$(const.n)",
"Pin-Priority: 999", "$(const.n)" ),
if => regcmp( "(?i)present", "$(present_absent)" );
"/etc/apt/preferences.d/$(name)"
delete => default:tidy,
if => regcmp( "(?i)absent", "$(present_absent)" );
}
Now we have a re-usable bundle which can be used to specify a packages that should be pinned to a specific version or not.
The next logical step would be to elevate this policy to a CFEngine Build4 module so that it’s easier for others to access and use. Will someone beat me to the punch?
-
Yack shaving, a task that leads you to perform another related task and so on, and so on – distracting from the original goal. https://americanexpress.io/yak-shaving/ ↩︎
-
I would appreciate it greatly if you would point me to a canonical answer for what the proper permissions for these configuration files should be. I was unable to find any specific mention across Debian bug trackers and man pages. ↩︎
-
DRY: Don’t Replete Yourself https://en.wikipedia.org/wiki/Don%27t_repeat_yourself ↩︎
-
The CFEngine Build website and main index of modules https://build.cfengine.com ↩︎