right_link 5.9.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 (199) hide show
  1. data/actors/agent_manager.rb +88 -0
  2. data/actors/instance_scheduler.rb +321 -0
  3. data/actors/instance_services.rb +64 -0
  4. data/actors/instance_setup.rb +567 -0
  5. data/bin/cloud +25 -0
  6. data/bin/cook_runner +44 -0
  7. data/bin/deploy +120 -0
  8. data/bin/enroll +385 -0
  9. data/bin/rad +32 -0
  10. data/bin/rchk +29 -0
  11. data/bin/rnac +39 -0
  12. data/bin/rs_connect +33 -0
  13. data/bin/rs_log_level +31 -0
  14. data/bin/rs_ohai +28 -0
  15. data/bin/rs_reenroll +31 -0
  16. data/bin/rs_run_recipe +34 -0
  17. data/bin/rs_run_right_script +34 -0
  18. data/bin/rs_shutdown +33 -0
  19. data/bin/rs_tag +33 -0
  20. data/bin/rs_thunk +33 -0
  21. data/bin/rstat +31 -0
  22. data/bin/system +16 -0
  23. data/ext/Rakefile +18 -0
  24. data/init/config.yml +5 -0
  25. data/init/init.rb +79 -0
  26. data/lib/chef/ohai_setup.rb +51 -0
  27. data/lib/chef/plugins/cloud.rb +91 -0
  28. data/lib/chef/plugins/cloudstack.rb +23 -0
  29. data/lib/chef/plugins/ec2.rb +23 -0
  30. data/lib/chef/plugins/linux/block_device2.rb +24 -0
  31. data/lib/chef/plugins/rackspace.rb +23 -0
  32. data/lib/chef/plugins/rightscale.rb +125 -0
  33. data/lib/chef/plugins/windows/network.rb +114 -0
  34. data/lib/chef/plugins.rb +74 -0
  35. data/lib/chef/providers/dns_dnsmadeeasy_provider.rb +81 -0
  36. data/lib/chef/providers/dns_resource.rb +100 -0
  37. data/lib/chef/providers/executable_schedule_provider.rb +70 -0
  38. data/lib/chef/providers/executable_schedule_resource.rb +144 -0
  39. data/lib/chef/providers/remote_recipe_provider.rb +86 -0
  40. data/lib/chef/providers/remote_recipe_resource.rb +101 -0
  41. data/lib/chef/providers/right_link_tag_provider.rb +73 -0
  42. data/lib/chef/providers/right_link_tag_resource.rb +59 -0
  43. data/lib/chef/providers/right_script_provider.rb +190 -0
  44. data/lib/chef/providers/right_script_resource.rb +113 -0
  45. data/lib/chef/providers/rs_shutdown_provider.rb +75 -0
  46. data/lib/chef/providers/rs_shutdown_resource.rb +55 -0
  47. data/lib/chef/providers/server_collection_provider.rb +66 -0
  48. data/lib/chef/providers/server_collection_resource.rb +93 -0
  49. data/lib/chef/providers/windows/powershell_provider.rb +151 -0
  50. data/lib/chef/providers/windows/powershell_resource.rb +111 -0
  51. data/lib/chef/providers/windows/unsupported_provider.rb +51 -0
  52. data/lib/chef/right_providers.rb +55 -0
  53. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/ChefNodeCmdlet.csproj +104 -0
  54. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/ChefNodeCmdlet.dll-Help.xml +141 -0
  55. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/Exceptions.cs +182 -0
  56. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetChefNodeCommand.cs +58 -0
  57. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetChefNodeRequest.cs +46 -0
  58. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetChefNodeResponse.cs +45 -0
  59. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetCurrentResourceCommand.cs +58 -0
  60. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetCurrentResourceRequest.cs +46 -0
  61. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetCurrentResourceResponse.cs +45 -0
  62. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNewResourceCommand.cs +58 -0
  63. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNewResourceRequest.cs +46 -0
  64. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNewResourceResponse.cs +45 -0
  65. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNextActionCommand.cs +178 -0
  66. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNextActionRequest.cs +67 -0
  67. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNextActionResponse.cs +58 -0
  68. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNodeValueCommandBase.cs +142 -0
  69. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNodeValueRequestBase.cs +64 -0
  70. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/GetNodeValueResponseBase.cs +69 -0
  71. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/JsonTransport.cs +110 -0
  72. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/PipeClient.cs +158 -0
  73. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/PipeServer.cs +142 -0
  74. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/Properties/AssemblyInfo.cs +16 -0
  75. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/ProtocolConstants.cs +55 -0
  76. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/ProtocolUtilities.cs +77 -0
  77. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/ReadMe.txt +53 -0
  78. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetChefNodeCommand.cs +59 -0
  79. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetChefNodeRequest.cs +46 -0
  80. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetChefNodeResponse.cs +58 -0
  81. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetCurrentResourceCommand.cs +59 -0
  82. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetCurrentResourceRequest.cs +46 -0
  83. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetCurrentResourceResponse.cs +40 -0
  84. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNewResourceCommand.cs +59 -0
  85. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNewResourceRequest.cs +46 -0
  86. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNewResourceResponse.cs +40 -0
  87. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNodeValueCommandBase.cs +293 -0
  88. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNodeValueRequestBase.cs +75 -0
  89. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/SetNodeValueResponseBase.cs +45 -0
  90. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet/Transport.cs +91 -0
  91. data/lib/chef/windows/ChefNodeCmdlet/ChefNodeCmdlet.sln +35 -0
  92. data/lib/chef/windows/ChefNodeCmdlet/TestChefNodeCmdlet/Program.cs +374 -0
  93. data/lib/chef/windows/ChefNodeCmdlet/TestChefNodeCmdlet/Properties/AssemblyInfo.cs +16 -0
  94. data/lib/chef/windows/ChefNodeCmdlet/TestChefNodeCmdlet/TestChefNodeCmdlet.csproj +65 -0
  95. data/lib/chef/windows/ChefNodeCmdlet/TestNextActionCmdlet/Program.cs +136 -0
  96. data/lib/chef/windows/ChefNodeCmdlet/TestNextActionCmdlet/Properties/AssemblyInfo.cs +36 -0
  97. data/lib/chef/windows/ChefNodeCmdlet/TestNextActionCmdlet/ReadMe.txt +46 -0
  98. data/lib/chef/windows/ChefNodeCmdlet/TestNextActionCmdlet/TestNextActionCmdlet.csproj +68 -0
  99. data/lib/chef/windows/bin/Newtonsoft.Json.dll +0 -0
  100. data/lib/chef/windows/chef_node_server.rb +463 -0
  101. data/lib/chef/windows/dynamic_powershell_provider.rb +296 -0
  102. data/lib/chef/windows/pipe_server.rb +283 -0
  103. data/lib/chef/windows/powershell_host.rb +285 -0
  104. data/lib/chef/windows/powershell_pipe_server.rb +136 -0
  105. data/lib/chef/windows/powershell_provider_base.rb +92 -0
  106. data/lib/chef/windows/scripts/run_loop.ps1 +105 -0
  107. data/lib/clouds/cloud.rb +557 -0
  108. data/lib/clouds/cloud_factory.rb +250 -0
  109. data/lib/clouds/cloud_utilities.rb +244 -0
  110. data/lib/clouds/clouds/azure.rb +106 -0
  111. data/lib/clouds/clouds/cloudstack.rb +114 -0
  112. data/lib/clouds/clouds/ec2.rb +113 -0
  113. data/lib/clouds/clouds/eucalyptus.rb +46 -0
  114. data/lib/clouds/clouds/google.rb +102 -0
  115. data/lib/clouds/clouds/none.rb +76 -0
  116. data/lib/clouds/clouds/openstack.rb +30 -0
  117. data/lib/clouds/clouds/rackspace-ng.rb +54 -0
  118. data/lib/clouds/clouds/rackspace.rb +78 -0
  119. data/lib/clouds/clouds/softlayer.rb +91 -0
  120. data/lib/clouds/metadata_formatter.rb +108 -0
  121. data/lib/clouds/metadata_provider.rb +128 -0
  122. data/lib/clouds/metadata_source.rb +87 -0
  123. data/lib/clouds/metadata_sources/certificate_metadata_source.rb +207 -0
  124. data/lib/clouds/metadata_sources/config_drive_metadata_source.rb +129 -0
  125. data/lib/clouds/metadata_sources/file_metadata_source.rb +74 -0
  126. data/lib/clouds/metadata_sources/http_metadata_source.rb +277 -0
  127. data/lib/clouds/metadata_sources/selective_metadata_source.rb +122 -0
  128. data/lib/clouds/metadata_tree_climber.rb +144 -0
  129. data/lib/clouds/metadata_writer.rb +155 -0
  130. data/lib/clouds/metadata_writers/dictionary_metadata_writer.rb +72 -0
  131. data/lib/clouds/metadata_writers/ruby_metadata_writer.rb +76 -0
  132. data/lib/clouds/metadata_writers/shell_metadata_writer.rb +121 -0
  133. data/lib/clouds/register_clouds.rb +34 -0
  134. data/lib/clouds.rb +32 -0
  135. data/lib/gem_dependencies.rb +83 -0
  136. data/lib/git_hooks/commit-msg.rb +7 -0
  137. data/lib/instance/agent_config.rb +168 -0
  138. data/lib/instance/agent_watcher.rb +233 -0
  139. data/lib/instance/audit_cook_stub.rb +104 -0
  140. data/lib/instance/audit_proxy.rb +247 -0
  141. data/lib/instance/bundle_queue.rb +104 -0
  142. data/lib/instance/cook/agent_connection.rb +109 -0
  143. data/lib/instance/cook/audit_logger.rb +165 -0
  144. data/lib/instance/cook/audit_stub.rb +142 -0
  145. data/lib/instance/cook/ca-bundle.crt +2794 -0
  146. data/lib/instance/cook/chef_state.rb +211 -0
  147. data/lib/instance/cook/cook.rb +306 -0
  148. data/lib/instance/cook/cook_state.rb +298 -0
  149. data/lib/instance/cook/cookbook_path_mapping.rb +66 -0
  150. data/lib/instance/cook/cookbook_repo_retriever.rb +190 -0
  151. data/lib/instance/cook/executable_sequence.rb +765 -0
  152. data/lib/instance/cook/external_parameter_gatherer.rb +190 -0
  153. data/lib/instance/cook/repose_downloader.rb +349 -0
  154. data/lib/instance/cook/shutdown_request_proxy.rb +121 -0
  155. data/lib/instance/cook.rb +41 -0
  156. data/lib/instance/downloader.rb +208 -0
  157. data/lib/instance/duplicable.rb +67 -0
  158. data/lib/instance/exceptions.rb +49 -0
  159. data/lib/instance/executable_sequence_proxy.rb +278 -0
  160. data/lib/instance/instance_commands.rb +577 -0
  161. data/lib/instance/instance_state.rb +633 -0
  162. data/lib/instance/json_utilities.rb +102 -0
  163. data/lib/instance/login_manager.rb +533 -0
  164. data/lib/instance/login_user_manager.rb +522 -0
  165. data/lib/instance/message_encoder.rb +118 -0
  166. data/lib/instance/multi_thread_bundle_queue.rb +232 -0
  167. data/lib/instance/operation_context.rb +60 -0
  168. data/lib/instance/options_bag.rb +65 -0
  169. data/lib/instance/payload_formatter.rb +46 -0
  170. data/lib/instance/policy.rb +53 -0
  171. data/lib/instance/policy_audit.rb +100 -0
  172. data/lib/instance/policy_manager.rb +146 -0
  173. data/lib/instance/reenroll_manager.rb +104 -0
  174. data/lib/instance/right_scripts_cookbook.rb +181 -0
  175. data/lib/instance/shutdown_request.rb +221 -0
  176. data/lib/instance/single_thread_bundle_queue.rb +189 -0
  177. data/lib/instance/volume_management.rb +450 -0
  178. data/lib/instance.rb +50 -0
  179. data/lib/repo_conf_generators/apt_conf_generators.rb +106 -0
  180. data/lib/repo_conf_generators/gem_conf_generators.rb +80 -0
  181. data/lib/repo_conf_generators/rightscale_conf_generators.rb +254 -0
  182. data/lib/repo_conf_generators/rightscale_key.pub +17 -0
  183. data/lib/repo_conf_generators/yum_conf_generators.rb +225 -0
  184. data/lib/repo_conf_generators.rb +30 -0
  185. data/lib/run_shell.rb +28 -0
  186. data/scripts/agent_checker.rb +571 -0
  187. data/scripts/agent_controller.rb +247 -0
  188. data/scripts/agent_deployer.rb +148 -0
  189. data/scripts/bundle_runner.rb +336 -0
  190. data/scripts/cloud_controller.rb +176 -0
  191. data/scripts/log_level_manager.rb +142 -0
  192. data/scripts/ohai_runner.rb +33 -0
  193. data/scripts/reenroller.rb +193 -0
  194. data/scripts/server_importer.rb +293 -0
  195. data/scripts/shutdown_client.rb +183 -0
  196. data/scripts/system_configurator.rb +367 -0
  197. data/scripts/tagger.rb +381 -0
  198. data/scripts/thunker.rb +356 -0
  199. metadata +418 -0
@@ -0,0 +1,765 @@
1
+ #
2
+ # Copyright (c) 2009-2012 RightScale Inc
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+
23
+ require 'right_http_connection'
24
+ require 'right_popen'
25
+ require 'socket'
26
+ require 'tempfile'
27
+ require 'fileutils'
28
+
29
+ # The daemonize method of AR clashes with the daemonize Chef attribute, we don't need that method so undef it
30
+ undef :daemonize if methods.include?('daemonize')
31
+
32
+ require File.normalize_path(File.join(File.dirname(__FILE__), '..', '..', 'chef', 'ohai_setup'))
33
+ require File.normalize_path(File.join(File.dirname(__FILE__), 'cookbook_path_mapping'))
34
+ require File.normalize_path(File.join(File.dirname(__FILE__), 'cookbook_repo_retriever'))
35
+
36
+ class File
37
+ class << self
38
+ unless method_defined?(:world_writable?)
39
+ def world_writable?(filename)
40
+ (File.stat(filename).mode & 0002) != 0
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module RightScale
47
+ # Bundle sequence, includes installing dependent packages,
48
+ # downloading attachments and running scripts in given bundle.
49
+ # Also downloads cookbooks and run recipes in given bundle.
50
+ # Runs in separate (runner) process.
51
+ class ExecutableSequence
52
+ include EM::Deferrable
53
+
54
+ # Min number of seconds to wait before retrying Ohai to get the hostname
55
+ OHAI_RETRY_MIN_DELAY = 20
56
+ # Max number of seconds to wait before retrying Ohai to get the hostname
57
+ OHAI_RETRY_MAX_DELAY = 20 * 60
58
+ # Regexp to use when reporting extended information about Chef failures (line-number, etc)
59
+ BACKTRACE_LINE_REGEXP = /(.+):(\d+):in `(.+)'/
60
+
61
+ # Patch to be applied to inputs stored in core
62
+ attr_accessor :inputs_patch
63
+
64
+ # Failure title and message if any
65
+ attr_reader :failure_title, :failure_message
66
+
67
+ # Initialize sequence
68
+ #
69
+ # === Parameter
70
+ # bundle(RightScale::ExecutableBundle):: Bundle to be run
71
+ def initialize(bundle)
72
+ @description = bundle.to_s
73
+ @thread_name = get_thread_name_from_bundle(bundle)
74
+ @right_scripts_cookbook = RightScriptsCookbook.new(@thread_name)
75
+ @scripts = bundle.executables.select { |e| e.is_a?(RightScriptInstantiation) }
76
+ run_list_recipes = bundle.executables.map { |e| e.is_a?(RecipeInstantiation) ? e : @right_scripts_cookbook.recipe_from_right_script(e) }
77
+ @cookbooks = bundle.cookbooks
78
+ @downloader = ReposeDownloader.new(bundle.repose_servers)
79
+ @downloader.logger = Log
80
+ @download_path = File.join(AgentConfig.cookbook_download_dir, @thread_name)
81
+ @powershell_providers = nil
82
+ @ohai_retry_delay = OHAI_RETRY_MIN_DELAY
83
+ @audit = AuditStub.instance
84
+ @logger = Log
85
+ @cookbook_repo_retriever= CookbookRepoRetriever.new(@download_path, bundle.dev_cookbooks)
86
+
87
+ # Initialize run list for this sequence (partial converge support)
88
+ @run_list = []
89
+ @inputs = { }
90
+ breakpoint = CookState.breakpoint
91
+ run_list_recipes.each do |recipe|
92
+ if recipe.nickname == breakpoint
93
+ @audit.append_info("Breakpoint set to < #{breakpoint} >")
94
+ break
95
+ end
96
+ @run_list << recipe.nickname
97
+ ::RightSupport::Data::HashTools.deep_merge!(@inputs, recipe.attributes)
98
+ end
99
+
100
+ # Retrieve node attributes and deep merge in inputs
101
+ @attributes = ChefState.attributes
102
+ ::RightSupport::Data::HashTools.deep_merge!(@attributes, @inputs)
103
+ end
104
+
105
+ # FIX: thread_name should never be nil from the core in future, but
106
+ # temporarily we must supply the default thread_name before if nil. in
107
+ # future we should fail execution when thread_name is reliably present and
108
+ # for any reason does not match ::RightScale::AgentConfig.valid_thread_name
109
+ # see also ExecutableSequenceProxy#initialize
110
+ #
111
+ # === Parameters
112
+ # bundle(ExecutableBundle):: An executable bundle
113
+ #
114
+ # === Return
115
+ # result(String):: Thread name of this bundle
116
+ def get_thread_name_from_bundle(bundle)
117
+ thread_name = nil
118
+ thread_name = bundle.runlist_policy.thread_name if bundle.respond_to?(:runlist_policy) && bundle.runlist_policy
119
+ Log.warn("Encountered a nil thread name unexpectedly, defaulting to '#{RightScale::AgentConfig.default_thread_name}'") unless thread_name
120
+ thread_name ||= RightScale::AgentConfig.default_thread_name
121
+ unless thread_name =~ RightScale::AgentConfig.valid_thread_name
122
+ raise ArgumentError, "Invalid thread name #{thread_name.inspect}"
123
+ end
124
+ thread_name
125
+ end
126
+
127
+ # FIX: This code can be removed once the core sends a runlist policy
128
+ #
129
+ # === Parameters
130
+ # bundle(ExecutableBundle):: An executable bundle
131
+ #
132
+ # === Return
133
+ # result(String):: Policy name of this bundle
134
+ def get_policy_name_from_bundle(bundle)
135
+ policy_name = nil
136
+ policy_name ||= bundle.runlist_policy.policy_name if bundle.respond_to?(:runlist_policy) && bundle.runlist_policy
137
+ policy_name
138
+ end
139
+
140
+ # Run given executable bundle
141
+ # Asynchronous, set deferrable object's disposition
142
+ #
143
+ # === Return
144
+ # true:: Always return true
145
+ def run
146
+ @ok = true
147
+ if @run_list.empty?
148
+ # Deliberately avoid auditing anything since we did not run any recipes
149
+ # Still download the cookbooks repos if in dev mode
150
+ checkout_cookbook_repos
151
+ download_cookbooks if CookState.cookbooks_path
152
+ report_success(nil)
153
+ else
154
+ configure_ohai
155
+ configure_logging
156
+ configure_chef
157
+ download_attachments if @ok
158
+ install_packages if @ok
159
+ checkout_cookbook_repos if @ok
160
+ download_cookbooks if @ok
161
+ update_cookbook_path if @ok
162
+ setup_powershell_providers if RightScale::Platform.windows?
163
+ check_ohai { |o| converge(o) } if @ok
164
+ end
165
+ true
166
+ rescue Exception => e
167
+ report_failure('Execution failed', "The following exception was caught while preparing for execution: (#{e.message}) from\n#{e.backtrace.join("\n")}")
168
+ end
169
+
170
+ protected
171
+
172
+ def configure_ohai
173
+ # Ohai plugins path and logging.
174
+ #
175
+ # note that this was moved to a separate .rb file to ensure that plugins
176
+ # path is not relative to this potentially relocatable source file.
177
+ RightScale::OhaiSetup.configure_ohai
178
+ end
179
+
180
+ # Initialize and configure the logger
181
+ def configure_logging
182
+ Chef::Log.logger = AuditLogger.new
183
+ Chef::Log.logger.level = Log.level_from_sym(Log.level)
184
+ end
185
+
186
+ # Configure chef so it can find cookbooks and so its logs go to the audits
187
+ #
188
+ # === Return
189
+ # true:: Always return true
190
+ def configure_chef
191
+ # setup logger for mixlib-shellout gem to consume instead of the chef
192
+ # v0.10.10 behavior of not logging ShellOut calls by default. also setup
193
+ # command failure exception and callback for legacy reasons.
194
+ ::Mixlib::ShellOut.default_logger = ::Chef::Log
195
+ ::Mixlib::ShellOut.command_failure_callback = lambda do |params|
196
+ failure_reason = ::RightScale::SubprocessFormatting.reason(params[:status])
197
+ expected_error_codes = Array(params[:args][:returns]).join(' or ')
198
+ ::RightScale::Exceptions::Exec.new("\"#{params[:args][:command]}\" #{failure_reason}, expected #{expected_error_codes}.",
199
+ params[:args][:cwd])
200
+ end
201
+
202
+ # Chef run mode is always solo for cook
203
+ Chef::Config[:solo] = true
204
+
205
+ # determine default cookbooks path. If debugging cookbooks, place the debug pat(s) first, otherwise
206
+ # clear out the list as it will be filled out with cookbooks needed for this converge as they are downloaded.
207
+ if CookState.use_cookbooks_path?
208
+ Chef::Config[:cookbook_path] = [CookState.cookbooks_path].flatten
209
+ @audit.append_info("Using development cookbooks repositories path:\n\t- #{Chef::Config[:cookbook_path].join("\n\t- ")}")
210
+ else
211
+ # reset the cookbook path. Will be filled out with cookbooks needed for this execution
212
+ Chef::Config[:cookbook_path] = []
213
+ end
214
+ # add the rightscript cookbook if there are rightscripts in this converge
215
+ Chef::Config[:cookbook_path] << @right_scripts_cookbook.repo_dir unless @right_scripts_cookbook.empty?
216
+
217
+ # must set file cache path and ensure it exists otherwise evented run_command will fail
218
+ file_cache_path = File.join(AgentConfig.cache_dir, 'chef')
219
+ Chef::Config[:file_cache_path] = file_cache_path
220
+ FileUtils.mkdir_p(Chef::Config[:file_cache_path])
221
+
222
+ Chef::Config[:cache_options][:path] = File.join(file_cache_path, 'checksums')
223
+ FileUtils.mkdir_p(Chef::Config[:cache_options][:path])
224
+
225
+ # Where backups of chef-managed files should go. Set to nil to backup to the same directory the file being backed up is in.
226
+ Chef::Config[:file_backup_path] = nil
227
+
228
+ true
229
+ end
230
+
231
+ # Download attachments, update @ok
232
+ #
233
+ # === Return
234
+ # true:: Always return true
235
+ def download_attachments
236
+ unless @scripts.all? { |s| s.attachments.empty? }
237
+ @audit.create_new_section('Downloading attachments')
238
+ audit_time do
239
+ @scripts.each do |script|
240
+ attach_dir = @right_scripts_cookbook.cache_dir(script)
241
+ script.attachments.each do |a|
242
+ script_file_path = File.join(attach_dir, a.file_name)
243
+ @audit.update_status("Downloading #{a.file_name} into #{script_file_path} through Repose")
244
+ begin
245
+ attachment_dir = File.dirname(script_file_path)
246
+ FileUtils.mkdir_p(attachment_dir)
247
+ tempfile = Tempfile.open('attachment', attachment_dir)
248
+ tempfile.binmode
249
+ @downloader.download(a.url) do |response|
250
+ tempfile << response
251
+ end
252
+ File.unlink(script_file_path) if File.exists?(script_file_path)
253
+ File.link(tempfile.path, script_file_path)
254
+ tempfile.close!
255
+ @audit.append_info(@downloader.details)
256
+ rescue Exception => e
257
+ tempfile.close! unless tempfile.nil?
258
+ @audit.append_info("Repose download failed: #{e.message}.")
259
+ if e.kind_of?(ReposeDownloader::DownloadException) && e.message.include?("Forbidden")
260
+ @audit.append_info("Often this means the download URL has expired while waiting for inputs to be satisfied.")
261
+ end
262
+ report_failure("Failed to download attachment '#{a.file_name}'", e.message)
263
+ end
264
+ end
265
+ end
266
+ end
267
+ end
268
+ true
269
+ end
270
+
271
+ # Install required software packages, update @ok
272
+ # Always update the apt cache even if there is no package for recipes
273
+ #
274
+ # === Return
275
+ # true:: Always return true
276
+ def install_packages
277
+ packages = []
278
+ @scripts.each { |s| packages.push(s.packages) if s.packages && !s.packages.empty? }
279
+ return true if packages.empty?
280
+
281
+ success = false
282
+ installer = RightScale::Platform.installer
283
+
284
+ @audit.create_new_section("Installing packages: #{packages.uniq.join(' ')}")
285
+ audit_time do
286
+ success = retry_execution('Installation of packages failed, retrying...') do
287
+ begin
288
+ installer.install(packages)
289
+ rescue Exception => e
290
+ @audit.append_output(installer.output)
291
+ report_failure('Failed to install packages', e.message)
292
+ else
293
+ @audit.append_output(installer.output)
294
+ end
295
+ $?.success?
296
+ end
297
+ end
298
+ report_failure('Failed to install packages', 'Package install exited with bad status') unless success
299
+ true
300
+ end
301
+
302
+ # Update the Chef cookbook_path based on the cookbooks in the bundle.
303
+ #
304
+ # === Return
305
+ # true:: Always return true
306
+ def update_cookbook_path
307
+ # both cookbook sequences and paths are listed in same order as
308
+ # presented in repo UI. previous to RL v5.7 we received cookbook sequences
309
+ # in an arbitrary order, but this has been fixed as of the release of v5.8
310
+ # (we will not change the order for v5.7-).
311
+ # for chef to execute repos and paths in the order listed, both of these
312
+ # ordered lists need to be inserted in reverse order because the chef code
313
+ # replaces cookbook paths as it reads the array from beginning to end.
314
+ @cookbooks.reverse.each do |cookbook_sequence|
315
+ local_basedir = File.join(@download_path, cookbook_sequence.hash)
316
+ cookbook_sequence.paths.reverse.each do |path|
317
+ dir = File.expand_path(File.join(local_basedir, path))
318
+ unless Chef::Config[:cookbook_path].include?(dir)
319
+ if File.directory?(dir)
320
+ Chef::Config[:cookbook_path] << dir
321
+ else
322
+ RightScale::Log.info("Excluding #{path} from chef cookbooks_path because it was not downloaded")
323
+ end
324
+ end
325
+ end
326
+ end
327
+ RightScale::Log.info("Updated cookbook_path to: #{Chef::Config[:cookbook_path].join(", ")}")
328
+ true
329
+ end
330
+
331
+ AUDIT_BEGIN_OPERATIONS = Set.new([:scraping]).freeze unless defined?(AUDIT_BEGIN_OPERATIONS)
332
+
333
+ AUDIT_COMMIT_OPERATIONS = Set.new([:initialize,
334
+ :retrieving,
335
+ :reading_cookbook,
336
+ :scraping]).freeze unless defined?(AUDIT_COMMIT_OPERATIONS)
337
+
338
+ # Checkout repositories for selected cookbooks. Audit progress and errors, do not fail on checkout error.
339
+ #
340
+ # === Return
341
+ # true:: Always return true
342
+ def checkout_cookbook_repos
343
+ return true unless @cookbook_repo_retriever.has_cookbooks?
344
+
345
+ @audit.create_new_section('Checking out cookbooks for development')
346
+ @audit.append_info("Cookbook repositories will be checked out to #{@cookbook_repo_retriever.checkout_root}")
347
+
348
+ audit_time do
349
+ # only create a scraper if there are dev cookbooks
350
+ @cookbook_repo_retriever.checkout_cookbook_repos do |state, operation, explanation, exception|
351
+ # audit progress
352
+ case state
353
+ when :begin
354
+ @audit.append_info("start #{operation} #{explanation}") if AUDIT_BEGIN_OPERATIONS.include?(operation)
355
+ when :commit
356
+ @audit.append_info("finish #{operation} #{explanation}") if AUDIT_COMMIT_OPERATIONS.include?(operation)
357
+ when :abort
358
+ @audit.append_error("Failed #{operation} #{explanation}")
359
+ Log.error(Log.format("Failed #{operation} #{explanation}", exception, :trace))
360
+ end
361
+ end
362
+ end
363
+ end
364
+
365
+ # Download required cookbooks from Repose mirror; update @ok.
366
+ # Note: Starting with Chef 0.8, the cookbooks repositories list must be traversed in reverse
367
+ # order to preserve the semantic of the dashboard (first repo has priority)
368
+ #
369
+ # === Return
370
+ # true:: Always return true
371
+ def download_cookbooks
372
+ # first, if @download_path is world writable, stop that nonsense right this second.
373
+ unless RightScale::Platform.windows?
374
+ if File.exists?(@download_path) && File.world_writable?(@download_path)
375
+ Log.warn("Cookbooks download path world writable; fixing.")
376
+ File.chmod(0755, @download_path)
377
+ end
378
+ end
379
+
380
+ unless CookState.download_once?
381
+ Log.info("Deleting existing cookbooks")
382
+ # second, wipe out any preexisting cookbooks in the download path
383
+ if File.directory?(@download_path)
384
+ Dir.foreach(@download_path) do |entry|
385
+ FileUtils.remove_entry_secure(File.join(@download_path, entry)) if entry =~ /\A[[:xdigit:]]+\Z/
386
+ end
387
+ end
388
+ end
389
+
390
+ unless @cookbooks.empty?
391
+ # only create audit output if we're actually going to download something!
392
+ @audit.create_new_section('Retrieving cookbooks')
393
+ audit_time do
394
+ @cookbooks.each do |cookbook_sequence|
395
+ cookbook_sequence.positions.each do |position|
396
+ if @cookbook_repo_retriever.should_be_linked?(cookbook_sequence.hash, position.position)
397
+ begin
398
+ @cookbook_repo_retriever.link(cookbook_sequence.hash, position.position)
399
+ rescue Exception => e
400
+ ::RightScale::Log.error("Failed to link #{position.cookbook.name} for development", e)
401
+ end
402
+ else
403
+ # download with repose
404
+ cookbook_path = CookbookPathMapping.repose_path(@download_path, cookbook_sequence.hash, position.position)
405
+ if File.exists?(cookbook_path)
406
+ @audit.append_info("Skipping #{position.cookbook.name}, already there")
407
+ else
408
+ download_cookbook(cookbook_path, position.cookbook)
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
414
+ end
415
+
416
+ # record that cookbooks have been downloaded so we do not download them again in Dev mode
417
+ CookState.has_downloaded_cookbooks = true
418
+
419
+ true
420
+ rescue Exception => e
421
+ report_failure("Failed to download cookbook", "Cannot continue due to #{e.class.name}: #{e.message}.")
422
+ Log.debug(Log.format("Failed to download cookbook", e, :trace))
423
+ end
424
+
425
+ #
426
+ # Download a cookbook from Repose mirror and extract it to the filesystem.
427
+ #
428
+ # === Parameters
429
+ # root_dir(String):: subdir of basedir into which this cookbook goes
430
+ # cookbook(Cookbook):: cookbook
431
+ #
432
+ # === Raise
433
+ # Propagates exceptions raised by callees, namely DownloadFailure
434
+ # and ReposeServerFailure
435
+ #
436
+ # === Return
437
+ # true:: always returns true
438
+ def download_cookbook(root_dir, cookbook)
439
+ cache_dir = File.join(AgentConfig.cache_dir, "right_link", "cookbooks")
440
+ cookbook_tarball = File.join(cache_dir, "#{cookbook.hash.split('?').first}.tar")
441
+ begin
442
+ FileUtils.mkdir_p(cache_dir)
443
+ File.open(cookbook_tarball, "ab") do |tarball|
444
+ if tarball.stat.size == 0
445
+ #audit cookbook name & part of hash (as a disambiguator)
446
+ name = cookbook.name ; tag = cookbook.hash[0..4]
447
+ @audit.append_info("Downloading cookbook '#{name}' (#{tag})")
448
+ @downloader.download("/cookbooks/#{cookbook.hash}") do |response|
449
+ tarball << response
450
+ end
451
+ @audit.append_info(@downloader.details)
452
+ end
453
+ end
454
+ rescue Exception => e
455
+ File.unlink(cookbook_tarball) if File.exists?(cookbook_tarball)
456
+ raise
457
+ end
458
+
459
+ @audit.append_info("Success; unarchiving cookbook")
460
+
461
+ # The local basedir is the faux "repository root" into which we extract all related
462
+ # cookbooks in that set, "related" meaning a set of cookbooks that originally came
463
+ # from the same Chef cookbooks repository as observed by the scraper.
464
+ #
465
+ # Even though we are pulling individually-packaged cookbooks and not the whole repository,
466
+ # we preserve the position of cookbooks in the directory hierarchy such that a given cookbook
467
+ # has the same path relative to the local basedir as the original cookbook had relative to the
468
+ # base directory of its repository.
469
+ #
470
+ # This ensures we will be able to deal with future changes to the Chef merge algorithm,
471
+ # as well as accommodate "naughty" cookbooks that side-load data from the filesystem
472
+ # using relative paths to other cookbooks.
473
+ FileUtils.mkdir_p(root_dir)
474
+ Dir.chdir(root_dir) do
475
+ # note that Windows uses a "tar.cmd" file which is found via the PATH
476
+ # used by the command interpreter.
477
+ cmd = "tar xf #{cookbook_tarball.inspect} 2>&1"
478
+ Log.debug(cmd)
479
+ output = `#{cmd}`
480
+ @audit.append_info(output)
481
+ unless $?.success?
482
+ report_failure("Unknown error", SubprocessFormatting.reason($?))
483
+ end
484
+ end
485
+ return true
486
+ end
487
+
488
+ # Create Powershell providers from cookbook repos
489
+ #
490
+ #
491
+ # === Return
492
+ # true:: Always return true
493
+ def setup_powershell_providers
494
+ dynamic_provider = DynamicPowershellProvider.new
495
+ dynamic_provider.generate_providers(Chef::Config[:cookbook_path])
496
+ @powershell_providers = dynamic_provider.providers
497
+ end
498
+
499
+ # Checks whether Ohai is ready and calls given block with it
500
+ # if that's the case otherwise schedules itself to try again
501
+ # indefinitely
502
+ #
503
+ # === Block
504
+ # Given block should take one argument which corresponds to
505
+ # ohai instance
506
+ #
507
+ # === Return
508
+ # true:: Always return true
509
+ def check_ohai(&block)
510
+ ohai = create_ohai
511
+ if ohai[:hostname]
512
+ block.call(ohai)
513
+ else
514
+ Log.warning("Could not determine node name from Ohai, will retry in #{@ohai_retry_delay}s...")
515
+ # Need to execute on defer thread consistent with where ExecutableSequence is running
516
+ # otherwise EM main thread command client activity will block
517
+ EM.add_timer(@ohai_retry_delay) { EM.defer { check_ohai(&block) } }
518
+ @ohai_retry_delay = [2 * @ohai_retry_delay, OHAI_RETRY_MAX_DELAY].min
519
+ end
520
+ true
521
+ end
522
+
523
+ # Creates a new ohai and configures it.
524
+ #
525
+ # === Return
526
+ # ohai(Ohai::System):: configured ohai
527
+ def create_ohai
528
+ ohai = Ohai::System.new
529
+ ohai.require_plugin('os')
530
+ ohai.require_plugin('hostname')
531
+ return ohai
532
+ end
533
+
534
+ # Chef converge
535
+ #
536
+ # === Parameters
537
+ # ohai(Ohai):: Ohai instance to be used by Chef
538
+ #
539
+ # === Return
540
+ # true:: Always return true
541
+ def converge(ohai)
542
+ begin
543
+ # suppress unnecessary error log output for cases of explictly exiting
544
+ # from converge (rs_shutdown, etc.).
545
+ ::Chef::Client.clear_notifications
546
+
547
+ if @cookbooks.size > 0
548
+ @audit.create_new_section('Converging')
549
+ else
550
+ @audit.create_new_section('Preparing execution')
551
+ end
552
+
553
+ @audit.append_info("Run list for thread #{@thread_name.inspect} contains #{@run_list.size} items.")
554
+ @audit.append_info(@run_list.join(', '))
555
+
556
+ attribs = { 'run_list' => @run_list }
557
+ attribs.merge!(@attributes) if @attributes
558
+ c = Chef::Client.new(attribs)
559
+ c.ohai = ohai
560
+ audit_time do
561
+ # Ensure that Ruby subprocesses invoked by Chef do not inherit our
562
+ # RubyGems/Bundler environment.
563
+ without_bundler_env do
564
+ c.run
565
+ end
566
+ end
567
+ rescue SystemExit => e
568
+ # exit is expected in case where a script has invoked rs_shutdown
569
+ # (command line tool or Chef resource). exit is considered to be
570
+ # unexpected if rs_shutdown has not been called. note that it is
571
+ # possible, but not a 'best practice', for a recipe (but not a
572
+ # RightScript) to call rs_shutdown as an external command line utility
573
+ # without calling exit (i.e. request a deferred reboot) and continue
574
+ # running recipes until the list of recipes is complete. in this case,
575
+ # the shutdown occurs after subsequent recipes have finished. the best
576
+ # practice for a recipe is to use the rs_shutdown chef resource which
577
+ # calls exit when appropriate.
578
+ shutdown_request = RightScale::ShutdownRequestProxy.instance
579
+ if shutdown_request.continue?
580
+ report_failure('Execution failed due to rs_shutdown not being called before exit', chef_error(e))
581
+ Log.debug(Log.format("Execution failed", e, :trace))
582
+ else
583
+ Log.info("Shutdown requested by script: #{shutdown_request}")
584
+ end
585
+ rescue Exception => e
586
+ report_failure('Execution failed', chef_error(e))
587
+ Log.debug(Log.format("Execution failed", e, :trace))
588
+ ensure
589
+ # terminate the powershell providers
590
+ # terminate the providers before the node server as the provider term scripts may still use the node server
591
+ if @powershell_providers
592
+ @powershell_providers.each do |p|
593
+ begin
594
+ p.terminate
595
+ rescue Exception => e
596
+ Log.debug(Log.format("Error terminating #{p.inspect}", e, :trace))
597
+ end
598
+ end
599
+ end
600
+
601
+ # kill the chef node provider
602
+ RightScale::Windows::ChefNodeServer.instance.stop rescue nil if RightScale::Platform.windows?
603
+ end
604
+ report_success(c.node) if @ok
605
+ true
606
+ end
607
+
608
+ # Initialize inputs patch and report success
609
+ #
610
+ # === Parameters
611
+ # node(ChefNode):: Chef node used to converge, can be nil (patch is empty in this case)
612
+ #
613
+ # === Return
614
+ # true:: Always return true
615
+ def report_success(node)
616
+ ChefState.merge_attributes(node.normal_attrs) if node
617
+ patch = ::RightSupport::Data::HashTools.deep_create_patch(@inputs, ChefState.attributes)
618
+ # We don't want to send back new attributes (ohai etc.)
619
+ patch[:right_only] = { }
620
+ @inputs_patch = patch
621
+ EM.next_tick { succeed }
622
+ true
623
+ end
624
+
625
+ # Set status with failure message and audit it
626
+ #
627
+ # === Parameters
628
+ # title(String):: Title used to update audit status
629
+ # msg(String):: Failure message
630
+ #
631
+ # === Return
632
+ # true:: Always return true
633
+ def report_failure(title, msg)
634
+ @ok = false
635
+ @failure_title = title
636
+ @failure_message = msg
637
+ # note that the errback handler is expected to audit the message based on
638
+ # the preserved title and message and so we don't audit it here.
639
+ EM.next_tick { fail }
640
+ true
641
+ end
642
+
643
+ # Wrap chef exception with explanatory information and show
644
+ # context of failure
645
+ #
646
+ # === Parameters
647
+ # e(Exception):: Exception raised while executing Chef recipe
648
+ #
649
+ # === Return
650
+ # msg(String):: Human friendly error message
651
+ def chef_error(e)
652
+ if e.is_a?(::RightScale::Exceptions::Exec)
653
+ msg = "External command error: "
654
+ if match = /RightScale::Exceptions::Exec: (.*)/.match(e.message)
655
+ cmd_output = match[1]
656
+ else
657
+ cmd_output = e.message
658
+ end
659
+ msg += cmd_output
660
+ msg += "\nThe command was run from \"#{e.path}\"" if e.path
661
+ elsif e.is_a?(::Chef::Exceptions::ValidationFailed) && (e.message =~ /Option action must be equal to one of:/)
662
+ msg = "[chef] recipe references an action that does not exist. #{e.message}"
663
+ elsif e.is_a?(::NoMethodError) && (missing_action_match = /undefined method .action_(\S*)' for #<\S*:\S*>/.match(e.message)) && missing_action_match[1]
664
+ msg = "[chef] recipe references the action <#{missing_action_match[1]}> which is missing an implementation"
665
+ else
666
+ msg = "Execution error:\n"
667
+ msg += e.message
668
+ file, line, meth = e.backtrace[0].scan(BACKTRACE_LINE_REGEXP).flatten
669
+ line_number = line.to_i
670
+ if file && line && (line_number.to_s == line)
671
+ dir = AgentConfig.cookbook_download_dir
672
+ if file[0..dir.size - 1] == dir
673
+ path = "[COOKBOOKS]/" + file[dir.size..file.size]
674
+ else
675
+ path = file
676
+ end
677
+ msg += "\n\nThe error occurred line #{line} of #{path}"
678
+ msg += " in method '#{meth}'" if meth
679
+ context = ""
680
+ if File.readable?(file)
681
+ File.open(file, 'r') do |f|
682
+ lines = f.readlines
683
+ lines_count = lines.size
684
+ if lines_count >= line_number
685
+ upper = [lines_count, line_number + 2].max
686
+ padding = upper.to_s.size
687
+ context += context_line(lines, line_number - 2, padding)
688
+ context += context_line(lines, line_number - 1, padding)
689
+ context += context_line(lines, line_number, padding, '*')
690
+ context += context_line(lines, line_number + 1, padding)
691
+ context += context_line(lines, line_number + 2, padding)
692
+ end
693
+ end
694
+ end
695
+ msg += " while executing:\n\n#{context}" unless context.empty?
696
+ end
697
+ end
698
+ msg
699
+ end
700
+
701
+ # Format a single line for the error context, return empty string
702
+ # if given index is negative or greater than the lines array size
703
+ #
704
+ # === Parameters
705
+ # lines(Array):: Lines of text
706
+ # index(Integer):: Index of line that should be formatted for context
707
+ # padding(Integer):: Number of character to pad line with (includes prefix)
708
+ # prefix(String):: Single character string used to prefix line
709
+ # use line number if not specified
710
+ def context_line(lines, index, padding, prefix=nil)
711
+ return '' if index < 1 || index > lines.size
712
+ margin = prefix ? prefix * index.to_s.size : index.to_s
713
+ "#{margin}#{' ' * ([padding - margin.size, 0].max)} #{lines[index - 1]}"
714
+ end
715
+
716
+ # Retry executing given block given number of times
717
+ # Block should return true when it succeeds
718
+ #
719
+ # === Parameters
720
+ # retry_message(String):: Message to audit before retrying
721
+ # times(Integer):: Number of times block should be retried before giving up
722
+ #
723
+ # === Block
724
+ # Block to be executed
725
+ #
726
+ # === Return
727
+ # success(Boolean):: true if execution was successful, false otherwise.
728
+ def retry_execution(retry_message, times = AgentConfig.max_packages_install_retries)
729
+ count = 0
730
+ success = false
731
+ begin
732
+ count += 1
733
+ success = yield
734
+ @audit.append_info("\n#{retry_message}\n") unless success || count > times
735
+ end while !success && count <= times
736
+ success
737
+ end
738
+
739
+ # Audit startup time and duration of given action
740
+ #
741
+ # === Block
742
+ # Block whose execution should be timed
743
+ #
744
+ # === Return
745
+ # res(Object):: Result returned by given block
746
+ def audit_time
747
+ start_time = Time.now
748
+ @audit.append_info("Starting at #{start_time}")
749
+ res = yield
750
+ @audit.append_info("Duration: #{'%.2f' % (Time.now - start_time)} seconds\n\n")
751
+ res
752
+ end
753
+
754
+ def without_bundler_env
755
+ original_env = ENV.to_hash
756
+ ENV.delete_if {|k,v| k =~ /^GEM_|^BUNDLE_/}
757
+ if ENV.key?('RUBYOPT')
758
+ ENV['RUBYOPT'] = ENV['RUBYOPT'].split(" ").select {|word| word !~ /bundler/}.join(" ")
759
+ end
760
+ yield
761
+ ensure
762
+ ENV.replace(original_env.to_hash)
763
+ end
764
+ end
765
+ end