right_agent 2.0.7-x86-mingw32

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 (176) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +82 -0
  3. data/Rakefile +113 -0
  4. data/lib/right_agent.rb +59 -0
  5. data/lib/right_agent/actor.rb +182 -0
  6. data/lib/right_agent/actor_registry.rb +76 -0
  7. data/lib/right_agent/actors/agent_manager.rb +232 -0
  8. data/lib/right_agent/agent.rb +1149 -0
  9. data/lib/right_agent/agent_config.rb +480 -0
  10. data/lib/right_agent/agent_identity.rb +210 -0
  11. data/lib/right_agent/agent_tag_manager.rb +237 -0
  12. data/lib/right_agent/audit_formatter.rb +107 -0
  13. data/lib/right_agent/clients.rb +31 -0
  14. data/lib/right_agent/clients/api_client.rb +383 -0
  15. data/lib/right_agent/clients/auth_client.rb +247 -0
  16. data/lib/right_agent/clients/balanced_http_client.rb +369 -0
  17. data/lib/right_agent/clients/base_retry_client.rb +495 -0
  18. data/lib/right_agent/clients/right_http_client.rb +279 -0
  19. data/lib/right_agent/clients/router_client.rb +493 -0
  20. data/lib/right_agent/command.rb +30 -0
  21. data/lib/right_agent/command/agent_manager_commands.rb +150 -0
  22. data/lib/right_agent/command/command_client.rb +136 -0
  23. data/lib/right_agent/command/command_constants.rb +33 -0
  24. data/lib/right_agent/command/command_io.rb +126 -0
  25. data/lib/right_agent/command/command_parser.rb +87 -0
  26. data/lib/right_agent/command/command_runner.rb +118 -0
  27. data/lib/right_agent/command/command_serializer.rb +63 -0
  28. data/lib/right_agent/connectivity_checker.rb +179 -0
  29. data/lib/right_agent/console.rb +65 -0
  30. data/lib/right_agent/core_payload_types.rb +44 -0
  31. data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
  32. data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
  33. data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
  34. data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
  35. data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
  36. data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
  37. data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
  38. data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
  39. data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
  40. data/lib/right_agent/core_payload_types/login_user.rb +79 -0
  41. data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
  42. data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
  43. data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
  44. data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
  45. data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
  46. data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
  47. data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
  48. data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
  49. data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
  50. data/lib/right_agent/daemonize.rb +35 -0
  51. data/lib/right_agent/dispatched_cache.rb +109 -0
  52. data/lib/right_agent/dispatcher.rb +272 -0
  53. data/lib/right_agent/enrollment_result.rb +221 -0
  54. data/lib/right_agent/exceptions.rb +87 -0
  55. data/lib/right_agent/history.rb +145 -0
  56. data/lib/right_agent/log.rb +460 -0
  57. data/lib/right_agent/minimal.rb +46 -0
  58. data/lib/right_agent/monkey_patches.rb +30 -0
  59. data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
  60. data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
  61. data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
  62. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
  63. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
  64. data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
  65. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
  66. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
  67. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
  68. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
  69. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
  70. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
  71. data/lib/right_agent/multiplexer.rb +102 -0
  72. data/lib/right_agent/offline_handler.rb +270 -0
  73. data/lib/right_agent/operation_result.rb +300 -0
  74. data/lib/right_agent/packets.rb +673 -0
  75. data/lib/right_agent/payload_formatter.rb +104 -0
  76. data/lib/right_agent/pending_requests.rb +128 -0
  77. data/lib/right_agent/pid_file.rb +159 -0
  78. data/lib/right_agent/platform.rb +770 -0
  79. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  80. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  81. data/lib/right_agent/platform/unix/platform.rb +226 -0
  82. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  83. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  84. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  85. data/lib/right_agent/protocol_version_mixin.rb +69 -0
  86. data/lib/right_agent/retryable_request.rb +195 -0
  87. data/lib/right_agent/scripts/agent_controller.rb +543 -0
  88. data/lib/right_agent/scripts/agent_deployer.rb +400 -0
  89. data/lib/right_agent/scripts/common_parser.rb +160 -0
  90. data/lib/right_agent/scripts/log_level_manager.rb +192 -0
  91. data/lib/right_agent/scripts/stats_manager.rb +268 -0
  92. data/lib/right_agent/scripts/usage.rb +58 -0
  93. data/lib/right_agent/secure_identity.rb +92 -0
  94. data/lib/right_agent/security.rb +32 -0
  95. data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
  96. data/lib/right_agent/security/certificate.rb +102 -0
  97. data/lib/right_agent/security/certificate_cache.rb +89 -0
  98. data/lib/right_agent/security/distinguished_name.rb +56 -0
  99. data/lib/right_agent/security/encrypted_document.rb +83 -0
  100. data/lib/right_agent/security/rsa_key_pair.rb +76 -0
  101. data/lib/right_agent/security/signature.rb +86 -0
  102. data/lib/right_agent/security/static_certificate_store.rb +85 -0
  103. data/lib/right_agent/sender.rb +792 -0
  104. data/lib/right_agent/serialize.rb +29 -0
  105. data/lib/right_agent/serialize/message_pack.rb +107 -0
  106. data/lib/right_agent/serialize/secure_serializer.rb +151 -0
  107. data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
  108. data/lib/right_agent/serialize/serializable.rb +151 -0
  109. data/lib/right_agent/serialize/serializer.rb +159 -0
  110. data/lib/right_agent/subprocess.rb +38 -0
  111. data/lib/right_agent/tracer.rb +124 -0
  112. data/right_agent.gemspec +101 -0
  113. data/spec/actor_registry_spec.rb +80 -0
  114. data/spec/actor_spec.rb +162 -0
  115. data/spec/agent_config_spec.rb +235 -0
  116. data/spec/agent_identity_spec.rb +78 -0
  117. data/spec/agent_spec.rb +734 -0
  118. data/spec/agent_tag_manager_spec.rb +319 -0
  119. data/spec/clients/api_client_spec.rb +423 -0
  120. data/spec/clients/auth_client_spec.rb +272 -0
  121. data/spec/clients/balanced_http_client_spec.rb +576 -0
  122. data/spec/clients/base_retry_client_spec.rb +635 -0
  123. data/spec/clients/router_client_spec.rb +594 -0
  124. data/spec/clients/spec_helper.rb +111 -0
  125. data/spec/command/agent_manager_commands_spec.rb +51 -0
  126. data/spec/command/command_io_spec.rb +93 -0
  127. data/spec/command/command_parser_spec.rb +79 -0
  128. data/spec/command/command_runner_spec.rb +107 -0
  129. data/spec/command/command_serializer_spec.rb +51 -0
  130. data/spec/connectivity_checker_spec.rb +83 -0
  131. data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
  132. data/spec/core_payload_types/dev_repository_spec.rb +33 -0
  133. data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
  134. data/spec/core_payload_types/login_user_spec.rb +102 -0
  135. data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
  136. data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
  137. data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
  138. data/spec/core_payload_types/spec_helper.rb +23 -0
  139. data/spec/dispatched_cache_spec.rb +136 -0
  140. data/spec/dispatcher_spec.rb +324 -0
  141. data/spec/enrollment_result_spec.rb +53 -0
  142. data/spec/history_spec.rb +246 -0
  143. data/spec/log_spec.rb +192 -0
  144. data/spec/monkey_patches/eventmachine_spec.rb +62 -0
  145. data/spec/multiplexer_spec.rb +48 -0
  146. data/spec/offline_handler_spec.rb +340 -0
  147. data/spec/operation_result_spec.rb +208 -0
  148. data/spec/packets_spec.rb +461 -0
  149. data/spec/pending_requests_spec.rb +136 -0
  150. data/spec/platform/spec_helper.rb +216 -0
  151. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  152. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  153. data/spec/platform/unix/spec_helper.rb +149 -0
  154. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  155. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  156. data/spec/platform/windows/spec_helper.rb +720 -0
  157. data/spec/retryable_request_spec.rb +306 -0
  158. data/spec/secure_identity_spec.rb +50 -0
  159. data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
  160. data/spec/security/certificate_cache_spec.rb +71 -0
  161. data/spec/security/certificate_spec.rb +49 -0
  162. data/spec/security/distinguished_name_spec.rb +46 -0
  163. data/spec/security/encrypted_document_spec.rb +55 -0
  164. data/spec/security/rsa_key_pair_spec.rb +55 -0
  165. data/spec/security/signature_spec.rb +66 -0
  166. data/spec/security/static_certificate_store_spec.rb +58 -0
  167. data/spec/sender_spec.rb +1045 -0
  168. data/spec/serialize/message_pack_spec.rb +131 -0
  169. data/spec/serialize/secure_serializer_spec.rb +132 -0
  170. data/spec/serialize/serializable_spec.rb +90 -0
  171. data/spec/serialize/serializer_spec.rb +197 -0
  172. data/spec/spec.opts +2 -0
  173. data/spec/spec.win32.opts +1 -0
  174. data/spec/spec_helper.rb +130 -0
  175. data/spec/tracer_spec.rb +114 -0
  176. metadata +447 -0
@@ -0,0 +1,1808 @@
1
+ #
2
+ # Copyright (c) 2009-2013 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 ::File.expand_path('../../../platform', __FILE__)
24
+
25
+ require 'fileutils'
26
+ require 'tmpdir'
27
+ begin
28
+ # for shell folder definitions such as Dir::COMMON_APPDATA.
29
+ # older versions use win32/api, latest uses ffi.
30
+ require 'win32/dir'
31
+ rescue LoadError
32
+ # ignore unless Windows as a concession to spec testing from non-Windows.
33
+ # any windows-only gems must be fully mocked under test.
34
+ raise if RUBY_PLATFORM =~ /mswin|mingw/
35
+ end
36
+
37
+ module RightScale
38
+
39
+ # Windows specific implementation
40
+ class Platform
41
+
42
+ # exceptions
43
+ class Win32Error < ::RightScale::Exceptions::PlatformError
44
+ attr_reader :error_code
45
+
46
+ # @param [String] context for message
47
+ # @param [Integer] error_code for formatting or nil for last error
48
+ def initialize(context, error_code = nil)
49
+ @error_code = error_code || ::RightScale::Platform.windows_common.GetLastError()
50
+ super(format_error_message(context, @error_code))
51
+ end
52
+
53
+ # Formats using system error message, if any.
54
+ #
55
+ # @param [String] context for message
56
+ # @param [Integer] error_code for formatting
57
+ #
58
+ # @return [String] formatted error message
59
+ def format_error_message(context, error_code)
60
+ error_message = error_message_for(error_code)
61
+ result = []
62
+ result << context.to_s if context && !context.empty?
63
+ result << "Win32 error code = #{error_code}"
64
+ result << error_message if error_message && !error_message.empty?
65
+ result.join("\n")
66
+ end
67
+
68
+ # Queries raw error message for given error code.
69
+ #
70
+ # @param [Integer] error_code for query
71
+ #
72
+ # @return [String] system error message or empty
73
+ def error_message_for(error_code)
74
+ # this API won't respond with required buffer size if buffer is too
75
+ # small; it returns zero and there is no way to tell if there is no
76
+ # valid message or the buffer is too small. use a 4KB buffer to avoid
77
+ # failure due to buffer size.
78
+ buffer = 0.chr * 4096
79
+ flags = ::RightScale::Platform::WindowsCommon::FORMAT_MESSAGE_FROM_SYSTEM |
80
+ ::RightScale::Platform::WindowsCommon::FORMAT_MESSAGE_IGNORE_INSERTS
81
+ length = ::RightScale::Platform.windows_common.FormatMessage(
82
+ flags, nil, error_code, 0, buffer, buffer.size, nil)
83
+ buffer[0, length].strip
84
+ end
85
+ end
86
+
87
+ # helpers
88
+ class PlatformHelperBase
89
+
90
+ # constants
91
+ SIZEOF_DWORD = 4 # double-word
92
+ SIZEOF_QWORD = 8 # quad-word or double-dword
93
+
94
+ private
95
+
96
+ # checks for mistakes in parameter passing even though neither ruby nor
97
+ # Windows APIs are are strict about boolean types.
98
+ def must_be_bool(value)
99
+ value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
100
+ (raise ::ArgumentError, 'not a boolean')
101
+ end
102
+
103
+ # integer values are constrained to DWORDs for most Windows APIs. 64-bit
104
+ # integers are passed as QWORD structures. negative values in native
105
+ # languages have the high bit (0x80000000) set so no valid negative DWORD
106
+ # can have the high bit set.
107
+ def must_be_dword(value)
108
+ (value.is_a?(Integer) && value >= -0x7fffffff && value <= 0xffffffff) ||
109
+ (raise ::ArgumentError, 'not a dword')
110
+ end
111
+
112
+ # strings passed to Windows APIs must always be NUL-terminated because
113
+ # Windows APIs will overrun buffers and fail without explanation.
114
+ def must_be_string(value)
115
+ (value.is_a?(String) && !value.empty? && value[-1].ord == 0) ||
116
+ (raise ::ArgumentError, 'not a NUL-terminated string')
117
+ end
118
+
119
+ def must_be_string_or_nil(value, &callback)
120
+ value.nil? || must_be_string(value, &callback)
121
+ end
122
+
123
+ # input buffers (structures) need only be a valid string buffer. some APIs
124
+ # accept variable-sized structures that contain counts of substructures
125
+ # instead of an overall size.
126
+ def must_be_buffer(value)
127
+ value.is_a?(String) || (raise ::ArgumentError, 'not a string buffer')
128
+ end
129
+
130
+ def must_be_buffer_or_nil(value)
131
+ value.nil? || must_be_buffer(value)
132
+ end
133
+
134
+ # some Windows APIs count buffer size in bytes and the ruby buffer size
135
+ # must agree with byte count.
136
+ def must_be_byte_buffer(value, count)
137
+ must_be_buffer(value)
138
+ must_be_dword(count)
139
+ count == value.bytesize || (raise ::ArgumentError, 'unexpected byte count')
140
+ end
141
+
142
+ def must_be_byte_buffer_or_nil(value, count)
143
+ if value.nil?
144
+ count == 0 || (raise ::ArgumentError, 'nil buffer must have a zero byte count')
145
+ else
146
+ must_be_byte_buffer(value, count)
147
+ end
148
+ end
149
+
150
+ # some Windows APIs count buffer size in characters and the ruby buffer
151
+ # length must agree with character count.
152
+ def must_be_char_buffer(value, count)
153
+ must_be_buffer(value)
154
+ must_be_dword(count)
155
+ count == value.size || (raise ::ArgumentError, 'unexpected character count')
156
+ end
157
+
158
+ def must_be_char_buffer_or_nil(value, count)
159
+ if value.nil?
160
+ count == 0 || (raise ::ArgumentError, 'nil buffer must have a zero character count')
161
+ else
162
+ must_be_char_buffer(value, count)
163
+ end
164
+ end
165
+
166
+ # some Windows APIs accept a struct that contains a leading DWORD with the
167
+ # size in bytes of the entire struct. even stranger, some have both a
168
+ # leading DWORD and an additional argument that both specify struct size.
169
+ def must_be_size_prefixed_buffer(value, count = nil)
170
+ must_be_buffer(value)
171
+ (value.bytesize >= SIZEOF_DWORD) ||
172
+ (raise ::ArgumentError, 'insufficient buffer size')
173
+ (value[0, SIZEOF_DWORD].unpack('L')[0] == value.bytesize) ||
174
+ (raise ::ArgumentError, 'unexpected buffer size')
175
+ count.nil? || count == value.size || (raise ::ArgumentError, 'unexpected buffer size')
176
+ end
177
+
178
+ def must_be_size_prefixed_buffer_or_nil(value, count = nil)
179
+ if value.nil?
180
+ count.nil? || count == 0 || (raise ::ArgumentError, 'nil buffer must have a zero character count')
181
+ else
182
+ must_be_size_prefixed_buffer(value, count)
183
+ end
184
+ end
185
+
186
+ # Many Windows APIs follow a pattern of taking a buffer and buffer size as
187
+ # arguments and returning the required buffer size if the buffer is too
188
+ # small or else the exact length of the data if the buffer was sufficient.
189
+ # This method implements retry loop logic for such APIs. The details of
190
+ # how the API is called is left to the block after the buffer has been
191
+ # created.
192
+ #
193
+ # @param [Integer] initial_buffer_count or zero to query required buffer
194
+ # size (Default = MAX_PATH).
195
+ # @param [Integer] max_tries to raise out of loop in case the API
196
+ # will not settle on a buffer size (Default = 4).
197
+ # @param [Integer] max_buffer_size to raise out of loop in case the API
198
+ # demands an unreasonable buffer size (Default = 64KB).
199
+ #
200
+ # @yield [buffer] yields the buffer at current size for API call
201
+ # @yieldparam [String] buffer to use for API call (get size from buffer)
202
+ # or else nil if the last call returned zero (usually an API error).
203
+ #
204
+ # @return [String] buffered data clipped to exact length
205
+ def with_resizable_buffer(initial_buffer_count = Filesystem::MAX_PATH,
206
+ max_tries = 4,
207
+ max_buffer_size = 0x10000)
208
+ try_count = 0
209
+ buffer_count = initial_buffer_count
210
+ buffer = nil
211
+ loop do
212
+ buffer = 0.chr * buffer_count
213
+ length_or_buffer_count = yield(buffer)
214
+ if 0 == length_or_buffer_count
215
+ yield(nil)
216
+ elsif buffer_count < length_or_buffer_count
217
+ # once or twice is usually reasonable. if the required buffer is
218
+ # strictly non-decreasing and unsatisfiable on each call to the API
219
+ # then something is wrong.
220
+ try_count += 1
221
+ if try_count > max_tries
222
+ raise ::RightScale::Exceptions::PlatformError,
223
+ "Infinite loop detected in API retry after #{max_tries} attempts."
224
+ end
225
+ if length_or_buffer_count > max_buffer_size
226
+ raise ::RightScale::Exceptions::PlatformError,
227
+ "API requested unexpected buffer size = #{length_or_buffer_count}"
228
+ end
229
+ buffer_count = length_or_buffer_count
230
+ else
231
+ buffer = buffer[0, length_or_buffer_count]
232
+ break
233
+ end
234
+ end
235
+ buffer
236
+ end
237
+
238
+ end
239
+
240
+ class Filesystem
241
+
242
+ # constants
243
+
244
+ # Windows-defined maximum path length for legacy Windows APIs that
245
+ # restrict path buffer sizes by default.
246
+ MAX_PATH = 260
247
+
248
+ SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1 # @see CreateSymbolicLink function
249
+
250
+ # this can change because companies get bought and managers insist on
251
+ # using the current company name for some reason (better learn that or
252
+ # else you will be finding and fixing all the hardcoded company names).
253
+ COMPANY_DIR_NAME = 'RightScale'
254
+
255
+ # Overrides base Filesystem#find_executable_in_path
256
+ def find_executable_in_path(command_name)
257
+ # must search all known (executable) path extensions unless the
258
+ # explicit extension was given. this handles a case such as 'curl'
259
+ # which can either be on the path as 'curl.exe' or as a command shell
260
+ # shortcut called 'curl.cmd', etc.
261
+ use_path_extensions = 0 == ::File.extname(command_name).length
262
+ path_extensions = use_path_extensions ? ::ENV['PATHEXT'].split(/;/) : nil
263
+
264
+ # must check the current working directory first just to be completely
265
+ # sure what would happen if the command were executed. note that linux
266
+ # ignores the CWD, so this is platform-specific behavior for windows.
267
+ cwd = ::Dir.getwd
268
+ path = ::ENV['PATH']
269
+ path = (path.nil? || 0 == path.length) ? cwd : (cwd + ';' + path)
270
+ path.split(/;/).each do |dir|
271
+ if use_path_extensions
272
+ path_extensions.each do |path_extension|
273
+ path = pretty_path(::File.join(dir, command_name + path_extension))
274
+ return path if ::File.executable?(path)
275
+ end
276
+ else
277
+ path = pretty_path(::File.join(dir, command_name))
278
+ return path if ::File.executable?(path)
279
+ end
280
+ end
281
+ return nil
282
+ end
283
+
284
+ # Convenience method for pretty common appdata dir and for mocking what is
285
+ # normally a Dir constant during test.
286
+ #
287
+ # @return [String] pretty common application data dir
288
+ def common_app_data_dir
289
+ @common_app_data_dir ||= pretty_path(::Dir::COMMON_APPDATA)
290
+ end
291
+
292
+ # Convenience method for pretty program files (x86) dir and for mocking
293
+ # what is normally a Dir constant during test.
294
+ #
295
+ # @return [String] pretty program files (x86) dir
296
+ def program_files_dir
297
+ @program_files_dir ||= pretty_path(::Dir::PROGRAM_FILES)
298
+ end
299
+
300
+ # Common app data for all products of this company (whose name is not
301
+ # necessarily a constant because companies get bought, you know).
302
+ #
303
+ # @return [String] company common application data dir
304
+ def company_app_data_dir
305
+ @company_app_data_dir ||= ::File.join(common_app_data_dir, COMPANY_DIR_NAME)
306
+ end
307
+
308
+ # Program files base for all products of this company.
309
+ #
310
+ # @return [String] path to installed RightScale directory
311
+ def company_program_files_dir
312
+ @company_program_files_dir ||= ::File.join(program_files_dir, COMPANY_DIR_NAME)
313
+ end
314
+
315
+ # @return [String] system root
316
+ def system_root
317
+ @system_root ||= pretty_path(::ENV['SystemRoot'])
318
+ end
319
+
320
+ # Home directory for user settings and documents or else the temp dir if
321
+ # undefined.
322
+ #
323
+ # @return [String] user home
324
+ def user_home_dir
325
+ @user_home_dir ||= pretty_path(::ENV['USERPROFILE'] || temp_dir)
326
+ end
327
+
328
+ # Overrides base Filesystem#right_agent_cfg_dir
329
+ def right_agent_cfg_dir
330
+ @right_agent_cfg_dir ||= ::File.join(company_app_data_dir, 'right_agent')
331
+ end
332
+
333
+ # Overrides base Filesystem#right_scale_static_state_dir
334
+ def right_scale_static_state_dir
335
+ @right_scale_static_state_dir ||= ::File.join(company_app_data_dir, 'rightscale.d')
336
+ end
337
+
338
+ # Overrides base Filesystem#right_link_static_state_dir
339
+ def right_link_static_state_dir
340
+ @right_link_static_state_dir ||= ::File.join(right_scale_static_state_dir, 'right_link')
341
+ end
342
+
343
+ # Overrides base Filesystem#right_link_dynamic_state_dir
344
+ def right_link_dynamic_state_dir
345
+ @right_link_dynamic_state_dir ||= ::File.join(company_app_data_dir, 'right_link')
346
+ end
347
+
348
+ # Overrides base Filesystem#spool_dir
349
+ def spool_dir
350
+ @spool_dir ||= ::File.join(company_app_data_dir, 'spool')
351
+ end
352
+
353
+ # Overrides base Filesystem#ssh_cfg_dir
354
+ def ssh_cfg_dir
355
+ @ssh_cfg_dir ||= ::File.join(user_home_dir, '.ssh')
356
+ end
357
+
358
+ # Overrides base Filesystem#cache_dir
359
+ def cache_dir
360
+ @cache_dir ||= ::File.join(company_app_data_dir, 'cache')
361
+ end
362
+
363
+ # Overrides base Filesystem#log_dir
364
+ def log_dir
365
+ @log_dir ||= ::File.join(company_app_data_dir, 'log')
366
+ end
367
+
368
+ # Overrides base Filesystem#source_code_dir
369
+ def source_code_dir
370
+ @source_code_dir ||= ::File.join(company_app_data_dir, 'src')
371
+ end
372
+
373
+ # Overrides base Filesystem#temp_dir
374
+ def temp_dir
375
+ # Dir.tmpdir has historically had some odd behavior when running as
376
+ # SYSTEM so our legacy behavior is to prefer the API call. specifically
377
+ # the Dir.tmpdir doesn't necessarily exist but GetTempPath dir always
378
+ # exists. calling Dir.mktmpdir is reliable because it creates tmpdir.
379
+ unless @temp_dir
380
+ # MAX_PATH + (trailing backslash); see API documentation.
381
+ data = with_resizable_buffer(MAX_PATH + 1) do |buffer|
382
+ if buffer
383
+ GetTempPath(buffer.size, buffer)
384
+ else
385
+ raise ::RightScale::Platform::Win32Error, 'Failed to query temp path'
386
+ end
387
+ end
388
+
389
+ # note that temp path is documented as always having a trailing slash
390
+ # but a defensive programmer never trusts that 'always' remark so use
391
+ # a conditional chomp.
392
+ @temp_dir = pretty_path(data.chomp('\\'))
393
+ end
394
+ @temp_dir
395
+ end
396
+
397
+ # Overrides base Filesystem#pid_dir
398
+ def pid_dir
399
+ @pid_dir ||= ::File.join(company_app_data_dir, 'run')
400
+ end
401
+
402
+ # @return [String] installed RightLink directory path
403
+ def right_link_home_dir
404
+ @right_link_home_dir ||=
405
+ ::File.normalize_path(
406
+ pretty_path(
407
+ ::ENV['RS_RIGHT_LINK_HOME'] ||
408
+ ::File.join(company_program_files_dir, 'RightLink')))
409
+ end
410
+
411
+ # Overrides base Filesystem#private_bin_dir
412
+ def private_bin_dir
413
+ @private_bin_dir ||= ::File.join(right_link_home_dir, 'bin')
414
+ end
415
+
416
+ # Overrides base Filesystem#sandbox_dir
417
+ def sandbox_dir
418
+ @sandbox_dir ||= ::File.join(right_link_home_dir, 'sandbox')
419
+ end
420
+
421
+ # Overrides base Filesystem#long_path_to_short_path
422
+ #
423
+ # Converts a long path to a short path. In Windows terms this means taking
424
+ # any file/folder name over 8 characters in length and truncating it to
425
+ # six (6) characters with ~1..~n appended depending on how many similar
426
+ # names exist in the same directory. File extensions are simply chopped
427
+ # at three (3) letters. The short name is equivalent for all API calls to
428
+ # the long path but requires no special quoting, etc. Windows APIs are
429
+ # also subject to the MAX_PATH limitation (due to originally being
430
+ # designed to run on 16-bit DOS) unless special 32KB path extenders (i.e.
431
+ # prepending "\\?\" to input paths) are used. Converting paths from long
432
+ # to short paths makes file APIs alot less likely to fail with a path
433
+ # length error. Note that it is possible to configure an NTFS volume to
434
+ # not support short-paths (i.e. only long paths are kept by the file
435
+ # system) in which case this method will always return the long path (and
436
+ # probably lead to lots of path length errors).
437
+ def long_path_to_short_path(long_path)
438
+ result = nil
439
+ if ::File.exists?(long_path)
440
+ query_path = long_path + 0.chr # ensure nul-terminated
441
+ data = with_resizable_buffer do |buffer|
442
+ if buffer
443
+ GetShortPathName(query_path, buffer, buffer.size)
444
+ else
445
+ raise ::RightScale::Platform::Win32Error,
446
+ "Failed to query short path for #{long_path.inspect}"
447
+ end
448
+ end
449
+ result = pretty_path(data)
450
+ else
451
+ # must get short path for any existing ancestor since child doesn't
452
+ # (currently) exist.
453
+ child_name = File.basename(long_path)
454
+ long_parent_path = File.dirname(long_path)
455
+
456
+ # note that root dirname is root itself (at least in windows)
457
+ if long_path == long_parent_path
458
+ result = long_path
459
+ else
460
+ # recursion
461
+ short_parent_path = long_path_to_short_path(::File.dirname(long_path))
462
+ result = ::File.join(short_parent_path, child_name)
463
+ end
464
+ end
465
+ result
466
+ end
467
+
468
+ # Overrides base Filesystem#pretty_path
469
+ #
470
+ # pretties up paths which assists Dir.glob() and Dir[] calls which will
471
+ # return empty if the path contains any \ characters. windows doesn't
472
+ # care (most of the time) about whether you use \ or / in paths. as
473
+ # always, there are exceptions to this rule (such as "del c:/xyz" which
474
+ # fails while "del c:\xyz" succeeds)
475
+ def pretty_path(path, native_fs_flag = false)
476
+ result = nil
477
+ if native_fs_flag
478
+ result = path.gsub('/', "\\").gsub(/\\+/, "\\")
479
+ else
480
+ result = path.gsub("\\", '/').gsub(/\/+/, '/')
481
+ end
482
+ result
483
+ end
484
+
485
+ # Ensures a local drive location for the file or folder given by path
486
+ # by copying to a local temp directory given by name only if the item
487
+ # does not appear on the home drive. This method is useful because
488
+ # secure applications refuse to run scripts from network locations, etc.
489
+ # Replaces any similar files in temp dir to ensure latest updates.
490
+ #
491
+ # @param [String] path to file or directory to be placed locally
492
+ # @param [String] temp_dir_name as relative path of temp directory to
493
+ # use only if the file or folder is not on a local drive.
494
+ #
495
+ # @return [String] local drive path
496
+ def ensure_local_drive_path(path, temp_dir_name)
497
+ homedrive = ::ENV['HOMEDRIVE']
498
+ if homedrive && homedrive.upcase != path[0,2].upcase
499
+ local_dir = ::File.join(temp_dir, temp_dir_name)
500
+ ::FileUtils.mkdir_p(local_dir)
501
+ local_path = ::File.join(local_dir, ::File.basename(path))
502
+ if ::File.directory?(path)
503
+ ::FileUtils.rm_rf(local_path) if ::File.directory?(local_path)
504
+ ::FileUtils.cp_r(::File.join(path, '.'), local_path)
505
+ else
506
+ ::FileUtils.cp(path, local_path)
507
+ end
508
+ path = local_path
509
+ end
510
+ return path
511
+ end
512
+
513
+ # Overrides base Filesystem#create_symlink
514
+ #
515
+ # Ruby on Windows does not support File.symlink. Windows 2008 Server and
516
+ # newer versions of Windows do support the CreateSymbolicLink API.
517
+ def create_symlink(from_path, to_path)
518
+
519
+ # TEAL FIX this actually requires the SE_CREATE_SYMBOLIC_LINK_NAME
520
+ # privilege to be held, but because the agent always runs elevated we
521
+ # have never had to acquire it.
522
+ flags = ::File.directory?(from_path) ? SYMBOLIC_LINK_FLAG_DIRECTORY : 0
523
+ symlink_file_path = to_path + 0.chr
524
+ target_file_path = from_path + 0.chr
525
+ unless CreateSymbolicLink(symlink_file_path, target_file_path, flags)
526
+ raise ::RightScale::Platform::Win32Error,
527
+ "Failed to create link from #{from_path.inspect} to #{to_path.inspect}"
528
+ end
529
+ 0 # zero to emulate File.symlink
530
+ end
531
+
532
+ # :bool CreateSymbolicLink(:buffer_in, :buffer_in, :dword)
533
+ #
534
+ # CreateSymbolicLink function
535
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa363866%28v=vs.85%29.aspx
536
+ #
537
+ # @param [String] symlink_file_path for symlinking
538
+ # @param [String] target_file_path for symlinking
539
+ # @param [Integer] flags for symlinking
540
+ #
541
+ # @return [TrueClass|FalseClass] true if successful
542
+ def CreateSymbolicLink(symlink_file_path, target_file_path, flags)
543
+ must_be_overridden
544
+ end
545
+
546
+ # :dword GetShortPathName(:buffer_in, :buffer_out, :dword)
547
+ #
548
+ # GetShortPathName function
549
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa364989%28v=vs.85%29.aspx
550
+ #
551
+ # @param [String] long_path for query
552
+ # @param [String] short_path_buffer to be filled
553
+ # @param [String] short_path_buffer_length as limit
554
+ #
555
+ # @return [Integer] zero on failure or length or required buffer size
556
+ def GetShortPathName(long_path, short_path_buffer, short_path_buffer_length)
557
+ must_be_overridden
558
+ end
559
+
560
+ # :dword GetTempPath(:dword, :buffer_out)
561
+ #
562
+ # GetTempPath function
563
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa364992%28v=vs.85%29.aspx
564
+ #
565
+ # @param [Integer] buffer_length as limit in chars
566
+ # @param [String] buffer to be filled
567
+ #
568
+ # @return [Integer] zero on failure or length or required buffer size
569
+ def GetTempPath(buffer_length, buffer)
570
+ must_be_overridden
571
+ end
572
+ end # Filesystem
573
+
574
+ # Provides utilities for managing volumes (disks).
575
+ class VolumeManager
576
+ def initialize
577
+ @assignable_disk_regex = /^[D-Zd-z]:[\/\\]?$/
578
+ @assignable_path_regex = /^[A-Za-z]:[\/\\][\/\\\w\s\d\-_\.~]+$/
579
+ end
580
+
581
+ # Determines if the given path is valid for a Windows volume attachemnt
582
+ # (excluding the reserved A: B: C: drives).
583
+ #
584
+ # @return [TrueClass|FalseClass] true if path is a valid volume root
585
+ def is_attachable_volume_path?(path)
586
+ return (nil != (path =~ @assignable_disk_regex) || nil != (path =~ @assignable_path_regex))
587
+ end
588
+
589
+ # Gets a list of physical or virtual disks in the form:
590
+ # [{:index, :status, :total_size, :free_size, :dynamic, :gpt}*]
591
+ #
592
+ # where
593
+ # :index >= 0
594
+ # :status = 'Online' | 'Offline'
595
+ # :total_size = bytes used by partitions
596
+ # :free_size = bytes not used by partitions
597
+ # :dynamic = true | false
598
+ # :gpt = true | false
599
+ #
600
+ # GPT = GUID partition table
601
+ #
602
+ # @param [Hash] conditions to match or nil or empty (Default = no conditions)
603
+ #
604
+ # @return [Array] array of volume info hashes detailing visible disks
605
+ #
606
+ # @raise [VolumeError] on failure to list disks
607
+ # @raise [ParserError] on failure to parse disks from output
608
+ def disks(conditions = nil)
609
+ script = <<EOF
610
+ rescan
611
+ list disk
612
+ EOF
613
+ output_text = run_diskpart_script(script, 'list disks')
614
+ return parse_disks(output_text, conditions)
615
+ end
616
+
617
+ # Gets a list of currently visible volumes in the form:
618
+ # [{:index, :device, :label, :filesystem, :type, :total_size, :status, :info}*]
619
+ #
620
+ # where
621
+ # :index >= 0
622
+ # :device = "[A-Z]:"
623
+ # :label = up to 11 characters
624
+ # :filesystem = nil | 'NTFS' | <undocumented>
625
+ # :type = 'NTFS' | <undocumented>
626
+ # :total_size = size in bytes
627
+ # :status = 'Healthy' | <undocumented>
628
+ # :info = 'System' | empty | <undocumented>
629
+ #
630
+ # note that a strange aspect of diskpart is that it won't correlate
631
+ # disks to volumes in any list even though partition lists are always
632
+ # in the context of a selected disk.
633
+ #
634
+ # volume order can change as volumes are created/destroyed between
635
+ # diskpart sessions so volume 0 can represent C: in one session and
636
+ # then be represented as volume 1 in the next call to diskpart.
637
+ #
638
+ # volume labels are truncated to 11 characters by diskpart even though
639
+ # NTFS allows up to 32 characters.
640
+ #
641
+ # @param [Hash] conditions to match or nil or empty (Default = no conditions)
642
+ #
643
+ # @return [Array] array of volume info hashes detailing visible volumes
644
+ #
645
+ # @raise [VolumeError] on failure to list volumes
646
+ # @raise [ParserError] on failure to parse volumes from output
647
+ def volumes(conditions = nil)
648
+ script = <<EOF
649
+ rescan
650
+ list volume
651
+ EOF
652
+ output_text = run_diskpart_script(script, 'list volumes')
653
+ return parse_volumes(output_text, conditions)
654
+ end
655
+
656
+ # Gets a list of partitions for the disk given by index in the form:
657
+ # {:index, :type, :size, :offset}
658
+ #
659
+ # where
660
+ # :index >= 0
661
+ # :type = 'OEM' | 'Primary' | <undocumented>
662
+ # :size = size in bytes used by partition on disk
663
+ # :offset = offset of partition in bytes from head of disk
664
+ #
665
+ # @param [Integer] disk index to query
666
+ # @param [Hash] conditions to match or nil or empty (Default = no conditions)
667
+ #
668
+ # @return [Array] list of partitions or empty
669
+ #
670
+ # @raise [VolumeError] on failure to list partitions
671
+ # @raise [ParserError] on failure to parse partitions from output
672
+ def partitions(disk_index, conditions = nil)
673
+ script = <<EOF
674
+ rescan
675
+ select disk #{disk_index}
676
+ list partition
677
+ EOF
678
+ output_text = run_diskpart_script(script, 'list partitions')
679
+ return parse_partitions(output_text, conditions)
680
+ end
681
+
682
+ # Formats a disk given by disk index and the device (e.g. "D:") for the
683
+ # volume on the primary NTFS partition which will be created.
684
+ #
685
+ # @param [Integer] disk_index as zero-based disk index (from disks list, etc.)
686
+ # @param [String] device as disk letter or mount path specified for the volume to create
687
+ #
688
+ # @return [TrueClass] always true
689
+ #
690
+ # @raise [ArgumentError] on invalid parameters
691
+ # @raise [VolumeError] on failure to format
692
+ def format_disk(disk_index, device)
693
+ if device.match(@assignable_path_regex) && os_version.major < 6
694
+ raise ArgumentError.new("Mount path assignment is not supported in this version of windows")
695
+ end
696
+ # note that creating the primary partition automatically creates and
697
+ # selects a new volume, which can be assigned a letter before the
698
+ # partition has actually been formatted.
699
+ raise ArgumentError.new("Invalid index = #{disk_index}") unless disk_index >= 0
700
+ raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
701
+
702
+ # note that Windows 2003 server version of diskpart doesn't support
703
+ # format so that has to be done separately.
704
+ format_command = (os_version.major < 6) ? '' : 'format FS=NTFS quick'
705
+ script = <<EOF
706
+ rescan
707
+ list disk
708
+ select disk #{disk_index}
709
+ #{get_clear_readonly_command('disk')}
710
+ #{get_online_disk_command}
711
+ clean
712
+ create partition primary
713
+ #{get_assign_command_for_device(device)}
714
+ #{format_command}
715
+ EOF
716
+ run_diskpart_script(script, 'format disk')
717
+
718
+ # must format using command shell's FORMAT command before 2008 server.
719
+ if os_version.major < 6
720
+ command = "echo Y | format #{device[0,1]}: /Q /V: /FS:NTFS"
721
+ begin
722
+ execute(command)
723
+ rescue ::RightScale::Platform::CommandError => e
724
+ raise VolumeError,
725
+ "Failed to format disk #{disk_index} for device #{device}: #{e.message}\n#{e.output_text}"
726
+ end
727
+ end
728
+ true
729
+ end
730
+
731
+ # Brings the disk given by index online and clears the readonly
732
+ # attribute, if necessary. The latter is required for some kinds of
733
+ # disks to online successfully and SAN volumes may be readonly when
734
+ # initially attached. As this change may bring additional volumes online
735
+ # the updated volumes list is returned.
736
+ #
737
+ # @param [Integer] disk_index as zero-based disk index
738
+ # @param [Hash] options for online
739
+ # @option options [TrueClass|FalseClass] :idempotent true to check the
740
+ # current disk statuses before attempting to online the disk and bails
741
+ # out when disk is already online (Default = false)
742
+ #
743
+ # @return [TrueClass] always true
744
+ #
745
+ # @raise [ArgumentError] on invalid parameters
746
+ # @raise [VolumeError] on failure to online disk
747
+ # @raise [ParserError] on failure to parse disk list
748
+ def online_disk(disk_index, options = {})
749
+ raise ArgumentError.new("Invalid disk_index = #{disk_index}") unless disk_index >= 0
750
+ # Set some defaults for backward compatibility, allow user specified options to override defaults
751
+ options = { :idempotent => false }.merge(options)
752
+ script = <<EOF
753
+ rescan
754
+ list disk
755
+ select disk #{disk_index}
756
+ #{get_clear_readonly_command('disk')}
757
+ #{get_online_disk_command}
758
+ EOF
759
+
760
+ if options[:idempotent]
761
+ disk = disks(:index => disk_index).first
762
+ return true if disk && disk[:status] == 'Online'
763
+ end
764
+
765
+ run_diskpart_script(script, 'online disk')
766
+ true
767
+ end
768
+
769
+ # Brings the disk given by index offline
770
+ #
771
+ # @param [Integer] disk_index as zero-based disk index
772
+ #
773
+ # @return [TrueClass] always true
774
+ #
775
+ # @raise [ArgumentError] on invalid parameters
776
+ # @raise [VolumeError] on failure to offline disk
777
+ # @raise [ParserError] on failure to parse disk list
778
+ # @raise [RightScale::Exceptions::PlatformError] if offline unsupported
779
+ def offline_disk(disk_index)
780
+ raise ArgumentError.new("Invalid disk_index = #{disk_index}") unless disk_index >= 0
781
+ if os_version.major < 6
782
+ raise ::RightScale::Exceptions::PlatformError,
783
+ 'Offline disk is not supported by this platform'
784
+ end
785
+
786
+ # Set some defaults for backward compatibility, allow user specified options to override defaults
787
+ script = <<EOF
788
+ rescan
789
+ list disk
790
+ select disk #{disk_index}
791
+ offline disk noerr
792
+ EOF
793
+
794
+ run_diskpart_script(script, 'offline disk')
795
+ true
796
+ end
797
+
798
+ # Assigns the given device name to the volume given by index and clears
799
+ # the readonly attribute, if necessary. The device must not currently be
800
+ # in use.
801
+ #
802
+ # @param [Integer|String] volume_device_or_index as old device or
803
+ # zero-based volume index (from volumes list, etc.) to select for
804
+ # assignment.
805
+ # @param [String] device as disk letter or mount path specified for the
806
+ # volume to create
807
+ # @param [Hash] options for assignment
808
+ # @option options [TrueClass|FalseClass] :clear_readonly if true will
809
+ # clear the volume readonly flag if set (Default = true)
810
+ # @option options [TrueClass|FalseClass] :remove_all if true will remove
811
+ # all previously assigned devices and paths, essentially a big RESET
812
+ # button for volume assignment (Default = false)
813
+ # @option options [TrueClass|FalseClass] :idempotent if true will checks
814
+ # the current device assignments before assigning the device according
815
+ # to the specified parameters and bails out if already assigned
816
+ # (Default = false)
817
+ #
818
+ # @return [TrueClass] always true
819
+ #
820
+ # @raise [ArgumentError] on invalid parameters
821
+ # @raise [VolumeError] on failure to assign device name
822
+ # @raise [ParserError] on failure to parse volume list
823
+ def assign_device(volume_device_or_index, device, options = {})
824
+ # Set some defaults for backward compatibility, allow user specified options to override defaults
825
+ options = {
826
+ :clear_readonly => true,
827
+ :remove_all => false,
828
+ :idempotent => false
829
+ }.merge(options)
830
+ if device.match(@assignable_path_regex) && os_version.major < 6
831
+ raise ArgumentError.new('Mount path assignment is not supported in this version of windows')
832
+ end
833
+ # Volume selector for drive letter assignments
834
+ volume_selector_match = volume_device_or_index.to_s.match(/^([D-Zd-z]|\d+):?$/)
835
+ # Volume selector for mount path assignments
836
+ volume_selector_match = volume_device_or_index.to_s.match(@assignable_path_regex) unless volume_selector_match
837
+ raise ArgumentError.new("Invalid volume_device_or_index = #{volume_device_or_index}") unless volume_selector_match
838
+ volume_selector = volume_selector_match[1]
839
+ raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
840
+ if options[:idempotent]
841
+ # Device already assigned?
842
+ already_assigned = volumes.any? do |volume|
843
+ volume[:device] == device &&
844
+ (volume[:index] == volume_device_or_index.to_s ||
845
+ volume[:device] == volume_device_or_index.to_s)
846
+ end
847
+ return true if already_assigned
848
+ end
849
+ # Validation ends here, and real stuff starts to happen
850
+
851
+ script = <<EOF
852
+ rescan
853
+ list volume
854
+ select volume "#{volume_selector}"
855
+ #{get_clear_readonly_command('volume') if options[:clear_readonly]}
856
+ #{'remove all noerr' if options[:remove_all]}
857
+ #{get_assign_command_for_device(device)}
858
+ EOF
859
+ run_diskpart_script(script, 'assign device')
860
+ true
861
+ end
862
+
863
+ private
864
+
865
+ # Parses raw output from diskpart looking for the (first) disk list.
866
+ #
867
+ # Example of raw output from diskpart (column width is dictated by the
868
+ # header and some columns can be empty):
869
+ #
870
+ # Disk ### Status Size Free Dyn Gpt
871
+ # -------- ---------- ------- ------- --- ---
872
+ # Disk 0 Online 80 GB 0 B
873
+ #* Disk 1 Offline 4096 MB 4096 MB
874
+ # Disk 2 Online 4096 MB 4096 MB *
875
+ #
876
+ # @param [String] output_text raw output from diskpart
877
+ # @param [Hash] conditions as hash of conditions to match or empty or nil (Default = no conditions)
878
+ #
879
+ # @return [Array] disks or empty
880
+ #
881
+ # @raise [ParserError] on failure to parse disk list
882
+ def parse_disks(output_text, conditions = nil)
883
+ result = []
884
+ line_regex = nil
885
+ header_regex = / -------- (-+) ------- ------- --- ---/
886
+ header_match = nil
887
+ output_text.lines.each do |line|
888
+ line = line.chomp
889
+ if line_regex
890
+ if line.strip.empty?
891
+ break
892
+ end
893
+ match_data = line.match(line_regex)
894
+ raise ParserError.new("Failed to parse disk info from #{line.inspect} using #{line_regex.inspect}") unless match_data
895
+ data = {:index => match_data[1].to_i,
896
+ :status => match_data[2].strip,
897
+ :total_size => size_factor_to_bytes(match_data[3], match_data[4]),
898
+ :free_size => size_factor_to_bytes(match_data[5], match_data[6]),
899
+ :dynamic => match_data[7].strip[0,1] == '*',
900
+ :gpt => match_data[8].strip[0,1] == '*'}
901
+ if conditions
902
+ matched = true
903
+ conditions.each do |key, value|
904
+ unless data[key] == value
905
+ matched = false
906
+ break
907
+ end
908
+ end
909
+ result << data if matched
910
+ else
911
+ result << data
912
+ end
913
+ elsif header_match = line.match(header_regex)
914
+ # account for some fields being variable width between versions of the OS.
915
+ status_width = header_match[1].length
916
+ line_regex_text = "^[\\* ] Disk (\\d[\\d ]\{2\}) (.\{#{status_width}\}) "\
917
+ "[ ]?([\\d ]\{3\}\\d) (.?B) [ ]?([\\d ]\{3\}\\d) (.?B) ([\\* ]) ([\\* ])"
918
+ line_regex = Regexp.compile(line_regex_text)
919
+ else
920
+ # one or more lines of ignored headers
921
+ end
922
+ end
923
+ raise ParserError.new("Failed to parse disk list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
924
+ return result
925
+ end
926
+
927
+ # Parses raw output from diskpart looking for the (first) volume list.
928
+ #
929
+ # Example of raw output from diskpart (column width is dictated by the
930
+ # header and some columns can be empty):
931
+ #
932
+ # Volume ### Ltr Label Fs Type Size Status Info
933
+ # ---------- --- ----------- ----- ---------- ------- --------- --------
934
+ # Volume 0 C 2008Boot NTFS Partition 80 GB Healthy System
935
+ #* Volume 1 D NTFS Partition 4094 MB Healthy
936
+ # Volume 2 NTFS Partition 4094 MB Healthy
937
+ #
938
+ # @param [String] output_text as raw output from diskpart
939
+ # @param [Hash] conditions as hash of conditions to match or empty or nil (Default = no conditions)
940
+ #
941
+ # @return [Array] volumes or empty. Drive letters are appended with ':'
942
+ # even though they aren't returned that way from diskpart
943
+ #
944
+ # @raise [ParserError] on failure to parse volume list
945
+ def parse_volumes(output_text, conditions = nil)
946
+ result = []
947
+ header_regex = / ---------- --- (-+) (-+) (-+) ------- (-+) (-+)/
948
+ header_match = nil
949
+ line_regex = nil
950
+ output_text.lines.each do |line|
951
+ line = line.chomp
952
+ if line_regex
953
+ if line.strip.empty?
954
+ break
955
+ end
956
+ match_data = line.match(line_regex)
957
+ unless match_data
958
+ path_match_regex = /([A-Za-z]:[\/\\][\/\\\w\s\d]+)/
959
+ match_data = line.match(path_match_regex)
960
+ if match_data
961
+ result.last[:device] = match_data[1]
962
+ next
963
+ end
964
+ end
965
+ raise ParserError.new("Failed to parse volume info from #{line.inspect} using #{line_regex.inspect}") unless match_data
966
+ letter = nil_if_empty(match_data[2])
967
+ device = "#{letter.upcase}:" if letter
968
+ data = {:index => match_data[1].to_i,
969
+ :device => device,
970
+ :label => nil_if_empty(match_data[3]),
971
+ :filesystem => nil_if_empty(match_data[4]),
972
+ :type => nil_if_empty(match_data[5]),
973
+ :total_size => size_factor_to_bytes(match_data[6], match_data[7]),
974
+ :status => nil_if_empty(match_data[8]),
975
+ :info => nil_if_empty(match_data[9])}
976
+ if conditions
977
+ matched = true
978
+ conditions.each do |key, value|
979
+ unless data[key] == value
980
+ matched = false
981
+ break
982
+ end
983
+ end
984
+ result << data if matched
985
+ else
986
+ result << data
987
+ end
988
+ elsif header_match = line.match(header_regex)
989
+ # account for some fields being variable width between versions of the OS.
990
+ label_width = header_match[1].length
991
+ filesystem_width = header_match[2].length
992
+ type_width = header_match[3].length
993
+ status_width = header_match[4].length
994
+ info_width = header_match[5].length
995
+ line_regex_text = "^[\\* ] Volume (\\d[\\d ]\{2\}) ([A-Za-z ]) "\
996
+ "(.\{#{label_width}\}) (.\{#{filesystem_width}\}) "\
997
+ "(.\{#{type_width}\}) [ ]?([\\d ]\{3\}\\d) (.?B)\\s{0,2}"\
998
+ "(.\{#{status_width}\})\\s{0,2}(.\{0,#{info_width}\})"
999
+ line_regex = Regexp.compile(line_regex_text)
1000
+ else
1001
+ # one or more lines of ignored headers
1002
+ end
1003
+ end
1004
+ raise ParserError.new("Failed to parse volume list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
1005
+ return result
1006
+ end
1007
+
1008
+ # Parses raw output from diskpart looking for the (first) partition list.
1009
+ #
1010
+ # Example of raw output from diskpart (column width is dictated by the
1011
+ # header and some columns can be empty):
1012
+ #
1013
+ # Partition ### Type Size Offset
1014
+ # ------------- ---------------- ------- -------
1015
+ # Partition 1 OEM 39 MB 31 KB
1016
+ #* Partition 2 Primary 14 GB 40 MB
1017
+ # Partition 3 Primary 451 GB 14 GB
1018
+ #
1019
+ # @param [String] output_text as raw output from diskpart
1020
+ # @param [Hash] conditions as hash of conditions to match or empty or nil (Default = no conditions)
1021
+ #
1022
+ # @return [Array] volumes or empty
1023
+ #
1024
+ # @raise [ParserError] on failure to parse partition list
1025
+ def parse_partitions(output_text, conditions = nil)
1026
+ result = []
1027
+ header_regex = / ------------- (-+) ------- -------/
1028
+ header_match = nil
1029
+ line_regex = nil
1030
+ output_text.lines.each do |line|
1031
+ line = line.chomp
1032
+ if line_regex
1033
+ if line.strip.empty?
1034
+ break
1035
+ end
1036
+ match_data = line.match(line_regex)
1037
+ raise ParserError.new("Failed to parse partition info from #{line.inspect} using #{line_regex.inspect}") unless match_data
1038
+ data = {:index => match_data[1].to_i,
1039
+ :type => nil_if_empty(match_data[2]),
1040
+ :size => size_factor_to_bytes(match_data[3], match_data[4]),
1041
+ :offset => size_factor_to_bytes(match_data[5], match_data[6])}
1042
+ if conditions
1043
+ matched = true
1044
+ conditions.each do |key, value|
1045
+ unless data[key] == value
1046
+ matched = false
1047
+ break
1048
+ end
1049
+ end
1050
+ result << data if matched
1051
+ else
1052
+ result << data
1053
+ end
1054
+ elsif header_match = line.match(header_regex)
1055
+ # account for some fields being variable width between versions of the OS.
1056
+ type_width = header_match[1].length
1057
+ line_regex_text = "^[\\* ] Partition (\\d[\\d ]\{2\}) (.\{#{type_width}\}) "\
1058
+ "[ ]?([\\d ]\{3\}\\d) (.?B) [ ]?([\\d ]\{3\}\\d) (.?B)"
1059
+ line_regex = Regexp.compile(line_regex_text)
1060
+ elsif line.start_with?("There are no partitions on this disk")
1061
+ return []
1062
+ else
1063
+ # one or more lines of ignored headers
1064
+ end
1065
+ end
1066
+ raise ParserError.new("Failed to parse volume list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
1067
+ return result
1068
+ end
1069
+
1070
+ # Determines if the given value is empty and returns nil in that case.
1071
+ #
1072
+ # @param [String] value to check
1073
+ #
1074
+ # @return [String] trimmed value or nil
1075
+ def nil_if_empty(value)
1076
+ value = value.strip
1077
+ return nil if value.empty?
1078
+ return value
1079
+ end
1080
+
1081
+ # Multiplies a raw size value by a size factor given as a standardized
1082
+ # bytes acronym.
1083
+ #
1084
+ # @param [String|Number] size_by value to multiply
1085
+ # @param [String] size_factor multiplier acronym
1086
+ #
1087
+ # @return [Integer] bytes
1088
+ def size_factor_to_bytes(size_by, size_factor)
1089
+ value = size_by.to_i
1090
+ case size_factor
1091
+ when 'KB' then return value * 1024
1092
+ when 'MB' then return value * 1024 * 1024
1093
+ when 'GB' then return value * 1024 * 1024 * 1024
1094
+ when 'TB' then return value * 1024 * 1024 * 1024 * 1024
1095
+ else return value # assume bytes
1096
+ end
1097
+ end
1098
+
1099
+ # Returns the correct diskpart assignment command for the specified device (either drive letter, or path)
1100
+ #
1101
+ # @param [String] device as either a drive letter or mount path
1102
+ #
1103
+ # @return [String] the correct diskpart assignment command
1104
+ def get_assign_command_for_device(device)
1105
+ if device.match(@assignable_disk_regex)
1106
+ "assign letter=#{device[0,1]}"
1107
+ elsif device.match(@assignable_path_regex)
1108
+ "assign mount=\"#{device}\""
1109
+ end
1110
+ end
1111
+
1112
+ # Returns the correct 'online disk' diskpart command based on the OS version
1113
+ #
1114
+ # @return [String] either "online noerr" or "online disk noerr" depending upon the OS version
1115
+ def get_online_disk_command
1116
+ (os_version.major < 6) ? 'online noerr' : 'online disk noerr'
1117
+ end
1118
+
1119
+ # Returns the correct 'attribute disk clear readonly' diskpart command based on the OS version
1120
+ #
1121
+ # @param [String] object_type as one of "disk" or "volume" to clear read only for
1122
+ #
1123
+ # @return [String] either a blank string or "attribute #{object_type} clear readonly noerr" depending upon the OS version
1124
+ def get_clear_readonly_command(object_type)
1125
+ (os_version.major < 6) ? '' : "attribute #{object_type} clear readonly noerr"
1126
+ end
1127
+
1128
+ private
1129
+
1130
+ # Run a diskpart script and get the exit code and text output. See also
1131
+ # technet and search for "DiskPart Command-Line Options" or else
1132
+ # "http://technet.microsoft.com/en-us/library/cc766465%28WS.10%29.aspx".
1133
+ # Note that there are differences between 2003 and 2008 server versions
1134
+ # of this utility.
1135
+ #
1136
+ # @param [String] script with commands delimited by newlines
1137
+ # @param [String] context for exception on failure
1138
+ #
1139
+ # @return [String] output from diskpart
1140
+ #
1141
+ # @raise [VolumeError] on diskpart script failure
1142
+ def run_diskpart_script(script_text, context)
1143
+ ::Dir.mktmpdir do |temp_dir_path|
1144
+ script_file_path = File.join(temp_dir_path, 'rs_diskpart_script.txt')
1145
+ ::File.open(script_file_path, 'w') { |f| f.puts(script_text.strip) }
1146
+ executable_path = 'diskpart.exe'
1147
+ executable_arguments = ['/s', ::File.normalize_path(script_file_path)]
1148
+ shell = ::RightScale::Platform.shell
1149
+ executable_path, executable_arguments = shell.format_right_run_path(executable_path, executable_arguments)
1150
+ command = shell.format_executable_command(executable_path, executable_arguments)
1151
+ execute(command)
1152
+ end
1153
+ rescue ::RightScale::Platform::CommandError => e
1154
+ raise VolumeError,
1155
+ "Failed to #{context}: #{e.message}\nScript =\n#{script_text}\nOutput = #{e.output_text}"
1156
+ end
1157
+
1158
+ # Caches result from os info query. Convenient for mocking under test.
1159
+ def os_version
1160
+ @os_version ||= ::RightScale::Platform.windows_system_information.version
1161
+ end
1162
+
1163
+ end # VolumeManager
1164
+
1165
+ # Provides utilities for formatting executable shell commands, etc.
1166
+ class Shell
1167
+ POWERSHELL_V1x0_EXECUTABLE_PATH = 'powershell.exe'
1168
+ POWERSHELL_V1x0_SCRIPT_EXTENSION = '.ps1'
1169
+ RUBY_SCRIPT_EXTENSION = '.rb'
1170
+
1171
+ # defined for backward compatibility; use Shell#null_output_name
1172
+ NULL_OUTPUT_NAME = 'NUL'
1173
+
1174
+ # Overrides base Shell#null_output_name
1175
+ def null_output_name
1176
+ NULL_OUTPUT_NAME
1177
+ end
1178
+
1179
+ # Overrides base Shell#format_script_file_name
1180
+ def format_script_file_name(partial_script_file_path, default_extension = nil)
1181
+ default_extension ||= POWERSHELL_V1x0_SCRIPT_EXTENSION
1182
+ extension = ::File.extname(partial_script_file_path)
1183
+ if 0 == extension.length
1184
+ return partial_script_file_path + default_extension
1185
+ end
1186
+
1187
+ # quick out for default extension.
1188
+ if 0 == (extension <=> default_extension)
1189
+ return partial_script_file_path
1190
+ end
1191
+
1192
+ # confirm that the "extension" is really something understood by
1193
+ # the command shell as being executable.
1194
+ if executable_extensions.include?(extension.downcase)
1195
+ return partial_script_file_path
1196
+ end
1197
+
1198
+ # not executable; use default extension.
1199
+ return partial_script_file_path + default_extension
1200
+ end
1201
+
1202
+ # Overrides base Shell#format_shell_command
1203
+ def format_shell_command(shell_script_file_path, *arguments)
1204
+ # special case for powershell scripts and ruby scripts (because we
1205
+ # don't necessarily setup the association for .rb with our sandbox
1206
+ # ruby in the environment).
1207
+ extension = File.extname(shell_script_file_path)
1208
+ unless extension.to_s.empty?
1209
+ if 0 == POWERSHELL_V1x0_SCRIPT_EXTENSION.casecmp(extension)
1210
+ return format_powershell_command(shell_script_file_path, *arguments)
1211
+ elsif 0 == RUBY_SCRIPT_EXTENSION.casecmp(extension)
1212
+ return format_ruby_command(shell_script_file_path, *arguments)
1213
+ end
1214
+ end
1215
+
1216
+ # execution is based on script extension (.bat, .cmd, .js, .vbs, etc.)
1217
+ return format_executable_command(shell_script_file_path, *arguments)
1218
+ end
1219
+
1220
+ # Overrides base Shell#sandbox_ruby
1221
+ def sandbox_ruby
1222
+ unless @sandbox_ruby
1223
+ filesystem = ::RightScale::Platform.filesystem
1224
+ @sandbox_ruby =
1225
+ ::File.normalize_path(
1226
+ filesystem.pretty_path(
1227
+ ::ENV['RS_RUBY_EXE'] ||
1228
+ ::File.join(filesystem.sandbox_dir, 'ruby', 'bin', 'ruby.exe')))
1229
+ end
1230
+ end
1231
+
1232
+ # Overrides base Shell#uptime
1233
+ def uptime
1234
+ (::Time.now.to_i.to_f - booted_at.to_f) rescue 0.0
1235
+ end
1236
+
1237
+ # Overrides base Shell#booted_at
1238
+ def booted_at
1239
+ begin
1240
+ # the tmpdir is for Windows Server 2003 behavior where a turd file was
1241
+ # created in the working directory when wmic was invoked.
1242
+ wmic_output = nil
1243
+ ::Dir.mktmpdir do |temp_dir_path|
1244
+ ::Dir.chdir(temp_dir_path) do
1245
+ wmic_output = execute('echo | wmic OS Get LastBootUpTime 2>&1')
1246
+ end
1247
+ end
1248
+ match = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.\d{6}([+-]\d{3})/.match(wmic_output)
1249
+
1250
+ year, mon, day, hour, min, sec, tz = match[1..-1]
1251
+
1252
+ # convert timezone from [+-]mmm to [+-]hh:mm
1253
+ tz = "#{tz[0...1]}#{(tz.to_i.abs / 60).to_s.rjust(2,'0')}:#{(tz.to_i.abs % 60).to_s.rjust(2,'0')}"
1254
+
1255
+ # finally, parse the WMIC output as an XML-schema time, which is the
1256
+ # only reliable way to parse a time with arbitrary zone in Ruby (?!)
1257
+ return Time.xmlschema("#{year}-#{mon}-#{day}T#{hour}:#{min}:#{sec}#{tz}").to_i
1258
+ rescue ::RightScale::Platform::CommandError
1259
+ return nil
1260
+ end
1261
+ end
1262
+
1263
+ # Formats an executable path and arguments by inserting a reference to
1264
+ # RightRun.exe only when necessary.
1265
+ #
1266
+ # @param [String] executable_file_path for formatting
1267
+ # @param [Array] arguments for command or empty
1268
+ #
1269
+ # @return [Array] tuple for updated [executable_path, executable_arguments]
1270
+ def format_right_run_path(executable_file_path, executable_arguments)
1271
+ unless right_run_path.empty?
1272
+ executable_arguments.unshift(executable_file_path)
1273
+ executable_file_path = right_run_path
1274
+ end
1275
+
1276
+ return executable_file_path, executable_arguments
1277
+ end
1278
+
1279
+
1280
+ # Formats an executable command by quoting any of the arguments as
1281
+ # needed and building an executable command string.
1282
+ #
1283
+ # @param [String] executable_file_path for formatting
1284
+ # @param [Array] arguments for command or empty
1285
+ #
1286
+ # @return [String] executable command string
1287
+ def format_executable_command(executable_file_path, *arguments)
1288
+ escaped = []
1289
+ [executable_file_path, arguments].flatten.each do |arg|
1290
+ value = arg.to_s
1291
+ escaped << (value.index(' ') ? "\"#{value}\"" : value)
1292
+ end
1293
+
1294
+ # let cmd do the extension resolution if no extension was given
1295
+ ext = File.extname(executable_file_path)
1296
+ if ext.nil? || ext.empty?
1297
+ "cmd.exe /C \"#{escaped.join(" ")}\""
1298
+ else
1299
+ escaped.join(' ')
1300
+ end
1301
+ end
1302
+
1303
+ # Formats a powershell command using the given script path and arguments.
1304
+ # Allows for specifying powershell from a specific installed location.
1305
+ # This method is only implemented for Windows.
1306
+ #
1307
+ # @param [String] shell_script_file_path for formatting
1308
+ # @param [Array] arguments for command or empty
1309
+ #
1310
+ # @return [String] executable command string
1311
+ def format_powershell_command(shell_script_file_path, *arguments)
1312
+ return format_powershell_command4(
1313
+ POWERSHELL_V1x0_EXECUTABLE_PATH,
1314
+ lines_before_script = nil,
1315
+ lines_after_script = nil,
1316
+ shell_script_file_path,
1317
+ *arguments)
1318
+ end
1319
+
1320
+ # Formats a powershell command using the given script path and arguments.
1321
+ # Allows for specifying powershell from a specific installed location.
1322
+ # This method is only implemented for Windows.
1323
+ #
1324
+ # @param [String] powershell_exe_path for formatting
1325
+ # @param [String] shell_script_file_path for formatting
1326
+ # @param [Array] arguments for command or empty
1327
+ #
1328
+ # @return [String] executable command string
1329
+ def format_powershell_command4(powershell_exe_path,
1330
+ lines_before_script,
1331
+ lines_after_script,
1332
+ shell_script_file_path,
1333
+ *arguments)
1334
+ # special case for powershell scripts.
1335
+ escaped = []
1336
+ [shell_script_file_path, arguments].flatten.each do |arg|
1337
+ value = arg.to_s
1338
+ # note that literal ampersand must be quoted on the powershell command
1339
+ # line because it otherwise means 'execute what follows'.
1340
+ escaped << ((value.index(' ') || value.index('&')) ? "'#{value.gsub("'", "''")}'" : value)
1341
+ end
1342
+
1343
+ # resolve lines before & after script.
1344
+ defaulted_lines_after_script = lines_after_script.nil?
1345
+ lines_before_script ||= []
1346
+ lines_after_script ||= []
1347
+
1348
+ # execute powershell with RemoteSigned execution policy. the issue
1349
+ # is that powershell by default will only run digitally-signed
1350
+ # scripts.
1351
+ # FIX: search for any attempt to alter execution policy in lines
1352
+ # before insertion.
1353
+ # FIX: support digitally signed scripts and/or signing on the fly by
1354
+ # checking for a signature file side-by-side with script.
1355
+ lines_before_script.insert(0, 'set-executionpolicy -executionPolicy RemoteSigned -Scope Process')
1356
+
1357
+ # insert error checking only in case of defaulted "lines after script"
1358
+ # to be backward compatible with existing scripts.
1359
+ if defaulted_lines_after_script
1360
+ # ensure for a generic powershell script that any errors left in the
1361
+ # global $Error list are noted and result in script failure. the
1362
+ # best practice is for the script to handle errors itself (and clear
1363
+ # the $Error list if necessary), so this is a catch-all for any
1364
+ # script which does not handle errors "properly".
1365
+ lines_after_script << 'if ($NULL -eq $LastExitCode) { $LastExitCode = 0 }'
1366
+ lines_after_script << "if ((0 -eq $LastExitCode) -and ($Error.Count -gt 0)) { $RS_message = 'Script exited successfully but $Error contained '+($Error.Count)+' error(s):'; write-output $RS_message; write-output $Error; $LastExitCode = 1 }"
1367
+ end
1368
+
1369
+ # ensure last exit code gets marshalled.
1370
+ marshall_last_exit_code_cmd = 'exit $LastExitCode'
1371
+ if defaulted_lines_after_script || (lines_after_script.last != marshall_last_exit_code_cmd)
1372
+ lines_after_script << marshall_last_exit_code_cmd
1373
+ end
1374
+
1375
+ # format powershell command string.
1376
+ powershell_command = "&{#{lines_before_script.join('; ')}; &#{escaped.join(' ')}; #{lines_after_script.join('; ')}}"
1377
+
1378
+ # in order to run 64-bit powershell from this 32-bit ruby process, we need to launch it using
1379
+ # our special RightRun utility from program files, if it is installed (it is not installed for
1380
+ # 32-bit instances and perhaps not for test/dev environments).
1381
+ executable_path = powershell_exe_path
1382
+ executable_arguments = ['-command', powershell_command]
1383
+ executable_path, executable_arguments = format_right_run_path(executable_path, executable_arguments)
1384
+
1385
+ # combine command string with powershell executable and arguments.
1386
+ return format_executable_command(executable_path, executable_arguments)
1387
+ end
1388
+
1389
+ # @return [Array] list of dot-prefixed executable file extensions from PATHEXT
1390
+ def executable_extensions
1391
+ @executable_extensions ||= ::ENV['PATHEXT'].downcase.split(';')
1392
+ end
1393
+
1394
+ # @return [String] path to RightRun.exe or empty in cases where it is unneeded
1395
+ def right_run_path
1396
+ unless @right_run_path
1397
+ @right_run_path = ''
1398
+ if ::ENV['ProgramW6432'] && (@right_run_path = ::ENV['RS_RIGHT_RUN_EXE'].to_s).empty?
1399
+ temp_path = ::File.join(
1400
+ ::ENV['ProgramW6432'],
1401
+ ::RightScale::Platform::Filesystem::COMPANY_DIR_NAME,
1402
+ 'Shared',
1403
+ 'RightRun.exe')
1404
+ if ::File.file?(temp_path)
1405
+ @right_run_path = ::File.normalize_path(temp_path).gsub('/', "\\")
1406
+ end
1407
+ end
1408
+ end
1409
+ @right_run_path
1410
+ end
1411
+ end # Shell
1412
+
1413
+ class Controller
1414
+
1415
+ TOKEN_ADJUST_PRIVILEGES = 0x0020
1416
+
1417
+ TOKEN_QUERY = 0x0008
1418
+
1419
+ SE_PRIVILEGE_ENABLED = 0x00000002
1420
+
1421
+ # Overrides base Controller#reboot
1422
+ def reboot
1423
+ initiate_system_shutdown(true)
1424
+ end
1425
+
1426
+ # Overrides base Controller#shutdown
1427
+ def shutdown
1428
+ initiate_system_shutdown(false)
1429
+ end
1430
+
1431
+ def initiate_system_shutdown(reboot_after_shutdown)
1432
+ # APIs
1433
+ process = ::RightScale::Platform.process
1434
+ windows_common = ::RightScale::Platform.windows_common
1435
+ windows_security = ::RightScale::Platform.windows_security
1436
+
1437
+ # get current process token.
1438
+ token_handle = 0.chr * 4
1439
+ unless process.OpenProcessToken(
1440
+ process_handle = process.GetCurrentProcess(),
1441
+ desired_access = TOKEN_ADJUST_PRIVILEGES + TOKEN_QUERY,
1442
+ token_handle)
1443
+ raise ::RightScale::Platform::Win32Error,
1444
+ 'Failed to open process token'
1445
+ end
1446
+ token_handle = token_handle.unpack('V')[0]
1447
+
1448
+ begin
1449
+ # lookup shutdown privilege ID.
1450
+ luid = 0.chr * 8
1451
+ unless windows_security.LookupPrivilegeValue(
1452
+ system_name = nil,
1453
+ priviledge_name = 'SeShutdownPrivilege' + 0.chr,
1454
+ luid)
1455
+ raise ::RightScale::Platform::Win32Error,
1456
+ 'Failed to lookup shutdown privilege'
1457
+ end
1458
+ luid = luid.unpack('VV')
1459
+
1460
+ # adjust token privilege to enable shutdown.
1461
+ token_privileges = 0.chr * 16 # TOKEN_PRIVILEGES tokenPrivileges;
1462
+ token_privileges[0,4] = [1].pack('V') # tokenPrivileges.PrivilegeCount = 1;
1463
+ token_privileges[4,8] = luid.pack('VV') # tokenPrivileges.Privileges[0].Luid = luid;
1464
+ token_privileges[12,4] = [SE_PRIVILEGE_ENABLED].pack('V') # tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
1465
+ unless windows_security.AdjustTokenPrivileges(
1466
+ token_handle, disable_all_privileges = false,
1467
+ token_privileges, buffer_length = 0,
1468
+ previous_state = nil, return_length = nil)
1469
+ raise ::RightScale::Platform::Win32Error,
1470
+ 'Failed to adjust token privileges'
1471
+ end
1472
+ unless InitiateSystemShutdown(machine_name = nil,
1473
+ message = nil,
1474
+ timeout_secs = 1,
1475
+ force_apps_closed = true,
1476
+ reboot_after_shutdown)
1477
+ raise ::RightScale::Platform::Win32Error,
1478
+ 'Failed to initiate system shutdown'
1479
+ end
1480
+ ensure
1481
+ windows_common.CloseHandle(token_handle)
1482
+ end
1483
+ true
1484
+ end
1485
+
1486
+ # :bool InitiateSystemShutdown(:string, :string, :dword, :bool, :bool)
1487
+ #
1488
+ # InitiateSystemShutdown function
1489
+ # @see http://msdn.microsoft.com/en-us/library/aa376873%28v=VS.85%29.aspx
1490
+ #
1491
+ # @param [String] machine_name for shutdown or nil
1492
+ # @param [String] message for shutdown or nil
1493
+ # @param [Integer] timeout for shutdown or 0
1494
+ # @param [TrueClass|FalseClass] force_apps_closed as true to forcibly close applications
1495
+ # @param [TrueClass|FalseClass] reboot_after_shutdown as true to restart immediately after shutting down
1496
+ #
1497
+ # @return [TrueClass|FalseClass] true if successful
1498
+ def InitiateSystemShutdown(machine_name, message, timeout, force_apps_closed, reboot_after_shutdown)
1499
+ must_be_overridden
1500
+ end
1501
+
1502
+ end # Controller
1503
+
1504
+ class Rng
1505
+
1506
+ # Overrides base Rng#pseudorandom_bytes
1507
+ def pseudorandom_bytes(count)
1508
+ bytes = ''
1509
+ count.times { bytes << rand(0xff) }
1510
+ bytes
1511
+ end
1512
+ end
1513
+
1514
+ class Process
1515
+
1516
+ # Overrides base Process#resident_set_size
1517
+ def resident_set_size(pid = nil)
1518
+ # TEAL NOTE no use case for getting memory info for non-current process.
1519
+ raise ::NotImplementedError.new('pid != nil not yet implemented') if pid
1520
+
1521
+ buffer = create_process_memory_counters
1522
+ unless GetProcessMemoryInfo(GetCurrentProcess(), buffer, buffer.bytesize)
1523
+ raise ::RightScale::Platform::Win32Error,
1524
+ 'Failed to get resident set size for process'
1525
+ end
1526
+
1527
+ # PROCESS_MEMORY_COUNTERS.WorkingSetSize (bytes) is equivalent of Linux'
1528
+ # ps resident set size (KB).
1529
+ process_memory_counters = unpack_process_memory_counters(buffer)
1530
+ resident_set_size_bytes = process_memory_counters[3]
1531
+ resident_set_size_bytes / 1024 # bytes to KB
1532
+ end
1533
+
1534
+ # PROCESS_MEMORY_COUNTERS structure
1535
+ # @see http://msdn.microsoft.com/en-us/library/ms684877%28VS.85%29.aspx
1536
+ #
1537
+ # @return [String] initialized PROCESS_MEMORY_COUNTERS structure
1538
+ def create_process_memory_counters
1539
+ [
1540
+ 40, # size of PROCESS_MEMORY_COUNTERS (IN)
1541
+ 0, # PageFaultCount (OUT)
1542
+ 0, # PeakWorkingSetSize (OUT)
1543
+ 0, # WorkingSetSize (OUT)
1544
+ 0, # QuotaPeakPagedPoolUsage (OUT)
1545
+ 0, # QuotaPagedPoolUsage (OUT)
1546
+ 0, # QuotaPeakNonPagedPoolUsage (OUT)
1547
+ 0, # QuotaNonPagedPoolUsage (OUT)
1548
+ 0, # PagefileUsage (OUT)
1549
+ 0 # PeakPagefileUsage (OUT)
1550
+ ].pack('LLLLLLLLLL')
1551
+ end
1552
+
1553
+ # @param [String] buffer to unpack
1554
+ #
1555
+ # @return [Array] unpacked PROCESS_MEMORY_COUNTERS members
1556
+ def unpack_process_memory_counters(buffer)
1557
+ buffer.unpack('LLLLLLLLLL')
1558
+ end
1559
+
1560
+ # :handle GetCurrentProcess()
1561
+ #
1562
+ # GetCurrentProcess function
1563
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683179%28v=vs.85%29.aspx
1564
+ #
1565
+ # @return [Integer] current process handle
1566
+ def GetCurrentProcess
1567
+ must_be_overridden
1568
+ end
1569
+
1570
+ # :bool GetProcessMemoryInfo(:handle, :buffer_out, :dword)
1571
+ #
1572
+ # GetProcessMemoryInfo function
1573
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms683219%28v=vs.85%29.aspx
1574
+ #
1575
+ # @param [Integer] process_handle for query
1576
+ # @param [String] process_memory_info_buffer for response
1577
+ # @param [String] process_memory_info_buffer_size as limit
1578
+ #
1579
+ # @return [TrueClass|FalseClass] true if successful
1580
+ def GetProcessMemoryInfo(process_handle, process_memory_info_buffer, process_memory_info_buffer_size)
1581
+ must_be_overridden
1582
+ end
1583
+
1584
+ # :bool OpenProcessToken(:handle, :dword, :pointer)
1585
+ #
1586
+ # OpenProcessToken function
1587
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa379295%28v=vs.85%29.aspx
1588
+ #
1589
+ # @param [Integer] process_handle for token
1590
+ # @param [Integer] desired_access for token
1591
+ # @param [String] token_handle to be returned
1592
+ #
1593
+ # @return [TrueClass|FalseClass] true if successful
1594
+ def OpenProcessToken(process_handle, desired_access, token_handle)
1595
+ must_be_overridden
1596
+ end
1597
+ end
1598
+
1599
+ class Installer
1600
+
1601
+ # Overrides base Installer#install
1602
+ def install(packages)
1603
+ raise ::RightScale::Exceptions::PlatformError,
1604
+ 'No package installers supported on Windows'
1605
+ end
1606
+ end
1607
+
1608
+ # Provides common Windows APIs
1609
+ class WindowsCommon < PlatformHelperBase
1610
+
1611
+ # @see FormatMessage function
1612
+ FORMAT_MESSAGE_IGNORE_INSERTS = 0x00000200
1613
+ FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
1614
+
1615
+ # :bool CloseHandle(:handle)
1616
+ #
1617
+ # CloseHandle function
1618
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms724211%28v=vs.85%29.aspx
1619
+ #
1620
+ # @param [Integer] handle
1621
+ #
1622
+ # @return [TrueClass|FalseClass] true if succeeded
1623
+ def CloseHandle(handle)
1624
+ must_be_overridden
1625
+ end
1626
+
1627
+ # :dword FormatMessage(:dword, :buffer_in, :dword, :dword, :buffer_out, :dword, :pointer)
1628
+ #
1629
+ # FormatMessage function
1630
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
1631
+ #
1632
+ # @param [Integer] flags for formatting
1633
+ # @param [String] source for formatting or nil
1634
+ # @param [Integer] message_id for formatting or zero
1635
+ # @param [Integer] language_id for formatting or zero
1636
+ # @param [String] buffer to receive formatted string
1637
+ # @param [Integer] buffer_size for buffer limit
1638
+ # @param [String] arguments for formatting or nil
1639
+ #
1640
+ # @return [Integer] length of formatted string or zero on failure
1641
+ def FormatMessage(flags, source, message_id, language_id, buffer, buffer_size, arguments)
1642
+ must_be_overridden
1643
+ end
1644
+
1645
+ # :dword GetLastError()
1646
+ #
1647
+ # GetLastError function
1648
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms679360%28v=vs.85%29.aspx
1649
+ #
1650
+ # @return [Integer] last error code or zero
1651
+ def GetLastError
1652
+ must_be_overridden
1653
+ end
1654
+ end
1655
+
1656
+ # Provides Windows security
1657
+ class WindowsSecurity < PlatformHelperBase
1658
+
1659
+ # :bool LookupPrivilegeValue(:string, :string, :pointer)
1660
+ #
1661
+ # LookupPrivilegeValue function
1662
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa379180%28v=vs.85%29.aspx
1663
+ #
1664
+ # @param [String] system_name for lookup or nil
1665
+ # @param [String] name for lookup
1666
+ # @param [String] luid to be returned
1667
+ #
1668
+ # @return [TrueClass|FalseClass] true if successful
1669
+ def LookupPrivilegeValue(system_name, name, luid)
1670
+ must_be_overridden
1671
+ end
1672
+
1673
+ # :bool AdjustTokenPrivileges(:handle, :bool, :pointer, :dword, :pointer, :pointer)
1674
+ #
1675
+ # AdjustTokenPrivileges function
1676
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/aa375202%28v=vs.85%29.aspx
1677
+ #
1678
+ # @param [Integer] token_handle for adjustment
1679
+ # @param [TrueClass|FalseClass] disable_all_privileges true to disable all
1680
+ # @param [String] new_state for adjustment
1681
+ # @param [Integer] buffer_length sizeof previous_state buffer
1682
+ # @param [String] previous_state output buffer
1683
+ # @param [String] return_length output length
1684
+ #
1685
+ # @return [TrueClass|FalseClass] true if successful
1686
+ def AdjustTokenPrivileges(token_handle, disable_all_privileges, new_state, buffer_length, previous_state, return_length)
1687
+ must_be_overridden
1688
+ end
1689
+ end
1690
+
1691
+ # Provides Windows system information
1692
+ class WindowsSystemInformation < PlatformHelperBase
1693
+
1694
+ # System version
1695
+ class Version
1696
+ attr_reader :major, :minor, :build
1697
+
1698
+ def initialize(major, minor, build)
1699
+ @major = major
1700
+ @minor = minor
1701
+ @build = build
1702
+ end
1703
+
1704
+ # @return [String] stringized version
1705
+ def to_s
1706
+ [major, minor, build].join('.')
1707
+ end
1708
+ end
1709
+
1710
+ def initialize
1711
+ @version = nil
1712
+ @os_version_info = nil
1713
+ end
1714
+
1715
+ # @return [Version] version
1716
+ def version
1717
+ unless @version
1718
+ osvi = os_version_info
1719
+ @version = ::RightScale::Platform::WindowsSystemInformation::Version.new(
1720
+ major = osvi[1],
1721
+ minor = osvi[2],
1722
+ build = osvi[3])
1723
+ end
1724
+ @version
1725
+ end
1726
+
1727
+ # @return [Array] members of queried OSVERSIONINFO struct
1728
+ def os_version_info
1729
+ unless @os_version_info
1730
+ buffer = create_os_version_info
1731
+ unless GetVersionEx(buffer)
1732
+ raise ::RightScale::Platform::Win32Error,
1733
+ 'Failed to query Windows version'
1734
+ end
1735
+ @os_version_info = unpack_os_version_info(buffer)
1736
+ end
1737
+ @os_version_info
1738
+ end
1739
+
1740
+ # :bool GetVersionEx(:buffer_out)
1741
+ #
1742
+ # GetVersionEx function
1743
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms724451%28v=vs.85%29.aspx
1744
+ #
1745
+ # @param [String] version_info_buffer
1746
+ #
1747
+ # @return [TrueClass|FalseClass] true if successful
1748
+ def GetVersionEx(version_info_buffer)
1749
+ must_be_overridden
1750
+ end
1751
+
1752
+ # OSVERSIONINFO structure
1753
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms724834%28v=vs.85%29.aspx
1754
+ #
1755
+ # @return [String] initialized buffer
1756
+ def create_os_version_info
1757
+ must_be_overridden
1758
+ end
1759
+
1760
+ # @param [String] buffer to unpack
1761
+ #
1762
+ # @return [Array] unpacked OSVERSIONINFO members
1763
+ def unpack_os_version_info(buffer)
1764
+ must_be_overridden
1765
+ end
1766
+ end
1767
+
1768
+ # @return [WindowsCommon] Platform-specific Windows common
1769
+ def windows_common
1770
+ platform_service(:windows_common)
1771
+ end
1772
+
1773
+ # @return [WindowsSecurity] Platform-specific Windows security
1774
+ def windows_security
1775
+ platform_service(:windows_security)
1776
+ end
1777
+
1778
+ # @return [WindowsSystemInformation] Platform-specific Windows system information
1779
+ def windows_system_information
1780
+ platform_service(:windows_system_information)
1781
+ end
1782
+
1783
+ private
1784
+
1785
+ # Overrides base Platform#initialize_genus
1786
+ def initialize_genus
1787
+ @windows_common = nil
1788
+ @windows_security = nil
1789
+ @windows_system_information = nil
1790
+
1791
+ # TEAL HACK ensure GetLastError is loaded early (and also prove that the
1792
+ # platform has basic functionality) so that any just-in-time call to load
1793
+ # the API won't reset the calling thread's last error to zero (= success)
1794
+ self.windows_common.GetLastError
1795
+
1796
+ # flavor and release are more for Linux but supply Windows OS info in case
1797
+ # anyone asks. pre-release (but not release) versions of Windows have
1798
+ # codenames but they are not available as system info in the same manner
1799
+ # as Linux.
1800
+ @flavor = :windows
1801
+ @release = self.windows_system_information.version.to_s
1802
+ @codename = ''
1803
+ true
1804
+ end
1805
+
1806
+ end # Platform
1807
+
1808
+ end # RightScale