blower 0.2.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 52e337abe103b677ac90233cfd4f38121585fe0b
4
- data.tar.gz: 47e87f53486d3b7ec27e2abc78d24098b5759e0d
3
+ metadata.gz: 43a7308c9eaa1ad35d5a1c3942fad1850b32f3dc
4
+ data.tar.gz: 0d43517ed6d42111fd7aa0eabe3f30275ee1d227
5
5
  SHA512:
6
- metadata.gz: 6f250fa25824a1e5c003cadb0102f90305fd6e536b7a4602021a7222ddf7d3259e2870e4dc5d4efd5d1f617367474f541be6ec1f1d784b164c6f621a00bbec87
7
- data.tar.gz: 518c595f5587dc82b6ef0b2fb831c095e237c407c614807e56e7e126d72d58a66c7a530552fd4ca1f3bf61ad990e271c472445a3c5bcfe944ae152d37f8edbda
6
+ metadata.gz: dd7de8fa74c568387e02af7afde83fa52d719a9ccb8658eae7a26ef9fa483b924654bb7bf9fe5f0b97d08b595a7652f72c4994810424b833058d14feb3dbed89
7
+ data.tar.gz: 200326faec848b91ea49c5422f8fb74843c17de9ac622559fd927e19099628a1a5de6d10573655ae5b4bd67056c9cca459b97758a4d3c160867bec3ba486a422
data/bin/blow CHANGED
@@ -1,24 +1,20 @@
1
1
  #!/usr/bin/env ruby
2
-
3
2
  require 'blower'
3
+ require 'pathname'
4
+ require 'optparse'
4
5
 
5
- huff = Blower::Huff.new
6
-
7
- args = ARGV
8
- args = ["main"] if args.empty? && (File.exist?("main") || !Dir["main.*"].empty?)
9
-
10
- if args.empty?
11
- huff.log.fail "Usage: blow TASK..."
12
- exit 1
13
- end
14
-
15
- args = ["hosts.ini", *args] if File.exist?("hosts.ini")
6
+ OptionParser.new do |opts|
7
+ opts.banner = "Usage: blow [options] task..."
8
+ opts.on "-d DIR", "Change directory" do |v|
9
+ Dir.chdir v
10
+ end
11
+ end.order!
16
12
 
17
13
  begin
18
- args.each do |arg|
19
- huff.run arg
14
+ context = Blower::Context.new([".", Dir.pwd])
15
+ context.target = nil
16
+ context.run "Blowfile"
17
+ ARGV.each do |arg|
18
+ context.run arg
20
19
  end
21
- rescue RuntimeError => e
22
- huff.log.fail e.message
23
- exit 1
24
20
  end
@@ -0,0 +1,107 @@
1
+ require 'forwardable'
2
+
3
+ module Blower
4
+
5
+ class Context
6
+ extend Forwardable
7
+
8
+ attr_accessor :path
9
+ attr_accessor :location
10
+ attr_accessor :target
11
+
12
+ def initialize (path)
13
+ @path = path
14
+ @have_seen = {}
15
+ end
16
+
17
+ def log (message, level, &block)
18
+ message = "(on #{target.name}) " + message if target.respond_to?(:name)
19
+ Logger.instance.log(message, level, &block)
20
+ end
21
+
22
+ def stage (message, &block)
23
+ log message, :info, &block
24
+ end
25
+
26
+ def one_host (name = nil, &block)
27
+ each_host [name || target.hosts.sample], &block
28
+ end
29
+
30
+ def each_host (hosts = target, parallel: true, &block)
31
+ hosts.each do |host|
32
+ ctx = dup
33
+ ctx.target = host
34
+ ctx.instance_exec(&block)
35
+ end
36
+ end
37
+
38
+ def reboot
39
+ begin
40
+ sh "shutdown -r now"
41
+ rescue IOError
42
+ sleep 0.1 while ping
43
+ sleep 1.0 until ping
44
+ end
45
+ end
46
+
47
+ def sh (command)
48
+ log "execute #{command}", :debug
49
+ target.sh(command)
50
+ end
51
+
52
+ def cp (from, to)
53
+ log "upload #{Array(from).join(", ")} -> #{to}", :debug
54
+ target.cp(from, to)
55
+ end
56
+
57
+ def write (string, to)
58
+ log "upload data to #{to}", :debug
59
+ target.cp(StringIO.new(string), to)
60
+ end
61
+
62
+ def sh? (command)
63
+ log "execute #{command}", :debug
64
+ target.sh(command)
65
+ rescue Blower::Host::ExecuteError
66
+ false
67
+ end
68
+
69
+ def capture (command)
70
+ stdout = ""
71
+ target.sh(command, stdout)
72
+ stdout
73
+ end
74
+
75
+ def run (task)
76
+ files = []
77
+ @path.each do |dir|
78
+ name = File.join(dir, task)
79
+ name += ".rb" unless File.exist?(name)
80
+ if File.directory?(name)
81
+ dirtask = File.join(name, File.basename(@task))
82
+ dirtask += ".rb" unless File.exist?(dirtask)
83
+ name = dirtask
84
+ blowfile = File.join(name, "Blowfile")
85
+ files << blowfile if File.exist?(blowfile) && !@have_seen[blowfile]
86
+ end
87
+ files << name if File.exist?(name)
88
+ break unless files.empty?
89
+ end
90
+ if files.empty?
91
+ fail "can't find #{task}"
92
+ else
93
+ begin
94
+ old_task, @task = @task, task
95
+ files.each do |file|
96
+ @have_seen[file] = true
97
+ instance_eval(File.read(file), file)
98
+ end
99
+ ensure
100
+ @task = old_task
101
+ end
102
+ end
103
+ end
104
+
105
+ end
106
+
107
+ end
@@ -0,0 +1,112 @@
1
+ require 'net/ssh'
2
+ require 'net/scp'
3
+ require 'monitor'
4
+ require 'colorize'
5
+
6
+ module Blower
7
+
8
+ class Host
9
+ include MonitorMixin
10
+ extend Forwardable
11
+
12
+ attr_accessor :name
13
+ attr_accessor :user
14
+ attr_accessor :data
15
+
16
+ def_delegators :data, :[], :[]=
17
+
18
+ class ExecuteError < Exception
19
+ attr_accessor :status
20
+ def initialize (status)
21
+ @status = status
22
+ end
23
+ end
24
+
25
+ def initialize (name, user = "root")
26
+ @name = name
27
+ @user = user
28
+ @data = {}
29
+ super()
30
+ end
31
+
32
+ def log
33
+ Logger.instance
34
+ end
35
+
36
+ def ping
37
+ Timeout.timeout(1) do
38
+ TCPSocket.new(name, 22).close
39
+ end
40
+ true
41
+ rescue Timeout::ExitException
42
+ false
43
+ rescue Errno::ECONNREFUSED
44
+ false
45
+ end
46
+
47
+ def cp (from, to, output = "")
48
+ synchronize do
49
+ if from.is_a?(String) || from.is_a?(Array)
50
+ to += "/" if to[-1] != "/" && from.is_a?(Array)
51
+ IO.popen(["rsync", "-z", "-r", "--progress", *from, "#{@user}@#{@name}:#{to}"],
52
+ in: :close, err: %i(child out)) do |io|
53
+ until io.eof?
54
+ begin
55
+ output << io.read_nonblock(100)
56
+ rescue IO::WaitReadable
57
+ IO.select([io])
58
+ retry
59
+ end
60
+ end
61
+ end
62
+ elsif from.is_a?(StringIO) or from.is_a?(IO)
63
+ log.info "string -> #{to}" unless quiet
64
+ ssh.scp.upload!(from, to)
65
+ else
66
+ fail "Don't know how to copy a #{from.class}: #{from}"
67
+ end
68
+ end
69
+ true
70
+ rescue => e
71
+ false
72
+ end
73
+
74
+ def sh (command, output = "")
75
+ synchronize do
76
+ result = nil
77
+ ch = ssh.open_channel do |ch|
78
+ ch.exec(command) do |_, success|
79
+ fail "failed to execute command" unless success
80
+ ch.on_data do |_, data|
81
+ output << data
82
+ end
83
+ ch.on_extended_data do |_, _, data|
84
+ output << data.colorize(:red)
85
+ end
86
+ ch.on_request("exit-status") { |_, data| result = data.read_long }
87
+ end
88
+ end
89
+ ch.wait
90
+ if result != 0
91
+ log.fatal "failed on #{name}"
92
+ log.raw output
93
+ exit 1
94
+ end
95
+ result
96
+ end
97
+ end
98
+
99
+ def each (&block)
100
+ block.(self)
101
+ end
102
+
103
+ private
104
+
105
+ def ssh
106
+ @ssh = nil if @ssh && @ssh.closed?
107
+ @ssh ||= Net::SSH.start(name, user)
108
+ end
109
+
110
+ end
111
+
112
+ end
@@ -0,0 +1,36 @@
1
+ module Blower
2
+
3
+ class HostGroup
4
+
5
+ attr_accessor :hosts
6
+ attr_accessor :root
7
+ attr_accessor :location
8
+
9
+ def initialize (hosts)
10
+ @hosts = hosts
11
+ end
12
+
13
+ def sh (command = nil, *args, &block)
14
+ each do |host|
15
+ command = block.() if block
16
+ host.sh(command)
17
+ end
18
+ end
19
+
20
+ def cp (from, to)
21
+ each do |host|
22
+ host.cp(from, to)
23
+ end
24
+ end
25
+
26
+ def each (&block)
27
+ hosts.map do |host|
28
+ Thread.new do
29
+ block.(host)
30
+ end
31
+ end.map(&:join)
32
+ end
33
+
34
+ end
35
+
36
+ end
data/lib/blower/logger.rb CHANGED
@@ -1,44 +1,52 @@
1
- require "colorize"
1
+ require "singleton"
2
2
 
3
3
  module Blower
4
- class Logger
5
4
 
6
- def initialize ()
7
- @logdent = 0
8
- end
9
-
10
- def info (message, **keys, &block)
11
- log(message, :blue, **keys, &block)
12
- end
13
-
14
- def warn (message, **keys, &block)
15
- log(message, :yellow, STDERR, **keys, &block)
16
- end
5
+ class Logger
6
+ include MonitorMixin
7
+ include Singleton
8
+
9
+ COLORS = {
10
+ trace: :light_black,
11
+ debug: :light_black,
12
+ info: :blue,
13
+ warn: :yellow,
14
+ error: :red,
15
+ fatal: :magenta,
16
+ }
17
+
18
+ def initialize
19
+ @indent = 0
20
+ super()
21
+ end
17
22
 
18
- def fail (message, **keys, &block)
19
- log(message, :red, STDERR, **keys, &block)
20
- end
23
+ def trace (a=nil, *b, &c); log(a, :trace, *b, &c); end
24
+ def debug (a=nil, *b, &c); log(a, :debug, *b, &c); end
25
+ def info (a=nil, *b, &c); log(a, :info, *b, &c); end
26
+ def warn (a=nil, *b, &c); log(a, :warn, *b, &c); end
27
+ def error (a=nil, *b, &c); log(a, :error, *b, &c); end
28
+ def fatal (a=nil, *b, &c); log(a, :fatal, *b, &c); end
29
+ def raw (a=nil, *b, &c); log(a, nil, *b, &c); end
30
+
31
+ def log (message = nil, level = :info, &block)
32
+ if message
33
+ synchronize do
34
+ message = message.colorize(COLORS[level]) if level
35
+ puts " " * @indent + message
36
+ end
37
+ end
38
+ begin
39
+ @indent += 1
40
+ block.()
41
+ ensure
42
+ @indent -= 1
43
+ end if block
44
+ end
21
45
 
22
- def win (message, **keys, &block)
23
- log(message, :green, **keys, &block)
24
46
  end
25
47
 
26
- def log (message, color=nil, to=STDOUT, prefix: "", &block)
27
- message.to_s.scan(/(?:[^\n\r]*[\n\r])|(?:[^\n\r]+$)/) do |line|
28
- case line[-1]
29
- when "\n", "\r"
30
- else
31
- line = line + "\n"
32
- end
33
- STDOUT.write " " * @logdent + prefix + (color ? line.colorize(color) : line)
34
- end
35
- begin
36
- @logdent +=1
37
- block.()
38
- ensure
39
- @logdent -= 1
40
- end if block
48
+ def self.log (*args, &block)
49
+ Logger.instance.log(*args, &block)
41
50
  end
42
51
 
43
52
  end
44
- end
@@ -0,0 +1,42 @@
1
+ module Blower
2
+
3
+ class MockHost
4
+ extend Forwardable
5
+
6
+ attr_accessor :log
7
+ attr_accessor :data
8
+
9
+ def_delegators :data, :[], :[]=
10
+
11
+ def initialize (name)
12
+ @log = Logger.new("mock #{name.ljust(15)} | ")
13
+ @data = {}
14
+ end
15
+
16
+ def sh (command, stdout: nil, stdin: nil)
17
+ log.info command
18
+ sleep rand * 0.1
19
+ end
20
+
21
+ def cp (from, to, quiet: false)
22
+ if from.is_a?(String)
23
+ to += File.basename(from) if to[-1] == "/"
24
+ log.info "#{from} -> #{to}" unless quiet
25
+ elsif from.is_a?(Array)
26
+ to += "/" unless to[-1] == "/"
27
+ log.info "#{from.join(", ")} -> #{to}" unless quiet
28
+ elsif from.is_a?(StringIO) or from.is_a?(IO)
29
+ log.info "string -> #{to}" unless quiet
30
+ else
31
+ fail "Don't know how to copy a #{from.class}: #{from}"
32
+ end
33
+ sleep rand * 0.1
34
+ end
35
+
36
+ def each (&block)
37
+ block.(self)
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -1,3 +1,3 @@
1
1
  module Blower
2
- VERSION = "0.2.3"
2
+ VERSION = "2.0.0"
3
3
  end
data/lib/blower.rb CHANGED
@@ -1,4 +1,6 @@
1
1
  require 'blower/logger'
2
- require 'blower/huff'
3
- require 'blower/puff'
2
+ require 'blower/context'
3
+ require 'blower/host'
4
+ require 'blower/host_group'
5
+ require 'blower/mock_host'
4
6
  require 'blower/version'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: blower
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Baum
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-01 00:00:00.000000000 Z
11
+ date: 2015-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-ssh
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-scp
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: colorize
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -47,9 +61,11 @@ extra_rdoc_files: []
47
61
  files:
48
62
  - bin/blow
49
63
  - lib/blower.rb
50
- - lib/blower/huff.rb
64
+ - lib/blower/context.rb
65
+ - lib/blower/host.rb
66
+ - lib/blower/host_group.rb
51
67
  - lib/blower/logger.rb
52
- - lib/blower/puff.rb
68
+ - lib/blower/mock_host.rb
53
69
  - lib/blower/version.rb
54
70
  homepage: http://www.github.org/nbaum/blower
55
71
  licenses:
data/lib/blower/huff.rb DELETED
@@ -1,56 +0,0 @@
1
- module Blower
2
- class Huff
3
-
4
- attr_accessor :hosts, :log, :puffs, :env
5
-
6
- def initialize ()
7
- @hosts = ["127.0.0.1"]
8
- @log = Logger.new
9
- @logdent = 0
10
- @puffs = Hash.new { |h, k| h[k] = Puff.new(self, k) }
11
- @env = {}
12
- end
13
-
14
- def hosts (hosts)
15
- log.info "new hosts: #{hosts.join(", ")}"
16
- @hosts = hosts
17
- end
18
-
19
- def ruby (task)
20
- instance_eval(File.read(task), task)
21
- end
22
-
23
- def shell (task)
24
- @hosts.each do |host|
25
- log.info "running #{task} on #{host}" do
26
- puffs[host].shell(task)
27
- end
28
- end
29
- end
30
-
31
- def inventory (task)
32
- hosts File.read(task).split
33
- end
34
-
35
- def run (name)
36
- name = name.to_s
37
- task = File.exist?(name) ? name : Dir[name + ".*"].first
38
- if !task
39
- fail "Can't find #{name}"
40
- elsif File.directory?(task)
41
- Dir.chdir(task) do
42
- run("main")
43
- end
44
- elsif task =~ /\.rb$/
45
- ruby(task)
46
- elsif task =~ /\.sh$/
47
- shell(task)
48
- elsif task =~ /\.ini$/
49
- inventory(task)
50
- else
51
- fail "Don't know what to do with #{task}"
52
- end
53
- end
54
-
55
- end
56
- end
data/lib/blower/puff.rb DELETED
@@ -1,57 +0,0 @@
1
- require "net/ssh"
2
- require "shellwords"
3
-
4
- module Blower
5
- class Puff
6
-
7
- attr_accessor :huff, :host, :log, :env
8
-
9
- def initialize (huff, host)
10
- @huff = huff
11
- @log = huff.log
12
- @host = host
13
- @env = {}
14
- end
15
-
16
- def env2shell ()
17
- huff.env.merge(@env).map do |name, value|
18
- next unless name =~ /\A[A-Za-z\d_]+\z/
19
- "#{name}=#{value.shellescape}"
20
- end.join("\n") + "\n"
21
- end
22
-
23
- def shell (task)
24
- Net::SSH.start(host, "root") do |ssh|
25
- command = File.read(task)
26
- status, signal = nil, nil
27
- ssh.open_channel do |ch|
28
- stdout, stderr = "", ""
29
- ch.exec(env2shell + command) do |_, success|
30
- fail "failed to execute command" unless success
31
- ch.on_data do |_, data|
32
- stdout << data
33
- if i = stdout.rindex(/[\n\r]/)
34
- data, stdout = stdout[0..i], (stdout[(i + 1)..-1] || "")
35
- log.log data
36
- end
37
- end
38
- ch.on_extended_data do |_, _, data|
39
- stderr << data
40
- if i = stderr.rindex(/[\n\r]/)
41
- data, stderr = stderr[0..i], (stderr[(i + 1)..-1] || "")
42
- log.fail data
43
- end
44
- end
45
- ch.on_request("exit-status") { |_, data| status = data.read_long }
46
- ch.on_request("exit-signal") { |_, data| signal = data.read_long }
47
- end
48
- end
49
- ssh.loop
50
- if status != 0
51
- fail "exit status #{status}"
52
- end
53
- end
54
- end
55
-
56
- end
57
- end