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
|