chef-provisioning-aws 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +2 -0
  3. data/lib/chef/provider/aws_auto_scaling_group.rb +30 -41
  4. data/lib/chef/provider/aws_dhcp_options.rb +70 -0
  5. data/lib/chef/provider/aws_ebs_volume.rb +182 -34
  6. data/lib/chef/provider/aws_eip_address.rb +63 -60
  7. data/lib/chef/provider/aws_key_pair.rb +18 -27
  8. data/lib/chef/provider/aws_launch_configuration.rb +50 -0
  9. data/lib/chef/provider/aws_route_table.rb +122 -0
  10. data/lib/chef/provider/aws_s3_bucket.rb +42 -49
  11. data/lib/chef/provider/aws_security_group.rb +252 -59
  12. data/lib/chef/provider/aws_sns_topic.rb +10 -26
  13. data/lib/chef/provider/aws_sqs_queue.rb +16 -38
  14. data/lib/chef/provider/aws_subnet.rb +85 -32
  15. data/lib/chef/provider/aws_vpc.rb +163 -23
  16. data/lib/chef/provisioning/aws_driver.rb +18 -1
  17. data/lib/chef/provisioning/aws_driver/aws_provider.rb +206 -0
  18. data/lib/chef/provisioning/aws_driver/aws_resource.rb +186 -0
  19. data/lib/chef/provisioning/aws_driver/aws_resource_with_entry.rb +114 -0
  20. data/lib/chef/provisioning/aws_driver/driver.rb +317 -255
  21. data/lib/chef/provisioning/aws_driver/resources.rb +8 -5
  22. data/lib/chef/provisioning/aws_driver/super_lwrp.rb +45 -0
  23. data/lib/chef/provisioning/aws_driver/version.rb +1 -1
  24. data/lib/chef/resource/aws_auto_scaling_group.rb +15 -13
  25. data/lib/chef/resource/aws_dhcp_options.rb +57 -0
  26. data/lib/chef/resource/aws_ebs_volume.rb +20 -22
  27. data/lib/chef/resource/aws_eip_address.rb +50 -25
  28. data/lib/chef/resource/aws_image.rb +20 -0
  29. data/lib/chef/resource/aws_instance.rb +20 -0
  30. data/lib/chef/resource/aws_internet_gateway.rb +16 -0
  31. data/lib/chef/resource/aws_key_pair.rb +6 -10
  32. data/lib/chef/resource/aws_launch_configuration.rb +15 -0
  33. data/lib/chef/resource/aws_load_balancer.rb +16 -0
  34. data/lib/chef/resource/aws_network_interface.rb +16 -0
  35. data/lib/chef/resource/aws_route_table.rb +76 -0
  36. data/lib/chef/resource/aws_s3_bucket.rb +8 -18
  37. data/lib/chef/resource/aws_security_group.rb +49 -19
  38. data/lib/chef/resource/aws_sns_topic.rb +14 -15
  39. data/lib/chef/resource/aws_sqs_queue.rb +16 -14
  40. data/lib/chef/resource/aws_subnet.rb +87 -17
  41. data/lib/chef/resource/aws_vpc.rb +137 -15
  42. data/spec/integration/aws_security_group_spec.rb +55 -0
  43. data/spec/spec_helper.rb +8 -2
  44. data/spec/support/aws_support.rb +211 -0
  45. metadata +33 -10
  46. data/lib/chef/provider/aws_launch_config.rb +0 -43
  47. data/lib/chef/provider/aws_provider.rb +0 -22
  48. data/lib/chef/provisioning/aws_driver/aws_profile.rb +0 -73
  49. data/lib/chef/resource/aws_launch_config.rb +0 -14
  50. data/lib/chef/resource/aws_resource.rb +0 -10
  51. data/spec/chef_zero_rspec_helper.rb +0 -8
  52. data/spec/unit/provider/aws_subnet_spec.rb +0 -67
  53. data/spec/unit/resource/aws_subnet_spec.rb +0 -23
@@ -1,29 +1,24 @@
1
1
  require 'chef/provider/lwrp_base'
2
- require 'chef/provisioning/aws_driver'
3
- require 'chef/provider/aws_provider'
2
+ require 'chef/provisioning/aws_driver/aws_provider'
4
3
  require 'aws-sdk-v1'
5
4
 
6
5
 
7
- class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
8
-
9
- use_inline_resources
10
-
11
- def whyrun_supported?
12
- true
13
- end
6
+ class Chef::Provider::AwsKeyPair < Chef::Provisioning::AWSDriver::AWSProvider
14
7
 
15
8
  action :create do
16
9
  create_key(:create)
17
10
  end
18
11
 
19
- action :delete do
12
+ action :destroy do
20
13
  if current_resource_exists?
21
- new_driver.ec2.key_pairs[new_resource.name].delete
14
+ converge_by "delete AWS key pair #{new_resource.name} on region #{region}" do
15
+ driver.ec2.key_pairs[new_resource.name].delete
16
+ end
22
17
  end
23
18
  end
24
19
 
25
20
  def key_description
26
- "#{new_resource.name} on #{new_driver.driver_url}"
21
+ "#{new_resource.name} on #{driver.driver_url}"
27
22
  end
28
23
 
29
24
  @@use_pkcs8 = nil # For Ruby 1.9 and below, PKCS can be run
@@ -79,8 +74,8 @@ class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
79
74
  if !new_fingerprints.any? { |f| compare_public_key f }
80
75
  if new_resource.allow_overwrite
81
76
  converge_by "update #{key_description} to match local key at #{new_resource.private_key_path}" do
82
- new_driver.ec2.key_pairs[new_resource.name].delete
83
- new_driver.ec2.key_pairs.import(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
77
+ driver.ec2.key_pairs[new_resource.name].delete
78
+ driver.ec2.key_pairs.import(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
84
79
  end
85
80
  else
86
81
  raise "#{key_description} with fingerprint #{@current_fingerprint} does not match local key fingerprint(s) #{new_fingerprints}, and allow_overwrite is false!"
@@ -92,12 +87,12 @@ class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
92
87
 
93
88
  # Create key
94
89
  converge_by "create #{key_description} from local key at #{new_resource.private_key_path}" do
95
- new_driver.ec2.key_pairs.import(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
90
+ driver.ec2.key_pairs.import(new_resource.name, Cheffish::KeyFormatter.encode(desired_key, :format => :openssh))
96
91
  end
97
92
  end
98
93
  end
99
94
 
100
- def new_driver
95
+ def driver
101
96
  run_context.chef_provisioning.driver_for(new_resource.driver)
102
97
  end
103
98
 
@@ -135,15 +130,11 @@ class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
135
130
  end
136
131
 
137
132
  def existing_keypair
138
- @existing_keypair ||= begin
139
- new_driver.ec2.key_pairs[fqn]
140
- rescue
141
- nil
142
- end
133
+ @existing_keypair ||= new_resource.aws_object
143
134
  end
144
135
 
145
136
  def current_resource_exists?
146
- @current_resource.action != [ :delete ]
137
+ @current_resource.action != [ :destroy ]
147
138
  end
148
139
 
149
140
  def compare_public_key(new)
@@ -160,9 +151,9 @@ class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
160
151
  private_key_path = new_resource.private_key_path || new_resource.name
161
152
  if private_key_path.is_a?(Symbol)
162
153
  private_key_path
163
- elsif Pathname.new(private_key_path).relative? && new_driver.config[:private_key_write_path]
154
+ elsif Pathname.new(private_key_path).relative? && driver.config[:private_key_write_path]
164
155
  @should_create_directory = true
165
- ::File.join(new_driver.config[:private_key_write_path], private_key_path)
156
+ ::File.join(driver.config[:private_key_write_path], private_key_path)
166
157
  else
167
158
  private_key_path
168
159
  end
@@ -175,11 +166,11 @@ class Chef::Provider::AwsKeyPair < Chef::Provider::AwsProvider
175
166
  def load_current_resource
176
167
  @current_resource = Chef::Resource::AwsKeyPair.new(new_resource.name, run_context)
177
168
 
178
- current_key_pair = new_driver.ec2.key_pairs[new_resource.name]
179
- if current_key_pair && current_key_pair.exists?
169
+ current_key_pair = new_resource.aws_object
170
+ if current_key_pair
180
171
  @current_fingerprint = current_key_pair ? current_key_pair.fingerprint : nil
181
172
  else
182
- current_resource.action :delete
173
+ current_resource.action :destroy
183
174
  end
184
175
 
185
176
  if new_private_key_path && ::File.exist?(new_private_key_path)
@@ -0,0 +1,50 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+ require 'chef/resource/aws_image'
3
+
4
+ class Chef::Provider::AwsLaunchConfiguration < Chef::Provisioning::AWSDriver::AWSProvider
5
+ protected
6
+
7
+ def create_aws_object
8
+ image = Chef::Resource::AwsImage.get_aws_object_id(new_resource.image, resource: new_resource)
9
+ instance_type = new_resource.instance_type || new_resource.driver.default_instance_type
10
+ options = AWSResource.lookup_options(new_resource.options || options, resource: new_resource)
11
+
12
+ converge_by "Creating new Launch Configuration #{new_resource.name} in #{region}" do
13
+ new_resource.driver.auto_scaling.launch_configurations.create(
14
+ new_resource.name,
15
+ image,
16
+ instance_type,
17
+ options
18
+ )
19
+ end
20
+ end
21
+
22
+ def update_aws_object(launch_configuration)
23
+ if new_resource.image
24
+ image = Chef::Resource::AwsImage.get_aws_object_id(new_resource.image, resource: new_resource)
25
+ if image != launch_configuration.image_id
26
+ raise "#{new_resource.to_s}.image = #{new_resource.image} (#{image.id}), but actual launch configuration has image set to #{launch_configuration.image_id}. Cannot be modified!"
27
+ end
28
+ end
29
+ if new_resource.instance_type
30
+ if new_resource.instance_type != launch_configuration.instance_type
31
+ raise "#{new_resource.to_s}.instance_type = #{new_resource.instance_type}, but actual launch configuration has instance_type set to #{launch_configuration.instance_type}. Cannot be modified!"
32
+ end
33
+ end
34
+ # TODO compare options
35
+ end
36
+
37
+ def destroy_aws_object(launch_configuration)
38
+ converge_by "delete Launch Configuration #{new_resource.name} in #{region}" do
39
+ # TODO add a timeout here.
40
+ # TODO is InUse really a status guaranteed to go away??
41
+ begin
42
+ launch_configuration.delete
43
+ rescue AWS::AutoScaling::Errors::ResourceInUse
44
+ sleep 5
45
+ retry
46
+ end
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,122 @@
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
+
3
+ class Chef::Provider::AwsRouteTable < Chef::Provisioning::AWSDriver::AWSProvider
4
+
5
+ def action_create
6
+ route_table = super
7
+
8
+ if !new_resource.routes.nil?
9
+ update_routes(vpc, route_table)
10
+ end
11
+ end
12
+
13
+ protected
14
+
15
+ def create_aws_object
16
+ options = {}
17
+ options[:vpc] = new_resource.vpc
18
+ options = AWSResource.lookup_options(options, resource: new_resource)
19
+ self.vpc = Chef::Resource::AwsVpc.get_aws_object(options[:vpc], resource: new_resource)
20
+
21
+ converge_by "create new route table #{new_resource.name} in VPC #{new_resource.vpc} (#{vpc.id}) and region #{region}" do
22
+ route_table = new_resource.driver.ec2.route_tables.create(options)
23
+ route_table.tags['Name'] = new_resource.name
24
+ route_table
25
+ end
26
+ end
27
+
28
+ def update_aws_object(route_table)
29
+ self.vpc = route_table.vpc
30
+
31
+ if new_resource.vpc
32
+ desired_vpc = Chef::Resource::AwsVpc.get_aws_object(new_resource.vpc, resource: new_resource)
33
+ if vpc != desired_vpc
34
+ raise "VPC of route table #{new_resource.name} (#{route_table.id}) is #{route_table.vpc.id}, but desired vpc is #{new_resource.vpc}! Moving (or rather, recreating) a route table is not yet supported."
35
+ end
36
+ end
37
+ end
38
+
39
+ def destroy_aws_object(route_table)
40
+ converge_by "delete route table #{new_resource.name} (#{route_table.id}) in #{region}" do
41
+ route_table.delete
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ attr_accessor :vpc
48
+
49
+ def update_routes(vpc, route_table)
50
+ # Collect current routes
51
+ current_routes = {}
52
+ route_table.routes.each do |route|
53
+ # Ignore the automatic local route
54
+ next if route.target.id == 'local'
55
+ current_routes[route.destination_cidr_block] = route
56
+ end
57
+
58
+ # Add or replace routes from `routes`
59
+ new_resource.routes.each do |destination_cidr_block, route_target|
60
+ options = get_route_target(vpc, route_target)
61
+ target = options.values.first
62
+ # If we already have a route to that CIDR block, replace it.
63
+ if current_routes[destination_cidr_block]
64
+ current_route = current_routes.delete(destination_cidr_block)
65
+ if current_route.target != target
66
+ action_handler.perform_action "reroute #{destination_cidr_block} to #{route_target} (#{target.id}) instead of #{current_route.target.id}" do
67
+ current_route.replace(options)
68
+ end
69
+ end
70
+ else
71
+ action_handler.perform_action "route #{destination_cidr_block} to #{route_target} (#{target.id})" do
72
+ route_table.create_route(destination_cidr_block, options)
73
+ end
74
+ end
75
+ end
76
+
77
+ # Delete anything that's left (that wasn't replaced)
78
+ current_routes.values.each do |current_route|
79
+ action_handler.perform_action "remove route sending #{current_route.destination_cidr_block} to #{current_route.target.id}" do
80
+ current_route.delete
81
+ end
82
+ end
83
+ end
84
+
85
+ def get_route_target(vpc, route_target)
86
+ case route_target
87
+ when :internet_gateway
88
+ route_target = { internet_gateway: vpc.internet_gateway }
89
+ if !route_target[:internet_gateway]
90
+ raise "VPC #{new_resource.vpc} (#{vpc.id}) does not have an internet gateway to route to! Use `internet_gateway true` on the VPC itself to create one."
91
+ end
92
+ when /^igw-[A-Fa-f0-9]{8}$/, Chef::Resource::AwsInternetGateway, AWS::EC2::InternetGateway
93
+ route_target = { internet_gateway: route_target }
94
+ when /^eni-[A-Fa-f0-9]{8}$/, Chef::Resource::AwsNetworkInterface, AWS::EC2::NetworkInterface
95
+ route_target = { network_interface: route_target }
96
+ when String, Chef::Resource::AwsInstance
97
+ route_target = { instance: route_target }
98
+ when Chef::Resource::Machine
99
+ route_target = { instance: route_target.name }
100
+ when AWS::EC2::Instance
101
+ route_target = { instance: route_target.id }
102
+ when Hash
103
+ if route_target.size != 1
104
+ raise "Route target #{route_target} must have exactly one key, either :internet_gateway, :instance or :network_interface!"
105
+ end
106
+ route_target = route_target.dup
107
+ else
108
+ raise "Unrecognized route destination #{route_target.inspect}"
109
+ end
110
+ route_target.each do |name, value|
111
+ case name
112
+ when :instance
113
+ route_target[name] = Chef::Resource::AwsInstance.get_aws_object(value, resource: new_resource)
114
+ when :network_interface
115
+ route_target[name] = Chef::Resource::AwsNetworkInterface.get_aws_object(value, resource: new_resource)
116
+ when :internet_gateway
117
+ route_target[name] = Chef::Resource::AwsInternetGateway.get_aws_object(value, resource: new_resource)
118
+ end
119
+ end
120
+ route_target
121
+ end
122
+ end
@@ -1,81 +1,74 @@
1
- require 'chef/provider/aws_provider'
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
2
  require 'date'
3
3
 
4
- class Chef::Provider::AwsS3Bucket < Chef::Provider::AwsProvider
5
- action :create do
6
- if existing_bucket == nil
7
- converge_by "Creating new S3 bucket #{fqn}" do
8
- bucket = new_driver.s3.buckets.create(fqn)
9
- bucket.tags['Name'] = new_resource.name
10
- end
11
- end
12
-
13
- if modifies_website_configuration?
14
- if new_resource.enable_website_hosting
15
- converge_by "Setting website configuration for bucket #{fqn}" do
16
- existing_bucket.website_configuration = AWS::S3::WebsiteConfiguration.new(
4
+ class Chef::Provider::AwsS3Bucket < Chef::Provisioning::AWSDriver::AWSProvider
5
+ def action_create
6
+ bucket = super
7
+
8
+ if new_resource.enable_website_hosting
9
+ if !bucket.website?
10
+ converge_by "Enabling website configuration for bucket #{new_resource.name}" do
11
+ bucket.website_configuration = AWS::S3::WebsiteConfiguration.new(
17
12
  new_resource.website_options)
18
13
  end
19
- else
20
- converge_by "Disabling website configuration for bucket #{fqn}" do
21
- existing_bucket.website_configuration = nil
14
+ elsif modifies_website_configuration?(bucket)
15
+ converge_by "Reconfiguring website configuration for bucket #{new_resource.name} to #{new_resource.website_options}" do
16
+ bucket.website_configuration = AWS::S3::WebsiteConfiguration.new(
17
+ new_resource.website_options)
18
+ end
19
+ end
20
+ else
21
+ if bucket.website?
22
+ converge_by "Disabling website configuration for bucket #{new_resource.name}" do
23
+ bucket.website_configuration = nil
22
24
  end
23
25
  end
24
26
  end
25
- new_resource.endpoint "#{fqn}.s3-website-#{s3_website_endpoint_region}.amazonaws.com"
26
- new_resource.save
27
27
  end
28
28
 
29
- action :delete do
30
- if existing_bucket
31
- converge_by "Deleting S3 bucket #{fqn}" do
32
- existing_bucket.delete
33
- end
29
+ protected
30
+
31
+ def create_aws_object
32
+ converge_by "create new S3 bucket #{new_resource.name}" do
33
+ bucket = new_resource.driver.s3.buckets.create(new_resource.name)
34
+ bucket.tags['Name'] = new_resource.name
35
+ bucket
34
36
  end
37
+ end
35
38
 
36
- new_resource.delete
39
+ def update_aws_object(bucket)
37
40
  end
38
41
 
39
- def existing_bucket
40
- Chef::Log.debug("Checking for S3 bucket #{fqn}")
41
- @existing_bucket ||= new_driver.s3.buckets[fqn] if new_driver.s3.buckets[fqn].exists?
42
+ def destroy_aws_object(bucket)
43
+ converge_by "delete S3 bucket #{new_resource.name}" do
44
+ bucket.delete
45
+ end
42
46
  end
43
47
 
44
- def modifies_website_configuration?
48
+ private
49
+
50
+ def modifies_website_configuration?(aws_object)
45
51
  # This is incomplete, routing rules have many optional values, so its
46
52
  # possible aws will put in default values for those which won't be in
47
53
  # the requested config.
48
- new_web_config = new_resource.website_options
49
- current_web_config = current_website_configuration
54
+ new_web_config = new_resource.website_options || {}
50
55
 
51
- !!existing_bucket.website_configuration != new_resource.enable_website_hosting ||
52
- (current_web_config[:index_document] != new_web_config.fetch(:index_document, {}) ||
53
- current_web_config[:error_document] != new_web_config.fetch(:error_document, {}) ||
54
- current_web_config[:routing_rules] != new_web_config.fetch(:routing_rules, []))
55
- end
56
+ current_web_config = (aws_object.website_configuration || {}).to_hash
56
57
 
57
- def current_website_configuration
58
- if existing_bucket.website_configuration
59
- existing_bucket.website_configuration.to_hash
60
- else
61
- {}
62
- end
58
+ (current_web_config[:index_document] != new_web_config.fetch(:index_document, {}) ||
59
+ current_web_config[:error_document] != new_web_config.fetch(:error_document, {}) ||
60
+ current_web_config[:routing_rules] != new_web_config.fetch(:routing_rules, []))
63
61
  end
64
62
 
65
63
  def s3_website_endpoint_region
66
64
  # ¯\_(ツ)_/¯
67
- case existing_bucket.location_constraint
65
+ case aws_object.location_constraint
68
66
  when nil, 'US'
69
67
  'us-east-1'
70
68
  when 'EU'
71
69
  'eu-west-1'
72
70
  else
73
- existing_bucket.location_constraint
71
+ aws_object.location_constraint
74
72
  end
75
73
  end
76
-
77
- # Fully qualified bucket name (i.e resource_region unless otherwise specified)
78
- def id
79
- new_resource.bucket_name || new_resource.name
80
- end
81
74
  end
@@ -1,89 +1,282 @@
1
- require 'chef/provider/aws_provider'
1
+ require 'chef/provisioning/aws_driver/aws_provider'
2
2
  require 'date'
3
+ require 'ipaddr'
4
+ require 'set'
3
5
 
4
- class Chef::Provider::AwsSecurityGroup < Chef::Provider::AwsProvider
5
-
6
- action :create do
7
- if existing_sg == nil
8
- converge_by "Creating new SG #{new_resource.name} in #{new_driver.aws_config.region}" do
9
- opts = {
10
- :description => new_resource.description,
11
- :vpc => nil
12
- }
13
- # Use VPC ID if provided, otherwise lookup VPC by name
14
- if new_resource.vpc_id
15
- opts[:vpc] = new_resource.vpc_id
16
- elsif new_resource.vpc_name
17
- existing_vpc = new_driver.ec2.vpcs.with_tag('Name', new_resource.vpc_name).first
18
- Chef::Log.debug("Existing VPC: #{existing_vpc.inspect}")
19
- if existing_vpc
20
- opts[:vpc] = existing_vpc
21
- end
22
- end
6
+ class Chef::Provider::AwsSecurityGroup < Chef::Provisioning::AWSDriver::AWSProvider
7
+
8
+ def action_create
9
+ sg = super
10
+
11
+ apply_rules(sg)
12
+ end
13
+
14
+ protected
15
+
16
+ def create_aws_object
17
+ converge_by "Creating new SG #{new_resource.name} in #{region}" do
18
+ options = { description: new_resource.description }
19
+ options[:vpc] = new_resource.vpc if new_resource.vpc
20
+ options = AWSResource.lookup_options(options, resource: new_resource)
21
+ Chef::Log.debug("VPC: #{options[:vpc]}")
22
+
23
+ sg = new_resource.driver.ec2.security_groups.create(new_resource.name, options)
24
+ end
25
+ end
23
26
 
24
- sg = new_driver.ec2.security_groups.create(new_resource.name, opts)
25
- new_resource.security_group_id sg.group_id
26
- new_resource.save
27
+ def update_aws_object(sg)
28
+ if !new_resource.description.nil? && new_resource.description != sg.description
29
+ raise "Security Group descriptions cannot be changed after being created! Desired description for #{new_resource.name} (#{sg.id}) was \"#{new_resource.description}\" and actual description is \"#{sg.description}\""
30
+ end
31
+ if !new_resource.vpc.nil?
32
+ desired_vpc = Chef::Resource::AwsVpc.get_aws_object_id(new_resource.vpc, resource: new_resource)
33
+ if desired_vpc != sg.vpc_id
34
+ raise "Security Group VPC cannot be changed after being created! Desired VPC for #{new_resource.name} (#{sg.id}) was #{new_resource.vpc} (#{desired_vpc}) and actual VPC is #{sg.vpc_id}"
27
35
  end
28
36
  end
37
+ apply_rules(sg)
38
+ end
29
39
 
30
- # Update rules
31
- apply_rules(existing_sg)
40
+ def destroy_aws_object(sg)
41
+ converge_by "Deleting SG #{new_resource.name} in #{region}" do
42
+ sg.delete
43
+ end
32
44
  end
33
45
 
34
- action :delete do
35
- if existing_sg
36
- converge_by "Deleting SG #{new_resource.name} in #{new_driver.aws_config.region}" do
37
- existing_sg.delete
38
- end
46
+ private
47
+
48
+ def apply_rules(sg)
49
+ vpc = sg.vpc
50
+ if !new_resource.outbound_rules.nil?
51
+ update_outbound_rules(sg, vpc)
39
52
  end
40
53
 
41
- new_resource.delete
54
+ if !new_resource.inbound_rules.nil?
55
+ update_inbound_rules(sg, vpc)
56
+ end
42
57
  end
43
58
 
44
- # TODO check existing rules and compare / remove?
45
- def apply_rules(security_group)
46
- # Incoming
47
- if new_resource.inbound_rules
59
+ def update_inbound_rules(sg, vpc)
60
+ #
61
+ # Get desired rules
62
+ #
63
+ desired_rules = {}
64
+
65
+ case new_resource.inbound_rules
66
+ when Hash
67
+ new_resource.inbound_rules.each do |sources_spec, port_spec|
68
+ add_rule(desired_rules, get_port_ranges(port_spec), get_actors(vpc, sources_spec))
69
+ end
70
+
71
+ when Array
72
+ # [ { port: X, protocol: Y, sources: [ ... ]}]
48
73
  new_resource.inbound_rules.each do |rule|
49
- begin
50
- converge_by "Updating SG #{new_resource.name} in #{new_driver.aws_config.region} to allow inbound #{rule[:protocol]}/#{rule[:ports]} from #{rule[:sources]}" do
51
- security_group.authorize_ingress(rule[:protocol], rule[:ports], *rule[:sources])
52
- end
53
- rescue AWS::EC2::Errors::InvalidPermission::Duplicate
54
- Chef::Log.debug 'Duplicate rule, ignoring.'
55
- end
74
+ port_ranges = get_port_ranges(port_range: rule[:port], protocol: rule[:protocol])
75
+ add_rule(desired_rules, port_ranges, get_actors(vpc, rule[:sources]))
56
76
  end
77
+
78
+ else
79
+ raise ArgumentError, "inbound_rules must be a Hash or Array (was #{new_resource.inbound_rules.inspect})"
57
80
  end
58
81
 
59
- # Outgoing
60
- if new_resource.outbound_rules
82
+ #
83
+ # Actually update the rules (remove, add)
84
+ #
85
+ update_rules(desired_rules, sg.ip_permissions_list,
86
+
87
+ authorize: proc do |port_range, protocol, actors|
88
+ names = actors.map { |a| a.is_a?(Hash) ? a[:group_id] : a }
89
+ converge_by "authorize #{names.join(', ')} to send traffic to group #{new_resource.name} (#{sg.id}) on port_range #{port_range} with protocol #{protocol}" do
90
+ sg.authorize_ingress(protocol, port_range, *actors)
91
+ end
92
+ end,
93
+
94
+ revoke: proc do |port_range, protocol, actors|
95
+ names = actors.map { |a| a.is_a?(Hash) ? a[:group_id] : a }
96
+ converge_by "revoke the ability of #{names.join(', ')} to send traffic to group #{new_resource.name} (#{sg.id}) on port_range #{port_range} with protocol #{protocol}" do
97
+ sg.revoke_ingress(protocol, port_range, *actors)
98
+ end
99
+ end
100
+ )
101
+ end
102
+
103
+ def update_outbound_rules(sg, vpc)
104
+ #
105
+ # Get desired rules
106
+ #
107
+ desired_rules = {}
108
+
109
+ case new_resource.outbound_rules
110
+ when Hash
111
+ new_resource.outbound_rules.each do |port_spec, sources_spec|
112
+ add_rule(desired_rules, get_port_ranges(port_spec), get_actors(vpc, sources_spec))
113
+ end
114
+
115
+ when Array
116
+ # [ { port: X, protocol: Y, sources: [ ... ]}]
61
117
  new_resource.outbound_rules.each do |rule|
62
- begin
63
- converge_by "Updating SG #{new_resource.name} in #{new_driver.aws_config.region} to allow outbound #{rule[:protocol]}/#{rule[:ports]} to #{rule[:destinations]}" do
64
- security_group.authorize_egress( *rule[:destinations], :protocol => rule[:protocol], :ports => rule[:ports])
65
- end
66
- rescue AWS::EC2::Errors::InvalidPermission::Duplicate
67
- Chef::Log.debug 'Duplicate rule, ignoring.'
118
+ port_ranges = get_port_ranges(port_range: rule[:port], protocol: rule[:protocol])
119
+ add_rule(desired_rules, port_ranges, get_actors(vpc, rule[:destinations]))
120
+ end
121
+
122
+ else
123
+ raise ArgumentError, "outbound_rules must be a Hash or Array (was #{new_resource.outbound_rules.inspect})"
124
+ end
125
+
126
+ #
127
+ # Actually update the rules (remove, add)
128
+ #
129
+ update_rules(desired_rules, sg.ip_permissions_list_egress,
130
+
131
+ authorize: proc do |port_range, protocol, actors|
132
+ names = actors.map { |a| a.is_a?(Hash) ? a[:group_id] : a }
133
+ converge_by "authorize group #{new_resource.name} (#{sg.id}) to send traffic to #{names.join(', ')} on port_range #{port_range} with protocol #{protocol}" do
134
+ sg.authorize_egress(*actors, ports: port_range, protocol: protocol)
135
+ end
136
+ end,
137
+
138
+ revoke: proc do |port_range, protocol, actors|
139
+ names = actors.map { |a| a.is_a?(Hash) ? a[:group_id] : a }
140
+ converge_by "revoke the ability of group #{new_resource.name} (#{sg.id}) to send traffic to #{names.join(', ')} on port_range #{port_range} with protocol #{protocol}" do
141
+ sg.revoke_egress(*actors, ports: port_range, protocol: protocol)
68
142
  end
69
143
  end
144
+ )
145
+ end
146
+
147
+ def update_rules(desired_rules, actual_rules_list, authorize: nil, revoke: nil)
148
+ actual_rules = {}
149
+ actual_rules_list.each do |rule|
150
+ port_range = {
151
+ port_range: rule[:from_port] ? rule[:from_port]..rule[:to_port] : nil,
152
+ protocol: rule[:ip_protocol].to_s.to_sym
153
+ }
154
+ add_rule(actual_rules, [ port_range ], rule[:groups]) if rule[:groups]
155
+ add_rule(actual_rules, [ port_range ], rule[:ip_ranges].map { |r| r[:cidr_ip] }) if rule[:ip_ranges]
156
+ end
157
+
158
+ #
159
+ # Get the list of permissions to add and remove
160
+ #
161
+ actual_rules.each do |port_range, actors|
162
+ if desired_rules[port_range]
163
+ intersection = actors & desired_rules[port_range]
164
+ # Anything unhandled in desired_rules will be added
165
+ desired_rules[port_range] -= intersection
166
+ # Anything unhandled in actual_rules will be removed
167
+ actual_rules[port_range] -= intersection
168
+ end
169
+ end
170
+
171
+ #
172
+ # Add any new rules
173
+ #
174
+ desired_rules.each do |port_range, actors|
175
+ unless actors.empty?
176
+ authorize.call(port_range[:port_range], port_range[:protocol], actors)
177
+ end
178
+ end
179
+
180
+ #
181
+ # Remove any rules no longer in effect
182
+ #
183
+ actual_rules.each do |port_range, actors|
184
+ unless actors.empty?
185
+ revoke.call(port_range[:port_range], port_range[:protocol], actors)
186
+ end
187
+ end
188
+ end
189
+
190
+ def add_rule(rules, port_ranges, actors)
191
+ unless actors.empty?
192
+ port_ranges.each do |port_range|
193
+ rules[port_range] ||= Set.new
194
+ rules[port_range] += actors
195
+ end
70
196
  end
71
197
  end
72
198
 
73
- def existing_sg
74
- @existing_sg ||= begin
75
- if id != nil
76
- new_driver.ec2.security_groups[id]
199
+ def get_port_ranges(port_spec)
200
+ case port_spec
201
+ when Integer
202
+ [ { port_range: port_spec..port_spec, protocol: :tcp } ]
203
+ when Range
204
+ [ { port_range: port_spec, protocol: :tcp } ]
205
+ when Array
206
+ port_spec.map { |p| get_port_ranges(p) }.flatten
207
+ when Hash
208
+ if port_spec[:protocol]
209
+ [ { port_range: port_spec[:port_range] || port_spec[:port], protocol: port_spec[:protocol].to_s.to_sym } ]
77
210
  else
78
- nil
211
+ get_port_ranges(port_spec[:port_range] || port_spec[:port])
79
212
  end
80
- rescue
81
- nil
213
+ # The to_s.to_sym dance is because if you specify a protocol number, AWS symbolifies it,
214
+ # but 26.to_sym doesn't work (so we have to to_s it first).
215
+ when nil
216
+ [ { port_range: nil, protocol: :any } ]
82
217
  end
83
218
  end
84
219
 
85
- def id
86
- new_resource.security_group_id
220
+ #
221
+ # Turns an actor_spec into a uniform array, containing CIDRs, AWS::EC2::LoadBalancers and AWS::EC2::SecurityGroups.
222
+ #
223
+ def get_actors(vpc, actor_spec)
224
+ result = case actor_spec
225
+
226
+ # An array is always considered a list of actors. Each one may follow any supported format.
227
+ when Array
228
+ actor_spec.map { |a| get_actors(vpc, a) }
229
+
230
+ # Hashes come in several forms:
231
+ when Hash
232
+ # The default AWS Ruby SDK form with :user_id, :group_id and :group_name forms
233
+ if actor_spec.keys.all? { |key| [ :user_id, :group_id, :group_name ].include?(key) }
234
+ if actor_spec.has_key?(:group_name)
235
+ actor_spec[:group_id] ||= vpc.security_groups.filter('group-name', actor_spec[:group_name]).first.id
236
+ end
237
+ actor_spec[:user_id] ||= new_resource.driver.account_id
238
+ { user_id: actor_spec[:user_id], group_id: actor_spec[:group_id] }
239
+
240
+ # load_balancer: <load balancer name>
241
+ elsif actor_spec.keys == [ :load_balancer ]
242
+ lb = Chef::Resource::AwsLoadBalancer.get_aws_object(actor_spec.values.first, resource: new_resource)
243
+ get_actors(vpc, lb)
244
+
245
+ # security_group: <security group name>
246
+ elsif actor_spec.keys == [ :security_group ]
247
+ Chef::Resource::AwsSecurityGroup.get_aws_object(actor_spec[:security_group], resource: new_resource)
248
+
249
+ else
250
+ raise "Unable to reference security group with spec #{actor_spec}"
251
+ end
252
+
253
+ # If a load balancer is specified, grab it and then get its automatic security group
254
+ when /^elb-[a-fA-F0-9]{8}$/, AWS::ELB::LoadBalancer, Chef::Resource::AwsLoadBalancer
255
+ lb = Chef::Resource::AwsLoadBalancer.get_aws_object(actor_spec, resource: new_resource)
256
+ get_actors(vpc, lb.source_security_group)
257
+
258
+ # If a security group is specified, grab it
259
+ when /^sg-[a-fA-F0-9]{8}$/, AWS::EC2::SecurityGroup, Chef::Resource::AwsSecurityGroup
260
+ Chef::Resource::AwsSecurityGroup.get_aws_object(actor_spec, resource: new_resource)
261
+
262
+ # If an IP addresses / CIDR are passed, return it verbatim; otherwise, assume it's the
263
+ # name of a security group.
264
+ when String
265
+ begin
266
+ IPAddr.new(actor_spec)
267
+ # Add /32 to the end of raw IP addresses
268
+ actor_spec =~ /\// ? actor_spec : "#{actor_spec}/32"
269
+ rescue
270
+ Chef::Resource::AwsSecurityGroup.get_aws_object(actor_spec, resource: new_resource)
271
+ end
272
+
273
+ else
274
+ raise "Unexpected actor #{actor_spec} in rules list"
275
+ end
276
+
277
+ result = { user_id: result.owner_id, group_id: result.id } if result.is_a?(AWS::EC2::SecurityGroup)
278
+
279
+ [ result ].flatten
87
280
  end
88
281
 
89
282
  end