awx 0.5.0 → 0.5.1

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