anywhere 0.0.1
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 +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
|