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.
- data/LICENSE +20 -0
- data/README.rdoc +82 -0
- data/Rakefile +113 -0
- data/lib/right_agent.rb +59 -0
- data/lib/right_agent/actor.rb +182 -0
- data/lib/right_agent/actor_registry.rb +76 -0
- data/lib/right_agent/actors/agent_manager.rb +232 -0
- data/lib/right_agent/agent.rb +1149 -0
- data/lib/right_agent/agent_config.rb +480 -0
- data/lib/right_agent/agent_identity.rb +210 -0
- data/lib/right_agent/agent_tag_manager.rb +237 -0
- data/lib/right_agent/audit_formatter.rb +107 -0
- data/lib/right_agent/clients.rb +31 -0
- data/lib/right_agent/clients/api_client.rb +383 -0
- data/lib/right_agent/clients/auth_client.rb +247 -0
- data/lib/right_agent/clients/balanced_http_client.rb +369 -0
- data/lib/right_agent/clients/base_retry_client.rb +495 -0
- data/lib/right_agent/clients/right_http_client.rb +279 -0
- data/lib/right_agent/clients/router_client.rb +493 -0
- data/lib/right_agent/command.rb +30 -0
- data/lib/right_agent/command/agent_manager_commands.rb +150 -0
- data/lib/right_agent/command/command_client.rb +136 -0
- data/lib/right_agent/command/command_constants.rb +33 -0
- data/lib/right_agent/command/command_io.rb +126 -0
- data/lib/right_agent/command/command_parser.rb +87 -0
- data/lib/right_agent/command/command_runner.rb +118 -0
- data/lib/right_agent/command/command_serializer.rb +63 -0
- data/lib/right_agent/connectivity_checker.rb +179 -0
- data/lib/right_agent/console.rb +65 -0
- data/lib/right_agent/core_payload_types.rb +44 -0
- data/lib/right_agent/core_payload_types/cookbook.rb +61 -0
- data/lib/right_agent/core_payload_types/cookbook_position.rb +46 -0
- data/lib/right_agent/core_payload_types/cookbook_repository.rb +116 -0
- data/lib/right_agent/core_payload_types/cookbook_sequence.rb +70 -0
- data/lib/right_agent/core_payload_types/dev_repositories.rb +100 -0
- data/lib/right_agent/core_payload_types/dev_repository.rb +76 -0
- data/lib/right_agent/core_payload_types/event_categories.rb +38 -0
- data/lib/right_agent/core_payload_types/executable_bundle.rb +130 -0
- data/lib/right_agent/core_payload_types/login_policy.rb +72 -0
- data/lib/right_agent/core_payload_types/login_user.rb +79 -0
- data/lib/right_agent/core_payload_types/planned_volume.rb +94 -0
- data/lib/right_agent/core_payload_types/recipe_instantiation.rb +73 -0
- data/lib/right_agent/core_payload_types/repositories_bundle.rb +50 -0
- data/lib/right_agent/core_payload_types/right_script_attachment.rb +95 -0
- data/lib/right_agent/core_payload_types/right_script_instantiation.rb +94 -0
- data/lib/right_agent/core_payload_types/runlist_policy.rb +44 -0
- data/lib/right_agent/core_payload_types/secure_document.rb +66 -0
- data/lib/right_agent/core_payload_types/secure_document_location.rb +63 -0
- data/lib/right_agent/core_payload_types/software_repository_instantiation.rb +61 -0
- data/lib/right_agent/daemonize.rb +35 -0
- data/lib/right_agent/dispatched_cache.rb +109 -0
- data/lib/right_agent/dispatcher.rb +272 -0
- data/lib/right_agent/enrollment_result.rb +221 -0
- data/lib/right_agent/exceptions.rb +87 -0
- data/lib/right_agent/history.rb +145 -0
- data/lib/right_agent/log.rb +460 -0
- data/lib/right_agent/minimal.rb +46 -0
- data/lib/right_agent/monkey_patches.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/array_patch.rb +29 -0
- data/lib/right_agent/monkey_patches/ruby_patch/darwin_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch.rb +24 -0
- data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +30 -0
- data/lib/right_agent/monkey_patches/ruby_patch/object_patch.rb +49 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch.rb +32 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +60 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/process_patch.rb +63 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/stdio_patch.rb +27 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/time_patch.rb +55 -0
- data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/win32ole_patch.rb +34 -0
- data/lib/right_agent/multiplexer.rb +102 -0
- data/lib/right_agent/offline_handler.rb +270 -0
- data/lib/right_agent/operation_result.rb +300 -0
- data/lib/right_agent/packets.rb +673 -0
- data/lib/right_agent/payload_formatter.rb +104 -0
- data/lib/right_agent/pending_requests.rb +128 -0
- data/lib/right_agent/pid_file.rb +159 -0
- data/lib/right_agent/platform.rb +770 -0
- data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
- data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
- data/lib/right_agent/platform/unix/platform.rb +226 -0
- data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
- data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
- data/lib/right_agent/platform/windows/platform.rb +1808 -0
- data/lib/right_agent/protocol_version_mixin.rb +69 -0
- data/lib/right_agent/retryable_request.rb +195 -0
- data/lib/right_agent/scripts/agent_controller.rb +543 -0
- data/lib/right_agent/scripts/agent_deployer.rb +400 -0
- data/lib/right_agent/scripts/common_parser.rb +160 -0
- data/lib/right_agent/scripts/log_level_manager.rb +192 -0
- data/lib/right_agent/scripts/stats_manager.rb +268 -0
- data/lib/right_agent/scripts/usage.rb +58 -0
- data/lib/right_agent/secure_identity.rb +92 -0
- data/lib/right_agent/security.rb +32 -0
- data/lib/right_agent/security/cached_certificate_store_proxy.rb +77 -0
- data/lib/right_agent/security/certificate.rb +102 -0
- data/lib/right_agent/security/certificate_cache.rb +89 -0
- data/lib/right_agent/security/distinguished_name.rb +56 -0
- data/lib/right_agent/security/encrypted_document.rb +83 -0
- data/lib/right_agent/security/rsa_key_pair.rb +76 -0
- data/lib/right_agent/security/signature.rb +86 -0
- data/lib/right_agent/security/static_certificate_store.rb +85 -0
- data/lib/right_agent/sender.rb +792 -0
- data/lib/right_agent/serialize.rb +29 -0
- data/lib/right_agent/serialize/message_pack.rb +107 -0
- data/lib/right_agent/serialize/secure_serializer.rb +151 -0
- data/lib/right_agent/serialize/secure_serializer_initializer.rb +47 -0
- data/lib/right_agent/serialize/serializable.rb +151 -0
- data/lib/right_agent/serialize/serializer.rb +159 -0
- data/lib/right_agent/subprocess.rb +38 -0
- data/lib/right_agent/tracer.rb +124 -0
- data/right_agent.gemspec +101 -0
- data/spec/actor_registry_spec.rb +80 -0
- data/spec/actor_spec.rb +162 -0
- data/spec/agent_config_spec.rb +235 -0
- data/spec/agent_identity_spec.rb +78 -0
- data/spec/agent_spec.rb +734 -0
- data/spec/agent_tag_manager_spec.rb +319 -0
- data/spec/clients/api_client_spec.rb +423 -0
- data/spec/clients/auth_client_spec.rb +272 -0
- data/spec/clients/balanced_http_client_spec.rb +576 -0
- data/spec/clients/base_retry_client_spec.rb +635 -0
- data/spec/clients/router_client_spec.rb +594 -0
- data/spec/clients/spec_helper.rb +111 -0
- data/spec/command/agent_manager_commands_spec.rb +51 -0
- data/spec/command/command_io_spec.rb +93 -0
- data/spec/command/command_parser_spec.rb +79 -0
- data/spec/command/command_runner_spec.rb +107 -0
- data/spec/command/command_serializer_spec.rb +51 -0
- data/spec/connectivity_checker_spec.rb +83 -0
- data/spec/core_payload_types/dev_repositories_spec.rb +64 -0
- data/spec/core_payload_types/dev_repository_spec.rb +33 -0
- data/spec/core_payload_types/executable_bundle_spec.rb +67 -0
- data/spec/core_payload_types/login_user_spec.rb +102 -0
- data/spec/core_payload_types/recipe_instantiation_spec.rb +81 -0
- data/spec/core_payload_types/right_script_attachment_spec.rb +65 -0
- data/spec/core_payload_types/right_script_instantiation_spec.rb +79 -0
- data/spec/core_payload_types/spec_helper.rb +23 -0
- data/spec/dispatched_cache_spec.rb +136 -0
- data/spec/dispatcher_spec.rb +324 -0
- data/spec/enrollment_result_spec.rb +53 -0
- data/spec/history_spec.rb +246 -0
- data/spec/log_spec.rb +192 -0
- data/spec/monkey_patches/eventmachine_spec.rb +62 -0
- data/spec/multiplexer_spec.rb +48 -0
- data/spec/offline_handler_spec.rb +340 -0
- data/spec/operation_result_spec.rb +208 -0
- data/spec/packets_spec.rb +461 -0
- data/spec/pending_requests_spec.rb +136 -0
- data/spec/platform/spec_helper.rb +216 -0
- data/spec/platform/unix/darwin/platform_spec.rb +181 -0
- data/spec/platform/unix/linux/platform_spec.rb +540 -0
- data/spec/platform/unix/spec_helper.rb +149 -0
- data/spec/platform/windows/mingw/platform_spec.rb +222 -0
- data/spec/platform/windows/mswin/platform_spec.rb +259 -0
- data/spec/platform/windows/spec_helper.rb +720 -0
- data/spec/retryable_request_spec.rb +306 -0
- data/spec/secure_identity_spec.rb +50 -0
- data/spec/security/cached_certificate_store_proxy_spec.rb +62 -0
- data/spec/security/certificate_cache_spec.rb +71 -0
- data/spec/security/certificate_spec.rb +49 -0
- data/spec/security/distinguished_name_spec.rb +46 -0
- data/spec/security/encrypted_document_spec.rb +55 -0
- data/spec/security/rsa_key_pair_spec.rb +55 -0
- data/spec/security/signature_spec.rb +66 -0
- data/spec/security/static_certificate_store_spec.rb +58 -0
- data/spec/sender_spec.rb +1045 -0
- data/spec/serialize/message_pack_spec.rb +131 -0
- data/spec/serialize/secure_serializer_spec.rb +132 -0
- data/spec/serialize/serializable_spec.rb +90 -0
- data/spec/serialize/serializer_spec.rb +197 -0
- data/spec/spec.opts +2 -0
- data/spec/spec.win32.opts +1 -0
- data/spec/spec_helper.rb +130 -0
- data/spec/tracer_spec.rb +114 -0
- 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
|