chef-expander 0.10.0.beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/LICENSE +201 -0
- data/README.rdoc +20 -0
- data/bin/chef-expander +30 -0
- data/bin/chef-expander-cluster +29 -0
- data/bin/chef-expanderctl +30 -0
- data/conf/chef-expander.rb.example +9 -0
- data/lib/chef/expander/cluster_supervisor.rb +127 -0
- data/lib/chef/expander/configuration.rb +307 -0
- data/lib/chef/expander/control.rb +206 -0
- data/lib/chef/expander/daemonizable.rb +150 -0
- data/lib/chef/expander/flattener.rb +79 -0
- data/lib/chef/expander/loggable.rb +40 -0
- data/lib/chef/expander/logger.rb +135 -0
- data/lib/chef/expander/node.rb +177 -0
- data/lib/chef/expander/solrizer.rb +275 -0
- data/lib/chef/expander/version.rb +37 -0
- data/lib/chef/expander/vnode.rb +106 -0
- data/lib/chef/expander/vnode_supervisor.rb +265 -0
- data/lib/chef/expander/vnode_table.rb +83 -0
- data/lib/chef/expander.rb +36 -0
- data/scripts/check_queue_size +93 -0
- data/scripts/check_queue_size_munin +51 -0
- data/scripts/make_solr_xml +58 -0
- data/scripts/traffic-creator +97 -0
- metadata +202 -0
@@ -0,0 +1,307 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
3
|
+
# Author:: Seth Falcon (<seth@opscode.com>)
|
4
|
+
# Author:: Chris Walters (<cw@opscode.com>)
|
5
|
+
# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
|
6
|
+
# License:: Apache License, Version 2.0
|
7
|
+
#
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
17
|
+
# See the License for the specific language governing permissions and
|
18
|
+
# limitations under the License.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'pp'
|
22
|
+
require 'optparse'
|
23
|
+
require 'singleton'
|
24
|
+
|
25
|
+
require 'chef/expander/flattener'
|
26
|
+
require 'chef/expander/loggable'
|
27
|
+
require 'chef/expander/version'
|
28
|
+
|
29
|
+
module Chef
|
30
|
+
module Expander
|
31
|
+
|
32
|
+
def self.config
|
33
|
+
@config ||= Configuration::Base.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.init_config(argv)
|
37
|
+
config.apply_defaults
|
38
|
+
remaining_opts_after_parse = Configuration::CLI.parse_options(argv)
|
39
|
+
# Need to be able to override the default config file location on the command line
|
40
|
+
config_file_to_use = Configuration::CLI.config.config_file || config.config_file
|
41
|
+
config.merge_config(Configuration::Base.from_chef_compat_config(config_file_to_use))
|
42
|
+
# But for all other config options, the CLI config should win over config file
|
43
|
+
config.merge_config(Configuration::CLI.config)
|
44
|
+
remaining_opts_after_parse
|
45
|
+
end
|
46
|
+
|
47
|
+
class ChefCompatibleConfig
|
48
|
+
|
49
|
+
attr_reader :config_hash
|
50
|
+
|
51
|
+
def initialize
|
52
|
+
@config_hash = {}
|
53
|
+
end
|
54
|
+
|
55
|
+
def load(file)
|
56
|
+
file = File.expand_path(file)
|
57
|
+
instance_eval(IO.read(file), file, 1) if File.readable?(file)
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing(method_name, *args, &block)
|
61
|
+
if args.size == 1
|
62
|
+
@config_hash[method_name] = args.first
|
63
|
+
elsif args.empty?
|
64
|
+
@config_hash[method_name] or super
|
65
|
+
else
|
66
|
+
super
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
module Configuration
|
73
|
+
|
74
|
+
class InvalidConfiguration < StandardError
|
75
|
+
end
|
76
|
+
|
77
|
+
class Base
|
78
|
+
|
79
|
+
DEFAULT_PIDFILE = Object.new
|
80
|
+
|
81
|
+
include Loggable
|
82
|
+
|
83
|
+
def self.from_chef_compat_config(file)
|
84
|
+
config = ChefCompatibleConfig.new
|
85
|
+
config.load(file)
|
86
|
+
from_hash(config.config_hash)
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.from_hash(config_hash)
|
90
|
+
config = new
|
91
|
+
config_hash.each do |setting, value|
|
92
|
+
setter = "#{setting}=".to_sym
|
93
|
+
if config.respond_to?(setter)
|
94
|
+
config.send(setter, value)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
config
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.configurables
|
101
|
+
@configurables ||= []
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.validations
|
105
|
+
@validations ||= []
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.defaults
|
109
|
+
@defaults ||= {}
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.configurable(setting, default=nil, &validation)
|
113
|
+
attr_accessor(setting)
|
114
|
+
configurables << setting
|
115
|
+
defaults[setting] = default
|
116
|
+
validations << validation if block_given?
|
117
|
+
|
118
|
+
setting
|
119
|
+
end
|
120
|
+
|
121
|
+
configurable :config_file, "/etc/chef/solr.rb" do
|
122
|
+
unless (config_file && File.exist?(config_file) && File.readable?(config_file))
|
123
|
+
log.warn {"* " * 40}
|
124
|
+
log.warn {"Config file #{config_file} does not exist or cannot be read by user (#{Process.euid})"}
|
125
|
+
log.warn {"Default configuration settings will be used"}
|
126
|
+
log.warn {"* " * 40}
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
configurable :index do
|
131
|
+
invalid("You must specify this node's position in the ring as an integer") unless index.kind_of?(Integer)
|
132
|
+
invalid("The index cannot be larger than the cluster size (node-count)") unless (index <= node_count.to_i)
|
133
|
+
end
|
134
|
+
|
135
|
+
configurable :node_count do
|
136
|
+
invalid("You must specify the cluster size as an integer") unless node_count.kind_of?(Integer)
|
137
|
+
invalid("The cluster size (node-count) cannot be smaller than the index") unless node_count >= index.to_i
|
138
|
+
end
|
139
|
+
|
140
|
+
configurable :ps_tag, ""
|
141
|
+
|
142
|
+
configurable :solr_url, "http://localhost:8983"
|
143
|
+
|
144
|
+
configurable :amqp_host, '0.0.0.0'
|
145
|
+
|
146
|
+
configurable :amqp_port, 5672
|
147
|
+
|
148
|
+
configurable :amqp_user, 'chef'
|
149
|
+
|
150
|
+
configurable :amqp_pass, 'testing'
|
151
|
+
|
152
|
+
configurable :amqp_vhost, '/chef'
|
153
|
+
|
154
|
+
configurable :user, nil
|
155
|
+
|
156
|
+
configurable :group, nil
|
157
|
+
|
158
|
+
configurable :daemonize, false
|
159
|
+
|
160
|
+
alias :daemonize? :daemonize
|
161
|
+
|
162
|
+
configurable :pidfile, DEFAULT_PIDFILE
|
163
|
+
|
164
|
+
def pidfile
|
165
|
+
if @pidfile.equal?(DEFAULT_PIDFILE)
|
166
|
+
Process.euid == 0 ? '/var/run/chef-expander.pid' : '/tmp/chef-expander.pid'
|
167
|
+
else
|
168
|
+
@pidfile
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
configurable :log_level, :info
|
173
|
+
|
174
|
+
# override the setter for log_level to also actually set the level
|
175
|
+
def log_level=(level)
|
176
|
+
if level #don't accept nil for an answer
|
177
|
+
level = level.to_sym
|
178
|
+
Loggable::LOGGER.level = level
|
179
|
+
@log_level = log_level
|
180
|
+
end
|
181
|
+
level
|
182
|
+
end
|
183
|
+
|
184
|
+
configurable :log_location, STDOUT
|
185
|
+
|
186
|
+
# override the setter for log_location to re-init the logger
|
187
|
+
def log_location=(location)
|
188
|
+
Loggable::LOGGER.init(location) unless location.nil?
|
189
|
+
end
|
190
|
+
|
191
|
+
def initialize
|
192
|
+
reset!
|
193
|
+
end
|
194
|
+
|
195
|
+
def reset!(stdout=nil)
|
196
|
+
self.class.configurables.each do |setting|
|
197
|
+
send("#{setting}=".to_sym, nil)
|
198
|
+
end
|
199
|
+
@stdout = stdout || STDOUT
|
200
|
+
end
|
201
|
+
|
202
|
+
def apply_defaults
|
203
|
+
self.class.defaults.each do |setting, value|
|
204
|
+
self.send("#{setting}=".to_sym, value)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
def merge_config(other)
|
209
|
+
self.class.configurables.each do |setting|
|
210
|
+
value = other.send(setting)
|
211
|
+
self.send("#{setting}=".to_sym, value) if value
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def fail_if_invalid
|
216
|
+
validate!
|
217
|
+
rescue InvalidConfiguration => e
|
218
|
+
@stdout.puts("Invalid configuration: #{e.message}")
|
219
|
+
exit(1)
|
220
|
+
end
|
221
|
+
|
222
|
+
def invalid(message)
|
223
|
+
raise InvalidConfiguration, message
|
224
|
+
end
|
225
|
+
|
226
|
+
def validate!
|
227
|
+
self.class.validations.each do |validation_proc|
|
228
|
+
instance_eval(&validation_proc)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def vnode_numbers
|
233
|
+
vnodes_per_node = VNODES / node_count
|
234
|
+
lower_bound = (index - 1) * vnodes_per_node
|
235
|
+
upper_bound = lower_bound + vnodes_per_node
|
236
|
+
upper_bound += VNODES % vnodes_per_node if index == node_count
|
237
|
+
(lower_bound...upper_bound).to_a
|
238
|
+
end
|
239
|
+
|
240
|
+
def amqp_config
|
241
|
+
{:host => amqp_host, :port => amqp_port, :user => amqp_user, :pass => amqp_pass, :vhost => amqp_vhost}
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
module CLI
|
247
|
+
@config = Configuration::Base.new
|
248
|
+
|
249
|
+
@option_parser = OptionParser.new do |o|
|
250
|
+
o.banner = "Usage: chef-expander [options]"
|
251
|
+
|
252
|
+
o.on('-c', '--config CONFIG_FILE', 'a configuration file to use') do |conf|
|
253
|
+
@config.config_file = File.expand_path(conf)
|
254
|
+
end
|
255
|
+
|
256
|
+
o.on('-i', '--index INDEX', 'the slot this node will occupy in the ring') do |i|
|
257
|
+
@config.index = i.to_i
|
258
|
+
end
|
259
|
+
|
260
|
+
o.on('-n', '--node-count NUMBER', 'the number of nodes in the ring') do |n|
|
261
|
+
@config.node_count = n.to_i
|
262
|
+
end
|
263
|
+
|
264
|
+
o.on('-l', '--log-level LOG_LEVEL', 'set the log level') do |l|
|
265
|
+
@config.log_level = l
|
266
|
+
end
|
267
|
+
|
268
|
+
o.on('-L', '--logfile LOG_LOCATION', 'Logfile to use') do |l|
|
269
|
+
@config.log_location = l
|
270
|
+
end
|
271
|
+
|
272
|
+
o.on('-d', '--daemonize', 'fork into the background') do
|
273
|
+
@config.daemonize = true
|
274
|
+
end
|
275
|
+
|
276
|
+
o.on('-P', '--pid PIDFILE') do |p|
|
277
|
+
@config.pidfile = p
|
278
|
+
end
|
279
|
+
|
280
|
+
o.on_tail('-h', '--help', 'show this message') do
|
281
|
+
puts "chef-expander #{Expander.version}"
|
282
|
+
puts ''
|
283
|
+
puts o
|
284
|
+
exit 1
|
285
|
+
end
|
286
|
+
|
287
|
+
o.on_tail('-v', '--version', 'show the version and exit') do
|
288
|
+
puts "chef-expander #{Expander.version}"
|
289
|
+
exit 0
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.parse_options(argv)
|
295
|
+
@option_parser.parse!(argv.dup)
|
296
|
+
end
|
297
|
+
|
298
|
+
def self.config
|
299
|
+
@config
|
300
|
+
end
|
301
|
+
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
3
|
+
# Author:: Seth Falcon (<seth@opscode.com>)
|
4
|
+
# Author:: Chris Walters (<cw@opscode.com>)
|
5
|
+
# Copyright:: Copyright (c) 2010-2011 Opscode, Inc.
|
6
|
+
# License:: Apache License, Version 2.0
|
7
|
+
#
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
9
|
+
# you may not use this file except in compliance with the License.
|
10
|
+
# You may obtain a copy of the License at
|
11
|
+
#
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
13
|
+
#
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
17
|
+
# See the License for the specific language governing permissions and
|
18
|
+
# limitations under the License.
|
19
|
+
#
|
20
|
+
|
21
|
+
require 'bunny'
|
22
|
+
require 'yajl'
|
23
|
+
require 'eventmachine'
|
24
|
+
require 'amqp'
|
25
|
+
require 'mq'
|
26
|
+
require 'highline'
|
27
|
+
|
28
|
+
require 'chef/expander/node'
|
29
|
+
require 'chef/expander/configuration'
|
30
|
+
|
31
|
+
require 'pp'
|
32
|
+
|
33
|
+
module Chef
|
34
|
+
module Expander
|
35
|
+
class Control
|
36
|
+
|
37
|
+
def self.run(argv)
|
38
|
+
remaining_args_after_opts = Expander.init_config(ARGV)
|
39
|
+
new(remaining_args_after_opts).run
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.desc(description)
|
43
|
+
@desc = description
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.option(*args)
|
47
|
+
#TODO
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.arg(*args)
|
51
|
+
#TODO
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.descriptions
|
55
|
+
@descriptions ||= []
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.method_added(method_name)
|
59
|
+
if @desc
|
60
|
+
descriptions << [method_name, method_name.to_s.gsub('_', '-'), @desc]
|
61
|
+
@desc = nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#--
|
66
|
+
# TODO: this is confusing and unneeded. Just whitelist the methods
|
67
|
+
# that map to commands and use +send+
|
68
|
+
def self.compile
|
69
|
+
run_method = "def run; case @argv.first;"
|
70
|
+
descriptions.each do |method_name, command_name, desc|
|
71
|
+
run_method << "when '#{command_name}';#{method_name};"
|
72
|
+
end
|
73
|
+
run_method << "else; help; end; end;"
|
74
|
+
class_eval(run_method, __FILE__, __LINE__)
|
75
|
+
end
|
76
|
+
|
77
|
+
def initialize(argv)
|
78
|
+
@argv = argv.dup
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Show this message"
|
82
|
+
def help
|
83
|
+
puts "Chef Expander #{Expander.version}"
|
84
|
+
puts "Usage: chef-expanderctl COMMAND"
|
85
|
+
puts
|
86
|
+
puts "Commands:"
|
87
|
+
self.class.descriptions.each do |method_name, command_name, desc|
|
88
|
+
puts " #{command_name}".ljust(15) + desc
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
desc "display the aggregate queue backlog"
|
93
|
+
def queue_depth
|
94
|
+
h = HighLine.new
|
95
|
+
message_counts = []
|
96
|
+
|
97
|
+
amqp_client = Bunny.new(Expander.config.amqp_config)
|
98
|
+
amqp_client.start
|
99
|
+
|
100
|
+
0.upto(VNODES - 1) do |vnode|
|
101
|
+
q = amqp_client.queue("vnode-#{vnode}", :durable => true)
|
102
|
+
message_counts << q.status[:message_count]
|
103
|
+
end
|
104
|
+
total_messages = message_counts.inject(0) { |sum, count| sum + count }
|
105
|
+
max = message_counts.max
|
106
|
+
min = message_counts.min
|
107
|
+
|
108
|
+
avg = total_messages.to_f / message_counts.size.to_f
|
109
|
+
|
110
|
+
puts " total messages: #{total_messages}"
|
111
|
+
puts " average queue depth: #{avg}"
|
112
|
+
puts " max queue depth: #{max}"
|
113
|
+
puts " min queue depth: #{min}"
|
114
|
+
ensure
|
115
|
+
amqp_client.stop if defined?(amqp_client) && amqp_client
|
116
|
+
end
|
117
|
+
|
118
|
+
desc "show the backlog and consumer count for each vnode queue"
|
119
|
+
def queue_status
|
120
|
+
h = HighLine.new
|
121
|
+
queue_status = [h.color("VNode", :bold), h.color("Messages", :bold), h.color("Consumers", :bold)]
|
122
|
+
|
123
|
+
total_messages = 0
|
124
|
+
|
125
|
+
amqp_client = Bunny.new(Expander.config.amqp_config)
|
126
|
+
amqp_client.start
|
127
|
+
|
128
|
+
0.upto(VNODES - 1) do |vnode|
|
129
|
+
q = amqp_client.queue("vnode-#{vnode}", :durable => true)
|
130
|
+
status = q.status
|
131
|
+
# returns {:message_count => method.message_count, :consumer_count => method.consumer_count}
|
132
|
+
queue_status << vnode.to_s << status[:message_count].to_s << status[:consumer_count].to_s
|
133
|
+
total_messages += status[:message_count]
|
134
|
+
end
|
135
|
+
puts " total messages: #{total_messages}"
|
136
|
+
puts
|
137
|
+
puts h.list(queue_status, :columns_across, 3)
|
138
|
+
ensure
|
139
|
+
amqp_client.stop if defined?(amqp_client) && amqp_client
|
140
|
+
end
|
141
|
+
|
142
|
+
desc "show the status of the nodes in the cluster"
|
143
|
+
def node_status
|
144
|
+
status_mutex = Mutex.new
|
145
|
+
h = ::HighLine.new
|
146
|
+
node_status = [h.color("Host", :bold), h.color("PID", :bold), h.color("GUID", :bold), h.color("Vnodes", :bold)]
|
147
|
+
|
148
|
+
print("Collecting status info from the cluster...")
|
149
|
+
|
150
|
+
AMQP.start(Expander.config.amqp_config) do
|
151
|
+
node = Expander::Node.local_node
|
152
|
+
node.exclusive_control_queue.subscribe do |header, message|
|
153
|
+
status = Yajl::Parser.parse(message)
|
154
|
+
status_mutex.synchronize do
|
155
|
+
node_status << status["hostname_f"]
|
156
|
+
node_status << status["pid"].to_s
|
157
|
+
node_status << status["guid"]
|
158
|
+
# BIG ASSUMPTION HERE that nodes only have contiguous vnode ranges
|
159
|
+
# will not be true once vnode recovery is implemented
|
160
|
+
node_status << "#{status["vnodes"].min}-#{status["vnodes"].max}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
node.broadcast_message(Yajl::Encoder.encode(:action => :status, :rsvp => node.exclusive_control_queue_name))
|
164
|
+
EM.add_timer(2) { AMQP.stop;EM.stop }
|
165
|
+
end
|
166
|
+
|
167
|
+
puts "done"
|
168
|
+
puts
|
169
|
+
puts h.list(node_status, :columns_across, 4)
|
170
|
+
puts
|
171
|
+
end
|
172
|
+
|
173
|
+
desc "sets the log level of all nodes in the cluster"
|
174
|
+
def log_level
|
175
|
+
@argv.shift
|
176
|
+
level = @argv.first
|
177
|
+
acceptable_levels = %w{debug info warn error fatal}
|
178
|
+
unless acceptable_levels.include?(level)
|
179
|
+
puts "Log level must be one of #{acceptable_levels.join(', ')}"
|
180
|
+
exit 1
|
181
|
+
end
|
182
|
+
|
183
|
+
h = HighLine.new
|
184
|
+
response_mutex = Mutex.new
|
185
|
+
|
186
|
+
responses = [h.color("Host", :bold), h.color("PID", :bold), h.color("GUID", :bold), h.color("Log Level", :bold)]
|
187
|
+
AMQP.start(Expander.config.amqp_config) do
|
188
|
+
node = Expander::Node.local_node
|
189
|
+
node.exclusive_control_queue.subscribe do |header, message|
|
190
|
+
reply = Yajl::Parser.parse(message)
|
191
|
+
n = reply['node']
|
192
|
+
response_mutex.synchronize do
|
193
|
+
responses << n["hostname_f"] << n["pid"].to_s << n["guid"] << reply["level"]
|
194
|
+
end
|
195
|
+
end
|
196
|
+
node.broadcast_message(Yajl::Encoder.encode({:action => :set_log_level, :level => level, :rsvp => node.exclusive_control_queue_name}))
|
197
|
+
EM.add_timer(2) { AMQP.stop; EM.stop }
|
198
|
+
end
|
199
|
+
puts h.list(responses, :columns_across, 4)
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
compile
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Daniel DeLeo (<dan@opscode.com>)
|
3
|
+
# Copyright:: Copyright (c) 2011 Opscode, Inc.
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'etc'
|
20
|
+
require 'chef/expander/loggable'
|
21
|
+
|
22
|
+
module Chef
|
23
|
+
module Expander
|
24
|
+
|
25
|
+
class AlreadyRunning < RuntimeError
|
26
|
+
end
|
27
|
+
|
28
|
+
class NoSuchUser < ArgumentError
|
29
|
+
end
|
30
|
+
|
31
|
+
class NoSuchGroup < ArgumentError
|
32
|
+
end
|
33
|
+
|
34
|
+
module Daemonizable
|
35
|
+
include Loggable
|
36
|
+
|
37
|
+
# Daemonizes the process if configured to do so, and ensures that only one
|
38
|
+
# copy of the process is running with a given config by obtaining an
|
39
|
+
# exclusive lock on the pidfile. Also sets process user and group if so
|
40
|
+
# configured.
|
41
|
+
# ===Raises
|
42
|
+
# * AlreadyRunning::: when another process has the exclusive lock on the pidfile
|
43
|
+
# * NoSuchUser::: when a user is configured that doesn't exist
|
44
|
+
# * NoSuchGroup::: when a group is configured that doesn't exist
|
45
|
+
# * SystemCallError::: if there is an error creating the pidfile
|
46
|
+
def configure_process
|
47
|
+
Expander.config.daemonize? ? daemonize : ensure_exclusive
|
48
|
+
set_user_and_group
|
49
|
+
end
|
50
|
+
|
51
|
+
def daemonize
|
52
|
+
acquire_locks
|
53
|
+
exit if fork
|
54
|
+
Process.setsid
|
55
|
+
exit if fork
|
56
|
+
write_pid
|
57
|
+
Dir.chdir('/')
|
58
|
+
STDIN.reopen("/dev/null")
|
59
|
+
STDOUT.reopen("/dev/null", "a")
|
60
|
+
STDERR.reopen("/dev/null", "a")
|
61
|
+
end
|
62
|
+
|
63
|
+
# When not forking into the background, this ensures only one chef-expander
|
64
|
+
# is running with a given config and writes the process id to the pidfile.
|
65
|
+
def ensure_exclusive
|
66
|
+
acquire_locks
|
67
|
+
write_pid
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_user_and_group
|
71
|
+
return nil if Expander.config.user.nil?
|
72
|
+
|
73
|
+
if Expander.config.group.nil?
|
74
|
+
log.info {"Changing user to #{Expander.config.user}"}
|
75
|
+
else
|
76
|
+
log.info {"Changing user to #{Expander.config.user} and group to #{Expander.config.group}"}
|
77
|
+
end
|
78
|
+
|
79
|
+
unless (set_group && set_user)
|
80
|
+
log.error {"Unable to change user to #{Expander.config.user} - Are you root?"}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Deletes the pidfile, releasing the exclusive lock on it in the process.
|
85
|
+
def release_locks
|
86
|
+
File.unlink(@pidfile.path) if File.exist?(@pidfile.path)
|
87
|
+
@pidfile.close unless @pidfile.closed?
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def set_user
|
93
|
+
Process::Sys.setuid(target_uid)
|
94
|
+
true
|
95
|
+
rescue Errno::EPERM => e
|
96
|
+
log.debug {e}
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def set_group
|
101
|
+
if gid = target_uid
|
102
|
+
Process::Sys.setgid(gid)
|
103
|
+
end
|
104
|
+
true
|
105
|
+
rescue Errno::EPERM
|
106
|
+
log.debug {e}
|
107
|
+
false
|
108
|
+
end
|
109
|
+
|
110
|
+
def target_uid
|
111
|
+
user = Expander.config.user
|
112
|
+
user.kind_of?(Fixnum) ? user : Etc.getpwnam(user).uid
|
113
|
+
rescue ArgumentError => e
|
114
|
+
log.debug {e}
|
115
|
+
raise NoSuchUser, "Cannot change user to #{user} - failed to find the uid"
|
116
|
+
end
|
117
|
+
|
118
|
+
def target_gid
|
119
|
+
if group = Expander.config.group
|
120
|
+
group.kind_of?(Fixnum) ? group : Etc.getgrnam(group).gid
|
121
|
+
else
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
rescue ArgumentError => e
|
125
|
+
log.debug {e}
|
126
|
+
raise NoSuchGroup, "Cannot change group to #{group} - failed to find the gid"
|
127
|
+
end
|
128
|
+
|
129
|
+
def acquire_locks
|
130
|
+
@pidfile = File.open(Expander.config.pidfile, File::RDWR|File::CREAT, 0644)
|
131
|
+
unless @pidfile.flock(File::LOCK_EX | File::LOCK_NB)
|
132
|
+
pid = @pidfile.read.strip
|
133
|
+
msg = "Another instance of chef-expander (pid: #{pid}) has a lock on the pidfile (#{Expander.config.pidfile}). \n"\
|
134
|
+
"Configure a different pidfile to run multiple instances of chef-expander at once."
|
135
|
+
raise AlreadyRunning, msg
|
136
|
+
end
|
137
|
+
rescue Exception
|
138
|
+
@pidfile.close if @pidfile && !@pidfile.closed?
|
139
|
+
raise
|
140
|
+
end
|
141
|
+
|
142
|
+
def write_pid
|
143
|
+
@pidfile.truncate(0)
|
144
|
+
@pidfile.print("#{Process.pid}\n")
|
145
|
+
@pidfile.flush
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|