right_slicehost 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,604 @@
1
+ #
2
+ # Copyright (c) 2007-2009 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
+ #
24
+ module Rightscale
25
+
26
+ class SlicehostBenchmarkingBlock #:nodoc:
27
+ attr_accessor :parser, :service
28
+ def initialize
29
+ # Benchmark::Tms instance for service access benchmarking.
30
+ @service = Benchmark::Tms.new()
31
+ # Benchmark::Tms instance for parsing benchmarking.
32
+ @parser = Benchmark::Tms.new()
33
+ end
34
+ end
35
+
36
+ class SlicehostNoChange < RuntimeError #:nodoc:
37
+ end
38
+
39
+ module RightSlicehostInterface
40
+ DEFAULT_SLICEHOST_URL = 'https://api.slicehost.com'
41
+
42
+ # Text, if found in an error message returned by Slicehost, indicates that this may be a transient
43
+ # error. Transient errors are automatically retried with exponential back-off.
44
+ # TODO: gather Slicehost errors here
45
+ SLICEHOST_PROBLEMS = [ ]
46
+ @@slicehost_problems = SLICEHOST_PROBLEMS
47
+ # Returns a list of Slicehost responses which are known to be transient problems.
48
+ # We have to re-request if we get any of them, because the problem will probably disappear.
49
+ # By default this method returns the same value as the SLICEHOST_PROBLEMS const.
50
+ def self.slicehost_problems
51
+ @@slicehost_problems
52
+ end
53
+
54
+ @@caching = false
55
+ def self.caching
56
+ @@caching
57
+ end
58
+ def self.caching=(caching)
59
+ @@caching = caching
60
+ end
61
+
62
+ @@bench = SlicehostBenchmarkingBlock.new
63
+ def self.bench_parser
64
+ @@bench.parser
65
+ end
66
+ def self.bench_slicehost
67
+ @@bench.service
68
+ end
69
+
70
+ # Current Slicehost API key
71
+ attr_reader :slicehost_pasword
72
+ # Last HTTP request object
73
+ attr_reader :last_request
74
+ # Last HTTP response object
75
+ attr_reader :last_response
76
+ # Last Slicehost errors list (used by SlicehostErrorHandler)
77
+ attr_accessor :last_errors
78
+ # Logger object
79
+ attr_accessor :logger
80
+ # Initial params hash
81
+ attr_accessor :params
82
+ # RightHttpConnection instance
83
+ attr_reader :connection
84
+ # Cache
85
+ attr_reader :cache
86
+
87
+
88
+ #
89
+ # Params:
90
+ # :slicehost_url
91
+ # :logger
92
+ # :multi_thread
93
+ #
94
+ def init(slicehost_pasword, params={}) #:nodoc:
95
+ @params = params
96
+ @cache = {}
97
+ @error_handler = nil
98
+ # deny working without credentials
99
+ if slicehost_pasword.blank?
100
+ raise SlicehostError.new("Slicehost password is required to operate on Slicehost API service")
101
+ end
102
+ @slicehost_pasword = slicehost_pasword
103
+ # parse Slicehost URL
104
+ @params[:slicehost_url] ||= ENV['SLICEHOST_URL'] || DEFAULT_SLICEHOST_URL
105
+ @params[:server] = URI.parse(@params[:slicehost_url]).host
106
+ @params[:port] = URI.parse(@params[:slicehost_url]).port
107
+ @params[:service] = URI.parse(@params[:slicehost_url]).path
108
+ @params[:protocol] = URI.parse(@params[:slicehost_url]).scheme
109
+ # other params
110
+ @params[:multi_thread] ||= defined?(SLICEHOST_DAEMON)
111
+ @logger = @params[:logger] || (defined?(RAILS_DEFAULT_LOGGER) && RAILS_DEFAULT_LOGGER) || Logger.new(STDOUT)
112
+ @logger.info "New #{self.class.name} using #{@params[:multi_thread] ? 'multi' : 'single'}-threaded mode"
113
+ end
114
+
115
+ def on_exception(options={:raise=>true, :log=>true}) # :nodoc:
116
+ raise if $!.is_a?(SlicehostNoChange)
117
+ SlicehostError::on_slicehost_exception(self, options)
118
+ end
119
+
120
+ # --------
121
+ # Helpers
122
+ # --------
123
+
124
+ # Return +true+ if this instance works in multi_thread mode and +false+ otherwise.
125
+ def multi_thread
126
+ @params[:multi_thread]
127
+ end
128
+
129
+ def cgi_escape_params(params) # :nodoc:
130
+ params.map {|k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" }.join("&")
131
+ end
132
+
133
+ # ----------------------------------
134
+ # request generation and processing
135
+ # ----------------------------------
136
+
137
+ # Generate a handy request hash.
138
+ def generate_request(method, path, params={}) # :nodoc:
139
+ # default request params
140
+ params = cgi_escape_params(params)
141
+
142
+ request_url = "#{@params[:service]}/#{path}"
143
+ request_url << "?#{params}" unless params.blank?
144
+
145
+ request = method.new(request_url)
146
+ case method.name[/([^:]*)$/] && $1
147
+ when 'Post'
148
+ request['Accept'] = '*/*'
149
+ request['Content-Type'] = 'application/xml'
150
+ when 'Put'
151
+ request['Accept'] = '*/*'
152
+ request['Content-Type'] = 'application/xml'
153
+ else
154
+ request['Accept'] = 'application/xml'
155
+ end
156
+
157
+ request.basic_auth(@slicehost_pasword,'')
158
+
159
+ { :request => request,
160
+ :server => @params[:server],
161
+ :port => @params[:port],
162
+ :protocol => @params[:protocol] }
163
+ end
164
+
165
+ # Perform a request.
166
+ # (4xx and 5xx error handling is being made through SlicehostErrorHandler)
167
+ def request_info(request, parser) #:nodoc:
168
+ # check single/multi threading mode
169
+ thread = @params[:multi_thread] ? Thread.current : Thread.main
170
+ # create a connection if needed
171
+ thread[:connection] ||= Rightscale::HttpConnection.new(:exception => SlicehostError, :logger => @logger)
172
+ @connection = thread[:connection]
173
+ @last_request = request[:request]
174
+ @last_response = nil
175
+ # perform a request
176
+ @@bench.service.add!{ @last_response = @connection.request(request) }
177
+ # check response for success...
178
+ if @last_response.is_a?(Net::HTTPSuccess)
179
+ @error_handler = nil
180
+ @@bench.parser.add! { parser.parse(@last_response) }
181
+ return parser.result
182
+ else
183
+ @error_handler ||= SlicehostErrorHandler.new(self, parser, :errors_list => @@slicehost_problems)
184
+ check_result = @error_handler.check(request)
185
+ if check_result
186
+ @error_handler = nil
187
+ return check_result
188
+ end
189
+ raise SlicehostError.new(@last_errors, @last_response.code)
190
+ end
191
+ rescue
192
+ @error_handler = nil
193
+ raise
194
+ end
195
+
196
+ # --------
197
+ # Caching
198
+ # --------
199
+
200
+ # Perform a request.
201
+ # Skips a response parsing if caching is used.
202
+ def request_cache_or_info(method, request_hash, parser_class, use_cache=true) #:nodoc:
203
+ # We do not want to break the logic of parsing hence will use a dummy parser to process all the standard
204
+ # steps (errors checking etc). The dummy parser does nothig - just returns back the params it received.
205
+ # If the caching is enabled and hit then throw SlicehostNoChange.
206
+ # P.S. caching works for the whole images list only! (when the list param is blank)
207
+ response = request_info(request_hash, SlicehostDummyParser.new)
208
+ # check cache
209
+ cache_hits?(method.to_sym, response.body) if use_cache
210
+ parser = parser_class.new(:logger => @logger)
211
+ @@bench.parser.add!{ parser.parse(response) }
212
+ result = block_given? ? yield(parser) : parser.result
213
+ # update parsed data
214
+ update_cache(method.to_sym, :parsed => result) if use_cache
215
+ result
216
+ end
217
+
218
+ # Returns +true+ if the describe_xxx responses are being cached
219
+ def caching?
220
+ @params.key?(:cache) ? @params[:cache] : @@caching
221
+ end
222
+
223
+ # Check if the slicehost function response hits the cache or not.
224
+ # If the cache hits:
225
+ # - raises an +SlicehostNoChange+ exception if +do_raise+ == +:raise+.
226
+ # - returnes parsed response from the cache if it exists or +true+ otherwise.
227
+ # If the cache miss or the caching is off then returns +false+.
228
+ def cache_hits?(function, response, do_raise=:raise) # :nodoc:
229
+ result = false
230
+ if caching?
231
+ function = function.to_sym
232
+ response_md5 = MD5.md5(response).to_s
233
+ # well, the response is new, reset cache data
234
+ unless @cache[function] && @cache[function][:response_md5] == response_md5
235
+ update_cache(function, {:response_md5 => response_md5,
236
+ :timestamp => Time.now,
237
+ :hits => 0,
238
+ :parsed => nil})
239
+ else
240
+ # aha, cache hits, update the data and throw an exception if needed
241
+ @cache[function][:hits] += 1
242
+ if do_raise == :raise
243
+ raise(SlicehostNoChange, "Cache hit: #{function} response has not changed since "+
244
+ "#{@cache[function][:timestamp].strftime('%Y-%m-%d %H:%M:%S')}, "+
245
+ "hits: #{@cache[function][:hits]}.")
246
+ else
247
+ result = @cache[function][:parsed] || true
248
+ end
249
+ end
250
+ end
251
+ result
252
+ end
253
+
254
+ def update_cache(function, hash) # :nodoc:
255
+ (@cache[function.to_sym] ||= {}).merge!(hash) if caching?
256
+ end
257
+ end
258
+
259
+
260
+ # Exception class to signal any Slicehost errors. All errors occuring during calls to Slicehost's
261
+ # web services raise this type of error.
262
+ # Attribute inherited by RuntimeError:
263
+ # message - the text of the error
264
+ class SlicehostError < RuntimeError # :nodoc:
265
+
266
+ # either an array of errors where each item is itself an array of [code, message]),
267
+ # or an error string if the error was raised manually, as in <tt>SlicehostError.new('err_text')</tt>
268
+ attr_reader :errors
269
+
270
+ # Response HTTP error code
271
+ attr_reader :http_code
272
+
273
+ def initialize(errors=nil, http_code=nil)
274
+ @errors = errors
275
+ @http_code = http_code
276
+ super(@errors.is_a?(Array) ? @errors.map{|code, msg| "#{code}: #{msg}"}.join("; ") : @errors.to_s)
277
+ end
278
+
279
+ # Does any of the error messages include the regexp +pattern+?
280
+ # Used to determine whether to retry request.
281
+ def include?(pattern)
282
+ if @errors.is_a?(Array)
283
+ @errors.each{ |code, msg| return true if code =~ pattern }
284
+ else
285
+ return true if @errors_str =~ pattern
286
+ end
287
+ false
288
+ end
289
+
290
+ # Generic handler for SlicehostErrors.
291
+ # object that caused the exception (it must provide last_request and last_response). Supported
292
+ # boolean options are:
293
+ # * <tt>:log</tt> print a message into the log using slicehost.logger to access the Logger
294
+ # * <tt>:puts</tt> do a "puts" of the error
295
+ # * <tt>:raise</tt> re-raise the error after logging
296
+ def self.on_slicehost_exception(slicehost, options={:raise=>true, :log=>true})
297
+ # Only log & notify if not user error
298
+ if !options[:raise] || system_error?($!)
299
+ error_text = "#{$!.inspect}\n#{$@}.join('\n')}"
300
+ puts error_text if options[:puts]
301
+ # Log the error
302
+ if options[:log]
303
+ request = slicehost.last_request ? slicehost.last_request.path : '-none-'
304
+ response = slicehost.last_response ? "#{slicehost.last_response.code} -- #{slicehost.last_response.message} -- #{slicehost.last_response.body}" : '-none-'
305
+ slicehost.logger.error error_text
306
+ slicehost.logger.error "Request was: #{request}"
307
+ slicehost.logger.error "Response was: #{response}"
308
+ end
309
+ end
310
+ raise if options[:raise] # re-raise an exception
311
+ return nil
312
+ end
313
+
314
+ # True if e is an Slicehost system error, i.e. something that is for sure not the caller's fault.
315
+ # Used to force logging.
316
+ # TODO: Place Slicehost Errors here - these are AWS errs
317
+ def self.system_error?(e)
318
+ !e.is_a?(self) || e.message =~ /InternalError|InsufficientInstanceCapacity|Unavailable/
319
+ end
320
+
321
+ end
322
+
323
+ class SlicehostErrorHandler # :nodoc:
324
+ # 0-100 (%)
325
+ DEFAULT_CLOSE_ON_4XX_PROBABILITY = 10
326
+
327
+ @@reiteration_start_delay = 0.2
328
+ def self.reiteration_start_delay
329
+ @@reiteration_start_delay
330
+ end
331
+ def self.reiteration_start_delay=(reiteration_start_delay)
332
+ @@reiteration_start_delay = reiteration_start_delay
333
+ end
334
+
335
+ @@reiteration_time = 5
336
+ def self.reiteration_time
337
+ @@reiteration_time
338
+ end
339
+ def self.reiteration_time=(reiteration_time)
340
+ @@reiteration_time = reiteration_time
341
+ end
342
+
343
+ @@close_on_error = true
344
+ def self.close_on_error
345
+ @@close_on_error
346
+ end
347
+ def self.close_on_error=(close_on_error)
348
+ @@close_on_error = close_on_error
349
+ end
350
+
351
+ @@close_on_4xx_probability = DEFAULT_CLOSE_ON_4XX_PROBABILITY
352
+ def self.close_on_4xx_probability
353
+ @@close_on_4xx_probability
354
+ end
355
+ def self.close_on_4xx_probability=(close_on_4xx_probability)
356
+ @@close_on_4xx_probability = close_on_4xx_probability
357
+ end
358
+
359
+ # params:
360
+ # :reiteration_time
361
+ # :errors_list
362
+ # :close_on_error = true | false
363
+ # :close_on_4xx_probability = 1-100
364
+ def initialize(slicehost, parser, params={}) #:nodoc:
365
+ @slicehost = slicehost
366
+ @parser = parser # parser to parse a response
367
+ @started_at = Time.now
368
+ @stop_at = @started_at + (params[:reiteration_time] || @@reiteration_time)
369
+ @errors_list = params[:errors_list] || []
370
+ @reiteration_delay = @@reiteration_start_delay
371
+ @retries = 0
372
+ # close current HTTP(S) connection on 5xx, errors from list and 4xx errors
373
+ @close_on_error = params[:close_on_error].nil? ? @@close_on_error : params[:close_on_error]
374
+ @close_on_4xx_probability = params[:close_on_4xx_probability] || @@close_on_4xx_probability
375
+ end
376
+
377
+ # Returns false if
378
+ def check(request) #:nodoc:
379
+ result = false
380
+ error_found = false
381
+ error_match = nil
382
+ last_errors_text = ''
383
+ response = @slicehost.last_response
384
+ # log error
385
+ request_text_data = "#{request[:server]}:#{request[:port]}#{request[:request].path}"
386
+ @slicehost.logger.warn("##### #{@slicehost.class.name} returned an error: #{response.code} #{response.message}\n#{response.body} #####")
387
+ @slicehost.logger.warn("##### #{@slicehost.class.name} request: #{request_text_data} ####")
388
+
389
+ if response.body && response.body[/<\?xml/]
390
+ error_parser = SliceErrorResponseParser.new
391
+ error_parser.parse(response)
392
+ @slicehost.last_errors = error_parser.errors.map{|e| [response.code, e]}
393
+ last_errors_text = @slicehost.last_errors.flatten.join("\n")
394
+ else
395
+ @slicehost.last_errors = [[response.code, "#{response.message} (#{request_text_data})"]]
396
+ last_errors_text = response.message
397
+ end
398
+
399
+ # now - check the error
400
+ @errors_list.each do |error_to_find|
401
+ if last_errors_text[/#{error_to_find}/i]
402
+ error_found = true
403
+ error_match = error_to_find
404
+ @slicehost.logger.warn("##### Retry is needed, error pattern match: #{error_to_find} #####")
405
+ break
406
+ end
407
+ end
408
+ # check the time has gone from the first error come
409
+ if error_found
410
+ # Close the connection to the server and recreate a new one.
411
+ # It may have a chance that one server is a semi-down and reconnection
412
+ # will help us to connect to the other server
413
+ if @close_on_error
414
+ @slicehost.connection.finish "#{self.class.name}: error match to pattern '#{error_match}'"
415
+ end
416
+
417
+ if (Time.now < @stop_at)
418
+ @retries += 1
419
+
420
+ @slicehost.logger.warn("##### Retry ##{@retries} is being performed. Sleeping for #{@reiteration_delay} sec. Whole time: #{Time.now-@started_at} sec ####")
421
+ sleep @reiteration_delay
422
+ @reiteration_delay *= 2
423
+
424
+ # Always make sure that the fp is set to point to the beginning(?)
425
+ # of the File/IO. TODO: it assumes that offset is 0, which is bad.
426
+ if(request[:request].body_stream && request[:request].body_stream.respond_to?(:pos))
427
+ begin
428
+ request[:request].body_stream.pos = 0
429
+ rescue Exception => e
430
+ @logger.warn("Retry may fail due to unable to reset the file pointer" +
431
+ " -- #{self.class.name} : #{e.inspect}")
432
+ end
433
+ end
434
+ result = @slicehost.request_info(request, @parser)
435
+ else
436
+ @slicehost.logger.warn("##### Ooops, time is over... ####")
437
+ end
438
+ # aha, this is unhandled error:
439
+ elsif @close_on_error
440
+ # Is this a 5xx error ?
441
+ if @slicehost.last_response.code.to_s[/^5\d\d$/]
442
+ @slicehost.connection.finish "#{self.class.name}: code: #{@slicehost.last_response.code}: '#{@slicehost.last_response.message}'"
443
+ # Is this a 4xx error ?
444
+ elsif @slicehost.last_response.code.to_s[/^4\d\d$/] && @close_on_4xx_probability > rand(100)
445
+ @slicehost.connection.finish "#{self.class.name}: code: #{@slicehost.last_response.code}: '#{@slicehost.last_response.message}', " +
446
+ "probability: #{@close_on_4xx_probability}%"
447
+ end
448
+ end
449
+ result
450
+ end
451
+ end
452
+
453
+ #-----------------------------------------------------------------
454
+
455
+ class RightSaxParserCallback #:nodoc:
456
+ def self.include_callback
457
+ include XML::SaxParser::Callbacks
458
+ end
459
+ def initialize(slice_parser)
460
+ @slice_parser = slice_parser
461
+ end
462
+ def on_start_element(name, attr_hash)
463
+ @slice_parser.tag_start(name, attr_hash)
464
+ end
465
+ def on_characters(chars)
466
+ @slice_parser.text(chars)
467
+ end
468
+ def on_end_element(name)
469
+ @slice_parser.tag_end(name)
470
+ end
471
+ def on_start_document; end
472
+ def on_comment(msg); end
473
+ def on_processing_instruction(target, data); end
474
+ def on_cdata_block(cdata); end
475
+ def on_end_document; end
476
+ end
477
+
478
+ class RightSlicehostParser #:nodoc:
479
+ # default parsing library
480
+ DEFAULT_XML_LIBRARY = 'rexml'
481
+ # a list of supported parsers
482
+ @@supported_xml_libs = [DEFAULT_XML_LIBRARY, 'libxml']
483
+
484
+ @@xml_lib = DEFAULT_XML_LIBRARY # xml library name: 'rexml' | 'libxml'
485
+ def self.xml_lib
486
+ @@xml_lib
487
+ end
488
+ def self.xml_lib=(new_lib_name)
489
+ @@xml_lib = new_lib_name
490
+ end
491
+
492
+ attr_accessor :result
493
+ attr_reader :xmlpath
494
+ attr_accessor :xml_lib
495
+
496
+ def initialize(params={})
497
+ @xmlpath = ''
498
+ @result = false
499
+ @text = ''
500
+ @xml_lib = params[:xml_lib] || @@xml_lib
501
+ @logger = params[:logger]
502
+ reset
503
+ end
504
+ def tag_start(name, attributes)
505
+ @text = ''
506
+ tagstart(name, attributes)
507
+ @xmlpath += @xmlpath.empty? ? name : "/#{name}"
508
+ end
509
+ def tag_end(name)
510
+ @xmlpath[/^(.*?)\/?#{name}$/]
511
+ @xmlpath = $1
512
+ tagend(name)
513
+ end
514
+ def text(text)
515
+ @text += text
516
+ tagtext(text)
517
+ end
518
+ # Parser method.
519
+ # Params:
520
+ # xml_text - xml message text(String) or Net:HTTPxxx instance (response)
521
+ # params[:xml_lib] - library name: 'rexml' | 'libxml'
522
+ def parse(xml_text, params={})
523
+ # Get response body
524
+ xml_text = xml_text.body unless xml_text.is_a?(String)
525
+ @xml_lib = params[:xml_lib] || @xml_lib
526
+ # check that we had no problems with this library otherwise use default
527
+ @xml_lib = DEFAULT_XML_LIBRARY unless @@supported_xml_libs.include?(@xml_lib)
528
+ # load xml library
529
+ if @xml_lib=='libxml' && !defined?(XML::SaxParser)
530
+ begin
531
+ require 'xml/libxml'
532
+ # is it new ? - Setup SaxParserCallback
533
+ if XML::Parser::VERSION >= '0.5.1.0'
534
+ RightSaxParserCallback.include_callback
535
+ end
536
+ rescue LoadError => e
537
+ @@supported_xml_libs.delete(@xml_lib)
538
+ @xml_lib = DEFAULT_XML_LIBRARY
539
+ if @logger
540
+ @logger.error e.inspect
541
+ @logger.error e.backtrace
542
+ @logger.info "Can not load 'libxml' library. '#{DEFAULT_XML_LIBRARY}' is used for parsing."
543
+ end
544
+ end
545
+ end
546
+ # Parse the xml text
547
+ case @xml_lib
548
+ when 'libxml'
549
+ xml = XML::SaxParser.new
550
+ xml.string = xml_text
551
+ # check libxml-ruby version
552
+ if XML::Parser::VERSION >= '0.5.1.0'
553
+ xml.callbacks = RightSaxParserCallback.new(self)
554
+ else
555
+ xml.on_start_element{|name, attr_hash| self.tag_start(name, attr_hash)}
556
+ xml.on_characters{ |text| self.text(text)}
557
+ xml.on_end_element{ |name| self.tag_end(name)}
558
+ end
559
+ xml.parse
560
+ else
561
+ REXML::Document.parse_stream(xml_text, self)
562
+ end
563
+ end
564
+ # Parser must have a lots of methods
565
+ # (see /usr/lib/ruby/1.8/rexml/parsers/streamparser.rb)
566
+ # We dont need most of them in RightSlicehostParser and method_missing helps us
567
+ # to skip their definition
568
+ def method_missing(method, *params)
569
+ # if the method is one of known - just skip it ...
570
+ return if [:comment, :attlistdecl, :notationdecl, :elementdecl,
571
+ :entitydecl, :cdata, :xmldecl, :attlistdecl, :instruction,
572
+ :doctype].include?(method)
573
+ # ... else - call super to raise an exception
574
+ super(method, params)
575
+ end
576
+ # the functions to be overriden by children (if nessesery)
577
+ def reset ; end
578
+ def tagstart(name, attributes); end
579
+ def tagend(name) ; end
580
+ def tagtext(text) ; end
581
+ end
582
+
583
+ # Dummy parser - does nothing
584
+ # Returns the original params back
585
+ class SlicehostDummyParser # :nodoc:
586
+ attr_accessor :result
587
+ def parse(response)
588
+ @result = response
589
+ end
590
+ end
591
+
592
+ class SliceErrorResponseParser < RightSlicehostParser #:nodoc:
593
+ attr_accessor :errors # array of hashes: error/message
594
+ def tagend(name)
595
+ case name
596
+ when 'error' then @errors << @text
597
+ end
598
+ end
599
+ def reset
600
+ @errors = []
601
+ end
602
+ end
603
+
604
+ end