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