bow 0.0.0 → 0.5.0

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