chef-provisioning 2.0.1 → 2.0.2

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/CHANGELOG.md +906 -899
  3. data/Gemfile +17 -17
  4. data/LICENSE +201 -201
  5. data/README.md +312 -312
  6. data/Rakefile +55 -55
  7. data/chef-provisioning.gemspec +38 -38
  8. data/lib/chef/provider/load_balancer.rb +75 -75
  9. data/lib/chef/provider/machine.rb +219 -219
  10. data/lib/chef/provider/machine_batch.rb +224 -224
  11. data/lib/chef/provider/machine_execute.rb +36 -36
  12. data/lib/chef/provider/machine_file.rb +55 -55
  13. data/lib/chef/provider/machine_image.rb +105 -105
  14. data/lib/chef/provisioning.rb +110 -110
  15. data/lib/chef/provisioning/action_handler.rb +68 -68
  16. data/lib/chef/provisioning/add_prefix_action_handler.rb +35 -35
  17. data/lib/chef/provisioning/chef_managed_entry_store.rb +128 -128
  18. data/lib/chef/provisioning/chef_provider_action_handler.rb +74 -74
  19. data/lib/chef/provisioning/chef_run_data.rb +132 -132
  20. data/lib/chef/provisioning/convergence_strategy.rb +28 -28
  21. data/lib/chef/provisioning/convergence_strategy/ignore_convergence_failure.rb +54 -54
  22. data/lib/chef/provisioning/convergence_strategy/install_cached.rb +188 -188
  23. data/lib/chef/provisioning/convergence_strategy/install_msi.rb +71 -71
  24. data/lib/chef/provisioning/convergence_strategy/install_sh.rb +71 -71
  25. data/lib/chef/provisioning/convergence_strategy/no_converge.rb +35 -35
  26. data/lib/chef/provisioning/convergence_strategy/precreate_chef_objects.rb +255 -255
  27. data/lib/chef/provisioning/driver.rb +323 -323
  28. data/lib/chef/provisioning/load_balancer_spec.rb +14 -14
  29. data/lib/chef/provisioning/machine.rb +112 -112
  30. data/lib/chef/provisioning/machine/basic_machine.rb +84 -84
  31. data/lib/chef/provisioning/machine/unix_machine.rb +288 -288
  32. data/lib/chef/provisioning/machine/windows_machine.rb +108 -108
  33. data/lib/chef/provisioning/machine_image_spec.rb +34 -34
  34. data/lib/chef/provisioning/machine_spec.rb +58 -58
  35. data/lib/chef/provisioning/managed_entry.rb +121 -121
  36. data/lib/chef/provisioning/managed_entry_store.rb +136 -136
  37. data/lib/chef/provisioning/recipe_dsl.rb +99 -99
  38. data/lib/chef/provisioning/rspec.rb +27 -27
  39. data/lib/chef/provisioning/transport.rb +100 -100
  40. data/lib/chef/provisioning/transport/ssh.rb +403 -403
  41. data/lib/chef/provisioning/transport/winrm.rb +144 -144
  42. data/lib/chef/provisioning/version.rb +5 -5
  43. data/lib/chef/resource/chef_data_bag_resource.rb +146 -146
  44. data/lib/chef/resource/load_balancer.rb +57 -57
  45. data/lib/chef/resource/machine.rb +128 -128
  46. data/lib/chef/resource/machine_batch.rb +78 -78
  47. data/lib/chef/resource/machine_execute.rb +30 -30
  48. data/lib/chef/resource/machine_file.rb +34 -34
  49. data/lib/chef/resource/machine_image.rb +35 -35
  50. data/lib/chef_metal.rb +1 -1
  51. data/spec/chef/provisioning/convergence_strategy/ignore_convergence_failure_spec.rb +86 -86
  52. data/spec/spec_helper.rb +27 -27
  53. metadata +10 -4
@@ -1,108 +1,108 @@
1
- require 'chef/provisioning/machine/basic_machine'
2
-
3
- class Chef
4
- module Provisioning
5
- class Machine
6
- class WindowsMachine < BasicMachine
7
- def initialize(machine_spec, transport, convergence_strategy)
8
- super
9
- end
10
-
11
- # Options include:
12
- #
13
- # command_prefix - prefix to put in front of any command, e.g. sudo
14
- attr_reader :options
15
-
16
- # Delete file
17
- def delete_file(action_handler, path)
18
- if file_exists?(path)
19
- action_handler.perform_action "delete file #{escape(path)} on #{machine_spec.name}" do
20
- transport.execute("Remove-Item #{escape(path)}").error!
21
- end
22
- end
23
- end
24
-
25
- def is_directory?(path)
26
- parse_boolean(transport.execute("Test-Path #{escape(path)} -pathtype container", :read_only => true).stdout)
27
- end
28
-
29
- # Return true or false depending on whether file exists
30
- def file_exists?(path)
31
- parse_boolean(transport.execute("Test-Path #{escape(path)}", :read_only => true).stdout)
32
- end
33
-
34
- def files_different?(path, local_path, content=nil)
35
- if !file_exists?(path) || (local_path && !File.exists?(local_path))
36
- return true
37
- end
38
-
39
- # Get remote checksum of file (from http://stackoverflow.com/a/13926809)
40
- result = transport.execute(<<-EOM, :read_only => true)
41
- $md5 = [System.Security.Cryptography.MD5]::Create("MD5")
42
- $fd = [System.IO.File]::OpenRead(#{path.inspect})
43
- $buf = new-object byte[] (1024*1024*8) # 8mb buffer
44
- while (($read_len = $fd.Read($buf,0,$buf.length)) -eq $buf.length){
45
- $total += $buf.length
46
- $md5.TransformBlock($buf,$offset,$buf.length,$buf,$offset)
47
- }
48
- # finalize the last read
49
- $md5.TransformFinalBlock($buf,0,$read_len)
50
- $hash = $md5.Hash
51
- # convert hash bytes to hex formatted string
52
- $hash | foreach { $hash_txt += $_.ToString("x2") }
53
- $hash_txt
54
- EOM
55
- result.error!
56
- remote_sum = result.stdout.split(' ')[0]
57
- digest = Digest::SHA256.new
58
- if content
59
- digest.update(content)
60
- else
61
- File.open(local_path, 'rb') do |io|
62
- while (buf = io.read(4096)) && buf.length > 0
63
- digest.update(buf)
64
- end
65
- end
66
- end
67
- remote_sum != digest.hexdigest
68
- end
69
-
70
- def create_dir(action_handler, path)
71
- if !file_exists?(path)
72
- action_handler.perform_action "create directory #{path} on #{machine_spec.name}" do
73
- transport.execute("New-Item #{escape(path)} -Type directory")
74
- end
75
- end
76
- end
77
-
78
- def system_drive
79
- transport.execute('$env:SystemDrive').stdout.strip
80
- end
81
-
82
- # Set file attributes { :owner, :group, :rights }
83
- # def set_attributes(action_handler, path, attributes)
84
- # end
85
-
86
- # Get file attributes { :owner, :group, :rights }
87
- # def get_attributes(path)
88
- # end
89
-
90
- def dirname_on_machine(path)
91
- path.split(/[\\\/]/)[0..-2].join('\\')
92
- end
93
-
94
- def escape(string)
95
- transport.escape(string)
96
- end
97
-
98
- def parse_boolean(string)
99
- if string =~ /^\s*true\s*$/mi
100
- true
101
- else
102
- false
103
- end
104
- end
105
- end
106
- end
107
- end
108
- end
1
+ require 'chef/provisioning/machine/basic_machine'
2
+
3
+ class Chef
4
+ module Provisioning
5
+ class Machine
6
+ class WindowsMachine < BasicMachine
7
+ def initialize(machine_spec, transport, convergence_strategy)
8
+ super
9
+ end
10
+
11
+ # Options include:
12
+ #
13
+ # command_prefix - prefix to put in front of any command, e.g. sudo
14
+ attr_reader :options
15
+
16
+ # Delete file
17
+ def delete_file(action_handler, path)
18
+ if file_exists?(path)
19
+ action_handler.perform_action "delete file #{escape(path)} on #{machine_spec.name}" do
20
+ transport.execute("Remove-Item #{escape(path)}").error!
21
+ end
22
+ end
23
+ end
24
+
25
+ def is_directory?(path)
26
+ parse_boolean(transport.execute("Test-Path #{escape(path)} -pathtype container", :read_only => true).stdout)
27
+ end
28
+
29
+ # Return true or false depending on whether file exists
30
+ def file_exists?(path)
31
+ parse_boolean(transport.execute("Test-Path #{escape(path)}", :read_only => true).stdout)
32
+ end
33
+
34
+ def files_different?(path, local_path, content=nil)
35
+ if !file_exists?(path) || (local_path && !File.exists?(local_path))
36
+ return true
37
+ end
38
+
39
+ # Get remote checksum of file (from http://stackoverflow.com/a/13926809)
40
+ result = transport.execute(<<-EOM, :read_only => true)
41
+ $md5 = [System.Security.Cryptography.MD5]::Create("MD5")
42
+ $fd = [System.IO.File]::OpenRead(#{path.inspect})
43
+ $buf = new-object byte[] (1024*1024*8) # 8mb buffer
44
+ while (($read_len = $fd.Read($buf,0,$buf.length)) -eq $buf.length){
45
+ $total += $buf.length
46
+ $md5.TransformBlock($buf,$offset,$buf.length,$buf,$offset)
47
+ }
48
+ # finalize the last read
49
+ $md5.TransformFinalBlock($buf,0,$read_len)
50
+ $hash = $md5.Hash
51
+ # convert hash bytes to hex formatted string
52
+ $hash | foreach { $hash_txt += $_.ToString("x2") }
53
+ $hash_txt
54
+ EOM
55
+ result.error!
56
+ remote_sum = result.stdout.split(' ')[0]
57
+ digest = Digest::SHA256.new
58
+ if content
59
+ digest.update(content)
60
+ else
61
+ File.open(local_path, 'rb') do |io|
62
+ while (buf = io.read(4096)) && buf.length > 0
63
+ digest.update(buf)
64
+ end
65
+ end
66
+ end
67
+ remote_sum != digest.hexdigest
68
+ end
69
+
70
+ def create_dir(action_handler, path)
71
+ if !file_exists?(path)
72
+ action_handler.perform_action "create directory #{path} on #{machine_spec.name}" do
73
+ transport.execute("New-Item #{escape(path)} -Type directory")
74
+ end
75
+ end
76
+ end
77
+
78
+ def system_drive
79
+ transport.execute('$env:SystemDrive').stdout.strip
80
+ end
81
+
82
+ # Set file attributes { :owner, :group, :rights }
83
+ # def set_attributes(action_handler, path, attributes)
84
+ # end
85
+
86
+ # Get file attributes { :owner, :group, :rights }
87
+ # def get_attributes(path)
88
+ # end
89
+
90
+ def dirname_on_machine(path)
91
+ path.split(/[\\\/]/)[0..-2].join('\\')
92
+ end
93
+
94
+ def escape(string)
95
+ transport.escape(string)
96
+ end
97
+
98
+ def parse_boolean(string)
99
+ if string =~ /^\s*true\s*$/mi
100
+ true
101
+ else
102
+ false
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -1,34 +1,34 @@
1
- require 'chef/provisioning/managed_entry'
2
-
3
- class Chef
4
- module Provisioning
5
- #
6
- # Specification for a image. Sufficient information to find and contact it
7
- # after it has been set up.
8
- #
9
- class MachineImageSpec < ManagedEntry
10
- alias :location :reference
11
- alias :location= :reference=
12
-
13
- def from_image
14
- data['from_image']
15
- end
16
- def from_image=(value)
17
- data['from_image'] = value
18
- end
19
- def run_list
20
- data['run_list']
21
- end
22
- def run_list=(value)
23
- data['run_list'] = value
24
- end
25
- def machine_options
26
- @machine_options
27
- end
28
- def machine_options=(value)
29
- Chef::Log.warn("Machine options are no longer stored in machine_image_spec. Drivers that store machine_options will stop working with Provisioning 1.0.")
30
- @machine_options = value
31
- end
32
- end
33
- end
34
- end
1
+ require 'chef/provisioning/managed_entry'
2
+
3
+ class Chef
4
+ module Provisioning
5
+ #
6
+ # Specification for a image. Sufficient information to find and contact it
7
+ # after it has been set up.
8
+ #
9
+ class MachineImageSpec < ManagedEntry
10
+ alias :location :reference
11
+ alias :location= :reference=
12
+
13
+ def from_image
14
+ data['from_image']
15
+ end
16
+ def from_image=(value)
17
+ data['from_image'] = value
18
+ end
19
+ def run_list
20
+ data['run_list']
21
+ end
22
+ def run_list=(value)
23
+ data['run_list'] = value
24
+ end
25
+ def machine_options
26
+ @machine_options
27
+ end
28
+ def machine_options=(value)
29
+ Chef::Log.warn("Machine options are no longer stored in machine_image_spec. Drivers that store machine_options will stop working with Provisioning 1.0.")
30
+ @machine_options = value
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,58 +1,58 @@
1
- require 'chef/provisioning/managed_entry'
2
-
3
- class Chef
4
- module Provisioning
5
- #
6
- # Specification for a machine. Sufficient information to find and contact it
7
- # after it has been set up.
8
- #
9
- class MachineSpec < ManagedEntry
10
- def initialize(*args)
11
- super
12
- data['name'] ||= name
13
- # Upgrade from metal to chef_provisioning ASAP.
14
- if data['normal'] && !data['normal']['chef_provisioning'] && data['normal']['metal']
15
- data['normal']['chef_provisioning'] = data['normal'].delete('metal')
16
- end
17
- end
18
-
19
- alias :node :data
20
-
21
- def attrs
22
- data['normal'] ||= {}
23
- data['normal']['chef_provisioning'] ||= {}
24
- end
25
-
26
- #
27
- # Location of this machine. This should be a freeform hash, with enough
28
- # information for the driver to look it up and create a Machine object to
29
- # access it.
30
- #
31
- # This MUST include a 'driver_url' attribute with the driver's URL in it.
32
- #
33
- # chef-provisioning will do its darnedest to not lose this information.
34
- #
35
- def reference
36
- attrs['reference'] || attrs['location']
37
- end
38
-
39
- #
40
- # Set the location for this machine.
41
- #
42
- def reference=(value)
43
- attrs.delete('location')
44
- attrs['reference'] = value
45
- end
46
-
47
- alias :location :reference
48
- alias :location= :reference=
49
-
50
- def from_image
51
- attrs['from_image']
52
- end
53
- def from_image=(value)
54
- attrs['from_image'] = value
55
- end
56
- end
57
- end
58
- end
1
+ require 'chef/provisioning/managed_entry'
2
+
3
+ class Chef
4
+ module Provisioning
5
+ #
6
+ # Specification for a machine. Sufficient information to find and contact it
7
+ # after it has been set up.
8
+ #
9
+ class MachineSpec < ManagedEntry
10
+ def initialize(*args)
11
+ super
12
+ data['name'] ||= name
13
+ # Upgrade from metal to chef_provisioning ASAP.
14
+ if data['normal'] && !data['normal']['chef_provisioning'] && data['normal']['metal']
15
+ data['normal']['chef_provisioning'] = data['normal'].delete('metal')
16
+ end
17
+ end
18
+
19
+ alias :node :data
20
+
21
+ def attrs
22
+ data['normal'] ||= {}
23
+ data['normal']['chef_provisioning'] ||= {}
24
+ end
25
+
26
+ #
27
+ # Location of this machine. This should be a freeform hash, with enough
28
+ # information for the driver to look it up and create a Machine object to
29
+ # access it.
30
+ #
31
+ # This MUST include a 'driver_url' attribute with the driver's URL in it.
32
+ #
33
+ # chef-provisioning will do its darnedest to not lose this information.
34
+ #
35
+ def reference
36
+ attrs['reference'] || attrs['location']
37
+ end
38
+
39
+ #
40
+ # Set the location for this machine.
41
+ #
42
+ def reference=(value)
43
+ attrs.delete('location')
44
+ attrs['reference'] = value
45
+ end
46
+
47
+ alias :location :reference
48
+ alias :location= :reference=
49
+
50
+ def from_image
51
+ attrs['from_image']
52
+ end
53
+ def from_image=(value)
54
+ attrs['from_image'] = value
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,121 +1,121 @@
1
- class Chef
2
- module Provisioning
3
- #
4
- # Specification for a managed thing. Remembers where it was stored, and lets
5
- # you stuff reference data in it.
6
- #
7
- class ManagedEntry
8
- def initialize(managed_entry_store, resource_type, name, data=nil)
9
- @managed_entry_store = managed_entry_store
10
- @resource_type = resource_type
11
- @name = name
12
- @data = data || {}
13
- end
14
-
15
- attr_reader :managed_entry_store
16
- attr_reader :resource_type
17
- attr_reader :name
18
- attr_reader :data
19
-
20
- def attrs
21
- data
22
- end
23
-
24
- #
25
- # Globally unique identifier for this machine. Does not depend on the machine's
26
- # reference or existence.
27
- #
28
- def id
29
- managed_entry_store.identifier(resource_type, name)
30
- end
31
-
32
- #
33
- # Reference to this managed thing. This should be a freeform hash, with enough
34
- # information for the driver to look it up and create a Machine object to
35
- # access it.
36
- #
37
- # This MUST include a 'driver_url' attribute with the driver's URL in it.
38
- #
39
- # chef-provisioning will do its darnedest to not lose this information.
40
- #
41
- def reference
42
- # Backcompat: old data bags didn't have the "reference" field. If we have
43
- # no reference field in the data, and the data bag is non-empty, return
44
- # the root of the data bag.
45
- attrs['reference'] || attrs['location'] || (attrs == {} ? nil : attrs)
46
- end
47
-
48
- #
49
- # Set the reference for this machine.
50
- #
51
- def reference=(value)
52
- self.attrs['reference'] = value
53
- end
54
-
55
- # URL to the driver.
56
- def driver_url
57
- attrs['driver_url'] || (reference ? reference['driver_url'] : nil)
58
- end
59
- def driver_url=(value)
60
- attrs['driver_url'] = value
61
- end
62
-
63
- #
64
- # Save this node to the server. If you have significant information that
65
- # could be lost, you should do this as quickly as possible. Data will be
66
- # saved automatically for you after allocate_machine and ready_machine.
67
- #
68
- def save(action_handler)
69
- managed_entry_store.save_data(resource_type, name, data, action_handler)
70
- end
71
-
72
- def delete(action_handler)
73
- managed_entry_store.delete_data(resource_type, name, action_handler)
74
- end
75
-
76
-
77
- #
78
- # Subclass interface
79
- #
80
-
81
- #
82
- # Get the given data
83
- #
84
- # @param resource_type [Symbol] The type of thing to retrieve (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet, ...)
85
- # @param name [String] The unique identifier of the thing to retrieve
86
- #
87
- # @return [Hash,Array] The data, or `nil` if the data does not exist. Will be JSON- and YAML-compatible (Hash, Array, String, Integer, Boolean, Nil)
88
- #
89
- def get_data(resource_type, name)
90
- raise NotImplementedError, :delete_data
91
- end
92
-
93
- #
94
- # Save the given data
95
- #
96
- # @param resource_type [Symbol] The type of thing to save (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet ...)
97
- # @param name [String] The unique identifier of the thing to save
98
- # @param data [Hash,Array] The data to save. Must be JSON- and YAML-compatible (Hash, Array, String, Integer, Boolean, Nil)
99
- #
100
- def save_data(resource_type, name, data, action_handler)
101
- raise NotImplementedError, :delete_data
102
- end
103
-
104
- #
105
- # Delete the given data
106
- #
107
- # @param resource_type [Symbol] The type of thing to delete (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet, ...)
108
- # @param name [String] The unique identifier of the thing to delete
109
- #
110
- # @return [Boolean] Whether anything was deleted or not.
111
- #
112
- def delete_data(resource_type, name, action_handler)
113
- raise NotImplementedError, :delete_data
114
- end
115
-
116
- def identifier(resource_type, name)
117
- raise NotImplementedError, :identifier
118
- end
119
- end
120
- end
121
- end
1
+ class Chef
2
+ module Provisioning
3
+ #
4
+ # Specification for a managed thing. Remembers where it was stored, and lets
5
+ # you stuff reference data in it.
6
+ #
7
+ class ManagedEntry
8
+ def initialize(managed_entry_store, resource_type, name, data=nil)
9
+ @managed_entry_store = managed_entry_store
10
+ @resource_type = resource_type
11
+ @name = name
12
+ @data = data || {}
13
+ end
14
+
15
+ attr_reader :managed_entry_store
16
+ attr_reader :resource_type
17
+ attr_reader :name
18
+ attr_reader :data
19
+
20
+ def attrs
21
+ data
22
+ end
23
+
24
+ #
25
+ # Globally unique identifier for this machine. Does not depend on the machine's
26
+ # reference or existence.
27
+ #
28
+ def id
29
+ managed_entry_store.identifier(resource_type, name)
30
+ end
31
+
32
+ #
33
+ # Reference to this managed thing. This should be a freeform hash, with enough
34
+ # information for the driver to look it up and create a Machine object to
35
+ # access it.
36
+ #
37
+ # This MUST include a 'driver_url' attribute with the driver's URL in it.
38
+ #
39
+ # chef-provisioning will do its darnedest to not lose this information.
40
+ #
41
+ def reference
42
+ # Backcompat: old data bags didn't have the "reference" field. If we have
43
+ # no reference field in the data, and the data bag is non-empty, return
44
+ # the root of the data bag.
45
+ attrs['reference'] || attrs['location'] || (attrs == {} ? nil : attrs)
46
+ end
47
+
48
+ #
49
+ # Set the reference for this machine.
50
+ #
51
+ def reference=(value)
52
+ self.attrs['reference'] = value
53
+ end
54
+
55
+ # URL to the driver.
56
+ def driver_url
57
+ attrs['driver_url'] || (reference ? reference['driver_url'] : nil)
58
+ end
59
+ def driver_url=(value)
60
+ attrs['driver_url'] = value
61
+ end
62
+
63
+ #
64
+ # Save this node to the server. If you have significant information that
65
+ # could be lost, you should do this as quickly as possible. Data will be
66
+ # saved automatically for you after allocate_machine and ready_machine.
67
+ #
68
+ def save(action_handler)
69
+ managed_entry_store.save_data(resource_type, name, data, action_handler)
70
+ end
71
+
72
+ def delete(action_handler)
73
+ managed_entry_store.delete_data(resource_type, name, action_handler)
74
+ end
75
+
76
+
77
+ #
78
+ # Subclass interface
79
+ #
80
+
81
+ #
82
+ # Get the given data
83
+ #
84
+ # @param resource_type [Symbol] The type of thing to retrieve (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet, ...)
85
+ # @param name [String] The unique identifier of the thing to retrieve
86
+ #
87
+ # @return [Hash,Array] The data, or `nil` if the data does not exist. Will be JSON- and YAML-compatible (Hash, Array, String, Integer, Boolean, Nil)
88
+ #
89
+ def get_data(resource_type, name)
90
+ raise NotImplementedError, :delete_data
91
+ end
92
+
93
+ #
94
+ # Save the given data
95
+ #
96
+ # @param resource_type [Symbol] The type of thing to save (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet ...)
97
+ # @param name [String] The unique identifier of the thing to save
98
+ # @param data [Hash,Array] The data to save. Must be JSON- and YAML-compatible (Hash, Array, String, Integer, Boolean, Nil)
99
+ #
100
+ def save_data(resource_type, name, data, action_handler)
101
+ raise NotImplementedError, :delete_data
102
+ end
103
+
104
+ #
105
+ # Delete the given data
106
+ #
107
+ # @param resource_type [Symbol] The type of thing to delete (:machine, :machine_image, :load_balancer, :aws_vpc, :aws_subnet, ...)
108
+ # @param name [String] The unique identifier of the thing to delete
109
+ #
110
+ # @return [Boolean] Whether anything was deleted or not.
111
+ #
112
+ def delete_data(resource_type, name, action_handler)
113
+ raise NotImplementedError, :delete_data
114
+ end
115
+
116
+ def identifier(resource_type, name)
117
+ raise NotImplementedError, :identifier
118
+ end
119
+ end
120
+ end
121
+ end