]> git.immae.eu Git - perso/Immae/Projets/Puppet.git/blobdiff - modules/base_installation/lib/puppet/provider/package/pacman.rb
Add monitoring
[perso/Immae/Projets/Puppet.git] / modules / base_installation / lib / puppet / provider / package / pacman.rb
diff --git a/modules/base_installation/lib/puppet/provider/package/pacman.rb b/modules/base_installation/lib/puppet/provider/package/pacman.rb
new file mode 100644 (file)
index 0000000..0a5e5d0
--- /dev/null
@@ -0,0 +1,283 @@
+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