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.
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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