Codebase list facter / e4eb583
(#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
4 changed file(s) with 284 addition(s) and 13 deletion(s). Raw diff Collapse all Expand all
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
1010 #
1111 # Caveats:
1212 # 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
2516 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
0 #!usr/bin/env rspec
0 #! /usr/bin/env ruby
11
22 require 'spec_helper'
33