amiral 0.1.0

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