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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bow
4
+ module Commands
5
+ class Apply < Command
6
+ def description
7
+ 'Apply provision on remote hosts.'
8
+ end
9
+
10
+ def run
11
+ ThreadPool.new do |t|
12
+ t.from_enumerable targets do |host|
13
+ result = app.ssh_helper(host).prepare_provision(PROVISION_PATH)
14
+ ResponseFormatter.pretty_print(host, result)
15
+
16
+ cmd = "'cd #{PROVISION_PATH} && rake #{host.group}:provision'"
17
+ result = app.ssh_helper(host).execute(cmd)
18
+ ResponseFormatter.pretty_print(host, result)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+ require 'pp'
5
+
6
+ module Bow
7
+ module Commands
8
+ class Exec < Command
9
+ def description
10
+ 'Exec command on remote hosts.'
11
+ end
12
+
13
+ def usage
14
+ "bow #{command_name} command [args] [options]"
15
+ end
16
+
17
+ def run
18
+ raise ArgumentError, 'Command required!' unless @argv && !@argv&.empty?
19
+ cmd = @argv.shift
20
+ ThreadPool.new do |t|
21
+ t.from_enumerable targets do |host|
22
+ result = app.ssh_helper(host).execute(cmd)
23
+ ResponseFormatter.pretty_print(host, result)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bow
4
+ module Commands
5
+ class Init < Command
6
+ def description
7
+ 'Initialize minimal bow scaffold.'
8
+ end
9
+
10
+ def run
11
+ InventoryExample.new.init
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bow
4
+ module Commands
5
+ class Ping < Command
6
+ def description
7
+ 'Check connectivity to all given hosts.'
8
+ end
9
+
10
+ def run
11
+ @argv << 'echo pong'
12
+ Exec.new(@app, @argv).run
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bow
4
+ module Commands
5
+ class Prepare < Command
6
+ def description
7
+ 'Install RVM, Ruby and Rake on provisioned hosts'
8
+ end
9
+
10
+ def run
11
+ ThreadPool.new do |t|
12
+ t.from_enumerable targets do |host|
13
+ result = app.ssh_helper(host).prepare_provision(PROVISION_PATH)
14
+ ResponseFormatter.pretty_print(host, result)
15
+
16
+ provision_cmd = "bash #{PROVISION_PATH}/preprovision.sh"
17
+ result = app.ssh_helper(host).execute(provision_cmd)
18
+ ResponseFormatter.pretty_print(host, result)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bow
4
+ class Config
5
+ class << self
6
+ def host
7
+ @host ||= Config.new(:host, HOST_BASE_DIR)
8
+ end
9
+
10
+ def guest
11
+ @guest ||= Config.new(:guest, GUEST_BASE_DIR)
12
+ end
13
+
14
+ def guest_from_host
15
+ @guest_from_host ||= Config.new(:guest, '~')
16
+ end
17
+ end
18
+
19
+ HOST_BASE_DIR = File.dirname(File.dirname(File.dirname(__FILE__)))
20
+
21
+ GUEST_BASE_DIR = "#{Dir.home}/.bow"
22
+
23
+ GUEST_FROM_HOST_BASE_DIR = '~/.bow'
24
+
25
+ PATHS = {
26
+ guest: {
27
+ base_dir: '%s',
28
+ rake_dir: '%s/rake',
29
+ history: '%s/.history',
30
+ pre_script: '%s/provision.sh'
31
+ },
32
+ host: {
33
+ base_dir: '%s',
34
+ pre_script: '%s/src/provision.sh'
35
+ }
36
+ }.freeze
37
+
38
+ def initialize(type, base_dir)
39
+ @type = type
40
+ @base_dir = base_dir
41
+ end
42
+
43
+ def get(name)
44
+ name.to_sym
45
+ value = PATHS[@type][name]
46
+ substitude_dir(value)
47
+ end
48
+
49
+ def substitude_dir(orig)
50
+ format(orig, @base_dir) if orig
51
+ end
52
+
53
+ def [](name)
54
+ get(name)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/file_list'
4
+
5
+ module Bow
6
+ class Inventory
7
+ DEFAULT_RAKEFILES = [
8
+ 'Rakefile',
9
+ 'rakefile',
10
+ 'Rakefile.rb',
11
+ 'rakefile.rb'
12
+ ].freeze
13
+
14
+ DEFAULT_TARGETFILES = [
15
+ # 'bow.yaml',
16
+ # 'bow.rb',
17
+ 'targets.json'
18
+ ].freeze
19
+
20
+ attr_reader :rakefile, :targetfile, :location
21
+
22
+ def initialize
23
+ @rakefiles = DEFAULT_RAKEFILES.dup
24
+ @targetfiles = DEFAULT_TARGETFILES.dup
25
+ @original_dir = Dir.pwd
26
+ end
27
+
28
+ def parse
29
+ return if @inventory_loaded
30
+ rakefile, targetfile, @location = inventory_location
31
+ @rakefile = File.join(@location, rakefile) if rakefile
32
+ @targetfile = File.join(@location, targetfile) if targetfile
33
+ @inventory_loaded = true
34
+ end
35
+
36
+ def valid?
37
+ parse
38
+ !!(rakefile && targetfile && location)
39
+ end
40
+
41
+ def ensure!
42
+ parse
43
+ [
44
+ ['Rakefile', @rakefile, @rakefiles],
45
+ ['Target file', @targetfile, @targetfiles]
46
+ ].each do |name, file, variants|
47
+ unless file
48
+ raise "No #{name} found (looking for: #{variants.join(', ')})"
49
+ end
50
+ end
51
+ self
52
+ end
53
+
54
+ private
55
+
56
+ def inventory_location
57
+ reset_path do
58
+ here = @original_dir
59
+ until (files = inspect_dir)
60
+ break if here == '/'
61
+ Dir.chdir('..')
62
+ here = Dir.pwd
63
+ end
64
+ [files, here].flatten
65
+ end
66
+ end
67
+
68
+ def reset_path
69
+ yield
70
+ ensure
71
+ Dir.chdir(@original_dir)
72
+ end
73
+
74
+ def inspect_dir
75
+ rakefile = detect_files(@rakefiles)
76
+ targetfile = detect_files(@targetfiles)
77
+ [rakefile, targetfile]
78
+ end
79
+
80
+ def detect_files(variants)
81
+ variants.detect { |fn| File.exist?(fn) }
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ module Bow
5
+ class InventoryExample
6
+ TARGETING_EXAMPLE = {
7
+ 'example_group1' => [
8
+ '192.168.50.27',
9
+ '192.168.50.37'
10
+ ],
11
+ 'example_group2' => [
12
+ '192.168.50.47',
13
+ '192.168.50.57'
14
+ ]
15
+ }.freeze
16
+
17
+ RAKEFILE_EXAMPLE = <<-RAKEFILE.gsub(/^ {6}/, '').freeze
18
+ require 'bow/rake'
19
+
20
+ Rake.application.options.trace_rules = true
21
+
22
+ PROVISION_DIR = '/tmp/rake_provision'
23
+
24
+ namespace :example_group1 do
25
+ task provision: :print_hello do
26
+ end
27
+
28
+ flow run: :once
29
+ task :print_hello do
30
+ sh 'echo "Hello from example group #1 server!"'
31
+ end
32
+ end
33
+
34
+ namespace :example_group2 do
35
+ task provision: :print_hello do
36
+ end
37
+
38
+ flow enabled: false, revert_task: :print_goodbye
39
+ task :print_hello do
40
+ sh 'echo "Hello from example group #2 server!"'
41
+ end
42
+
43
+ task :print_goodbye do
44
+ sh 'echo "Goodbye! The task at example group #2 is disabled!"'
45
+ end
46
+ end
47
+ RAKEFILE
48
+
49
+ def init
50
+ raise 'Can not init. Directory not empty!' unless Dir.empty?(Dir.pwd)
51
+ {
52
+ 'Rakefile' => RAKEFILE_EXAMPLE,
53
+ 'targets.json' => JSON.pretty_generate(TARGETING_EXAMPLE)
54
+ }.each do |targ, code|
55
+ f = File.open(targ, 'w')
56
+ f.write(code)
57
+ f.close
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,180 @@
1
+ require 'fileutils'
2
+ require 'objspace'
3
+
4
+ # Locker is a task state manager.
5
+ # It tracks the task status and saves it to the history file.
6
+ module Bow
7
+ class Locker
8
+ class << self
9
+ attr_accessor :file_path
10
+
11
+ def load
12
+ @instance ||= new
13
+ end
14
+
15
+ def load!
16
+ @instance = new
17
+ end
18
+ end
19
+
20
+ SEPARATOR = ";\t".freeze
21
+ LINE_SEP = "\n".freeze
22
+ NOT_FOUND = :not_found
23
+
24
+ attr_accessor :runtime_cache
25
+
26
+ def initialize
27
+ @file_path = self.class.file_path || Config.guest[:history]
28
+ @modified = false
29
+ @file_opened = false
30
+ @runtime_cache = {}
31
+ end
32
+
33
+ def applied?(task)
34
+ record = parse(find(task))
35
+ !!(record && record[1])
36
+ end
37
+
38
+ def reverted?(task)
39
+ record = parse(find(task))
40
+ !!(record && record[2])
41
+ end
42
+
43
+ def apply(task)
44
+ return if applied?(task)
45
+ add(task, true, reverted?(task))
46
+ end
47
+
48
+ def revert(task)
49
+ return if reverted?(task)
50
+ add(task, applied?(task), true)
51
+ end
52
+
53
+ def parse(meta)
54
+ return false if meta[:record] == NOT_FOUND
55
+ record = meta[:record].split(SEPARATOR)
56
+ record.map do |v|
57
+ v.strip!
58
+ case v
59
+ when 'true' then true
60
+ when 'false' then false
61
+ else v
62
+ end
63
+ end
64
+ end
65
+
66
+ def reset(task)
67
+ meta = find(task)
68
+ reset_cache(task)
69
+ return if meta[:record] == NOT_FOUND
70
+ file.seek(meta[:first_c], IO::SEEK_SET)
71
+ str_len = meta[:last_c] - meta[:first_c] + 1
72
+ file.write ' ' * str_len
73
+ end
74
+
75
+ def find(task)
76
+ cached = from_cache(task)
77
+ return cached if cached
78
+ result = pure_find(task)
79
+ result
80
+ end
81
+
82
+ def from_cache(task)
83
+ @runtime_cache[task]
84
+ end
85
+
86
+ def to_cache(task, result)
87
+ @runtime_cache[task] = result
88
+ end
89
+
90
+ def reset_cache(task)
91
+ @runtime_cache[task] = nil
92
+ end
93
+
94
+ # rubocop:disable Metrics/MethodLength
95
+ # rubocop:disable Style/PerlBackrefs
96
+ def pure_find(task)
97
+ file.rewind
98
+ current_line = ''
99
+ first_c = 0
100
+ last_c = 0
101
+ file.each_char.with_index do |c, idx|
102
+ if c == LINE_SEP
103
+ current_line =~ /^([^\s]+)#{SEPARATOR}/
104
+ to_cache(task, record: $1, first_c: first_c, last_c: last_c)
105
+ if task == $1
106
+ return { record: current_line, first_c: first_c, last_c: last_c }
107
+ end
108
+ current_line = ''
109
+ first_c = idx + 1
110
+ last_c = first_c
111
+ else
112
+ current_line << c
113
+ last_c = idx
114
+ end
115
+ end
116
+ { record: NOT_FOUND, first_c: nil, last_c: nil }
117
+ end
118
+ # rubocop:enable Style/PerlBackrefs
119
+ # rubocop:enable Metrics/MethodLength
120
+
121
+ def add(task, applied = false, reverted = false)
122
+ reset(task)
123
+ file.seek(0, IO::SEEK_END)
124
+ write(task, applied, reverted)
125
+ self
126
+ end
127
+
128
+ def write(task, applied = false, reverted = false)
129
+ reset_cache(task)
130
+ record = [task, applied, reverted].join(SEPARATOR)
131
+ first_c = file.tell
132
+ file.puts(record)
133
+ last_c = first_c + record.size - 1
134
+ to_cache(task, record: record, first_c: first_c, last_c: last_c)
135
+ @modified = true
136
+ self
137
+ end
138
+
139
+ def flush
140
+ return unless @modified && @file_opened
141
+ file.close
142
+ @file_opened = false
143
+ @modified = false
144
+ self
145
+ end
146
+
147
+ def compact
148
+ compressed = ''
149
+ file.each do |l|
150
+ compressed << l unless l[0] == ' '
151
+ end
152
+ file.rewind
153
+ file.write(compressed)
154
+ file.truncate(compressed.size)
155
+ @runtime_cache = {}
156
+ @modified = true
157
+ self
158
+ end
159
+
160
+ def empty!
161
+ file.rewind
162
+ file.truncate(0)
163
+ @runtime_cache = {}
164
+ end
165
+
166
+ def file
167
+ return @file if @file && @file_opened
168
+ ensure_file(@file_path)
169
+ @file = File.open(@file_path, 'r+')
170
+ @file_opened = true
171
+ file
172
+ end
173
+
174
+ def ensure_file(name)
175
+ return if File.exist?(name)
176
+ FileUtils.mkdir_p(File.dirname(name))
177
+ FileUtils.touch(name)
178
+ end
179
+ end
180
+ end