Skip to content

Commit e96ac6b

Browse files
committed
(#19875) Package descriptions obtained by rpm/dpkg providers
Previously Puppet::Provider::Package::Rpm and Dpkg implementations obtained package information from the system without capturing package description information. This change modifies the rpm and dpkg-query format strings to include descriptions. This can be used for querying package state on a node. The rpm implementation is a simple change relying on a SUMMARY field which is a single line and which does not change the one line per package parsing structure in the Rpm class. The dpkg implementation is a more complicated change because we are using a DESCRIPTION field in dpkg-query's format string, which is a multi-line value. The first line is a summary, and is all we want, but dpkg-query then print's x additional lines of long description. A new parse_multi_line method has been added to treat these multi-line results as a single entry, grab the first line with summary and ignore the excess lines without triggering warnings. Completely invalid package entry results should still trigger warnings. Current dpkg-query versions (as of 1.16.2 from 2012, I believe) have a binary:Summary field which would return this to a single line parse and make it simpler and less error prone again. But earlier Debian/Ubuntu installations don't have this (Ubuntu 12.04 for instance). Dpkg.instance, Dpkg#query both used a Dpkg.dpkgquery_piped to expose an IO pipe for parsing. Regex match used in Rpm to ensure we only consume good query output lines.
1 parent f5bd405 commit e96ac6b

File tree

7 files changed

+506
-172
lines changed

7 files changed

+506
-172
lines changed

lib/puppet/provider/package/dpkg.rb

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,75 @@
1111
commands :dpkg_deb => "/usr/bin/dpkg-deb"
1212
commands :dpkgquery => "/usr/bin/dpkg-query"
1313

14+
# Performs a dpkgquery call with a pipe so that output can be processed
15+
# inline in a passed block.
16+
# @param args [Array<String>] any command line arguments to be appended to the command
17+
# @param block expected to be passed on to execpipe
18+
# @return whatever the block returns
19+
# @see Puppet::Util::Execution.execpipe
20+
# @api private
21+
def self.dpkgquery_piped(*args, &block)
22+
cmd = args.unshift(command(:dpkgquery))
23+
Puppet::Util::Execution.execpipe(cmd, &block)
24+
end
25+
1426
def self.instances
1527
packages = []
1628

1729
# list out all of the packages
18-
cmd = "#{command(:dpkgquery)} -W --showformat '${Status} ${Package} ${Version}\\n'"
19-
Puppet.debug "Executing '#{cmd}'"
20-
Puppet::Util::Execution.execpipe(cmd) do |process|
21-
# our regex for matching dpkg output
22-
regex = %r{^(\S+) +(\S+) +(\S+) (\S+) (\S*)$}
23-
fields = [:desired, :error, :status, :name, :ensure]
24-
hash = {}
25-
26-
# now turn each returned line into a package object
27-
process.each_line { |line|
28-
if hash = parse_line(line)
29-
packages << new(hash)
30-
end
31-
}
30+
dpkgquery_piped('-W', '--showformat', self::DPKG_QUERY_FORMAT_STRING) do |pipe|
31+
until pipe.eof?
32+
hash = parse_multi_line(pipe)
33+
packages << new(hash) if hash
34+
end
3235
end
3336

3437
packages
3538
end
3639

37-
self::REGEX = %r{^(\S+) +(\S+) +(\S+) (\S+) (\S*)$}
38-
self::FIELDS = [:desired, :error, :status, :name, :ensure]
40+
private
41+
42+
# Note: self:: is required here to keep these constants in the context of what will
43+
# eventually become this Puppet:Type::Package::ProviderDpkg class.
44+
self::DPKG_DESCRIPTION_DELIMITER = ':DESC:'
45+
self::DPKG_QUERY_FORMAT_STRING = %Q{'${Status} ${Package} ${Version} #{self::DPKG_DESCRIPTION_DELIMITER} ${Description}\\n#{self::DPKG_DESCRIPTION_DELIMITER}\\n'}
46+
self::FIELDS_REGEX = %r{^(\S+) +(\S+) +(\S+) (\S+) (\S*) #{self::DPKG_DESCRIPTION_DELIMITER} (.*)$}
47+
self::FIELDS= [:desired, :error, :status, :name, :ensure, :description]
48+
self::END_REGEX = %r{^#{self::DPKG_DESCRIPTION_DELIMITER}$}
49+
50+
# Handles parsing one package's worth of multi-line dpkg-query output. Will
51+
# emit warnings if it encounters an initial line that does not match
52+
# DPKG_QUERY_FORMAT_STRING. Swallows extra description lines silently.
53+
#
54+
# @param pipe [IO] the pipe yielded while processing dpkg output
55+
# @return [Hash,nil] parsed dpkg-query entry as a hash of FIELDS strings or
56+
# nil if we failed to parse
57+
# @api private
58+
def self.parse_multi_line(pipe)
59+
60+
line = pipe.gets
61+
unless hash = parse_line(line)
62+
Puppet.warning "Failed to match dpkg-query line #{line.inspect}"
63+
return nil
64+
end
65+
66+
consume_excess_description(pipe)
67+
68+
return hash
69+
end
3970

71+
# @param line [String] one line of dpkg-query output
72+
# @return [Hash,nil] a hash of FIELDS or nil if we failed to match
73+
# @api private
4074
def self.parse_line(line)
41-
if match = self::REGEX.match(line)
75+
hash = nil
76+
77+
if match = self::FIELDS_REGEX.match(line)
4278
hash = {}
4379

44-
self::FIELDS.zip(match.captures) { |field,value|
80+
self::FIELDS.zip(match.captures) do |field,value|
4581
hash[field] = value
46-
}
82+
end
4783

4884
hash[:provider] = self.name
4985

@@ -53,14 +89,33 @@ def self.parse_line(line)
5389
hash[:ensure] = :absent
5490
end
5591
hash[:ensure] = :held if hash[:desired] == 'hold'
56-
else
57-
Puppet.warning "Failed to match dpkg-query line #{line.inspect}"
58-
return nil
5992
end
6093

61-
hash
94+
return hash
6295
end
6396

97+
# Silently consumes the extra description lines from dpkg-query and brings
98+
# us to the next package entry start.
99+
#
100+
# @note dpkg-query Description field has a one line summary and a multi-line
101+
# description. dpkg-query binary:Summary is what we want to use but was
102+
# introduced in 2012 dpkg 1.16.2
103+
# (https://launchpad.net/debian/+source/dpkg/1.16.2) and is not not available
104+
# in older Debian versions. So we're placing a delimiter marker at the end
105+
# of the description so we can consume and ignore the multiline description
106+
# without issuing warnings
107+
#
108+
# @param pipe [IO] the pipe yielded while processing dpkg output
109+
# @return nil
110+
def self.consume_excess_description(pipe)
111+
until pipe.eof?
112+
break if self::END_REGEX.match(pipe.gets)
113+
end
114+
return nil
115+
end
116+
117+
public
118+
64119
def install
65120
unless file = @resource[:source]
66121
raise ArgumentError, "You cannot install dpkg packages without a source"
@@ -94,26 +149,24 @@ def latest
94149
end
95150

96151
def query
97-
packages = []
98-
99-
fields = [:desired, :error, :status, :name, :ensure]
100-
101-
hash = {}
152+
hash = nil
102153

103154
# list out our specific package
104155
begin
105-
output = dpkgquery(
156+
self.class.dpkgquery_piped(
106157
"-W",
107158
"--showformat",
108-
'${Status} ${Package} ${Version}\\n',
159+
self.class::DPKG_QUERY_FORMAT_STRING,
109160
@resource[:name]
110-
)
161+
) do |pipe|
162+
hash = self.class.parse_multi_line(pipe)
163+
end
111164
rescue Puppet::ExecutionFailure
112165
# dpkg-query exits 1 if the package is not found.
113166
return {:ensure => :purged, :status => 'missing', :name => @resource[:name], :error => 'ok'}
114167
end
115168

116-
hash = self.class.parse_line(output) || {:ensure => :absent, :status => 'missing', :name => @resource[:name], :error => 'ok'}
169+
hash ||= {:ensure => :absent, :status => 'missing', :name => @resource[:name], :error => 'ok'}
117170

118171
if hash[:error] != "ok"
119172
raise Puppet::Error.new(
@@ -148,4 +201,5 @@ def unhold
148201
execute([:dpkg, "--set-selections"], :failonfail => false, :combine => false, :stdinfile => tmpfile.path.to_s)
149202
end
150203
end
204+
151205
end

lib/puppet/provider/package/rpm.rb

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@
77

88
has_feature :versionable
99

10+
# Note: self:: is required here to keep these constants in the context of what will
11+
# eventually become this Puppet:Type::Package::ProviderRpm class.
12+
self::RPM_DESCRIPTION_DELIMITER = ':DESC:'
1013
# The query format by which we identify installed packages
11-
NEVRAFORMAT = "%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}"
12-
NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch]
14+
self::NEVRA_FORMAT = %Q{'%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} #{self::RPM_DESCRIPTION_DELIMITER} %{SUMMARY}\\n'}
15+
self::NEVRA_REGEX = %r{^(\S+) (\S+) (\S+) (\S+) (\S+) #{self::RPM_DESCRIPTION_DELIMITER} ?(.*)$}
16+
self::NEVRA_FIELDS = [:name, :epoch, :version, :release, :arch, :description]
1317

1418
commands :rpm => "rpm"
1519

@@ -44,11 +48,11 @@ def self.instances
4448

4549
# list out all of the packages
4650
begin
47-
execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf '#{NEVRAFORMAT}\n'") { |process|
51+
execpipe("#{command(:rpm)} -qa #{nosignature} #{nodigest} --qf #{self::NEVRA_FORMAT}") { |process|
4852
# now turn each returned line into a package object
4953
process.each_line { |line|
5054
hash = nevra_to_hash(line)
51-
packages << new(hash)
55+
packages << new(hash) unless hash.empty?
5256
}
5357
}
5458
rescue Puppet::ExecutionFailure
@@ -65,7 +69,7 @@ def query
6569
#NOTE: Prior to a fix for issue 1243, this method potentially returned a cached value
6670
#IF YOU CALL THIS METHOD, IT WILL CALL RPM
6771
#Use get(:property) to check if cached values are available
68-
cmd = ["-q", @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", "#{NEVRAFORMAT}\n"]
72+
cmd = ["-q", @resource[:name], "#{self.class.nosignature}", "#{self.class.nodigest}", "--qf", self.class::NEVRA_FORMAT]
6973

7074
begin
7175
output = rpm(*cmd)
@@ -74,7 +78,7 @@ def query
7478
end
7579

7680
# FIXME: We could actually be getting back multiple packages
77-
# for multilib
81+
# for multilib and this will only return the first such package
7882
@property_hash.update(self.class.nevra_to_hash(output))
7983

8084
@property_hash.dup
@@ -86,7 +90,7 @@ def latest
8690
@resource.fail "RPMs must specify a package source"
8791
end
8892

89-
cmd = [command(:rpm), "-q", "--qf", "#{NEVRAFORMAT}\n", "-p", "#{@resource[:source]}"]
93+
cmd = [command(:rpm), "-q", "--qf", self.class::NEVRA_FORMAT, "-p", source]
9094
h = self.class.nevra_to_hash(execfail(cmd, Puppet::Error))
9195
h[:ensure]
9296
end
@@ -135,12 +139,25 @@ def update
135139
self.install
136140
end
137141

142+
private
143+
144+
# @param line [String] one line of rpm package query information
145+
# @return [Hash] of NEVRA_FIELDS strings parsed from package info
146+
# if we failed to parse
147+
# @note warns if failed to match a line, and returns an empty Hash.
148+
# @api private
138149
def self.nevra_to_hash(line)
139-
line.chomp!
150+
line.strip!
140151
hash = {}
141-
NEVRA_FIELDS.zip(line.split) { |f, v| hash[f] = v }
142-
hash[:provider] = self.name
143-
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
144-
hash
152+
153+
if match = self::NEVRA_REGEX.match(line)
154+
self::NEVRA_FIELDS.zip(match.captures) { |f, v| hash[f] = v }
155+
hash[:provider] = self.name
156+
hash[:ensure] = "#{hash[:version]}-#{hash[:release]}"
157+
else
158+
Puppet.warning "Failed to match rpm line #{line}"
159+
end
160+
161+
return hash
145162
end
146163
end

spec/unit/provider/package/apt_spec.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88
@resource = stub 'resource', :[] => "asdf"
99
@provider = provider.new(@resource)
1010

11-
@fakeresult = "install ok installed asdf 1.0\n"
11+
@fakeresult = <<-EOF
12+
install ok installed asdf 1.0 "asdf summary
13+
asdf multiline description
14+
with multiple lines
15+
EOF
1216
end
1317

1418
it "should be versionable" do

spec/unit/provider/package/aptitude_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
it { should be_versionable }
1111

1212
context "when retrieving ensure" do
13-
{ :absent => "deinstall ok config-files faff 1.2.3-1\n",
14-
"1.2.3-1" => "install ok installed faff 1.2.3-1\n",
13+
{ :absent => "deinstall ok config-files faff 1.2.3-1 :DESC: faff summary\n:DESC:\n",
14+
"1.2.3-1" => "install ok installed faff 1.2.3-1 :DESC: faff summary\n:DESC:\n",
1515
}.each do |expect, output|
1616
it "should detect #{expect} packages" do
17-
pkg.provider.expects(:dpkgquery).
18-
with('-W', '--showformat', '${Status} ${Package} ${Version}\n', 'faff').
19-
returns(output)
17+
Puppet::Util::Execution.expects(:execpipe).
18+
with([pkg.provider.command(:dpkgquery), '-W', '--showformat', "'${Status} ${Package} ${Version} :DESC: ${Description}\\n:DESC:\\n'", 'faff']).
19+
yields(StringIO.new(output))
2020

2121
pkg.property(:ensure).retrieve.should == expect
2222
end

spec/unit/provider/package/aptrpm_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
def rpm
2020
pkg.provider.expects(:rpm).
2121
with('-q', 'faff', '--nosignature', '--nodigest', '--qf',
22-
"%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH}\n")
22+
"'%{NAME} %|EPOCH?{%{EPOCH}}:{0}| %{VERSION} %{RELEASE} %{ARCH} :DESC: %{SUMMARY}\\n'")
2323
end
2424

2525
it "should report absent packages" do
@@ -28,7 +28,7 @@ def rpm
2828
end
2929

3030
it "should report present packages correctly" do
31-
rpm.returns("faff-1.2.3-1 0 1.2.3-1 5 i686\n")
31+
rpm.returns("faff-1.2.3-1 0 1.2.3-1 5 i686 :DESC: faff desc\n")
3232
pkg.property(:ensure).retrieve.should == "1.2.3-1-5"
3333
end
3434
end

0 commit comments

Comments
 (0)