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,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
|