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,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,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
|
data/lib/bow/config.rb
ADDED
@@ -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
|
data/lib/bow/locker.rb
ADDED
@@ -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
|