pazuzu 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,152 @@
1
+ module Pazuzu
2
+ module CommandLine
3
+
4
+ class Controller
5
+
6
+ def initialize
7
+ end
8
+
9
+ def run!(arguments)
10
+ config_path, socket_path, format = nil, nil, 'text'
11
+
12
+ OptionParser.new do |opts|
13
+ opts.banner = "#{File.basename($0)} [OPTIONS] COMMAND [...]"
14
+ opts.separator ""
15
+ opts.on('--socket PATH', 'Connect to socket PATH') do |value|
16
+ socket_path = File.expand_path(value)
17
+ end
18
+ opts.on("-c FILE", "--config FILE", 'Read configuration from FILE') do |value|
19
+ config_path = File.expand_path(value)
20
+ end
21
+ opts.on('-f FORMAT', '--format FORMAT') do |value|
22
+ format = value
23
+ end
24
+ opts.on("-h", "--help", "Show this help.") do
25
+ puts opts
26
+ exit
27
+ end
28
+ opts.parse!(arguments)
29
+ end
30
+
31
+ if arguments.empty?
32
+ abort "No command specified. Run with 'help' for list of commands."
33
+ end
34
+
35
+ unless %w(text json).include?(format)
36
+ abort "Invalid format '#{format}'."
37
+ end
38
+
39
+ if config_path
40
+ configuration = YAML.load(File.open(config_path))
41
+ socket_path = configuration['socket_path']
42
+ end
43
+ socket_path ||= Supervisor::DEFAULT_SOCKET_PATH
44
+
45
+ command, command_arguments = arguments[0], arguments[1..-1]
46
+
47
+ begin
48
+ client = Control::SocketClient.new(socket_path)
49
+ rescue Errno::ECONNREFUSED
50
+ abort "Connection refused on #{socket_path}. Server might not be running."
51
+ else
52
+ response = client.execute(command, *command_arguments)
53
+ if response
54
+ case format
55
+ when 'json'
56
+ puts JSON.dump(response, :pretty => true)
57
+ when 'text'
58
+ if response['success']
59
+ puts(response['success'])
60
+ elsif response['error']
61
+ abort(response['error'])
62
+ elsif response['results']
63
+ case command
64
+ when 'log'
65
+ print_log(response['results'])
66
+ when 'help'
67
+ print_help(response['results'])
68
+ when 'list'
69
+ print_applications(response['results'])
70
+ else
71
+ abort("Don't know how to print results from '#{command}'.")
72
+ end
73
+ else
74
+ abort("Unrecognized response from server: #{JSON.dump response}")
75
+ end
76
+ end
77
+ else
78
+ exit(1)
79
+ end
80
+ end
81
+ end
82
+
83
+ def print_log(entries)
84
+ entries.each do |entry|
85
+ puts [
86
+ entry['time'],
87
+ entry['source'],
88
+ entry['message']
89
+ ].join("\t")
90
+ end
91
+ end
92
+
93
+ def print_help(items)
94
+ puts "Command Description"
95
+ puts "------------------------- ------------------------------"
96
+ items.each do |item|
97
+ puts [
98
+ "%-25s" % item['syntax'],
99
+ "%-30s" % item['description']
100
+ ].join(" ")
101
+ end
102
+ end
103
+
104
+ def print_applications(applications)
105
+ puts "Process State Command"
106
+ puts "---------------------- ------------------------------ ----------------------------"
107
+ applications.each do |application|
108
+ (application['workers'] || []).each do |worker|
109
+ (worker['instances'] || []).each do |instance|
110
+ state = instance['state']
111
+ if state == 'running'
112
+ state = format_uptime(instance['uptime'])
113
+ end
114
+ recovery_count = instance['recovery_count']
115
+ if recovery_count and recovery_count > 0
116
+ state << " (#{recovery_count} recoveries)"
117
+ end
118
+ puts [
119
+ "%-22s" % instance['name'],
120
+ "%-30s" % state,
121
+ instance['command']
122
+ ].join(" ")
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ def format_uptime(uptime)
129
+ s = 'running'
130
+ if uptime
131
+ s << ' for '
132
+ days, hours, minutes, seconds =
133
+ (uptime / 1.day).floor,
134
+ (uptime / 1.hour).floor,
135
+ (uptime / 1.minute).floor,
136
+ (uptime / 1.second).floor
137
+ if days > 0
138
+ s << "#{days}d, #{hours}h"
139
+ elsif hours > 0 or minutes > 0
140
+ s << "#{hours}h, " if hours > 0
141
+ s << "#{minutes}m"
142
+ else
143
+ s << "#{seconds}s"
144
+ end
145
+ end
146
+ s
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,304 @@
1
+ module Pazuzu
2
+ module Control
3
+
4
+ class Protocol
5
+
6
+ COMMAND_MAPPINGS = [
7
+ {
8
+ :pattern => /^help$/,
9
+ :command => :help,
10
+ :usage => 'help',
11
+ :description => 'Show commands.'
12
+ },
13
+ {
14
+ :pattern => /^q(?:uit)?$/,
15
+ :command => :quit,
16
+ :usage => 'q, quit',
17
+ :description => 'Quits session.'
18
+ },
19
+ {
20
+ :pattern => /^list(?:\s+(?<spec>[^\s]+))?$/,
21
+ :command => :list,
22
+ :usage => 'list [SPEC]',
23
+ :description => 'Lists applications whose name, worker or instance matches SPEC (eg., "myapp", "myapp.web*").'
24
+ },
25
+ {
26
+ :pattern => /^log(?:\s+(?<spec>[^\s]+)(?:\s+(?<limit>.*))?)?$/,
27
+ :command => :log,
28
+ :usage => 'log [SPEC] [LIMIT]',
29
+ :description => 'Lists log entries for application, workers or instances matching SPEC, at most LIMIT lines.'
30
+ },
31
+ {
32
+ :pattern => /^start(?:\s+(?<spec>[^\s]+))?$/,
33
+ :command => :start,
34
+ :usage => 'start SPEC',
35
+ :description => 'Starts an application, worker or instance by SPEC.'
36
+ },
37
+ {
38
+ :pattern => /^restart(?:\s+(?<spec>[^\s]+))?$/,
39
+ :command => :restart,
40
+ :usage => 'restart SPEC',
41
+ :description => 'Restarts an application, worker or instance by SPEC. Stopped matches will be started.'
42
+ },
43
+ {
44
+ :pattern => /^stop(?:\s+(?<spec>[^\s]+))?$/,
45
+ :command => :stop,
46
+ :usage => 'stop SPEC',
47
+ :description => 'Stops an application, worker or instance by SPEC.'
48
+ },
49
+ {
50
+ :pattern => /^scale\s+(?<spec>[^\s]+)(?:\s+(?<value>(?:\+|-)?[^\s]+))?$/,
51
+ :command => :scale,
52
+ :usage => 'scale WORKER-SPEC VALUE',
53
+ :description => 'Set the number of instances for a worker (or workers). Value may be an absolute number, or a relative (+1, -1), or the string "default".'
54
+ }
55
+ ].freeze
56
+
57
+ def initialize(io, supervisor)
58
+ @io = io
59
+ @supervisor = supervisor
60
+ @active = true
61
+ @logger = Utility::AnnotatedLogger.new(
62
+ @supervisor.logger, 'Control socket')
63
+ end
64
+
65
+ def run!
66
+ while @active
67
+ line = @io.gets
68
+ break unless line
69
+ line.strip!
70
+ unless line.blank?
71
+ handle_command(line)
72
+ end
73
+ end
74
+ end
75
+
76
+ def handle_command(line)
77
+ @logger.info "Received: #{line}"
78
+ matched = false
79
+ COMMAND_MAPPINGS.each do |command|
80
+ match = command[:pattern].match(line)
81
+ if match
82
+ self.send("handle_#{command[:command]}_command", match)
83
+ matched = true
84
+ break
85
+ end
86
+ end
87
+ unless matched
88
+ error! "Don't understand '#{line}'"
89
+ end
90
+ end
91
+
92
+ def handle_help_command(options)
93
+ results! COMMAND_MAPPINGS.map { |command|
94
+ {
95
+ :syntax => command[:usage],
96
+ :description => command[:description]
97
+ }
98
+ }
99
+ end
100
+
101
+ def handle_list_command(options)
102
+ spec = options[:spec]
103
+ results = Set.new
104
+ match_objects(spec) do |object|
105
+ case object
106
+ when Application
107
+ results.add(object)
108
+ when Worker
109
+ results.add(object.application)
110
+ when Instance
111
+ results.add(object.worker.application)
112
+ end
113
+ end
114
+ results! results.map { |object| serialize_object(object) }
115
+ end
116
+
117
+ def handle_log_command(options)
118
+ spec = options[:spec]
119
+ limit = options[:limit].try(:to_i)
120
+ matches = collapse_hierarchy(match_objects(spec))
121
+ entries = matches.map { |object| object.log_entries }
122
+ entries.flatten!(1)
123
+ entries.sort_by! { |(source, time, message)| time }
124
+ entries.slice!(0, entries.length - limit) if limit
125
+ results! entries.map { |(source, time, message)|
126
+ {:source => source, :time => time, :message => message}
127
+ }
128
+ end
129
+
130
+ def handle_start_command(options)
131
+ spec = options[:spec]
132
+ matches = collapse_hierarchy(match_objects(spec))
133
+ matches |= match_objects(spec).select { |object| object.run_state != :running }
134
+ matches.each do |object|
135
+ object.start!
136
+ end
137
+ if matches.any?
138
+ success! "Started #{matches.map(&:qname).join(', ')}."
139
+ else
140
+ error! "No matches."
141
+ end
142
+ end
143
+
144
+ def handle_restart_command(options)
145
+ spec = options[:spec]
146
+ matches = collapse_hierarchy(match_objects(spec))
147
+ matches |= match_objects(spec).select { |object| object.run_state != :running }
148
+ matches.each do |object|
149
+ object.stop!
150
+ end
151
+ if matches.any?
152
+ begin
153
+ timeout(30) do
154
+ loop do
155
+ break if matches.all? { |object| object.run_state == :stopped }
156
+ sleep(1)
157
+ end
158
+ end
159
+ matches.each do |object|
160
+ object.start!
161
+ end
162
+ timeout(30) do
163
+ loop do
164
+ break if matches.all? { |object| object.run_state == :running }
165
+ sleep(1)
166
+ end
167
+ end
168
+ rescue Timeout::Error
169
+ error! "Timed out."
170
+ else
171
+ success! "Restarted #{matches.map(&:qname).join(', ')}."
172
+ end
173
+ else
174
+ error! "No matches."
175
+ end
176
+ end
177
+
178
+ def handle_stop_command(options)
179
+ spec = options[:spec]
180
+ matches = collapse_hierarchy(match_objects(spec))
181
+ matches.each do |object|
182
+ object.stop!
183
+ end
184
+ if matches.any?
185
+ success! "Stopped #{matches.map(&:qname).join(', ')}."
186
+ else
187
+ error! "No matches."
188
+ end
189
+ end
190
+
191
+ def handle_scale_command(options)
192
+ spec, value = options[:spec], options[:value]
193
+ if value
194
+ matches = []
195
+ match_objects(spec) do |object|
196
+ if object.is_a?(Worker)
197
+ case value
198
+ when /^(\+|-)/
199
+ object.add_instance_count!(value.to_i)
200
+ when /^default$/i
201
+ object.revert_dynamic_instance_count!
202
+ else
203
+ object.set_instance_count!(value.to_i)
204
+ end
205
+ matches << object
206
+ end
207
+ end
208
+ if matches.any?
209
+ success! "Adjusted #{matches.map(&:qname).join(', ')}."
210
+ else
211
+ error! "No matches."
212
+ end
213
+ else
214
+ error! "No value specified."
215
+ end
216
+ end
217
+
218
+ def handle_quit_command(options)
219
+ success! 'It was a pleasure.'
220
+ @active = false
221
+ end
222
+
223
+ def respond!(data)
224
+ output = JSON.dump(data, :pretty => true)
225
+ @logger.debug "Sending: #{output}"
226
+ @io.puts('%010x' % output.length)
227
+ @io.puts(output)
228
+ end
229
+
230
+ def results!(results)
231
+ respond!(:results => results)
232
+ end
233
+
234
+ def error!(message)
235
+ respond!(:error => message)
236
+ end
237
+
238
+ def success!(message)
239
+ respond!(:success => message)
240
+ end
241
+
242
+ def serialize_object(object)
243
+ data = {}
244
+ data[:name] = object.qname
245
+ data[:command] = object.command_line if object.respond_to?(:command_line)
246
+ data[:state] = object.run_state
247
+ data[:started_at] = object.started_at.iso8601 if object.started_at
248
+ data[:stopped_at] = object.stopped_at.iso8601 if object.stopped_at
249
+ data[:uptime] = Time.now - object.started_at if object.started_at
250
+ case object
251
+ when Application
252
+ data[:workers] = object.workers.map { |worker| serialize_object(worker) }
253
+ when Worker
254
+ data[:instances] = object.instances.map { |instance| serialize_object(instance) }
255
+ when Instance
256
+ data[:recovery_count] = object.recovery_count
257
+ end
258
+ data
259
+ end
260
+
261
+ def collapse_hierarchy(objects)
262
+ return objects.dup.delete_if { |object|
263
+ case object
264
+ when Worker
265
+ objects.include?(object.application)
266
+ when Instance
267
+ objects.include?(object.worker) || objects.include?(object.worker.application)
268
+ else
269
+ false
270
+ end
271
+ }
272
+ end
273
+
274
+ def match_objects(spec, &block)
275
+ results = Set.new
276
+ @supervisor.applications.each do |application|
277
+ application.workers.each do |worker|
278
+ worker.instances.each do |instance|
279
+ if match_name?(instance.qname, spec)
280
+ results << instance
281
+ yield instance if block
282
+ end
283
+ end
284
+ if match_name?(worker.qname, spec)
285
+ results << worker
286
+ yield worker if block
287
+ end
288
+ end
289
+ if match_name?(application.qname, spec)
290
+ results << application
291
+ yield application if block
292
+ end
293
+ end
294
+ results.to_a
295
+ end
296
+
297
+ def match_name?(name, spec)
298
+ spec.blank? || name == spec || File.fnmatch(spec, name)
299
+ end
300
+
301
+ end
302
+
303
+ end
304
+ end