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
@@ -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