bow 0.0.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|