right_agent 0.17.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. data/lib/right_agent.rb +0 -1
  2. data/lib/right_agent/agent_config.rb +1 -1
  3. data/lib/right_agent/minimal.rb +8 -7
  4. data/lib/right_agent/monkey_patches.rb +4 -2
  5. data/lib/right_agent/monkey_patches/ruby_patch.rb +9 -9
  6. data/lib/right_agent/monkey_patches/ruby_patch/linux_patch/file_patch.rb +2 -2
  7. data/lib/right_agent/monkey_patches/ruby_patch/windows_patch/file_patch.rb +21 -51
  8. data/lib/right_agent/packets.rb +5 -1
  9. data/lib/right_agent/platform.rb +727 -299
  10. data/lib/right_agent/platform/unix/darwin/platform.rb +102 -0
  11. data/lib/right_agent/platform/unix/linux/platform.rb +305 -0
  12. data/lib/right_agent/platform/unix/platform.rb +226 -0
  13. data/lib/right_agent/platform/windows/mingw/platform.rb +447 -0
  14. data/lib/right_agent/platform/windows/mswin/platform.rb +236 -0
  15. data/lib/right_agent/platform/windows/platform.rb +1808 -0
  16. data/right_agent.gemspec +13 -8
  17. data/spec/platform/spec_helper.rb +216 -0
  18. data/spec/platform/unix/darwin/platform_spec.rb +181 -0
  19. data/spec/platform/unix/linux/platform_spec.rb +540 -0
  20. data/spec/platform/unix/spec_helper.rb +149 -0
  21. data/spec/platform/windows/mingw/platform_spec.rb +222 -0
  22. data/spec/platform/windows/mswin/platform_spec.rb +259 -0
  23. data/spec/platform/windows/spec_helper.rb +720 -0
  24. metadata +45 -30
  25. data/lib/right_agent/platform/darwin.rb +0 -285
  26. data/lib/right_agent/platform/linux.rb +0 -537
  27. data/lib/right_agent/platform/windows.rb +0 -1384
  28. data/spec/platform/darwin_spec.rb +0 -13
  29. data/spec/platform/linux_spec.rb +0 -38
  30. data/spec/platform/linux_volume_manager_spec.rb +0 -201
  31. data/spec/platform/platform_spec.rb +0 -80
  32. data/spec/platform/windows_spec.rb +0 -13
  33. data/spec/platform/windows_volume_manager_spec.rb +0 -318
data/lib/right_agent.rb CHANGED
@@ -26,7 +26,6 @@ require File.expand_path(File.join(File.dirname(__FILE__), 'right_agent', 'minim
26
26
  require 'json'
27
27
  require 'openssl'
28
28
  require 'right_amqp'
29
- require 'right_support'
30
29
 
31
30
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'monkey_patches'))
32
31
  require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'payload_formatter'))
@@ -63,7 +63,7 @@ module RightScale
63
63
  module AgentConfig
64
64
 
65
65
  # Current agent protocol version
66
- PROTOCOL_VERSION = 21
66
+ PROTOCOL_VERSION = 22
67
67
 
68
68
  # Current agent protocol version
69
69
  #
@@ -23,13 +23,14 @@
23
23
  require 'rubygems'
24
24
  require 'eventmachine'
25
25
  require 'fileutils'
26
+ require 'right_support'
26
27
  require 'yaml'
27
28
 
28
29
  # load definition for File.normalize_path, etc.
29
- require File.expand_path(File.join(File.dirname(__FILE__), 'platform'))
30
+ require ::File.expand_path('../monkey_patches', __FILE__)
30
31
 
31
32
  unless defined?(RIGHT_AGENT_BASE_DIR)
32
- RIGHT_AGENT_BASE_DIR = File.normalize_path(File.dirname(__FILE__))
33
+ RIGHT_AGENT_BASE_DIR = ::File.normalize_path('..', __FILE__)
33
34
  end
34
35
 
35
36
  # require minimal gems needed to create a CommandClient and send a command.
@@ -37,8 +38,8 @@ end
37
38
  # FIX: agent_controller is currently the only minimal-load use case so these
38
39
  # requires are oriented toward that. any additional use cases may require a
39
40
  # rethink of minimal loading.
40
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'agent_config'))
41
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'command'))
42
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'log'))
43
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'pid_file'))
44
- require File.normalize_path(File.join(RIGHT_AGENT_BASE_DIR, 'serialize', 'serializable'))
41
+ require ::File.normalize_path('agent_config', RIGHT_AGENT_BASE_DIR)
42
+ require ::File.normalize_path('command', RIGHT_AGENT_BASE_DIR)
43
+ require ::File.normalize_path('log', RIGHT_AGENT_BASE_DIR)
44
+ require ::File.normalize_path('pid_file', RIGHT_AGENT_BASE_DIR)
45
+ require ::File.normalize_path('serialize/serializable', RIGHT_AGENT_BASE_DIR)
@@ -23,6 +23,8 @@
23
23
  require 'rubygems'
24
24
  require 'rbconfig'
25
25
 
26
- MONKEY_PATCHES_BASE_DIR = File.join(File.dirname(__FILE__), 'monkey_patches')
26
+ unless defined?(RIGHT_AGENT_MONKEY_PATCHES_BASE_DIR)
27
+ require ::File.expand_path('../monkey_patches/ruby_patch', __FILE__)
27
28
 
28
- require File.normalize_path(File.join(MONKEY_PATCHES_BASE_DIR, 'ruby_patch'))
29
+ RIGHT_AGENT_MONKEY_PATCHES_BASE_DIR = ::File.normalize_path('../monkey_patches', __FILE__)
30
+ end
@@ -32,24 +32,24 @@ require 'socket'
32
32
  # using short path. Since this is where we define the File.normalize_path
33
33
  # method to alleviate this issue, we have a chicken & egg problem. So detect if
34
34
  # we already required this file and skip the rest if that was the case.
35
- unless defined?(RUBY_PATCH_BASE_DIR)
35
+ unless defined?(RIGHT_AGENT_RUBY_PATCH_BASE_DIR)
36
36
 
37
37
  # Load platform-specific patches before any other patches (in order to define
38
38
  # File.normalize_path, etc.)
39
- case (family = RbConfig::CONFIG['host_os'])
39
+ case ::RbConfig::CONFIG['host_os']
40
40
  when /mswin|win32|dos|mingw|cygwin/i
41
- require File.expand_path(File.join(File.dirname(__FILE__), 'ruby_patch', 'windows_patch'))
41
+ require ::File.expand_path('../ruby_patch/windows_patch', __FILE__)
42
42
  when /linux/i
43
- require File.expand_path(File.join(File.dirname(__FILE__), 'ruby_patch', 'linux_patch'))
43
+ require ::File.expand_path('../ruby_patch/linux_patch', __FILE__)
44
44
  when /darwin/i
45
- require File.expand_path(File.join(File.dirname(__FILE__), 'ruby_patch', 'darwin_patch'))
45
+ require ::File.expand_path('../ruby_patch/darwin_patch', __FILE__)
46
46
  else
47
- raise LoadError, "Unsupported platform: #{family}"
47
+ raise LoadError, "Unsupported platform: #{::RbConfig::CONFIG['host_os']}"
48
48
  end
49
49
 
50
- RUBY_PATCH_BASE_DIR = File.join(File.dirname(__FILE__), 'ruby_patch')
50
+ RIGHT_AGENT_RUBY_PATCH_BASE_DIR = ::File.normalize_path('../ruby_patch', __FILE__)
51
51
 
52
- require File.normalize_path(File.join(RUBY_PATCH_BASE_DIR, 'array_patch'))
53
- require File.normalize_path(File.join(RUBY_PATCH_BASE_DIR, 'object_patch'))
52
+ require File.join(RIGHT_AGENT_RUBY_PATCH_BASE_DIR, 'array_patch')
53
+ require File.join(RIGHT_AGENT_RUBY_PATCH_BASE_DIR, 'object_patch')
54
54
 
55
55
  end # Unless already defined
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2011 RightScale Inc
2
+ # Copyright (c) 2011-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -24,7 +24,7 @@ class File
24
24
 
25
25
  # On *nix systems, resolves to File.expand_path
26
26
  def self.normalize_path(file_name, *dir_string)
27
- File.expand_path(file_name, *dir_string)
27
+ self.expand_path(file_name, *dir_string)
28
28
  end
29
29
 
30
30
  end
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2011 RightScale Inc
2
+ # Copyright (c) 2011-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -23,68 +23,38 @@
23
23
  require 'rubygems'
24
24
 
25
25
  begin
26
- require 'windows/file'
26
+
27
+ # attempt to early-load basic Windows API gems so that we can rescue and
28
+ # switch to using the simple definition of File.normalize_path when these gems
29
+ # are unavailable.
30
+ require ::File.expand_path('../../../../platform', __FILE__)
27
31
 
28
32
  class File
29
33
 
30
- # converts a long path to a short path. in windows terms, this means
31
- # taking any file/folder name over 8 characters in length and truncating
32
- # it to 6 characters with ~1..~n appended depending on how many similar
33
- # names exist in the same directory. file extensions are simply chopped
34
- # at three letters. the short name is equivalent for all API calls to
35
- # the long path but requires no special quoting, etc. the path must
36
- # exist at least partially for the API call to succeed.
34
+ # Expand the path then shorten the directory, if possible.
35
+ # Only shortens the parent directory and not the leaf (file or directory)
36
+ # name because 'gem' wants the file name to be long for require.
37
37
  #
38
- # === Parameters
39
- # long_path(String):: fully or partially existing long path to be
40
- # converted to its short path equivalent.
38
+ # @param [String] file_name to normalize
39
+ # @param [String] dir_string as base directory path for relative file_name
40
+ # or ignored when file_name is absolute (Default = working directory).
41
41
  #
42
- # === Return
43
- # short_path(String):: short path equivalent or same path if non-existent
44
- def self.long_path_to_short_path(long_path)
45
- if File.exists?(long_path)
46
- length = 260
47
- while true
48
- buffer = 0.chr * length
49
- length = ::Windows::File::GetShortPathName.call(long_path, buffer, buffer.length)
50
- if length < buffer.length
51
- break
52
- end
53
- end
54
- return buffer.unpack('A*').first.gsub("\\", "/")
55
- else
56
- # must get short path for any existing ancestor since child doesn't
57
- # (currently) exist.
58
- child_name = File.basename(long_path)
59
- long_parent_path = File.dirname(long_path)
60
-
61
- # note that root dirname is root itself (at least in windows)
62
- return long_path if long_path == long_parent_path
63
-
64
- # recursion
65
- short_parent_path = long_path_to_short_path(File.dirname(long_path))
66
- return File.join(short_parent_path, child_name)
67
- end
68
- end
69
-
70
- # First expand the path then shorten the directory.
71
- # Only shorten the directory and not the file name because 'gem' wants
72
- # long file names
42
+ # @return [String] normalized path
73
43
  def self.normalize_path(file_name, *dir_string)
74
- path = File.expand_path(file_name, *dir_string)
75
- dir = self.long_path_to_short_path(File.dirname(path))
76
- File.join(dir, File.basename(path))
44
+ path = self.expand_path(file_name, *dir_string)
45
+ dir = ::RightScale::Platform.filesystem.long_path_to_short_path(self.dirname(path))
46
+ self.join(dir, self.basename(path))
77
47
  end
78
-
79
48
  end
80
49
 
81
50
  rescue LoadError
82
- # use the simple definition of normalize_path to avoid breaking code which
83
- # depends on having this definition.
51
+
52
+ # use the simple definition of normalize_path on load error. the purpose of
53
+ # normalize_path is to disambiguate load paths but it is possible to continue
54
+ # with ambiguity in most cases and any other Win32 API calls will fail.
84
55
  class File
85
56
  def self.normalize_path(file_name, *dir_string)
86
- File.expand_path(file_name, *dir_string)
57
+ self.expand_path(file_name, *dir_string)
87
58
  end
88
59
  end
89
-
90
60
  end
@@ -24,6 +24,10 @@
24
24
  module JSON
25
25
  class << self
26
26
  def parse(source, opts = {})
27
+ # In gem version this options is set to true by default
28
+ # but in ruby native json library is set to false by default
29
+ # Explicitly set it to true so both json libraries act the same
30
+ opts[:create_additions] = true
27
31
  if source =~ /(.*)json_class":"Nanite::(.*)/
28
32
  JSON.parser.new( Regexp.last_match(1) + 'json_class":"RightScale::' + Regexp.last_match(2), opts).parse
29
33
  else
@@ -248,7 +252,7 @@ module RightScale
248
252
  def trace
249
253
  audit_id = self.respond_to?(:payload) && payload.is_a?(Hash) && (payload['audit_id'] || payload[:audit_id])
250
254
  tok = self.respond_to?(:token) && token
251
- tr = "<#{audit_id || nil}> <#{tok}>"
255
+ tr = "<#{audit_id || nil}> <#{tok}>"
252
256
  end
253
257
 
254
258
  # Retrieve protocol version of original creator of packet
@@ -1,5 +1,5 @@
1
1
  #
2
- # Copyright (c) 2009-2011 RightScale Inc
2
+ # Copyright (c) 2009-2013 RightScale Inc
3
3
  #
4
4
  # Permission is hereby granted, free of charge, to any person obtaining
5
5
  # a copy of this software and associated documentation files (the
@@ -26,317 +26,745 @@
26
26
  # we already required this file and skip the rest if that was the case.
27
27
  unless defined?(RightScale::Platform)
28
28
 
29
- # Note that the platform-specific submodules will be loaded on demand to resolve
30
- # some install-time gem dependency issues.
31
-
32
- require 'rbconfig'
33
- require 'right_support'
34
-
35
-
36
- # Load ruby interpreter monkey-patches first (to ensure File.normalize_path is defined, etc.).
37
- require File.expand_path(File.join(File.dirname(__FILE__), 'monkey_patches', 'ruby_patch'))
38
-
39
- module RightScale
40
- # Throw when the current platform is not supported for some reason
41
- class PlatformNotSupported < Exception; end
42
-
43
- # A utility class that provides information about the platform on which the RightAgent is running
44
- # Available information includes:
45
- # - which flavor cloud (EC2, Rackspace, Eucalyptus, ..)
46
- # - which flavor operating system (Linux, Windows or Mac)
47
- # - which OS release (a numeric value that is specific to the OS)
48
- # - directories in which various bits of RightScale state may be found
49
- # - platform-specific information such as Linux flavor or release
50
- #
51
- # For platform information only used in specific contexts, the dispatch method may be used
52
- #
53
- # This is a summary of the information you can query by calling Platform's instance methods:
54
- # - .flavor
55
- # - .release
56
- # - .linux?
57
- # - .darwin?
58
- # - .windows?
59
- # - .ec2?
60
- # - .rackspace?
61
- # - .eucalyptus?
62
- # - .filesystem
63
- # - right_scale_state_dir
64
- # - spool_dir
65
- # - cache_dir
66
- # - .linux (only available under Linux)
67
- # - ubuntu?
68
- # - centos?
69
- # - suse?
70
- class Platform
71
-
72
- include RightSupport::Ruby::EasySingleton
73
-
74
- # Generic platform family
75
- #
76
- # === Return
77
- # family(Symbol):: One of :linux, :windows or :darwin
78
- def family
79
- @family ||= case RbConfig::CONFIG['host_os']
80
- when /mswin|win32|dos|mingw|cygwin/i then :windows
81
- when /darwin/i then :darwin
82
- when /linux/i then :linux
83
- end
84
- end
85
-
86
- # Is current platform linux?
87
- #
88
- # === Return
89
- # true:: If current platform is linux
90
- # false:: Otherwise
91
- def linux?
92
- return family == :linux
93
- end
94
-
95
- # Is current platform darwin?
96
- #
97
- # === Return
98
- # true:: If current platform is darwin
99
- # false:: Otherwise
100
- def darwin?
101
- return family == :darwin
102
- end
103
- # Is current platform windows?
104
- #
105
- # === Return
106
- # true:: If current platform is Windows
107
- # false:: Otherwise
108
- def windows?
109
- return family == :windows
110
- end
111
-
112
- # Call platform specific implementation of method whose symbol is returned
113
- # by the passed in block. Arguments are passed through.
114
- # e.g.
115
- #
116
- # Platform.dispatch(2) { :echo }
117
- #
118
- # will result in 'echo_linux(2)' being executed in self if running on linux,
119
- # 'echo_windows(2)' if running on Windows and 'echo_darwin(2)' if on Mac OS X.
120
- # Note that the method is run in the instance of the caller.
121
- #
122
- # === Parameters
123
- # args:: Pass-through arguments
124
- #
125
- # === Block
126
- # Given block should not take any argument and return a symbol for the
127
- # method that should be called
128
- #
129
- # === Return
130
- # res(Object):: Result returned by platform specific implementation
131
- def dispatch(*args, &blk)
132
- raise "Platform.dispatch requires a block" unless blk
133
- binding = blk.binding.eval('self')
134
- meth = blk.call
135
- target = dispatch_candidates(meth).detect do |candidate|
136
- binding.respond_to?(candidate)
137
- end
138
- raise "No platform dispatch target found in #{binding.class} for " +
139
- "'#{meth.inspect}', tried " + dispatch_candidates(meth).join(', ') unless target
140
- binding.__send__(target, *args)
141
- end
142
-
143
- # Load platform specific implementation
144
- #
145
- # === Return
146
- # true:: Always return true
147
- def load_platform_specific
148
- if linux?
149
- require_linux
150
- elsif darwin?
151
- require_darwin
152
- elsif windows?
153
- require_windows
154
- else
155
- raise PlatformError.new('Unknown platform')
156
- end
157
- end
158
-
159
- # Are we in an EC2 cloud?
160
- #
161
- # === Return
162
- # true:: If machine is located in an EC2 cloud
163
- # false:: Otherwise
164
- def ec2?
165
- resolve_cloud_type if @ec2.nil?
166
- @ec2
167
- end
168
-
169
- # Are we in a Rackspace cloud?
170
- #
171
- # === Return
172
- # true:: If machine is located in an Rackspace cloud
173
- # false:: Otherwise
174
- def rackspace?
175
- resolve_cloud_type if @rackspace.nil?
176
- @rackspace
177
- end
178
-
179
- # Are we in a Eucalyptus cloud?
29
+ # Note that the platform-specific submodules will be loaded on demand to resolve
30
+ # some install-time gem dependency issues.
31
+ require 'rubygems'
32
+ require 'rbconfig'
33
+ require 'right_support'
34
+
35
+ require ::File.expand_path('../exceptions', __FILE__)
36
+
37
+ module RightScale
38
+
39
+ # A utility class that provides information about the platform on which the
40
+ # RightAgent is running.
180
41
  #
181
- # === Return
182
- # true:: If machine is located in an Eucalyptus cloud
183
- # false:: Otherwise
184
- def eucalyptus?
185
- resolve_cloud_type if @eucalyptus.nil?
186
- @eucalyptus
187
- end
188
-
189
- # Controller object
42
+ # Available information includes:
43
+ # - which flavor cloud (EC2, Rackspace, Eucalyptus, ..)
44
+ # - which flavor operating system (Linux, Windows or Mac)
45
+ # - which OS release (a numeric value that is specific to the OS)
46
+ # - directories in which various bits of RightScale state may be found
47
+ # - platform-specific information such as Linux flavor or release
190
48
  #
191
- # === Return
192
- # (Controller):: Platform-specific controller object
193
- def controller
194
- platform_service(:controller)
195
- end
49
+ # For platform information only used in specific contexts, the dispatch
50
+ # method may be used.
51
+ class Platform
196
52
 
197
- # Filesystem config object
198
- #
199
- # === Return
200
- # (Filesystem):: Platform-specific filesystem config object
201
- def filesystem
202
- platform_service(:filesystem)
203
- end
53
+ include RightSupport::Ruby::EasySingleton
204
54
 
205
- # VolumeManager config object
206
- #
207
- # === Return
208
- # (VolumeManager):: Platform-specific volume manager config object
209
- def volume_manager
210
- platform_service(:volume_manager)
211
- end
55
+ # exceptions
56
+ class CommandError < RightScale::Exceptions::PlatformError
57
+ attr_accessor :command
58
+ attr_accessor :status
59
+ attr_accessor :output_text
60
+ end
212
61
 
213
- # Shell information object
214
- #
215
- # === Return
216
- # (Object):: Platform-specific shell information object
217
- def shell
218
- platform_service(:shell)
219
- end
62
+ attr_reader :flavor, :release, :codename
63
+
64
+ # Generic platform family
65
+ #
66
+ # @deprecated family is a legacy definition, see genus/species for a more
67
+ # granular definition.
68
+ #
69
+ # @return [Symbol] result as one of :linux, :windows or :darwin
70
+ def family
71
+ warn "#{self.class.name}#family is deprecated, use genus or species instead.\n#{caller[0,2].join("\n")}"
72
+ (genus == :unix) ? species : genus
73
+ end
220
74
 
221
- # SSH information object
222
- #
223
- # === Return
224
- # (Object):: Platform-specific ssh object
225
- def ssh
226
- platform_service(:ssh)
227
- end
75
+ # @return [Symbol] platform genus
76
+ def genus
77
+ resolve_taxonomy unless @genus
78
+ @genus
79
+ end
228
80
 
229
- # Platform random number generator (RNG) facilities.
230
- #
231
- # === Return
232
- # (Object):: Platform-specific RNG object
233
- def rng
234
- platform_service(:rng)
235
- end
81
+ # @return [Symbol] platform species
82
+ def species
83
+ resolve_taxonomy unless @species
84
+ @species
85
+ end
236
86
 
237
- # Platform process facilities.
238
- #
239
- # === Return
240
- # (Object):: Platform-specific process facilities object
241
- def process
242
- platform_service(:process)
243
- end
244
-
245
- # Installer information object
246
- #
247
- # === Return
248
- # (Object):: Platform-specific installer information object
249
- def installer
250
- platform_service(:installer)
251
- end
252
-
253
- # Determines which cloud we're on by the cheap but simple expedient of
254
- # reading the RightScale cloud file
255
- def resolve_cloud_type
256
- cloud_type = read_cloud_file
257
- @ec2 = false
258
- @rackspace = false
259
- @eucalyptus = false
260
- case cloud_type
261
- when 'ec2' then @ec2 = true
262
- when 'rackspace' then @rackspace = true
263
- when 'eucalyptus' then @eucalyptus = true
264
- end
265
- end
266
-
267
- private
268
-
269
- # Load platform specific implementation
270
- def initialize
271
- require File.expand_path(File.join(File.dirname(__FILE__), 'platform', family.to_s))
272
- @filesystem = nil
273
- @shell = nil
274
- @ssh = nil
275
- @controller = nil
276
- @installer = nil
277
-
278
- @ec2 = nil
279
- @rackspace = nil
280
- @eucalyptus = nil
281
-
282
- init
283
-
284
- # Note that we must defer any use of filesystem until requested because
285
- # Windows setup scripts attempt to use Platform before installing some
286
- # of the required gems. Don't attempt to call code that requires gems in
287
- # initialize().
288
- end
289
-
290
- def require_linux
291
- require File.expand_path(File.join(File.dirname(__FILE__), 'platform', 'linux'))
292
- end
293
-
294
- def require_darwin
295
- require File.expand_path(File.join(File.dirname(__FILE__), 'platform', 'darwin'))
296
- end
297
-
298
- def require_windows
299
- require File.expand_path(File.join(File.dirname(__FILE__), 'platform', 'windows'))
300
- end
301
-
302
- # Reads the RightScale cloud file and returns its contents
303
- def read_cloud_file
304
- File.read(File.join(self.filesystem.right_scale_state_dir, 'cloud')) rescue nil
305
- end
306
-
307
- # Retrieve platform specific service implementation
308
- #
309
- # === Parameters
310
- # name(Symbol):: Service name, one of :filesystem, :shell, :ssh, :controller, :installer
311
- #
312
- # === Return
313
- # res(Object):: Service instance
314
- #
315
- # === Raise
316
- # RightScale::Exceptions::PlatformError:: If the service is not known
317
- def platform_service(name)
318
- instance_var = "@#{name.to_s}".to_sym
319
- const_name = name.to_s.camelize
87
+ # Is current platform in the Unix genus?
88
+ #
89
+ # @return [TrueClass|FalseClass] true if Unix
90
+ def unix?
91
+ genus == :unix
92
+ end
93
+
94
+ # Is current platform Linux?
95
+ #
96
+ # @return [TrueClass|FalseClass] true if Linux
97
+ def linux?
98
+ species == :linux
99
+ end
100
+
101
+ # Is current platform Darwin?
102
+ #
103
+ # @return [TrueClass|FalseClass] true if Darwin
104
+ def darwin?
105
+ species == :darwin
106
+ end
107
+
108
+ # Is current platform Windows?
109
+ #
110
+ # @return [TrueClass|FalseClass] true if Windows
111
+ def windows?
112
+ genus == :windows
113
+ end
114
+
115
+ # Are we on an EC2 cloud instance?
116
+ #
117
+ # @deprecated use right_link cloud libraries instead.
118
+ #
119
+ # @return [TrueClass|FalseClass] true if EC2
120
+ def ec2?
121
+ warn "#{self.class.name}#ec2? is deprecated, use right_link cloud libraries instead.\n#{caller[0,2].join("\n")}"
122
+ resolve_cloud_type unless @cloud_type
123
+ @cloud_type == 'ec2'
124
+ end
125
+
126
+ # Are we on an Rackspace cloud instance?
127
+ #
128
+ # @deprecated use right_link cloud libraries instead.
129
+ #
130
+ # @return [TrueClass|FalseClass] true if Rackspace
131
+ def rackspace?
132
+ warn "#{self.class.name}#rackspace? is deprecated, use right_link cloud libraries instead.\n#{caller[0,2].join("\n")}"
133
+ resolve_cloud_type unless @cloud_type
134
+ @cloud_type == 'rackspace'
135
+ end
136
+
137
+ # Are we on an Eucalyptus cloud instance?
138
+ #
139
+ # @deprecated use right_link cloud libraries instead.
140
+ #
141
+ # @return [TrueClass|FalseClass] true if Eucalyptus
142
+ def eucalyptus?
143
+ warn "#{self.class.name}#eucalyptus? is deprecated, use right_link cloud libraries instead.\n#{caller[0,2].join("\n")}"
144
+ resolve_cloud_type unless @cloud_type
145
+ @cloud_type == 'eucalyptus'
146
+ end
147
+
148
+ # Call platform specific implementation of method whose symbol is returned
149
+ # by the passed in block. Arguments are passed through.
150
+ # e.g.
151
+ #
152
+ # Platform.dispatch(2) { :echo }
153
+ #
154
+ # will result in 'echo_linux(2)' being executed in self if running on
155
+ # linux, 'echo_windows(2)' if running on Windows and 'echo_darwin(2)' if
156
+ # on Mac OS X. Note that the method is run in the instance of the caller.
157
+ #
158
+ # @param [Array] args as pass-through arguments
159
+ #
160
+ # @yield [] given block should not take any argument and return a symbol
161
+ # for the method that should be called.
162
+ #
163
+ # @return [Object] result of Platform-specific implementation
164
+ def dispatch(*args, &blk)
165
+ raise 'Platform.dispatch requires a block' unless blk
166
+ binding = blk.binding.eval('self')
167
+ meth = blk.call
168
+ target = dispatch_candidates(meth).detect do |candidate|
169
+ binding.respond_to?(candidate)
170
+ end
171
+ raise "No platform dispatch target found in #{binding.class} for " +
172
+ "'#{meth.inspect}', tried " + dispatch_candidates(meth).join(', ') unless target
173
+ binding.__send__(target, *args)
174
+ end
175
+
176
+ # @return [Controller] Platform-specific controller object
177
+ def controller
178
+ platform_service(:controller)
179
+ end
180
+
181
+ # @return [Filesystem] Platform-specific filesystem config object
182
+ def filesystem
183
+ platform_service(:filesystem)
184
+ end
185
+
186
+ # @return [VolumeManager] Platform-specific volume manager config object
187
+ def volume_manager
188
+ platform_service(:volume_manager)
189
+ end
190
+
191
+ # @return [Shell] Platform-specific shell information object
192
+ def shell
193
+ platform_service(:shell)
194
+ end
195
+
196
+ # @return [Rng] Platform-specific RNG object
197
+ def rng
198
+ platform_service(:rng)
199
+ end
200
+
201
+ # @return [Process] Platform-specific process facilities object
202
+ def process
203
+ platform_service(:process)
204
+ end
205
+
206
+ # @return [Installer] Platform-specific installer information object
207
+ def installer
208
+ platform_service(:installer)
209
+ end
210
+
211
+ # Blocking call to invoke a command line tool used to perform platform-
212
+ # specific tasks.
213
+ #
214
+ # Also provides a consistent interface for mocking command output during
215
+ # spec testing. Implementations should use this method instead of
216
+ # embedding popen/backtick calls to assist with testing.
217
+ #
218
+ # @param [String] command to run
219
+ # @param [Hash] options for execution
220
+ # @option options [TrueClass|FalseClass] :raise_on_failure true to raise on command failure (Default)
221
+ #
222
+ # @return [String] output from command or empty
223
+ #
224
+ # @raise [CommandError] on failure (by default)
225
+ def execute(command, options = {})
226
+ options = { :raise_on_failure => true }.merge(options)
227
+ raise_on_failure = options[:raise_on_failure]
228
+ output_text = ''
229
+ begin
230
+ output_text = `#{command}`
231
+ if !$?.success? && options[:raise_on_failure]
232
+ message = []
233
+ message << "Command failed with exit code = #{$?.exitstatus}:"
234
+ message << "> #{command}"
235
+ error_output_text = output_text.strip
236
+ message << error_output_text unless error_output_text.empty?
237
+ e = CommandError.new(message.join("\n"))
238
+ e.command = command
239
+ e.status = $?
240
+ e.output_text = output_text
241
+ raise e
242
+ end
243
+ rescue Errno::ENOENT => e
244
+ if raise_on_failure
245
+ raise CommandError, "Command failed: #{e.message}"
246
+ end
247
+ end
248
+ output_text
249
+ end
250
+
251
+ # Base class for platform helpers.
252
+ class PlatformHelperBase
253
+
254
+ private
255
+
256
+ # Convenience method for declaring must-be-overridden interfaces.
257
+ #
258
+ # @raise [NotImplementedError] always unless overridden
259
+ def must_be_overridden
260
+ raise ::NotImplementedError, 'Must be overridden'
261
+ end
262
+
263
+ # Convenience method for executing a command via platform.
264
+ #
265
+ # See Platform#execute
266
+ def execute(cmd, options = {})
267
+ ::RightScale::Platform.execute(cmd, options)
268
+ end
269
+ end
270
+
271
+ # Declares various file system APIs.
272
+ class Filesystem < PlatformHelperBase
273
+
274
+ # Is given command available in the PATH?
275
+ #
276
+ # @param [String] command_name to be tested
277
+ #
278
+ # @return [TrueClass|FalseClass] true if command is in path
279
+ def has_executable_in_path(command_name)
280
+ return !!find_executable_in_path(command_name)
281
+ end
282
+
283
+ # Finds the given command name in the PATH. this emulates the 'which'
284
+ # command from linux (without the terminating newline).
285
+ #
286
+ # @param [String] command_name to be tested
287
+ #
288
+ # @return [String] path to first matching executable file in PATH or nil
289
+ def find_executable_in_path(command_name)
290
+ must_be_overridden
291
+ end
292
+
293
+ # @return [String] directory containing generated agent configuration files
294
+ def right_agent_cfg_dir
295
+ must_be_overridden
296
+ end
297
+
298
+ # @return [String] static (time-invariant) state that is common to all RightScale apps/agents
299
+ def right_scale_static_state_dir
300
+ must_be_overridden
301
+ end
302
+
303
+ # @return [String] static (time-invariant) state that is specific to RightLink
304
+ def right_link_static_state_dir
305
+ must_be_overridden
306
+ end
307
+
308
+ # @return [String] dynamic, persistent runtime state that is specific to RightLink
309
+ def right_link_dynamic_state_dir
310
+ must_be_overridden
311
+ end
312
+
313
+ # @return [String] data which is awaiting some kind of later processing
314
+ def spool_dir
315
+ must_be_overridden
316
+ end
317
+
318
+ # TEAL TODO description
319
+ def ssh_cfg_dir
320
+ must_be_overridden
321
+ end
322
+
323
+ # Cached data from applications. Such data is locally generated as a
324
+ # result of time-consuming I/O or calculation. The application must
325
+ # be able to regenerate or restore the data.
326
+ #
327
+ # @return [String] cache directory
328
+ def cache_dir
329
+ must_be_overridden
330
+ end
331
+
332
+ # @return [String] system logs
333
+ def log_dir
334
+ must_be_overridden
335
+ end
320
336
 
321
- unless res = self.instance_variable_get(instance_var)
337
+ # For Unix compatibility; has no significance in Windows
338
+ #
339
+ # @return [String] source code directory, for reference purposes and for development
340
+ def source_code_dir
341
+ must_be_overridden
342
+ end
343
+
344
+ # @return [String] temporary files.
345
+ def temp_dir
346
+ must_be_overridden
347
+ end
348
+
349
+ # @return [String] path to place pid files
350
+ def pid_dir
351
+ must_be_overridden
352
+ end
353
+
354
+ # @return [String] installed home (parent of) right_link directory path
355
+ def right_link_home_dir
356
+ must_be_overridden
357
+ end
358
+
359
+ # @return [String] path to right link configuration and internal usage scripts
360
+ def private_bin_dir
361
+ must_be_overridden
362
+ end
363
+
364
+ # TEAL TODO description
365
+ def sandbox_dir
366
+ must_be_overridden
367
+ end
368
+
369
+ # Converts a long path (e.g. "C:/Program Files") to a short path
370
+ # (e.g. "C:/PROGRA~1") if necessary. See implementation for notes.
371
+ #
372
+ # For Windows compatibility; has no significance in Linux
373
+ #
374
+ # @param [String] long_path to convert
375
+ #
376
+ # @return [String] short path
377
+ def long_path_to_short_path(long_path)
378
+ must_be_overridden
379
+ end
380
+
381
+ # Converts slashes in a path to a consistent style.
382
+ #
383
+ # For Windows compatibility; has no significance in Linux
384
+ #
385
+ # @param [String] path to make pretty
386
+ # @param [String] native_fs_flag as true if path is pretty for native
387
+ # file system (i.e. file system calls more likely to succeed), false
388
+ # if pretty for Ruby interpreter (default).
389
+ #
390
+ # @return [String] pretty path
391
+ def pretty_path(path, native_fs_flag = false)
392
+ must_be_overridden
393
+ end
394
+
395
+ # Ensures a local drive location for the file or folder given by path
396
+ # by copying to a local temp directory given by name only if the item
397
+ # does not appear on the home drive. This method is useful because
398
+ # secure applications refuse to run scripts from network locations, etc.
399
+ # Replaces any similar files in given temp dir to ensure latest updates.
400
+ #
401
+ # For Windows compatibility; has no significance in Linux
402
+ #
403
+ # @param [String] path to file or directory to be placed locally
404
+ # @param [String] temp_dir_name relative to user temp_dir to use only if the file or folder is not on a local drive
405
+ #
406
+ # @return [String] local drive path
407
+ def ensure_local_drive_path(path, temp_dir_name)
408
+ must_be_overridden
409
+ end
410
+
411
+ # Creates a symlink (if supported by platform).
412
+ #
413
+ # @param [String] from_path the path to the real file/directory
414
+ # @param [String] to_path the path to the symlink to be created
415
+ #
416
+ # @return [Fixnum] always 0 as does File.symlink under Linux
417
+ #
418
+ # @raise [RightScale::Exceptions::PlatformError] on failure
419
+ def create_symlink(from_path, to_path)
420
+ must_be_overridden
421
+ end
422
+ end
423
+
424
+ # Provides utilities for managing volumes (disks).
425
+ class VolumeManager < PlatformHelperBase
426
+
427
+ # exceptions
428
+ class ParserError < ::RightScale::Exceptions::PlatformError; end
429
+ class VolumeError < ::RightScale::Exceptions::PlatformError; end
430
+
431
+ # Gets a list of currently visible volumes in the form:
432
+ # [{:device, :label, :uuid, :type, :filesystem}]
433
+ #
434
+ # @param [Hash] conditions to match, if any (Default = no conditions)
435
+ #
436
+ # @return [Array] volume info as an array of hashes or empty
437
+ #
438
+ # @raise [ParserError] on failure to parse volume list
439
+ # @raise [VolumeError] on failure to execute `blkid` to obtain raw output
440
+ def volumes(conditions = nil)
441
+ must_be_overridden
442
+ end
443
+ end # VolumeManager
444
+
445
+ # Declares various command shell APIs.
446
+ class Shell < PlatformHelperBase
447
+
448
+ # @return [String] name or path of file reserved for null output redirection
449
+ def null_output_name
450
+ must_be_overridden
451
+ end
452
+
453
+ # Fully qualifies a partial script file path to ensure it is executable on
454
+ # the current platform.
455
+ #
456
+ # For Windows compatibility; has no significance in Linux
457
+ #
458
+ # @param [String] partial_script_file_path to format
459
+ # @param [String] default_extension to use if no extension (Default = platform specific)
460
+ #
461
+ # @return [String] full script path
462
+ def format_script_file_name(partial_script_file_path, default_extension = nil)
463
+ must_be_overridden
464
+ end
465
+
466
+ # Formats an executable command by quoting any of the arguments as needed
467
+ # and building an executable command string.
468
+ #
469
+ # @param [String] executable_file_path full or partial
470
+ # @param [Array] arguments for executable, if any
471
+ #
472
+ # @return [String] executable command string
473
+ def format_executable_command(executable_file_path, *arguments)
474
+ must_be_overridden
475
+ end
476
+
477
+ # Formats a shell command using the given script path and arguments.
478
+ # Provides the path to the executable for the script as needed for the
479
+ # current platform.
480
+ #
481
+ # @param [String] shell_script_file_path shell script file path
482
+ # @param [Array] arguments for executable, if any
483
+ #
484
+ # @return [String] executable command string
485
+ def format_shell_command(shell_script_file_path, *arguments)
486
+ must_be_overridden
487
+ end
488
+
489
+ # Formats a ruby command using the given script path and arguments and
490
+ # the sandbox ruby path.
491
+ #
492
+ # @param [String] shell_script_file_path for formatting
493
+ # @param [Array] arguments for command or empty
494
+ #
495
+ # @return [String] executable command string
496
+ def format_ruby_command(shell_script_file_path, *arguments)
497
+ return format_executable_command(sandbox_ruby, [shell_script_file_path, arguments])
498
+ end
499
+
500
+ # Appends STDOUT redirection to the given shell command.
501
+ #
502
+ # @param [String] cmd to format
503
+ # @param [String] redirection target (Default = null output)
504
+ #
505
+ # @return [String] formatted for redirection
506
+ def format_redirect_stdout(cmd, target = nil)
507
+ target ||= null_output_name
508
+ return cmd + " 1>#{target}"
509
+ end
510
+
511
+ # Appends STDERR redirection to the given shell command.
512
+ #
513
+ # @param [String] cmd to format
514
+ # @param [String] redirection target (Default = null output)
515
+ #
516
+ # @return [String] formatted for redirection
517
+ def format_redirect_stderr(cmd, target = nil)
518
+ target ||= null_output_name
519
+ return cmd + " 2>#{target}"
520
+ end
521
+
522
+ # Appends STDERR redirection to the given shell command.
523
+ #
524
+ # @param [String] cmd to format
525
+ # @param [String] redirection target (Default = null output)
526
+ #
527
+ # @return [String] formatted for redirection
528
+ def format_redirect_both(cmd, target = nil)
529
+ target ||= null_output_name
530
+ return cmd + " 1>#{target} 2>&1"
531
+ end
532
+
533
+ # @return [String] full path to the RightScale sandboxed ruby executable
534
+ def sandbox_ruby
535
+ must_be_overridden
536
+ end
537
+
538
+ # Gets the current system uptime.
539
+ #
540
+ # @return [Float] time the machine has been up, in seconds, or 0.0
541
+ def uptime
542
+ must_be_overridden
543
+ end
544
+
545
+ # Gets the time at which the system was booted.
546
+ #
547
+ # @return [Integer] the UTC timestamp at which the system was booted or nil on failure
548
+ def booted_at
549
+ must_be_overridden
550
+ end
551
+ end
552
+
553
+ # System controller APIs.
554
+ class Controller < PlatformHelperBase
555
+
556
+ # Reboot machine now.
557
+ #
558
+ # @return [TrueClass] always true
559
+ def reboot
560
+ must_be_overridden
561
+ end
562
+
563
+ # Shutdown machine now.
564
+ #
565
+ # @return [TrueClass] always true
566
+ def shutdown
567
+ must_be_overridden
568
+ end
569
+ end
570
+
571
+ # Randomizer APIs.
572
+ class Rng < PlatformHelperBase
573
+
574
+ # Generates a pseudo-random byte string.
575
+ #
576
+ # @param [Fixnum] count of bytes
577
+ #
578
+ # @return [String] bytes
579
+ def pseudorandom_bytes(count)
580
+ must_be_overridden
581
+ end
582
+ end
583
+
584
+ # Process APIs.
585
+ class Process < PlatformHelperBase
586
+
587
+ # Queries resident/working set size (total memory used by process) for
588
+ # the process given by identifier (PID).
589
+ #
590
+ # @param [Fixnum] pid for query (Default = current process)
591
+ #
592
+ # @return [Integer] current set size in KB
593
+ def resident_set_size(pid = nil)
594
+ must_be_overridden
595
+ end
596
+ end
597
+
598
+ # Package installation APIs.
599
+ class Installer < PlatformHelperBase
600
+
601
+ # exceptions
602
+ class PackageNotFound < ::RightScale::Exceptions::PlatformError; end
603
+ class PackageManagerNotFound < ::RightScale::Exceptions::PlatformError; end
604
+
605
+ # @return [String] installer output or nil
606
+ attr_accessor :output
607
+
608
+ def initialize
609
+ @output = nil
610
+ end
611
+
612
+ # Install packages based on installed package manager.
613
+ #
614
+ # For Unix compatibility; has no significance in Windows
615
+ #
616
+ # @param [Array] packages to be installed
617
+ #
618
+ # @return [TrueClass] always true
619
+ #
620
+ # @raise [RightScale::Exceptions::PlatformError] if not supported by platform
621
+ # @raise [PackageNotFound] if package is not found
622
+ # @raise [PackageManagerNotFound] if package manager is not available
623
+ # @raise [CommandError] on any other command failure
624
+ def install(packages)
625
+ must_be_overridden
626
+ end
627
+ end
628
+
629
+ private
630
+
631
+ # Load platform specific implementation
632
+ def initialize
633
+ @genus = nil
634
+ @species = nil
635
+ @filesystem = nil
636
+ @shell = nil
637
+ @ssh = nil
638
+ @controller = nil
639
+ @installer = nil
640
+ @flavor = nil
641
+ @release = nil
642
+ @codename = nil
643
+
644
+ initialize_platform_specific
645
+ end
646
+
647
+ # First-initialization tasks. Also convenient for overriding during
648
+ # testing on platforms that differ from current platform.
649
+ def initialize_platform_specific
322
650
  load_platform_specific
323
- if linux?
324
- res = Platform.const_get(const_name).new
325
- elsif darwin?
326
- res = Platform.const_get(const_name).new
327
- elsif windows?
328
- res = Platform.const_get(const_name).new
651
+ initialize_genus
652
+ initialize_species
653
+ end
654
+
655
+ # Load platform specific implementation
656
+ #
657
+ # @return [TrueClass|FalseClass] true if loaded first time, false if already loaded
658
+ def load_platform_specific
659
+ # TEAL NOTE the unusal thing about this singleton is that it is
660
+ # redefined incrementally by first loading the base then the genus then
661
+ # the species, all of which define parts of the whole.
662
+ result = require platform_genus_path
663
+ result = (require platform_species_path) && result
664
+ result
665
+ end
666
+
667
+ # Performs any platform genus-specific initialization. This method is
668
+ # invoked only after the current platform's specific implementation has
669
+ # been loaded.
670
+ #
671
+ # @return [TrueClass] always true
672
+ def initialize_genus
673
+ raise ::NotImplementedError, 'Must be overridden'
674
+ end
675
+
676
+ # Performs any platform species-specific initialization. This method is
677
+ # invoked only after the current platform's specific implementation has
678
+ # been loaded.
679
+ #
680
+ # @return [TrueClass] always true
681
+ def initialize_species
682
+ raise ::NotImplementedError, 'Must be overridden'
683
+ end
684
+
685
+ # Determines genus/species for current platform.
686
+ def resolve_taxonomy
687
+ case ::RbConfig::CONFIG['host_os']
688
+ when /darwin/i
689
+ @genus = :unix
690
+ @species = :darwin
691
+ when /linux/i
692
+ @genus = :unix
693
+ @species = :linux
694
+ when /mingw/i
695
+ @genus = :windows
696
+ @species = :mingw
697
+ when /mswin/i
698
+ @genus = :windows
699
+ @species = :mswin
700
+ when /windows|win32|dos|cygwin/i
701
+ raise ::RightScale::Exceptions::PlatformError,
702
+ 'Unsupported Ruby-on-Windows variant'
703
+ else
704
+ raise ::RightScale::Exceptions::PlatformError, 'Unknown platform'
329
705
  end
330
- self.instance_variable_set(instance_var, res)
706
+ true
707
+ end
708
+
709
+ # @return [String] path to platform-independent implementation.
710
+ def platform_base_path
711
+ ::File.expand_path('../platform', __FILE__)
712
+ end
713
+
714
+ # @return [String] path to platform-specific genus implementation.
715
+ def platform_genus_path
716
+ ::File.expand_path("#{genus}/platform", platform_base_path)
717
+ end
718
+
719
+ # @return [String] path to platform-specific species implementation.
720
+ def platform_species_path
721
+ ::File.expand_path("#{genus}/#{species}/platform", platform_base_path)
722
+ end
723
+
724
+ # Retrieve platform specific service implementation
725
+ #
726
+ # @param [Symbol] name of platform service
727
+ #
728
+ # @return [PlatformHelperBase] service instance
729
+ #
730
+ # @raise [RightScale::Exceptions::PlatformError] on unknown service
731
+ def platform_service(name)
732
+ instance_var = "@#{name.to_s}".to_sym
733
+ const_name = name.to_s.camelize
734
+
735
+ unless res = self.instance_variable_get(instance_var)
736
+ load_platform_specific
737
+ if clazz = Platform.const_get(const_name)
738
+ res = clazz.new
739
+ self.instance_variable_set(instance_var, res)
740
+ else
741
+ raise ::RightScale::Exceptions::PlatformError,
742
+ "Unknown platform service: #{name}"
743
+ end
744
+ end
745
+ return res
746
+ end
747
+
748
+ # Determines which cloud we're on by the cheap but simple expedient of
749
+ # reading the RightScale cloud file.
750
+ #
751
+ # @deprecated leverage the right_link cloud libraries for any cloud-
752
+ # specific behavior because the behavior of all possible clouds is
753
+ # beyond the scope of hard-coded case statements.
754
+ #
755
+ # @return [String] cloud type or nil
756
+ def resolve_cloud_type
757
+ cloud_file_path = ::File.join(self.filesystem.right_scale_static_state_dir, 'cloud')
758
+ @cloud_type = ::File.read(cloud_file_path) rescue nil
759
+ @cloud_type
331
760
  end
332
- return res
333
- end
334
761
 
335
- end # Platform
762
+ end # Platform
336
763
 
337
- end # RightScale
764
+ end # RightScale
338
765
 
339
- # Initialize for current platform
340
- RightScale::Platform.load_platform_specific
766
+ # Initialize for current platform and/or force singleton creation on current
767
+ # thread to avoid any weird threaded initialization issues.
768
+ ::RightScale::Platform.instance
341
769
 
342
- end # Unless already defined
770
+ end # unless already defined