aboutsummaryrefslogtreecommitdiff
path: root/modules/base_installation/lib/puppet/provider/package/pacman.rb
blob: 0a5e5d0d1f10c9efa1087b9f353f2bfe054e0ad9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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