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 ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in knife-batch.gemspec
4
+ gemspec
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"
@@ -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
@@ -0,0 +1,5 @@
1
+ module Knife
2
+ module Batch
3
+ VERSION = "1.0.0"
4
+ end
5
+ 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: []