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,3 +1,20 @@
1
1
  require 'chef/provisioning'
2
2
  require 'chef/provisioning/aws_driver/driver'
3
- require 'chef/provisioning/aws_driver/resources'
3
+
4
+ require "chef/resource/aws_auto_scaling_group"
5
+ require "chef/resource/aws_dhcp_options"
6
+ require "chef/resource/aws_ebs_volume"
7
+ require "chef/resource/aws_eip_address"
8
+ require "chef/resource/aws_image"
9
+ require "chef/resource/aws_instance"
10
+ require "chef/resource/aws_internet_gateway"
11
+ require "chef/resource/aws_launch_configuration"
12
+ require "chef/resource/aws_load_balancer"
13
+ require "chef/resource/aws_network_interface"
14
+ require "chef/resource/aws_route_table"
15
+ require "chef/resource/aws_s3_bucket"
16
+ require "chef/resource/aws_security_group"
17
+ require "chef/resource/aws_sns_topic"
18
+ require "chef/resource/aws_sqs_queue"
19
+ require "chef/resource/aws_subnet"
20
+ require "chef/resource/aws_vpc"
@@ -0,0 +1,206 @@
1
+ require 'chef/provider/lwrp_base'
2
+ require 'chef/provisioning/aws_driver/aws_resource'
3
+ require 'chef/provisioning/aws_driver/aws_resource_with_entry'
4
+ require 'chef/provisioning/chef_managed_entry_store'
5
+ require 'chef/provisioning/chef_provider_action_handler'
6
+
7
+ module Chef::Provisioning::AWSDriver
8
+ class AWSProvider < Chef::Provider::LWRPBase
9
+ use_inline_resources
10
+
11
+ AWSResource = Chef::Provisioning::AWSDriver::AWSResource
12
+
13
+ def action_handler
14
+ @action_handler ||= Chef::Provisioning::ChefProviderActionHandler.new(self)
15
+ end
16
+
17
+ # All these need to implement whyrun
18
+ def whyrun_supported?
19
+ true
20
+ end
21
+
22
+ def region
23
+ new_resource.driver.aws_config.region
24
+ end
25
+
26
+ #
27
+ # Return the damned value from the block, not whatever weirdness converge_by
28
+ # normally returns.
29
+ #
30
+ def converge_by(*args, &block)
31
+ result = nil
32
+ super(*args) do
33
+ result = block.call
34
+ end
35
+ result
36
+ end
37
+
38
+ action :create do
39
+ #
40
+ # If the user specified an ID, get the object for it, and fail if it does not exist.
41
+ #
42
+ desired_driver = new_resource.driver
43
+ desired_id = new_resource.public_send(new_resource.class.aws_id_attribute) if new_resource.class.aws_id_attribute
44
+ if desired_id
45
+ aws_object = new_resource.class.get_aws_object(desired_id, resource: new_resource)
46
+ end
47
+
48
+ #
49
+ # If Chef has already associated the object with an AWS ID, check if it's
50
+ # the same as the desired AWS ID.
51
+ #
52
+ if new_resource.is_a?(AWSResourceWithEntry)
53
+ entry_driver, entry_id, entry = new_resource.get_id_from_managed_entry
54
+ if entry_id
55
+ if desired_id
56
+
57
+ #
58
+ # We have both a desired ID and an entry ID. Find out whether they
59
+ # match and warn if they don't (because we're going to reassociate and
60
+ # update the *desired* AWS thing.).
61
+ #
62
+ if desired_driver.driver_url == entry_driver.driver_url && desired_id == entry_id
63
+ Chef::Log.debug "#{new_resource.to_s} is already associated with #{entry_id} in #{entry_driver.driver_url}"
64
+ else
65
+ Chef::Log.warn "#{new_resource.to_s} is currently associated with #{entry_id} in #{entry_driver.driver_url}, but the desired ID is #{desired_id} in #{new_resource.driver.driver_url}! Will associate with new desired ID #{desired_id}."
66
+ end
67
+
68
+ else
69
+
70
+ #
71
+ # If we don't have desired (common case), we'll update the existing
72
+ # resource or create a new one if it's been deleted.
73
+ #
74
+ aws_object = new_resource.class.get_aws_object(entry_id, driver: entry_driver, resource: new_resource, required: false)
75
+ if aws_object
76
+ Chef::Log.debug "#{new_resource.to_s} is currently associated with #{entry_id} in #{entry_driver.driver_url}."
77
+ else
78
+ Chef::Log.warn "#{new_resource.to_s} is currently associated with #{entry_id} in #{entry_driver.driver_url}, but it does not exist! We will create a new one to replace it."
79
+ end
80
+ end
81
+
82
+ else
83
+
84
+ #
85
+ # If we don't currently have an AWS ID associated with this resource, we
86
+ # will either associate the desired one, or create a new one.
87
+ #
88
+ if desired_id
89
+ Chef::Log.debug "#{new_resource.to_s} is not yet associated with anything. Associating with desired object #{desired_id} in #{desired_driver.driver_url}."
90
+ else
91
+ Chef::Log.debug "#{new_resource.to_s} is not yet associated with anything. Creating a new one in #{desired_driver.driver_url} ..."
92
+ end
93
+ end
94
+
95
+ else
96
+
97
+ #
98
+ # If it does not support storing IDs in Chef at all, just grab the existing
99
+ # object and we'll update (or not) based on that.
100
+ #
101
+ aws_object ||= new_resource.aws_object
102
+
103
+ end
104
+
105
+ #
106
+ # Actually update or create the AWS object
107
+ #
108
+ if aws_object
109
+ action, new_obj = update_aws_object(aws_object)
110
+ if action == :replaced_aws_object
111
+ aws_object = new_obj
112
+ end
113
+ else
114
+ aws_object = create_aws_object
115
+ end
116
+
117
+ #
118
+ # Associate the managed entry with the AWS object
119
+ #
120
+ if new_resource.is_a?(AWSResourceWithEntry)
121
+ new_resource.save_managed_entry(aws_object, action_handler, existing_entry: entry)
122
+ end
123
+
124
+ aws_object
125
+ end
126
+
127
+ action :destroy do
128
+ desired_driver = new_resource.driver
129
+ desired_id = new_resource.public_send(new_resource.class.aws_id_attribute) if new_resource.class.aws_id_attribute
130
+
131
+ #
132
+ # If the user specified an ID, delete THAT; do NOT delete the associated object.
133
+ #
134
+ if desired_id
135
+ aws_object = new_resource.class.get_aws_object(desired_id, resource: new_resource, required: false)
136
+ if aws_object
137
+ Chef::Log.debug "#{new_resource.to_s} provided #{new_resource.class.aws_id_attribute} #{desired_id} in #{desired_driver.driver_url}. Will delete."
138
+ end
139
+ end
140
+
141
+ #
142
+ # Managed entries are looked up by ID.
143
+ #
144
+ if new_resource.is_a?(AWSResourceWithEntry)
145
+ entry_driver, entry_id, entry = new_resource.get_id_from_managed_entry
146
+ if entry_id
147
+ if desired_id && (desired_id != entry_id || desired_driver.driver_url != entry_driver.driver_url)
148
+ if new_resource.class.get_aws_object(entry_id, driver: entry_driver, resource: new_resource, required: false)
149
+ # If the desired ID / driver differs from the entry, don't delete. We
150
+ # certainly can't delete the AWS object itself, and we don't *want* to
151
+ # delete the association, because the expectation is that after doing a
152
+ # delete, you should be able to create a new thing.
153
+ raise "#{new_resource.to_s} provided #{new_resource.class.aws_id_attribute} #{desired_id} in #{desired_driver.driver_url}, but is currently associated with #{entry_id} in #{entry_driver.driver_url}. Cannot delete the entry or the association until this inconsistency is resolved."
154
+ else
155
+ Chef::Log.debug "#{new_resource.to_s} provided #{new_resource.class.aws_id_attribute} #{desired_id} in #{desired_driver.driver_url}, but is currently associated with #{entry_id} in #{entry_driver.driver_url}, which does not exist. Will delete #{desired_id} and disassociate from #{entry_id}."
156
+ end
157
+ else
158
+
159
+ # Normal case: entry exists, and is the same as desired (or no desired)
160
+ aws_object = new_resource.class.get_aws_object(entry_id, driver: entry_driver, resource: new_resource, required: false)
161
+ if aws_object
162
+ Chef::Log.debug "#{new_resource.to_s} is associated with #{entry_id} in #{entry_driver.driver_url}. Will delete."
163
+ else
164
+ Chef::Log.debug "#{new_resource.to_s} is associated with #{entry_id} in #{entry_driver.driver_url}, but it does not exist. Will disassociate the entry but not delete."
165
+ end
166
+ end
167
+ end
168
+
169
+ #
170
+ # Non-managed entries all have their own way of looking it up
171
+ #
172
+ else
173
+ aws_object ||= new_resource.aws_object
174
+ end
175
+
176
+ #
177
+ # Call the delete method
178
+ #
179
+ if aws_object
180
+ destroy_aws_object(aws_object)
181
+ end
182
+
183
+ #
184
+ # Associate the managed entry with the AWS object
185
+ #
186
+ if new_resource.is_a?(AWSResourceWithEntry) && entry
187
+ new_resource.delete_managed_entry(action_handler)
188
+ end
189
+ end
190
+
191
+ protected
192
+
193
+ def create_aws_object
194
+ raise NotImplementedError, :create_aws_object
195
+ end
196
+
197
+ def update_aws_object(obj)
198
+ raise NotImplementedError, :update_aws_object
199
+ end
200
+
201
+ def destroy_aws_object(obj)
202
+ raise NotImplementedError, :destroy_aws_object
203
+ end
204
+
205
+ end
206
+ end
@@ -0,0 +1,186 @@
1
+ require 'aws'
2
+ require 'chef/provisioning/aws_driver/super_lwrp'
3
+ require 'chef/provisioning/chef_managed_entry_store'
4
+
5
+ # Common AWS resource - contains metadata that all AWS resources will need
6
+ module Chef::Provisioning::AWSDriver
7
+ class AWSResource < Chef::Provisioning::AWSDriver::SuperLWRP
8
+ actions :create, :destroy, :nothing
9
+ default_action :create
10
+
11
+ def initialize(name, run_context=nil)
12
+ name = name.public_send(self.class.aws_sdk_class_id) if name.is_a?(self.class.aws_sdk_class)
13
+ super
14
+ if run_context
15
+ driver run_context.chef_provisioning.current_driver
16
+ chef_server run_context.cheffish.current_chef_server
17
+ end
18
+ end
19
+
20
+ # Backwards compatibility for action :destroy
21
+ def action(*args)
22
+ if args == [ :delete ]
23
+ super(:destroy)
24
+ else
25
+ super
26
+ end
27
+ end
28
+ def action=(value)
29
+ action(value)
30
+ end
31
+
32
+ #
33
+ # The desired driver.
34
+ #
35
+ attribute :driver, kind_of: Chef::Provisioning::Driver,
36
+ coerce: (proc do |value|
37
+ case value
38
+ when nil, Chef::Provisioning::Driver
39
+ value
40
+ else
41
+ run_context.chef_provisioning.driver_for(value)
42
+ end
43
+ end)
44
+
45
+ #
46
+ # The Chef server on which any IDs should be looked up.
47
+ #
48
+ attribute :chef_server, kind_of: Hash
49
+
50
+ #
51
+ # The managed entry store.
52
+ #
53
+ attribute :managed_entry_store, kind_of: Chef::Provisioning::ManagedEntryStore,
54
+ lazy_default: proc { Chef::Provisioning::ChefManagedEntryStore.new(chef_server) }
55
+
56
+ #
57
+ # Get the current AWS object.
58
+ #
59
+ def aws_object
60
+ raise NotImplementedError, :aws_object
61
+ end
62
+
63
+ #
64
+ # Look up an AWS options list, translating standard names using the appropriate
65
+ # classes.
66
+ #
67
+ # For example, `load_balancer_options` is passed into `lookup_options`, and if
68
+ # it looks like this: `{ subnets: `[ 'subnet1', 'subnet2' ] }`, then
69
+ # `AWSResource.lookup_options` will translate each ID with
70
+ # `AwsSubnet.get_aws_object('subnet1')`, which supports Chef names
71
+ # (`mysubnet`) as well as AWS subnet Ids (`subnet-1234abcd`) or AWS objects
72
+ # (`AWS::EC2::Subnet`).
73
+ #
74
+ # Keys that represent non-AWS-objects (such as `timeout`) are left alone.
75
+ #
76
+ def self.lookup_options(options, **handler_options)
77
+ options = options.dup
78
+ options.each do |name, value|
79
+ if name.to_s.end_with?('s')
80
+ handler_name = :"#{name[0..-2]}"
81
+ if aws_option_handlers[handler_name]
82
+ options[name] = [options[name]].flatten.map { |value| aws_option_handlers[handler_name].get_aws_object_id(value, **handler_options) }
83
+ end
84
+ else
85
+ if aws_option_handlers[name]
86
+ options[name] = aws_option_handlers[name].get_aws_object_id(value, **handler_options)
87
+ end
88
+ end
89
+ end
90
+ options
91
+ end
92
+
93
+ def self.get_aws_object(value, resource: nil, run_context: nil, driver: nil, managed_entry_store: nil, required: true)
94
+ return nil if value.nil?
95
+
96
+ if resource
97
+ run_context ||= resource.run_context
98
+ driver ||= resource.driver
99
+ managed_entry_store ||= resource.managed_entry_store
100
+ end
101
+ if value.is_a?(self)
102
+ resource = value
103
+ else
104
+ resource = new(value, run_context)
105
+ resource.driver driver if driver
106
+ resource.managed_entry_store managed_entry_store if managed_entry_store
107
+ end
108
+ result = resource.aws_object
109
+ if required && result.nil?
110
+ raise "#{self}[#{value}] does not exist!"
111
+ end
112
+ result
113
+ end
114
+
115
+ def self.get_aws_object_id(value, **options)
116
+ aws_object = get_aws_object(value, **options)
117
+ aws_object.public_send(aws_sdk_class_id) if aws_object
118
+ end
119
+
120
+ protected
121
+
122
+ NOT_PASSED = Object.new
123
+
124
+ def self.aws_sdk_type(sdk_class,
125
+ option_names: nil,
126
+ option_name: NOT_PASSED,
127
+ load_provider: true,
128
+ id: :name,
129
+ aws_id_prefix: nil)
130
+ self.resource_name = self.dsl_name
131
+ @aws_sdk_class = sdk_class
132
+ @aws_sdk_class_id = id
133
+ @aws_id_prefix = aws_id_prefix
134
+
135
+ # Go ahead and require the provider since we're here anyway ...
136
+ require "chef/provider/#{resource_name}" if load_provider
137
+
138
+ option_name = :"#{resource_name[4..-1]}" if option_name == NOT_PASSED
139
+ @aws_sdk_option_name = option_name
140
+
141
+ option_names ||= begin
142
+ option_names = []
143
+ option_names << aws_sdk_option_name
144
+ option_names << :"#{option_name}_#{aws_sdk_class_id}" if aws_sdk_class_id
145
+ option_names
146
+ end
147
+ option_names.each do |option_name|
148
+ aws_option_handlers[option_name] = self
149
+ end
150
+
151
+ name = self.name.split('::')[-1]
152
+ eval("Chef::Provisioning::AWSDriver::Resources::#{name} = self", binding, __FILE__, __LINE__)
153
+ end
154
+
155
+ def self.aws_sdk_class
156
+ @aws_sdk_class
157
+ end
158
+
159
+ def self.aws_sdk_class_id
160
+ @aws_sdk_class_id
161
+ end
162
+
163
+ def self.aws_id_prefix
164
+ @aws_id_prefix
165
+ end
166
+
167
+ def self.aws_sdk_option_name
168
+ @aws_sdk_option_name
169
+ end
170
+
171
+ @@aws_option_handlers = {}
172
+ def self.aws_option_handlers
173
+ @@aws_option_handlers
174
+ end
175
+
176
+ # Add support for aws_id_attribute: true
177
+ def self.attribute(name, aws_id_attribute: false, **validation_opts)
178
+ @aws_id_attribute = name if aws_id_attribute
179
+ super(name, validation_opts)
180
+ end
181
+
182
+ def self.aws_id_attribute
183
+ @aws_id_attribute
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,114 @@
1
+ require 'chef/provisioning/aws_driver/aws_resource'
2
+ require 'chef/provisioning/aws_driver/resources'
3
+
4
+ # Common AWS resource - contains metadata that all AWS resources will need
5
+ class Chef::Provisioning::AWSDriver::AWSResourceWithEntry < Chef::Provisioning::AWSDriver::AWSResource
6
+ #
7
+ # Dissociate the ID of this object from Chef.
8
+ #
9
+ # @param action_handler [Chef::Provisioning::ActionHandler] The action handler,
10
+ # which handles progress reporting, update reporting ("little green text")
11
+ # and dry run.
12
+ #
13
+ def delete_managed_entry(action_handler)
14
+ if should_have_managed_entry?
15
+ managed_entry_store.delete(self.class.resource_name, name, action_handler)
16
+ end
17
+ end
18
+
19
+ #
20
+ # Save the ID of this object to Chef.
21
+ #
22
+ # @param aws_object [AWS::EC2::Core] The AWS object containing the ID.
23
+ # @param action_handler [Chef::Provisioning::ActionHandler] The action handler,
24
+ # which handles progress reporting, update reporting ("little green text")
25
+ # and dry run.
26
+ # @param existing_entry [Chef::Provisioning::ManagedEntry] The existing entry
27
+ # (if any). If this is passed in, and no values are changed, we will
28
+ # not attempt to update it (this prevents us from retrieving it twice).
29
+ #
30
+ def save_managed_entry(aws_object, action_handler, existing_entry: nil)
31
+ if should_have_managed_entry?
32
+ managed_entry = existing_entry ||
33
+ managed_entry_store.new_entry(self.class.resource_name, name)
34
+ updated = update_managed_entry(aws_object, managed_entry)
35
+ if updated || !existing_entry
36
+ managed_entry.save(action_handler)
37
+ end
38
+ end
39
+ end
40
+
41
+ def get_id_from_managed_entry
42
+ if should_have_managed_entry?
43
+ entry = managed_entry_store.get(self.class.managed_entry_type, name)
44
+ if entry
45
+ driver = self.driver
46
+ if entry.driver_url != driver.driver_url
47
+ # TODO some people don't send us run_context (like Drivers). We might need
48
+ # to exit early here if the driver_url doesn't match the provided driver.
49
+ driver = run_context.chef_provisioning.driver_for(entry.driver_url)
50
+ end
51
+ [ driver, entry.reference[self.class.managed_entry_id_name], entry ]
52
+ end
53
+ end
54
+ end
55
+
56
+ # Formatted output for logging statements - contains resource type, resource name and aws object id (if available)
57
+ def to_s
58
+ id = get_driver_and_id[1]
59
+ "#{declared_type}[#{@name}] (#{ id ? id : 'no AWS object id'})"
60
+ end
61
+
62
+ protected
63
+
64
+ #
65
+ # Update an existing ManagedEntry object.
66
+ #
67
+ # @return true if the entry was changed, and false if not
68
+ #
69
+ def update_managed_entry(aws_object, managed_entry)
70
+ new_value = { self.class.managed_entry_id_name => aws_object.public_send(self.class.aws_sdk_class_id) }
71
+ if managed_entry.reference != new_value
72
+ managed_entry.reference = new_value
73
+ changed = true
74
+ end
75
+ if managed_entry.driver_url != driver.driver_url
76
+ managed_entry.driver_url = driver.driver_url
77
+ changed = true
78
+ end
79
+ changed
80
+ end
81
+
82
+ def get_driver_and_id
83
+ driver, id, entry = get_id_from_managed_entry
84
+ # If the value isn't already stored, look up the user-specified public_ip
85
+ driver, id = self.driver, self.public_send(self.class.aws_id_attribute) if !id
86
+ [ driver, id ]
87
+ end
88
+
89
+ def self.aws_sdk_type(sdk_class,
90
+ id: :id,
91
+ managed_entry_type: nil,
92
+ managed_entry_id_name: 'id',
93
+ backcompat_data_bag_name: nil,
94
+ **options)
95
+ super(sdk_class, id: id, **options)
96
+ @managed_entry_type = managed_entry_type || resource_name.to_sym
97
+ @managed_entry_id_name = managed_entry_id_name
98
+ if backcompat_data_bag_name
99
+ Chef::Provisioning::ChefManagedEntryStore.type_names_for_backcompat[resource_name] = backcompat_data_bag_name
100
+ end
101
+ end
102
+
103
+ def self.managed_entry_type
104
+ @managed_entry_type
105
+ end
106
+
107
+ def self.managed_entry_id_name
108
+ @managed_entry_id_name
109
+ end
110
+
111
+ def should_have_managed_entry?
112
+ name != public_send(self.class.aws_id_attribute)
113
+ end
114
+ end