require 'puppet/provider/package' require 'set' require 'uri' Puppet::Type.type(:package).provide :pacman, :parent => Puppet::Provider::Package do desc "Support for the Package Manager Utility (pacman) used in Archlinux. This provider supports the `install_options` attribute, which allows command-line flags to be passed to pacman. These options should be specified as a string (e.g. '--flag'), a hash (e.g. {'--flag' => 'value'}), or an array where each element is either a string or a hash." # If aura is installed, we can make use of it def self.aura? @aura ||= Puppet::FileSystem.exist?('/usr/bin/aura') end commands :pacman => "/usr/bin/pacman" # Aura is a common AUR helper which, if installed, we can use to query the AUR commands :aura => "/usr/bin/aura" if aura? confine :operatingsystem => [:archlinux, :manjarolinux] defaultfor :operatingsystem => [:archlinux, :manjarolinux] has_feature :install_options has_feature :uninstall_options has_feature :upgradeable has_feature :virtual_packages # Checks if a given name is a group def self.group?(name) begin !pacman("-Sg", name).empty? rescue Puppet::ExecutionFailure # pacman returns an expected non-zero exit code when the name is not a group false end end # Install a package using 'pacman', or 'aura' if available. # Installs quietly, without confirmation or progress bar, updates package # list from servers defined in pacman.conf. def install if @resource[:source] install_from_file else install_from_repo end unless self.query fail(_("Could not find package '%{name}'") % { name: @resource[:name] }) end end # Fetch the list of packages and package groups that are currently installed on the system. # Only package groups that are fully installed are included. If a group adds packages over time, it will not # be considered as fully installed any more, and we would install the new packages on the next run. # If a group removes packages over time, nothing will happen. This is intended. def self.instances instances = [] # Get the installed packages installed_packages = get_installed_packages installed_packages.sort_by { |k, _| k }.each do |package, version| instances << new(to_resource_hash(package, version)) end # Get the installed groups get_installed_groups(installed_packages).each do |group, version| instances << new(to_resource_hash(group, version)) end instances end # returns a hash package => version of installed packages def self.get_installed_packages begin packages = {} execpipe([command(:pacman), "-Q"]) do |pipe| # pacman -Q output is 'packagename version-rel' regex = %r{^(\S+)\s(\S+)} pipe.each_line do |line| if match = regex.match(line) packages[match.captures[0]] = match.captures[1] else warning(_("Failed to match line '%{line}'") % { line: line }) end end end packages rescue Puppet::ExecutionFailure fail(_("Error getting installed packages")) end end # returns a hash of group => version of installed groups def self.get_installed_groups(installed_packages, filter = nil) groups = {} begin # Build a hash of group name => list of packages command = [command(:pacman), "-Sgg"] command << filter if filter execpipe(command) do |pipe| pipe.each_line do |line| name, package = line.split packages = (groups[name] ||= []) packages << package end end # Remove any group that doesn't have all its packages installed groups.delete_if do |_, packages| !packages.all? { |package| installed_packages[package] } end # Replace the list of packages with a version string consisting of packages that make up the group groups.each do |name, packages| groups[name] = packages.sort.map {|package| "#{package} #{installed_packages[package]}"}.join ', ' end rescue Puppet::ExecutionFailure # pacman returns an expected non-zero exit code when the filter name is not a group raise unless filter end groups end # Because Archlinux is a rolling release based distro, installing a package # should always result in the newest release. def update # Install in pacman can be used for update, too self.install end # We rescue the main check from Pacman with a check on the AUR using aura, if installed def latest # Synchronize the database pacman "-Sy" resource_name = @resource[:name] # If target is a group, construct the group version return pacman("-Sp", "--print-format", "%n %v", resource_name).lines.map{ |line| line.chomp }.sort.join(', ') if self.class.group?(resource_name) # Start by querying with pacman first # If that fails, retry using aura against the AUR pacman_check = true begin if pacman_check output = pacman "-Sp", "--print-format", "%v", resource_name return output.chomp else output = aura "-Ai", resource_name output.split("\n").each do |line| return line.split[2].chomp if line.split[0] =~ /Version/ end end rescue Puppet::ExecutionFailure if pacman_check and self.class.aura? pacman_check = false # now try the AUR retry else raise end end end # Queries information for a package or package group def query installed_packages = self.class.get_installed_packages resource_name = @resource[:name] # Check for the resource being a group version = self.class.get_installed_groups(installed_packages, resource_name)[resource_name] if version unless @resource.allow_virtual? warning(_("%{resource_name} is a group, but allow_virtual is false.") % { resource_name: resource_name }) return nil end else version = installed_packages[resource_name] end # Return nil if no package or group found return nil unless version self.class.to_resource_hash(resource_name, version) end def self.to_resource_hash(name, version) { :name => name, :ensure => version, :provider => self.name } end # Removes a package from the system. def uninstall resource_name = @resource[:name] is_group = self.class.group?(resource_name) fail(_("Refusing to uninstall package group %{resource_name}, because allow_virtual is false.") % { resource_name: resource_name }) if is_group && !@resource.allow_virtual? cmd = %w{--noconfirm --noprogressbar} cmd += uninstall_options if @resource[:uninstall_options] cmd << "-R" cmd << '-s' if is_group cmd << resource_name if self.class.aura? aura(*cmd) else pacman(*cmd) end end private def install_with_aura? resource_name = @resource[:name] if !self.class.aura? return false end begin pacman "-Sp", resource_name return false rescue Puppet::ExecutionFailure return true end end def install_options join_options(@resource[:install_options]) end def uninstall_options join_options(@resource[:uninstall_options]) end def install_from_file source = @resource[:source] begin source_uri = URI.parse source rescue => detail self.fail Puppet::Error, _("Invalid source '%{source}': %{detail}") % { source: source, detail: detail }, detail end source = case source_uri.scheme when nil then source when /https?/i then source when /ftp/i then source when /file/i then source_uri.path when /puppet/i fail _("puppet:// URL is not supported by pacman") else fail _("Source %{source} is not supported by pacman") % { source: source } end pacman "--noconfirm", "--noprogressbar", "-Sy" pacman "--noconfirm", "--noprogressbar", "-U", source end def install_from_repo resource_name = @resource[:name] # Refuse to install if not allowing virtual packages and the resource is a group fail(_("Refusing to install package group %{resource_name}, because allow_virtual is false.") % { resource_name: resource_name }) if self.class.group?(resource_name) && !@resource.allow_virtual? cmd = %w{--noconfirm --needed} cmd += install_options if @resource[:install_options] if install_with_aura? cmd << "-Aq" << resource_name aura(*cmd) else cmd << "--noprogressbar" cmd << "-Sy" << resource_name pacman(*cmd) end end end