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.
@@ -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