pazuzu 0.1.2

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,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