right_agent 0.17.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -1,1384 +0,0 @@
1
- #
2
- # Copyright (c) 2009-2011 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 'rubygems'
24
- require 'fileutils'
25
- require 'tmpdir'
26
- require 'rbconfig'
27
-
28
- begin
29
- require 'win32/dir'
30
- require 'windows/api'
31
- require 'windows/error'
32
- require 'windows/handle'
33
- require 'windows/security'
34
- require 'windows/system_info'
35
- require 'Win32API'
36
- rescue LoadError => e
37
- raise e if !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i)
38
- end
39
-
40
- module RightScale
41
- # Throw when a Win32 API fails. Message will contain the last error message
42
- class Win32Error < Exception
43
- include ::Windows::Error
44
-
45
- def initialize(msg = "")
46
- super(msg)
47
- @last_error = get_last_error
48
- end
49
-
50
- def message
51
- original_message = super
52
- result = ""
53
- result << "#{original_message}\n Error Detail: " unless original_message.nil? || original_message.empty?
54
- result << "#{@last_error}"
55
- end
56
- end
57
-
58
- # Windows specific implementation
59
- class Platform
60
-
61
- attr_reader :flavor, :release
62
-
63
- def init
64
- getversionex = Win32API.new("kernel32", "GetVersionEx", 'P', 'L')
65
- osversioninfo = [
66
- 148, # size of this struct (IN)
67
- 0, # major version (OUT)
68
- 0, # minor version (OUT)
69
- 0, # build (OUT)
70
- 0, # platform (OUT)
71
- "\0" * 128 # additional info (OUT)
72
- ].pack('LLLLLa128')
73
-
74
- raise 'Failed to detect Windows version' if 0 == getversionex.call(osversioninfo) # zero is failure
75
- version = osversioninfo.unpack('LLLLLZ128') # 'Z' means ASCIIZ string
76
- @flavor = 'windows' # there can be only one
77
- @release = "#{version[1]}.#{version[2]}.#{version[3]}"
78
- end
79
-
80
- class Filesystem
81
- MAX_PATH = 260
82
-
83
- @@get_temp_dir_api = nil
84
-
85
- def initialize
86
- @temp_dir = nil
87
- end
88
-
89
- # Is given command available in the PATH?
90
- #
91
- # === Parameters
92
- # command_name(String):: Name of command to be tested with
93
- # or without the expected windows file extension.
94
- #
95
- # === Return
96
- # true:: If command is in path
97
- # false:: Otherwise
98
- def has_executable_in_path(command_name)
99
- return nil != find_executable_in_path(command_name)
100
- end
101
-
102
- # Finds the given command name in the PATH. this emulates the 'which'
103
- # command from linux (without the terminating newline).
104
- #
105
- # === Parameters
106
- # command_name(String):: Name of command to be tested with
107
- # or without the expected windows file extension.
108
- #
109
- # === Return
110
- # path to first matching executable file in PATH or nil
111
- def find_executable_in_path(command_name)
112
- # must search all known (executable) path extensions unless the
113
- # explicit extension was given. this handles a case such as 'curl'
114
- # which can either be on the path as 'curl.exe' or as a command shell
115
- # shortcut called 'curl.cmd', etc.
116
- use_path_extensions = 0 == File.extname(command_name).length
117
- path_extensions = use_path_extensions ? ENV['PATHEXT'].split(/;/) : nil
118
-
119
- # must check the current working directory first just to be completely
120
- # sure what would happen if the command were executed. note that linux
121
- # ignores the CWD, so this is platform-specific behavior for windows.
122
- cwd = Dir.getwd
123
- path = ENV['PATH']
124
- path = (path.nil? || 0 == path.length) ? cwd : (cwd + ';' + path)
125
- path.split(/;/).each do |dir|
126
- if use_path_extensions
127
- path_extensions.each do |path_extension|
128
- path = File.join(dir, command_name + path_extension)
129
- return path if File.executable?(path)
130
- end
131
- else
132
- path = File.join(dir, command_name)
133
- return path if File.executable?(path)
134
- end
135
- end
136
- return nil
137
- end
138
-
139
- # Directory containing generated agent configuration files
140
- # @deprecated
141
- def cfg_dir
142
- warn "cfg_dir is deprecated; please use right_agent_cfg_dir"
143
- right_agent_cfg_dir
144
- end
145
-
146
- # RightScale state directory for the current platform
147
- # @deprecated
148
- def right_scale_state_dir
149
- warn "right_scale_state_dir is deprecated; please use either right_scale_static_state_dir or right_agent_dynamic_state_dir"
150
- right_scale_static_state_dir
151
- end
152
-
153
- # Directory containing generated agent configuration files
154
- def right_agent_cfg_dir
155
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'right_agent'))
156
- end
157
-
158
- # Static (time-invariant) state that is common to all RightScale apps/agents
159
- def right_scale_static_state_dir
160
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'rightscale.d'))
161
- end
162
-
163
- # Static (time-invariant) state that is specific to RightLink
164
- def right_link_static_state_dir
165
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'rightscale.d', 'right_link'))
166
- end
167
-
168
- # Dynamic, persistent runtime state that is specific to RightLink
169
- def right_link_dynamic_state_dir
170
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'right_link'))
171
- end
172
-
173
- # Spool directory for the current platform
174
- def spool_dir
175
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'spool'))
176
- end
177
-
178
- def ssh_cfg_dir
179
- return pretty_path(File.join(ENV['USERPROFILE'] || temp_dir, '.ssh'))
180
- end
181
-
182
- # Cache directory for the current platform
183
- def cache_dir
184
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'cache'))
185
- end
186
-
187
- # Log directory for the current platform
188
- def log_dir
189
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'log'))
190
- end
191
-
192
- # Source code, for reference purposes and for development.
193
- def source_code_dir
194
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'src'))
195
- end
196
-
197
- # Temp directory for the current platform
198
- def temp_dir
199
- if @temp_dir.nil?
200
- @@get_temp_dir_api = Win32::API.new('GetTempPath', 'LP', 'L') unless @@get_temp_dir_api
201
- buffer = 0.chr * MAX_PATH
202
- @@get_temp_dir_api.call(buffer.length, buffer)
203
- @temp_dir = pretty_path(buffer.unpack('A*').first.chomp('\\'))
204
- end
205
- rescue
206
- @temp_dir = File.join(Dir::WINDOWS, "temp")
207
- ensure
208
- return @temp_dir
209
- end
210
-
211
- # Path to place pid files
212
- def pid_dir
213
- return pretty_path(File.join(Dir::COMMON_APPDATA, 'RightScale', 'run'))
214
- end
215
-
216
- def right_link_home_dir
217
- unless @right_link_home_dir
218
- @right_link_home_dir = ENV['RS_RIGHT_LINK_HOME'] ||
219
- File.normalize_path(File.join(company_program_files_dir, 'RightLink'))
220
- end
221
- @right_link_home_dir
222
- end
223
-
224
- # Path to right link configuration and internal usage scripts
225
- def private_bin_dir
226
- return pretty_path(File.join(right_link_home_dir, 'bin'))
227
- end
228
-
229
- def sandbox_dir
230
- return pretty_path(File.join(right_link_home_dir, 'sandbox'))
231
- end
232
-
233
- # System root path
234
- def system_root
235
- return pretty_path(ENV['SystemRoot'])
236
- end
237
-
238
- # converts a long path to a short path. in windows terms, this means
239
- # taking any file/folder name over 8 characters in length and truncating
240
- # it to 6 characters with ~1..~n appended depending on how many similar
241
- # names exist in the same directory. file extensions are simply chopped
242
- # at three letters. the short name is equivalent for all API calls to
243
- # the long path but requires no special quoting, etc. the path must
244
- # exist at least partially for the API call to succeed.
245
- #
246
- # === Parameters
247
- # long_path(String):: fully or partially existing long path to be
248
- # converted to its short path equivalent.
249
- #
250
- # === Return
251
- # short_path(String):: short path equivalent or same path if non-existent
252
- def long_path_to_short_path(long_path)
253
- return File.long_path_to_short_path(long_path)
254
- end
255
-
256
- # specific to the windows environment to aid in resolving paths to
257
- # executables in test scenarios.
258
- def company_program_files_dir
259
- return pretty_path(File.join(Dir::PROGRAM_FILES, 'RightScale'))
260
- end
261
-
262
- # pretties up paths which assists Dir.glob() and Dir[] calls which will
263
- # return empty if the path contains any \ characters. windows doesn't
264
- # care (most of the time) about whether you use \ or / in paths. as
265
- # always, there are exceptions to this rule (such as "del c:/xyz" which
266
- # fails while "del c:\xyz" succeeds)
267
- #
268
- # === Parameters
269
- # path(String):: path to make pretty
270
- # native_fs_flag(Boolean):: true if path is pretty for native file
271
- # system (i.e. file system calls more likely to succeed), false if
272
- # pretty for Ruby interpreter (default).
273
- def pretty_path(path, native_fs_flag = false)
274
- return native_fs_flag ? path.gsub("/", "\\") : path.gsub("\\", "/")
275
- end
276
-
277
- # Ensures a local drive location for the file or folder given by path
278
- # by copying to a local temp directory given by name only if the item
279
- # does not appear on the home drive. This method is useful because
280
- # secure applications refuse to run scripts from network locations, etc.
281
- # Replaces any similar files in temp dir to ensure latest updates.
282
- #
283
- # === Parameters
284
- # path(String):: path to file or directory to be placed locally.
285
- #
286
- # temp_dir_name(String):: name (or relative path) of temp directory to
287
- # use only if the file or folder is not on a local drive.
288
- #
289
- # === Returns
290
- # result(String):: local drive path
291
- def ensure_local_drive_path(path, temp_dir_name)
292
- homedrive = ENV['HOMEDRIVE']
293
- if homedrive && homedrive.upcase != path[0,2].upcase
294
- local_dir = ::File.join(temp_dir, temp_dir_name)
295
- FileUtils.mkdir_p(local_dir)
296
- local_path = ::File.join(local_dir, ::File.basename(path))
297
- if ::File.directory?(path)
298
- FileUtils.rm_rf(local_path) if ::File.directory?(local_path)
299
- FileUtils.cp_r(::File.join(path, '.'), local_path)
300
- else
301
- FileUtils.cp(path, local_path)
302
- end
303
- path = local_path
304
- end
305
- return path
306
- end
307
-
308
- # Ruby 1.8.7 on Windows does not support File.symlink. Windows Vista and newer
309
- # versions of Windows do support symlinks via CreateSymbolicLink, so we will use CreateSymbolicLink if it
310
- # is available, otherwise throw (on 2003 and earlier)
311
- #
312
- # === Parameters
313
- # old_name (String):: the path to the real file/directory
314
- # new_name (String):: the path to the link
315
- #
316
- # === Results
317
- # always 0 as does File.symlink
318
- #
319
- # === Raises
320
- # Win32Error:: if failed to create the link
321
- # PlatformNotSupported:: if the current platform does not support the CreateSymbolicLink API
322
- def create_symlink(old_name, new_name)
323
- raise ::RightScale::PlatformNotSupported, "Cannot create symlinks on this platform" unless defined?(::Windows::File::CreateSymbolicLink)
324
- flags = File.directory?(old_name) ? ::Windows::File::SYMBOLIC_LINK_FLAG_DIRECTORY : 0
325
- result = ::Windows::File::CreateSymbolicLink.call(new_name, old_name, flags)
326
- raise ::RightScale::Win32Error, "failed to create link from #{old_name} to #{new_name}" unless (result == 1)
327
- 0
328
- end
329
- end # Filesystem
330
-
331
- # Provides utilities for managing volumes (disks).
332
- class VolumeManager
333
-
334
- class ParserError < Exception; end
335
- class VolumeError < Exception; end
336
-
337
- def initialize
338
- @os_info = OSInformation.new
339
-
340
- @assignable_disk_regex = /^[D-Zd-z]:[\/\\]?$/
341
- @assignable_path_regex = /^[A-Za-z]:[\/\\][\/\\\w\s\d\-_\.~]+$/
342
- end
343
-
344
- # Determines if the given path is valid for a Windows volume attachemnt
345
- # (excluding the reserved A: B: C: drives).
346
- #
347
- # === Return
348
- # result(Boolean):: true if path is a valid volume root
349
- def is_attachable_volume_path?(path)
350
- return (nil != (path =~ @assignable_disk_regex) || nil != (path =~ @assignable_path_regex))
351
- end
352
-
353
- # Gets a list of physical or virtual disks in the form:
354
- # [{:index, :status, :total_size, :free_size, :dynamic, :gpt}*]
355
- #
356
- # where
357
- # :index >= 0
358
- # :status = 'Online' | 'Offline'
359
- # :total_size = bytes used by partitions
360
- # :free_size = bytes not used by partitions
361
- # :dynamic = true | false
362
- # :gpt = true | false
363
- #
364
- # GPT = GUID partition table
365
- #
366
- # === Parameters
367
- # conditions{Hash):: hash of conditions to match or nil (default)
368
- #
369
- # === Return
370
- # volumes(Array):: array of hashes detailing visible volumes.
371
- #
372
- # === Raise
373
- # VolumeError:: on failure to list disks
374
- # ParserError:: on failure to parse disks from output
375
- def disks(conditions = nil)
376
- script = <<EOF
377
- rescan
378
- list disk
379
- EOF
380
- exit_code, output_text = run_script(script)
381
- raise VolumeError.new("Failed to list disks: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
382
- return parse_disks(output_text, conditions)
383
- end
384
-
385
- # Gets a list of currently visible volumes in the form:
386
- # [{:index, :device, :label, :filesystem, :type, :total_size, :status, :info}*]
387
- #
388
- # where
389
- # :index >= 0
390
- # :device = "[A-Z]:"
391
- # :label = up to 11 characters
392
- # :filesystem = nil | 'NTFS' | <undocumented>
393
- # :type = 'NTFS' | <undocumented>
394
- # :total_size = size in bytes
395
- # :status = 'Healthy' | <undocumented>
396
- # :info = 'System' | empty | <undocumented>
397
- #
398
- # note that a strange aspect of diskpart is that it won't correlate
399
- # disks to volumes in any list even though partition lists are always
400
- # in the context of a selected disk.
401
- #
402
- # volume order can change as volumes are created/destroyed between
403
- # diskpart sessions so volume 0 can represent C: in one session and
404
- # then be represented as volume 1 in the next call to diskpart.
405
- #
406
- # volume labels are truncated to 11 characters by diskpart even though
407
- # NTFS allows up to 32 characters.
408
- #
409
- # === Parameters
410
- # conditions{Hash):: hash of conditions to match or nil (default)
411
- #
412
- # === Return
413
- # volumes(Array):: array of hashes detailing visible volumes.
414
- #
415
- # === Raise
416
- # VolumeError:: on failure to list volumes
417
- # ParserError:: on failure to parse volumes from output
418
- def volumes(conditions = nil)
419
- script = <<EOF
420
- rescan
421
- list volume
422
- EOF
423
- exit_code, output_text = run_script(script)
424
- raise VolumeError.new("Failed to list volumes exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
425
- return parse_volumes(output_text, conditions)
426
- end
427
-
428
- # Gets a list of partitions for the disk given by index in the form:
429
- # {:index, :type, :size, :offset}
430
- #
431
- # where
432
- # :index >= 0
433
- # :type = 'OEM' | 'Primary' | <undocumented>
434
- # :size = size in bytes used by partition on disk
435
- # :offset = offset of partition in bytes from head of disk
436
- #
437
- # === Parameters
438
- # disk_index(int):: disk index to query
439
- # conditions{Hash):: hash of conditions to match or nil (default)
440
- #
441
- # === Return
442
- # result(Array):: list of partitions or empty
443
- #
444
- # === Raise
445
- # VolumeError:: on failure to list partitions
446
- # ParserError:: on failure to parse partitions from output
447
- def partitions(disk_index, conditions = nil)
448
- script = <<EOF
449
- rescan
450
- select disk #{disk_index}
451
- list partition
452
- EOF
453
- exit_code, output_text = run_script(script)
454
- raise VolumeError.new("Failed to list partitions exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
455
- return parse_partitions(output_text, conditions)
456
- end
457
-
458
- # Formats a disk given by disk index and the device (e.g. "D:") for the
459
- # volume on the primary NTFS partition which will be created.
460
- #
461
- # === Parameters
462
- # disk_index(int): zero-based disk index (from disks list, etc.)
463
- # device(String):: disk letter or mount path specified for the volume to create
464
- #
465
- # === Return
466
- # always true
467
- #
468
- # === Raise
469
- # ArgumentError:: on invalid parameters
470
- # VolumeError:: on failure to format
471
- def format_disk(disk_index, device)
472
- if device.match(@assignable_path_regex) && @os_info.major < 6
473
- raise ArgumentError.new("Mount path assignment is not supported in this version of windows")
474
- end
475
- # note that creating the primary partition automatically creates and
476
- # selects a new volume, which can be assigned a letter before the
477
- # partition has actually been formatted.
478
- raise ArgumentError.new("Invalid index = #{disk_index}") unless disk_index >= 0
479
- raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
480
-
481
- # note that Windows 2003 server version of diskpart doesn't support
482
- # format so that has to be done separately.
483
- format_command = if @os_info.major < 6; ""; else; "format FS=NTFS quick"; end
484
- script = <<EOF
485
- rescan
486
- list disk
487
- select disk #{disk_index}
488
- #{get_clear_readonly_command('disk')}
489
- #{get_online_disk_command}
490
- clean
491
- create partition primary
492
- #{get_assign_command_for_device(device)}
493
- #{format_command}
494
- EOF
495
- exit_code, output_text = run_script(script)
496
- raise VolumeError.new("Failed to format disk #{disk_index} for device #{device}: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
497
-
498
- # must format using command shell's FORMAT command before 2008 server.
499
- if @os_info.major < 6
500
- command = "echo Y | format #{device[0,1]}: /Q /V: /FS:NTFS"
501
- output_text = `#{command}`
502
- exit_code = $?.exitstatus
503
- raise VolumeError.new("Failed to format disk #{disk_index} for device #{device}: exit code = #{exit_code}\n#{output_text}") if exit_code != 0
504
- end
505
- true
506
- end
507
-
508
- # Brings the disk given by index online and clears the readonly
509
- # attribute, if necessary. The latter is required for some kinds of
510
- # disks to online successfully and SAN volumes may be readonly when
511
- # initially attached. As this change may bring additional volumes online
512
- # the updated volumes list is returned.
513
- #
514
- # === Parameters
515
- # disk_index(int):: zero-based disk index
516
- # options(Hash):: A hash of options which allows different behavior while onlining a drive. Possible options are;
517
- # :idempotent(Bool) - Checks the current disk statuses before attempting to online the disk. If the drive is already online, it bails out making this method idempotent.
518
- #
519
- # === Return
520
- # always true
521
- #
522
- # === Raise
523
- # ArgumentError:: on invalid parameters
524
- # VolumeError:: on failure to online disk
525
- # ParserError:: on failure to parse volume list
526
- def online_disk(disk_index, options={})
527
- raise ArgumentError.new("Invalid disk_index = #{disk_index}") unless disk_index >= 0
528
- # Set some defaults for backward compatibility, allow user specified options to override defaults
529
- options = {:idempotent => false}.merge(options)
530
- script = <<EOF
531
- rescan
532
- list disk
533
- select disk #{disk_index}
534
- #{get_clear_readonly_command('disk')}
535
- #{get_online_disk_command}
536
- EOF
537
-
538
- if(options[:idempotent])
539
- disk = disks(:index => disk_index)
540
- return true if disk && disk[:status] == "Online"
541
- end
542
-
543
- exit_code, output_text = run_script(script)
544
- raise VolumeError.new("Failed to online disk #{disk_index}: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
545
- true
546
- end
547
-
548
- # Brings the disk given by index offline
549
- #
550
- # === Parameters
551
- # disk_index(int):: zero-based disk index
552
- #
553
- # === Return
554
- # always true
555
- #
556
- # === Raise
557
- # ArgumentError:: on invalid parameters
558
- # VolumeError:: on failure to online disk
559
- # ParserError:: on failure to parse volume list
560
- def offline_disk(disk_index)
561
- raise ArgumentError.new("Invalid disk_index = #{disk_index}") unless disk_index >= 0
562
- # Set some defaults for backward compatibility, allow user specified options to override defaults
563
- script = <<EOF
564
- rescan
565
- list disk
566
- select disk #{disk_index}
567
- offline disk noerr
568
- EOF
569
-
570
- exit_code, output_text = run_script(script)
571
- raise VolumeError.new("Failed to offline disk #{disk_index}: exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
572
- true
573
- end
574
-
575
- # Assigns the given device name to the volume given by index and clears
576
- # the readonly attribute, if necessary. The device must not currently be
577
- # in use.
578
- #
579
- # === Parameters
580
- # volume_device_or_index(int):: old device or zero-based volume index (from volumes list, etc.) to select for assignment.
581
- # device(String):: disk letter or mount path specified for the volume to create
582
- # options(Hash):: A hash of options which allows different behavior while assigning a device. Possible options are:
583
- # :clear_readonly(Bool) - Set to true by default, since the previous implementation of this method always cleared readonly
584
- # :remove_all(Bool) - Removes all previously assigned devices and paths, essentially a big RESET button for volume assignment
585
- # :idempotent(Bool) - Checks the current device assignments before assigning the device according to the specified parameters. If the device is already assigned, it bails out making this method idempotent.
586
- #
587
- # === Return
588
- # always true
589
- #
590
- # === Raise
591
- # ArgumentError:: on invalid parameters
592
- # VolumeError:: on failure to assign device name
593
- # ParserError:: on failure to parse volume list
594
- def assign_device(volume_device_or_index, device, options={})
595
- # Set some defaults for backward compatibility, allow user specified options to override defaults
596
- options = {:clear_readonly => true, :idempotent => false}.merge(options)
597
-
598
- if device.match(@assignable_path_regex) && @os_info.major < 6
599
- raise ArgumentError.new("Mount path assignment is not supported in this version of windows")
600
- end
601
- # Volume selector for drive letter assignments
602
- volume_selector_match = volume_device_or_index.to_s.match(/^([D-Zd-z]|\d+):?$/)
603
- # Volume selector for mount path assignments
604
- volume_selector_match = volume_device_or_index.to_s.match(@assignable_path_regex) unless volume_selector_match
605
- raise ArgumentError.new("Invalid volume_device_or_index = #{volume_device_or_index}") unless volume_selector_match
606
- volume_selector = volume_selector_match[1]
607
- raise ArgumentError.new("Invalid device = #{device}") unless is_attachable_volume_path?(device)
608
- if(options[:idempotent])
609
- # Device already assigned?
610
- vols = volumes
611
- already_assigned = vols.select do |k,v|
612
- # The volume is specified by it's index and is already mounted to the specified device/path
613
- (k == :index && v == volume_device_or_index && vols[:device] == device) ||
614
- # The volume is specified by it's current device/path assignment and is already mounted to the specified device/path
615
- (k == :device && v == volume_device_or_index)
616
- end
617
-
618
- return true if already_assigned.length > 0
619
- end
620
- # Validation ends here, and real stuff starts to happen
621
-
622
- script = <<EOF
623
- rescan
624
- list volume
625
- select volume "#{volume_selector}"
626
- #{get_clear_readonly_command('volume') if options[:clear_readonly]}
627
- #{"remove all noerr" if options[:remove_all]}
628
- #{get_assign_command_for_device(device)}
629
- EOF
630
-
631
- exit_code, output_text = run_script(script)
632
- raise VolumeError.new("Failed to assign device \"#{device}\" for volume \"#{volume_device_or_index}\": exit code = #{exit_code}\n#{script}\n#{output_text}") if exit_code != 0
633
- true
634
- end
635
-
636
- protected
637
-
638
- # Parses raw output from diskpart looking for the (first) disk list.
639
- #
640
- # Example of raw output from diskpart (column width is dictated by the
641
- # header and some columns can be empty):
642
- #
643
- # Disk ### Status Size Free Dyn Gpt
644
- # -------- ---------- ------- ------- --- ---
645
- # Disk 0 Online 80 GB 0 B
646
- #* Disk 1 Offline 4096 MB 4096 MB
647
- # Disk 2 Online 4096 MB 4096 MB *
648
- #
649
- # === Parameters
650
- # output_text(String):: raw output from diskpart
651
- # conditions{Hash):: hash of conditions to match or nil (default)
652
- #
653
- # === Return
654
- # result(Array):: volumes or empty
655
- #
656
- # === Raise
657
- # ParserError:: on failure to parse disk list
658
- def parse_disks(output_text, conditions = nil)
659
- result = []
660
- line_regex = nil
661
- header_regex = / -------- (-+) ------- ------- --- ---/
662
- header_match = nil
663
- output_text.lines.each do |line|
664
- line = line.chomp
665
- if line_regex
666
- if line.strip.empty?
667
- break
668
- end
669
- match_data = line.match(line_regex)
670
- raise ParserError.new("Failed to parse disk info from #{line.inspect} using #{line_regex.inspect}") unless match_data
671
- data = {:index => match_data[1].to_i,
672
- :status => match_data[2].strip,
673
- :total_size => size_factor_to_bytes(match_data[3], match_data[4]),
674
- :free_size => size_factor_to_bytes(match_data[5], match_data[6]),
675
- :dynamic => match_data[7].strip[0,1] == '*',
676
- :gpt => match_data[8].strip[0,1] == '*'}
677
- if conditions
678
- matched = true
679
- conditions.each do |key, value|
680
- unless data[key] == value
681
- matched = false
682
- break
683
- end
684
- end
685
- result << data if matched
686
- else
687
- result << data
688
- end
689
- elsif header_match = line.match(header_regex)
690
- # account for some fields being variable width between versions of the OS.
691
- status_width = header_match[1].length
692
- line_regex_text = "^[\\* ] Disk (\\d[\\d ]\{2\}) (.\{#{status_width}\}) "\
693
- "[ ]?([\\d ]\{3\}\\d) (.?B) [ ]?([\\d ]\{3\}\\d) (.?B) ([\\* ]) ([\\* ])"
694
- line_regex = Regexp.compile(line_regex_text)
695
- else
696
- # one or more lines of ignored headers
697
- end
698
- end
699
- raise ParserError.new("Failed to parse disk list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
700
- return result
701
- end
702
-
703
- # Parses raw output from diskpart looking for the (first) volume list.
704
- #
705
- # Example of raw output from diskpart (column width is dictated by the
706
- # header and some columns can be empty):
707
- #
708
- # Volume ### Ltr Label Fs Type Size Status Info
709
- # ---------- --- ----------- ----- ---------- ------- --------- --------
710
- # Volume 0 C 2008Boot NTFS Partition 80 GB Healthy System
711
- #* Volume 1 D NTFS Partition 4094 MB Healthy
712
- # Volume 2 NTFS Partition 4094 MB Healthy
713
- #
714
- # === Parameters
715
- # output_text(String):: raw output from diskpart
716
- # conditions{Hash):: hash of conditions to match or nil (default)
717
- #
718
- # === Return
719
- # result(Array):: volumes or empty. Drive letters are appended with ':' even though they aren't
720
- # returned that way from diskpart
721
- #
722
- # === Raise
723
- # ParserError:: on failure to parse volume list
724
- def parse_volumes(output_text, conditions = nil)
725
- result = []
726
- header_regex = / ---------- --- (-+) (-+) (-+) ------- (-+) (-+)/
727
- header_match = nil
728
- line_regex = nil
729
- output_text.lines.each do |line|
730
- line = line.chomp
731
- if line_regex
732
- if line.strip.empty?
733
- break
734
- end
735
- match_data = line.match(line_regex)
736
- unless match_data
737
- path_match_regex = /([A-Za-z]:[\/\\][\/\\\w\s\d]+)/
738
- match_data = line.match(path_match_regex)
739
- if match_data
740
- result.last[:device] = match_data[1]
741
- next
742
- end
743
- end
744
- raise ParserError.new("Failed to parse volume info from #{line.inspect} using #{line_regex.inspect}") unless match_data
745
- letter = nil_if_empty(match_data[2])
746
- device = "#{letter.upcase}:" if letter
747
- data = {:index => match_data[1].to_i,
748
- :device => device,
749
- :label => nil_if_empty(match_data[3]),
750
- :filesystem => nil_if_empty(match_data[4]),
751
- :type => nil_if_empty(match_data[5]),
752
- :total_size => size_factor_to_bytes(match_data[6], match_data[7]),
753
- :status => nil_if_empty(match_data[8]),
754
- :info => nil_if_empty(match_data[9])}
755
- if conditions
756
- matched = true
757
- conditions.each do |key, value|
758
- unless data[key] == value
759
- matched = false
760
- break
761
- end
762
- end
763
- result << data if matched
764
- else
765
- result << data
766
- end
767
- elsif header_match = line.match(header_regex)
768
- # account for some fields being variable width between versions of the OS.
769
- label_width = header_match[1].length
770
- filesystem_width = header_match[2].length
771
- type_width = header_match[3].length
772
- status_width = header_match[4].length
773
- info_width = header_match[5].length
774
- line_regex_text = "^[\\* ] Volume (\\d[\\d ]\{2\}) ([A-Za-z ]) "\
775
- "(.\{#{label_width}\}) (.\{#{filesystem_width}\}) "\
776
- "(.\{#{type_width}\}) [ ]?([\\d ]\{3\}\\d) (.?B)\\s{0,2}"\
777
- "(.\{#{status_width}\})\\s{0,2}(.\{0,#{info_width}\})"
778
- line_regex = Regexp.compile(line_regex_text)
779
- else
780
- # one or more lines of ignored headers
781
- end
782
- end
783
- raise ParserError.new("Failed to parse volume list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
784
- return result
785
- end
786
-
787
- # Parses raw output from diskpart looking for the (first) partition list.
788
- #
789
- # Example of raw output from diskpart (column width is dictated by the
790
- # header and some columns can be empty):
791
- #
792
- # Partition ### Type Size Offset
793
- # ------------- ---------------- ------- -------
794
- # Partition 1 OEM 39 MB 31 KB
795
- #* Partition 2 Primary 14 GB 40 MB
796
- # Partition 3 Primary 451 GB 14 GB
797
- #
798
- # === Parameters
799
- # output_text(String):: raw output from diskpart
800
- # conditions{Hash):: hash of conditions to match or nil (default)
801
- #
802
- # === Return
803
- # result(Array):: volumes or empty
804
- #
805
- # === Raise
806
- # ParserError:: on failure to parse volume list
807
- def parse_partitions(output_text, conditions = nil)
808
- result = []
809
- header_regex = / ------------- (-+) ------- -------/
810
- header_match = nil
811
- line_regex = nil
812
- output_text.lines.each do |line|
813
- line = line.chomp
814
- if line_regex
815
- if line.strip.empty?
816
- break
817
- end
818
- match_data = line.match(line_regex)
819
- raise ParserError.new("Failed to parse partition info from #{line.inspect} using #{line_regex.inspect}") unless match_data
820
- data = {:index => match_data[1].to_i,
821
- :type => nil_if_empty(match_data[2]),
822
- :size => size_factor_to_bytes(match_data[3], match_data[4]),
823
- :offset => size_factor_to_bytes(match_data[5], match_data[6])}
824
- if conditions
825
- matched = true
826
- conditions.each do |key, value|
827
- unless data[key] == value
828
- matched = false
829
- break
830
- end
831
- end
832
- result << data if matched
833
- else
834
- result << data
835
- end
836
- elsif header_match = line.match(header_regex)
837
- # account for some fields being variable width between versions of the OS.
838
- type_width = header_match[1].length
839
- line_regex_text = "^[\\* ] Partition (\\d[\\d ]\{2\}) (.\{#{type_width}\}) "\
840
- "[ ]?([\\d ]\{3\}\\d) (.?B) [ ]?([\\d ]\{3\}\\d) (.?B)"
841
- line_regex = Regexp.compile(line_regex_text)
842
- elsif line.start_with?("There are no partitions on this disk")
843
- return []
844
- else
845
- # one or more lines of ignored headers
846
- end
847
- end
848
- raise ParserError.new("Failed to parse volume list header from output #{output_text.inspect} using #{header_regex.inspect}") unless header_match
849
- return result
850
- end
851
-
852
- # Run a diskpart script and get the exit code and text output. See also
853
- # technet and search for "DiskPart Command-Line Options" or else
854
- # "http://technet.microsoft.com/en-us/library/cc766465%28WS.10%29.aspx".
855
- # Note that there are differences between 2003 and 2008 server versions
856
- # of this utility.
857
- #
858
- # === Parameters
859
- # script(String):: diskpart script with commands delimited by newlines
860
- #
861
- # === Return
862
- # result(Array):: tuple of [exit_code, output_text]
863
- def run_script(script)
864
- Dir.mktmpdir do |temp_dir_path|
865
- script_file_path = File.join(temp_dir_path, "rs_diskpart_script.txt")
866
- File.open(script_file_path, "w") { |f| f.puts(script.strip) }
867
- executable_path = "diskpart.exe"
868
- executable_arguments = ["/s", File.normalize_path(script_file_path)]
869
- shell = RightScale::Platform.shell
870
- executable_path, executable_arguments = shell.format_right_run_path(executable_path, executable_arguments)
871
- command = shell.format_executable_command(executable_path, executable_arguments)
872
- output_text = `#{command}`
873
- return $?.exitstatus, output_text
874
- end
875
- end
876
-
877
- # Determines if the given value is empty and returns nil in that case.
878
- #
879
- # === Parameters
880
- # value(String):: value to chec
881
- #
882
- # === Return
883
- # result(String):: trimmed value or nil
884
- def nil_if_empty(value)
885
- value = value.strip
886
- return nil if value.empty?
887
- return value
888
- end
889
-
890
- # Multiplies a raw size value by a size factor given as a standardized
891
- # bytes acronym.
892
- #
893
- # === Parameters
894
- # size_by(String or Number):: value to multiply
895
- # size_factor(String):: multiplier acronym
896
- #
897
- # === Return
898
- # result(int):: bytes
899
- def size_factor_to_bytes(size_by, size_factor)
900
- value = size_by.to_i
901
- case size_factor
902
- when 'KB' then return value * 1024
903
- when 'MB' then return value * 1024 * 1024
904
- when 'GB' then return value * 1024 * 1024 * 1024
905
- when 'TB' then return value * 1024 * 1024 * 1024 * 1024
906
- else return value # assume bytes
907
- end
908
- end
909
-
910
- # Returns the correct diskpart assignment command for the specified device (either drive letter, or path)
911
- #
912
- # === Parameters
913
- # device(String):: Either a drive letter or mount path
914
- #
915
- # === Return
916
- # result(String):: The correct diskpart assignment command
917
- def get_assign_command_for_device(device)
918
- if device.match(@assignable_disk_regex)
919
- "assign letter=#{device[0,1]}"
920
- elsif device.match(@assignable_path_regex)
921
- "assign mount=\"#{device}\""
922
- end
923
- end
924
-
925
- # Returns the correct 'online disk' diskpart command based on the OS version
926
- #
927
- # === Return
928
- # result(String):: Either "online noerr" or "online disk noerr" depending upon the OS version
929
- def get_online_disk_command()
930
- if @os_info.major < 6; "online noerr" else; "online disk noerr" end
931
- end
932
-
933
- # Returns the correct 'attribute disk clear readonly' diskpart command based on the OS version
934
- #
935
- # === Parameters
936
- # object_type(String):: One of "disk" or "volume" to clear read only for
937
- #
938
- # === Return
939
- # result(String):: Either a blank string or "attribute #{object_type} clear readonly noerr" depending upon the OS version
940
- def get_clear_readonly_command(object_type)
941
- if @os_info.major < 6; "" else; "attribute #{object_type} clear readonly noerr" end
942
- end
943
-
944
- end # VolumeManager
945
-
946
- # Provides utilities for formatting executable shell commands, etc.
947
- class Shell
948
- POWERSHELL_V1x0_EXECUTABLE_PATH = "powershell.exe"
949
- POWERSHELL_V1x0_SCRIPT_EXTENSION = ".ps1"
950
- RUBY_SCRIPT_EXTENSION = ".rb"
951
- NULL_OUTPUT_NAME = "nul"
952
-
953
- @@executable_extensions = nil
954
- @@right_run_path = nil
955
-
956
- # Formats an executable path and arguments by inserting a reference to
957
- # RightRun.exe on platforms only when necessary.
958
- #
959
- # === Parameters
960
- # executable_path(String):: 64-bit executable path
961
- # executable_arguments(Array):: arguments for 64-bit executable
962
- #
963
- # === Return
964
- # result(Array):: tuple for updated [executable_path, executable_arguments]
965
- def format_right_run_path(executable_path, executable_arguments)
966
- if @@right_run_path.nil?
967
- @@right_run_path = ""
968
- if ENV['ProgramW6432'] && (@@right_run_path = ENV['RS_RIGHT_RUN_EXE'].to_s).empty?
969
- temp_path = File.join(ENV['ProgramW6432'], 'RightScale', 'Shared', 'RightRun.exe')
970
- if File.file?(temp_path)
971
- @@right_run_path = File.normalize_path(temp_path).gsub("/", "\\")
972
- end
973
- end
974
- end
975
- unless @@right_run_path.empty?
976
- executable_arguments.unshift(executable_path)
977
- executable_path = @@right_run_path
978
- end
979
-
980
- return executable_path, executable_arguments
981
- end
982
-
983
- # Formats a script file name to ensure it is executable on the current
984
- # platform.
985
- #
986
- # === Parameters
987
- # partial_script_file_path(String):: full or partial script file path
988
- #
989
- # default_extension(String):: default script extension for platforms
990
- # which require a known file extension to execute.
991
- #
992
- # === Returns
993
- # executable_script_file_path(String):: executable path
994
- def format_script_file_name(partial_script_file_path, default_extension = POWERSHELL_V1x0_SCRIPT_EXTENSION)
995
- extension = File.extname(partial_script_file_path)
996
- if 0 == extension.length
997
- return partial_script_file_path + default_extension
998
- end
999
-
1000
- # quick out for default extension.
1001
- if 0 == (extension <=> default_extension)
1002
- return partial_script_file_path
1003
- end
1004
-
1005
- # confirm that the "extension" is really something understood by
1006
- # the command shell as being executable.
1007
- if @@executable_extensions.nil?
1008
- @@executable_extensions = ENV['PATHEXT'].downcase.split(';')
1009
- end
1010
- if @@executable_extensions.include?(extension.downcase)
1011
- return partial_script_file_path
1012
- end
1013
-
1014
- # not executable; use default extension.
1015
- return partial_script_file_path + default_extension
1016
- end
1017
-
1018
- # Formats an executable command by quoting any of the arguments as
1019
- # needed and building an executable command string.
1020
- #
1021
- # === Parameters
1022
- # executable_file_path(String):: full or partial executable file path
1023
- #
1024
- # arguments(Array):: variable stringizable arguments
1025
- #
1026
- # === Returns
1027
- # executable_command(String):: executable command string
1028
- def format_executable_command(executable_file_path, *arguments)
1029
- escaped = []
1030
- [executable_file_path, arguments].flatten.each do |arg|
1031
- value = arg.to_s
1032
- escaped << (value.index(' ') ? "\"#{value}\"" : value)
1033
- end
1034
-
1035
- # let cmd do the extension resolution if no extension was given
1036
- ext = File.extname(executable_file_path)
1037
- if ext.nil? || ext.empty?
1038
- "cmd.exe /C \"#{escaped.join(" ")}\""
1039
- else
1040
- escaped.join(" ")
1041
- end
1042
- end
1043
-
1044
- # Formats a powershell command using the given script path and arguments.
1045
- # Allows for specifying powershell from a specific installed location.
1046
- # This method is only implemented for Windows.
1047
- #
1048
- # === Parameters
1049
- # shell_script_file_path(String):: shell script file path
1050
- # arguments(Array):: variable stringizable arguments
1051
- #
1052
- # === Returns
1053
- # executable_command(string):: executable command string
1054
- def format_powershell_command(shell_script_file_path, *arguments)
1055
- return format_powershell_command4(POWERSHELL_V1x0_EXECUTABLE_PATH, nil, nil, shell_script_file_path, *arguments)
1056
- end
1057
-
1058
- # Formats a ruby command using the given script path and arguments.
1059
- # This method is only implemented for Windows since ruby scripts on
1060
- # linux should rely on shebang to indicate the ruby bin path.
1061
- #
1062
- # === Parameters
1063
- # shell_script_file_path(String):: shell script file path
1064
- # arguments(Array):: variable stringizable arguments
1065
- #
1066
- # === Returns
1067
- # executable_command(string):: executable command string
1068
- def format_ruby_command(shell_script_file_path, *arguments)
1069
- return format_executable_command(sandbox_ruby, *([shell_script_file_path] + arguments))
1070
- end
1071
-
1072
- # Formats a powershell command using the given script path and arguments.
1073
- # Allows for specifying powershell from a specific installed location.
1074
- # This method is only implemented for Windows.
1075
- #
1076
- # === Parameters
1077
- # powershell_exe_path(String):: path to powershell executable
1078
- # shell_script_file_path(String):: shell script file path
1079
- # arguments(Array):: variable stringizable arguments
1080
- #
1081
- # === Returns
1082
- # executable_command(string):: executable command string
1083
- def format_powershell_command4(powershell_exe_path,
1084
- lines_before_script,
1085
- lines_after_script,
1086
- shell_script_file_path,
1087
- *arguments)
1088
- # special case for powershell scripts.
1089
- escaped = []
1090
- [shell_script_file_path, arguments].flatten.each do |arg|
1091
- value = arg.to_s
1092
- # note that literal ampersand must be quoted on the powershell command
1093
- # line because it otherwise means 'execute what follows'.
1094
- escaped << ((value.index(' ') || value.index('&')) ? "'#{value.gsub("'", "''")}'" : value)
1095
- end
1096
-
1097
- # resolve lines before & after script.
1098
- defaulted_lines_after_script = lines_after_script.nil?
1099
- lines_before_script ||= []
1100
- lines_after_script ||= []
1101
-
1102
- # execute powershell with RemoteSigned execution policy. the issue
1103
- # is that powershell by default will only run digitally-signed
1104
- # scripts.
1105
- # FIX: search for any attempt to alter execution policy in lines
1106
- # before insertion.
1107
- # FIX: support digitally signed scripts and/or signing on the fly by
1108
- # checking for a signature file side-by-side with script.
1109
- lines_before_script.insert(0, "set-executionpolicy -executionPolicy RemoteSigned -Scope Process")
1110
-
1111
- # insert error checking only in case of defaulted "lines after script"
1112
- # to be backward compatible with existing scripts.
1113
- if defaulted_lines_after_script
1114
- # ensure for a generic powershell script that any errors left in the
1115
- # global $Error list are noted and result in script failure. the
1116
- # best practice is for the script to handle errors itself (and clear
1117
- # the $Error list if necessary), so this is a catch-all for any
1118
- # script which does not handle errors "properly".
1119
- lines_after_script << "if ($NULL -eq $LastExitCode) { $LastExitCode = 0 }"
1120
- 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 }"
1121
- end
1122
-
1123
- # ensure last exit code gets marshalled.
1124
- marshall_last_exit_code_cmd = "exit $LastExitCode"
1125
- if defaulted_lines_after_script || (lines_after_script.last != marshall_last_exit_code_cmd)
1126
- lines_after_script << marshall_last_exit_code_cmd
1127
- end
1128
-
1129
- # format powershell command string.
1130
- powershell_command = "&{#{lines_before_script.join("; ")}; &#{escaped.join(" ")}; #{lines_after_script.join("; ")}}"
1131
-
1132
- # in order to run 64-bit powershell from this 32-bit ruby process, we need to launch it using
1133
- # our special RightRun utility from program files, if it is installed (it is not installed for
1134
- # 32-bit instances and perhaps not for test/dev environments).
1135
- executable_path = powershell_exe_path
1136
- executable_arguments = ["-command", powershell_command]
1137
- executable_path, executable_arguments = format_right_run_path(executable_path, executable_arguments)
1138
-
1139
- # combine command string with powershell executable and arguments.
1140
- return format_executable_command(executable_path, executable_arguments)
1141
- end
1142
-
1143
- # Formats a shell command using the given script path and arguments.
1144
- #
1145
- # === Parameters
1146
- # shell_script_file_path(String):: shell script file path
1147
- # arguments(Array):: variable stringizable arguments
1148
- #
1149
- # === Returns
1150
- # executable_command(string):: executable command string
1151
- def format_shell_command(shell_script_file_path, *arguments)
1152
- # special case for powershell scripts and ruby scripts (because we
1153
- # don't necessarily setup the association for .rb with our sandbox
1154
- # ruby in the environment).
1155
- extension = File.extname(shell_script_file_path)
1156
- unless extension.to_s.empty?
1157
- if 0 == POWERSHELL_V1x0_SCRIPT_EXTENSION.casecmp(extension)
1158
- return format_powershell_command(shell_script_file_path, *arguments)
1159
- elsif 0 == RUBY_SCRIPT_EXTENSION.casecmp(extension)
1160
- return format_ruby_command(shell_script_file_path, *arguments)
1161
- end
1162
- end
1163
-
1164
- # execution is based on script extension (.bat, .cmd, .js, .vbs, etc.)
1165
- return format_executable_command(shell_script_file_path, *arguments)
1166
- end
1167
-
1168
- # Formats a command string to redirect stdout to the given target.
1169
- #
1170
- # === Parameters
1171
- # cmd(String):: executable command string
1172
- #
1173
- # target(String):: target file (optional, defaults to nul redirection)
1174
- def format_redirect_stdout(cmd, target = NULL_OUTPUT_NAME)
1175
- return cmd + " 1>#{target}"
1176
- end
1177
-
1178
- # Formats a command string to redirect stderr to the given target.
1179
- #
1180
- # === Parameters
1181
- # cmd(String):: executable command string
1182
- #
1183
- # target(String):: target file (optional, defaults to nul redirection)
1184
- def format_redirect_stderr(cmd, target = NULL_OUTPUT_NAME)
1185
- return cmd + " 2>#{target}"
1186
- end
1187
-
1188
- # Formats a command string to redirect both stdout and stderr to the
1189
- # given target.
1190
- #
1191
- # === Parameters
1192
- # cmd(String):: executable command string
1193
- #
1194
- # target(String):: target file (optional, defaults to nul redirection)
1195
- def format_redirect_both(cmd, target = NULL_OUTPUT_NAME)
1196
- return cmd + " 1>#{target} 2>&1"
1197
- end
1198
-
1199
- # Returns path to sandbox ruby executable.
1200
- def sandbox_ruby
1201
- unless @sandbox_ruby
1202
- @sandbox_ruby = ENV['RS_RUBY_EXE'] ||
1203
- File.normalize_path(File.join(RightScale::Platform.filesystem.sandbox_dir, 'Ruby', 'bin', 'ruby.exe'))
1204
- end
1205
- @sandbox_ruby
1206
- end
1207
-
1208
- # Gets the current system uptime.
1209
- #
1210
- # === Return
1211
- # the time the machine has been up in seconds, 0 if there was an error.
1212
- def uptime
1213
- begin
1214
- return Time.now.to_i.to_f - booted_at.to_f
1215
- rescue Exception
1216
- return 0.0
1217
- end
1218
- end
1219
-
1220
- # Gets the time at which the system was booted
1221
- #
1222
- # === Return
1223
- # the UTC timestamp at which the system was booted
1224
- def booted_at
1225
- begin
1226
- Dir.mktmpdir do |temp_dir_path|
1227
- Dir.chdir(temp_dir_path) do
1228
- wmic_output = `echo | wmic OS Get LastBootUpTime`
1229
- end
1230
- end
1231
-
1232
- match = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.\d{6}([+-]\d{3})/.match(wmic_output)
1233
-
1234
- year, mon, day, hour, min, sec, tz = match[1..-1]
1235
-
1236
- #Convert timezone from [+-]mmm to [+-]hh:mm
1237
- tz = "#{tz[0...1]}#{(tz.to_i.abs / 60).to_s.rjust(2,'0')}:#{(tz.to_i.abs % 60).to_s.rjust(2,'0')}"
1238
-
1239
- #Finally, parse the WMIC output as an XML-schema time, which is the only reliable
1240
- #way to parse a time with arbitrary zone in Ruby (?!)
1241
- return Time.xmlschema("#{year}-#{mon}-#{day}T#{hour}:#{min}:#{sec}#{tz}").to_i
1242
- rescue Exception
1243
- return nil
1244
- end
1245
- end
1246
-
1247
- end # Shell
1248
-
1249
- class Controller
1250
- include ::Windows::Process
1251
- include ::Windows::Error
1252
- include ::Windows::Handle
1253
- include ::Windows::Security
1254
-
1255
- @@initiate_system_shutdown_api = nil
1256
-
1257
- # Shutdown machine now
1258
- def shutdown
1259
- initiate_system_shutdown(false)
1260
- end
1261
-
1262
- # Reboot machine now
1263
- def reboot
1264
- initiate_system_shutdown(true)
1265
- end
1266
-
1267
- private
1268
-
1269
- def initiate_system_shutdown(reboot_after_shutdown)
1270
-
1271
- @@initiate_system_shutdown_api = Win32::API.new('InitiateSystemShutdown', 'PPLLL', 'B', 'advapi32') unless @@initiate_system_shutdown_api
1272
-
1273
- # get current process token.
1274
- token_handle = 0.chr * 4
1275
- unless OpenProcessToken(process_handle = GetCurrentProcess(),
1276
- desired_access = TOKEN_ADJUST_PRIVILEGES + TOKEN_QUERY,
1277
- token_handle)
1278
- raise get_last_error
1279
- end
1280
- token_handle = token_handle.unpack('V')[0]
1281
-
1282
- begin
1283
- # lookup shutdown privilege ID.
1284
- luid = 0.chr * 8
1285
- unless LookupPrivilegeValue(system_name = nil,
1286
- priviledge_name = 'SeShutdownPrivilege',
1287
- luid)
1288
- raise get_last_error
1289
- end
1290
- luid = luid.unpack('VV')
1291
-
1292
- # adjust token privilege to enable shutdown.
1293
- token_privileges = 0.chr * 16 # TOKEN_PRIVILEGES tokenPrivileges;
1294
- token_privileges[0,4] = [1].pack("V") # tokenPrivileges.PrivilegeCount = 1;
1295
- token_privileges[4,8] = luid.pack("VV") # tokenPrivileges.Privileges[0].Luid = luid;
1296
- token_privileges[12,4] = [SE_PRIVILEGE_ENABLED].pack("V") # tokenPrivileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
1297
- unless AdjustTokenPrivileges(token_handle,
1298
- disable_all_privileges = 0,
1299
- token_privileges,
1300
- new_state = 0,
1301
- previous_state = nil,
1302
- return_length = nil)
1303
- raise get_last_error
1304
- end
1305
- unless @@initiate_system_shutdown_api.call(machine_name = nil,
1306
- message = nil,
1307
- timeout_secs = 1,
1308
- force_apps_closed = 1,
1309
- reboot_after_shutdown ? 1 : 0)
1310
- raise get_last_error
1311
- end
1312
- ensure
1313
- CloseHandle(token_handle)
1314
- end
1315
- true
1316
- end
1317
-
1318
- end # Controller
1319
-
1320
- class Rng
1321
- def pseudorandom_bytes(count)
1322
- bytes = ''
1323
- count.times do
1324
- bytes << rand(0xff)
1325
- end
1326
-
1327
- bytes
1328
- end
1329
- end
1330
-
1331
- class Process
1332
- include ::Windows::Process
1333
-
1334
- @@get_process_memory_info = nil
1335
-
1336
- # see PROCESS_MEMORY_COUNTERS structure: "http://msdn.microsoft.com/en-us/library/ms684877%28VS.85%29.aspx"
1337
- SIZEOF_PROCESS_MEMORY_COUNTERS = 10 * 4
1338
-
1339
- # queries resident set size (current working set size in Windows).
1340
- #
1341
- # === Parameters
1342
- # pid(Fixnum):: process ID or nil for current process
1343
- #
1344
- # === Return
1345
- # result(Fixnum):: current set size in KB
1346
- def resident_set_size(pid=nil)
1347
- @@get_process_memory_info = ::Win32::API.new("GetProcessMemoryInfo", 'LPL', 'B', 'psapi') unless @@get_process_memory_info
1348
-
1349
- # FIX: call OpenProcess and ensure proper access and close if given PID.
1350
- raise NotImplementedError.new("pid != nil not yet implemented") if pid
1351
- process_handle = GetCurrentProcess()
1352
- process_memory_counters = "\0" * SIZEOF_PROCESS_MEMORY_COUNTERS
1353
- result = @@get_process_memory_info.call(process_handle, process_memory_counters, process_memory_counters.size)
1354
- # note that the 'B' return type is a Fixnum (i.e. not TrueClass or FalseClass) of 'zero' on failure or 'non-zero' on success
1355
- raise ::RightScale::Win32Error.new("Failed to get resident set size for process") if 0 == result
1356
-
1357
- # current .WorkingSetSize (bytes) is equivalent of Linux' ps resident set size (KB)
1358
- return process_memory_counters[12..16].unpack("L")[0] / 1024 # bytes to KB
1359
- end
1360
- end
1361
-
1362
- class Installer
1363
- def install(packages)
1364
- raise ::RightScale::Win32Error.new("Not implemented yet")
1365
- end
1366
- end
1367
-
1368
- # internal class for querying OS version, etc.
1369
- class OSInformation
1370
- include ::Windows::SystemInfo
1371
-
1372
- attr_reader :version, :major, :minor, :build
1373
-
1374
- def initialize
1375
- @version = GetVersion()
1376
- @major = LOBYTE(LOWORD(version))
1377
- @minor = HIBYTE(LOWORD(version))
1378
- @build = HIWORD(version)
1379
- end
1380
- end
1381
-
1382
- end # Platform
1383
-
1384
- end # RightScale