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.
@@ -10,7 +10,7 @@ module Rubber
10
10
  @capistrano = capistrano
11
11
  end
12
12
 
13
- def before_create_instance(instance_alias, role_names)
13
+ def before_create_instance(instance)
14
14
  # No-op by default.
15
15
  end
16
16
 
@@ -143,6 +143,10 @@ module Rubber
143
143
  source_ips = rule['source_ips']
144
144
 
145
145
  if protocol && source_ips
146
+ if source_ips.respond_to?(:split)
147
+ source_ips = source_ips.split(",").map(&:strip)
148
+ end
149
+
146
150
  source_ips.each do |source|
147
151
  if from_port && to_port
148
152
  if from_port != to_port
@@ -184,6 +188,10 @@ exit 0
184
188
  capistrano.run_script('setup_firewall_rules', script, :hosts => instance.external_ip)
185
189
  end
186
190
 
191
+ def setup_vpc
192
+ # No-op by default.
193
+ end
194
+
187
195
  def describe_security_groups(group_name=nil)
188
196
  rules = capistrano.capture("iptables -S INPUT", :hosts => rubber_env.rubber_instances.collect(&:external_ip)).strip.split("\r\n")
189
197
  scoped_rules = rules.select { |r| r =~ /dport/ }
@@ -223,4 +231,4 @@ exit 0
223
231
  end
224
232
 
225
233
  end
226
- end
234
+ end
@@ -1,3 +1,4 @@
1
+ require 'ext/fog/compute/digital_ocean_v2'
1
2
  require 'rubber/cloud/fog'
2
3
 
3
4
  module Rubber
@@ -8,8 +9,8 @@ module Rubber
8
9
  def initialize(env, capistrano)
9
10
  compute_credentials = {
10
11
  :provider => 'DigitalOcean',
11
- :digitalocean_api_key => env.api_key,
12
- :digitalocean_client_id => env.client_key
12
+ :version => 'v2',
13
+ :digitalocean_token => env.digital_ocean_token,
13
14
  }
14
15
 
15
16
  if env.cloud_providers && env.cloud_providers.aws
@@ -29,17 +30,13 @@ module Rubber
29
30
  super(env, capistrano)
30
31
  end
31
32
 
32
- # As of October 2014 Digital Ocean supports private networking in
33
- # New York 2 (id 4), New York 3 (id 8), Amsterdam 2 (id 5), Amsterdam 3 (id 9), Singapore 1 (id 6) and London 1 (id 7)
34
- REGIONS_WITH_PRIVATE_NETWORKING = [4, 5, 6, 7, 8, 9]
35
-
36
33
  def create_instance(instance_alias, image_name, image_type, security_groups, availability_zone, region, fog_options={})
37
- do_region = compute_provider.regions.find { |r| r.name == region }
34
+ do_region = compute_provider.regions.find { |r| [r.name, r.slug].include?(region) }
38
35
  if do_region.nil?
39
36
  raise "Invalid region for DigitalOcean: #{region}"
40
37
  end
41
38
 
42
- if env.private_networking && ! REGIONS_WITH_PRIVATE_NETWORKING.include?(do_region.id)
39
+ if env.private_networking && ! do_region.features.include?("private_networking")
43
40
  raise "Private networking is enabled, but region #{region} does not support it"
44
41
  end
45
42
 
@@ -48,11 +45,17 @@ module Rubber
48
45
  raise "Invalid image name for DigitalOcean: #{image_name}"
49
46
  end
50
47
 
51
- flavor = compute_provider.flavors.find { |f| f.name == image_type }
48
+ # Downcase image_type for backward compatability with v1
49
+ flavor = compute_provider.flavors.find { |f| f.slug == image_type.downcase }
50
+
52
51
  if flavor.nil?
53
52
  raise "Invalid image type for DigitalOcean: #{image_type}"
54
53
  end
55
54
 
55
+ if env.key_name.nil?
56
+ raise 'missing key_name for DigitalOcean'
57
+ end
58
+
56
59
  # Check if the SSH key has been added to DigitalOcean yet.
57
60
  # TODO (nirvdrum 03/23/13): DigitalOcean has an API for getting a single SSH key, but it hasn't been added to fog yet. We should add it.
58
61
  ssh_key = compute_provider.list_ssh_keys.body['ssh_keys'].find { |key| key['name'] == env.key_name }
@@ -75,13 +78,15 @@ module Rubber
75
78
  end
76
79
 
77
80
  response = compute_provider.servers.create({:name => "#{Rubber.env}-#{instance_alias}",
78
- :image_id => image.id,
79
- :flavor_id => flavor.id,
80
- :region_id => do_region.id,
81
- :ssh_key_ids => [ssh_key['id']],
82
- :private_networking => (env.private_networking.to_s.downcase == 'true')}.
83
- merge(Rubber::Util.symbolize_keys(fog_options))
84
- )
81
+ :image => image.slug,
82
+ :size => flavor.slug,
83
+ :flavor => flavor.slug,
84
+ :region => do_region.slug,
85
+ :ssh_keys => [ssh_key['id']],
86
+ :private_networking => (env.private_networking.to_s.downcase == 'true')
87
+ }
88
+ .merge(Rubber::Util.symbolize_keys(fog_options))
89
+ )
85
90
 
86
91
  response.id
87
92
  end
@@ -99,11 +104,28 @@ module Rubber
99
104
  response.each do |item|
100
105
  instance = {}
101
106
  instance[:id] = item.id
102
- instance[:state] = item.state
103
- instance[:type] = item.flavor_id
104
- instance[:external_ip] = item.public_ip_address
105
- instance[:internal_ip] = item.private_ip_address || item.public_ip_address
106
- instance[:region_id] = item.region_id
107
+ instance[:state] = item.status
108
+ instance[:type] = item.size_slug
109
+
110
+ public_networking_info = item.networks['v4'].find do |n|
111
+ n['type'] == 'public'
112
+ end
113
+
114
+ if public_networking_info
115
+ instance[:external_ip] = public_networking_info['ip_address']
116
+ end
117
+
118
+ private_networking_info = item.networks['v4'].find do |n|
119
+ n['type'] == 'private'
120
+ end
121
+
122
+ if private_networking_info
123
+ instance[:internal_ip] = private_networking_info['ip_address']
124
+ elsif public_networking_info
125
+ instance[:internal_ip] = public_networking_info['ip_address']
126
+ end
127
+
128
+ instance[:region_id] = item.region
107
129
  instance[:provider] = 'digital_ocean'
108
130
  instance[:platform] = Rubber::Platforms::LINUX
109
131
  instances << instance
@@ -115,6 +137,25 @@ module Rubber
115
137
  def active_state
116
138
  'active'
117
139
  end
140
+
141
+ def destroy_instance(instance_id)
142
+ # The Digital Ocean API will return a 422 if we attempt to destroy an
143
+ # instance that's in the middle of booting up, so wait until it's
144
+ # in a non-"new" state
145
+ print 'Waiting for non-new instance state'
146
+
147
+ loop do
148
+ instance = describe_instances(instance_id).first
149
+
150
+ print '.'
151
+
152
+ break unless instance[:state] == 'new'
153
+
154
+ sleep 1
155
+ end
156
+
157
+ response = compute_provider.servers.get(instance_id).delete()
158
+ end
118
159
  end
119
160
  end
120
161
  end
@@ -12,9 +12,9 @@ module Rubber
12
12
  'saved'
13
13
  end
14
14
 
15
- def before_create_instance(instance_alias, role_names)
15
+ def before_create_instance(instance)
16
16
  unless ENV.has_key?('RUN_FROM_VAGRANT')
17
- capistrano.fatal "Since you are using the 'vagrant' provider, you must create instances by running `vagrant up #{instance_alias}`."
17
+ capistrano.fatal "Since you are using the 'vagrant' provider, you must create instances by running `vagrant up #{instance.instance_alias}`."
18
18
  end
19
19
  end
20
20
 
@@ -61,4 +61,4 @@ module Rubber
61
61
  end
62
62
  end
63
63
  end
64
- end
64
+ end
@@ -18,7 +18,7 @@ module Rubber
18
18
  @opts = opts
19
19
 
20
20
  @items = {}
21
- @artifacts = {'volumes' => {}, 'static_ips' => {}}
21
+ @artifacts = {'volumes' => {}, 'static_ips' => {}, 'vpc' => {}}
22
22
 
23
23
  @filters = Rubber::Util::parse_aliases(ENV['FILTER'])
24
24
  @filters, @filters_negated = @filters.partition {|f| f !~ /^-/ }
@@ -196,7 +196,10 @@ module Rubber
196
196
  # The configuration for a single instance
197
197
  class InstanceItem
198
198
  UBUNTU_OS_VERSION_CMD = 'lsb_release -sr'.freeze
199
- VARIABLES_TO_OMIT_IN_SERIALIZATION = ['@capistrano', '@os_version']
199
+ VARIABLES_TO_OMIT_IN_SERIALIZATION = [
200
+ '@capistrano', '@os_version', '@subnet_id', '@vpc_id',
201
+ '@vpc_cidr'
202
+ ]
200
203
 
201
204
  attr_reader :name, :domain, :instance_id, :image_type, :image_id, :security_groups
202
205
  attr_accessor :roles, :zone
@@ -206,6 +209,11 @@ module Rubber
206
209
  attr_accessor :spot_instance_request_id
207
210
  attr_accessor :provider, :platform
208
211
  attr_accessor :capistrano
212
+ attr_accessor :vpc_id
213
+ attr_accessor :network # more generic term for vpc_alias
214
+ attr_accessor :vpc_cidr
215
+ attr_accessor :subnet_id
216
+ attr_accessor :gateway
209
217
 
210
218
  def initialize(name, domain, roles, instance_id, image_type, image_id, security_group_list=[])
211
219
  @name = name
@@ -227,7 +235,7 @@ module Rubber
227
235
  end
228
236
  return item
229
237
  end
230
-
238
+
231
239
  def to_hash
232
240
  hash = {}
233
241
  instance_variables.each do |iv|
@@ -240,11 +248,11 @@ module Rubber
240
248
  end
241
249
  return hash
242
250
  end
243
-
251
+
244
252
  def <=>(rhs)
245
253
  name <=> rhs.name
246
254
  end
247
-
255
+
248
256
  def full_name
249
257
  "#{@name}.#{@domain}"
250
258
  end
@@ -262,20 +262,25 @@ namespace :rubber do
262
262
  def create_instance(instance_alias, instance_roles, create_spot_instance)
263
263
  role_names = instance_roles.collect{|x| x.name}
264
264
  env = rubber_cfg.environment.bind(role_names, instance_alias)
265
+ cloud_env = env.cloud_providers[env.cloud_provider]
265
266
 
266
- monitor.synchronize do
267
- cloud.before_create_instance(instance_alias, role_names)
268
- end
267
+ availability_zone = cloud_env.availability_zone
269
268
 
270
269
  security_groups = get_assigned_security_groups(instance_alias, role_names)
271
270
 
272
- cloud_env = env.cloud_providers[env.cloud_provider]
273
271
  ami = cloud_env.image_id
274
272
  ami_type = cloud_env.image_type
275
- availability_zone = cloud_env.availability_zone
276
273
  region = cloud_env.region
277
274
  fog_options = cloud_env.fog_options || {}
278
275
 
276
+ instance_item = Rubber::Configuration::InstanceItem.new(instance_alias, env.domain, instance_roles, nil, ami_type, ami, security_groups)
277
+
278
+ instance_item.zone = availability_zone
279
+
280
+ monitor.synchronize do
281
+ cloud.before_create_instance(instance_item)
282
+ end
283
+
279
284
  create_spot_instance ||= cloud_env.spot_instance
280
285
 
281
286
  if create_spot_instance
@@ -308,20 +313,54 @@ namespace :rubber do
308
313
  end
309
314
 
310
315
  if !create_spot_instance || (create_spot_instance && max_wait_time < 0)
311
- logger.info "Creating instance #{ami}/#{ami_type}/#{security_groups.join(',') rescue 'Default'}/#{availability_zone || region || 'Default'}"
312
- instance_id = cloud.create_instance(instance_alias, ami, ami_type, security_groups, availability_zone, region, fog_options)
316
+ sg_str = security_groups.join(',') rescue 'Default'
317
+ az_str = availability_zone || region || 'Default'
318
+ vpc_str = instance_item.vpc_id || 'No VPC'
319
+
320
+ logger.info "Creating instance #{ami}/#{ami_type}/#{sg_str}/#{az_str}/#{vpc_str}"
321
+
322
+ if instance_item.vpc_id
323
+ fog_options[:vpc_id] = instance_item.vpc_id
324
+ fog_options[:subnet_id] = instance_item.subnet_id
325
+ fog_options[:associate_public_ip] = (instance_item.gateway == 'public')
326
+ end
313
327
  end
314
328
 
329
+ # Security Groups are handled in the after_create_instance callback of the
330
+ # Vpc cloud provider, so pass an empty array here to make sure it isn't
331
+ # assigned to any other default groups that might be floating around.
332
+ instance_id = cloud.create_instance(
333
+ instance_alias,
334
+ ami,
335
+ ami_type,
336
+ fog_options[:vpc_id] ? security_groups : [],
337
+ availability_zone,
338
+ region,
339
+ fog_options
340
+ )
341
+
315
342
  logger.info "Instance #{instance_alias} created: #{instance_id}"
316
343
 
317
- instance_item = Rubber::Configuration::InstanceItem.new(instance_alias, env.domain, instance_roles, instance_id, ami_type, ami, security_groups)
318
- instance_item.spot_instance_request_id = request_id if create_spot_instance
319
- instance_item.capistrano = self
320
- rubber_instances.add(instance_item)
344
+ # Recreate the InstanceItem now that we have an instance_id
345
+ created_instance_item = Rubber::Configuration::InstanceItem.new(
346
+ instance_alias,
347
+ env.domain,
348
+ instance_roles,
349
+ instance_id,
350
+ ami_type,
351
+ ami,
352
+ security_groups
353
+ )
354
+ created_instance_item.vpc_id = instance_item.vpc_id
355
+ created_instance_item.network = instance_item.network
356
+ created_instance_item.spot_instance_request_id = request_id if create_spot_instance
357
+ created_instance_item.capistrano = self
358
+ created_instance_item.gateway = instance_item.gateway
359
+ rubber_instances.add(created_instance_item)
321
360
  rubber_instances.save()
322
361
 
323
362
  monitor.synchronize do
324
- cloud.after_create_instance(instance_item)
363
+ cloud.after_create_instance(created_instance_item)
325
364
  end
326
365
  end
327
366
 
@@ -370,16 +409,28 @@ namespace :rubber do
370
409
  rubber_instances.save()
371
410
 
372
411
  if instance_item.linux?
373
- # weird cap/netssh bug, sometimes just hangs forever on initial connect, so force a timeout
374
412
  begin
413
+ # davebenvenuti: Sometimes with VPC subnets, we see an IOError
414
+ # (connection refused) while the instance is booting, so give it some
415
+ # time. It would be preferable to catch the exception and retry, but
416
+ # I couldn't figure out a good way to do that.
417
+ sleep 10 if instance_item.network
418
+
419
+ # weird cap/netssh bug, sometimes just hangs forever on initial connect, so force a timeout
375
420
  Timeout::timeout(env.enable_root_login_timeout || 30) do
376
421
  puts 'Trying to enable root login'
377
422
 
378
423
  # turn back on root ssh access if we are using root as the capistrano user for connecting
379
- enable_root_ssh(instance_item.external_ip, fetch(:initial_ssh_user, 'ubuntu')) if user == 'root'
424
+ if Rubber::Util.is_instance_id?(instance_item.gateway)
425
+ ip = instance_item.internal_ip
426
+ else
427
+ ip = instance_item.external_ip
428
+ end
429
+
430
+ enable_root_ssh(ip, fetch(:initial_ssh_user, 'ubuntu')) if user == 'root'
380
431
 
381
432
  # force a connection so if above isn't enabled we still timeout if initial connection hangs
382
- direct_connection(instance_item.external_ip) do
433
+ direct_connection(ip) do
383
434
  run "echo"
384
435
  end
385
436
  end
@@ -625,6 +676,7 @@ namespace :rubber do
625
676
  when /#{instance_item.full_name}/; ''
626
677
  when /#{instance_item.external_host}/; ''
627
678
  when /#{instance_item.external_ip}/; ''
679
+ when /#{instance_item.internal_ip}/; ''
628
680
  else line;
629
681
  end
630
682
  out << line
@@ -650,5 +702,5 @@ namespace :rubber do
650
702
  end if rubber_env.role_dependencies
651
703
  return deps
652
704
  end
653
-
654
705
  end
706
+
@@ -171,7 +171,12 @@ namespace :rubber do
171
171
  end
172
172
 
173
173
  hosts_data.compact.each do |host_name|
174
- local_hosts << ic.external_ip.ljust(18) << host_name << "\n"
174
+ # Private instances must be connected to via an ssh gateway
175
+ if Rubber::Util.is_instance_id?(ic.gateway)
176
+ local_hosts << ic.internal_ip.ljust(18) << host_name << "\n"
177
+ else
178
+ local_hosts << ic.external_ip.ljust(18) << host_name << "\n"
179
+ end
175
180
  end
176
181
 
177
182
  else # non-Windows OS
@@ -189,7 +194,12 @@ namespace :rubber do
189
194
  end
190
195
  end
191
196
 
192
- local_hosts << ic.external_ip << ' ' << hosts_data.compact.join(' ') << "\n"
197
+ # Private instances must be connected to via an ssh gateway
198
+ if Rubber::Util.is_instance_id?(ic.gateway)
199
+ local_hosts << ic.internal_ip << ' ' << hosts_data.compact.join(' ') << "\n"
200
+ else
201
+ local_hosts << ic.external_ip << ' ' << hosts_data.compact.join(' ') << "\n"
202
+ end
193
203
  end
194
204
  end
195
205
 
@@ -0,0 +1,92 @@
1
+ require 'rubber/environment'
2
+
3
+ namespace :rubber do
4
+ desc <<-DESC
5
+ Sets up the VPC identified by vpc_alias
6
+ DESC
7
+ required_task :create_vpc do
8
+ vpc_alias = get_env("ALIAS", "VPC Alias: ", true, Rubber.config.cloud_providers['aws'].vpc_alias)
9
+ vpc_cidr = get_env("CIDR", "CIDR Block (eg: 10.0.0.0/16): ", true, Rubber.config.cloud_providers['aws'].vpc_cidr)
10
+
11
+ cloud.setup_vpc(vpc_alias, vpc_cidr)
12
+ end
13
+
14
+ desc <<-DESC
15
+ List all VPCs (not just Rubber-created)
16
+ DESC
17
+ required_task :describe_vpcs do
18
+ vpcs = cloud.describe_vpcs
19
+
20
+ if vpcs
21
+ puts "VPCs:"
22
+
23
+ vpcs.each do |vpc|
24
+ puts "\t#{vpc[:id]}\t#{vpc[:name]}\t#{vpc[:rubber_alias]}"
25
+ vpc[:subnets].each do |subnet|
26
+ puts "\t\t#{subnet[:id]}\t#{subnet[:rubber_alias]}\t#{subnet[:cidr_block]}\t#{subnet[:public] ? 'public' : 'private'}"
27
+ end
28
+ end
29
+ else
30
+ puts "No VPCs found"
31
+ end
32
+ end
33
+
34
+ desc <<-DESC
35
+ Configure a new subnet on the configured VPC
36
+ DESC
37
+ required_task :create_vpc_subnet do
38
+ vpc_alias = get_env("ALIAS", "VPC Alias: ", true, Rubber.config.cloud_providers['aws'].vpc_alias)
39
+ cidr = get_env("CIDR", "CIDR Block (eg: 10.0.0.0/24): ", true)
40
+ gateway = get_env("GATEWAY", "Gateway (\"public\" or NAT instance alias): ", true)
41
+ zone = get_env("ZONE", "Availability Zone: ", true, Rubber.config.cloud_providers['aws'].availability_zone)
42
+ name = get_env("NAME", "Subnet name: ", true)
43
+
44
+ vpc_id = cloud.compute_provider.vpcs.all('tag:RubberVpcAlias' => vpc_alias).first.id
45
+
46
+ unless gateway == 'public'
47
+ gateway = Rubber.config.rubber_instances.find { |i|
48
+ i.name == gateway
49
+ }.instance_id
50
+ end
51
+
52
+ private_nic = {
53
+ subnet_cidr: subnet_cidr,
54
+ gateway: gateway
55
+ }
56
+
57
+ cloud.setup_vpc_subnet(vpc_id, vpc_alias, private_nic, zone, name)
58
+ end
59
+
60
+ desc <<-DESC
61
+ Destroy any VPC with a Rubber-defined Alias
62
+ DESC
63
+ required_task :destroy_vpc do
64
+ vpc_alias = get_env("ALIAS", "VPC Alias: ", true, Rubber.config.cloud_providers['aws'].vpc_alias)
65
+ value = Capistrano::CLI.ui.ask("Are you sure you want to destroy vpc #{vpc_alias} [yes/NO]?: ")
66
+
67
+ if value == 'yes'
68
+ cloud.destroy_vpc(vpc_alias)
69
+ else
70
+ fatal "aborted", 0
71
+ end
72
+ end
73
+
74
+ desc <<-DESC
75
+ Refresh the public gateway on a public subnet, or configure a new NAT instance on a private subnet.
76
+ This currently does not support switching a public subnet to private or vice versa.
77
+ DESC
78
+ required_task :update_vpc_gateway do
79
+ vpc_alias = get_env("ALIAS", "VPC Alias: ", true, Rubber.config.cloud_providers['aws'].vpc_alias)
80
+ zone = get_env("ZONE", "Availability Zone: ", true, Rubber.config.cloud_providers['aws'].availability_zone)
81
+ gateway = get_env("GATEWAY", "Gateway (\"public\" or NAT instance alias): ", true)
82
+
83
+ unless gateway == 'public'
84
+ gateway = Rubber.config.rubber_instances.find { |i|
85
+ i.name == gateway
86
+ }.instance_id
87
+ end
88
+
89
+ cloud.update_vpc_gateway(vpc_alias, zone, gateway)
90
+ end
91
+ end
92
+