ridley 0.10.2 → 0.11.0.rc1

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 (85) hide show
  1. data/README.md +147 -216
  2. data/lib/ridley.rb +2 -0
  3. data/lib/ridley/bootstrap_bindings/unix_template_binding.rb +21 -25
  4. data/lib/ridley/bootstrap_bindings/windows_template_binding.rb +29 -34
  5. data/lib/ridley/bootstrapper.rb +2 -2
  6. data/lib/ridley/bootstrapper/context.rb +5 -5
  7. data/lib/ridley/chef.rb +0 -1
  8. data/lib/ridley/chef/cookbook.rb +0 -9
  9. data/lib/ridley/chef_object.rb +128 -0
  10. data/lib/ridley/chef_objects.rb +3 -0
  11. data/lib/ridley/chef_objects/client_object.rb +55 -0
  12. data/lib/ridley/chef_objects/cookbook_object.rb +190 -0
  13. data/lib/ridley/chef_objects/data_bag_item_obect.rb +104 -0
  14. data/lib/ridley/chef_objects/data_bag_object.rb +31 -0
  15. data/lib/ridley/chef_objects/environment_object.rb +59 -0
  16. data/lib/ridley/chef_objects/node_object.rb +161 -0
  17. data/lib/ridley/chef_objects/role_object.rb +62 -0
  18. data/lib/ridley/chef_objects/sandbox_object.rb +58 -0
  19. data/lib/ridley/client.rb +76 -45
  20. data/lib/ridley/connection.rb +1 -1
  21. data/lib/ridley/errors.rb +8 -1
  22. data/lib/ridley/host_connector.rb +26 -6
  23. data/lib/ridley/host_connector/ssh.rb +3 -3
  24. data/lib/ridley/host_connector/ssh/worker.rb +7 -9
  25. data/lib/ridley/host_connector/winrm/worker.rb +4 -5
  26. data/lib/ridley/mixin/bootstrap_binding.rb +1 -12
  27. data/lib/ridley/resource.rb +51 -171
  28. data/lib/ridley/resources/client_resource.rb +18 -68
  29. data/lib/ridley/resources/cookbook_resource.rb +181 -381
  30. data/lib/ridley/resources/data_bag_item_resource.rb +55 -161
  31. data/lib/ridley/resources/data_bag_resource.rb +20 -61
  32. data/lib/ridley/resources/environment_resource.rb +9 -64
  33. data/lib/ridley/resources/node_resource.rb +135 -311
  34. data/lib/ridley/resources/role_resource.rb +1 -57
  35. data/lib/ridley/resources/sandbox_resource.rb +80 -65
  36. data/lib/ridley/resources/search_resource.rb +99 -0
  37. data/lib/ridley/sandbox_uploader.rb +12 -52
  38. data/lib/ridley/version.rb +1 -1
  39. data/spec/acceptance/bootstrapping_spec.rb +1 -1
  40. data/spec/acceptance/client_resource_spec.rb +15 -37
  41. data/spec/acceptance/data_bag_item_resource_spec.rb +8 -14
  42. data/spec/acceptance/data_bag_resource_spec.rb +1 -1
  43. data/spec/acceptance/environment_resource_spec.rb +13 -22
  44. data/spec/acceptance/node_resource_spec.rb +10 -29
  45. data/spec/acceptance/role_resource_spec.rb +14 -13
  46. data/spec/acceptance/sandbox_resource_spec.rb +2 -2
  47. data/spec/support/shared_examples/ridley_resource.rb +2 -23
  48. data/spec/unit/ridley/bootstrap_bindings/unix_template_binding_spec.rb +3 -4
  49. data/spec/unit/ridley/bootstrap_bindings/windows_template_binding_spec.rb +3 -5
  50. data/spec/unit/ridley/bootstrapper/context_spec.rb +2 -3
  51. data/spec/unit/ridley/bootstrapper_spec.rb +1 -1
  52. data/spec/unit/ridley/chef_object_spec.rb +240 -0
  53. data/spec/unit/ridley/chef_objects/client_object_spec.rb +11 -0
  54. data/spec/unit/ridley/chef_objects/cookbook_object_spec.rb +93 -0
  55. data/spec/unit/ridley/chef_objects/data_bag_item_object_spec.rb +74 -0
  56. data/spec/unit/ridley/chef_objects/data_bag_object_spec.rb +9 -0
  57. data/spec/unit/ridley/chef_objects/environment_object_spec.rb +57 -0
  58. data/spec/unit/ridley/chef_objects/node_object_spec.rb +252 -0
  59. data/spec/unit/ridley/chef_objects/role_object_spec.rb +57 -0
  60. data/spec/unit/ridley/chef_objects/sandbox_object_spec.rb +66 -0
  61. data/spec/unit/ridley/client_spec.rb +51 -51
  62. data/spec/unit/ridley/host_connector/ssh/worker_spec.rb +4 -4
  63. data/spec/unit/ridley/host_connector/ssh_spec.rb +26 -24
  64. data/spec/unit/ridley/host_connector/winrm/worker_spec.rb +3 -4
  65. data/spec/unit/ridley/host_connector/winrm_spec.rb +4 -4
  66. data/spec/unit/ridley/host_connector_spec.rb +40 -3
  67. data/spec/unit/ridley/mixin/bootstrap_binding_spec.rb +1 -1
  68. data/spec/unit/ridley/resource_spec.rb +81 -109
  69. data/spec/unit/ridley/resources/client_resource_spec.rb +18 -33
  70. data/spec/unit/ridley/resources/cookbook_resource_spec.rb +56 -230
  71. data/spec/unit/ridley/resources/data_bag_item_resource_spec.rb +2 -57
  72. data/spec/unit/ridley/resources/data_bag_resource_spec.rb +12 -7
  73. data/spec/unit/ridley/resources/environment_resource_spec.rb +10 -118
  74. data/spec/unit/ridley/resources/node_resource_spec.rb +83 -394
  75. data/spec/unit/ridley/resources/role_resource_spec.rb +2 -56
  76. data/spec/unit/ridley/resources/sandbox_resource_spec.rb +139 -136
  77. data/spec/unit/ridley/resources/search_resource_spec.rb +234 -0
  78. data/spec/unit/ridley/sandbox_uploader_spec.rb +13 -58
  79. metadata +36 -17
  80. data/lib/ridley/chef/chefignore.rb +0 -76
  81. data/lib/ridley/resources/encrypted_data_bag_item_resource.rb +0 -55
  82. data/lib/ridley/resources/search.rb +0 -101
  83. data/spec/fixtures/chefignore +0 -8
  84. data/spec/unit/ridley/chef/chefignore_spec.rb +0 -40
  85. data/spec/unit/ridley/resources/search_spec.rb +0 -221
@@ -0,0 +1,190 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ class CookbookObject < Ridley::ChefObject
4
+ include Ridley::Logging
5
+
6
+ FILE_TYPES = [
7
+ :resources,
8
+ :providers,
9
+ :recipes,
10
+ :definitions,
11
+ :libraries,
12
+ :attributes,
13
+ :files,
14
+ :templates,
15
+ :root_files
16
+ ].freeze
17
+
18
+ set_chef_id "name"
19
+ set_chef_type "cookbook"
20
+ set_chef_json_class "Chef::Cookbook"
21
+
22
+ attribute :name,
23
+ required: true
24
+
25
+ attribute :attributes,
26
+ type: Array,
27
+ default: Array.new
28
+
29
+ attribute :cookbook_name,
30
+ type: String
31
+
32
+ attribute :definitions,
33
+ type: Array,
34
+ default: Array.new
35
+
36
+ attribute :files,
37
+ type: Array,
38
+ default: Array.new
39
+
40
+ attribute :libraries,
41
+ type: Array,
42
+ default: Array.new
43
+
44
+ attribute :metadata,
45
+ type: Hashie::Mash
46
+
47
+ attribute :providers,
48
+ type: Array,
49
+ default: Array.new
50
+
51
+ attribute :recipes,
52
+ type: Array,
53
+ default: Array.new
54
+
55
+ attribute :resources,
56
+ type: Array,
57
+ default: Array.new
58
+
59
+ attribute :root_files,
60
+ type: Array,
61
+ default: Array.new
62
+
63
+ attribute :templates,
64
+ type: Array,
65
+ default: Array.new
66
+
67
+ attribute :version,
68
+ type: String
69
+
70
+ attribute :frozen?,
71
+ type: Boolean
72
+
73
+ # Download the entire cookbook
74
+ #
75
+ # @param [String] destination (Dir.mktmpdir)
76
+ # the place to download the cookbook too. If no value is provided the cookbook
77
+ # will be downloaded to a temporary location
78
+ #
79
+ # @return [String]
80
+ # the path to the directory the cookbook was downloaded to
81
+ def download(destination = Dir.mktmpdir)
82
+ destination = File.expand_path(destination)
83
+ log.debug { "downloading cookbook: '#{name}'" }
84
+
85
+ FILE_TYPES.each do |filetype|
86
+ next unless manifest.has_key?(filetype)
87
+
88
+ manifest[filetype].each do |file|
89
+ file_destination = File.join(destination, file[:path].gsub('/', File::SEPARATOR))
90
+ FileUtils.mkdir_p(File.dirname(file_destination))
91
+ download_file(filetype, file[:path], file_destination)
92
+ end
93
+ end
94
+
95
+ destination
96
+ end
97
+
98
+ # Download a single file from a cookbook
99
+ #
100
+ # @param [#to_sym] filetype
101
+ # the type of file to download. These are broken up into the following types in Chef:
102
+ # - attribute (unsupported until resolved https://github.com/reset/chozo/issues/17)
103
+ # - definition
104
+ # - file
105
+ # - library
106
+ # - provider
107
+ # - recipe
108
+ # - resource
109
+ # - root_file
110
+ # - template
111
+ # these types are where the files are stored in your cookbook's structure. For example, a
112
+ # recipe would be stored in the recipes directory while a root_file is stored at the root
113
+ # of your cookbook
114
+ # @param [String] path
115
+ # path of the file to download
116
+ # @param [String] destination
117
+ # where to download the file to
118
+ #
119
+ # @return [nil]
120
+ def download_file(filetype, path, destination)
121
+ download_fun(filetype).call(path, destination)
122
+ end
123
+
124
+ # A hash containing keys for all of the different cookbook filetypes with values
125
+ # representing each file of that type this cookbook contains
126
+ #
127
+ # @example
128
+ # {
129
+ # root_files: [
130
+ # {
131
+ # :name => "afile.rb",
132
+ # :path => "files/ubuntu-9.10/afile.rb",
133
+ # :checksum => "2222",
134
+ # :specificity => "ubuntu-9.10"
135
+ # },
136
+ # ],
137
+ # templates: [ manifest_record1, ... ],
138
+ # ...
139
+ # }
140
+ #
141
+ # @return [Hash]
142
+ def manifest
143
+ {}.tap do |manifest|
144
+ FILE_TYPES.each do |filetype|
145
+ manifest[filetype] = get_attribute(filetype)
146
+ end
147
+ end
148
+ end
149
+
150
+ def to_s
151
+ "#{name}: #{manifest}"
152
+ end
153
+
154
+ private
155
+
156
+ # Return a lambda for downloading a file from the cookbook of the given type
157
+ #
158
+ # @param [#to_sym] filetype
159
+ #
160
+ # @return [lambda]
161
+ # a lambda which takes to parameters: target and path. Target is the URL to download from
162
+ # and path is the location on disk to steam the contents of the remote URL to.
163
+ def download_fun(filetype)
164
+ collection = case filetype.to_sym
165
+ when :attribute, :attributes; method(:attributes)
166
+ when :definition, :definitions; method(:definitions)
167
+ when :file, :files; method(:files)
168
+ when :library, :libraries; method(:libraries)
169
+ when :provider, :providers; method(:providers)
170
+ when :recipe, :recipes; method(:recipes)
171
+ when :resource, :resources; method(:resources)
172
+ when :root_file, :root_files; method(:root_files)
173
+ when :template, :templates; method(:templates)
174
+ else
175
+ raise Errors::UnknownCookbookFileType.new(filetype)
176
+ end
177
+
178
+ ->(target, destination) {
179
+ files = collection.call # JW: always chaining .call.find results in a nil value. WHY?
180
+ file = files.find { |f| f[:path] == target }
181
+ return nil if file.nil?
182
+
183
+ destination = File.expand_path(destination)
184
+ log.debug { "downloading '#{filetype}' file: #{file} to: '#{destination}'" }
185
+
186
+ connection.stream(file[:url], destination)
187
+ }
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,104 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ class DataBagItemObject < ChefObject
4
+ set_chef_id "id"
5
+ set_assignment_mode :carefree
6
+
7
+ # @return [Ridley::DataBagObject]
8
+ attr_reader :data_bag
9
+
10
+ attribute :id,
11
+ type: String,
12
+ required: true
13
+
14
+ alias_method :attributes=, :mass_assign
15
+ alias_method :attributes, :_attributes_
16
+
17
+ # @param [Ridley::DataBagItemResource] resource
18
+ # @param [Ridley::DataBagObject] data_bag
19
+ # @param [#to_hash] new_attrs
20
+ def initialize(resource, data_bag, new_attrs = {})
21
+ super(resource, new_attrs)
22
+ @data_bag = data_bag
23
+ end
24
+
25
+ # Creates a resource on the target remote or updates one if the resource
26
+ # already exists.
27
+ #
28
+ # @raise [Errors::InvalidResource]
29
+ # if the resource does not pass validations
30
+ #
31
+ # @return [Boolean]
32
+ # true if successful and false for failure
33
+ def save
34
+ raise Errors::InvalidResource.new(self.errors) unless valid?
35
+
36
+ mass_assign(resource.create(data_bag, self)._attributes_)
37
+ true
38
+ rescue Errors::HTTPConflict
39
+ self.update
40
+ true
41
+ end
42
+
43
+ # Decrypts this data bag item.
44
+ #
45
+ # @return [Hash] decrypted attributes
46
+ def decrypt
47
+ decrypted_hash = Hash[_attributes_.map { |key, value| [key, key == "id" ? value : decrypt_value(value)] }]
48
+ mass_assign(decrypted_hash)
49
+ end
50
+
51
+ def decrypt_value(value)
52
+ if encrypted_data_bag_secret.nil?
53
+ raise Errors::EncryptedDataBagSecretNotSet
54
+ end
55
+
56
+ decoded_value = Base64.decode64(value)
57
+
58
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
59
+ cipher.decrypt
60
+ cipher.pkcs5_keyivgen(encrypted_data_bag_secret)
61
+ decrypted_value = cipher.update(decoded_value) + cipher.final
62
+
63
+ YAML.load(decrypted_value)
64
+ end
65
+
66
+ # Reload the attributes of the instantiated resource
67
+ #
68
+ # @return [Object]
69
+ def reload
70
+ mass_assign(resource.find(data_bag, self)._attributes_)
71
+ self
72
+ end
73
+
74
+ # Updates the instantiated resource on the target remote with any changes made
75
+ # to self
76
+ #
77
+ # @raise [Errors::InvalidResource]
78
+ # if the resource does not pass validations
79
+ #
80
+ # @return [Boolean]
81
+ def update
82
+ raise Errors::InvalidResource.new(self.errors) unless valid?
83
+
84
+ mass_assign(resource.update(data_bag, self)._attributes_)
85
+ true
86
+ end
87
+
88
+ # @param [#to_hash] hash
89
+ #
90
+ # @return [Object]
91
+ def from_hash(hash)
92
+ hash = Hashie::Mash.new(hash.to_hash)
93
+
94
+ mass_assign(hash.has_key?(:raw_data) ? hash[:raw_data] : hash)
95
+ self
96
+ end
97
+
98
+ private
99
+
100
+ def encrypted_data_bag_secret
101
+ resource.encrypted_data_bag_secret
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,31 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ class DataBagObject < ChefObject
4
+ set_chef_id "name"
5
+
6
+ attribute :name,
7
+ required: true
8
+
9
+ def item
10
+ DataBagItemProxy.new(self, resource.item_resource)
11
+ end
12
+
13
+ # @author Jamie Winsor <reset@riotgames.com>
14
+ # @api private
15
+ class DataBagItemProxy
16
+ attr_reader :data_bag_object
17
+ attr_reader :item_resource
18
+
19
+ # @param [Ridley::DataBagObject] data_bag_object
20
+ # @param [Ridley::DataBagItemResource] item_resource
21
+ def initialize(data_bag_object, item_resource)
22
+ @data_bag_object = data_bag_object
23
+ @item_resource = item_resource
24
+ end
25
+
26
+ def method_missing(fun, *args, &block)
27
+ @item_resource.send(fun, data_bag_object, *args, &block)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,59 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ class EnvironmentObject < Ridley::ChefObject
4
+ set_chef_id "name"
5
+ set_chef_type "environment"
6
+ set_chef_json_class "Chef::Environment"
7
+
8
+ attribute :name,
9
+ required: true
10
+
11
+ attribute :description,
12
+ default: String.new
13
+
14
+ attribute :default_attributes,
15
+ default: Hashie::Mash.new
16
+
17
+ attribute :override_attributes,
18
+ default: Hashie::Mash.new
19
+
20
+ attribute :cookbook_versions,
21
+ default: Hashie::Mash.new
22
+
23
+ # Set an environment level default attribute given the dotted path representation of
24
+ # the Chef attribute and value
25
+ #
26
+ # @example setting and saving an environment level default attribute
27
+ #
28
+ # obj = environment.find("production")
29
+ # obj.set_default_attribute("my_app.billing.enabled", false)
30
+ # obj.save
31
+ #
32
+ # @param [String] key
33
+ # @param [Object] value
34
+ #
35
+ # @return [HashWithIndifferentAccess]
36
+ def set_default_attribute(key, value)
37
+ attr_hash = HashWithIndifferentAccess.from_dotted_path(key, value)
38
+ self.default_attributes = self.default_attributes.deep_merge(attr_hash)
39
+ end
40
+
41
+ # Set an environment level override attribute given the dotted path representation of
42
+ # the Chef attribute and value
43
+ #
44
+ # @example setting and saving an environment level override attribute
45
+ #
46
+ # obj = environment.find("production")
47
+ # obj.set_override_attribute("my_app.billing.enabled", false)
48
+ # obj.save
49
+ #
50
+ # @param [String] key
51
+ # @param [Object] value
52
+ #
53
+ # @return [HashWithIndifferentAccess]
54
+ def set_override_attribute(key, value)
55
+ attr_hash = HashWithIndifferentAccess.from_dotted_path(key, value)
56
+ self.override_attributes = self.override_attributes.deep_merge(attr_hash)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,161 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <reset@riotgames.com>
3
+ class NodeObject < Ridley::ChefObject
4
+ set_chef_id "name"
5
+ set_chef_type "node"
6
+ set_chef_json_class "Chef::Node"
7
+
8
+ attribute :name,
9
+ required: true
10
+
11
+ attribute :chef_environment,
12
+ default: "_default"
13
+
14
+ attribute :automatic,
15
+ default: Hashie::Mash.new
16
+
17
+ attribute :normal,
18
+ default: Hashie::Mash.new
19
+
20
+ attribute :default,
21
+ default: Hashie::Mash.new
22
+
23
+ attribute :override,
24
+ default: Hashie::Mash.new
25
+
26
+ attribute :run_list,
27
+ default: Array.new
28
+
29
+ alias_method :normal_attributes, :normal
30
+ alias_method :automatic_attributes, :automatic
31
+ alias_method :default_attributes, :default
32
+ alias_method :override_attributes, :override
33
+
34
+ alias_method :normal_attributes=, :normal=
35
+ alias_method :automatic_attributes=, :automatic=
36
+ alias_method :default_attributes=, :default=
37
+ alias_method :override_attributes=, :override=
38
+
39
+ # Set a node level normal attribute given the dotted path representation of the Chef
40
+ # attribute and value.
41
+ #
42
+ # @note It is not possible to set any other attribute level on a node and have it persist after
43
+ # a Chef Run. This is because all other attribute levels are truncated at the start of a Chef Run.
44
+ #
45
+ # @example setting and saving a node level normal attribute
46
+ #
47
+ # obj = node.find("jwinsor-1")
48
+ # obj.set_chef_attribute("my_app.billing.enabled", false)
49
+ # obj.save
50
+ #
51
+ # @param [String] key
52
+ # @param [Object] value
53
+ #
54
+ # @return [Hashie::Mash]
55
+ def set_chef_attribute(key, value)
56
+ attr_hash = Hashie::Mash.from_dotted_path(key, value)
57
+ self.normal = self.normal.deep_merge(attr_hash)
58
+ end
59
+
60
+ # Returns the public hostname of the instantiated node. This hostname should be used for
61
+ # public communications to the node.
62
+ #
63
+ # @example
64
+ # node.public_hostname => "reset.riotgames.com"
65
+ #
66
+ # @return [String]
67
+ def public_hostname
68
+ self.cloud? ? self.automatic[:cloud][:public_hostname] : self.automatic[:fqdn]
69
+ end
70
+
71
+ # Returns the public IPv4 address of the instantiated node. This ip address should be
72
+ # used for public communications to the node.
73
+ #
74
+ # @example
75
+ # node.public_ipv4 => "10.33.33.1"
76
+ #
77
+ # @return [String]
78
+ def public_ipv4
79
+ self.cloud? ? self.automatic[:cloud][:public_ipv4] : self.automatic[:ipaddress]
80
+ end
81
+ alias_method :public_ipaddress, :public_ipv4
82
+
83
+ # Returns the cloud provider of the instantiated node. If the node is not identified as
84
+ # a cloud node, then nil is returned.
85
+ #
86
+ # @example
87
+ # node_1.cloud_provider => "eucalyptus"
88
+ # node_2.cloud_provider => "ec2"
89
+ # node_3.cloud_provider => "rackspace"
90
+ # node_4.cloud_provider => nil
91
+ #
92
+ # @return [nil, String]
93
+ def cloud_provider
94
+ self.cloud? ? self.automatic[:cloud][:provider] : nil
95
+ end
96
+
97
+ # Returns true if the node is identified as a cloud node.
98
+ #
99
+ # @return [Boolean]
100
+ def cloud?
101
+ self.automatic.has_key?(:cloud)
102
+ end
103
+
104
+ # Returns true if the node is identified as a cloud node using the eucalyptus provider.
105
+ #
106
+ # @return [Boolean]
107
+ def eucalyptus?
108
+ self.cloud_provider == "eucalyptus"
109
+ end
110
+
111
+ # Returns true if the node is identified as a cloud node using the ec2 provider.
112
+ #
113
+ # @return [Boolean]
114
+ def ec2?
115
+ self.cloud_provider == "ec2"
116
+ end
117
+
118
+ # Returns true if the node is identified as a cloud node using the rackspace provider.
119
+ #
120
+ # @return [Boolean]
121
+ def rackspace?
122
+ self.cloud_provider == "rackspace"
123
+ end
124
+
125
+ # Executes a Chef run on the node
126
+ #
127
+ # @return [HostConnector::Response]
128
+ def chef_run
129
+ resource.chef_run(self.public_hostname)
130
+ end
131
+
132
+ # Puts the configured encrypted data bag secret on the node
133
+ #
134
+ # @return [HostConnector::Response]
135
+ def put_secret
136
+ resource.put_secret(self.public_hostname)
137
+ end
138
+
139
+ # Merges the instaniated nodes data with the given data and updates
140
+ # the remote with the merged results
141
+ #
142
+ # @option options [Array] :run_list
143
+ # run list items to merge
144
+ # @option options [Hash] :attributes
145
+ # attributes of normal precedence to merge
146
+ #
147
+ # @return [Ridley::NodeResource]
148
+ def merge_data(options = {})
149
+ unless options[:run_list].nil?
150
+ self.run_list = (self.run_list + Array(options[:run_list])).uniq
151
+ end
152
+
153
+ unless options[:attributes].nil?
154
+ self.normal = self.normal.deep_merge(options[:attributes])
155
+ end
156
+
157
+ self.update
158
+ self
159
+ end
160
+ end
161
+ end