chef-expander 0.10.0.beta.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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