fum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,301 @@
1
+ require 'formatador'
2
+ require 'formatador/table'
3
+ require 'json'
4
+
5
+ module Fum
6
+ module Commands
7
+ class Template < Fum::Command
8
+
9
+ def parse_options
10
+ options = Trollop::options do
11
+ banner "usage: template [options] <environment-id>, where options are:"
12
+ opt :options, "Describe Configuration Options"
13
+ opt :json, "Output in JSON format"
14
+ opt :application, "Application name (for creating templates only)", :type => :string
15
+ opt :description, "Description (update/create only)", :type => :string
16
+ #opt :prompt, "Prompt for options when updating or creating"
17
+ opt :stack, "Use the specified solution stack.", :type => :string
18
+ opt :create, "Create the specified template"
19
+ opt :update, "Update the specified template"
20
+ opt :delete, "Delete the specified template"
21
+ opt :list, "List all templates with some details"
22
+ opt :settings, "Print the current settings of the template"
23
+ opt :compare, "Compares two or more templates"
24
+ opt :from_template, "Base template to use when creating a new template", :type => :string
25
+ opt :from_json, "JSON File to use for create or update", :type => :string
26
+ opt :from_application, "Application to use for source template", :type => :string
27
+ end
28
+ if options[:options] || options[:settings] || options[:create] || options[:update] || options[:delete]
29
+ if ARGV.empty?
30
+ die "Please specify a template name for this operation"
31
+ else
32
+ options[:template_name] = ARGV.shift
33
+ end
34
+ elsif options[:compare]
35
+ die "Please specify two templates to compare" unless ARGV.length >= 2
36
+ options[:template_names] = ARGV.dup
37
+ end
38
+
39
+
40
+ options
41
+ end
42
+
43
+ #
44
+ #
45
+ # * :type
46
+ def execute(options)
47
+ app = @application.main_decl
48
+
49
+ # TODO make sure only one of these is is set, for now do least destructive order.
50
+ if options[:settings] || options[:update] || options[:options] || options[:delete]
51
+
52
+ template = Fog::AWS[:beanstalk].templates.get(app.name, options[:template_name])
53
+ die "No configuration template named #{options[:template_name]}" if template.nil?
54
+
55
+ if options[:settings]
56
+ display_settings(template.option_settings, options)
57
+ elsif options[:options]
58
+ display_options(template.options, options)
59
+ elsif options[:update]
60
+ update_template(template, options)
61
+ elsif options[:delete]
62
+ template.destroy
63
+ puts "Deleted template #{template.name}."
64
+ end
65
+ elsif options[:create]
66
+ create_template(options)
67
+ elsif options[:compare]
68
+ compare_settings(options)
69
+ elsif options[:list]
70
+ list_templates(options)
71
+ elsif options[:stack]
72
+ begin
73
+ config_opts = Fog::AWS[:beanstalk].describe_configuration_options('SolutionStackName' => options[:stack])
74
+ config_opts = config_opts.body['DescribeConfigurationOptionsResult']['Options']
75
+ display_options(config_opts, options)
76
+ rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
77
+ die ex.message
78
+ end
79
+ end
80
+
81
+ end
82
+
83
+ def list_templates(options)
84
+ templates = Fog::AWS[:beanstalk].templates
85
+
86
+ templates = templates.sort_by { |a| [ a.application_name, a.name ] }
87
+
88
+ table = []
89
+ templates.each { |template|
90
+ table << {
91
+ 'name' => template.name,
92
+ 'application' => template.application_name,
93
+ 'description' => template.description,
94
+ 'created' => template.created_at,
95
+ 'updated' => template.updated_at
96
+ }
97
+ }
98
+ Formatador.display_compact_table(table, %w(application name description created updated))
99
+ end
100
+
101
+ def display_settings(values, options)
102
+ values = values.sort_by { |a| [ a["Namespace"], a["OptionName"]]}
103
+ if options[:json]
104
+ # Prune nil values in JSON, or we can't update from JSON
105
+ values = values.select { |v| !v["Value"].nil? }
106
+ puts JSON.pretty_generate(values)
107
+ else
108
+ values.each { |value|
109
+ value['Namespace'] = "[bold]#{value['Namespace']}[/]"
110
+ }
111
+ Formatador.display_compact_table(values, %w(Namespace OptionName Value))
112
+ end
113
+ end
114
+
115
+ def compare_settings(options)
116
+ templates = Hash.new{ |h,k| h[k] = Hash.new(&h.default_proc) }
117
+ table = []
118
+ all_names = {}
119
+ columns = %w(Namespace Name)
120
+
121
+ options[:template_names].each { |name|
122
+ template = Fog::AWS[:beanstalk].templates.get(@application.main_decl.name, name)
123
+ die "No configuration template named #{name}" if template.nil?
124
+ settings = template.option_settings #.sort_by { |a| [ a["Namespace"], a["OptionName"]]}
125
+ #templates[name] = settings
126
+
127
+ settings.each { |setting|
128
+ namespace = setting['Namespace']
129
+ option_name = setting['OptionName']
130
+
131
+ # Maintain a list of all unique namespace/name combinations
132
+ names = all_names[setting['Namespace']] || []
133
+ names << setting['OptionName'] unless names.include?(setting['OptionName'])
134
+ all_names[setting['Namespace']] = names
135
+
136
+ # Build Index for comparison later
137
+
138
+ templates[name][namespace][option_name] = setting['Value']
139
+ }
140
+
141
+ columns << name
142
+ }
143
+
144
+ all_names.each { |namespace, option_names|
145
+
146
+ option_names.each { |option_name|
147
+ line = {
148
+ 'Namespace' => namespace,
149
+ 'Name' => option_name
150
+ }
151
+
152
+ values = {}
153
+ prev_value = :init
154
+ values_differ = false
155
+
156
+ options[:template_names].each { |template_name|
157
+ value = templates[template_name][namespace][option_name]
158
+
159
+ if !values_differ && prev_value != :init
160
+ values_differ = value != prev_value
161
+ prev_value = value
162
+ end
163
+ prev_value = value if prev_value == :init
164
+
165
+ values[template_name] = value
166
+ }
167
+
168
+ if values_differ
169
+ values.each { |key, value|
170
+ values[key] = "[bold]#{value.to_s}[/]"
171
+ }
172
+ end
173
+
174
+ table << line.merge(values)
175
+ }
176
+ }
177
+
178
+ table.sort_by { |a| [ a["Namespace"], a["Name"]]}
179
+
180
+ Formatador.display_compact_table(table, columns)
181
+
182
+
183
+ end
184
+
185
+ def display_options(values, options)
186
+ values = values.sort_by { |a| [ a["Namespace"], a["Name"]]}
187
+
188
+ if options[:json]
189
+ puts JSON.pretty_generate(values)
190
+ else
191
+ values.each { |value|
192
+ constraints = ''
193
+ if value['MinValue'] && value['MaxValue']
194
+ constraints = "Range(#{value['MinValue']}-#{value['MaxValue']})"
195
+ elsif value['ValueOptions']
196
+ type = value['ValueType'] == 'List' ? 'List' : 'Scalar'
197
+ constraints = "#{type}(#{value['ValueOptions'].join(', ')})"
198
+ elsif value['Regex']
199
+ constraints = "Regex(#{value['Regex']['Label']} = #{value['Regex']['Pattern']})"
200
+ elsif value['ValueType'] == 'Boolean'
201
+ constraints = "Boolean(true, false)"
202
+ end
203
+
204
+ if value['MaxLength']
205
+ max_length_constraint = "MaxLength(#{value['MaxLength']})"
206
+ constraints += ', ' unless constraints.empty?
207
+ constraints += max_length_constraint
208
+ end
209
+ value['Constraints'] = constraints
210
+ }
211
+ Formatador.display_compact_table(values, %w(Namespace Name DefaultValue Constraints))
212
+ end
213
+
214
+ end
215
+
216
+ def create_template(options)
217
+
218
+ die "Must specify application name " if options[:application].nil?
219
+
220
+ create_opts = {
221
+ :name => options[:template_name],
222
+ :application_name => options[:application]
223
+ }
224
+
225
+ create_opts[:solution_stack_name] = options[:stack] if options[:stack]
226
+ create_opts[:description] = options[:description] unless options[:description].nil?
227
+ create_opts[:source_configuration] = {
228
+ 'ApplicationName' => options[:from_application].nil? ? options[:application] : options[:from_application],
229
+ 'TemplateName' => options[:from_template]
230
+ } if options[:from_template]
231
+
232
+ begin
233
+ template = Fog::AWS[:beanstalk].templates.create(create_opts)
234
+ puts "Created template #{template.name}"
235
+ rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
236
+ die ex
237
+ end
238
+
239
+ end
240
+
241
+ def update_template(template, options)
242
+
243
+ settings = []
244
+ if options[:from_json]
245
+ settings = JSON.parse(File.read(options[:from_json]))
246
+ # TODO add some sanity checks, verify array of hashes, etc.
247
+ end
248
+ new_attributes = {
249
+ :option_settings => settings
250
+ }
251
+ new_attributes[:description] = options[:description] unless options[:description].nil?
252
+
253
+ begin
254
+ template.modify(new_attributes)
255
+ puts "Updated template #{template.name}"
256
+ rescue Fog::AWS::ElasticBeanstalk::InvalidParameterError => ex
257
+ die ex
258
+ end
259
+
260
+ end
261
+
262
+ private
263
+
264
+ # Work in progress on prompting for options
265
+ def prompt_options(template, options)
266
+ require 'highline/import'
267
+ opts = template.options.sort_by { |a| [ a["Namespace"], a["Name"]]}
268
+ option_settings = template.option_settings
269
+
270
+ opts.each { |option|
271
+
272
+ answer_type = nil
273
+ if option['MinValue'] && option['MaxValue']
274
+ answer_type = Integer
275
+ elsif option['ValueOptions']
276
+ #type = value['ValueType'] == 'List' ? 'List' : 'Scalar'
277
+ answer_type = option['ValueOptions']
278
+ answer_type << ""
279
+ elsif option['Regex']
280
+ #answer_type = "Regex(#{value['Regex']['Label']} = #{value['Regex']['Pattern']})"
281
+ elsif option['ValueType'] == 'Boolean'
282
+ answer_type = %w(true false)
283
+ end
284
+
285
+ value = ask("#{option['Namespace']}:#{option['Name']} ? ", answer_type) { |q|
286
+ setting = option_settings.select { |a| a['OptionName'] == option['Name'] && a['Namespace'] == option['Namespace']}.shift
287
+ q.default = setting['Value'] unless setting.nil?
288
+ q.readline = true
289
+ if option['ValueType'] == 'List'
290
+ q.gather = ''
291
+ end
292
+
293
+ }
294
+ #pp value
295
+ }
296
+ end
297
+
298
+
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,54 @@
1
+ module Fum
2
+ module Commands
3
+ class Terminate < Fum::Command
4
+
5
+ def parse_options
6
+ opts = Trollop::options do
7
+ banner "usage: terminate [options] <stage>, where options are:"
8
+ opt :all, "Terminate all environments for this stage (use with caution)."
9
+ end
10
+ if ARGV.empty?
11
+ die "Please specify a stage name type to terminate."
12
+ end
13
+ opts[:stage_name] = ARGV.shift
14
+ opts
15
+ end
16
+
17
+ #
18
+ #
19
+ # * :type
20
+ def execute(options)
21
+ stage_name = options[:stage_name]
22
+ stage_decl = stage(@application.main_decl, stage_name)
23
+
24
+ analyzer = StageAnalyzer.new(stage_decl)
25
+
26
+ analyzer.analyze(options)
27
+
28
+ env_info = analyzer.env_map.values
29
+
30
+ targets = []
31
+
32
+ if options[:all]
33
+ targets = env_info.map { |e| e[:env] }
34
+ else
35
+ targets = env_info.select { |e| e[:state] == :inactive }.map { |e| e[:env] }
36
+ end
37
+
38
+ if targets.length > 0
39
+ targets.each { |target|
40
+ if target.ready?
41
+ puts "Terminating inactive environment #{target.name}."
42
+ target.destroy
43
+ end
44
+
45
+ }
46
+ else
47
+ puts "No environments to terminate."
48
+ end
49
+
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,135 @@
1
+ module Fum
2
+
3
+ # Mixin for common DNS methods
4
+ module DNS
5
+
6
+ require 'fog'
7
+
8
+ def dns
9
+ Fog::DNS[:AWS]
10
+ end
11
+
12
+ def update_zones(stage_decl, env, options)
13
+ zones = stage_decl.zones
14
+
15
+ hosted_zone_name_id = nil
16
+ dns_name = nil
17
+
18
+ unless options[:noop]
19
+ lb = env.load_balancer
20
+
21
+ hosted_zone_name_id = lb.hosted_zone_name_id
22
+ dns_name = lb.dns_name
23
+ end
24
+
25
+ zones.each { |zone| update_zone(zone, hosted_zone_name_id, dns_name, env.cname, options) }
26
+
27
+ end
28
+
29
+ def update_zone(zone_decl, hosted_zone_name_id, dns_name, env_cname, options)
30
+ dns = Fog::DNS[:aws]
31
+
32
+ zone = dns.zones.all({:domain => zone_decl.name}).shift
33
+ die "Could not find zone #{zone_decl.name} in account." unless zone
34
+
35
+ puts "Updating records in zone #{zone.domain}"
36
+ create_list = []
37
+ modify_list = []
38
+
39
+ zone_decl.records.each { |record|
40
+ fqdn = "#{record[:name]}.#{zone_decl.name}."
41
+
42
+ create_opts = {
43
+ :name => fqdn,
44
+ :type => record[:type]
45
+ }
46
+
47
+ case record[:type]
48
+ when 'CNAME'
49
+ create_opts[:value] = case record[:target]
50
+ when :elb
51
+ ensure_trailing_dot(dns_name)
52
+ when :env
53
+ ensure_trailing_dot(env_cname)
54
+ end
55
+ when 'A'
56
+ create_opts[:alias_target] = {
57
+ :hosted_zone_id => hosted_zone_name_id,
58
+ :dns_name => dns_name
59
+ }
60
+ else
61
+ raise RuntimeError, "Unknown type #{record[:type]}"
62
+ end
63
+
64
+ existing = find_records(zone, fqdn)
65
+
66
+ if existing.length > 1
67
+ # We do not currently handle this case, which would occur if AAAA records exist or weighted/latency records used.
68
+ puts "Cannot update record #{fqdn} in zone #{zone} because more than one A, AAAA, or CNAME record already exists."
69
+ end
70
+
71
+ if existing.length == 0
72
+ create_list << create_opts
73
+ else
74
+ modify_list << {
75
+ :record => existing.shift,
76
+ :create_opts => create_opts
77
+ }
78
+ end
79
+
80
+ }
81
+
82
+ # We do not currently do this atomically, but one record at a time. Should probably move to atomic at some
83
+ # point.
84
+
85
+ new_records = []
86
+
87
+ create_list.each { |create_options|
88
+ puts "Creating #{create_options[:type]} record with name #{create_options[:name]} in zone #{zone.domain}"
89
+ unless options[:noop]
90
+ new_records << zone.records.create(create_options)
91
+ end
92
+ }
93
+
94
+ modify_list.each { |record|
95
+ puts "Updating #{record[:create_opts][:type]} record with name #{record[:create_opts][:name]} in zone #{zone.domain}"
96
+ unless options[:noop]
97
+ record[:record].modify(record[:create_opts])
98
+ new_records << record[:record]
99
+ end
100
+ }
101
+
102
+ puts "Waiting for DNS records to sync in zone #{zone.domain}..."
103
+ # Wait for records to be ready.
104
+ new_records.each { |record|
105
+ record.wait_for { record.ready? }
106
+ }
107
+ puts "Updated records are now in sync in zone #{zone.domain}"
108
+
109
+ end
110
+
111
+
112
+ def find_records(zone, name)
113
+ existing_records = zone.records.all({:name => name})
114
+
115
+ matching = []
116
+ types = ['A', 'AAAA', 'CNAME']
117
+ existing_records.each { |record|
118
+ matching << record if record.name == name && types.include?(record.type)
119
+ }
120
+ matching
121
+ end
122
+
123
+ # Appends a trailing . to the given name if it doesn't have one
124
+ def ensure_trailing_dot(name)
125
+ name = "#{name}." unless name.nil? || name.end_with?(".")
126
+ name
127
+ end
128
+
129
+ # Returns true if the specified dns names equal, ignoring any trailing "."
130
+ def dns_names_equal(a, b)
131
+ ensure_trailing_dot(a) == ensure_trailing_dot(b)
132
+ end
133
+
134
+ end
135
+ end