awx 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 54e617e3d455c0aa8d4f5d3dce0573cbb8ed86c3
4
- data.tar.gz: 2039dffe2e3f9cc5e391fca408d726313280fcc7
3
+ metadata.gz: 52520a21d53d145c1da6785673db13f6f98bb927
4
+ data.tar.gz: fd8646cb8f06cf49a0cdda13e29a0dd64b3b7329
5
5
  SHA512:
6
- metadata.gz: d050dcd91c40b5fb9fc39f38bba5854fe6790cc9cf77f262ab656bb0d8e666d63d0bafcd567a84a75c7142501e0e730bb559dfb5e48d73687d3528ebb02af699
7
- data.tar.gz: 0cb3de40b28af605e5bbf33124a8c348c38441a95faa9a8fa90b81eafb8619fdf91d2ff6d8b74c0adadc71ae55739e6c50f70ba9d6ea35710f47406cecb3c1dc
6
+ metadata.gz: 5c81bba7df3c682a7b732712f610ba01de5c1d217e61a0da9fe5b4d60286353bbac987539bf36ffc48664610c25da336cd9214b4d76f8cdaa065788fdf9c4e79
7
+ data.tar.gz: 5b535a761bdc19a239b3eb5f8cb2a401ed9443428461fa0e8381ee701a27bb84f44c0ebf12c88faa0fb8082171c96bd97c436c3156ed164e438b7fd7e734fb9a
@@ -14,35 +14,11 @@ module App
14
14
  # Uploads a cloudformation template to S3 (so we can create a stack from it).
15
15
  # Returns the S3 URL.
16
16
  # @return string
17
- def self.upload_cloudformation_template(template_category, template_name, description)
18
- source = "#{get_cloudformation_path}/#{template_category}/#{template_name}/template.yml"
19
- tmp_file = "/tmp/aws-cf-upload-#{template_category}-#{template_name}-#{Blufin::Strings::random_string}.yml"
17
+ def self.upload_cloudformation_template(source, category, template)
20
18
  raise RuntimeError, "File does not exist: #{source}" unless Blufin::Files::file_exists(source)
19
+ tmp_file = "/tmp/aws-cf-upload-#{category}-#{template}-#{Blufin::Strings::random_string}.yml"
21
20
  Blufin::Terminal::execute("cp #{source} #{tmp_file}", text: "Preparing template: \x1B[38;5;240m#{tmp_file}\x1B[0m")
22
- # Add description to final YML file.
23
- Blufin::Files::write_line_to_file(tmp_file, "Description: \"#{description.gsub('"', '\"')}\"", /AWSTemplateFormatVersion:\s*("|')?[0-9\-]+("|')?/i)
24
- # This block of code removes the 2nd description tag.
25
- new_lines = []
26
- description_found = false
27
- parsing_parameters = true
28
- Blufin::Files::read_file(tmp_file).each do |line|
29
- line = line.gsub("\n", '')
30
- if line.strip =~ /^description:/i && !description_found
31
- new_lines << line unless description_found
32
- description_found = true
33
- else
34
- parsing_parameters = true if line.strip =~ /^parameters:$/i
35
- if parsing_parameters
36
- # AWS Template Anatomy: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-anatomy.html
37
- parsing_parameters = false if line.strip =~ /^(Metadata|Mappings|Conditions|Transform|Resources|Outputs)+:$/
38
- # This skips all the parameter defaults (as they may contain weird data which won't pass regex validation when initially sent to AWS).
39
- next if line.strip =~ /^default:/i && parsing_parameters
40
- end
41
- new_lines << line
42
- end
43
- end
44
- Blufin::Files::write_file(tmp_file, new_lines)
45
- template_filename = "#{template_category}-#{template_name}-#{DateTime.now.strftime('%Y%m%d-%H%M%S')}.yml"
21
+ template_filename = "#{category}-#{template}-#{DateTime.now.strftime('%Y%m%d-%H%M%S')}.yml"
46
22
  bucket_path = get_s3_bucket_path
47
23
  bucket_path = bucket_path == '' ? '' : "#{bucket_path}/"
48
24
  App::AWSCli::s3_upload(tmp_file, App::AWSProfile::get_profile['CloudFormation']['Uploads']['S3Bucket']['Name'], "#{bucket_path}#{template_filename}")
@@ -5,6 +5,7 @@ module App
5
5
  @@profiles = nil
6
6
  @@profile = nil
7
7
  @@credentials = nil
8
+ @@ssh_users = nil
8
9
 
9
10
  FILE_AWS_CONFIG = File.expand_path('~/.aws/config')
10
11
  FILE_AWS_CREDENTIALS = File.expand_path('~/.aws/credentials')
@@ -12,15 +13,17 @@ module App
12
13
  PROFILE = 'Profile'
13
14
  PROFILES = 'Profiles'
14
15
  CLOUDFORMATION = 'CloudFormation'
16
+ SSH_KEYS = 'SSHKeys'
15
17
 
16
18
  # Reads the config data and decides what profile to use.
17
19
  # @return void
18
20
  def self.init(config_data)
19
21
 
20
- raise RuntimeError, 'Cannot run App::AWSProfile::init more than once.' unless @@profiles.nil? && @@profile.nil? && @@credentials.nil?
22
+ raise RuntimeError, 'Cannot run App::AWSProfile::init more than once.' unless @@profiles.nil? && @@profile.nil? && @@credentials.nil? && @@ssh_users.nil?
21
23
 
22
- @@profiles = {}
23
- @@profile = {}
24
+ @@profiles = {}
25
+ @@profile = {}
26
+ @@ssh_users = {}
24
27
 
25
28
  first_key = nil
26
29
 
@@ -46,7 +49,7 @@ module App
46
49
 
47
50
  errors = []
48
51
 
49
- # Validate CloudFormation data (if exists)
52
+ # Validate CloudFormation data (if exist).
50
53
  if @@profile.has_key?(CLOUDFORMATION)
51
54
  # TODO AWX PROFILE - Must support S3.
52
55
  cloudformation_template_path = @@profile[CLOUDFORMATION]['Templates']['Local']['Path']
@@ -60,24 +63,27 @@ module App
60
63
  end
61
64
  end
62
65
 
66
+ # Validate SSHKeys (if exist).
67
+ download_s3_ssh_users(true)
68
+
63
69
  # Check the credentials exist.
64
70
  if Blufin::Files::file_exists(FILE_AWS_CREDENTIALS)
65
- @@aws_credentials = App::AWSCredentials.new
66
- profile = @@profile[PROFILE]
67
- config = Blufin::Files::file_exists(FILE_AWS_CONFIG) ? ParseConfig.new(FILE_AWS_CONFIG) : nil
68
- credentials = ParseConfig.new(FILE_AWS_CREDENTIALS)
71
+ @@credentials = App::AWSCredentials.new
72
+ profile = @@profile[PROFILE]
73
+ config = Blufin::Files::file_exists(FILE_AWS_CONFIG) ? ParseConfig.new(FILE_AWS_CONFIG) : nil
74
+ credentials = ParseConfig.new(FILE_AWS_CREDENTIALS)
69
75
  unless credentials.params[profile].nil?
70
76
  # Currently not used/required (but here just in case).
71
77
  unless config.nil? || config.params[profile].nil?
72
- @@aws_credentials.region = config.params[profile]['region'] unless config.params[profile]['region'].nil?
73
- @@aws_credentials.output = config.params[profile]['output'] unless config.params[profile]['output'].nil?
78
+ @@credentials.region = config.params[profile]['region'] unless config.params[profile]['region'].nil?
79
+ @@credentials.output = config.params[profile]['output'] unless config.params[profile]['output'].nil?
74
80
 
75
81
  end
76
- @@aws_credentials.aws_key = credentials.params[profile]['aws_access_key_id'] unless credentials.params[profile]['aws_access_key_id'].nil?
77
- @@aws_credentials.aws_secret = credentials.params[profile]['aws_secret_access_key'] unless credentials.params[profile]['aws_secret_access_key'].nil?
82
+ @@credentials.aws_key = credentials.params[profile]['aws_access_key_id'] unless credentials.params[profile]['aws_access_key_id'].nil?
83
+ @@credentials.aws_secret = credentials.params[profile]['aws_secret_access_key'] unless credentials.params[profile]['aws_secret_access_key'].nil?
78
84
  end
79
- errors << "aws-cli error. Cannot find #{profile}: #{Blufin::Terminal::format_invalid('aws_access_key_id')} in: #{Blufin::Terminal::format_directory(FILE_AWS_CREDENTIALS)}" if @@aws_credentials.aws_key.nil?
80
- errors << "aws-cli error. Cannot find #{profile}: #{Blufin::Terminal::format_invalid('aws_secret_access_key')} in: #{Blufin::Terminal::format_directory(FILE_AWS_CREDENTIALS)}" if @@aws_credentials.aws_secret.nil?
85
+ errors << "aws-cli error. Cannot find #{profile}: #{Blufin::Terminal::format_invalid('aws_access_key_id')} in: #{Blufin::Terminal::format_directory(FILE_AWS_CREDENTIALS)}" if @@credentials.aws_key.nil?
86
+ errors << "aws-cli error. Cannot find #{profile}: #{Blufin::Terminal::format_invalid('aws_secret_access_key')} in: #{Blufin::Terminal::format_directory(FILE_AWS_CREDENTIALS)}" if @@credentials.aws_secret.nil?
81
87
  else
82
88
  errors << "aws-cli error. Cannot find file: #{Blufin::Terminal::format_invalid(FILE_AWS_CREDENTIALS)}"
83
89
  end
@@ -109,7 +115,42 @@ module App
109
115
  # If credentials don't exist (or are missing information) -- nil is returned.
110
116
  # @return App::AWSCredentials
111
117
  def self.get_credentials
112
- @@aws_credentials
118
+ @@credentials
119
+ end
120
+
121
+ # Gets configured SSH users. Must be configured in YML and S3.
122
+ # @return Hash
123
+ def self.get_ssh_users
124
+ @@ssh_users.each do |user, pub_key|
125
+ Blufin::Terminal::error("Public key not found for user: #{Blufin::Terminal::format_invalid(user)}", "Expected file to exist: #{Blufin::Terminal::format_directory("#{user}.pub", false)}", true) if pub_key.nil?
126
+ end
127
+ @@ssh_users
128
+ end
129
+
130
+ # Gets Users from S3.
131
+ # Can be called multiple times (which you might do if you want to invalidate the cache).
132
+ # @return void
133
+ def self.download_s3_ssh_users(use_cache = true)
134
+ if @@profile.has_key?(SSH_KEYS)
135
+ s3 = @@profile[SSH_KEYS]['S3Bucket']
136
+ tmp_path = Blufin::AWS::download_s3_data(s3['Name'], s3['Path'], profile: @@profile[PROFILE], region: s3['Region'], use_cache: use_cache)
137
+ users = []
138
+ # Gets a unique list of users (since every user has 2 files, private and public key).
139
+ if Blufin::Files::path_exists(tmp_path)
140
+ Blufin::Files::get_files_in_dir(tmp_path).each do |file|
141
+ users << Blufin::Files::extract_file_name(file, false).gsub(/\.pub$/i, '')
142
+ end
143
+ end
144
+ @@ssh_users = {} unless use_cache
145
+ users.uniq!
146
+ users.sort!
147
+ users.each do |user|
148
+ pub_key = "#{tmp_path}/#{user}.pub"
149
+ @@ssh_users[user] = Blufin::Files::file_exists(pub_key) ? pub_key : nil
150
+ end
151
+
152
+ end
153
+
113
154
  end
114
155
 
115
156
  end
@@ -178,7 +178,7 @@ module App
178
178
 
179
179
  # Returns a Hash with all the resources that can be auto-fetched using a script.
180
180
  # @return Hash
181
- def self.get_auto_fetch_resources(data)
181
+ def self.get_lookups(data)
182
182
  auto_fetch_resources = {}
183
183
  data.each do |resource|
184
184
  if resource[1].has_key?(App::AWSReports::KEY_EXPORT)
data/lib/awx.rb CHANGED
@@ -52,11 +52,13 @@ TEMPLATE
52
52
  awx_cloudformation.command :create, :aliases => [:c] do |awx_cloudformation_create|
53
53
  awx_cloudformation_create.summary 'Create stack'
54
54
  awx_cloudformation_create.options do |opts|
55
+ opts.opt :clear_cache, 'Clear cache', :short => '-c', :long => '--clear-cache', :type => :boolean
56
+ opts.opt :rerun, "Re-run previous with cached values (if exists) \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-r', :long => '--re-run', :type => :boolean
57
+
55
58
  # TODO AWX PROFILE - Must support S3.
56
59
  if Blufin::Files::path_exists("#{File.expand_path(App::AWSProfile::get_profile['CloudFormation']['Templates']['Local']['Path'])}/test")
57
60
  opts.opt :test, 'Run through test-template.', :short => '-t', :long => '--test', :type => :boolean
58
61
  end
59
- opts.opt :test, "Re-run previous with cached values (if exists) \xe2\x80\x94 #{Blufin::Terminal::format_invalid('@NotImplemented')}", :short => '-R', :long => '--re-run', :type => :boolean
60
62
  end
61
63
  awx_cloudformation_create.action do |opts, args|
62
64
  AppCommand::AWSCloudFormationCreate.new(opts, args).execute
@@ -29,8 +29,8 @@ module AppCommand
29
29
  SPACER = '<<--Spacer-->>'
30
30
  CAPABILITIES = 'Capabilities'
31
31
  RETURN_VALUE = 'PjNkHK33EopWxCpzOQfuku3la'
32
- EC2_USER_DATA = 'EC2UserData'
33
- EC2_USER_DATA_FILE = 'cloud-config.yml'
32
+ SSH_USERS = 'SSHUsers'
33
+ SYSTEM = 'System'
34
34
 
35
35
  def execute
36
36
 
@@ -39,21 +39,22 @@ module AppCommand
39
39
  @opts = command_options
40
40
  @args = arguments
41
41
 
42
- @template = nil
43
- @templates = {}
44
- @params = {}
45
- @output = {}
46
- @regions = App::AWSCli::get_regions
47
- @auto_fetch_resources = {}
48
- @auto_fetch_cache = {}
49
- @data = nil
50
- @table_widths = {}
51
- @export_map = {}
52
- @columns = {}
53
- @options_default = {}
54
- @cache = {}
55
- @cache_valid = false
56
- @projects = {}
42
+ @template = nil
43
+ @templates = {}
44
+ @params = {}
45
+ @params_system = nil
46
+ @output = {}
47
+ @regions = App::AWSCli::get_regions
48
+ @lookups = {}
49
+ @lookup_cache = {}
50
+ @data = nil
51
+ @table_widths = {}
52
+ @export_map = {}
53
+ @columns = {}
54
+ @options_default = {}
55
+ @cache = {}
56
+ @cache_valid = false
57
+ @projects = {}
57
58
 
58
59
  @terminal_width = Blufin::Terminal::get_terminal_width
59
60
  @columns, @data, @export_map, @table_widths = App::AWSReports::parse_metadata(@regions)
@@ -76,18 +77,28 @@ module AppCommand
76
77
  # Windows is currently not supported, so bomb-out.
77
78
  Blufin::Tools::os_not_supported([Blufin::Tools::OS_WINDOWS])
78
79
 
80
+ # If clear-cache flag is set, removed all cached stuff.
81
+ if @opts[:clear_cache]
82
+ # Clear out the SSH keys (cached from S3).
83
+ App::AWSProfile::download_s3_ssh_users(false)
84
+ end
85
+
79
86
  # If Terminal window is smaller than 230, bomb-out.
80
87
  terminal_width_actual = Blufin::Terminal::get_terminal_width
81
88
  terminal_required_width = 227
82
89
  Blufin::Terminal::error("Output for this command \x1B[38;5;240m(#{Blufin::Terminal::format_action(terminal_required_width)}\x1B[38;5;240m-width)\x1B[0m does not fit in Terminal \x1B[38;5;240m(#{Blufin::Terminal::format_action(terminal_width_actual)}\x1B[38;5;240m-width)\x1B[0m", 'Please make your terminal wider and try again.', true) if terminal_width_actual < terminal_required_width
83
90
 
84
- @warnings = []
85
- @auto_fetch_resources = App::AWSReports::get_auto_fetch_resources(@data)
91
+ @warnings = []
92
+ @lookups = App::AWSReports::get_lookups(@data)
93
+
94
+ # Add SSH Users to Lookups (if they've been configured).
95
+ users = App::AWSProfile::get_ssh_users
96
+ @lookups[SSH_USERS] = users if users.any?
86
97
 
87
98
  @options_default[OPTION_ENVIRONMENT] = Blufin::Projects::get_environments
88
- @options_default[OPTION_REGION] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['Regions']
89
- @options_default[OPTION_STACK_NAME] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['StackName']
90
- @options_default[OPTION_TIMEOUT] = App::AWSProfile::get_profile['CloudFormation']['Defaults']['Timeout']
99
+ @options_default[OPTION_REGION] = App::AWSProfile::get_profile[App::AWSProfile::CLOUDFORMATION]['Defaults']['Regions']
100
+ @options_default[OPTION_STACK_NAME] = App::AWSProfile::get_profile[App::AWSProfile::CLOUDFORMATION]['Defaults']['StackName']
101
+ @options_default[OPTION_TIMEOUT] = App::AWSProfile::get_profile[App::AWSProfile::CLOUDFORMATION]['Defaults']['Timeout']
91
102
  @options_default[OPTION_PROJECT] = Blufin::Projects::get_project_names
92
103
 
93
104
  # Loop the entire blufin-secrets/cloudformation path(s) and validate all the template(s).
@@ -147,11 +158,12 @@ module AppCommand
147
158
  'MaxLength' => false,
148
159
  'MaxValue' => false,
149
160
  'ConstraintDescription' => false,
161
+ 'NoEcho' => false,
150
162
  }
151
163
  Blufin::Validate::assert_valid_keys(expected, param_data.keys, "#{file} \xe2\x86\x92 #{param_name}")
152
164
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template has reserved parameter: #{Blufin::Terminal::format_invalid(param_name)}" if [OPTION_STACK_NAME.downcase].concat(RESERVED_WORDS).include?(param_name.downcase)
153
165
  parameters[param_name] = param_data
154
- if @auto_fetch_resources.has_key?(param_name)
166
+ if @lookups.has_key?(param_name)
155
167
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Parameter: #{param_name} cannot have default value: '#{param_data[DEFAULT]}' because it is a live look-up list (from AWS)." if param_data.keys.include?(DEFAULT)
156
168
  end
157
169
  # Validate parameter type.
@@ -207,24 +219,11 @@ module AppCommand
207
219
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 AllowedValues must be Array, instead got: #{Blufin::Terminal::format_invalid(param_data['AllowedValues'].class)}"
208
220
  end
209
221
  end
210
- # Validate EC2UserData parameter (make sure cloud-init.txt exists).
211
- if param_name == EC2_USER_DATA
212
- cloud_init_file = "#{template_path}/#{EC2_USER_DATA_FILE}"
213
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template has #{Blufin::Terminal::format_highlight(EC2_USER_DATA)} parameter but no #{Blufin::Terminal::format_invalid(EC2_USER_DATA_FILE)} file." unless Blufin::Files::file_exists(cloud_init_file)
214
- end
215
222
  end
216
223
  end
217
224
  # Validate description (if exists).
218
225
  if yml_data.has_key?(OPTION_DESCRIPTION)
219
226
  description = yml_data[OPTION_DESCRIPTION]
220
- # Validate replaceable value(s) exist.
221
- matches = description.scan(/{{[A-Za-z0-9]+}}/)
222
- matches.each do |match|
223
- match = match.gsub(/^{{/, '').gsub(/}}$/, '')
224
- unless parameters.keys.include?(match)
225
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid description matcher: #{Blufin::Terminal::format_invalid(match)}"
226
- end
227
- end
228
227
  else
229
228
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Template is missing description."
230
229
  end
@@ -267,24 +266,9 @@ module AppCommand
267
266
  # Validate stack name.
268
267
  if stack_name.nil? || stack_name.strip == ''
269
268
  stack_name = @options_default[OPTION_STACK_NAME]
270
- else
271
- # Validate Stack Name (if exists).
272
- matches = stack_name.scan(/{{[A-Za-z0-9]+}}/)
273
- matches.each do |match|
274
- match = match.gsub(/^{{/, '').gsub(/}}$/, '')
275
- if match != match.upcase
276
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 All stack-name matchers must be uppercase, found: #{Blufin::Terminal::format_invalid(match)}"
277
- next
278
- end
279
- unless MATCHERS.include?(match)
280
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid stack-name matcher: #{Blufin::Terminal::format_invalid(match)}"
281
- end
282
- stack_name_stripped = stack_name.gsub(/{{[A-Za-z0-9]+}}/, '')
283
- if stack_name_stripped !~ /[a-z0-9\-]/
284
- @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Stack-name has invalid or non-matcher, uppercase characters: #{Blufin::Terminal::format_invalid(stack_name)} \x1B[38;5;240m(#{stack_name_stripped})\x1B[0m"
285
- end
286
- end
287
269
  end
270
+ results = Blufin::Replacer::scan_string(stack_name)
271
+ validate_matchers(file_cloudformation, results, template_name, parameters)
288
272
  # Make sure deployment stack is not a reserved word.
289
273
  if deployment_stack.is_a?(String) && %w(lambda).include?(deployment_stack.downcase)
290
274
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 #{Blufin::Terminal::format_highlight('DEPLOYMENT_STACK')} value is a reserved word: #{Blufin::Terminal::format_invalid(deployment_stack)}"
@@ -334,7 +318,7 @@ module AppCommand
334
318
  Object.send(:remove_const, :Template)
335
319
  end
336
320
  end
337
- # Make sure no-sort parameters exist.
321
+ # Validate no-sort parameters (if exist).
338
322
  unless parameters_no_sort.nil?
339
323
  if parameters_no_sort.is_a?(Array)
340
324
  parameters_no_sort.each do |pns|
@@ -344,6 +328,12 @@ module AppCommand
344
328
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Expected constant #{Blufin::Terminal::format_highlight('PARAMETERS_NO_SORT')} to be Array, instead got: #{Blufin::Terminal::format_invalid(parameters_no_sort.class)}"
345
329
  end
346
330
  end
331
+ # Validate template matchers.
332
+ unless file_cloudformation.nil?
333
+ results = Blufin::Replacer::scan_file(file_cloudformation)
334
+ # Handle errors first.
335
+ validate_matchers(file_cloudformation, results, template_name, parameters)
336
+ end
347
337
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 The #{Blufin::Terminal::format_highlight('template.yml')}\x1B[38;5;240m is missing, empty or invalid." if file_cloudformation.nil?
348
338
  @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 The #{Blufin::Terminal::format_highlight('template.rb')}\x1B[38;5;240m is missing, empty or invalid." if file_ruby.nil?
349
339
  @templates[category] = {} unless @templates.has_key?(category)
@@ -391,8 +381,8 @@ module AppCommand
391
381
 
392
382
  def opts_routing
393
383
 
394
- used_cache = true
395
- showing_tags = false
384
+ used_cache = true
385
+ showing_tags = false
396
386
 
397
387
  # Show prompt to select template.
398
388
  category, template, @template = select_template_prompt
@@ -455,11 +445,19 @@ module AppCommand
455
445
  capabilities_arr = []
456
446
  capabilities_str = nil
457
447
 
448
+ # Replace matchers in CloudFormation Template before uploading to S3.
449
+ source = File.expand_path("#{App::AWSCloudFormation::get_cloudformation_path}/#{category}/#{template}/template.yml")
450
+ raise RuntimeError, "File does not exist: #{source}" unless Blufin::Files::file_exists(source)
451
+ file_lines = Blufin::Replacer::replace_yml(source, get_replacer_params(category, template))
452
+ tmp_file = "/tmp/converted-template-#{Blufin::Strings::random_string(4)}.txt"
453
+ Blufin::Files::write_file(tmp_file, file_lines)
454
+
458
455
  # Upload the template to S3.
459
- s3_url = App::AWSCloudFormation::upload_cloudformation_template(category, template, @params[OPTION_DESCRIPTION])
456
+ s3_url = App::AWSCloudFormation::upload_cloudformation_template(tmp_file, category, template)
457
+ system("rm #{tmp_file}")
460
458
 
461
459
  # Validates the template.
462
- validation = App::AWSCli::cloudformation_stack_validate(@params[OPTION_REGION], s3_url)
460
+ validation = App::AWSCli::cloudformation_stack_validate(@params[OPTION_REGION], s3_url)
463
461
 
464
462
  # Check if validation output is JSON (and output appropriate format).
465
463
  begin
@@ -604,8 +602,9 @@ module AppCommand
604
602
  raise RuntimeError, "Template with category: #{category} and Id: #{template} does not exist." unless @templates.has_key?(category) && @templates[category].has_key?(template)
605
603
  Blufin::Terminal::error("Template: #{Blufin::Terminal::format_highlight(@templates[category][template][:name])} is currently broken/incomplete.", 'Please fix error(s) and try again.', true) if @templates[category][template][:broken]
606
604
  end
607
- @params = {}
608
- @template = @templates[category][template].dup
605
+ @params = {}
606
+ @params_system = nil
607
+ @template = @templates[category][template].dup
609
608
  system('clear')
610
609
  @template[:parameters] = {} if !@template.has_key?(:parameters) || !@template[:parameters].any?
611
610
  # Show summary (with intro if exists).
@@ -671,14 +670,12 @@ module AppCommand
671
670
  OPTION_DESCRIPTION => 'Should this stack be protected against accidental Termination?'
672
671
  }
673
672
  end
674
- # If one of the parameters is EC2UserData, this parameter gets handled differently.
675
- if @template[:parameters].is_a?(Hash) && @template[:parameters].has_key?(EC2_USER_DATA)
676
- @template[:parameters][EC2_USER_DATA][OPTION_DESCRIPTION] = "#{EC2_USER_DATA_FILE} (base64 encoded & sent automatically)"
677
- end
678
673
  # Get cached values (if exist and parameters haven't changed).
679
674
  # Even a one-character change in a description will invalidate the cache.
680
675
  cache_file = get_cache_file(category, template)
681
- @cache = {}
676
+ # Remove cache file (if --clear-cache flag is set).
677
+ Blufin::Terminal::execute("rm #{cache_file}", verbose: false) if @opts[:clear_cache] && Blufin::Files::file_exists(cache_file)
678
+ @cache = {}
682
679
  if Blufin::Files::file_exists(cache_file)
683
680
  @cache = nil
684
681
  @cache_valid = false
@@ -697,13 +694,13 @@ module AppCommand
697
694
  table_data = []
698
695
  # Loop again to build output.
699
696
  @template[:parameters].each do |param_name, param_data|
700
- if @auto_fetch_resources.has_key?(param_name)
697
+ if @lookups.has_key?(param_name)
701
698
  params_to_fetch << param_name
702
699
  params_to_fetch_colored << "\x1B[38;5;61m#{param_name}\x1B[38;5;208m"
703
700
  end
704
701
  ti = (param_data.has_key?(OPTIONS) ? :system : :normal)
705
702
  table_data << {
706
- :type_internal => @auto_fetch_resources.has_key?(param_name) ? :autocomplete : ti,
703
+ :type_internal => @lookups.has_key?(param_name) ? :autocomplete : ti,
707
704
  :parameter => param_name,
708
705
  :type => %w(string number boolean).include?(param_data['Type'].downcase) ? param_data['Type'] : SPECIAL,
709
706
  :description => param_data.has_key?('Description') ? param_data['Description'] : "\xe2\x80\x94",
@@ -728,7 +725,7 @@ module AppCommand
728
725
  params_to_fetch.each do |ptf|
729
726
  sleep(0.01)
730
727
  threads << Thread.new {
731
- options = fetch_autocomplete_options(ptf, silent: true)
728
+ options, multi = fetch_autocomplete_options(ptf, silent: true)
732
729
  if options.nil? || !options.any?
733
730
  empty_options << "\x1B[38;5;196m#{ptf}\x1B[0m \xe2\x80\x94 \x1B[38;5;240mFound 0 result(s)."
734
731
  end
@@ -742,15 +739,13 @@ module AppCommand
742
739
  end
743
740
  # If we have any auto-complete options that come back empty, we show an error and disable certain options.
744
741
  if empty_options.any?
745
- Blufin::Terminal::error("Cannot currently use this template because some #{Blufin::Terminal::format_highlight('required resources')} don't exist in \x1B[38;5;231m\x1B[48;5;17m AWS \x1B[0m :", empty_options, false)
746
- puts
747
- choices = []
742
+ Blufin::Terminal::error("Cannot currently use this template because empty #{Blufin::Terminal::format_highlight('required resource(s)')} were detected.", empty_options, true, false)
748
743
  else
749
- choices = [{value: 'y', text: 'Select this template'}]
750
- choices << {value: 'Y', text: "Select this template \x1B[38;5;198m(and apply cached values)\x1B[0m"} if @cache_valid
744
+ choices = [{ value: 'y', text: 'Select this template' }]
745
+ choices << { value: 'Y', text: "Select this template \x1B[38;5;198m(and apply cached values)\x1B[0m" } if @cache_valid
751
746
  end
752
747
  # The prompt at the end of the intro.
753
- choices << {value: 'n', text: "\x1B[38;5;240m#{Blufin::Strings::RETURN_CHARACTER}\x1B[0m"}
748
+ choices << { value: 'n', text: "\x1B[38;5;240m#{Blufin::Strings::RETURN_CHARACTER}\x1B[0m" }
754
749
  choice = Blufin::Terminal::prompt_select('What would you like to do?', choices)
755
750
  case choice
756
751
  when 'y'
@@ -772,17 +767,13 @@ module AppCommand
772
767
  def get_parameter_value(param_data, param_name, category, template)
773
768
  description = param_data.has_key?(OPTION_DESCRIPTION) ? param_data[OPTION_DESCRIPTION] : nil
774
769
  options_text = "Select #{param_name}:"
770
+ replace_hash = get_replacer_params(category, template)
775
771
  if [OPTION_STACK_NAME].include?(param_name)
776
772
  constraints = []
777
773
  constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
778
774
  constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
779
775
  default = @template[:stack_name]
780
- default = default.gsub(/{{CATEGORY}}/i, category) if default =~ /{{CATEGORY}}/i
781
- default = default.gsub(/{{TEMPLATE}}/i, template) if default =~ /{{TEMPLATE}}/i
782
- default = default.gsub(/{{PROJECT}}/i, @params[OPTION_PROJECT]) if default =~ /{{PROJECT}}/i
783
- default = default.gsub(/{{ENVIRONMENT}}/i, @params[OPTION_ENVIRONMENT]) if default =~ /{{ENVIRONMENT}}/i
784
- default = default.gsub(/{{REGION}}/i, @params[OPTION_REGION]) if default =~ /{{REGION}}/i
785
- default = default.gsub(/{{UUID}}/i, random_stack_suffix) if default =~ /{{UUID}}/i
776
+ default = Blufin::Replacer::replace_string(default, replace_hash)
786
777
  default = default.downcase
787
778
  help_text = 'Stack Name (will be displayed in CloudFormation console).'
788
779
  return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: help_text)
@@ -790,28 +781,33 @@ module AppCommand
790
781
  constraints = []
791
782
  constraints << "\x1B[38;5;240mMinLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MinLength']}" if param_data.has_key?('MinLength')
792
783
  constraints << "\x1B[38;5;240mMaxLength: \x1B[38;5;#{App::AWSOutputter::CONSTRAINT_COLOR}m#{param_data['MaxLength']}" if param_data.has_key?('MaxLength')
793
- default = param_data.has_key?(DEFAULT) ? param_data[DEFAULT] : nil
794
- unless default.nil?
795
- # Check if description has any replaceable values.
796
- default = replace_matchers_with_params(default, @params)
797
- end
784
+ default = param_data.has_key?(DEFAULT) ? param_data[DEFAULT] : nil
785
+ default = Blufin::Replacer::replace_string(default, replace_hash)
798
786
  help_text = 'Description (will be displayed in CloudFormation console).'
799
787
  return Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: help_text)
800
- elsif param_name == EC2_USER_DATA
801
- puts Blufin::Terminal::display_prompt_text(EC2_USER_DATA, EC2_USER_DATA_FILE)
802
- return "#{@template[:path]}/#{EC2_USER_DATA_FILE}"
803
788
  elsif param_name == OPTION_TIMEOUT
804
789
  return param_data[DEFAULT]
805
790
  elsif param_name == OPTION_TERM_PROTECT
806
791
  # Basically, by default Termination Protection is off (because most of the time your just testing and want to press Enter).
807
792
  return !Blufin::Terminal::prompt_yes?("Allow accidental Termination? (type 'n' to enable Termination Protection)")
808
- elsif @auto_fetch_resources.has_key?(param_name)
793
+ elsif @lookups.has_key?(param_name)
809
794
  # Sort alphabetically.
810
- options = fetch_autocomplete_options(param_name, silent: true)
795
+ options, multi = fetch_autocomplete_options(param_name, silent: true)
811
796
  options.sort_by! { |hsh| hsh[:sort] }
812
797
  # If we have a cached value, make that the first in the options list.
813
798
  options = move_default_option_to_top(options, param_name) if @cache.has_key?(param_name)
814
- return Blufin::Terminal::prompt_select(options_text, options, help: description)
799
+ if multi && options.length > 1
800
+ selected_values = []
801
+ selected_values = Blufin::Terminal::prompt_multi_select(options_text, options, help: description) until selected_values.any?
802
+ return selected_values
803
+ else
804
+ if options.length == 1
805
+ puts Blufin::Terminal::display_prompt_text(options_text, options[0][:text])
806
+ return multi ? [options[0][:value]] : options[0][:value]
807
+ else
808
+ return Blufin::Terminal::prompt_select(options_text, options, help: description)
809
+ end
810
+ end
815
811
  elsif param_data.has_key?(OPTIONS) || param_data.has_key?('AllowedValues')
816
812
  options = param_data[OPTIONS]
817
813
  options = param_data['AllowedValues'] if param_data.has_key?('AllowedValues')
@@ -829,7 +825,13 @@ module AppCommand
829
825
  # If we have a cached value, make that the first in the options list.
830
826
  options = move_default_option_to_top(options, param_name) if @cache.has_key?(param_name)
831
827
  puts Blufin::Terminal::display_prompt_help(param_data[OPTION_DESCRIPTION]) if param_data.has_key?(OPTION_DESCRIPTION)
832
- return Blufin::Terminal::prompt_select(options_text, options)
828
+ if param_data['Type'] == 'CommaDelimitedList'
829
+ selected_values = []
830
+ selected_values = Blufin::Terminal::prompt_multi_select(options_text, options) until selected_values.any?
831
+ return selected_values
832
+ else
833
+ return Blufin::Terminal::prompt_select(options_text, options)
834
+ end
833
835
  end
834
836
  else
835
837
  constraints = []
@@ -842,10 +844,7 @@ module AppCommand
842
844
  if default.nil?
843
845
  default = @cache[param_name] if @cache.has_key?(param_name)
844
846
  else
845
- if default =~ /{{(System):([A-Za-z0-9]+)}}/
846
- matches = default.scan(/{{(System):([A-Za-z0-9]+)}}/)
847
- default = replace_with_dynamic_data(default, matches) if matches.any?
848
- end
847
+ default = Blufin::Replacer::replace_string(default, replace_hash)
849
848
  end
850
849
  loop do
851
850
  value = Blufin::Terminal::prompt_ask("Enter #{param_name}#{render_constraints(constraints)}", default: default, help: description)
@@ -868,35 +867,50 @@ module AppCommand
868
867
  puts
869
868
  end
870
869
 
871
- # TODO - Think about possible combining both of the below methods?
872
-
873
- # Takes something like "{{System:RandomString}}" and returns "aif8d5njk".
874
- # @return string
875
- def replace_with_dynamic_data(original_string, matcher_pairs)
876
- raise RuntimeError, "Expected Array, instead got: #{matcher_pairs.class}" unless matcher_pairs.is_a?(Array)
877
- matcher_pairs.each do |pair|
878
- matcher = "#{pair[0]}:#{pair[1]}"
879
- case matcher
880
- when 'System:RandomString'
881
- original_string = original_string.gsub('{{System:RandomString}}', Blufin::Strings::random_string)
882
- else
883
- raise RuntimeError, "Unrecognized matcher: #{Blufin::Terminal::format_invalid(matcher)}"
870
+ # Recursive function to validate matchers.
871
+ # @return void
872
+ def validate_matchers(file_cloudformation, results, template_name, parameters)
873
+ if results[:errors].any?
874
+ results[:errors].each do |error|
875
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid matcher found in template: #{Blufin::Terminal::format_invalid(error)}"
884
876
  end
885
877
  end
886
- original_string
887
- end
888
-
889
- # Takes a string like: '{{Subdomain}}.{{Route53DomainName}}' and converts it to -> nuxt-v1.blufin.org
890
- # @return void
891
- def replace_matchers_with_params(default, params)
892
- unless default.nil? || !default.is_a?(String)
893
- matches = default.scan(/{{[A-Za-z0-9]+}}/)
894
- matches.each do |match|
895
- match = match.gsub(/^{{/, '').gsub(/}}$/, '')
896
- default = default.gsub(/{{#{match}}}/, params[match]) if params.has_key?(match)
878
+ # Still validate matchers regardless (in case there are more errors).
879
+ if results[:matchers].any?
880
+ results[:matchers].each do |key, matchers|
881
+ raise RuntimeError, "Expected Array, but got #{matchers.class}" unless matchers.is_a?(Array)
882
+ matchers.each do |m|
883
+ matcher_raw = "${{#{key}:#{m[:key]}#{m.has_key?(:modifier) ? ":#{m[:modifier]}" : ''}}}"
884
+ begin
885
+ case key
886
+ when PARAMETERS
887
+ valid_keys = parameters.keys.push(*[
888
+ OPTION_CATEGORY,
889
+ OPTION_TEMPLATE,
890
+ OPTION_PROJECT,
891
+ OPTION_ENVIRONMENT,
892
+ OPTION_REGION
893
+ ])
894
+ raise RuntimeError unless valid_keys.include?(m[:key])
895
+ when SYSTEM
896
+ raise RuntimeError unless %w(UUID-8 UUID-16 UUID-AWX).include?(m[:key])
897
+ when 'file'
898
+ file_path = "#{Blufin::Files::extract_path_name(file_cloudformation)}/#{m[:key]}"
899
+ unless Blufin::Files::file_exists(file_path)
900
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 File from matcher not found: #{Blufin::Terminal::format_invalid(file_path)}"
901
+ next
902
+ end
903
+ results_inner = Blufin::Replacer::scan_file(file_path)
904
+ validate_matchers(file_path, results_inner, template_name, parameters)
905
+ else
906
+ raise RuntimeError
907
+ end
908
+ rescue
909
+ @warnings << "\x1B[38;5;196m#{template_name}\x1B[38;5;240m \xe2\x80\x94 Invalid matcher found in template: #{Blufin::Terminal::format_invalid(matcher_raw)}"
910
+ end
911
+ end
897
912
  end
898
913
  end
899
- default
900
914
  end
901
915
 
902
916
  # Moves the default option to the top of the list.
@@ -918,14 +932,27 @@ module AppCommand
918
932
  # Goes off to AWS and gets values for autocomplete supported fields.
919
933
  # @return string
920
934
  def fetch_autocomplete_options(resource_name, silent: true)
921
- raise RuntimeError, "Key not found in @auto_fetch_resources: #{resource_name}" unless @auto_fetch_resources.has_key?(resource_name)
922
- return @auto_fetch_cache[resource_name] if @auto_fetch_cache.has_key?(resource_name)
923
- resource_title = @auto_fetch_resources[resource_name][:resource_title]
924
- resource = @auto_fetch_resources[resource_name][:resource]
925
- results = App::AWSReports::get_aws_data(@regions, resource, resource_title, silent: silent)
926
- options = App::AWSReports::parse_results_for_prompt(resource, resource_name, results)
927
- @auto_fetch_cache[resource_name] = options
928
- options
935
+ raise RuntimeError, "Key not found in @lookups: #{resource_name}" unless @lookups.has_key?(resource_name)
936
+ return @lookup_cache[resource_name] if @lookup_cache.has_key?(resource_name)
937
+ multi = false
938
+ if resource_name == SSH_USERS
939
+ multi = true
940
+ options = []
941
+ @lookups[SSH_USERS].each do |user, pub_key|
942
+ options << {
943
+ :value => pub_key,
944
+ :text => user,
945
+ :sort => user
946
+ }
947
+ end
948
+ else
949
+ resource_title = @lookups[resource_name][:resource_title]
950
+ resource = @lookups[resource_name][:resource]
951
+ results = App::AWSReports::get_aws_data(@regions, resource, resource_title, silent: silent)
952
+ options = App::AWSReports::parse_results_for_prompt(resource, resource_name, results)
953
+ end
954
+ @lookup_cache[resource_name] = options, multi
955
+ return options, multi
929
956
  end
930
957
 
931
958
  # Returns short-hand syntax for tags.
@@ -957,17 +984,17 @@ module AppCommand
957
984
  def assemble_params(params)
958
985
  output = []
959
986
  params.each do |key, value|
960
- if key == EC2_USER_DATA
961
- b64_cmd = Blufin::Tools::value_based_on_os(mac: "openssl base64 -in #{value}", linux: "base64 -w0 #{value}")
962
- result = Blufin::Terminal::command_capture(b64_cmd, nil, nil, nil)[0]
963
- result = result.split("\n").join('')
964
- output << {
965
- 'ParameterKey' => key,
966
- 'ParameterValue' => result
967
- }
968
- next
969
- end
970
987
  unless RESERVED_WORDS.include?(key.downcase)
988
+ # If this is a list, produce comma delimited output.
989
+ if @template[:parameters].has_key?(key)
990
+ if %w(List<Number> CommaDelimitedList).include?(@template[:parameters][key]['Type'])
991
+ value_comma_delimited = ''
992
+ value.each do |val|
993
+ value_comma_delimited = "#{value_comma_delimited},#{val.gsub(',', '\,')}"
994
+ end
995
+ value = value_comma_delimited[1..-1]
996
+ end
997
+ end
971
998
  output << {
972
999
  'ParameterKey' => key,
973
1000
  'ParameterValue' => value
@@ -1016,10 +1043,35 @@ module AppCommand
1016
1043
  constraints.any? ? " \x1B[38;5;#{constraint_bracket_color}m[ #{constraints.join("\x1B[38;5;240m | ")} \x1B[38;5;#{constraint_bracket_color}m] \x1B[38;5;214m:" : ':'
1017
1044
  end
1018
1045
 
1046
+ # Returns system params (such as -> System:UUID-AWX for example)
1047
+ # @return Hash
1048
+ def get_system_params
1049
+ if @params_system.nil?
1050
+ @params_system = {
1051
+ 'UUID-8' => Blufin::Strings::random_string(1),
1052
+ 'UUID-16' => Blufin::Strings::random_string(2),
1053
+ 'UUID-AWX' => random_stack_suffix,
1054
+ }
1055
+ end
1056
+ @params_system
1057
+ end
1058
+
1059
+ # Returns replacer params, generated new for each call.
1060
+ # @return Hash
1061
+ def get_replacer_params(category, template)
1062
+ params = @params
1063
+ params[OPTION_CATEGORY] = category
1064
+ params[OPTION_TEMPLATE] = template
1065
+ {
1066
+ PARAMETERS => params,
1067
+ SYSTEM => get_system_params
1068
+ }
1069
+ end
1070
+
1019
1071
  # Generates a random stack suffix that always has the same patters: a[a-z0-9]w[a-z0-9]x
1020
1072
  # @return string
1021
1073
  def random_stack_suffix
1022
- "a#{Blufin::Strings::random_string}10w17#{Blufin::Strings::random_string}x"
1074
+ "a#{Blufin::Strings::random_string(2)}10w17#{Blufin::Strings::random_string(2)}x"
1023
1075
  end
1024
1076
 
1025
1077
  # Replaces current random stack suffix with another one.
@@ -84,7 +84,7 @@ module AppCommand
84
84
 
85
85
  # Display metadata and exit (if -m flag is set)
86
86
  if @opts[:metadata]
87
- App::AWSReports::get_auto_fetch_resources(@data).each do |title, data|
87
+ App::AWSReports::get_lookups(@data).each do |title, data|
88
88
  output = []
89
89
  output << "\x1B[38;5;240m AWS-CLI Command:\x1B[38;5;106m #{data[:resource][App::AWSReports::KEY_CLI]['command']}" if data[:resource].has_key?(App::AWSReports::KEY_CLI) && data[:resource][App::AWSReports::KEY_CLI].has_key?('command')
90
90
  output << "\x1B[38;5;240m Region(s):\x1B[38;5;94m #{data[:resource][App::AWSReports::KEY_REGIONS].join(', ')}" if data[:resource].has_key?(App::AWSReports::KEY_REGIONS)
@@ -1 +1 @@
1
- AWX_VERSION = '0.5.0'
1
+ AWX_VERSION = '0.5.1'
@@ -67,6 +67,18 @@ mapping:
67
67
  Timeout:
68
68
  type: int
69
69
  required: yes
70
+ SSHKeys:
71
+ type: map
72
+ mapping:
73
+ S3Bucket:
74
+ type: map
75
+ mapping:
76
+ Name:
77
+ required: yes
78
+ Path:
79
+ required: yes
80
+ Region:
81
+ required: yes
70
82
  Projects:
71
83
  type: map
72
84
  mapping:
@@ -20,6 +20,11 @@ Profiles:
20
20
  Timeout: 60
21
21
  Regions:
22
22
  - us-west-2
23
+ SSHKeys:
24
+ S3Bucket:
25
+ Name: <<-S3-bucket-name->>
26
+ Path: <<-S3-bucket-path->>
27
+ Region: <<-S3-bucket-region->>
23
28
  Projects:
24
29
  Local:
25
30
  File: <<-path/to/file->>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awx
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Albert Rannetsperger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-10 00:00:00.000000000 Z
11
+ date: 2019-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: blufin-lib