cast-ssh 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.rspec +2 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE.md +7 -0
- data/README.md +78 -0
- data/bin/cast +58 -0
- data/cast.gemspec +25 -0
- data/lib/cast.rb +118 -0
- data/spec/lib/cast/cast_spec.rb +38 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/test.yml +10 -0
- metadata +91 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZmRlOTFjNjZiMjViMDFjODExYWI2NTIzOTUyZDYzZmNhZTliNWZmMQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MDJhMmNlYjU4MDYwOTU3OGVmYjU2NTBmZWIyOWJkNzhkMTdmZTQ3MA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDVjY2MyM2Q5NDQ3NzdmZDZjODNmMTk3YjJjNWMwZDNhN2QxMmY1NzA3YmY5
|
10
|
+
YTk1ZmRmZGFiZDNlN2JlOTczYjIwNmE3MmE4NTA4ZjE2ZjE5Y2NjMDEyNDY4
|
11
|
+
MTc4OGQzZjJiOGJlYjI3MWNjOGIyNjQyMzU5NWVjNTA3MWQyNDc=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
Yjg2OGUxZTMzOGFhMmI5Yzc5ZjY1OTJkODhlMGJiMDViMTA1YmEwMGY0ZDQ4
|
14
|
+
N2QxNmI0NDUwZTUwOWJhZDUyMWNkODk0NjgzYjFlNTE3ODY0YmEzM2RlOTY3
|
15
|
+
ZWY1MDgxYTE0OWE4ZWM2YWMwMDNlYTYwNTVjNzYwNDE3OTcxOGU=
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.md
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (C) 2013 Peter Bakkum
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# Cast
|
2
|
+
|
3
|
+
This is a Ruby gem that executes remote commands via ssh on groups of servers defined in a YAML file.
|
4
|
+
|
5
|
+
### Setup
|
6
|
+
|
7
|
+
Grab the gem:
|
8
|
+
|
9
|
+
gem install cast-ssh
|
10
|
+
|
11
|
+
Create your group file at ~/.cast.yml, like this:
|
12
|
+
|
13
|
+
group1:
|
14
|
+
- host1
|
15
|
+
- host2
|
16
|
+
- host3
|
17
|
+
|
18
|
+
group2:
|
19
|
+
- host1
|
20
|
+
- host4
|
21
|
+
- host5
|
22
|
+
|
23
|
+
Run commands in your shell:
|
24
|
+
|
25
|
+
cast group1 echo test
|
26
|
+
cast group1,group2 sudo whoami
|
27
|
+
cast -s group1,host4 df -h
|
28
|
+
|
29
|
+
The output from the second command will look something like this:
|
30
|
+
|
31
|
+
[cast] loading groups from ~/.cast.yml
|
32
|
+
[cast] running ssh host1 'sudo whoami'
|
33
|
+
[cast] running ssh host2 'sudo whoami'
|
34
|
+
[cast] running ssh host3 'sudo whoami'
|
35
|
+
[cast] running ssh host4 'sudo whoami'
|
36
|
+
[cast] running ssh host5 'sudo whoami'
|
37
|
+
[host3] root
|
38
|
+
[host1] root
|
39
|
+
[host2] root
|
40
|
+
[host5] root
|
41
|
+
[host4] root
|
42
|
+
|
43
|
+
Note that the commands are run in parallel, so the output may be out of order.
|
44
|
+
|
45
|
+
### Options
|
46
|
+
|
47
|
+
--serial, -s: Execute commands serially, rather than in parallel over the group
|
48
|
+
--delay, -d <f>: Delay in seconds between execution of serial commands (switches to serial mode if defined)
|
49
|
+
--groupfile, -g <s>: YAML file of server groups (default: ~/.cast.yml)
|
50
|
+
--list, -l: Print out contents of groupfile without executing command
|
51
|
+
--clusters, -c: Print out only groupnames without executing command
|
52
|
+
--version, -v: Print version and exit
|
53
|
+
--help, -h: Show this message
|
54
|
+
|
55
|
+
### API
|
56
|
+
|
57
|
+
You can also access the same functionality from within Ruby. The following methods are available:
|
58
|
+
|
59
|
+
* __Cast::remote__ host, cmd
|
60
|
+
|
61
|
+
Run a remote command on the given host, sending output to stdout.
|
62
|
+
|
63
|
+
* __Cast::local__ cmd, prefix = nil -> int
|
64
|
+
|
65
|
+
Run a command locally, printing stdout and stderr from the command. Returns the process' return value.
|
66
|
+
|
67
|
+
* __Cast::log__ msg, source = 'cast', stream = $stdout
|
68
|
+
|
69
|
+
Log a message through the Cast mutex using the given prefix and stream. Output will look like "[prefix] msg". The prefix box will be left out if the argument is nil.
|
70
|
+
|
71
|
+
* __Cast::load_groups__ file -> Hash
|
72
|
+
|
73
|
+
Load Cast groups from the given file, returning them as a hash.
|
74
|
+
|
75
|
+
* __Cast::expand_groups__ cmdgroups, groups = @@groups -> Array
|
76
|
+
|
77
|
+
Takes an array of groups and hosts, and expands the groups into their constituents given the input group hash. If no hash is given, use the value loaded most recently in load_groups. Returns an array of hostnames.
|
78
|
+
|
data/bin/cast
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'cast'
|
4
|
+
require 'trollop'
|
5
|
+
|
6
|
+
p = Trollop::Parser.new do
|
7
|
+
version "cast #{Cast::VERSION} (c) Peter Bakkum"
|
8
|
+
banner <<-EOS
|
9
|
+
Cast executes remote commands via ssh on servers or groups of servers, as defined by ~/.cast.yml
|
10
|
+
|
11
|
+
Usage:
|
12
|
+
cast [options] <servers/groups> <command>
|
13
|
+
|
14
|
+
Examples:
|
15
|
+
cast hadoop_cluster df
|
16
|
+
cast -s hadoop_cluster,machine1,machine2 whoami
|
17
|
+
|
18
|
+
Your group file (~/.cast.yml by default) should be proper YAML and look like this:
|
19
|
+
|
20
|
+
groupname:
|
21
|
+
- host1
|
22
|
+
- host2
|
23
|
+
- host3
|
24
|
+
|
25
|
+
Command Line Options:
|
26
|
+
|
27
|
+
EOS
|
28
|
+
|
29
|
+
opt :serial, 'Execute commands serially, rather than in parallel over the group', :short => 's'
|
30
|
+
opt :delay, 'Delay in seconds between execution of serial commands (switches to serial mode if defined)', :type => :float
|
31
|
+
opt :groupfile, 'YAML file of server groups', :default => Cast::DEFAULTGROUPS, :short => 'g'
|
32
|
+
opt :list, 'Print out contents of groupfile without executing command', :short => 'l'
|
33
|
+
opt :clusters, 'Print out only groupnames without executing command', :short => 'c'
|
34
|
+
end
|
35
|
+
|
36
|
+
opt = Trollop::with_standard_exception_handling p do
|
37
|
+
opt = p.parse ARGV
|
38
|
+
raise Trollop::HelpNeeded if ARGV.size < 2 && !opt[:list] && !opt[:clusters]
|
39
|
+
opt
|
40
|
+
end
|
41
|
+
|
42
|
+
groups = Cast::load_groups opt[:groupfile]
|
43
|
+
|
44
|
+
if opt[:list]
|
45
|
+
puts YAML::dump(groups)
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
if opt[:clusters]
|
50
|
+
puts groups.keys.join("\n")
|
51
|
+
exit
|
52
|
+
end
|
53
|
+
|
54
|
+
cmd = ARGV[1..-1].join(' ')
|
55
|
+
hosts = Cast::expand_groups ARGV[0].split(','), groups
|
56
|
+
|
57
|
+
Cast::run hosts, cmd, opt[:serial], opt[:delay]
|
58
|
+
|
data/cast.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require File.expand_path("../lib/cast.rb", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.add_development_dependency 'mocha'
|
6
|
+
gem.add_development_dependency 'autotest-standalone'
|
7
|
+
|
8
|
+
gem.authors = ["Peter Bakkum"]
|
9
|
+
gem.bindir = 'bin'
|
10
|
+
gem.description = %q{Execute remote commands via ssh on groups of machines}
|
11
|
+
gem.email = ['pbb7c@virginia.edu']
|
12
|
+
gem.executables = ['cast']
|
13
|
+
gem.extra_rdoc_files = ['LICENSE.md', 'README.md']
|
14
|
+
gem.files = Dir['LICENSE.md', 'README.md', 'cast.gemspec', 'Gemfile', '.rspec', '.travis.yml', 'lib/**/*', 'bin/*', 'spec/**/*']
|
15
|
+
gem.homepage = 'http://github.com/bakks/cast'
|
16
|
+
gem.name = 'cast-ssh'
|
17
|
+
gem.rdoc_options = ["--charset=UTF-8"]
|
18
|
+
gem.require_paths = ['lib']
|
19
|
+
gem.required_rubygems_version = Gem::Requirement.new(">= 1.3.6")
|
20
|
+
gem.summary = %q{Execute remote commands via ssh on groups of machines}
|
21
|
+
gem.test_files = Dir['spec/**/*']
|
22
|
+
gem.version = Cast::VERSION
|
23
|
+
gem.license = 'MIT'
|
24
|
+
end
|
25
|
+
|
data/lib/cast.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'open3'
|
3
|
+
require 'yaml'
|
4
|
+
require 'peach'
|
5
|
+
|
6
|
+
STDOUT.sync = true
|
7
|
+
STDERR.sync = true
|
8
|
+
|
9
|
+
module Cast
|
10
|
+
VERSION = '0.1.1'
|
11
|
+
DEFAULTGROUPS = '~/.cast.yml'
|
12
|
+
|
13
|
+
@@mux = Mutex.new
|
14
|
+
@@pids = []
|
15
|
+
@@groups = nil
|
16
|
+
|
17
|
+
def self.log msg, source = 'cast', stream = $stdout
|
18
|
+
@@mux.synchronize do
|
19
|
+
prefix = "[#{source}] " unless source == nil
|
20
|
+
stream.puts "#{prefix}#{msg}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.load_groups groupfile
|
25
|
+
file = File.expand_path groupfile
|
26
|
+
exists = File.exists? file
|
27
|
+
@@groups ||= {}
|
28
|
+
|
29
|
+
if !exists
|
30
|
+
raise "could not find #{groupfile}" if groupfile != DEFAULTGROUPS
|
31
|
+
log "no group file found at #{groupfile}"
|
32
|
+
else
|
33
|
+
log "loading groups from #{groupfile}"
|
34
|
+
@@groups = YAML.load_file file
|
35
|
+
end
|
36
|
+
|
37
|
+
return @@groups
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.expand_groups cmdgroups, groups = @@groups
|
41
|
+
hosts = []
|
42
|
+
cmdgroups.each do |group|
|
43
|
+
if groups.include? group
|
44
|
+
hosts += groups[group]
|
45
|
+
else
|
46
|
+
hosts << group
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
return hosts.uniq
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.run hosts, cmd, serial = false, delay = nil
|
54
|
+
if serial or delay
|
55
|
+
hosts.each_with_index do |host, i|
|
56
|
+
remote host, cmd
|
57
|
+
if delay and i < hosts.size - 1
|
58
|
+
log "delay for #{delay} seconds"
|
59
|
+
sleep delay
|
60
|
+
end
|
61
|
+
end
|
62
|
+
else
|
63
|
+
hosts.peach { |host| remote host, cmd }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.remote host, cmd
|
68
|
+
fullcmd = "ssh #{host} '#{cmd}'"
|
69
|
+
log "running #{fullcmd}"
|
70
|
+
local fullcmd, host
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.local cmd, prefix = nil
|
74
|
+
r = nil
|
75
|
+
|
76
|
+
Open3.popen3 cmd do |stdin, stdout, stderr, wait_thr|
|
77
|
+
@@pids << wait_thr[:pid]
|
78
|
+
|
79
|
+
stdin.close
|
80
|
+
|
81
|
+
while true
|
82
|
+
streams = []
|
83
|
+
streams << stdout if stdout
|
84
|
+
streams << stderr if stderr
|
85
|
+
break unless streams.size > 0
|
86
|
+
|
87
|
+
ios = IO.select streams
|
88
|
+
|
89
|
+
ios.first.each do |stream|
|
90
|
+
if stream == stdout
|
91
|
+
line = stream.gets
|
92
|
+
if line == nil
|
93
|
+
stdout = nil
|
94
|
+
next
|
95
|
+
end
|
96
|
+
log line, prefix
|
97
|
+
|
98
|
+
elsif stream == stderr
|
99
|
+
line = stream.gets
|
100
|
+
if line == nil
|
101
|
+
stderr = nil
|
102
|
+
next
|
103
|
+
end
|
104
|
+
log line, prefix, $stderr
|
105
|
+
|
106
|
+
else raise "unrecognized stream #{stream}"
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
r = wait_thr.value
|
113
|
+
end
|
114
|
+
|
115
|
+
return r
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Cast do
|
4
|
+
it 'should load groups' do
|
5
|
+
groups = Cast::load_groups 'spec/test.yml'
|
6
|
+
groups.size.should == 2
|
7
|
+
groups['group1'].size.should == 3
|
8
|
+
groups['group1'][0].should == 'host1'
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'should expand groups' do
|
12
|
+
groups = Cast::load_groups 'spec/test.yml'
|
13
|
+
hosts = Cast::expand_groups ['group1'], groups
|
14
|
+
hosts.size.should == 3
|
15
|
+
hosts[0].should == 'host1'
|
16
|
+
hosts[1].should == 'host2'
|
17
|
+
hosts[2].should == 'host3'
|
18
|
+
|
19
|
+
hosts = Cast::expand_groups ['group1', 'group2']
|
20
|
+
hosts.size.should == 5
|
21
|
+
hosts[0].should == 'host1'
|
22
|
+
hosts[1].should == 'host2'
|
23
|
+
hosts[2].should == 'host3'
|
24
|
+
hosts[3].should == 'host4'
|
25
|
+
hosts[4].should == 'host5'
|
26
|
+
|
27
|
+
hosts = Cast::expand_groups ['host10', 'group1', 'host11', 'group2']
|
28
|
+
hosts.size.should == 7
|
29
|
+
hosts[0].should == 'host10'
|
30
|
+
hosts[1].should == 'host1'
|
31
|
+
hosts[2].should == 'host2'
|
32
|
+
hosts[3].should == 'host3'
|
33
|
+
hosts[4].should == 'host11'
|
34
|
+
hosts[5].should == 'host4'
|
35
|
+
hosts[6].should == 'host5'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rspec'
|
5
|
+
require 'mocha/api'
|
6
|
+
require 'cast'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.mock_with :mocha
|
10
|
+
config.fail_fast = true
|
11
|
+
end
|
12
|
+
|
data/spec/test.yml
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cast-ssh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peter Bakkum
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-03-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: mocha
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: autotest-standalone
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
description: Execute remote commands via ssh on groups of machines
|
42
|
+
email:
|
43
|
+
- pbb7c@virginia.edu
|
44
|
+
executables:
|
45
|
+
- cast
|
46
|
+
extensions: []
|
47
|
+
extra_rdoc_files:
|
48
|
+
- LICENSE.md
|
49
|
+
- README.md
|
50
|
+
files:
|
51
|
+
- LICENSE.md
|
52
|
+
- README.md
|
53
|
+
- cast.gemspec
|
54
|
+
- Gemfile
|
55
|
+
- .rspec
|
56
|
+
- .travis.yml
|
57
|
+
- lib/cast.rb
|
58
|
+
- bin/cast
|
59
|
+
- spec/lib/cast/cast_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
- spec/test.yml
|
62
|
+
homepage: http://github.com/bakks/cast
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
metadata: {}
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --charset=UTF-8
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ! '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ! '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: 1.3.6
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 2.0.3
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Execute remote commands via ssh on groups of machines
|
87
|
+
test_files:
|
88
|
+
- spec/lib/cast/cast_spec.rb
|
89
|
+
- spec/spec_helper.rb
|
90
|
+
- spec/test.yml
|
91
|
+
has_rdoc:
|