fum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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