rubber 3.1.0 → 3.2.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,318 @@
1
+ require 'rubber/cloud/fog'
2
+ require 'rubber/cloud/aws'
3
+
4
+ module Rubber
5
+ module Cloud
6
+
7
+ class Aws::Base < Fog
8
+
9
+ def initialize(env, capistrano)
10
+
11
+ compute_credentials = {
12
+ :aws_access_key_id => env.access_key,
13
+ :aws_secret_access_key => env.secret_access_key
14
+ }
15
+
16
+ storage_credentials = {
17
+ :provider => 'AWS',
18
+ :aws_access_key_id => env.access_key,
19
+ :aws_secret_access_key => env.secret_access_key,
20
+ :path_style => true
21
+ }
22
+
23
+ @table_store = ::Fog::AWS::SimpleDB.new(compute_credentials)
24
+
25
+ compute_credentials[:region] = env.region
26
+ @elb = ::Fog::AWS::ELB.new(compute_credentials)
27
+
28
+ compute_credentials[:provider] = 'AWS' # We need to set the provider after the SimpleDB init because it fails if the provider value is specified.
29
+
30
+ storage_credentials[:region] = env.region
31
+
32
+ env['compute_credentials'] = compute_credentials
33
+ env['storage_credentials'] = storage_credentials
34
+ super(env, capistrano)
35
+ end
36
+
37
+ def table_store(table_key)
38
+ return Rubber::Cloud::Aws::TableStore.new(@table_store, table_key)
39
+ end
40
+
41
+ def describe_instances(instance_id=nil)
42
+ instances = []
43
+ opts = {}
44
+ opts["instance-id"] = instance_id if instance_id
45
+
46
+ response = compute_provider.servers.all(opts)
47
+ response.each do |item|
48
+ instance = {}
49
+ instance[:id] = item.id
50
+ instance[:type] = item.flavor_id
51
+ instance[:external_host] = item.dns_name
52
+ instance[:external_ip] = item.public_ip_address
53
+ instance[:internal_host] = item.private_dns_name
54
+ instance[:internal_ip] = item.private_ip_address
55
+ instance[:state] = item.state
56
+ instance[:zone] = item.availability_zone
57
+ instance[:provider] = 'aws'
58
+ instance[:platform] = item.platform || Rubber::Platforms::LINUX
59
+ instance[:root_device_type] = item.root_device_type
60
+ instances << instance
61
+ end
62
+
63
+ return instances
64
+ end
65
+
66
+ def active_state
67
+ 'running'
68
+ end
69
+
70
+ def stopped_state
71
+ 'stopped'
72
+ end
73
+
74
+ def before_create_instance(instance)
75
+ setup_security_groups(instance.instance_alias, instance.role_names)
76
+ end
77
+
78
+ def after_create_instance(instance)
79
+ # Sometimes tag creation will fail, indicating that the instance doesn't exist yet even though it does. It seems to
80
+ # be a propagation delay on Amazon's end, so the best we can do is wait and try again.
81
+ Rubber::Util.retry_on_failure(StandardError, :retry_sleep => 1, :retry_count => 120) do
82
+ Rubber::Tag::update_instance_tags(instance.name)
83
+ end
84
+ end
85
+
86
+ def after_refresh_instance(instance)
87
+ # Sometimes tag creation will fail, indicating that the instance doesn't exist yet even though it does. It seems to
88
+ # be a propagation delay on Amazon's end, so the best we can do is wait and try again.
89
+ Rubber::Util.retry_on_failure(StandardError, :retry_sleep => 1, :retry_count => 120) do
90
+ Rubber::Tag::update_instance_tags(instance.name)
91
+ end
92
+ end
93
+
94
+ def before_stop_instance(instance)
95
+ capistrano.fatal "Cannot stop spot instances!" if ! instance.spot_instance_request_id.nil?
96
+ capistrano.fatal "Cannot stop instances with instance-store root device!" if (instance.root_device_type != 'ebs')
97
+ end
98
+
99
+ def before_start_instance(instance)
100
+ capistrano.fatal "Cannot start spot instances!" if ! instance.spot_instance_request_id.nil?
101
+ capistrano.fatal "Cannot start instances with instance-store root device!" if (instance.root_device_type != 'ebs')
102
+ end
103
+
104
+ def after_start_instance(instance)
105
+ # Re-starting an instance will almost certainly give it a new set of IPs and DNS entries, so refresh the values.
106
+ capistrano.rubber.refresh_instance(instance.name)
107
+
108
+ # Static IPs, DNS, etc. need to be set up for the started instance.
109
+ capistrano.rubber.post_refresh
110
+ end
111
+
112
+ def create_image(image_name)
113
+
114
+ # validate all needed config set
115
+ ["key_file", "pk_file", "cert_file", "account", "secret_access_key", "image_bucket"].each do |k|
116
+ raise "Set #{k} in rubber.yml" unless "#{env[k]}".strip.size > 0
117
+ end
118
+ raise "create_image can only be called from a capistrano scope" unless capistrano
119
+
120
+ ec2_key = env.key_file
121
+ ec2_pk = env.pk_file
122
+ ec2_cert = env.cert_file
123
+
124
+ ec2_key_dest = "/mnt/#{File.basename(ec2_key)}"
125
+ ec2_pk_dest = "/mnt/#{File.basename(ec2_pk)}"
126
+ ec2_cert_dest = "/mnt/#{File.basename(ec2_cert)}"
127
+
128
+ storage(env.image_bucket).ensure_bucket
129
+
130
+ capistrano.put(File.read(ec2_key), ec2_key_dest)
131
+ capistrano.put(File.read(ec2_pk), ec2_pk_dest)
132
+ capistrano.put(File.read(ec2_cert), ec2_cert_dest)
133
+
134
+ arch = capistrano.capture("uname -m").strip
135
+ arch = case arch when /i\d86/ then "i386" else arch end
136
+
137
+ capistrano.sudo_script "create_bundle", <<-CMD
138
+ export RUBYLIB=/usr/lib/site_ruby/
139
+ unset RUBYOPT
140
+ nohup ec2-bundle-vol --batch -d /mnt -k #{ec2_pk_dest} -c #{ec2_cert_dest} -u #{env.account} -p #{image_name} -r #{arch} &> /tmp/ec2-bundle-vol.log &
141
+ bg_pid=$!
142
+ sleep 1
143
+
144
+ echo "Creating image from instance volume..."
145
+ while kill -0 $bg_pid &> /dev/null; do
146
+ echo -n .
147
+ sleep 5
148
+ done
149
+
150
+ # this returns exit code even if pid has already died, and thus triggers fail fast shell error
151
+ wait $bg_pid
152
+ CMD
153
+
154
+ capistrano.sudo_script "register_bundle", <<-CMD
155
+ export RUBYLIB=/usr/lib/site_ruby/
156
+ unset RUBYOPT
157
+ echo "Uploading image to S3..."
158
+ ec2-upload-bundle --batch -b #{env.image_bucket} -m /mnt/#{image_name}.manifest.xml -a #{env.access_key} -s #{env.secret_access_key}
159
+ CMD
160
+
161
+ image_location = "#{env.image_bucket}/#{image_name}.manifest.xml"
162
+ response = compute_provider.register_image(image_name,
163
+ "rubber bundled image",
164
+ image_location)
165
+ return response.body["imageId"]
166
+ end
167
+
168
+ def destroy_image(image_id)
169
+ image = compute_provider.images.get(image_id)
170
+ raise "Could not find image: #{image_id}, aborting destroy_image" if image.nil?
171
+
172
+ location_parts = image.location.split('/')
173
+ bucket = location_parts.first
174
+ image_name = location_parts.last.gsub(/\.manifest\.xml$/, '')
175
+
176
+ image.deregister
177
+
178
+ storage(bucket).walk_tree(image_name) do |f|
179
+ f.destroy
180
+ end
181
+ end
182
+
183
+ def describe_load_balancers(name=nil)
184
+ lbs = []
185
+ response = name.nil? ? @elb.load_balancers.all() : [@elb.load_balancers.get(name)].compact
186
+ response.each do |item|
187
+ lb = {}
188
+ lb[:name] = item.id
189
+ lb[:dns_name] = item.dns_name
190
+ lb[:zones] = item.availability_zones
191
+
192
+ item.listeners.each do |litem|
193
+ listener = {}
194
+ listener[:protocol] = litem.protocol
195
+ listener[:port] = litem.lb_portPort
196
+ listener[:instance_port] = litem.instance_port
197
+ lb[:listeners] ||= []
198
+ lb[:listeners] << listener
199
+ end
200
+
201
+ lbs << lb
202
+ end
203
+ return lbs
204
+ end
205
+
206
+ def describe_availability_zones
207
+ zones = []
208
+ response = compute_provider.describe_availability_zones()
209
+ items = response.body["availabilityZoneInfo"]
210
+ items.each do |item|
211
+ zone = {}
212
+ zone[:name] = item["zoneName"]
213
+ zone[:state] =item["zoneState"]
214
+ zones << zone
215
+ end
216
+ return zones
217
+ end
218
+
219
+ def create_spot_instance_request(spot_price, ami, ami_type, security_groups, availability_zone, fog_options={})
220
+ response = compute_provider.spot_requests.create({:price => spot_price,
221
+ :image_id => ami,
222
+ :flavor_id => ami_type,
223
+ :groups => security_groups,
224
+ :availability_zone => availability_zone,
225
+ :key_name => env.key_name}.merge(Rubber::Util.symbolize_keys(fog_options)))
226
+ request_id = response.id
227
+ return request_id
228
+ end
229
+
230
+ def describe_spot_instance_requests(request_id=nil)
231
+ requests = []
232
+ opts = {}
233
+ opts["spot-instance-request-id"] = request_id if request_id
234
+ response = compute_provider.spot_requests.all(opts)
235
+ response.each do |item|
236
+ request = {}
237
+ request[:id] = item.id
238
+ request[:spot_price] = item.price
239
+ request[:state] = item.state
240
+ request[:created_at] = item.created_at
241
+ request[:type] = item.flavor_id
242
+ request[:image_id] = item.image_id
243
+ request[:instance_id] = item.instance_id
244
+ requests << request
245
+ end
246
+ return requests
247
+ end
248
+
249
+ def setup_security_groups(host=nil, roles=[])
250
+ raise NotImplementedError("Implement #setup_security_groups")
251
+ end
252
+
253
+ def describe_security_groups(group_name=nil)
254
+ raise NotImplementedError("Implement #describe_security_groups")
255
+ end
256
+
257
+ def create_volume(instance, volume_spec)
258
+ fog_options = Rubber::Util.symbolize_keys(volume_spec['fog_options'] || {})
259
+ volume_data = {
260
+ :size => volume_spec['size'], :availability_zone => volume_spec['zone']
261
+ }.merge(fog_options)
262
+ volume = compute_provider.volumes.create(volume_data)
263
+ volume.id
264
+ end
265
+
266
+ def after_create_volume(instance, volume_id, volume_spec)
267
+ # After we create an EBS volume, we need to attach it to the instance.
268
+ volume = compute_provider.volumes.get(volume_id)
269
+ server = compute_provider.servers.get(instance.instance_id)
270
+ volume.device = volume_spec['device']
271
+ volume.server = server
272
+ end
273
+
274
+ def before_destroy_volume(volume_id)
275
+ # Before we can destroy an EBS volume, we must detach it from any running instances.
276
+ volume = compute_provider.volumes.get(volume_id)
277
+ volume.force_detach
278
+ end
279
+
280
+ def destroy_volume(volume_id)
281
+ compute_provider.volumes.get(volume_id).destroy
282
+ end
283
+
284
+ def describe_volumes(volume_id=nil)
285
+ volumes = []
286
+ opts = {}
287
+ opts[:'volume-id'] = volume_id if volume_id
288
+ response = compute_provider.volumes.all(opts)
289
+
290
+ response.each do |item|
291
+ volume = {}
292
+ volume[:id] = item.id
293
+ volume[:status] = item.state
294
+
295
+ if item.server_id
296
+ volume[:attachment_instance_id] = item.server_id
297
+ volume[:attachment_status] = item.attached_at ? "attached" : "waiting"
298
+ end
299
+
300
+ volumes << volume
301
+ end
302
+
303
+ volumes
304
+ end
305
+
306
+ # resource_id is any Amazon resource ID (e.g., instance ID or volume ID)
307
+ # tags is a hash of tag_name => tag_value pairs
308
+ def create_tags(resource_id, tags)
309
+ # Tags need to be created individually in fog
310
+ tags.each do |k, v|
311
+ compute_provider.tags.create(:resource_id => resource_id,
312
+ :key => k.to_s, :value => v.to_s)
313
+ end
314
+ end
315
+ end
316
+ end
317
+ end
318
+
@@ -0,0 +1,245 @@
1
+ require 'rubber/cloud/aws/base'
2
+
3
+ module Rubber
4
+ module Cloud
5
+
6
+ class Aws::Classic < Aws::Base
7
+
8
+ def setup_security_groups(host=nil, roles=[])
9
+ rubber_cfg = Rubber::Configuration.get_configuration(Rubber.env)
10
+ scoped_env = rubber_cfg.environment.bind(roles, host)
11
+ security_group_defns = Hash[scoped_env.security_groups.to_a]
12
+
13
+ if scoped_env.auto_security_groups
14
+ sghosts = (scoped_env.rubber_instances.collect{|ic| ic.name } + [host]).uniq.compact
15
+ sgroles = (scoped_env.rubber_instances.all_roles + roles).uniq.compact
16
+ security_group_defns = inject_auto_security_groups(security_group_defns, sghosts, sgroles)
17
+ end
18
+
19
+ sync_security_groups(security_group_defns)
20
+ end
21
+
22
+ def describe_security_groups(group_name=nil)
23
+ groups = []
24
+
25
+ opts = {}
26
+ opts["group-name"] = group_name if group_name
27
+ response = compute_provider.security_groups.all(opts)
28
+
29
+ response.each do |item|
30
+ group = {}
31
+ group[:name] = item.name
32
+ group[:description] = item.description
33
+
34
+ item.ip_permissions.each do |ip_item|
35
+ group[:permissions] ||= []
36
+ rule = {}
37
+
38
+ rule[:protocol] = ip_item["ipProtocol"]
39
+ rule[:from_port] = ip_item["fromPort"]
40
+ rule[:to_port] = ip_item["toPort"]
41
+
42
+ ip_item["groups"].each do |rule_group|
43
+ rule[:source_groups] ||= []
44
+ source_group = {}
45
+ source_group[:account] = rule_group["userId"]
46
+
47
+ # Amazon doesn't appear to be returning the groupName value when running in a default VPC. It's possible
48
+ # it's only returned for EC2 Classic. This is distinctly in conflict with the API documents and thus
49
+ # appears to be a bug on Amazon's end. Nonetheless, we need to handle it because otherwise our security
50
+ # group rule matching logic will fail and it messes up our users.
51
+ #
52
+ # Since every top-level item has both an ID and a name, if we're lacking the groupName we can search
53
+ # through the items for the one matching the groupId we have and then use its name value. This should
54
+ # represent precisely the same data.
55
+ source_group[:name] = if rule_group["groupName"]
56
+ rule_group["groupName"]
57
+ elsif rule_group["groupId"]
58
+ matching_security_group = response.find { |item| item.group_id == rule_group["groupId"] }
59
+ matching_security_group ? matching_security_group.name : nil
60
+ else
61
+ nil
62
+ end
63
+
64
+ rule[:source_groups] << source_group
65
+ end if ip_item["groups"]
66
+
67
+ ip_item["ipRanges"].each do |ip_range|
68
+ rule[:source_ips] ||= []
69
+ rule[:source_ips] << ip_range["cidrIp"]
70
+ end if ip_item["ipRanges"]
71
+
72
+ group[:permissions] << rule
73
+ end
74
+
75
+ groups << group
76
+ end
77
+
78
+ groups
79
+ end
80
+
81
+ private
82
+
83
+ def create_security_group(group_name, group_description)
84
+ compute_provider.security_groups.create(:name => group_name, :description => group_description)
85
+ end
86
+
87
+ def destroy_security_group(group_name)
88
+ compute_provider.security_groups.get(group_name).destroy
89
+ end
90
+
91
+ def add_security_group_rule(group_name, protocol, from_port, to_port, source)
92
+ group = compute_provider.security_groups.get(group_name)
93
+ opts = {:ip_protocol => protocol || 'tcp'}
94
+
95
+ if source.instance_of? Hash
96
+ opts[:group] = {source[:account] => source[:name]}
97
+ else
98
+ opts[:cidr_ip] = source
99
+ end
100
+
101
+ group.authorize_port_range(from_port.to_i..to_port.to_i, opts)
102
+ end
103
+
104
+ def remove_security_group_rule(group_name, protocol, from_port, to_port, source)
105
+ group = compute_provider.security_groups.get(group_name)
106
+ opts = {:ip_protocol => protocol || 'tcp'}
107
+
108
+ if source.instance_of? Hash
109
+ opts[:group] = {source[:account] => source[:name]}
110
+ else
111
+ opts[:cidr_ip] = source
112
+ end
113
+
114
+ group.revoke_port_range(from_port.to_i..to_port.to_i, opts)
115
+ end
116
+
117
+ def sync_security_groups(groups)
118
+ return unless groups
119
+
120
+ groups = Rubber::Util::stringify(groups)
121
+ groups = isolate_groups(groups)
122
+ group_keys = groups.keys.clone()
123
+
124
+ # For each group that does already exist in cloud
125
+ cloud_groups = describe_security_groups()
126
+ cloud_groups.each do |cloud_group|
127
+ group_name = cloud_group[:name]
128
+
129
+ # skip those groups that don't belong to this project/env
130
+ next if env.isolate_security_groups && group_name !~ /^#{isolate_prefix}/
131
+
132
+ if group_keys.delete(group_name)
133
+ # sync rules
134
+ capistrano.logger.debug "Security Group already in cloud, syncing rules: #{group_name}"
135
+ group = groups[group_name]
136
+
137
+ # convert the special case default rule into what it actually looks like when
138
+ # we query ec2 so that we can match things up when syncing
139
+ rules = group['rules'].clone
140
+ group['rules'].each do |rule|
141
+ if [2, 3].include?(rule.size) && rule['source_group_name'] && rule['source_group_account']
142
+ rules << rule.merge({'protocol' => 'tcp', 'from_port' => '1', 'to_port' => '65535' })
143
+ rules << rule.merge({'protocol' => 'udp', 'from_port' => '1', 'to_port' => '65535' })
144
+ rules << rule.merge({'protocol' => 'icmp', 'from_port' => '-1', 'to_port' => '-1' })
145
+ rules.delete(rule)
146
+ end
147
+ end
148
+
149
+ rule_maps = []
150
+
151
+ # first collect the rule maps from the request (group/user pairs are duplicated for tcp/udp/icmp,
152
+ # so we need to do this up frnot and remove duplicates before checking against the local rubber rules)
153
+ cloud_group[:permissions].each do |rule|
154
+ source_groups = rule.delete(:source_groups)
155
+ if source_groups
156
+ source_groups.each do |source_group|
157
+ rule_map = rule.clone
158
+ rule_map.delete(:source_ips)
159
+ rule_map[:source_group_name] = source_group[:name]
160
+ rule_map[:source_group_account] = source_group[:account]
161
+ rule_map = Rubber::Util::stringify(rule_map)
162
+ rule_maps << rule_map unless rule_maps.include?(rule_map)
163
+ end
164
+ else
165
+ rule_map = Rubber::Util::stringify(rule)
166
+ rule_maps << rule_map unless rule_maps.include?(rule_map)
167
+ end
168
+ end if cloud_group[:permissions]
169
+ # For each rule, if it exists, do nothing, otherwise remove it as its no longer defined locally
170
+ rule_maps.each do |rule_map|
171
+ if rules.delete(rule_map)
172
+ # rules match, don't need to do anything
173
+ # logger.debug "Rule in sync: #{rule_map.inspect}"
174
+ else
175
+ # rules don't match, remove them from cloud and re-add below
176
+ answer = nil
177
+ msg = "Rule '#{rule_map.inspect}' exists in cloud, but not locally"
178
+ if env.prompt_for_security_group_sync
179
+ answer = Capistrano::CLI.ui.ask("#{msg}, remove from cloud? [y/N]: ")
180
+ else
181
+ capistrano.logger.info(msg)
182
+ end
183
+
184
+ if answer =~ /^y/
185
+ rule_map = Rubber::Util::symbolize_keys(rule_map)
186
+ if rule_map[:source_group_name]
187
+ remove_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], {:name => rule_map[:source_group_name], :account => rule_map[:source_group_account]})
188
+ else
189
+ rule_map[:source_ips].each do |source_ip|
190
+ remove_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], source_ip)
191
+ end if rule_map[:source_ips]
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ rules.each do |rule_map|
198
+ # create non-existing rules
199
+ capistrano.logger.debug "Missing rule, creating: #{rule_map.inspect}"
200
+ rule_map = Rubber::Util::symbolize_keys(rule_map)
201
+ if rule_map[:source_group_name]
202
+ add_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], {:name => rule_map[:source_group_name], :account => rule_map[:source_group_account]})
203
+ else
204
+ rule_map[:source_ips].each do |source_ip|
205
+ add_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], source_ip)
206
+ end if rule_map[:source_ips]
207
+ end
208
+ end
209
+ else
210
+ # delete group
211
+ answer = nil
212
+ msg = "Security group '#{group_name}' exists in cloud but not locally"
213
+ if env.prompt_for_security_group_sync
214
+ answer = Capistrano::CLI.ui.ask("#{msg}, remove from cloud? [y/N]: ")
215
+ else
216
+ capistrano.logger.debug(msg)
217
+ end
218
+ destroy_security_group(group_name) if answer =~ /^y/
219
+ end
220
+ end
221
+
222
+ # For each group that didnt already exist in cloud
223
+ group_keys.each do |group_name|
224
+ group = groups[group_name]
225
+ capistrano.logger.debug "Creating new security group: #{group_name}"
226
+ # create each group
227
+ create_security_group(group_name, group['description'])
228
+ # create rules for group
229
+ group['rules'].each do |rule_map|
230
+ capistrano.logger.debug "Creating new rule: #{rule_map.inspect}"
231
+ rule_map = Rubber::Util::symbolize_keys(rule_map)
232
+ if rule_map[:source_group_name]
233
+ add_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], {:name => rule_map[:source_group_name], :account => rule_map[:source_group_account]})
234
+ else
235
+ rule_map[:source_ips].each do |source_ip|
236
+ add_security_group_rule(group_name, rule_map[:protocol], rule_map[:from_port], rule_map[:to_port], source_ip)
237
+ end if rule_map[:source_ips]
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ end
245
+ end