right_slicehost 0.1.0

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