From: Giovanni Bechis Subject: Re: Overhaul package handling in puppet 8 To: Sebastian Reitenbach Cc: ports@openbsd.org, kn@openbsd.org, atalaran@gmail.com Date: Wed, 10 Apr 2024 08:26:54 +0200 On 4/10/24 07:10, Sebastian Reitenbach wrote: > Hi, > On Tuesday, April 09, 2024 22:43 CEST, Giovanni Bechis wrote: > >> On Tue, Apr 09, 2024 at 10:17:30PM +0200, Sebastian Reitenbach wrote: >>> Since we now have a recent Puppet in ports, I started looking at how packages are handled with Puppet. >>> My current trouble is that it wasn't really possible to install banches properly: i.e. can't properly install gimp, >>> or auto* based on branch. Or esp, when want to install multiple of them, it was just not possible. >>> For ports where branches conflict, i.e. postfix, this was working, but had to specify exact version, and on every >>> upgrade bump the version .... _very_ annoying. >>> >>> Currently Puppet allows to install packages of a given version (ensure => "X.Y.Z"), or to follow updates (ensure => "latest"). >>> >> [...] >>> This is just for Puppet 8. Anyone still on Puppet 7? It should be easily ported to Puppet 7 as well. >>> >> I am on Puppet 7 and I do not have time to upgrade to Puppet 8 soon; I > > My upgrade from Puppet 7 to 8 took quite a long time. They removed a lot of backward compat > shim, and if you have many old and partially hand made or patched modules like I do, it took > quite a while to update all of them.... > upgrading is on my queue and I have lot of hand made modules, other then that, can a Puppet 7 client connect to a Puppet 8 server ? > Attached an untested version of the same changes for the Puppet 7 port. > >> am also using "ensure => latest" syntax but I can change my code to get >> rid of it OpenBSD will no more support it. > > What's your use-case to using "ensure => latest"? If there's really a "good one", > I can look into re-adding the feature. > My use-case up to now for sparingly using "latest" was to force using "snapshot" > branch of some packages. > my OpenBSD Puppet server manages Linux and Windows servers as well, on those OS I want to update automatically only some trusted packages, not all the packages the distro wants to update. Anyway I can change that part of my code without big issues. Cheers Giovanni >> >> Cheers >> Giovanni >> >> >>> cheers, >>> Sebastian >>> >>> >>> Index: Makefile >>> =================================================================== >>> RCS file: /cvs/ports/sysutils/ruby-puppet/8/Makefile,v >>> diff -u -r1.2 Makefile >>> --- Makefile 20 Mar 2024 21:21:14 -0000 1.2 >>> +++ Makefile 9 Apr 2024 19:51:40 -0000 >>> @@ -1,6 +1,7 @@ >>> PORTROACH= limit:^7 >>> >>> VERSION= 8.5.1 >>> +REVISION= 0 >>> >>> RUN_DEPENDS+= converters/ruby-multi_json,${MODRUBY_FLAVOR}>=1.13,<2 \ >>> devel/ruby-concurrent-ruby,${MODRUBY_FLAVOR}>=1,<2 \ >>> Index: patches/patch-lib_puppet_provider_package_openbsd_rb >>> =================================================================== >>> RCS file: /cvs/ports/sysutils/ruby-puppet/8/patches/patch-lib_puppet_provider_package_openbsd_rb,v >>> diff -u -r1.2 patch-lib_puppet_provider_package_openbsd_rb >>> --- patches/patch-lib_puppet_provider_package_openbsd_rb 20 Mar 2024 21:21:14 -0000 1.2 >>> +++ patches/patch-lib_puppet_provider_package_openbsd_rb 9 Apr 2024 19:51:40 -0000 >>> @@ -1,42 +1,89 @@ >>> -- Handle errors from pkg_add >>> -- Handle uninstall_options being 'nil' by default >>> -- If no flavor speficied, force the empty flavor with '--' >>> - but skipping the % un-ambiguity pkg names >>> -- Bail out on shortform PKG_PATH (i.e. 'ftp.openbsd.org') >>> -- pkg.conf is gone >>> -- properly handle packages with multiple versions and flavors, >>> - i.e. postfix-XXX-flavor >>> - >>> +- get rid of versionable (no ensure => "version X.X.X") >>> +- get rid of upgradeable (ensure => latest) >>> +- properly support branches >>> >>> Index: lib/puppet/provider/package/openbsd.rb >>> --- lib/puppet/provider/package/openbsd.rb.orig >>> +++ lib/puppet/provider/package/openbsd.rb >>> -@@ -24,6 +24,8 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> - has_feature :upgradeable >>> +@@ -6,10 +6,14 @@ require_relative '../../../puppet/provider/package' >>> + Puppet::Type.type(:package).provide :openbsd, :parent => Puppet::Provider::Package do >>> + desc "OpenBSD's form of `pkg_add` support. >>> + >>> ++ OpenBSD has the concept of package branches, providing multiple versions of the >>> ++ same package, i.e. `stable` vs. `snapshot`. To select a specific branch, >>> ++ suffix the package name with % sign follwed by the branch name, i.e. `gimp%stable`. >>> ++ >>> + This provider supports the `install_options` and `uninstall_options` >>> + attributes, which allow command-line flags to be passed to pkg_add and pkg_delete. >>> + These options should be specified as an array where each element is either a >>> +- string or a hash." >>> ++ string or a hash." >>> + >>> + commands :pkginfo => "pkg_info", >>> + :pkgadd => "pkg_add", >>> +@@ -18,220 +22,94 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> + defaultfor 'os.name' => :openbsd >>> + confine 'os.name' => :openbsd >>> + >>> +- has_feature :versionable >>> + has_feature :install_options >>> + has_feature :uninstall_options >>> +- has_feature :upgradeable >>> has_feature :supports_flavors >>> >>> -+ mk_resource_methods >>> -+ >>> def self.instances >>> - packages = [] >>> - >>> -@@ -46,12 +48,6 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> - >>> - packages << new(hash) >>> - hash = {} >>> +- packages = [] >>> +- >>> ++ final = [] >>> + begin >>> +- execpipe(listcmd) do |process| >>> +- # our regex for matching pkg_info output >>> +- regex = /^(.*)-(\d[^-]*)[-]?([\w-]*)(.*)$/ >>> +- fields = [:name, :ensure, :flavor] >>> +- hash = {} >>> ++ packages = listcmd >>> ++ packages.each { |package, value| >>> ++ if !package.empty?() >>> ++ value[:provider] = self.name >>> ++ final << new(value) >>> ++ end >>> ++ } >>> ++ return final >>> + >>> +- # now turn each returned line into a package object >>> +- process.each_line { |line| >>> +- match = regex.match(line.split[0]) >>> +- if match >>> +- fields.zip(match.captures) { |field, value| >>> +- hash[field] = value >>> +- } >>> +- >>> +- hash[:provider] = self.name >>> +- >>> +- packages << new(hash) >>> +- hash = {} >>> - else >>> - unless line =~ /Updating the pkgdb/ >>> - # Print a warning on lines we can't match, but move >>> - # on, since it should be non-fatal >>> - warning(_("Failed to match line %{line}") % { line: line }) >>> - end >>> - end >>> - } >>> - end >>> -@@ -67,26 +63,17 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> +- end >>> +- } >>> +- end >>> +- >>> +- return packages >>> + rescue Puppet::ExecutionFailure >>> +- return nil >>> ++ nil >>> + end >>> end >>> >>> - def latest >>> + def self.listcmd >>> +- [command(:pkginfo), "-a"] >>> +- end >>> +- >>> +- def latest >>> - parse_pkgconf >>> - >>> - if @resource[:source][-1, 1] == ::File::SEPARATOR >>> @@ -45,55 +92,51 @@ >>> - e_vars = {} >>> - end >>> - >>> - if @resource[:flavor] >>> - query = "#{@resource[:name]}--#{@resource[:flavor]}" >>> - else >>> +- if @resource[:flavor] >>> +- query = "#{@resource[:name]}--#{@resource[:flavor]}" >>> +- else >>> - query = @resource[:name] >>> -+ query = @resource[:name] + "--" >>> - end >>> - >>> +- end >>> +- >>> - output = Puppet::Util.withenv(e_vars) { pkginfo "-Q", query } >>> - version = properties[:ensure] >>> -+ output = Puppet::Util.withenv({}) {pkginfo "-Q", query} >>> - >>> - if output.nil? or output.size == 0 or output =~ /Error from / >>> - debug "Failed to query for #{resource[:name]}" >>> +- >>> +- if output.nil? or output.size == 0 or output =~ /Error from / >>> +- debug "Failed to query for #{resource[:name]}" >>> - return version >>> -+ return properties[:ensure] >>> - else >>> - # Remove all fuzzy matches first. >>> - output = output.split.select { |p| p =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*)/ }.join >>> -@@ -95,21 +82,22 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> - >>> - if output =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*) \(installed\)$/ >>> - debug "Package is already the latest available" >>> +- else >>> +- # Remove all fuzzy matches first. >>> +- output = output.split.select { |p| p =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*)/ }.join >>> +- debug "pkg_info -Q for #{resource[:name]}: #{output}" >>> +- end >>> +- >>> +- if output =~ /^#{resource[:name]}-(\d[^-]*)[-]?(\w*) \(installed\)$/ >>> +- debug "Package is already the latest available" >>> - return version >>> -+ return properties[:ensure] >>> - else >>> - match = /^(.*)-(\d[^-]*)[-]?(\w*)$/.match(output) >>> - debug "Latest available for #{resource[:name]}: #{match[2]}" >>> - >>> +- else >>> +- match = /^(.*)-(\d[^-]*)[-]?(\w*)$/.match(output) >>> +- debug "Latest available for #{resource[:name]}: #{match[2]}" >>> +- >>> - if version.to_sym == :absent || version.to_sym == :purged >>> -+ if properties[:ensure].to_sym == :absent >>> - return match[2] >>> - end >>> - >>> - vcmp = version.split('.').map { |s| s.to_i } <=> match[2].split('.').map { |s| s.to_i } >>> -+ vcmp = properties[:ensure].split('.').map{|s|s.to_i} <=> match[2].split('.').map{|s|s.to_i} >>> - if vcmp > 0 >>> - # The locally installed package may actually be newer than what a mirror >>> - # has. Log it at debug, but ignore it otherwise. >>> +- return match[2] >>> +- end >>> +- >>> +- vcmp = version.split('.').map { |s| s.to_i } <=> match[2].split('.').map { |s| s.to_i } >>> +- if vcmp > 0 >>> +- # The locally installed package may actually be newer than what a mirror >>> +- # has. Log it at debug, but ignore it otherwise. >>> - debug "Package #{resource[:name]} #{version} newer then available #{match[2]}" >>> - return version >>> -+ debug "Package #{resource[:name]} #{properties[:ensure]} newer then available #{match[2]}" >>> -+ return properties[:ensure] >>> - else >>> - return match[2] >>> - end >>> -@@ -120,57 +108,25 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> - self.install(true) >>> - end >>> - >>> +- else >>> +- return match[2] >>> +- end >>> +- end >>> +- end >>> +- >>> +- def update >>> +- self.install(true) >>> +- end >>> +- >>> - def parse_pkgconf >>> - unless @resource[:source] >>> - if Puppet::FileSystem.exist?("/etc/pkg.conf") >>> @@ -111,7 +154,20 @@ >>> - end >>> - end >>> - end >>> -- end >>> ++ regex_fuzzy = /^(.*)--([\w-]+)?(%[^w]+)?$/ >>> ++ f = [] >>> ++ f = pkginfo("-a", "-z").split("\n") >>> ++ packages = {} >>> ++ f.each do |line| >>> ++ match = regex_fuzzy.match(line.split[0]) >>> ++ name = match.captures[0] >>> ++ flavor = match.captures[1] >>> ++ branch = match.captures[2] >>> ++ if branch.nil? >>> ++ pname = name >>> ++ else >>> ++ pname = name + branch >>> + end >>> - >>> - unless @resource[:source] >>> - raise Puppet::Error, >>> @@ -121,14 +177,18 @@ >>> - raise Puppet::Error, >>> - _("You must specify a package source or configure an installpath in /etc/pkg.conf") >>> - end >>> -- end >>> -- end >>> -- >>> - def install(latest = false) >>> ++ packages[pname] = { :name => pname, :flavor => flavor, :branch => branch, :ensure => "present" } >>> + end >>> ++ packages >>> + end >>> + >>> +- def install(latest = false) >>> ++ def install >>> cmd = [] >>> >>> - parse_pkgconf >>> -- >>> ++ full_name = get_full_name(action="install") >>> + >>> - if @resource[:source][-1, 1] == ::File::SEPARATOR >>> - e_vars = { 'PKG_PATH' => @resource[:source] } >>> - full_name = get_full_name(latest) >>> @@ -139,27 +199,24 @@ >>> - >>> + cmd << '-r' >>> cmd << install_options >>> -- cmd << full_name >>> -+ cmd << get_full_name(latest) >>> + cmd << full_name >>> >>> - if latest >>> +- if latest >>> - cmd.unshift('-rz') >>> -+ cmd.unshift('-z') >>> - end >>> - >>> -- Puppet::Util.withenv(e_vars) { pkgadd cmd.flatten.compact } >>> + # pkg_add(1) doesn't set the return value upon failure so we have to peek >>> + # at it's output to see if something went wrong. >>> + output = Puppet::Util.withenv({}) { pkgadd cmd.flatten.compact } >>> -+ require 'pp' >>> -+ pp output >>> + if output =~ /Can't find / >>> + self.fail "pkg_add returned: #{output.chomp}" >>> -+ end >>> + end >>> +- >>> +- Puppet::Util.withenv(e_vars) { pkgadd cmd.flatten.compact } >>> end >>> >>> - def get_full_name(latest = false) >>> -@@ -179,11 +135,20 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> +- def get_full_name(latest = false) >>> ++ def get_full_name(action="install") >>> + # In case of a real update (i.e., the package already exists) then >>> + # pkg_add(8) can handle the flavors. However, if we're actually >>> # installing with 'latest', we do need to handle the flavors. This is >>> # done so we can feed pkg_add(8) the full package name to install to >>> # prevent ambiguity. >>> @@ -168,53 +225,35 @@ >>> - elsif latest >>> - # Don't depend on get_version for updates. >>> - @resource[:name] >>> -+ if resource[:flavor] >>> -+ # If :ensure contains a version, use that instead of looking it up. >>> -+ # This allows for installing packages with the same stem, but multiple >>> -+ # version such as postfix-VERSION-flavor. >>> -+ if @resource[:ensure].to_s =~ /(\d[^-]*)$/ >>> -+ use_version = @resource[:ensure] >>> -+ else >>> -+ use_version = '' >>> -+ end >>> -+ "#{resource[:name]}-#{use_version}-#{resource[:flavor]}" >>> -+ elsif resource[:name].to_s.match(/[a-z0-9]%[0-9a-z]/i) >>> -+ "#{resource[:name]}" >>> -+ elsif not latest >>> -+ "#{resource[:name]}--" >>> - else >>> - # If :ensure contains a version, use that instead of looking it up. >>> - # This allows for installing packages with the same stem, but multiple >>> -@@ -194,33 +159,41 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> - use_version = get_version >>> - end >>> +- else >>> +- # If :ensure contains a version, use that instead of looking it up. >>> +- # This allows for installing packages with the same stem, but multiple >>> +- # version such as openldap-server. >>> +- if @resource[:ensure].to_s =~ /(\d[^-]*)$/ >>> +- use_version = @resource[:ensure] >>> +- else >>> +- use_version = get_version >>> +- end >>> >>> - [@resource[:name], use_version, @resource[:flavor]].join('-').gsub(/-+$/, '') >>> -+ if resource[:flavor] >>> -+ [ @resource[:name], use_version, @resource[:flavor]].join('-').gsub(/-+$/, '') >>> -+ else >>> -+ [ @resource[:name], use_version ] >>> -+ end >>> ++ name_branch_regex = /^(\S*)(%\w*)$/ >>> ++ match = name_branch_regex.match(@resource[:name]) >>> ++ if match >>> ++ use_name = match.captures[0] >>> ++ use_branch = match.captures[1] >>> ++ else >>> ++ use_name = @resource[:name] >>> ++ use_branch = '' >>> end >>> - end >>> +- end >>> >>> - def get_version >>> +- def get_version >>> - execpipe([command(:pkginfo), "-I", @resource[:name]]) do |process| >>> - # our regex for matching pkg_info output >>> - regex = /^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ >>> - master_version = 0 >>> - version = -1 >>> -+ pkg_search_name = @resource[:name] >>> -+ unless pkg_search_name.match(/[a-z0-9]%[0-9a-z]/i) and not @resource[:flavor] >>> -+ # we are only called when no flavor is specified >>> -+ # so append '--' to the :name to avoid patch versions on flavors >>> -+ pkg_search_name << "--" >>> -+ end >>> -+ # our regex for matching pkg_info output >>> -+ regex = /^(.*)-(\d[^-]*)[-]?(\w*)(.*)$/ >>> -+ master_version = 0 >>> -+ version = -1 >>> - >>> +- >>> - process.each_line do |line| >>> - match = regex.match(line.split[0]) >>> - if match >>> @@ -224,29 +263,36 @@ >>> - >>> - master_version = version unless master_version > version >>> - end >>> -+ # pkg_info -I might return multiple lines, i.e. flavors >>> -+ matching_pkgs = pkginfo("-I", "pkg_search_name") >>> -+ matching_pkgs.each_line do |line| >>> -+ if match = regex.match(line.split[0]) >>> -+ # now we return the first version, unless ensure is latest >>> -+ version = match.captures[1] >>> -+ return version unless @resource[:ensure] == "latest" >>> -+ master_version = version unless master_version > version >>> - end >>> -+ end >>> - >>> +- end >>> +- >>> - return master_version unless master_version == 0 >>> - return '' if version == -1 >>> -+ return master_version unless master_version == 0 >>> -+ return '' if version == -1 >>> -+ raise Puppet::Error, _("%{version} is not available for this package") % { version: version } >>> - >>> +- >>> - raise Puppet::Error, _("%{version} is not available for this package") % { version: version } >>> -- end >>> - rescue Puppet::ExecutionFailure >>> - return nil >>> ++ if @resource[:flavor] >>> ++ return "#{use_name}--#{@resource[:flavor]}#{use_branch}" >>> ++ else >>> ++ return "#{use_name}--#{use_branch}" >>> + end >>> +- rescue Puppet::ExecutionFailure >>> +- return nil >>> ++ >>> end >>> -@@ -239,7 +212,7 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> + >>> + def query >>> +- # Search for the version info >>> +- if pkginfo(@resource[:name]) =~ /Information for (inst:)?#{@resource[:name]}-(\S+)/ >>> +- return { :ensure => $2 } >>> +- else >>> +- return nil >>> ++ pkg = self.class.instances.find do |package| >>> ++ @resource[:name] == package.name >>> + end >>> ++ pkg ? pkg.properties : nil >>> + end >>> + >>> + def install_options >>> +@@ -239,15 +117,19 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> end >>> >>> def uninstall_options >>> @@ -254,4 +300,27 @@ >>> + [join_options(resource[:uninstall_options])] >>> end >>> >>> - def uninstall >>> +- def uninstall >>> +- pkgdelete uninstall_options.flatten.compact, @resource[:name] >>> ++ def uninstall(purge = false) >>> ++ if purge >>> ++ pkgdelete "-c", "-qq", uninstall_options.flatten.compact, get_full_name(action="uninstall") >>> ++ else >>> ++ pkgdelete uninstall_options.flatten.compact, get_full_name(action="uninstall") >>> ++ end >>> + end >>> + >>> + def purge >>> +- pkgdelete "-c", "-q", @resource[:name] >>> ++ uninstall(purge=true) >>> + end >>> + >>> + def flavor >>> +@@ -256,7 +138,6 @@ Puppet::Type.type(:package).provide :openbsd, :parent >>> + >>> + def flavor=(value) >>> + if flavor != @resource.should(:flavor) >>> +- uninstall >>> + install >>> + end >>> + end >> >>