chef-provisioning-aws 0.4.0 → 0.5.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.
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