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.
@@ -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