bow 0.0.0 → 0.5.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.
- checksums.yaml +4 -4
- data/.gitignore +12 -0
- data/Gemfile +7 -0
- data/LICENSE +21 -0
- data/README.md +156 -0
- data/Rakefile +11 -0
- data/bin/bow +6 -0
- data/bow.gemspec +10 -4
- data/lib/bow.rb +25 -0
- data/lib/bow/application.rb +96 -0
- data/lib/bow/command.rb +60 -0
- data/lib/bow/commands/apply.rb +24 -0
- data/lib/bow/commands/exec.rb +29 -0
- data/lib/bow/commands/init.rb +15 -0
- data/lib/bow/commands/ping.rb +16 -0
- data/lib/bow/commands/prepare.rb +24 -0
- data/lib/bow/config.rb +57 -0
- data/lib/bow/inventory.rb +84 -0
- data/lib/bow/inventory_example.rb +61 -0
- data/lib/bow/locker.rb +180 -0
- data/lib/bow/memorable.rb +37 -0
- data/lib/bow/options.rb +74 -0
- data/lib/bow/rake.rb +118 -0
- data/lib/bow/response_formatter.rb +39 -0
- data/lib/bow/ssh/rsync.rb +24 -0
- data/lib/bow/ssh/scp.rb +29 -0
- data/lib/bow/ssh_helper.rb +84 -0
- data/lib/bow/targets.rb +54 -0
- data/lib/bow/thread_pool.rb +67 -0
- data/lib/bow/version.rb +5 -0
- data/src/preprovision.sh +42 -0
- metadata +48 -5
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bow
|
4
|
+
module Memorable
|
5
|
+
def task_history
|
6
|
+
@task_history ||= Locker.load
|
7
|
+
end
|
8
|
+
|
9
|
+
# def update_history(step = {})
|
10
|
+
# task_history.add(name, step) unless step.empty?
|
11
|
+
# end
|
12
|
+
|
13
|
+
def flush_history
|
14
|
+
task_history.flush
|
15
|
+
end
|
16
|
+
|
17
|
+
def apply
|
18
|
+
task_history.apply(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
def applied?
|
22
|
+
task_history.applied?(name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def revert
|
26
|
+
task_history.revert(name)
|
27
|
+
end
|
28
|
+
|
29
|
+
def reverted?
|
30
|
+
task_history.reverted?(name)
|
31
|
+
end
|
32
|
+
|
33
|
+
def reset
|
34
|
+
task_history.reset(name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/bow/options.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bow
|
4
|
+
class Options
|
5
|
+
OPTIONS = [
|
6
|
+
[
|
7
|
+
'-uUSER',
|
8
|
+
'--user=USER',
|
9
|
+
'Remote user (root used by default)',
|
10
|
+
:option_user
|
11
|
+
],
|
12
|
+
[
|
13
|
+
'-gGROUP',
|
14
|
+
'--group=GROUP',
|
15
|
+
'Hosts group (defined in config)',
|
16
|
+
:option_group
|
17
|
+
],
|
18
|
+
[
|
19
|
+
'-iINVENTORY',
|
20
|
+
'--inventory=INVENTORY',
|
21
|
+
'Path to inventory file',
|
22
|
+
:option_inventory
|
23
|
+
],
|
24
|
+
[
|
25
|
+
'-c',
|
26
|
+
'--copy-tool',
|
27
|
+
'Utilit used for files transfer (scp or rsync)',
|
28
|
+
:option_copy_tool
|
29
|
+
],
|
30
|
+
[
|
31
|
+
'-v',
|
32
|
+
'--version',
|
33
|
+
'Print version and exit',
|
34
|
+
:option_version
|
35
|
+
]
|
36
|
+
].freeze
|
37
|
+
|
38
|
+
def initialize(options)
|
39
|
+
@options = options
|
40
|
+
end
|
41
|
+
|
42
|
+
def parse(opts)
|
43
|
+
OPTIONS.each do |definition|
|
44
|
+
callable = definition.pop
|
45
|
+
opts.on(*definition, method(callable))
|
46
|
+
end
|
47
|
+
opts.on_tail('-h', '--help', 'Print this help and exit.') do
|
48
|
+
puts opts
|
49
|
+
exit
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def option_user(user)
|
54
|
+
@options[:user] = user
|
55
|
+
end
|
56
|
+
|
57
|
+
def option_group(group)
|
58
|
+
@options[:group] = group
|
59
|
+
end
|
60
|
+
|
61
|
+
def option_inventory(inventory)
|
62
|
+
@options[:inventory] = inventory
|
63
|
+
end
|
64
|
+
|
65
|
+
def option_copy_tool(copy_tool)
|
66
|
+
@options[:copy_tool] = copy_tool
|
67
|
+
end
|
68
|
+
|
69
|
+
def option_version(_v)
|
70
|
+
puts VERSION
|
71
|
+
exit
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/bow/rake.rb
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rake'
|
4
|
+
require 'bow'
|
5
|
+
|
6
|
+
module Bow
|
7
|
+
class Rake; end
|
8
|
+
end
|
9
|
+
|
10
|
+
module Rake
|
11
|
+
module DSL
|
12
|
+
# Describe the flow of the next rake task.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
# flow run: :once, enabled: true, revert: :world_down
|
16
|
+
# task world: [:build] do
|
17
|
+
# # ... build world
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# task :world_down do
|
21
|
+
# # ... destroy world
|
22
|
+
# end
|
23
|
+
def flow(*flow) # :doc:
|
24
|
+
Rake.application.last_flow = flow
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
module TaskManager
|
29
|
+
attr_accessor :last_flow
|
30
|
+
|
31
|
+
# Lookup a task. Return an existing task if found, otherwise
|
32
|
+
# create a task of the current type.
|
33
|
+
def intern(task_class, task_name)
|
34
|
+
@tasks[task_name.to_s] ||= task_class.new(task_name, self)
|
35
|
+
task = @tasks[task_name.to_s]
|
36
|
+
task.unpack_flow(get_flow(task))
|
37
|
+
task
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return current flow, clearing it in the process.
|
41
|
+
def get_flow(_task)
|
42
|
+
@last_flow ||= nil
|
43
|
+
flow = @last_flow&.first
|
44
|
+
@last_flow = nil
|
45
|
+
flow
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Task
|
50
|
+
include ::Bow::Memorable
|
51
|
+
|
52
|
+
ALLOWED_FLOW_RULES = %i[run enabled revert].freeze
|
53
|
+
|
54
|
+
alias orig__clear clear
|
55
|
+
alias orig__invoke_with_call_chain invoke_with_call_chain
|
56
|
+
|
57
|
+
def invoke_with_call_chain(task_args, invocation_chain) # :nodoc:
|
58
|
+
return apply_revert_task if disabled?
|
59
|
+
return if run_once? && applied?
|
60
|
+
result = orig__invoke_with_call_chain(task_args, invocation_chain)
|
61
|
+
apply if run_once?
|
62
|
+
flush_history
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def apply_revert_task
|
67
|
+
revert_task = find_revert_task
|
68
|
+
return if reverted? || !revert_task || revert_task.applied?
|
69
|
+
result = revert_task.execute
|
70
|
+
revert_task.apply if revert_task.run_once?
|
71
|
+
revert
|
72
|
+
flush_history
|
73
|
+
result
|
74
|
+
end
|
75
|
+
|
76
|
+
def clear
|
77
|
+
clear_flow
|
78
|
+
orig__clear
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear_flow
|
82
|
+
@flow = {}
|
83
|
+
self
|
84
|
+
end
|
85
|
+
|
86
|
+
def disabled?
|
87
|
+
!enabled?
|
88
|
+
end
|
89
|
+
|
90
|
+
def run_once?
|
91
|
+
flow[:run] == :once
|
92
|
+
end
|
93
|
+
|
94
|
+
def enabled?
|
95
|
+
!!flow[:enabled]
|
96
|
+
end
|
97
|
+
|
98
|
+
def flow
|
99
|
+
@flow ||= { enabled: true, run: :always, revert: nil }
|
100
|
+
end
|
101
|
+
|
102
|
+
def find_revert_task
|
103
|
+
return unless flow[:revert]
|
104
|
+
application.lookup(flow[:revert])
|
105
|
+
end
|
106
|
+
|
107
|
+
# Add flow to the task.
|
108
|
+
def unpack_flow(init_flow)
|
109
|
+
return unless init_flow
|
110
|
+
init_flow.each { |rule, val| add_flow_rule(rule, val) }
|
111
|
+
end
|
112
|
+
|
113
|
+
def add_flow_rule(rule, val)
|
114
|
+
return unless ALLOWED_FLOW_RULES.include? rule
|
115
|
+
flow[rule.to_sym] = val
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bow
|
4
|
+
class ResponseFormatter
|
5
|
+
ERROR = "\033[31m%s\033[0m"
|
6
|
+
INFO = "\033[33m%s\033[0m"
|
7
|
+
SUCCESS = "\033[32m%s\033[0m"
|
8
|
+
HEADER = "\033[1;35m%s\033[0m"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def pretty_print(*args)
|
12
|
+
puts "#{wrap(*args)}\n"
|
13
|
+
end
|
14
|
+
|
15
|
+
def wrap(host, result)
|
16
|
+
host_group = colorize("[#{host.group}]", HEADER)
|
17
|
+
host_addr = colorize(host.host, HEADER)
|
18
|
+
host_header = "\n#{host_group} #{host_addr}:\n\n"
|
19
|
+
response = colorize_result(result).compact.first
|
20
|
+
"#{host_header}#{response}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def colorize_result(result)
|
24
|
+
out, err = result.map { |m| m.to_s.strip }
|
25
|
+
err = err.empty? ? nil : colorize(err, ERROR)
|
26
|
+
out = if !out.empty?
|
27
|
+
colorize(out, SUCCESS)
|
28
|
+
elsif err.nil?
|
29
|
+
colorize('DONE', INFO)
|
30
|
+
end
|
31
|
+
[out, err]
|
32
|
+
end
|
33
|
+
|
34
|
+
def colorize(msg, color_pattern)
|
35
|
+
color_pattern % msg
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Bow
|
4
|
+
module Ssh
|
5
|
+
class Rsync
|
6
|
+
def initialize(ssh_helper)
|
7
|
+
@ssh_helper = ssh_helper
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(source, target)
|
11
|
+
@ssh_helper.run(cmd_rsync(source, conn, target))
|
12
|
+
end
|
13
|
+
|
14
|
+
def cmd_rsync(source, conn, target)
|
15
|
+
format(
|
16
|
+
'rsync --contimeout=10 --force -r %s %s:%s',
|
17
|
+
source,
|
18
|
+
conn,
|
19
|
+
target
|
20
|
+
)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/bow/ssh/scp.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Bow
|
4
|
+
module Ssh
|
5
|
+
class Scp
|
6
|
+
def initialize(ssh_helper)
|
7
|
+
@ssh_helper = ssh_helper
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(source, target)
|
11
|
+
@ssh_helper.execute(cmd_rm(target)) if cleanup_needed?
|
12
|
+
@ssh_helper.run(cmd_scp(source, conn, target))
|
13
|
+
@ssh_helper.run(cmd)
|
14
|
+
end
|
15
|
+
|
16
|
+
def cmd_scp(source, conn, target)
|
17
|
+
format('scp -o ConnectTimeout -r %s %s:%s', source, conn, target)
|
18
|
+
end
|
19
|
+
|
20
|
+
def cmd_rm(target)
|
21
|
+
format('rm -rf %s', target)
|
22
|
+
end
|
23
|
+
|
24
|
+
def cleanup_needed?(source, target)
|
25
|
+
File.basename(source) == File.basename(target)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'open3'
|
4
|
+
|
5
|
+
module Bow
|
6
|
+
class SshHelper
|
7
|
+
class << self
|
8
|
+
def method_missing(m, *args, &block)
|
9
|
+
new(args.shift).send m, *args, &block
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
COPY_TOOLS = { rsync: Ssh::Rsync, scp: Ssh::Scp }.freeze
|
14
|
+
|
15
|
+
attr_reader :conn
|
16
|
+
|
17
|
+
def initialize(conn, app)
|
18
|
+
@app = app
|
19
|
+
@conn = conn
|
20
|
+
end
|
21
|
+
|
22
|
+
def execute(cmd, timeout = 10)
|
23
|
+
cmd = "ssh -o ConnectTimeout=#{timeout} #{conn} #{cmd}"
|
24
|
+
run(cmd)
|
25
|
+
end
|
26
|
+
|
27
|
+
def copy(source, target)
|
28
|
+
source = source.match?(%r{^\/}) ? source : File.join(Dir.pwd, source)
|
29
|
+
copy_tool.call(source, target)
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_provision
|
33
|
+
@app.inventory.ensure!
|
34
|
+
results = []
|
35
|
+
results << ensure_base_dir
|
36
|
+
results << copy_preprovision_script
|
37
|
+
results << copy_rake_tasks
|
38
|
+
merge_results(*results)
|
39
|
+
end
|
40
|
+
|
41
|
+
def ensure_base_dir
|
42
|
+
execute("mkdir -p #{@app.config.guest_from_host[:base_dir]}")
|
43
|
+
end
|
44
|
+
|
45
|
+
def copy_preprovision_script
|
46
|
+
copy(
|
47
|
+
@app.config.host[:pre_script],
|
48
|
+
@app.config.guest_from_host[:pre_script]
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def copy_rake_tasks
|
53
|
+
copy(@app.inventory.location, @app.config.guest_from_host[:rake_dir])
|
54
|
+
end
|
55
|
+
|
56
|
+
def copy_tool
|
57
|
+
return @copy_tool if @copy_tool
|
58
|
+
unless (key = @app.options[:copy_tool]&.to_sym)
|
59
|
+
key = COPY_TOOLS.keys.detect { |t| system("which #{t} &>/dev/null") }
|
60
|
+
error = "Either #{COPY_TOOLS.keys.join(' or ')} should be installed!"
|
61
|
+
raise error unless key
|
62
|
+
end
|
63
|
+
@copy_tool = COPY_TOOLS[key].new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def merge_results(result1, *results)
|
67
|
+
merged = result1.map { |v| [v] }
|
68
|
+
results.each_with_object(merged) do |result, acc|
|
69
|
+
result.each_with_index do |val, i|
|
70
|
+
if val.is_a? Array
|
71
|
+
acc[i] += val
|
72
|
+
else
|
73
|
+
acc[i] << val
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def run(cmd)
|
80
|
+
return cmd if @app.debug?
|
81
|
+
Open3.capture3(cmd).first(2)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/lib/bow/targets.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
module Bow
|
7
|
+
Host = Struct.new(:host, :group, :conn)
|
8
|
+
|
9
|
+
class Targets
|
10
|
+
attr_reader :groups
|
11
|
+
attr_accessor :file, :group, :user
|
12
|
+
|
13
|
+
def initialize(file, user = 'root')
|
14
|
+
@file = file
|
15
|
+
@hosts = { all: Set.new }
|
16
|
+
@user = user
|
17
|
+
@groups = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def hosts(group = :all)
|
21
|
+
parse
|
22
|
+
group = group.to_sym
|
23
|
+
return @hosts[group] unless block_given?
|
24
|
+
@hosts[group.to_sym].each { |h| yield(h) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def parse
|
28
|
+
return if @parsed
|
29
|
+
raw_data.each do |group, hosts|
|
30
|
+
parse_group(group, hosts)
|
31
|
+
end
|
32
|
+
@hosts[:all] = @hosts[:all].uniq
|
33
|
+
@parsed = true
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def parse_group(group, hosts)
|
39
|
+
group = group.to_sym
|
40
|
+
hosts = hosts.uniq.map { |h| build_host(group, h) }
|
41
|
+
@hosts[group] = Set.new hosts
|
42
|
+
@hosts[:all] += hosts
|
43
|
+
groups << group
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_host(group, host)
|
47
|
+
Host.new(host, group, "#{@user}@#{host}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def raw_data
|
51
|
+
@raw_data ||= JSON.parse(File.read(@file))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|