vominator 0.0.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +25 -0
  3. data/.rspec +5 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +116 -0
  7. data/Rakefile +12 -0
  8. data/bin/vominate +12 -0
  9. data/circle.yml +5 -0
  10. data/lib/ec2.rb +12 -0
  11. data/lib/ec2/instances.rb +362 -0
  12. data/lib/ec2/security_groups.rb +314 -0
  13. data/lib/ec2/ssm.rb +81 -0
  14. data/lib/vominator/aws.rb +15 -0
  15. data/lib/vominator/constants.rb +53 -0
  16. data/lib/vominator/ec2.rb +308 -0
  17. data/lib/vominator/instances.rb +34 -0
  18. data/lib/vominator/route53.rb +125 -0
  19. data/lib/vominator/security_groups.rb +26 -0
  20. data/lib/vominator/ssm.rb +57 -0
  21. data/lib/vominator/version.rb +3 -0
  22. data/lib/vominator/vominator.rb +74 -0
  23. data/lib/vominator/vpc.rb +82 -0
  24. data/lib/vpc.rb +8 -0
  25. data/lib/vpc/create.rb +188 -0
  26. data/spec/lib/instances_spec.rb +2 -0
  27. data/spec/lib/vominator/aws_spec.rb +6 -0
  28. data/spec/lib/vominator/ec2_spec.rb +783 -0
  29. data/spec/lib/vominator/instances_spec.rb +96 -0
  30. data/spec/lib/vominator/route53_spec.rb +64 -0
  31. data/spec/lib/vominator/ssm_spec.rb +95 -0
  32. data/spec/lib/vominator/vominator_spec.rb +209 -0
  33. data/spec/spec_helper.rb +103 -0
  34. data/spec/support/matchers/exit_with_code.rb +24 -0
  35. data/test/puke/cloud-configs/.gitkeep +0 -0
  36. data/test/puke/cloud-configs/cloud-config-example.erb +63 -0
  37. data/test/puke/config.yaml +16 -0
  38. data/test/puke/products/sample-api/instances.yaml +37 -0
  39. data/test/puke/products/sample-api/security_groups.yaml +19 -0
  40. data/test/vominator.yaml +7 -0
  41. data/vominator.gemspec +34 -0
  42. metadata +259 -0
@@ -0,0 +1,308 @@
1
+ require 'aws-sdk'
2
+ require 'base64'
3
+ require_relative 'constants'
4
+
5
+ module Vominator
6
+ class EC2
7
+ def self.get_virt_type(instance_type)
8
+ begin
9
+ return EC2_INSTANCE_METADATA[instance_type.to_sym][:virtualization_type]
10
+ rescue NoMethodError
11
+ raise ArgumentError, 'You must specify a valid instance type'
12
+ end
13
+ end
14
+
15
+ def self.get_ephemeral_dev_count(instance_type)
16
+ return EC2_INSTANCE_METADATA[instance_type.to_sym][:ephemeral_devices]
17
+ end
18
+
19
+ def self.get_instances(resource)
20
+ instances = Hash.new
21
+ resource.instances.each do |instance|
22
+ instances[instance.private_ip_address] = {:instance_id => instance.id, :security_groups => instance.security_groups.map { |sg| sg.group_name}}
23
+ end
24
+ return instances
25
+ end
26
+
27
+ def self.get_instance(resource, instance_id)
28
+ return resource.instances(filters: [{name: 'instance-id', values: [instance_id]}]).first
29
+ end
30
+
31
+ def self.get_security_group_name_ids_hash(resource, vpc_id)
32
+ security_groups = Hash.new
33
+ resource.vpcs(filters: [{name: 'vpc-id', values: [vpc_id]}]).first.security_groups.each do |security_group|
34
+ security_groups[security_group.group_name] = security_group.id
35
+ end
36
+ return security_groups
37
+ end
38
+
39
+ def self.get_subnets(resource, vpc_id)
40
+ subnets = Hash.new
41
+ resource.vpcs(filters: [{name: 'vpc-id', values: [vpc_id]}]).first.subnets.each do |subnet|
42
+ subnets[subnet.cidr_block] = subnet
43
+ end
44
+ return subnets
45
+ end
46
+
47
+ def self.get_ami(puke_config, instance_type, family)
48
+ if Vominator::EC2.get_virt_type(instance_type) == 'hvm'
49
+ ami = puke_config['linux_hvm_base_image'] if family == 'linux'
50
+ ami = puke_config['windows_hvm_base_image'] if family == 'windows'
51
+ else
52
+ ami = puke_config['linux_paravirtual_base_image'] if family == 'linux'
53
+ ami = puke_config['windows_paravirtual_base_image'] if family == 'windows'
54
+ end
55
+ return ami
56
+ end
57
+
58
+ def self.create_subnet(resource, subnet, az, vpc_id, route_table_id=nil)
59
+ subnet = resource.vpcs(filters: [{name: 'vpc-id', values: [vpc_id]}]).first.create_subnet(:cidr_block => subnet, :availability_zone => az)
60
+ if route_table_id
61
+ route_table = Vominator::EC2.get_route_table(resource,route_table_id)
62
+ route_table.associate_with_subnet(subnet_id: subnet.subnet_id)
63
+ end
64
+ return subnet
65
+ end
66
+
67
+ def self.get_route_table(resource, route_table_id)
68
+ return resource.route_tables(filters: [{name: 'route-table-id', values: [route_table_id]}]).first
69
+ end
70
+
71
+ def self.get_termination_protection(client, instance_id)
72
+ return client.describe_instance_attribute(:instance_id => instance_id, :attribute => 'disableApiTermination').disable_api_termination.value
73
+ end
74
+
75
+ def self.set_termination_protection(client, instance_id, state)
76
+ client.modify_instance_attribute(:instance_id => instance_id, :disable_api_termination => { :value => state })
77
+ return client.describe_instance_attribute(:instance_id => instance_id, :attribute => 'disableApiTermination').disable_api_termination.value
78
+ end
79
+
80
+ def self.get_instance_state(resource, instance_id)
81
+ instance = Vominator::EC2.get_instance(resource,instance_id)
82
+
83
+ return instance.state.name
84
+ end
85
+
86
+ def self.set_instance_type(resource, instance_id, type, fqdn)
87
+ instance = Vominator::EC2.get_instance(resource,instance_id)
88
+ instance.stop
89
+
90
+ # TODO: Add a timed break?
91
+ sleep 5 until Vominator::EC2.get_instance_state(resource, instance_id) == 'stopped'
92
+ instance.modify_attribute(:attribute => 'instanceType', :value => type)
93
+
94
+ # TODO: We should add in a sleep (with a timed break) and verify the instance goes back to running
95
+ begin
96
+ instance.start
97
+ rescue Aws::EC2::Errors::Unsupported
98
+ LOGGER.warning("Disabling EBS Optimization for #{fqdn} because #{type} does not support this functionality")
99
+ instance.ebs_optimized = false
100
+ instance.start
101
+ end
102
+ return Vominator::EC2.get_instance(resource,instance_id).instance_type
103
+ end
104
+
105
+
106
+ def self.allocate_public_ip(client, domain='vpc')
107
+ return client.allocate_address(domain: domain)
108
+ end
109
+
110
+ def self.assign_public_ip(client, instance_id)
111
+ eip = client.allocate_address(:domain => 'vpc')
112
+ tries ||= 3
113
+ begin
114
+ sleep(0.25)
115
+ association = client.associate_address(:instance_id => instance_id, :allocation_id => eip.allocation_id)
116
+ rescue Aws::EC2::Errors::InvalidAllocationIDNotFound
117
+ retry unless (tries -= 1).zero?
118
+ end
119
+
120
+ if association
121
+ return eip.public_ip
122
+ else
123
+ return nil
124
+ end
125
+ end
126
+
127
+ def self.remove_public_ip(client, instance_id)
128
+ allocation_id = client.describe_addresses(filters: [{name: 'instance-id', values: [instance_id]}]).first['addresses'].first['association_id']
129
+ if client.disassociate_address(:association_id => allocation_id)
130
+ return true
131
+ else
132
+ return false
133
+ end
134
+ end
135
+
136
+ def self.set_ebs_optimized(resource, instance_id, state, fqdn)
137
+ instance = Vominator::EC2.get_instance(resource,instance_id)
138
+ instance.stop
139
+
140
+ # TODO: Add a timed break?
141
+ sleep 5 until Vominator::EC2.get_instance_state(resource, instance_id) == 'stopped'
142
+
143
+ # TODO: We should add in a sleep (with a timed break) and verify the instance goes back to running
144
+ begin
145
+ instance.modify_attribute(:ebs_optimized => {:value => state})
146
+ instance.start
147
+ return state
148
+ rescue Aws::EC2::Errors::Unsupported
149
+ LOGGER.error("#{fqdn} does not support setting EBS Optimization to #{state} as this is not supported for #{instance.instance_type}")
150
+ instance.modify_attribute(:ebs_optimized => {:value => false})
151
+ instance.start
152
+ return instance.ebs_optimized
153
+ end
154
+ end
155
+
156
+ def self.set_source_dest_check(resource, instance_id, state)
157
+ instance = Vominator::EC2.get_instance(resource,instance_id)
158
+ if instance.modify_attribute(:source_dest_check => {:value => state})
159
+ return state
160
+ end
161
+ end
162
+
163
+ def self.set_security_groups(resource, instance_id, security_groups, vpc_security_groups, append=true)
164
+ instance = Vominator::EC2.get_instance(resource,instance_id)
165
+ if append
166
+ security_groups = security_groups + instance.security_groups.map {|sg| sg[:group_name]}
167
+ end
168
+ group_ids = security_groups.map { |sg| vpc_security_groups[sg] }
169
+ instance.modify_attribute(:groups => group_ids.compact)
170
+ return Vominator::EC2.get_instance(resource,instance_id).security_groups.map {|sg| sg[:group_name]}
171
+ end
172
+
173
+ def self.get_ebs_volume(resource, ebs_volume_id)
174
+ return resource.volumes(filters: [{name: 'volume-id', values: [ebs_volume_id]}]).first
175
+ end
176
+
177
+ def self.get_instance_ebs_volumes(resource, instance_id)
178
+ instance = Vominator::EC2.get_instance(resource,instance_id)
179
+ return instance.block_device_mappings.map {|vol| vol[:device_name]}
180
+ end
181
+
182
+ def self.add_ebs_volume(resource, instance_id, volume_type, volume_size, mount_point, iops=false, encrypted=false)
183
+ instance = Vominator::EC2.get_instance(resource,instance_id)
184
+ availability_zone = instance.placement[:availability_zone]
185
+
186
+ case volume_type
187
+ when 'magnetic'
188
+ volume = resource.create_volume(:availability_zone => availability_zone, :volume_type => 'standard', :size => volume_size, :encrypted => encrypted)
189
+ when 'gp'
190
+ volume = resource.create_volume(:availability_zone => availability_zone, :volume_type => 'gp2', :size => volume_size, :encrypted => encrypted)
191
+ when 'piops'
192
+ iops = volume_size * 15 unless iops
193
+ volume = resource.create_volume(:availability_zone => availability_zone, :volume_type => 'io1', :size => volume_size, :iops => iops, :encrypted => encrypted)
194
+ else
195
+ volume = nil
196
+ LOGGER.fatal("#{volume_type} is unsupported")
197
+ end
198
+ LOGGER.info("Waiting for #{volume.id} to be provisioned and become available")
199
+
200
+ sleep 3 until Vominator::EC2.get_ebs_volume(resource, volume.id).state == 'available'
201
+
202
+ LOGGER.info("Attaching #{volume.id} to the instance and waiting for it to be attached.")
203
+ instance.attach_volume(:device => mount_point, :volume_id => volume.id)
204
+ sleep 3 until Vominator::EC2.get_ebs_volume(resource, volume.id).state == 'in-use'
205
+
206
+ return volume
207
+ end
208
+
209
+ def self.get_ephemeral_devices(instance_type)
210
+ device_count = Vominator::EC2.get_ephemeral_dev_count(instance_type)
211
+ devices = Hash.new
212
+ for i in 1..device_count
213
+ mount_point = (65 + (i)).chr.downcase
214
+ devices["/dev/sd#{mount_point}"] = "ephemeral#{i-1}"
215
+ end
216
+ return devices
217
+ end
218
+
219
+ def self.create_instance(resource, hostname, environment, ami_id, subnet_id, instance_type, key_name, private_ip_address, az, security_group_ids, user_data, ebs_optimized, iam_profile)
220
+ begin
221
+ LOGGER.info("Creating instance for #{hostname}.#{environment}")
222
+ instance = resource.create_instances(:min_count => 1, :max_count => 1, :image_id => ami_id, :subnet_id => subnet_id, :instance_type => instance_type, :key_name => key_name, :private_ip_address => private_ip_address, :placement => {:availability_zone => az}, :security_group_ids => security_group_ids, :user_data => Base64.encode64(user_data), :ebs_optimized => ebs_optimized, :iam_instance_profile => {:name => iam_profile}).first
223
+ rescue Aws::EC2::Errors::InvalidIPAddressInUse
224
+ LOGGER.fatal("Unable to create the instance as #{private_ip_address} is in use")
225
+ rescue Aws::EC2::Errors::InternalError
226
+ LOGGER.fatal("Unable to create instance due to an AWS internal server error. Try again in a bit")
227
+ end
228
+ LOGGER.info('Waiting for the instance to come online.')
229
+ sleep 10
230
+ sleep 2 until Vominator::EC2.get_instance_state(resource, instance.id) != 'pending'
231
+
232
+ instance.create_tags(:tags => [{:key => 'Name', :value => "#{hostname}.#{environment}"},{:key => 'Environment', :value => environment},{:key => 'Provisioned_By', :value => 'Vominator'}])
233
+
234
+ if Vominator::EC2.get_instance_state(resource, instance.id) == 'running'
235
+ return instance
236
+ else
237
+ return nil
238
+ end
239
+ end
240
+
241
+ def self.terminate_instance(resource, instance_id)
242
+ instance = Vominator::EC2.get_instance(resource,instance_id)
243
+ instance.terminate
244
+ sleep 2 until Vominator::EC2.get_instance_state(resource, instance.id) == 'terminated'
245
+ return true
246
+ end
247
+
248
+ def self.tag_resource(client, resource_id, tags)
249
+ client.create_tags(resources: [resource_id], tags: tags)
250
+ end
251
+
252
+ def self.get_security_groups(client, vpc_id)
253
+ return client.describe_security_groups(filters: [{name: 'vpc-id', values: [vpc_id]}]).first.security_groups
254
+ end
255
+
256
+ def self.create_security_group(client, name, vpc_id, description=nil)
257
+ return client.create_security_group(group_name: name, description: description || name, vpc_id: vpc_id)
258
+ end
259
+
260
+ def self.create_security_group_rule(client,type,group_id,rule)
261
+ case type
262
+ when 'ingress'
263
+ if rule[:source_security_group_id]
264
+ client.authorize_security_group_ingress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], user_id_group_pairs: [{group_id: rule[:source_security_group_id]}]}]})
265
+ end
266
+
267
+ if rule[:cidr_ip]
268
+ client.authorize_security_group_ingress({group_id: group_id, cidr_ip: rule[:cidr_ip], ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port]})
269
+ end
270
+
271
+ when 'egress'
272
+ if rule[:source_security_group_id]
273
+ client.authorize_security_group_egress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], user_id_group_pairs: [{group_id: rule[:source_security_group_id]}]}]})
274
+ end
275
+
276
+ if rule[:cidr_ip]
277
+ client.authorize_security_group_egress({group_id: group_id, cidr_ip: rule[:cidr_ip], ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port]})
278
+ end
279
+ else
280
+ return false
281
+ end
282
+ end
283
+
284
+ def self.delete_security_group_rule(client,type,group_id,rule)
285
+ case type
286
+ when 'ingress'
287
+ if rule[:source_security_group_id]
288
+ client.revoke_security_group_ingress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], user_id_group_pairs: [{group_id: rule[:source_security_group_id]}]}]})
289
+ end
290
+
291
+ if rule[:cidr_ip]
292
+ client.revoke_security_group_ingress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], ip_ranges: [{cidr_ip: rule[:cidr_ip]}]}]})
293
+ end
294
+
295
+ when 'egress'
296
+ if rule[:source_security_group_id]
297
+ client.revoke_security_group_egress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], user_id_group_pairs: [{group_id: rule[:source_security_group_id]}]}]})
298
+ end
299
+
300
+ if rule[:cidr_ip]
301
+ client.revoke_security_group_egress({group_id: group_id, ip_permissions: [{ip_protocol: rule[:ip_protocol], from_port: rule[:from_port], to_port: rule[:to_port], ip_ranges: [{cidr_ip: rule[:cidr_ip]}]}]})
302
+ end
303
+ else
304
+ return false
305
+ end
306
+ end
307
+ end
308
+ end
@@ -0,0 +1,34 @@
1
+ require_relative 'constants'
2
+ require 'erubis'
3
+
4
+ module Vominator
5
+ class Instances
6
+ def self.get_instances(environment, product, filter=false)
7
+ if PUKE_CONFIG[environment]['products'].include? product
8
+ config_file = File.expand_path("#{VOMINATOR_CONFIG['configuration_path']}/products/#{product}/instances.yaml")
9
+ if filter
10
+ instances = Array.new
11
+ YAML.load(File.read(config_file)).each do |instance|
12
+ if filter.include? instance.keys[0]
13
+ instances.push instance
14
+ end
15
+ end
16
+ else
17
+ instances = YAML.load(File.read(config_file))
18
+ end
19
+ return instances if instances.kind_of?(Array)
20
+ end
21
+ end
22
+
23
+ def self.generate_cloud_config(hostname, environment, family, roles, recipes)
24
+ template = "#{family}_cloud_config_template"
25
+ begin
26
+ cloud_config_template = File.read("#{VOMINATOR_CONFIG['configuration_path']}/cloud-configs/#{PUKE_CONFIG[environment][template]}")
27
+ rescue Errno::EISDIR
28
+ LOGGER.fatal("Unable to find #{template} in your cloud-config directory. Check that this file exists in #{VOMINATOR_CONFIG['configuration_path']}/cloud-configs/")
29
+ end
30
+ cloud_config = Erubis::Eruby.new(cloud_config_template)
31
+ return cloud_config.result(:hostname => hostname, :env => environment, :roles => roles, :recipes => recipes)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,125 @@
1
+ require 'aws-sdk'
2
+ require_relative 'constants'
3
+
4
+ module Vominator
5
+ class Route53
6
+ def self.get_zone_by_id(client, zone_id)
7
+ return client.get_hosted_zone(id: zone_id).hosted_zone
8
+ end
9
+
10
+ def self.get_zone_by_domain(client, domain_name)
11
+ zones = client.list_hosted_zones_by_name(dns_name: domain_name).hosted_zones
12
+ zone = nil
13
+ zones.each do |z|
14
+ zone = z if z.name == "#{domain_name}."
15
+ end
16
+
17
+ return zone
18
+ end
19
+
20
+ def self.create_zone(client, domain_name)
21
+ return client.create_hosted_zone(name: domain_name, caller_reference: "#{domain_name} + #{Time.now.to_i}")
22
+ end
23
+
24
+ def self.get_records(client, zone, max_items=50)
25
+ resp = client.list_resource_record_sets(:hosted_zone_id => zone, :max_items => max_items)
26
+ records = resp[:resource_record_sets]
27
+ while resp[:is_truncated]
28
+ resp = client.list_resource_record_sets(:hosted_zone_id => zone, :max_items => max_items, :start_record_name => resp[:next_record_name])
29
+ records += resp[:resource_record_sets]
30
+ end
31
+ names = Array.new
32
+ records.each do |record|
33
+ names.push record[:name]
34
+ end
35
+ return names
36
+ end
37
+
38
+ def self.create_nameserver_records(client, zone, fqdn, nameservers, ttl=172800)
39
+ resource_records = Array.new
40
+ nameservers.each do |nameserver|
41
+ hash = Hash.new
42
+ hash[:value] = nameserver
43
+ resource_records.push hash
44
+ end
45
+
46
+ resp = client.change_resource_record_sets(
47
+ :hosted_zone_id => zone,
48
+ :change_batch => {
49
+ :changes => [
50
+ {
51
+ :action => "CREATE",
52
+ :resource_record_set => {
53
+ :name => fqdn,
54
+ :type => 'NS',
55
+ :ttl => ttl,
56
+ :resource_records => resource_records
57
+ }
58
+ }
59
+ ]
60
+ }
61
+ )
62
+
63
+ if resp.change_info.status == 'PENDING'
64
+ return true
65
+ else
66
+ return false
67
+ end
68
+ end
69
+
70
+ def self.create_record(client, zone, fqdn, ip, type='A', ttl=600)
71
+ resp = client.change_resource_record_sets(
72
+ :hosted_zone_id => "/hostedzone/#{zone}",
73
+ :change_batch => {
74
+ :changes => [
75
+ {
76
+ :action => "CREATE",
77
+ :resource_record_set => {
78
+ :name => "#{fqdn}.",
79
+ :type => type,
80
+ :ttl => ttl,
81
+ :resource_records => [{
82
+ :value => "#{ip}"
83
+ }]
84
+ }
85
+ }
86
+ ]
87
+ }
88
+ )
89
+
90
+ if resp.change_info.status == 'PENDING'
91
+ return true
92
+ else
93
+ return false
94
+ end
95
+ end
96
+
97
+ def self.delete_record(client, zone, fqdn, ip, type='A', ttl=600)
98
+ resp = client.change_resource_record_sets(
99
+ :hosted_zone_id => "/hostedzone/#{zone}",
100
+ :change_batch => {
101
+ :changes => [
102
+ {
103
+ :action => "DELETE",
104
+ :resource_record_set => {
105
+ :name => "#{fqdn}.",
106
+ :type => type,
107
+ :ttl => ttl,
108
+ :resource_records => [{
109
+ :value => ip
110
+ }]
111
+ }
112
+
113
+ }
114
+ ]
115
+ }
116
+ )
117
+ if resp.change_info.status == 'PENDING'
118
+ return true
119
+ else
120
+ return false
121
+ end
122
+ end
123
+
124
+ end
125
+ end