awx 0.5.1 → 0.6.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/lib/aws/aws_cloudformation.rb +23 -7
- data/lib/aws/aws_outputter.rb +15 -11
- data/lib/aws/aws_profile.rb +13 -9
- data/lib/aws/aws_reports.rb +18 -16
- data/lib/awx.rb +76 -30
- data/lib/core/deployments.rb +65 -0
- data/lib/core/replacer.rb +282 -0
- data/lib/routes/{aws_cloudformation_create.rb → cloudformation_create.rb} +221 -116
- data/lib/routes/{aws_cloudformation_delete.rb → cloudformation_delete.rb} +1 -1
- data/lib/routes/{aws_cloudformation_detect_drift.rb → cloudformation_detect_drift.rb} +1 -1
- data/lib/routes/{aws_deploy.rb → deploy_deprecated.rb} +11 -8
- data/lib/routes/{aws_dynamo_db.rb → dynamo_db.rb} +1 -1
- data/lib/routes/infrastructure.rb +45 -0
- data/lib/routes/{aws_list.rb → list.rb} +20 -5
- data/lib/routes/ssh.rb +70 -0
- data/lib/routes/{aws_switch.rb → switch.rb} +1 -1
- data/lib/routes/upload.rb +49 -0
- data/lib/version.rb +1 -1
- data/opt/awx/deployment-schema.yml +57 -0
- data/opt/awx/reports.yml +105 -37
- data/opt/config/schema.yml +22 -0
- data/opt/config/template.yml +9 -1
- metadata +17 -11
@@ -0,0 +1,282 @@
|
|
1
|
+
module App
|
2
|
+
|
3
|
+
class Replacer
|
4
|
+
|
5
|
+
REGEXP_MATCHER = /\$\{\{[^}]+\}\}/
|
6
|
+
REGEXP_MULTILINE = /^[A-Za-z0-9]+:\s*\|$/
|
7
|
+
REGEXP_MODIFIER = /^\$\{\{awx-(for|end).*\}\}$/
|
8
|
+
TYPE_YML = 'yml'
|
9
|
+
VALID_MODIFIERS = %w(base64encode)
|
10
|
+
OP_SCAN = 'scan'
|
11
|
+
OP_REPLACE = 'replace'
|
12
|
+
SSH_USERS = 'SSHUsers'
|
13
|
+
|
14
|
+
# Return Array of Hashes containing all matchers.
|
15
|
+
# Use this for validation.
|
16
|
+
# @return Array
|
17
|
+
def self.scan_string(line)
|
18
|
+
matchers = {}
|
19
|
+
errors = []
|
20
|
+
x, matchers, errors = process_matchers(line, matchers, errors, OP_SCAN) if line =~ REGEXP_MATCHER
|
21
|
+
return {
|
22
|
+
:matchers => matchers,
|
23
|
+
:errors => errors
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Return Array of Hashes containing all matchers.
|
28
|
+
# Use this for validation.
|
29
|
+
# @return Hash
|
30
|
+
def self.scan_file(path_and_file, operation = OP_SCAN, data: nil, new_lines: nil)
|
31
|
+
raise RuntimeError, "File does not exist: #{path_and_file}" unless Blufin::Files::file_exists(path_and_file)
|
32
|
+
raise RuntimeError, "Cannot call scan_file without new_lines being an Array when operation is #{OP_REPLACE}." if operation == OP_REPLACE && !new_lines.is_a?(Array)
|
33
|
+
matchers = {}
|
34
|
+
errors = []
|
35
|
+
multiline = nil
|
36
|
+
awx_for = nil
|
37
|
+
line_count = 0
|
38
|
+
Blufin::Files::read_file(path_and_file).each do |line|
|
39
|
+
next if line =~ /^\s*#/ && line !~ /^\s*#cloud-config/ # Skip comments (but not #cloud-config).
|
40
|
+
line = line.gsub("\n", '')
|
41
|
+
line_count += 1
|
42
|
+
# This handles multi-line block (IE -> content: | ) which basically gets skipped.
|
43
|
+
# If you want to make replace work within comments, you will need to write more code (or possibly just add a flag?).
|
44
|
+
if line.strip =~ REGEXP_MULTILINE
|
45
|
+
# Stores the number of spaces from start.
|
46
|
+
multiline = line[/\A */].size
|
47
|
+
awx_for[:lines] << line if awx_for.is_a?(Hash)
|
48
|
+
new_lines << line if operation == OP_REPLACE && awx_for.nil?
|
49
|
+
next
|
50
|
+
elsif !multiline.nil?
|
51
|
+
whitespace_from_start = line[/\A */].size
|
52
|
+
# If white-space from beginning is same or higher, we're probably still in a multi-line.
|
53
|
+
if line.strip == '' || whitespace_from_start > multiline
|
54
|
+
awx_for[:lines] << line if awx_for.is_a?(Hash)
|
55
|
+
new_lines << line if operation == OP_REPLACE && awx_for.nil?
|
56
|
+
next
|
57
|
+
end
|
58
|
+
# Otherwise, break-out.
|
59
|
+
multiline = nil
|
60
|
+
end
|
61
|
+
# This handles ${{awx-[modifier]}} tags.
|
62
|
+
if line.strip =~ REGEXP_MODIFIER
|
63
|
+
awx_modifier = line.strip.gsub(/^\$\{\{awx-/, '').gsub(/\}\}$/, '')
|
64
|
+
# Handle end-tag first.
|
65
|
+
if awx_modifier == 'end'
|
66
|
+
if awx_for.is_a?(Hash)
|
67
|
+
if operation == OP_SCAN
|
68
|
+
# Handle matchers.
|
69
|
+
if awx_for[:matchers].any?
|
70
|
+
awx_for[:matchers].each do |k, v|
|
71
|
+
if k != awx_for[:item]
|
72
|
+
matchers[k] = [] unless matchers.has_key?(k)
|
73
|
+
matchers[k].push(*v)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
78
|
+
# Handle errors.
|
79
|
+
errors.push(*awx_for[:errors]) if awx_for[:errors].any?
|
80
|
+
else
|
81
|
+
ms = awx_for[:item_source].split(':')
|
82
|
+
raise RuntimeError, "Invalid key: #{ms[0]}" unless data.has_key?(ms[0])
|
83
|
+
raise RuntimeError, "Invalid key: #{ms[0]}.#{ms[1]}" unless data[ms[0]].has_key?(ms[1])
|
84
|
+
is = data[ms[0]][ms[1]]
|
85
|
+
awx_for_data = data
|
86
|
+
if is.is_a?(Array)
|
87
|
+
if ms[1] == SSH_USERS
|
88
|
+
# SSH Users get handled differently.
|
89
|
+
is.each do |ssh_key_file|
|
90
|
+
raise RuntimeError, "File not found: #{ssh_key_file}" unless Blufin::Files::file_exists(ssh_key_file)
|
91
|
+
contents = []
|
92
|
+
Blufin::Files::read_file(ssh_key_file).each do |c|
|
93
|
+
next if c.strip == ''
|
94
|
+
contents << c
|
95
|
+
end
|
96
|
+
awx_for_data[awx_for[:item]] = {
|
97
|
+
'name' => Blufin::Files::extract_file_name(ssh_key_file).gsub(/\.pub$/i, '').gsub(/\./, '-'),
|
98
|
+
'pub_key' => contents.join("\n")
|
99
|
+
}
|
100
|
+
new_lines = process_awx_for_loop(awx_for, awx_for_data, new_lines)
|
101
|
+
end
|
102
|
+
else
|
103
|
+
|
104
|
+
# TODO - Finish this.
|
105
|
+
raise RuntimeError, 'Not yet implemented!'
|
106
|
+
|
107
|
+
end
|
108
|
+
else
|
109
|
+
|
110
|
+
# TODO - Finish this.
|
111
|
+
raise RuntimeError, 'Not yet implemented!'
|
112
|
+
|
113
|
+
end
|
114
|
+
end
|
115
|
+
awx_for = nil
|
116
|
+
end
|
117
|
+
next
|
118
|
+
end
|
119
|
+
# If a tag is already open, return an error (or throw one).
|
120
|
+
unless awx_for.nil?
|
121
|
+
tag_open_error = "Detected #{line.strip} tag even though another one is already open (Line: #{line_count})."
|
122
|
+
if operation == OP_SCAN
|
123
|
+
errors << tag_open_error
|
124
|
+
next
|
125
|
+
else
|
126
|
+
raise RuntimeError, tag_open_error
|
127
|
+
end
|
128
|
+
end
|
129
|
+
# Hits here at the start of a for loop (IE: ${{awx-for(user in Parameters:SSHUsers)}})
|
130
|
+
if awx_modifier =~ /for\([A-Za-z0-9]+\s*in\s*[A-Za-z0-9]+:[A-Za-z0-9]+\)/
|
131
|
+
ams = awx_modifier.strip.gsub(/^for\(/, '').gsub(/\)$/, '')
|
132
|
+
ams = ams.split(' ')
|
133
|
+
awx_for = {
|
134
|
+
:lines => [],
|
135
|
+
:item => ams[0],
|
136
|
+
:item_source => ams[2],
|
137
|
+
:matchers => {},
|
138
|
+
:errors => []
|
139
|
+
}
|
140
|
+
line, matchers, errors = process_matchers("${{#{ams[2]}}}", matchers, errors, OP_SCAN, data: data, file: path_and_file)
|
141
|
+
next
|
142
|
+
else
|
143
|
+
raise RuntimeError, "Unsupported awx modifier: #{awx_modifier}"
|
144
|
+
end
|
145
|
+
end
|
146
|
+
# If we're in a for loop, start buffering content.
|
147
|
+
if awx_for.is_a?(Hash)
|
148
|
+
if operation == OP_SCAN
|
149
|
+
line, awx_for[:matchers], awx_for[:errors] = process_matchers(line, awx_for[:matchers], awx_for[:errors], operation, data: data, file: path_and_file)
|
150
|
+
end
|
151
|
+
awx_for[:lines] << line
|
152
|
+
next
|
153
|
+
end
|
154
|
+
# This just processes a regular line.
|
155
|
+
line, matchers, errors = process_matchers(line, matchers, errors, operation, data: data, file: path_and_file) if line =~ REGEXP_MATCHER
|
156
|
+
new_lines << line if operation == OP_REPLACE
|
157
|
+
end
|
158
|
+
if operation == OP_SCAN
|
159
|
+
return {
|
160
|
+
:matchers => matchers,
|
161
|
+
:errors => errors
|
162
|
+
}
|
163
|
+
else
|
164
|
+
return new_lines
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Takes a string, replaces all the matchers and returns string.
|
169
|
+
# @return string
|
170
|
+
def self.replace_string(line, data)
|
171
|
+
return nil if line.nil?
|
172
|
+
raise RuntimeError, "Expected String, instead got #{line.class}" unless line.is_a?(String)
|
173
|
+
raise RuntimeError, "Expected Hash, instead got #{data.class}" unless data.is_a?(Hash)
|
174
|
+
line, x, y = process_matchers(line, {}, [], OP_REPLACE, data: data)
|
175
|
+
return line
|
176
|
+
end
|
177
|
+
|
178
|
+
# Takes file path and returns array of lines (that can then be used to write same/new file).
|
179
|
+
# @return Array (of file lines)
|
180
|
+
def self.replace_yml(path_and_file, data)
|
181
|
+
raise RuntimeError, "Expected String, instead got #{path_and_file.class}" unless path_and_file.is_a?(String)
|
182
|
+
raise RuntimeError, "Expected Hash, instead got #{data.class}" unless data.is_a?(Hash)
|
183
|
+
extension = Blufin::Files::extract_file_name(path_and_file).split('.')
|
184
|
+
extension = extension[extension.length - 1].downcase
|
185
|
+
raise RuntimeError, "Expected YML file, instead got: #{path_and_file}" unless %w(yml yaml).include?(extension)
|
186
|
+
new_lines = []
|
187
|
+
scan_file(path_and_file, OP_REPLACE, data: data, new_lines: new_lines)
|
188
|
+
new_lines
|
189
|
+
end
|
190
|
+
|
191
|
+
private
|
192
|
+
|
193
|
+
# Process matchers. Either replaces line or populates Hash.
|
194
|
+
# @return multiple
|
195
|
+
def self.process_matchers(line, matchers, errors, operation, data: nil, file: nil)
|
196
|
+
matches = line.scan(REGEXP_MATCHER)
|
197
|
+
matches.each do |match|
|
198
|
+
ms = match.gsub(/^\$\{\{/, '').gsub(/\}\}$/, '')
|
199
|
+
ms = ms.split(':')
|
200
|
+
if ms.length <= 1 || ms.length >= 4
|
201
|
+
if operation == OP_SCAN
|
202
|
+
errors << match
|
203
|
+
next
|
204
|
+
else
|
205
|
+
raise RuntimeError, "Invalid matcher: #{match}"
|
206
|
+
end
|
207
|
+
end
|
208
|
+
matchers[ms[0]] = [] unless matchers.has_key?(ms[0])
|
209
|
+
if operation == OP_SCAN
|
210
|
+
matchers[ms[0]] << {:key => ms[1]} if ms.length == 2
|
211
|
+
if ms.length == 3
|
212
|
+
unless VALID_MODIFIERS.include?(ms[2])
|
213
|
+
errors << match
|
214
|
+
next
|
215
|
+
end
|
216
|
+
matchers[ms[0]] << {:key => ms[1], :modifier => ms[2]}
|
217
|
+
end
|
218
|
+
else
|
219
|
+
if ms[0] == 'file'
|
220
|
+
raise RuntimeError, "Cannot process file: #{ms[1]} because containing file path was not passed. This is an unsupported edge-case." if file.nil?
|
221
|
+
file_path = "#{Blufin::Files::extract_path_name(file)}/#{ms[1]}"
|
222
|
+
file_content = replace_yml(file_path, data)
|
223
|
+
if ms.length == 3
|
224
|
+
case ms[2]
|
225
|
+
when 'base64encode'
|
226
|
+
tmp_file = "/tmp/base64encode-#{Blufin::Strings::random_string(4)}.txt"
|
227
|
+
Blufin::Terminal::execute_proc("Base64 Encoding raw file: \x1B[38;5;240m#{tmp_file}\x1B[0m", Proc.new {
|
228
|
+
Blufin::Files::write_file(tmp_file, file_content)
|
229
|
+
b64_cmd = Blufin::Tools::value_based_on_os(mac: "openssl base64 -in #{tmp_file} | tr -d '\n'", linux: "base64 -w0 #{tmp_file}")
|
230
|
+
result = Blufin::Terminal::command_capture(b64_cmd, nil, nil, nil)[0]
|
231
|
+
result = result.split("\n").join('') # Removes line breaks, just in case :)
|
232
|
+
file_content = result
|
233
|
+
})
|
234
|
+
else
|
235
|
+
raise RuntimeError, "Invalid modifier: #{ms[2]}"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
file_content = file_content.join('') if file_content.is_a?(Array)
|
239
|
+
line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}\}\}/, file_content) if ms.length == 2
|
240
|
+
line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}:#{ms[2]}\}\}/, file_content) if ms.length == 3
|
241
|
+
elsif ms[0] == AppCommand::CloudFormationCreate::STACK
|
242
|
+
stack = ms[1]
|
243
|
+
Blufin::Terminal::error("Found: #{Blufin::Terminal::format_invalid("${{Stack:#{stack}}}")} on a line that does support this property.", ["Expected: #{Blufin::Terminal::format_highlight("TemplateURL: ${{Stack:#{stack}}}")}", " Got: #{Blufin::Terminal::format_invalid(line.strip)}"], true) unless line =~ /TemplateURL:\s*\$\{\{Stack:#{stack.gsub('/', '\/')}\}\}/
|
244
|
+
raise RuntimeError, "data is missing key: #{AppCommand::CloudFormationCreate::STACK}" unless data.has_key?(AppCommand::CloudFormationCreate::STACK)
|
245
|
+
raise RuntimeError, "data[Stack] is missing key: #{stack}" unless data[AppCommand::CloudFormationCreate::STACK].has_key?(stack)
|
246
|
+
line = line.gsub( /\$\{\{Stack:#{stack.gsub('/', '\/')}\}\}/, data[AppCommand::CloudFormationCreate::STACK][stack][:s3_url])
|
247
|
+
else
|
248
|
+
raise RuntimeError, "Invalid key: #{ms[0]}" unless data.has_key?(ms[0])
|
249
|
+
raise RuntimeError, "Invalid key: #{ms[0]}.#{ms[1]}" unless data[ms[0]].has_key?(ms[1])
|
250
|
+
line = line.gsub(/\$\{\{#{ms[0]}:#{ms[1]}\}\}/, data[ms[0]][ms[1]])
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
return line, matchers, errors
|
255
|
+
end
|
256
|
+
|
257
|
+
# Processes the awx-for content.
|
258
|
+
# @return Array
|
259
|
+
def self.process_awx_for_loop(awx_for, awx_for_data, new_lines)
|
260
|
+
ml = nil
|
261
|
+
awx_for[:lines].each do |line|
|
262
|
+
if line.strip =~ REGEXP_MULTILINE
|
263
|
+
ml = line[/\A */].size
|
264
|
+
new_lines << line
|
265
|
+
next
|
266
|
+
elsif !ml.nil?
|
267
|
+
whitespace_from_start = line[/\A */].size
|
268
|
+
if line.strip == '' || whitespace_from_start > ml
|
269
|
+
new_lines << line
|
270
|
+
next
|
271
|
+
end
|
272
|
+
ml = nil
|
273
|
+
end
|
274
|
+
line, x, y = process_matchers(line, {}, [], OP_REPLACE, data: awx_for_data)
|
275
|
+
new_lines << line
|
276
|
+
end
|
277
|
+
new_lines
|
278
|
+
end
|
279
|
+
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|