(#17890) Refactor the facter solaris zone facts
Without this patch the solaris zones fact executes `/usr/sbin/zoneadm
list -cp` on all systems. This behavior is not confined to SunOS
systems. This is a problem because this creates noise for the end user.
The root cause of the problem is that the block passed to the Resolution
instance executes before the confinement statement takes effect. In the
scope of this block is a call to Resolution.exec('/usr/sbin/zoneadm list
-cp').
This patch addresses the problem by moving the behavior for defining the
set of dynamic facts into the Facter::Util::SolarisZones.add_facts class
method. This method is called only when running on the SunOS kernel.
The add_facts method has the behavior of instantiating an instance of
SolarisZones to model the output of the zoneadm command. We model this
output because the system command is extremely expensive from a
performance and efficiency point of view. All of the dynamic facts that
are defined as a result of parsing the zoneadm output retain a reference
to this single model instance. In the situation where one, many, or all
of the fact values are flushed then the model instance is also flushed.
The first fact to resolve again will refresh the model and all
subsequent resolutions will re-use the data contained in the model.
It is not uncommon to have 10 or more zones defined on a system and
there are 7 dynamic facts generated for each zone. Without this shared
model instance it is unclear how to avoid executing 10*7 system calls
when all of the dynamic cache values are flushed.
Jeff McCune
11 years ago
0 | require 'facter/util/resolution' | |
1 | ||
2 | module Facter | |
3 | module Util | |
4 | ## | |
5 | # Provide a set of utility methods to interact with Solaris zones. This class | |
6 | # is expected to be instantiated once per set of resolutions in order to | |
7 | # cache the output of the zoneadm command, which can be quite expensive. | |
8 | # | |
9 | # @api private | |
10 | class SolarisZones | |
11 | attr_reader :zone_hash | |
12 | attr_reader :zoneadm_cmd | |
13 | attr_reader :zoneadm_output | |
14 | attr_reader :zoneadm_keys | |
15 | ||
16 | ## | |
17 | # add_facts defines all of the facts for solaris zones, for example `zones`, | |
18 | # `zone_global_id`, `zone_global_status`, etc... This method defines the | |
19 | # static fact named `zones`. The value of this fact is the numver of zones | |
20 | # reported by the zoneadm system command. The `zones` fact also defines | |
21 | # all of the dynamic facts describing the following seven attribute values | |
22 | # for each zone. | |
23 | # | |
24 | # Zones may be added to the system while Facter is loaded. In order to | |
25 | # define new dynamic facts that reflect this new information, the `virtual` | |
26 | # will define new facts as a side effect of refreshing it's own value. | |
27 | # | |
28 | # @api private | |
29 | def self.add_facts | |
30 | model = new | |
31 | model.refresh | |
32 | model.add_dynamic_facts | |
33 | Facter.add("zones") do | |
34 | setcode do | |
35 | model.refresh if model.flushed? | |
36 | model.add_dynamic_facts | |
37 | model.count | |
38 | end | |
39 | on_flush do | |
40 | model.flush! | |
41 | end | |
42 | end | |
43 | end | |
44 | ||
45 | ## | |
46 | # @param [Hash] opts the options to create the instance with | |
47 | # @option opts [String] :zoneadm_cmd ('/usr/sbin/zoneadm list -cp') the | |
48 | # system command to inspect zones | |
49 | # @option opts [String] :zoneadm_output (nil) the cached output of the | |
50 | # zoneadm_cmd | |
51 | def initialize(opts = {}) | |
52 | @zoneadm_keys = [:id, :name, :status, :path, :uuid, :brand, :iptype] | |
53 | @zoneadm_cmd = opts[:zoneadm_cmd] || '/usr/sbin/zoneadm list -cp' | |
54 | if opts[:zoneadm_output] | |
55 | @zoneadm_output = opts[:zoneadm_output] | |
56 | end | |
57 | end | |
58 | ||
59 | ## | |
60 | # add_dynamic_facts defines all of the dynamic facts derived from parsing | |
61 | # the output of the zoneadm command. The zone facts are dynamic, so this | |
62 | # method has the behavior of figuring out what dynamic zone facts need to | |
63 | # be defined and how they should be resolved. | |
64 | # | |
65 | # @param model [SolarisZones] the model used to store data from the system | |
66 | # | |
67 | # @api private | |
68 | def add_dynamic_facts | |
69 | model = self | |
70 | zone_hash.each_pair do |zone, attr_hsh| | |
71 | attr_hsh.keys.each do |attr| | |
72 | Facter.add("zone_#{zone}_#{attr}") do | |
73 | setcode do | |
74 | model.refresh if model.flushed? | |
75 | # Don't resolve if the zone has since been deleted | |
76 | if zone_hsh = model.zone_hash[zone] | |
77 | zone_hsh[attr] # the value | |
78 | end | |
79 | end | |
80 | on_flush do | |
81 | model.flush! | |
82 | end | |
83 | end | |
84 | end | |
85 | end | |
86 | end | |
87 | ||
88 | ## | |
89 | # refresh executes the zoneadm_cmd and stores the output data. | |
90 | # | |
91 | # @api private | |
92 | # | |
93 | # @return [Hash] the parsed output of the zoneadm command | |
94 | def refresh | |
95 | @zoneadm_output = Facter::Util::Resolution.exec(zoneadm_cmd) | |
96 | parse! | |
97 | end | |
98 | ||
99 | ## | |
100 | # parse! parses the string stored in {@zoneadm_output} and stores the | |
101 | # resulting Hash data structure in {@zone_hash} | |
102 | # | |
103 | # @api private | |
104 | def parse! | |
105 | rows = @zoneadm_output.split("\n").collect { |line| line.split(':') } | |
106 | ||
107 | @zone_hash = rows.inject({}) do |memo, fields| | |
108 | zone = fields[1].intern | |
109 | # Transform the row into a hash with keys named by the column names | |
110 | memo[zone] = Hash[*@zoneadm_keys.zip(fields).flatten] | |
111 | memo | |
112 | end | |
113 | end | |
114 | private :parse! | |
115 | ||
116 | ## | |
117 | # count returns the number of running zones, including the global zone. | |
118 | # This method is intended to be used from the setcode block of the `zones` | |
119 | # fact. | |
120 | # | |
121 | # @api private | |
122 | # | |
123 | # @return [Fixnum, nil] the number of running zones or nil if the number | |
124 | # could not be determined. | |
125 | def count | |
126 | if @zone_hash | |
127 | @zone_hash.size | |
128 | end | |
129 | end | |
130 | ||
131 | ## | |
132 | # flush! purges the saved data from the zoneadm_cmd output | |
133 | # | |
134 | # @api private | |
135 | def flush! | |
136 | @zoneadm_output = nil | |
137 | @zone_hash = nil | |
138 | end | |
139 | ||
140 | ## | |
141 | # flushed? returns true if the instance has no parsed data accessible via | |
142 | # the {zone_hash} method. | |
143 | # | |
144 | # @api private | |
145 | # | |
146 | # @return [Boolean] true if there is no parsed data, false otherwise | |
147 | def flushed? | |
148 | !@zone_hash | |
149 | end | |
150 | end | |
151 | end | |
152 | end |
10 | 10 | # |
11 | 11 | # Caveats: |
12 | 12 | # We dont support below s10 where zones are not available. |
13 | ||
14 | Facter.add("zones") do | |
15 | confine :kernel => :sunos | |
16 | fmt = [:id, :name, :status, :path, :uuid, :brand, :iptype] | |
17 | l = Facter::Util::Resolution.exec('/usr/sbin/zoneadm list -cp').split("\n").collect{|l|l.split(':')}.each do |val| | |
18 | fmt.each_index do |i| | |
19 | Facter.add "zone_%s_%s" % [val[1], fmt[i]] do | |
20 | setcode { val[i] } | |
21 | end | |
22 | end | |
23 | end | |
24 | setcode { l.length } | |
13 | require 'facter/util/solaris_zones' | |
14 | if Facter.value(:kernel) == 'SunOS' | |
15 | Facter::Util::SolarisZones.add_facts | |
25 | 16 | end |
0 | require 'spec_helper' | |
1 | require 'facter/util/solaris_zones' | |
2 | ||
3 | describe Facter::Util::SolarisZones do | |
4 | let :zone_list do | |
5 | zone_list = <<-EOF | |
6 | 0:global:running:/::native:shared | |
7 | -:local:configured:/::native:shared | |
8 | -:zoneA:stopped:/::native:shared | |
9 | EOF | |
10 | end | |
11 | ||
12 | let :zone_list2 do | |
13 | zone_list = <<-EOF | |
14 | 0:global:running:/::native:shared | |
15 | -:local:configured:/::native:shared | |
16 | -:zoneB:stopped:/::native:shared | |
17 | -:zoneC:stopped:/::native:shared | |
18 | EOF | |
19 | end | |
20 | ||
21 | subject do | |
22 | described_class.new(:zoneadm_output => zone_list) | |
23 | end | |
24 | ||
25 | describe '.add_facts' do | |
26 | before :each do | |
27 | zones = described_class.new(:zoneadm_output => zone_list) | |
28 | zones.send(:parse!) | |
29 | zones.stubs(:refresh) | |
30 | described_class.stubs(:new).returns(zones) | |
31 | end | |
32 | ||
33 | it 'defines the zones fact' do | |
34 | described_class.add_facts | |
35 | Facter.fact(:zones).value.should == 3 | |
36 | end | |
37 | ||
38 | it 'defines a fact for each attribute of a zone' do | |
39 | described_class.add_facts | |
40 | [:id, :name, :status, :path, :uuid, :brand, :iptype].each do |attr| | |
41 | Facter.fact("zone_local_#{attr}".intern). | |
42 | should be_a_kind_of Facter::Util::Fact | |
43 | end | |
44 | end | |
45 | end | |
46 | ||
47 | describe '#refresh' do | |
48 | it 'executes the zoneadm_cmd' do | |
49 | Facter::Util::Resolution.expects(:exec).with(subject.zoneadm_cmd).returns(zone_list) | |
50 | subject.refresh | |
51 | end | |
52 | end | |
53 | ||
54 | describe 'multiple facts sharing a single model' do | |
55 | context 'when zones is resolved for the first time' do | |
56 | it 'counts the number of zones' do | |
57 | given_initial_zone_facts | |
58 | Facter.fact(:zones).value.should == 3 | |
59 | end | |
60 | it 'defines facts for zoneA' do | |
61 | given_initial_zone_facts | |
62 | Facter.fact(:zone_zoneA_id).value.should == '-' | |
63 | end | |
64 | it 'does not define facts for zoneB' do | |
65 | given_initial_zone_facts | |
66 | Facter.fact(:zone_zoneB_id).should be_nil | |
67 | end | |
68 | it 'uses a single read of the system information for all of the dynamically generated zone facts' do | |
69 | given_initial_zone_facts # <= single read happens here | |
70 | ||
71 | Facter::Util::Resolution.expects(:exec).never | |
72 | Facter.fact(:zone_zoneA_id).value | |
73 | Facter.fact(:zone_local_id).value | |
74 | end | |
75 | end | |
76 | context 'when all facts have been flushed after zones was resolved once' do | |
77 | it 'updates the number of zones' do | |
78 | given_initial_zone_facts | |
79 | when_facts_have_been_resolved_then_flushed | |
80 | ||
81 | Facter.fact(:zones).value.should == 4 | |
82 | end | |
83 | it 'stops resolving a value for a zone that no longer exists' do | |
84 | given_initial_zone_facts | |
85 | when_facts_have_been_resolved_then_flushed | |
86 | ||
87 | Facter.fact(:zone_zoneA_id).value.should be_nil | |
88 | Facter.fact(:zone_zoneA_status).value.should be_nil | |
89 | Facter.fact(:zone_zoneA_path).value.should be_nil | |
90 | end | |
91 | it 'defines facts for new zones' do | |
92 | given_initial_zone_facts | |
93 | when_facts_have_been_resolved_then_flushed | |
94 | ||
95 | Facter.fact(:zone_zoneB_id).should be_nil | |
96 | Facter.fact(:zones).value | |
97 | Facter.fact(:zone_zoneB_id).value.should be_a_kind_of String | |
98 | end | |
99 | it 'uses a single read of the system information for all of the dynamically generated zone facts' do | |
100 | given_initial_zone_facts | |
101 | when_facts_have_been_resolved_then_flushed | |
102 | ||
103 | Facter::Util::Resolution.expects(:exec).once.returns(zone_list2) | |
104 | Facter.fact(:zones).value | |
105 | Facter.fact(:zone_zoneA_id).value | |
106 | Facter.fact(:zone_local_id).value | |
107 | end | |
108 | ||
109 | end | |
110 | end | |
111 | ||
112 | def given_initial_zone_facts | |
113 | Facter::Util::Resolution.stubs(:exec). | |
114 | with(subject.zoneadm_cmd). | |
115 | returns(zone_list) | |
116 | described_class.add_facts | |
117 | end | |
118 | ||
119 | def when_facts_have_been_resolved_then_flushed | |
120 | Facter.fact(:zones).value | |
121 | Facter.fact(:zone_zoneA_id).value | |
122 | Facter.fact(:zone_local_id).value | |
123 | Facter::Util::Resolution.stubs(:exec).returns(zone_list2) | |
124 | Facter.flush | |
125 | end | |
126 | end |