File: nim.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (291 lines) | stat: -rw-r--r-- 12,375 bytes parent folder | download
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
284
285
286
287
288
289
290
291
require_relative '../../../puppet/provider/package'
require_relative '../../../puppet/util/package'

Puppet::Type.type(:package).provide :nim, :parent => :aix, :source => :aix do
  desc "Installation from an AIX NIM LPP source.  The `source` parameter is required
      for this provider, and should specify the name of a NIM `lpp_source` resource
      that is visible to the puppet agent machine.  This provider supports the
      management of both BFF/installp and RPM packages.

      Note that package downgrades are *not* supported; if your resource specifies
      a specific version number and there is already a newer version of the package
      installed on the machine, the resource will fail with an error message."

  # The commands we are using on an AIX box are installed standard
  # (except nimclient) nimclient needs the bos.sysmgt.nim.client fileset.
  commands    :nimclient  => "/usr/sbin/nimclient",
              :lslpp      => "/usr/bin/lslpp",
              :rpm        => "rpm"

  # If NIM has not been configured, /etc/niminfo will not be present.
  # However, we have no way of knowing if the NIM server is not configured
  # properly.
  confine  :exists => "/etc/niminfo"

  has_feature :versionable

  attr_accessor :latest_info



  def self.srclistcmd(source)
    [ command(:nimclient), "-o", "showres", "-a", "installp_flags=L", "-a", "resource=#{source}" ]
  end

  def uninstall
    output = lslpp("-qLc", @resource[:name]).split(':')
    # the 6th index in the colon-delimited output contains a " " for installp/BFF
    # packages, and an "R" for RPMS.  (duh.)
    pkg_type = output[6]

    case pkg_type
    when " "
      installp "-gu", @resource[:name]
    when "R"
      rpm "-e", @resource[:name]
    else
      self.fail(_("Unrecognized AIX package type identifier: '%{pkg_type}'") % { pkg_type: pkg_type })
    end

    # installp will return an exit code of zero even if it didn't uninstall
    # anything... so let's make sure it worked.
    unless query().nil?
      self.fail _("Failed to uninstall package '%{name}'") % { name: @resource[:name] }
    end
  end

  def install(useversion = true)
    source = @resource[:source]
    unless source
      self.fail _("An LPP source location is required in 'source'")
    end

    pkg = @resource[:name]

    version_specified = (useversion and (! @resource.should(:ensure).is_a? Symbol))

    # This is unfortunate for a couple of reasons.  First, because of a subtle
    # difference in the command-line syntax for installing an RPM vs an
    # installp/BFF package, we need to know ahead of time which type of
    # package we're trying to install.  This means we have to execute an
    # extra command.
    #
    # Second, the command is easiest to deal with and runs fastest if we
    # pipe it through grep on the shell.  Unfortunately, the way that
    # the provider `make_command_methods` metaprogramming works, we can't
    # use that code path to execute the command (because it treats the arguments
    # as an array of args that all apply to `nimclient`, which fails when you
    # hit the `|grep`.)  So here we just call straight through to P::U.execute
    # with a single string argument for the full command, rather than going
    # through the metaprogrammed layer.  We could get rid of the grep and
    # switch back to the metaprogrammed stuff, and just parse all of the output
    # in Ruby... but we'd be doing an awful lot of unnecessary work.
    showres_command = "/usr/sbin/nimclient -o showres -a resource=#{source} |/usr/bin/grep -p -E "
    if (version_specified)
      version = @resource.should(:ensure)
      showres_command << "'#{Regexp.escape(pkg)}( |-)#{Regexp.escape(version)}'"
    else
      version = nil
      showres_command << "'#{Regexp.escape(pkg)}'"
    end
    output = Puppet::Util::Execution.execute(showres_command)


    if (version_specified)
      package_type = determine_package_type(output, pkg, version)
    else
      package_type, version = determine_latest_version(output, pkg)
    end

    if (package_type == nil)

      errmsg = if version_specified
                 _("Unable to find package '%{package}' with version '%{version}' on lpp_source '%{source}'") %
                     { package: pkg, version: version, source: source }
               else
                 _("Unable to find package '%{package}' on lpp_source '%{source}'") % { package: pkg, source: source }
               end
      self.fail errmsg
    end

    # This part is a bit tricky.  If there are multiple versions of the
    # package available, then `version` will be set to a value, and we'll need
    # to add that value to our installation command.  However, if there is only
    # one version of the package available, `version` will be set to `nil`, and
    # we don't need to add the version string to the command.
    if (version)
      # Now we know if the package type is RPM or not, and we can adjust our
      # `pkg` string for passing to the install command accordingly.
      if (package_type == :rpm)
        # RPMs expect a hyphen between the package name and the version number
        version_separator = "-"
      else
        # installp/BFF packages expect a space between the package name and the
        # version number.
        version_separator = " "
      end

      pkg += version_separator + version
    end

    # NOTE: the installp flags here are ignored (but harmless) for RPMs
    output = nimclient "-o", "cust", "-a", "installp_flags=acgwXY", "-a", "lpp_source=#{source}", "-a", "filesets=#{pkg}"

    # If the package is superseded, it means we're trying to downgrade and we
    # can't do that.
    case package_type
    when :installp
      if output =~ /^#{Regexp.escape(@resource[:name])}\s+.*\s+Already superseded by.*$/
        self.fail _("NIM package provider is unable to downgrade packages")
      end
    when :rpm
      if output =~ /^#{Regexp.escape(@resource[:name])}.* is superseded by.*$/
        self.fail _("NIM package provider is unable to downgrade packages")
      end
    end
  end


  private

  ## UTILITY METHODS FOR PARSING `nimclient -o showres` output

  # This makes me very sad.  These regexes seem pretty fragile, but
  # I spent a lot of time trying to figure out a solution that didn't
  # require parsing the `nimclient -o showres` output and was unable to
  # do so.
  self::HEADER_LINE_REGEX      = /^([^\s]+)\s+[^@]+@@(I|R|S):(\1)\s+[^\s]+$/
  self::PACKAGE_LINE_REGEX     = /^.*@@(I|R|S):(.*)$/
  self::RPM_PACKAGE_REGEX      = /^(.*)-(.*-\d+\w*) \2$/
  self::INSTALLP_PACKAGE_REGEX = /^(.*) (.*)$/

  # Here is some sample output that shows what the above regexes will be up
  # against:
  # FOR AN INSTALLP(bff) PACKAGE:
  #
  #    mypackage.foo                                                           ALL  @@I:mypackage.foo _all_filesets
  #    + 1.2.3.4  MyPackage Runtime Environment                       @@I:mypackage.foo 1.2.3.4
  #    + 1.2.3.8  MyPackage Runtime Environment                       @@I:mypackage.foo 1.2.3.8
  #
  # FOR AN INSTALLP(bff) PACKAGE with security update:
  #
  #    bos.net                                                                 ALL  @@S:bos.net _all_filesets
  #    + 7.2.0.1  TCP/IP ntp Applications                             @@S:bos.net.tcp.ntp 7.2.0.1
  #    + 7.2.0.2  TCP/IP ntp Applications                             @@S:bos.net.tcp.ntp 7.2.0.2
  #
  # FOR AN RPM PACKAGE:
  #
  # mypackage.foo                                                                ALL  @@R:mypackage.foo _all_filesets
  #   @@R:mypackage.foo-1.2.3-1 1.2.3-1
  #   @@R:mypackage.foo-1.2.3-4 1.2.3-4
  #   @@R:mypackage.foo-1.2.3-8 1.2.3-8


  # Parse the output of a `nimclient -o showres` command.  Returns a two-dimensional
  # hash, where the first-level keys are package names, the second-level keys are
  # version number strings for all of the available version numbers for a package,
  # and the values indicate the package type (:rpm / :installp)
  def parse_showres_output(showres_output)
    paragraphs = split_into_paragraphs(showres_output)
    packages = {}
    paragraphs.each do |para|
      lines = para.split(/$/)
      parse_showres_header_line(lines.shift)
      lines.each do |l|
        package, version, type = parse_showres_package_line(l)
        packages[package] ||= {}
        packages[package][version] = type
      end
    end
    packages
  end

  # This method basically just splits the multi-line input string into chunks
  # based on lines that contain nothing but whitespace.  It also strips any
  # leading or trailing whitespace (including newlines) from the resulting
  # strings and then returns them as an array.
  def split_into_paragraphs(showres_output)
    showres_output.split(/^\s*$/).map { |p| p.strip! }
  end

  def parse_showres_header_line(line)
    # This method doesn't produce any meaningful output; it's basically just
    # meant to validate that the header line for the package listing output
    # looks sane, so we know we're dealing with the kind of output that we
    # are capable of handling.
    unless line.match(self.class::HEADER_LINE_REGEX)
      self.fail _("Unable to parse output from nimclient showres: line does not match expected package header format:\n'%{line}'") % { line: line }
    end
  end

  def parse_installp_package_string(package_string)
    match = package_string.match(self.class::INSTALLP_PACKAGE_REGEX)
    unless match
      self.fail _("Unable to parse output from nimclient showres: package string does not match expected installp package string format:\n'%{package_string}'") % { package_string: package_string }
    end
    package_name = match.captures[0]
    version = match.captures[1]
    [package_name, version, :installp]
  end

  def parse_rpm_package_string(package_string)
    match = package_string.match(self.class::RPM_PACKAGE_REGEX)
    unless match
      self.fail _("Unable to parse output from nimclient showres: package string does not match expected rpm package string format:\n'%{package_string}'") % { package_string: package_string }
    end
    package_name = match.captures[0]
    version = match.captures[1]
    [package_name, version, :rpm]
  end

  def parse_showres_package_line(line)
    match = line.match(self.class::PACKAGE_LINE_REGEX)
    unless match
      self.fail _("Unable to parse output from nimclient showres: line does not match expected package line format:\n'%{line}'") % { line: line }
    end

    package_type_flag = match.captures[0]
    package_string = match.captures[1]

    case package_type_flag
      when "I","S"
        parse_installp_package_string(package_string)
      when "R"
        parse_rpm_package_string(package_string)
      else
        self.fail _("Unrecognized package type specifier: '%{package_type_flag}' in package line:\n'%{line}'") % { package_type_flag: package_type_flag, line: line }
    end
  end

  # Given a blob of output from `nimclient -o showres` and a package name,
  # this method checks to see if there are multiple versions of the package
  # available on the lpp_source.  If there are, the method returns
  # [package_type, latest_version] (where package_type is one of :installp or :rpm).
  # If there is only one version of the package available, it returns
  # [package_type, nil], because the caller doesn't need to pass the version
  # string to the command-line command if there is only one version available.
  # If the package is not available at all, the method simply returns nil (instead
  # of a tuple).
  def determine_latest_version(showres_output, package_name)
    packages = parse_showres_output(showres_output)
    unless packages.has_key?(package_name)
      return nil
    end
    if (packages[package_name].count == 1)
      version = packages[package_name].keys[0]
      return packages[package_name][version], nil
    else
      versions = packages[package_name].keys
      latest_version = (versions.sort { |a, b| Puppet::Util::Package.versioncmp(b, a) })[0]
      return packages[package_name][latest_version], latest_version
    end
  end

  def determine_package_type(showres_output, package_name, version)
    packages = parse_showres_output(showres_output)
    unless (packages.has_key?(package_name) and packages[package_name].has_key?(version))
      return nil
    end
    packages[package_name][version]
  end
end