test-kitchen-rsync 3.0.0.pre.1

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 (108) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +21 -0
  3. data/LICENSE +15 -0
  4. data/Rakefile +53 -0
  5. data/bin/zl-kitchen +11 -0
  6. data/lib/kitchen/base64_stream.rb +48 -0
  7. data/lib/kitchen/chef_utils_wiring.rb +40 -0
  8. data/lib/kitchen/cli.rb +413 -0
  9. data/lib/kitchen/collection.rb +52 -0
  10. data/lib/kitchen/color.rb +63 -0
  11. data/lib/kitchen/command/action.rb +41 -0
  12. data/lib/kitchen/command/console.rb +54 -0
  13. data/lib/kitchen/command/diagnose.rb +84 -0
  14. data/lib/kitchen/command/doctor.rb +39 -0
  15. data/lib/kitchen/command/exec.rb +37 -0
  16. data/lib/kitchen/command/list.rb +148 -0
  17. data/lib/kitchen/command/login.rb +39 -0
  18. data/lib/kitchen/command/package.rb +32 -0
  19. data/lib/kitchen/command/sink.rb +50 -0
  20. data/lib/kitchen/command/test.rb +47 -0
  21. data/lib/kitchen/command.rb +207 -0
  22. data/lib/kitchen/config.rb +344 -0
  23. data/lib/kitchen/configurable.rb +616 -0
  24. data/lib/kitchen/data_munger.rb +1024 -0
  25. data/lib/kitchen/diagnostic.rb +138 -0
  26. data/lib/kitchen/driver/base.rb +133 -0
  27. data/lib/kitchen/driver/dummy.rb +105 -0
  28. data/lib/kitchen/driver/exec.rb +70 -0
  29. data/lib/kitchen/driver/proxy.rb +70 -0
  30. data/lib/kitchen/driver/ssh_base.rb +351 -0
  31. data/lib/kitchen/driver.rb +40 -0
  32. data/lib/kitchen/errors.rb +243 -0
  33. data/lib/kitchen/generator/init.rb +254 -0
  34. data/lib/kitchen/instance.rb +726 -0
  35. data/lib/kitchen/lazy_hash.rb +148 -0
  36. data/lib/kitchen/lifecycle_hook/base.rb +78 -0
  37. data/lib/kitchen/lifecycle_hook/local.rb +53 -0
  38. data/lib/kitchen/lifecycle_hook/remote.rb +39 -0
  39. data/lib/kitchen/lifecycle_hooks.rb +92 -0
  40. data/lib/kitchen/loader/yaml.rb +377 -0
  41. data/lib/kitchen/logger.rb +422 -0
  42. data/lib/kitchen/logging.rb +52 -0
  43. data/lib/kitchen/login_command.rb +49 -0
  44. data/lib/kitchen/metadata_chopper.rb +49 -0
  45. data/lib/kitchen/platform.rb +64 -0
  46. data/lib/kitchen/plugin.rb +76 -0
  47. data/lib/kitchen/plugin_base.rb +60 -0
  48. data/lib/kitchen/provisioner/base.rb +269 -0
  49. data/lib/kitchen/provisioner/chef/berkshelf.rb +116 -0
  50. data/lib/kitchen/provisioner/chef/common_sandbox.rb +350 -0
  51. data/lib/kitchen/provisioner/chef/policyfile.rb +163 -0
  52. data/lib/kitchen/provisioner/chef_apply.rb +121 -0
  53. data/lib/kitchen/provisioner/chef_base.rb +705 -0
  54. data/lib/kitchen/provisioner/chef_infra.rb +167 -0
  55. data/lib/kitchen/provisioner/chef_solo.rb +82 -0
  56. data/lib/kitchen/provisioner/chef_zero.rb +12 -0
  57. data/lib/kitchen/provisioner/dummy.rb +75 -0
  58. data/lib/kitchen/provisioner/shell.rb +157 -0
  59. data/lib/kitchen/provisioner.rb +42 -0
  60. data/lib/kitchen/rake_tasks.rb +80 -0
  61. data/lib/kitchen/shell_out.rb +90 -0
  62. data/lib/kitchen/ssh.rb +289 -0
  63. data/lib/kitchen/state_file.rb +112 -0
  64. data/lib/kitchen/suite.rb +48 -0
  65. data/lib/kitchen/thor_tasks.rb +63 -0
  66. data/lib/kitchen/transport/base.rb +236 -0
  67. data/lib/kitchen/transport/dummy.rb +78 -0
  68. data/lib/kitchen/transport/exec.rb +145 -0
  69. data/lib/kitchen/transport/ssh.rb +579 -0
  70. data/lib/kitchen/transport/winrm.rb +546 -0
  71. data/lib/kitchen/transport.rb +40 -0
  72. data/lib/kitchen/util.rb +229 -0
  73. data/lib/kitchen/verifier/base.rb +243 -0
  74. data/lib/kitchen/verifier/busser.rb +275 -0
  75. data/lib/kitchen/verifier/dummy.rb +75 -0
  76. data/lib/kitchen/verifier/shell.rb +99 -0
  77. data/lib/kitchen/verifier.rb +39 -0
  78. data/lib/kitchen/version.rb +20 -0
  79. data/lib/kitchen/which.rb +26 -0
  80. data/lib/kitchen.rb +152 -0
  81. data/lib/vendor/hash_recursive_merge.rb +79 -0
  82. data/support/busser_install_command.ps1 +14 -0
  83. data/support/busser_install_command.sh +21 -0
  84. data/support/chef-client-fail-if-update-handler.rb +15 -0
  85. data/support/chef_base_init_command.ps1 +18 -0
  86. data/support/chef_base_init_command.sh +1 -0
  87. data/support/chef_base_install_command.ps1 +85 -0
  88. data/support/chef_base_install_command.sh +229 -0
  89. data/support/download_helpers.sh +109 -0
  90. data/support/dummy-validation.pem +27 -0
  91. data/templates/driver/CHANGELOG.md.erb +3 -0
  92. data/templates/driver/Gemfile.erb +3 -0
  93. data/templates/driver/README.md.erb +64 -0
  94. data/templates/driver/Rakefile.erb +21 -0
  95. data/templates/driver/driver.rb.erb +23 -0
  96. data/templates/driver/gemspec.erb +29 -0
  97. data/templates/driver/gitignore.erb +17 -0
  98. data/templates/driver/license_apachev2.erb +15 -0
  99. data/templates/driver/license_lgplv3.erb +16 -0
  100. data/templates/driver/license_mit.erb +22 -0
  101. data/templates/driver/license_reserved.erb +5 -0
  102. data/templates/driver/tailor.erb +4 -0
  103. data/templates/driver/travis.yml.erb +11 -0
  104. data/templates/driver/version.rb.erb +12 -0
  105. data/templates/init/chefignore.erb +2 -0
  106. data/templates/init/kitchen.yml.erb +18 -0
  107. data/test-kitchen.gemspec +52 -0
  108. metadata +528 -0
@@ -0,0 +1,138 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2013, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "util"
19
+ require_relative "version"
20
+
21
+ module Kitchen
22
+ # Combines and compiles diagnostic information about a Test Kitchen
23
+ # configuration suitable for support and troubleshooting.
24
+ #
25
+ # @author Fletcher Nichol <fnichol@nichol.ca>
26
+ class Diagnostic
27
+ # Constructs a new Diagnostic object with an optional loader and optional
28
+ # instances array.
29
+ #
30
+ # @param options [Hash] optional configuration
31
+ # @option options [#diagnose,Hash] :loader a loader instance that responds
32
+ # to `#diagnose` or an error Hash
33
+ # @option options [Array<#diagnose>,Hash] :instances an Array of instances
34
+ # that respond to `#diagnose` or an error Hash
35
+ # @option options [true,false] :plugins whether or not plugins should be
36
+ # returned
37
+ def initialize(options = {})
38
+ @loader = options.fetch(:loader, nil)
39
+ @instances = options.fetch(:instances, [])
40
+ @plugins = options.fetch(:plugins, false)
41
+ @result = {}
42
+ end
43
+
44
+ # Returns a Hash with stringified keys containing diagnostic information.
45
+ #
46
+ # @return [Hash] a configuration Hash
47
+ def read
48
+ prepare_common
49
+ prepare_plugins
50
+ prepare_loader
51
+ prepare_instances
52
+
53
+ Util.stringified_hash(result)
54
+ end
55
+
56
+ private
57
+
58
+ # @return [Hash] a result hash
59
+ # @api private
60
+ attr_reader :result
61
+
62
+ # @return [#diagnose,Hash] a loader instance that responds to `#diagnose`
63
+ # or an error Hash
64
+ # @api private
65
+ attr_reader :loader
66
+
67
+ # @return [Array<#diagnose>,Hash] an Array of instances that respond to
68
+ # `#diagnose` or an error Hash
69
+ # @api private
70
+ attr_reader :instances
71
+
72
+ # Adds common information to the result Hash.
73
+ #
74
+ # @api private
75
+ def prepare_common
76
+ result[:timestamp] = Time.now.gmtime.to_s
77
+ result[:kitchen_version] = Kitchen::VERSION
78
+ end
79
+
80
+ # Adds loader information to the result Hash.
81
+ #
82
+ # @api private
83
+ def prepare_loader
84
+ if error_hash?(loader)
85
+ result[:loader] = loader
86
+ else
87
+ result[:loader] = loader.diagnose if loader
88
+ end
89
+ end
90
+
91
+ # Adds plugin information to the result Hash.
92
+ #
93
+ # @api private
94
+ def prepare_plugins
95
+ return unless @plugins
96
+
97
+ if error_hash?(instances)
98
+ result[:plugins] = { error: instances[:error] }
99
+ elsif instances.empty?
100
+ result[:plugins] = {}
101
+ else
102
+ plugins = {
103
+ driver: [], provisioner: [], transport: [], verifier: []
104
+ }
105
+ instances.map(&:diagnose_plugins).each do |plugin_hash|
106
+ plugin_hash.each { |type, plugin| plugins[type] << plugin }
107
+ end
108
+ plugins.each do |type, list|
109
+ plugins[type] =
110
+ Hash[list.uniq.map { |hash| [hash.delete(:name), hash] }]
111
+ end
112
+ result[:plugins] = plugins
113
+ end
114
+ end
115
+
116
+ # Adds instance information to the result Hash.
117
+ #
118
+ # @api private
119
+ def prepare_instances
120
+ result[:instances] = {}
121
+ if error_hash?(instances)
122
+ result[:instances][:error] = instances[:error]
123
+ else
124
+ Array(instances).each { |i| result[:instances][i.name] = i.diagnose }
125
+ end
126
+ end
127
+
128
+ # Determins whether or not the object is an error hash. An error hash is
129
+ # defined as a Hash containing an `:error` key.
130
+ #
131
+ # @param obj [Object] an object
132
+ # @return [true,false] whether or not the object is an error hash
133
+ # @api private
134
+ def error_hash?(obj)
135
+ obj.is_a?(Hash) && obj.key?(:error)
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,133 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "../configurable"
19
+ require_relative "../errors"
20
+ require_relative "../lazy_hash"
21
+ require_relative "../logging"
22
+ require_relative "../plugin_base"
23
+ require_relative "../shell_out"
24
+
25
+ module Kitchen
26
+ module Driver
27
+ # Base class for a driver.
28
+ #
29
+ # @author Fletcher Nichol <fnichol@nichol.ca>
30
+ class Base < Kitchen::Plugin::Base
31
+ include Configurable
32
+ include Logging
33
+ include ShellOut
34
+
35
+ default_config :pre_create_command, nil
36
+
37
+ # Creates a new Driver object using the provided configuration data
38
+ # which will be merged with any default configuration.
39
+ #
40
+ # @param config [Hash] provided driver configuration
41
+ def initialize(config = {})
42
+ init_config(config)
43
+ end
44
+
45
+ # Creates an instance.
46
+ #
47
+ # @param state [Hash] mutable instance and driver state
48
+ # @raise [ActionFailed] if the action could not be completed
49
+ def create(state) # rubocop:disable Lint/UnusedMethodArgument
50
+ pre_create_command
51
+ end
52
+
53
+ # Destroys an instance.
54
+ #
55
+ # @param state [Hash] mutable instance and driver state
56
+ # @raise [ActionFailed] if the action could not be completed
57
+ def destroy(state); end
58
+
59
+ # Package an instance.
60
+ #
61
+ # @param state [Hash] mutable instance and driver state
62
+ # @raise [ActionFailed] if the action could not be completed
63
+ def package(state); end
64
+
65
+ # Check system and configuration for common errors.
66
+ #
67
+ # @param state [Hash] mutable instance and driver state
68
+ # @returns [Boolean] Return true if a problem is found.
69
+ def doctor(state)
70
+ false
71
+ end
72
+
73
+ # Sets the API version for this driver. If the driver does not set this
74
+ # value, then `nil` will be used and reported.
75
+ #
76
+ # Sets the API version for this driver
77
+ #
78
+ # @example setting an API version
79
+ #
80
+ # module Kitchen
81
+ # module Driver
82
+ # class NewDriver < Kitchen::Driver::Base
83
+ #
84
+ # kitchen_driver_api_version 2
85
+ #
86
+ # end
87
+ # end
88
+ # end
89
+ #
90
+ # @param version [Integer,String] a version number
91
+ #
92
+ def self.kitchen_driver_api_version(version)
93
+ @api_version = version
94
+ end
95
+
96
+ # Cache directory that a driver could implement to inform the provisioner
97
+ # that it can leverage it internally
98
+ #
99
+ # @return path [String] a path of the cache directory
100
+ def cache_directory; end
101
+
102
+ private
103
+
104
+ # Run command if config[:pre_create_command] is set
105
+ def pre_create_command
106
+ if config[:pre_create_command]
107
+ begin
108
+ run_command(config[:pre_create_command])
109
+ rescue ShellCommandFailed => error
110
+ raise ActionFailed,
111
+ "pre_create_command '#{config[:pre_create_command]}' failed to execute #{error}"
112
+ end
113
+ end
114
+ end
115
+
116
+ # Intercepts any bare #puts calls in subclasses and issues an INFO log
117
+ # event instead.
118
+ #
119
+ # @param msg [String] message string
120
+ def puts(msg)
121
+ info(msg)
122
+ end
123
+
124
+ # Intercepts any bare #print calls in subclasses and issues an INFO log
125
+ # event instead.
126
+ #
127
+ # @param msg [String] message string
128
+ def print(msg)
129
+ info(msg)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,105 @@
1
+ #
2
+ # Author:: Fletcher Nichol (<fnichol@nichol.ca>)
3
+ #
4
+ # Copyright (C) 2012, 2013, 2014 Fletcher Nichol
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require_relative "../../kitchen"
19
+
20
+ module Kitchen
21
+ module Driver
22
+ # Dummy driver for Kitchen. This driver does nothing but report what would
23
+ # happen if this driver did anything of consequence. As a result it may
24
+ # be a useful driver to use when debugging or developing new features or
25
+ # plugins.
26
+ #
27
+ # @author Fletcher Nichol <fnichol@nichol.ca>
28
+ class Dummy < Kitchen::Driver::Base
29
+ kitchen_driver_api_version 2
30
+
31
+ plugin_version Kitchen::VERSION
32
+
33
+ default_config :sleep, 0
34
+ default_config :random_failure, false
35
+
36
+ # (see Base#create)
37
+ def create(state)
38
+ # Intentionally not calling `super` to avoid pre_create_command.
39
+ state[:my_id] = "#{instance.name}-#{Time.now.to_i}"
40
+ report(:create, state)
41
+ end
42
+
43
+ # (see Base#setup)
44
+ def setup(state)
45
+ report(:setup, state)
46
+ end
47
+
48
+ # (see Base#verify)
49
+ def verify(state)
50
+ report(:verify, state)
51
+ end
52
+
53
+ # (see Base#destroy)
54
+ def destroy(state)
55
+ report(:destroy, state)
56
+ state.delete(:my_id)
57
+ end
58
+
59
+ private
60
+
61
+ # Report what action is taking place, sleeping if so configured, and
62
+ # possibly fail randomly.
63
+ #
64
+ # @param action [Symbol] the action currently taking place
65
+ # @param state [Hash] the state hash
66
+ # @api private
67
+ def report(action, state)
68
+ what = action.capitalize
69
+ info("[Dummy] #{what} on instance=#{instance} with state=#{state}")
70
+ sleep_if_set
71
+ failure_if_set(action)
72
+ debug("[Dummy] #{what} completed (#{config[:sleep]}s).")
73
+ end
74
+
75
+ # Sleep for a period of time, if a value is set in the config.
76
+ #
77
+ # @api private
78
+ def sleep_if_set
79
+ sleep(config[:sleep].to_f) if config[:sleep].to_f > 0.0
80
+ end
81
+
82
+ # Simulate a failure in an action, if set in the config.
83
+ #
84
+ # @param action [Symbol] the action currently taking place
85
+ # @api private
86
+ def failure_if_set(action)
87
+ if config[:"fail_#{action}"]
88
+ debug("[Dummy] Failure for action ##{action}.")
89
+ raise ActionFailed, "Action ##{action} failed for #{instance.to_str}."
90
+ elsif config[:random_failure] && randomly_fail?
91
+ debug("[Dummy] Random failure for action ##{action}.")
92
+ raise ActionFailed, "Action ##{action} failed for #{instance.to_str}."
93
+ end
94
+ end
95
+
96
+ # Determine whether or not to randomly fail.
97
+ #
98
+ # @return [true, false]
99
+ # @api private
100
+ def randomly_fail?
101
+ [true, false].sample
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,70 @@
1
+ #
2
+ # Licensed under the Apache License, Version 2.0 (the "License");
3
+ # you may not use this file except in compliance with the License.
4
+ # You may obtain a copy of the License at
5
+ #
6
+ # http://www.apache.org/licenses/LICENSE-2.0
7
+ #
8
+ # Unless required by applicable law or agreed to in writing, software
9
+ # distributed under the License is distributed on an "AS IS" BASIS,
10
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ # See the License for the specific language governing permissions and
12
+ # limitations under the License.
13
+ #
14
+
15
+ require_relative "base"
16
+ require_relative "../shell_out"
17
+ require_relative "../transport/exec"
18
+ require_relative "../version"
19
+
20
+ module Kitchen
21
+ module Driver
22
+ # Simple driver that runs commands locally. As with the proxy driver, this
23
+ # has no isolation in general.
24
+ class Exec < Kitchen::Driver::Base
25
+ include ShellOut
26
+
27
+ plugin_version Kitchen::VERSION
28
+
29
+ default_config :reset_command, nil
30
+
31
+ no_parallel_for :create, :converge, :destroy
32
+
33
+ # Hack to force using the exec transport when using this driver.
34
+ # If someone comes up with a use case for using the driver with a different
35
+ # transport, please let us know.
36
+ #
37
+ # @api private
38
+ def finalize_config!(instance)
39
+ super.tap do
40
+ instance.transport = Kitchen::Transport::Exec.new
41
+ end
42
+ end
43
+
44
+ # (see Base#create)
45
+ def create(state)
46
+ super
47
+ reset_instance(state)
48
+ end
49
+
50
+ # (see Base#destroy)
51
+ def destroy(state)
52
+ reset_instance(state)
53
+ end
54
+
55
+ private
56
+
57
+ # Resets the non-Kitchen managed instance using by issuing a command
58
+ # over SSH.
59
+ #
60
+ # @param state [Hash] the state hash
61
+ # @api private
62
+ def reset_instance(state)
63
+ if (cmd = config[:reset_command])
64
+ info("Resetting instance state with command: #{cmd}")
65
+ run_command(cmd)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,70 @@
1
+ #
2
+ # Author:: Seth Chisamore <schisamo@opscode.com>
3
+ #
4
+ # Copyright:: Copyright (c) 2013 Opscode, Inc.
5
+ # License:: Apache License, Version 2.0
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+ #
13
+ # Unless required by applicable law or agreed to in writing, software
14
+ # distributed under the License is distributed on an "AS IS" BASIS,
15
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ # See the License for the specific language governing permissions and
17
+ # limitations under the License.
18
+ #
19
+
20
+ require_relative "ssh_base"
21
+ require_relative "../version"
22
+
23
+ module Kitchen
24
+ module Driver
25
+ # Simple driver that proxies commands through to a test instance whose
26
+ # lifecycle is not managed by Test Kitchen. This driver is useful for long-
27
+ # lived non-ephemeral test instances that are simply "reset" between test
28
+ # runs. Think executing against devices like network switches--this is why
29
+ # the driver was created.
30
+ #
31
+ # @author Seth Chisamore <schisamo@opscode.com>
32
+ class Proxy < Kitchen::Driver::SSHBase
33
+ plugin_version Kitchen::VERSION
34
+
35
+ required_config :host
36
+ default_config :reset_command, nil
37
+
38
+ no_parallel_for :create, :destroy
39
+
40
+ # (see Base#create)
41
+ def create(state)
42
+ # TODO: Once this isn't using SSHBase, it should call `super` to support pre_create_command.
43
+ state[:hostname] = config[:host]
44
+ reset_instance(state)
45
+ end
46
+
47
+ # (see Base#destroy)
48
+ def destroy(state)
49
+ return if state[:hostname].nil?
50
+
51
+ reset_instance(state)
52
+ state.delete(:hostname)
53
+ end
54
+
55
+ private
56
+
57
+ # Resets the non-Kitchen managed instance using by issuing a command
58
+ # over SSH.
59
+ #
60
+ # @param state [Hash] the state hash
61
+ # @api private
62
+ def reset_instance(state)
63
+ if (cmd = config[:reset_command])
64
+ info("Resetting instance state with command: #{cmd}")
65
+ ssh(build_ssh_args(state), cmd)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end