right_agent 0.17.2 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. data/lib/right_agent.rb +0 -1
  2. data/lib/right_agent/agent_config.rb +1 -1
  3. data/lib/right_agent/minimal.rb +8 -7
  4. data/lib/right_agent/monkey_patches.rb +4 -2
  5. data/lib/right_agent/monkey_patches/ruby_patch.rb +9 -9
  6. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +2 -2
  7. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +21 -51
  8. data/lib/right_agent/packets.rb +5 -1
  9. data/lib/right_agent/platform.rb +727 -299
  10. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  11. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  12. data/lib/right_agent/platform/unix/platform.rb +226 -0
  13. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  14. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  15. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  16. data/right_agent.gemspec +13 -8
  17. data/spec/platform/spec_helper.rb +216 -0
  18. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  19. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  20. data/spec/platform/unix/spec_helper.rb +149 -0
  21. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  22. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  23. data/spec/platform/windows/spec_helper.rb +720 -0
  24. metadata +45 -30
  25. data/lib/right_agent/platform/darwin.rb +0 -285
  26. data/lib/right_agent/platform/linux.rb +0 -537
  27. data/lib/right_agent/platform/windows.rb +0 -1384
  28. data/spec/platform/darwin_spec.rb +0 -13
  29. data/spec/platform/linux_spec.rb +0 -38
  30. data/spec/platform/linux_volume_manager_spec.rb +0 -201
  31. data/spec/platform/platform_spec.rb +0 -80
  32. data/spec/platform/windows_spec.rb +0 -13
  33. data/spec/platform/windows_volume_manager_spec.rb +0 -318
@@ -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