right_agent 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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