anywhere 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +43 -0
- data/Rakefile +1 -0
- data/anywhere.gemspec +27 -0
- data/bin/anywhere +41 -0
- data/lib/anywhere.rb +10 -0
- data/lib/anywhere/base.rb +124 -0
- data/lib/anywhere/execution_error.rb +9 -0
- data/lib/anywhere/local.rb +39 -0
- data/lib/anywhere/logger.rb +51 -0
- data/lib/anywhere/result.rb +60 -0
- data/lib/anywhere/ssh.rb +57 -0
- data/lib/anywhere/system_package.rb +21 -0
- data/lib/anywhere/version.rb +3 -0
- data/spec/fixtures/arch.tgz +0 -0
- data/spec/fixtures/packages.txt +387 -0
- data/spec/integration/local_integration_spec.rb +39 -0
- data/spec/integration/ssh_integration_spec.rb +126 -0
- data/spec/lib/anywhere/base_spec.rb +25 -0
- data/spec/lib/anywhere/local_spec.rb +43 -0
- data/spec/lib/anywhere/logger_spec.rb +16 -0
- data/spec/lib/anywhere/ssh_spec.rb +11 -0
- data/spec/lib/anywhere/system_package_spec.rb +29 -0
- data/spec/lib/anywhere_spec.rb +8 -0
- data/spec/spec_helper.rb +11 -0
- metadata +181 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Tobias Schwab
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# Anywhere
|
2
|
+
|
3
|
+
Simple wrapper for Net/SSH.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'anywhere'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install anywhere
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
### From ruby
|
22
|
+
pry> require "anywhere/ssh"
|
23
|
+
pry> ssh = Anywhere::SSH.new(host = "test.host", user = "root", port: 1234)
|
24
|
+
pry> ssh.execute("uptime")
|
25
|
+
=> <run_time=0.659416, cmd=<uptime>, stdout=<1 lines, 61 chars>, stderr=<empty>, exit_status=0>
|
26
|
+
|
27
|
+
### From command line
|
28
|
+
|
29
|
+
$ anywhere root@host1 root@host2
|
30
|
+
|
31
|
+
pry> _"uptime"
|
32
|
+
2013-06-09T22:35:51.303413Z [host1] DEBUG 00:35:51 up 179 days, 7:48, 0 users, load average: 0.00, 0.00, 0.00
|
33
|
+
2013-06-09T22:35:51.307168Z [host2] DEBUG 22:35:51 up 299 days, 10:49, 0 users, load average: 0.08, 0.05, 0.06
|
34
|
+
=> [<run_time=0.06875, cmd=<uptime>, stdout=<1 lines, 72 chars>, stderr=<empty>, exit_status=0>,
|
35
|
+
<run_time=0.067885, cmd=<uptime>, stdout=<1 lines, 72 chars>, stderr=<empty>, exit_status=0>]
|
36
|
+
|
37
|
+
## Contributing
|
38
|
+
|
39
|
+
1. Fork it
|
40
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
41
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
42
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
43
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/anywhere.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'anywhere/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "anywhere"
|
8
|
+
spec.version = Anywhere::VERSION
|
9
|
+
spec.authors = ["Tobias Schwab"]
|
10
|
+
spec.email = ["tobias.schwab@dynport.de"]
|
11
|
+
spec.description = %q{Simple wrapper for Net/SSH}
|
12
|
+
spec.summary = %q{Simple wrapper for Net/SSH}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_dependency "net-ssh"
|
22
|
+
spec.add_dependency "colorize"
|
23
|
+
spec.add_dependency "pry"
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
25
|
+
spec.add_development_dependency "rake"
|
26
|
+
spec.add_development_dependency "rspec"
|
27
|
+
end
|
data/bin/anywhere
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.push(File.expand_path("../../lib", __FILE__))
|
3
|
+
require "anywhere"
|
4
|
+
require "anywhere/ssh"
|
5
|
+
RUNNER = Anywhere::SSH.new(host = "test.host", user = "root")
|
6
|
+
RUNNER.logger.prefix = "[" + RUNNER.host + "]"
|
7
|
+
|
8
|
+
hosts = []
|
9
|
+
names = []
|
10
|
+
RUNNERS = ARGV.map do |host|
|
11
|
+
name, user = host.split("@").reverse
|
12
|
+
user ||= "root"
|
13
|
+
names << name
|
14
|
+
Anywhere::SSH.new(name, user)
|
15
|
+
end
|
16
|
+
|
17
|
+
max_name = names.sort_by(&:length).last.length
|
18
|
+
|
19
|
+
RUNNERS.each do |runner|
|
20
|
+
runner.logger.prefix = "[%0#{max_name}s]" % [runner.host]
|
21
|
+
end
|
22
|
+
|
23
|
+
def _(*args)
|
24
|
+
RUNNERS.map do |runner|
|
25
|
+
Thread.new do
|
26
|
+
begin
|
27
|
+
runner.execute(args.join(" "))
|
28
|
+
rescue Anywhere::ExecutionError => err
|
29
|
+
err.result
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end.map(&:join).map(&:value)
|
33
|
+
end
|
34
|
+
|
35
|
+
begin
|
36
|
+
puts "hosts: #{RUNNERS.map(&:host).join(",")}"
|
37
|
+
puts %(usage: _"ls -la")
|
38
|
+
require "pry"
|
39
|
+
pry.binding
|
40
|
+
rescue SystemExit, NoMethodError
|
41
|
+
end
|
data/lib/anywhere.rb
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require "benchmark"
|
2
|
+
require "anywhere/logger"
|
3
|
+
|
4
|
+
module Anywhere
|
5
|
+
class Base
|
6
|
+
attr_writer :logger
|
7
|
+
|
8
|
+
def logger
|
9
|
+
@logger ||= Anywhere::Logger.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def root?
|
13
|
+
whoami == "root"
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute(cmd, stdin = nil)
|
17
|
+
do_execute(cmd, stdin) do |stream, data|
|
18
|
+
data.split("\n").each do |line|
|
19
|
+
if stream == :stderr
|
20
|
+
logger.error line
|
21
|
+
else
|
22
|
+
logger.debug line
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def whoami
|
29
|
+
@whoami ||= execute("whoami").stdout.strip
|
30
|
+
end
|
31
|
+
|
32
|
+
def do_execute(*args)
|
33
|
+
raise "implement me in subclass"
|
34
|
+
end
|
35
|
+
|
36
|
+
def file_exists?(path)
|
37
|
+
execute("test -e #{path}")
|
38
|
+
true
|
39
|
+
rescue Anywhere::ExecutionError => err
|
40
|
+
if err.result.exit_status == 1
|
41
|
+
false
|
42
|
+
else
|
43
|
+
raise
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def md5sum(path)
|
48
|
+
execute("md5sum #{path} | awk '{ print $1 }'").stdout.strip
|
49
|
+
end
|
50
|
+
|
51
|
+
def sudo_cmd
|
52
|
+
@sudo_cmd ||= root? ? "" : "sudo"
|
53
|
+
end
|
54
|
+
|
55
|
+
def add_system_user(login)
|
56
|
+
raise "user #{login} already exists" if user_exists?(login)
|
57
|
+
logger.info "adding system user #{login}"
|
58
|
+
execute!("#{sudo_cmd} adduser --system #{login}")
|
59
|
+
end
|
60
|
+
|
61
|
+
def run_as(user, cmd)
|
62
|
+
execute("sudo -- sudo -u #{user} -- #{cmd}")
|
63
|
+
end
|
64
|
+
|
65
|
+
def user_exists?(login)
|
66
|
+
execute("id #{login} 2>/dev/null").success?
|
67
|
+
end
|
68
|
+
|
69
|
+
def write_file(path, content, attributes = {})
|
70
|
+
md5 = Digest::MD5.hexdigest(content).to_s
|
71
|
+
if file_exists?(path)
|
72
|
+
logger.debug "file #{path} already exists"
|
73
|
+
file_md5 = md5sum(path)
|
74
|
+
if file_md5 == md5
|
75
|
+
logger.info "file #{path} did not change => not writing"
|
76
|
+
return :not_changed
|
77
|
+
end
|
78
|
+
end
|
79
|
+
logger.info "writing #{content.length} bytes to #{path} (md5: #{md5})"
|
80
|
+
tmp_path = "/opt/urknall/tmp/files.#{md5}"
|
81
|
+
execute("#{sudo_cmd} mkdir -p #{File.dirname(tmp_path)}")
|
82
|
+
execute("#{sudo_cmd} chown #{whoami} #{File.dirname(tmp_path)}")
|
83
|
+
logger.debug "writing to #{tmp_path}"
|
84
|
+
execute(%(rm -f #{tmp_path}; cat - > #{tmp_path}), content)
|
85
|
+
if mode = attributes[:mode]
|
86
|
+
logger.info "changing mode to #{mode}"
|
87
|
+
execute("chmod #{mode} #{tmp_path}")
|
88
|
+
end
|
89
|
+
if owner = attributes[:owner]
|
90
|
+
logger.info "changing owner to #{owner}"
|
91
|
+
execute("#{sudo_cmd} chown #{owner} #{tmp_path}")
|
92
|
+
end
|
93
|
+
execute("#{sudo_cmd} mkdir -p #{File.dirname(path)}")
|
94
|
+
logger.debug "moving #{tmp_path} to #{path}"
|
95
|
+
if file_exists?(path)
|
96
|
+
logger.debug "diff #{path} #{tmp_path}"
|
97
|
+
execute("diff #{tmp_path} #{path}").stdout.split("\n").each do |line|
|
98
|
+
logger.debug line
|
99
|
+
end
|
100
|
+
end
|
101
|
+
execute("#{sudo_cmd} mv #{tmp_path} #{path}")
|
102
|
+
end
|
103
|
+
|
104
|
+
def extract_tar(path, dst)
|
105
|
+
ms = Benchmark.measure do
|
106
|
+
data = File.open(path, "rb") do |f|
|
107
|
+
f.read
|
108
|
+
end
|
109
|
+
logger.info "writing #{data.length} bytes"
|
110
|
+
execute(%(mkdir -p #{dst} && cd #{dst} && tar xfz -), data)
|
111
|
+
end
|
112
|
+
logger.info "extracted archive in %.3f" % [ms.real]
|
113
|
+
end
|
114
|
+
|
115
|
+
def mkdir_p(path)
|
116
|
+
logger.info "creating directory #{path}"
|
117
|
+
execute("mkdir -p #{path}")
|
118
|
+
end
|
119
|
+
|
120
|
+
def home_dir
|
121
|
+
execute("env | grep HOME | cut -d = -f 2").stdout.strip
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "anywhere"
|
2
|
+
require "anywhere/result"
|
3
|
+
require "anywhere/execution_error"
|
4
|
+
require "anywhere/base"
|
5
|
+
|
6
|
+
module Anywhere
|
7
|
+
class Local < Base
|
8
|
+
def do_execute(cmd, stdin_data = nil)
|
9
|
+
require "open3"
|
10
|
+
result = Result.new(cmd)
|
11
|
+
result.started!
|
12
|
+
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
13
|
+
stdin.print stdin_data if stdin_data
|
14
|
+
stdin.close
|
15
|
+
while true
|
16
|
+
streams, _ = IO.select([stdout, stderr], [], [], 1)
|
17
|
+
break if streams.nil? || streams.all? { |s| s.eof? }
|
18
|
+
streams.compact.each do |stream|
|
19
|
+
stream.each do |line|
|
20
|
+
if stream == stdout
|
21
|
+
result.add_stdout(line.strip)
|
22
|
+
logger.info(line) if logger
|
23
|
+
elsif stream == stderr
|
24
|
+
result.add_stderr(line.strip)
|
25
|
+
logger.error(line) if logger
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
result.exit_status = wait_thr.value.exitstatus
|
31
|
+
end
|
32
|
+
result.finished!
|
33
|
+
if !result.success?
|
34
|
+
raise ExecutionError.new(result)
|
35
|
+
end
|
36
|
+
result
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "colorize"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Anywhere
|
5
|
+
class Logger
|
6
|
+
attr_accessor :prefix
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def mutex
|
10
|
+
@mutex ||= Mutex.new
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(attributes = {})
|
15
|
+
@attributes = attributes
|
16
|
+
end
|
17
|
+
|
18
|
+
def prefix
|
19
|
+
@prefix ||= @attributes[:prefix]
|
20
|
+
end
|
21
|
+
|
22
|
+
def stream
|
23
|
+
@attributes[:stream] ||= STDOUT
|
24
|
+
end
|
25
|
+
|
26
|
+
def info(message)
|
27
|
+
print_with_prefix "INFO ".green + " #{message}"
|
28
|
+
end
|
29
|
+
|
30
|
+
def error(message)
|
31
|
+
print_with_prefix "ERROR".red + " #{message}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def debug(message)
|
35
|
+
print_with_prefix "DEBUG".blue + " #{message}"
|
36
|
+
end
|
37
|
+
|
38
|
+
def print_with_prefix(message)
|
39
|
+
out = [Time.now.utc.iso8601(6)]
|
40
|
+
if prefix.is_a?(String)
|
41
|
+
out << prefix
|
42
|
+
elsif prefix.respond_to?(:call)
|
43
|
+
out << prefix.call
|
44
|
+
end
|
45
|
+
out << message
|
46
|
+
self.class.mutex.synchronize do
|
47
|
+
stream.puts out.join(" ")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Anywhere
|
2
|
+
class Result
|
3
|
+
attr_accessor :cmd, :stdout, :stderr, :exit_status
|
4
|
+
|
5
|
+
def initialize(cmd)
|
6
|
+
@cmd = cmd
|
7
|
+
@stdout = []
|
8
|
+
@stderr = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def add_stderr(line)
|
12
|
+
@stderr << line
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_stdout(line)
|
16
|
+
@stdout << line
|
17
|
+
end
|
18
|
+
|
19
|
+
def stderr
|
20
|
+
@stderr.join("\n")
|
21
|
+
end
|
22
|
+
|
23
|
+
def stdout
|
24
|
+
@stdout.join("\n")
|
25
|
+
end
|
26
|
+
|
27
|
+
def started!(time = Time.now)
|
28
|
+
@started = time
|
29
|
+
end
|
30
|
+
|
31
|
+
def finished!(time = Time.now)
|
32
|
+
@finished = time
|
33
|
+
end
|
34
|
+
|
35
|
+
def success?
|
36
|
+
@exit_status == 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def run_time
|
40
|
+
@finished - @started
|
41
|
+
end
|
42
|
+
|
43
|
+
def inspect
|
44
|
+
parts = ["run_time=#{run_time}"]
|
45
|
+
parts << "cmd=<#{@cmd}>"
|
46
|
+
parts << "stdout=#{inspect_string(@stdout)}"
|
47
|
+
parts << "stderr=#{inspect_string(@stderr)}"
|
48
|
+
parts << "exit_status=#{@exit_status}"
|
49
|
+
"<" + parts.join(", ") + ">"
|
50
|
+
end
|
51
|
+
|
52
|
+
def inspect_string(string)
|
53
|
+
if string.empty?
|
54
|
+
"<empty>"
|
55
|
+
else
|
56
|
+
"<#{string.count} lines, #{string.join(" ").length} chars>"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|