amiral 0.1.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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+
3
+ # This is an RVM Project .rvmrc file, used to automatically load the ruby
4
+ # development environment upon cd'ing into the directory
5
+
6
+ # First we specify our desired <ruby>[@<gemset>], the @gemset name is optional,
7
+ # Only full ruby name is supported here, for short names use:
8
+ # echo "rvm use 1.9.3" > .rvmrc
9
+ environment_id="ruby-1.9.3-p194@amiral"
10
+
11
+ # Uncomment the following lines if you want to verify rvm version per project
12
+ # rvmrc_rvm_version="1.16.6 (stable)" # 1.10.1 seams as a safe start
13
+ # eval "$(echo ${rvm_version}.${rvmrc_rvm_version} | awk -F. '{print "[[ "$1*65536+$2*256+$3" -ge "$4*65536+$5*256+$6" ]]"}' )" || {
14
+ # echo "This .rvmrc file requires at least RVM ${rvmrc_rvm_version}, aborting loading."
15
+ # return 1
16
+ # }
17
+
18
+ # First we attempt to load the desired environment directly from the environment
19
+ # file. This is very fast and efficient compared to running through the entire
20
+ # CLI and selector. If you want feedback on which environment was used then
21
+ # insert the word 'use' after --create as this triggers verbose mode.
22
+ if [[ -d "${rvm_path:-$HOME/.rvm}/environments"
23
+ && -s "${rvm_path:-$HOME/.rvm}/environments/$environment_id" ]]
24
+ then
25
+ \. "${rvm_path:-$HOME/.rvm}/environments/$environment_id"
26
+ [[ -s "${rvm_path:-$HOME/.rvm}/hooks/after_use" ]] &&
27
+ \. "${rvm_path:-$HOME/.rvm}/hooks/after_use" || true
28
+ else
29
+ # If the environment file has not yet been created, use the RVM CLI to select.
30
+ rvm --create "$environment_id" || {
31
+ echo "Failed to create RVM environment '${environment_id}'."
32
+ return 1
33
+ }
34
+ fi
35
+
36
+ # If you use bundler, this might be useful to you:
37
+ # if [[ -s Gemfile ]] && {
38
+ # ! builtin command -v bundle >/dev/null ||
39
+ # builtin command -v bundle | GREP_OPTIONS= \grep $rvm_path/bin/bundle >/dev/null
40
+ # }
41
+ # then
42
+ # printf "%b" "The rubygem 'bundler' is not installed. Installing it now.\n"
43
+ # gem install bundler
44
+ # fi
45
+ # if [[ -s Gemfile ]] && builtin command -v bundle >/dev/null
46
+ # then
47
+ # bundle install | GREP_OPTIONS= \grep -vE '^Using|Your bundle is complete'
48
+ # fi
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in amiral.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2012 Pierre-Yves Ritschard <pyr@spootnik.org>
2
+
3
+ Permission to use, copy, modify, and distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/amiral.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'amiral/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "amiral"
8
+ gem.version = Amiral::VERSION
9
+ gem.authors = ["Pierre-Yves Ritschard"]
10
+ gem.email = ["pyr@spootnik.org"]
11
+ gem.description = %q{simple command and control based on redis}
12
+ gem.summary = %q{issue commands on a fleet of hosts}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+ gem.add_dependency 'redis'
20
+ gem.add_dependency 'json'
21
+ gem.add_dependency 'facter'
22
+ gem.add_dependency 'awesome_print'
23
+ gem.add_dependency 'uuidtools'
24
+ gem.add_dependency 'daemons'
25
+ end
data/bin/amiral.rb ADDED
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ lib = File.expand_path('../../lib', __FILE__)
4
+ $:.unshift(lib) unless $:.include?(lib)
5
+
6
+ require 'rubygems'
7
+ require 'redis'
8
+ require 'amiral'
9
+ require 'optparse'
10
+ require 'ostruct'
11
+ require 'logger'
12
+
13
+ options = OpenStruct.new
14
+ options.logfile = "amiral.log"
15
+ options.host = "localhost"
16
+ options.port = 6379
17
+ options.match_spec = {:all => true}
18
+ options.foreground = false
19
+ options.rundir = "."
20
+ options.loglevel = Logger::INFO
21
+
22
+ opts = OptionParser.new do |opts|
23
+ opts.banner = "Usage: amiral-agent [options]"
24
+
25
+ opts.separator ""
26
+
27
+ opts.on("-k", "--private-key [keypath]", String, "SSH private key path") do |keypath|
28
+ tab = keypath.split(':')
29
+ if tab.length > 1
30
+ options.keytype = tab[0].to_sym
31
+ options.keypath = tab[1]
32
+ else
33
+ options.keytype = :dss
34
+ options.keypath = keypath
35
+ end
36
+ end
37
+
38
+ opts.on("-l", "--log-level [level]", String, "log level") do |level|
39
+ case level
40
+ when "debug"
41
+ options.loglevel = Logger::DEBUG
42
+ when "info"
43
+ options.loglevel = Logger::INFO
44
+ when "warn"
45
+ options.loglevel = Logger::WARN
46
+ when "error"
47
+ options.loglevel = Logger::ERROR
48
+ end
49
+ end
50
+
51
+ opts.on("-c", "--redis-host [host]", String, "redis host") do |host|
52
+ options.host = host
53
+ end
54
+
55
+ opts.on("-p", "--redis-port [port]", OptionParser::DecimalInteger,
56
+ "redis port") do |port|
57
+ options.port = port
58
+ end
59
+
60
+ opts.on("-x", "--exchange [exchange]", String,
61
+ "redis pubsub exchange name") do |exchange|
62
+ options.exchange = exchange
63
+ end
64
+
65
+ opts.on("-q", "--response-queue [queue]", String,
66
+ "redis response queue name") do |rqueue|
67
+ options.queue_response = rqueue
68
+ end
69
+
70
+ opts.on("-a", "--ack-queue [queue]", String,
71
+ "redis acknowledgement queue name") do |aqueue|
72
+ options.queue_ack = aqueue
73
+ end
74
+
75
+ opts.on("-r", "--rundir [directory]", String, "agent run directory") do |rundir|
76
+ options.rundir = rundir
77
+ end
78
+
79
+ opts.on("-f", "--[no]-foreground") do |foreground|
80
+ options.foreground = foreground
81
+ end
82
+
83
+ opts.on("-m", "--match [filter]", String, "node match filter") do |matcher|
84
+ options.match_spec = Amiral::Matcher.new(matcher).spec
85
+ end
86
+
87
+ opts.on("-T", "--timeouts [timeouts]", String,
88
+ "acknowledgement and response timeouts") do |timeouts|
89
+ tab = timeouts.split(',')
90
+ raise "timeouts need two values" unless tab.length == 2
91
+ options.ack_timeout = tab[0].to_i
92
+ options.reponse_timeout = tab[1].to_i
93
+ end
94
+ end.parse!
95
+
96
+ options.redis_in = Redis.new :host => options.host, :port => options.port
97
+ options.redis_out = Redis.new :host => options.host, :port => options.port
98
+
99
+ classes = {
100
+ 'agent' => Amiral::Agent,
101
+ 'controller' => Amiral::Controller
102
+ }
103
+
104
+ role = ARGV.shift
105
+
106
+ raise "no such role: #{role}" unless klass = classes[role]
107
+
108
+ klass.new(options.marshal_dump).tap{|o| o.run(ARGV)}.show
@@ -0,0 +1,3 @@
1
+ module Amiral
2
+ VERSION = '0.1.0'
3
+ end
data/lib/amiral.rb ADDED
@@ -0,0 +1,379 @@
1
+ require 'rubygems'
2
+ require 'redis'
3
+ require 'openssl'
4
+ require 'base64'
5
+ require 'json'
6
+ require 'facter'
7
+ require 'uuidtools'
8
+ require 'awesome_print'
9
+ require 'daemons'
10
+ require 'logger'
11
+
12
+ module Amiral
13
+ KEYTYPES = {
14
+ :dss => OpenSSL::PKey::DSA,
15
+ :rsa => OpenSSL::PKey::RSA
16
+ }
17
+
18
+ module Providers
19
+ class Uptime
20
+
21
+ PATTERN = /^([0-9:]+) up (.*),[ \t]+([0-9]+) users,[ \t]+load average: ([0-9.]+), ([0-9.]+), ([0-9.]+)/
22
+ def execute message
23
+ uptime = `uptime`.strip
24
+
25
+ if uptime =~ PATTERN
26
+ {
27
+ :exit => 0,
28
+ :time => $1,
29
+ :since => $2,
30
+ :users => $3,
31
+ :averages => [$4, $5, $6],
32
+ :short => uptime,
33
+ }
34
+ else
35
+ {
36
+ :exit => 1,
37
+ :short => "could not parse uptime",
38
+ :data => {:uptime => uptime}
39
+ }
40
+ end
41
+ end
42
+ end
43
+
44
+ class Facts
45
+ def execute message
46
+ facts = Facter.to_hash
47
+ {
48
+ :exit => 0,
49
+ :facts => facts,
50
+ :short => "returned #{facts.length} facts"
51
+ }
52
+ end
53
+ end
54
+
55
+ class Ping
56
+ def execute message
57
+ {:exit => 0, :short => "alive"}
58
+ end
59
+ end
60
+
61
+ class ProviderList
62
+ def execute message
63
+ {:exit => 0, :short => PROVIDERS.keys}
64
+ end
65
+ end
66
+
67
+ class Invalid
68
+ def execute message
69
+ {:exit => 1, :short => "invalid provider requested"}
70
+ end
71
+ end
72
+ end
73
+
74
+ PROVIDERS = {
75
+ "uptime" => Amiral::Providers::Uptime,
76
+ "facts" => Amiral::Providers::Facts,
77
+ "provider_list" => Amiral::Providers::ProviderList,
78
+ "ping" => Amiral::Providers::Ping,
79
+ "invalid" => Amiral::Providers::Invalid
80
+ }
81
+
82
+ module Message
83
+
84
+ def serialize data
85
+ json = data.to_json
86
+ digest = OpenSSL::Digest::SHA1.digest(json)
87
+ sig = Base64.encode64(@privkey.syssign(digest)).chomp
88
+ "#{sig}:#{json}"
89
+ end
90
+
91
+ def deserialize data
92
+ (sig, json) = data.split(':', 2)
93
+ raise "wrong format" unless (sig && json)
94
+ sig = Base64.decode64(sig)
95
+ unless @privkey.sysverify(OpenSSL::Digest::SHA1.digest(json), sig)
96
+ raise "invalid signature"
97
+ end
98
+ JSON.parse json
99
+ end
100
+
101
+ def hostname
102
+ `hostname`.strip
103
+ end
104
+
105
+ def uuidgen
106
+ UUIDTools::UUID.random_create.to_s
107
+ end
108
+
109
+ def timestamp
110
+ Time.new.to_i
111
+ end
112
+ end
113
+
114
+ class Matcher
115
+
116
+ attr_accessor :spec
117
+
118
+ def match
119
+ return true if (@spec.has_key? :all && @spec[:all])
120
+
121
+ if @spec.has_key? :hostname
122
+ return false unless `hostname`.chomp =~ Regexp.new(@spec[:hostname])
123
+ end
124
+
125
+ ## XXX: platform
126
+
127
+ if @spec.has_key? :provider
128
+ return false unless PROVIDERS.has_key? @spec[:provider]
129
+ end
130
+
131
+ if @spec.has_key? :facts
132
+ @spec[:facts].map do |k,v|
133
+ return false unless Facter.value(k).to_s =~ Regexp.new(v)
134
+ end
135
+ end
136
+ true
137
+ end
138
+
139
+ def initialize match_spec
140
+ case match_spec.class.to_s
141
+ when "String"
142
+ @spec = match_spec.split(',').map do |expr|
143
+ (lval, rval) = expr.split('=', 2)
144
+ case lval
145
+ when 'all'
146
+ { :all => true }
147
+ when /^fact:/
148
+ { :facts => { lval.split(':', 2)[1] => rval } }
149
+ when /^hostname/
150
+ { :hostname => rval }
151
+ when /^provider/
152
+ { :provider => rval }
153
+ when /^platform/
154
+ { :platform => rval }
155
+ else
156
+ raise "unsupported match filter type: #{lval}"
157
+ end
158
+ end.reduce do |e1,e2|
159
+ e1.merge(e2) do |e,c1,c2|
160
+ c1.merge c2
161
+ end
162
+ end
163
+ when "Hash"
164
+ @spec = match_spec.inject({}){|memo,(k,v)| memo[k.to_sym] = v; memo}
165
+ else
166
+ raise "don't know how to create match from #{match_spec} (#{match_spec.class})"
167
+ end
168
+ end
169
+
170
+ def serialize
171
+ @spec.to_json
172
+ end
173
+
174
+ end
175
+
176
+ class Agent
177
+ include Message
178
+
179
+ def initialize settings
180
+ @settings = {
181
+ :redis_in => Redis.new,
182
+ :redis_out => Redis.new,
183
+ :exchange => "req",
184
+ :queue_ack => "ack",
185
+ :queue_response => "res",
186
+ :keytype => :dss,
187
+ :hostname => hostname,
188
+ :loglevel => Logger::INFO,
189
+ :rundir => "."
190
+ }.merge settings
191
+
192
+ @logger = Logger.new(STDOUT)
193
+ @logger.level = @settings[:loglevel]
194
+
195
+ raise "need key path" unless @settings[:keypath]
196
+
197
+ @privkey = File.open @settings[:keypath] do |file|
198
+ KEYTYPES[@settings[:keytype]].new file
199
+ end
200
+
201
+ @redis_in = @settings[:redis_in]
202
+ @redis_out = @settings[:redis_out]
203
+ end
204
+
205
+ def run args
206
+ begin
207
+ Daemons.daemonize({:backtrace => true,
208
+ :ontop => @settings[:foreground],
209
+ :dir_mode => :normal,
210
+ :dir => @settings[:rundir],
211
+ :app_name => "amiral-agent",
212
+ :log_output => true})
213
+ listen
214
+ rescue SystemExit => err
215
+ @logger.debug("agent starting")
216
+ rescue Exception => err
217
+ @logger.fatal("caught exception, exiting")
218
+ @logger.fatal(err)
219
+ end
220
+ end
221
+
222
+ def show
223
+ end
224
+
225
+ def listen
226
+ exchange = @settings[:exchange]
227
+ @redis_in.subscribe exchange do |on|
228
+
229
+ on.message do |x, payload|
230
+
231
+ @logger.debug "incoming message"
232
+ message = deserialize payload
233
+
234
+ if agent_matches? message['match']
235
+ @logger.info "valid request type: #{message['command']['provider']}"
236
+ uuid = uuidgen
237
+ send_ack(:in_reply_to => message['reply_to'],
238
+ :uuid => uuid,
239
+ :status => "start")
240
+ begin
241
+ out = execute message
242
+ rescue Exception => e
243
+ @logger.error "provider crashed: #{e}"
244
+ @logger.error e
245
+ end
246
+ send_response(:in_reply_to => message['reply_to'],
247
+ :uuid => uuid,
248
+ :status => "complete",
249
+ :output => out)
250
+ else
251
+ send_ack(:in_reply_to => message['reply_to'],
252
+ :status => "noop")
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ def agent_matches? matcher
259
+ m = Matcher.new matcher
260
+ m.match
261
+ end
262
+
263
+ def execute message
264
+ (PROVIDERS[message['command']['provider']] ||
265
+ Amiral::Providers::Invalid).new.execute message
266
+ end
267
+
268
+ def send_ack response
269
+ send_payload @settings[:queue_ack], response
270
+ end
271
+
272
+ def send_response response
273
+ send_payload @settings[:queue_response], response
274
+ end
275
+
276
+ def send_payload q, response
277
+ response[:hostname] = @settings[:hostname]
278
+ @redis_out.lpush q, serialize(response)
279
+ end
280
+ end
281
+
282
+ class Controller
283
+ include Message
284
+
285
+ def initialize settings
286
+ @settings = {
287
+ :redis => Redis.new,
288
+ :exchange => "req",
289
+ :queue_ack => "ack",
290
+ :queue_response => "res",
291
+ :ack_timeout => 2,
292
+ :response_timeout => 5,
293
+ :match_spec => {:all => true},
294
+ :keytype => :dss,
295
+ }.merge settings
296
+
297
+ raise "need key path" unless @settings[:keypath]
298
+
299
+ @privkey = File.open @settings[:keypath] do |file|
300
+ KEYTYPES[@settings[:keytype]].new file
301
+ end
302
+
303
+ @redis = @settings[:redis]
304
+ end
305
+
306
+ def run args
307
+ provider = args.shift
308
+ request provider, {}
309
+ end
310
+
311
+ def request provider, args
312
+
313
+ uuid = uuidgen
314
+ request = {
315
+ :command => {
316
+ :provider => provider,
317
+ :args => args
318
+ },
319
+ :reply_to => uuid,
320
+ :match => @settings[:match_spec]
321
+ }
322
+
323
+ @redis.publish @settings[:exchange], serialize(request)
324
+
325
+ # pop items for 2 seconds
326
+ acks = []
327
+ puts "accepting acknowledgements for #{@settings[:ack_timeout]} seconds"
328
+ limit = timestamp + @settings[:ack_timeout]
329
+
330
+ while timestamp < limit do
331
+ t = timestamp
332
+ t = (limit - t == 0) ? 1 : (limit - t)
333
+ (q, data) = @redis.brpop(@settings[:queue_ack], t)
334
+ if data
335
+ ack = deserialize data
336
+ acks << ack if ack['in_reply_to'] == uuid
337
+ end
338
+ end
339
+
340
+ pos_acks = acks.keep_if do |a|
341
+ a['status'] == 'start'
342
+ end
343
+
344
+ remaining = pos_acks.length
345
+ puts "got #{remaining}/#{acks.length} positive acknowledgements"
346
+
347
+ # wait for responses now
348
+ @responses = []
349
+ limit = timestamp + @settings[:response_timeout]
350
+ while timestamp < limit && remaining > 0 do
351
+ t = timestamp
352
+ t = (limit - t == 0) ? 1 : (limit - t)
353
+ (q, data) = @redis.brpop(@settings[:queue_response], t)
354
+
355
+ if data
356
+ res = deserialize data
357
+ if res['in_reply_to'] == uuid
358
+ @responses << res
359
+ remaining -= 1
360
+ end
361
+ end
362
+ end
363
+
364
+ puts "got #{@responses.length}/#{pos_acks.length} responses"
365
+ @responses
366
+ end
367
+
368
+ def show
369
+ @responses.map do |node|
370
+ if node['output']['exit'] == 0
371
+ printf "%20s: %s\n", node['hostname'], node['output']['short']
372
+ else
373
+ printf "%20s: failed!\n", node['hostname']
374
+ ap node
375
+ end
376
+ end
377
+ end
378
+ end
379
+ end
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amiral
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Pierre-Yves Ritschard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: json
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: facter
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: awesome_print
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: uuidtools
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: daemons
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: simple command and control based on redis
111
+ email:
112
+ - pyr@spootnik.org
113
+ executables:
114
+ - amiral.rb
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - .gitignore
119
+ - .rvmrc
120
+ - Gemfile
121
+ - LICENSE.txt
122
+ - Rakefile
123
+ - amiral.gemspec
124
+ - bin/amiral.rb
125
+ - lib/amiral.rb
126
+ - lib/amiral/version.rb
127
+ homepage: ''
128
+ licenses: []
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ none: false
135
+ requirements:
136
+ - - ! '>='
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ none: false
141
+ requirements:
142
+ - - ! '>='
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ requirements: []
146
+ rubyforge_project:
147
+ rubygems_version: 1.8.24
148
+ signing_key:
149
+ specification_version: 3
150
+ summary: issue commands on a fleet of hosts
151
+ test_files: []