knife-batch 1.0.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 +4 -0
- data/Gemfile +4 -0
- data/README.markdown +15 -0
- data/Rakefile +1 -0
- data/knife-batch.gemspec +24 -0
- data/lib/chef/knife/batch.rb +213 -0
- data/lib/chef/knife/knife-batch.rb +213 -0
- data/lib/knife-batch/version.rb +5 -0
- metadata +65 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.markdown
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# knife batch
|
2
|
+
|
3
|
+
`knife batch` is a wonderful little plugin for executing commands a la `knife ssh`, but doing it in groups of `n` with a sleep between execution iterations.
|
4
|
+
|
5
|
+
# Installation
|
6
|
+
|
7
|
+
`gem install knife-batch`
|
8
|
+
|
9
|
+
# Usage
|
10
|
+
|
11
|
+
`knife batch` works exactly like `knife ssh` but with a couple of additional options.
|
12
|
+
`knife batch "role:cluster" "whoami" -B 10 -W 5` will execute `whoami` against 10 servers with a sleep of 5 seconds in between.
|
13
|
+
|
14
|
+
`-B INTEGER` defines how many servers at max will be batched.
|
15
|
+
`-W INTEGER` defines the time to sleep in between executions.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/knife-batch.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "knife-batch/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "knife-batch"
|
7
|
+
s.version = Knife::Batch::VERSION
|
8
|
+
s.authors = ["Ian Meyer"]
|
9
|
+
s.email = ["ianmmeyer@gmail.com"]
|
10
|
+
s.homepage = "http://github.com/imeyer/knife-batch"
|
11
|
+
s.summary = %q{Knife plugin to run ssh commands against batches of servers}
|
12
|
+
s.description = %q{`knife batch` is a wonderful little plugin for executing commands a la `knife ssh`, but doing it in groups of `n` with a sleep between execution iterations.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "knife-batch"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
s.add_runtime_dependency "chef"
|
24
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ian Meyer <ianmmeyer@gmail.com>
|
3
|
+
# Plugin name:: batch
|
4
|
+
#
|
5
|
+
# Copyright 2011, Ian Meyer
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
class Batch < Chef::Knife
|
21
|
+
banner "knife batch [QUERY] [CMD]"
|
22
|
+
|
23
|
+
deps do
|
24
|
+
require 'net/ssh'
|
25
|
+
require 'net/ssh/multi'
|
26
|
+
require 'readline'
|
27
|
+
require 'chef/search/query'
|
28
|
+
require 'chef/mixin/command'
|
29
|
+
end
|
30
|
+
|
31
|
+
option :wait,
|
32
|
+
:short => "-W SECONDS",
|
33
|
+
:long => "--wait SECONDS",
|
34
|
+
:description => "The number of seconds between batches.",
|
35
|
+
:default => 0.5
|
36
|
+
|
37
|
+
option :batch_size,
|
38
|
+
:short => "-B NODES",
|
39
|
+
:long => "--batch-size NODES",
|
40
|
+
:description => "The number of nodes to run per batch.",
|
41
|
+
:default => 5
|
42
|
+
|
43
|
+
option :stop_on_failure,
|
44
|
+
:short => "-S",
|
45
|
+
:long => "--stop-on-failure",
|
46
|
+
:description => "Stop on first failure of remote command",
|
47
|
+
:default => false
|
48
|
+
|
49
|
+
option :manual,
|
50
|
+
:short => "-m",
|
51
|
+
:long => "--manual-list",
|
52
|
+
:boolean => true,
|
53
|
+
:description => "QUERY is a space separated list of servers",
|
54
|
+
:default => false
|
55
|
+
|
56
|
+
option :ssh_user,
|
57
|
+
:short => "-x USERNAME",
|
58
|
+
:long => "--ssh-user USERNAME",
|
59
|
+
:description => "The ssh username"
|
60
|
+
|
61
|
+
option :ssh_password,
|
62
|
+
:short => "-P PASSWORD",
|
63
|
+
:long => "--ssh-password PASSWORD",
|
64
|
+
:description => "The ssh password"
|
65
|
+
|
66
|
+
option :ssh_port,
|
67
|
+
:short => "-p PORT",
|
68
|
+
:long => "--ssh-port PORT",
|
69
|
+
:description => "The ssh port",
|
70
|
+
:default => "22",
|
71
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
|
72
|
+
|
73
|
+
option :identity_file,
|
74
|
+
:short => "-i IDENTITY_FILE",
|
75
|
+
:long => "--identity-file IDENTITY_FILE",
|
76
|
+
:description => "The SSH identity file used for authentication"
|
77
|
+
|
78
|
+
option :no_host_key_verify,
|
79
|
+
:long => "--no-host-key-verify",
|
80
|
+
:description => "Disable host key verification",
|
81
|
+
:boolean => true,
|
82
|
+
:default => false
|
83
|
+
|
84
|
+
option :attribute,
|
85
|
+
:short => "-a ATTR",
|
86
|
+
:long => "--attribute ATTR",
|
87
|
+
:description => "The attribute to use for opening the connection - default is fqdn",
|
88
|
+
:default => "fqdn"
|
89
|
+
|
90
|
+
def session(nodes)
|
91
|
+
ssh_error_handler = Proc.new do |server|
|
92
|
+
if config[:manual]
|
93
|
+
node_name = server.host
|
94
|
+
else
|
95
|
+
nodes.each do |n|
|
96
|
+
node_name = n if format_for_display(n)[config[:attribute]] == server.host
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}"
|
100
|
+
$!.backtrace.each { |l| Chef::Log.debug(l) }
|
101
|
+
end
|
102
|
+
|
103
|
+
@ssh_session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler)
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_nodes
|
107
|
+
list = case config[:manual]
|
108
|
+
when true
|
109
|
+
@name_args[0].split(" ")
|
110
|
+
when false
|
111
|
+
r = Array.new
|
112
|
+
q = Chef::Search::Query.new
|
113
|
+
@action_nodes = q.search(:node, @name_args[0])[0]
|
114
|
+
@action_nodes.each do |item|
|
115
|
+
i = format_for_display(item)[config[:attribute]]
|
116
|
+
r.push(i) unless i.nil?
|
117
|
+
end
|
118
|
+
r
|
119
|
+
end
|
120
|
+
(ui.fatal("No nodes returned from search!"); exit 10) if list.length == 0
|
121
|
+
|
122
|
+
parent_ary = Array.new
|
123
|
+
child_ary = Array.new
|
124
|
+
iter = 0
|
125
|
+
list.each do |item|
|
126
|
+
if (iter +=1) <= config[:batch_size].to_i
|
127
|
+
child_ary << item
|
128
|
+
else
|
129
|
+
parent_ary << child_ary
|
130
|
+
child_ary = Array.new
|
131
|
+
iter = 0
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
parent_ary
|
136
|
+
end
|
137
|
+
|
138
|
+
def print_data(host, data)
|
139
|
+
if data =~ /\n/
|
140
|
+
data.split(/\n/).each { |d| print_data(host, d) }
|
141
|
+
else
|
142
|
+
padding = @longest - host.length
|
143
|
+
print ui.color(host, :cyan)
|
144
|
+
padding.downto(0) { print " " }
|
145
|
+
puts data
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def session_from_list(nodes)
|
150
|
+
nodes.each do |item|
|
151
|
+
Chef::Log.debug("Adding #{item}")
|
152
|
+
|
153
|
+
hostspec = config[:ssh_user] ? "#{config[:ssh_user]}@#{item}" : item
|
154
|
+
session_opts = {}
|
155
|
+
session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file]
|
156
|
+
session_opts[:password] = config[:ssh_password] if config[:ssh_password]
|
157
|
+
session_opts[:port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
|
158
|
+
session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
|
159
|
+
|
160
|
+
if config[:no_host_key_verify]
|
161
|
+
session_opts[:paranoid] = false
|
162
|
+
session_opts[:user_known_hosts_file] = "/dev/null"
|
163
|
+
end
|
164
|
+
session(nodes).use(hostspec, session_opts)
|
165
|
+
|
166
|
+
@longest = item.length if item.length > @longest
|
167
|
+
end
|
168
|
+
|
169
|
+
session(nodes)
|
170
|
+
end
|
171
|
+
|
172
|
+
def ssh_command(command, subsession=nil, nodes)
|
173
|
+
subsession ||= session(nodes)
|
174
|
+
subsession.open_channel do |channel|
|
175
|
+
host = channel[:host]
|
176
|
+
channel.request_pty
|
177
|
+
channel.exec command do |ch, success|
|
178
|
+
exit_code = nil
|
179
|
+
raise ArgumentError, "Cannot execute #{command}" unless success
|
180
|
+
channel.on_data do |ch, data|
|
181
|
+
print_data(host, data)
|
182
|
+
end
|
183
|
+
|
184
|
+
if config[:stop_on_failure]
|
185
|
+
channel.on_request("exit-status") do |ch, data|
|
186
|
+
exit_code = data.read_long
|
187
|
+
if not exit_code.nil?
|
188
|
+
exit 1 if exit_code.to_i > 0
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
@ssh_session.loop
|
195
|
+
@ssh_session = nil
|
196
|
+
end
|
197
|
+
|
198
|
+
def run
|
199
|
+
extend Chef::Mixin::Command
|
200
|
+
|
201
|
+
@longest = 0
|
202
|
+
all_nodes = get_nodes
|
203
|
+
all_nodes.each do |nodes|
|
204
|
+
session_from_list(nodes)
|
205
|
+
|
206
|
+
ssh_command(@name_args[1..-1].join(" "), nodes)
|
207
|
+
puts "*" * 80
|
208
|
+
puts "Taking a nap for #{config[:wait]} seconds..."
|
209
|
+
puts "*" * 80
|
210
|
+
sleep config[:wait].to_f
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
#
|
2
|
+
# Author:: Ian Meyer <ianmmeyer@gmail.com>
|
3
|
+
# Plugin name:: batch
|
4
|
+
#
|
5
|
+
# Copyright 2011, Ian Meyer
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
#
|
19
|
+
|
20
|
+
class Batch < Chef::Knife
|
21
|
+
banner "knife batch [QUERY] [CMD]"
|
22
|
+
|
23
|
+
deps do
|
24
|
+
require 'net/ssh'
|
25
|
+
require 'net/ssh/multi'
|
26
|
+
require 'readline'
|
27
|
+
require 'chef/search/query'
|
28
|
+
require 'chef/mixin/command'
|
29
|
+
end
|
30
|
+
|
31
|
+
option :wait,
|
32
|
+
:short => "-W SECONDS",
|
33
|
+
:long => "--wait SECONDS",
|
34
|
+
:description => "The number of seconds between batches.",
|
35
|
+
:default => 0.5
|
36
|
+
|
37
|
+
option :batch_size,
|
38
|
+
:short => "-B NODES",
|
39
|
+
:long => "--batch-size NODES",
|
40
|
+
:description => "The number of nodes to run per batch.",
|
41
|
+
:default => 5
|
42
|
+
|
43
|
+
option :stop_on_failure,
|
44
|
+
:short => "-S",
|
45
|
+
:long => "--stop-on-failure",
|
46
|
+
:description => "Stop on first failure of remote command",
|
47
|
+
:default => false
|
48
|
+
|
49
|
+
option :manual,
|
50
|
+
:short => "-m",
|
51
|
+
:long => "--manual-list",
|
52
|
+
:boolean => true,
|
53
|
+
:description => "QUERY is a space separated list of servers",
|
54
|
+
:default => false
|
55
|
+
|
56
|
+
option :ssh_user,
|
57
|
+
:short => "-x USERNAME",
|
58
|
+
:long => "--ssh-user USERNAME",
|
59
|
+
:description => "The ssh username"
|
60
|
+
|
61
|
+
option :ssh_password,
|
62
|
+
:short => "-P PASSWORD",
|
63
|
+
:long => "--ssh-password PASSWORD",
|
64
|
+
:description => "The ssh password"
|
65
|
+
|
66
|
+
option :ssh_port,
|
67
|
+
:short => "-p PORT",
|
68
|
+
:long => "--ssh-port PORT",
|
69
|
+
:description => "The ssh port",
|
70
|
+
:default => "22",
|
71
|
+
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key }
|
72
|
+
|
73
|
+
option :identity_file,
|
74
|
+
:short => "-i IDENTITY_FILE",
|
75
|
+
:long => "--identity-file IDENTITY_FILE",
|
76
|
+
:description => "The SSH identity file used for authentication"
|
77
|
+
|
78
|
+
option :no_host_key_verify,
|
79
|
+
:long => "--no-host-key-verify",
|
80
|
+
:description => "Disable host key verification",
|
81
|
+
:boolean => true,
|
82
|
+
:default => false
|
83
|
+
|
84
|
+
option :attribute,
|
85
|
+
:short => "-a ATTR",
|
86
|
+
:long => "--attribute ATTR",
|
87
|
+
:description => "The attribute to use for opening the connection - default is fqdn",
|
88
|
+
:default => "fqdn"
|
89
|
+
|
90
|
+
def session(nodes)
|
91
|
+
ssh_error_handler = Proc.new do |server|
|
92
|
+
if config[:manual]
|
93
|
+
node_name = server.host
|
94
|
+
else
|
95
|
+
nodes.each do |n|
|
96
|
+
node_name = n if format_for_display(n)[config[:attribute]] == server.host
|
97
|
+
end
|
98
|
+
end
|
99
|
+
ui.warn "Failed to connect to #{node_name} -- #{$!.class.name}: #{$!.message}"
|
100
|
+
$!.backtrace.each { |l| Chef::Log.debug(l) }
|
101
|
+
end
|
102
|
+
|
103
|
+
@ssh_session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency], :on_error => ssh_error_handler)
|
104
|
+
end
|
105
|
+
|
106
|
+
def get_nodes
|
107
|
+
list = case config[:manual]
|
108
|
+
when true
|
109
|
+
@name_args[0].split(" ")
|
110
|
+
when false
|
111
|
+
r = Array.new
|
112
|
+
q = Chef::Search::Query.new
|
113
|
+
@action_nodes = q.search(:node, @name_args[0])[0]
|
114
|
+
@action_nodes.each do |item|
|
115
|
+
i = format_for_display(item)[config[:attribute]]
|
116
|
+
r.push(i) unless i.nil?
|
117
|
+
end
|
118
|
+
r
|
119
|
+
end
|
120
|
+
(ui.fatal("No nodes returned from search!"); exit 10) if list.length == 0
|
121
|
+
|
122
|
+
parent_ary = Array.new
|
123
|
+
child_ary = Array.new
|
124
|
+
iter = 0
|
125
|
+
list.each do |item|
|
126
|
+
if (iter +=1) <= config[:batch_size].to_i
|
127
|
+
child_ary << item
|
128
|
+
else
|
129
|
+
parent_ary << child_ary
|
130
|
+
child_ary = Array.new
|
131
|
+
iter = 0
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
parent_ary
|
136
|
+
end
|
137
|
+
|
138
|
+
def print_data(host, data)
|
139
|
+
if data =~ /\n/
|
140
|
+
data.split(/\n/).each { |d| print_data(host, d) }
|
141
|
+
else
|
142
|
+
padding = @longest - host.length
|
143
|
+
print ui.color(host, :cyan)
|
144
|
+
padding.downto(0) { print " " }
|
145
|
+
puts data
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def session_from_list(nodes)
|
150
|
+
nodes.each do |item|
|
151
|
+
Chef::Log.debug("Adding #{item}")
|
152
|
+
|
153
|
+
hostspec = config[:ssh_user] ? "#{config[:ssh_user]}@#{item}" : item
|
154
|
+
session_opts = {}
|
155
|
+
session_opts[:keys] = File.expand_path(config[:identity_file]) if config[:identity_file]
|
156
|
+
session_opts[:password] = config[:ssh_password] if config[:ssh_password]
|
157
|
+
session_opts[:port] = Chef::Config[:knife][:ssh_port] || config[:ssh_port]
|
158
|
+
session_opts[:logger] = Chef::Log.logger if Chef::Log.level == :debug
|
159
|
+
|
160
|
+
if config[:no_host_key_verify]
|
161
|
+
session_opts[:paranoid] = false
|
162
|
+
session_opts[:user_known_hosts_file] = "/dev/null"
|
163
|
+
end
|
164
|
+
session(nodes).use(hostspec, session_opts)
|
165
|
+
|
166
|
+
@longest = item.length if item.length > @longest
|
167
|
+
end
|
168
|
+
|
169
|
+
session(nodes)
|
170
|
+
end
|
171
|
+
|
172
|
+
def ssh_command(command, subsession=nil, nodes)
|
173
|
+
subsession ||= session(nodes)
|
174
|
+
subsession.open_channel do |channel|
|
175
|
+
host = channel[:host]
|
176
|
+
channel.request_pty
|
177
|
+
channel.exec command do |ch, success|
|
178
|
+
exit_code = nil
|
179
|
+
raise ArgumentError, "Cannot execute #{command}" unless success
|
180
|
+
channel.on_data do |ch, data|
|
181
|
+
print_data(host, data)
|
182
|
+
end
|
183
|
+
|
184
|
+
if config[:stop_on_failure]
|
185
|
+
channel.on_request("exit-status") do |ch, data|
|
186
|
+
exit_code = data.read_long
|
187
|
+
if not exit_code.nil?
|
188
|
+
exit 1 if exit_code.to_i > 0
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
@ssh_session.loop
|
195
|
+
@ssh_session = nil
|
196
|
+
end
|
197
|
+
|
198
|
+
def run
|
199
|
+
extend Chef::Mixin::Command
|
200
|
+
|
201
|
+
@longest = 0
|
202
|
+
all_nodes = get_nodes
|
203
|
+
all_nodes.each do |nodes|
|
204
|
+
session_from_list(nodes)
|
205
|
+
|
206
|
+
ssh_command(@name_args[1..-1].join(" "), nodes)
|
207
|
+
puts "*" * 80
|
208
|
+
puts "Taking a nap for #{config[:wait]} seconds..."
|
209
|
+
puts "*" * 80
|
210
|
+
sleep config[:wait].to_f
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: knife-batch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Ian Meyer
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-10-15 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chef
|
16
|
+
requirement: &70191053580660 !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: *70191053580660
|
25
|
+
description: ! '`knife batch` is a wonderful little plugin for executing commands
|
26
|
+
a la `knife ssh`, but doing it in groups of `n` with a sleep between execution iterations.'
|
27
|
+
email:
|
28
|
+
- ianmmeyer@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- .gitignore
|
34
|
+
- Gemfile
|
35
|
+
- README.markdown
|
36
|
+
- Rakefile
|
37
|
+
- knife-batch.gemspec
|
38
|
+
- lib/chef/knife/batch.rb
|
39
|
+
- lib/chef/knife/knife-batch.rb
|
40
|
+
- lib/knife-batch/version.rb
|
41
|
+
homepage: http://github.com/imeyer/knife-batch
|
42
|
+
licenses: []
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
none: false
|
49
|
+
requirements:
|
50
|
+
- - ! '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubyforge_project: knife-batch
|
61
|
+
rubygems_version: 1.8.5
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Knife plugin to run ssh commands against batches of servers
|
65
|
+
test_files: []
|