slap.py 25.8 KB
Newer Older
Łukasz Nowak's avatar
Łukasz Nowak committed
1 2 3
# -*- coding: utf-8 -*-
##############################################################################
#
4 5
# Copyright (c) 2010, 2011, 2012 Vifib SARL and Contributors.
# All Rights Reserved.
Łukasz Nowak's avatar
Łukasz Nowak committed
6 7 8 9 10 11 12 13 14
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
15 16
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1
Łukasz Nowak's avatar
Łukasz Nowak committed
17 18 19 20 21 22 23
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
24
# You should have received a copy of the GNU Lesser General Public License
Łukasz Nowak's avatar
Łukasz Nowak committed
25 26 27 28
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
29 30 31 32
"""
Simple, easy to (un)marshall classes for slap client/server communication
"""

Łukasz Nowak's avatar
Łukasz Nowak committed
33 34
__all__ = ["slap", "ComputerPartition", "Computer", "SoftwareRelease",
           "Supply", "OpenOrder", "NotFoundError", "Unauthorized",
35
           "ResourceNotReady", "ServerError"]
Łukasz Nowak's avatar
Łukasz Nowak committed
36 37

import httplib
38
import logging
39
import re
Łukasz Nowak's avatar
Łukasz Nowak committed
40 41
import socket
import ssl
42
import traceback
Łukasz Nowak's avatar
Łukasz Nowak committed
43 44
import urllib
import urlparse
45

46
from xml.sax import saxutils
Łukasz Nowak's avatar
Łukasz Nowak committed
47
import zope.interface
48 49 50
from interface import slap as interface
from xml_marshaller import xml_marshaller

Marco Mariani's avatar
Marco Mariani committed
51
fallback_logger = logging.getLogger(__name__)
52 53 54
fallback_handler = logging.StreamHandler()
fallback_logger.setLevel(logging.INFO)
fallback_logger.addHandler(fallback_handler)
Łukasz Nowak's avatar
Łukasz Nowak committed
55 56


57
DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
58

Łukasz Nowak's avatar
Łukasz Nowak committed
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
# httplib.HTTPSConnection with key verification
class HTTPSConnectionCA(httplib.HTTPSConnection):
  """Patched version of HTTPSConnection which verifies server certificate"""
  def __init__(self, *args, **kwargs):
    self.ca_file = kwargs.pop('ca_file')
    if self.ca_file is None:
      raise ValueError('ca_file is required argument.')
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)

  def connect(self):
    "Connect to a host on a given (SSL) port and verify its certificate."

    sock = socket.create_connection((self.host, self.port),
                                    self.timeout, self.source_address)
    if self._tunnel_host:
      self.sock = sock
      self._tunnel()
    self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file,
        ca_certs=self.ca_file, cert_reqs=ssl.CERT_REQUIRED)


class SlapDocument:
81 82 83 84 85
  def __init__(self, connection_helper=None):
    if connection_helper is not None:
      # Do not require connection_helper to be provided, but when it's not,
      # cause failures when accessing _connection_helper property.
      self._connection_helper = connection_helper
Łukasz Nowak's avatar
Łukasz Nowak committed
86

87 88 89 90 91 92 93 94 95 96 97 98
class SlapRequester(SlapDocument):
  """
  Abstract class that allow to factor method for subclasses that use "request()"
  """
  def _requestComputerPartition(self, request_dict):
    try:
      self._connection_helper.POST('/requestComputerPartition', request_dict)
    except ResourceNotReady:
      return ComputerPartition(
        request_dict=request_dict,
        connection_helper=self._connection_helper,
      )
99 100 101 102 103 104 105 106
    xml = self._connection_helper.response.read()
    software_instance = xml_marshaller.loads(xml)
    computer_partition = ComputerPartition(
      software_instance.slap_computer_id.encode('UTF-8'),
      software_instance.slap_computer_partition_id.encode('UTF-8'),
      connection_helper=self._connection_helper,
    )
    # Hack to give all object attributes to the ComputerPartition instance
107 108 109 110
    # XXX Should be removed by correctly specifying difference between
    # ComputerPartition and SoftwareInstance
    computer_partition.__dict__ = dict(computer_partition.__dict__.items() +
                                       software_instance.__dict__.items())
111 112 113 114 115 116
    # XXX not generic enough.
    if xml_marshaller.loads(request_dict['shared_xml']):
      computer_partition._synced = True
      computer_partition._connection_dict = software_instance._connection_dict
      computer_partition._parameter_dict = software_instance._parameter_dict
    return computer_partition
117 118


Łukasz Nowak's avatar
Łukasz Nowak committed
119 120 121 122 123 124 125 126 127 128 129 130 131
class SoftwareRelease(SlapDocument):
  """
  Contains Software Release information
  """

  zope.interface.implements(interface.ISoftwareRelease)

  def __init__(self, software_release=None, computer_guid=None, **kw):
    """
    Makes easy initialisation of class parameters

    XXX **kw args only kept for compatibility
    """
132
    SlapDocument.__init__(self, kw.pop('connection_helper', None))
Łukasz Nowak's avatar
Łukasz Nowak committed
133 134 135 136 137 138 139 140 141
    self._software_instance_list = []
    if software_release is not None:
      software_release = software_release.encode('UTF-8')
    self._software_release = software_release
    self._computer_guid = computer_guid

  def __getinitargs__(self):
    return (self._software_release, self._computer_guid, )

142 143 144 145 146 147 148 149 150 151 152 153
  def getComputerId(self):
    if not self._computer_guid:
      raise NameError('computer_guid has not been defined.')
    else:
      return self._computer_guid

  def getURI(self):
    if not self._software_release:
      raise NameError('software_release has not been defined.')
    else:
      return self._software_release

154
  def error(self, error_log, logger=None):
155 156 157 158 159 160 161
    try:
      # Does not follow interface
      self._connection_helper.POST('/softwareReleaseError', {
        'url': self.getURI(),
        'computer_id' : self.getComputerId(),
        'error_log': error_log})
    except Exception:
162
      (logger or fallback_logger).error(traceback.format_exc())
Łukasz Nowak's avatar
Łukasz Nowak committed
163 164 165

  def available(self):
    self._connection_helper.POST('/availableSoftwareRelease', {
166 167
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
168 169 170

  def building(self):
    self._connection_helper.POST('/buildingSoftwareRelease', {
171 172
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
173

174 175
  def destroyed(self):
    self._connection_helper.POST('/destroyedSoftwareRelease', {
176 177
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
178

179 180 181
  def getState(self):
    return getattr(self, '_requested_state', 'available')

Łukasz Nowak's avatar
Łukasz Nowak committed
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197
# XXX What is this SoftwareInstance class?
class SoftwareInstance(SlapDocument):
  """
  Contains Software Instance information
  """

  def __init__(self, **kwargs):
    """
    Makes easy initialisation of class parameters
    """
    for k, v in kwargs.iteritems():
      setattr(self, k, v)

"""Exposed exceptions"""
# XXX Why do we need to expose exceptions?
class ResourceNotReady(Exception):
198
  zope.interface.implements(interface.IResourceNotReady)
Łukasz Nowak's avatar
Łukasz Nowak committed
199 200

class ServerError(Exception):
201
  zope.interface.implements(interface.IServerError)
Łukasz Nowak's avatar
Łukasz Nowak committed
202 203 204 205 206 207 208 209 210 211 212

class NotFoundError(Exception):
  zope.interface.implements(interface.INotFoundError)

class Unauthorized(Exception):
  zope.interface.implements(interface.IUnauthorized)

class Supply(SlapDocument):

  zope.interface.implements(interface.ISupply)

213
  def supply(self, software_release, computer_guid=None, state='available'):
214 215 216 217 218 219 220 221
    try:
      self._connection_helper.POST('/supplySupply', {
        'url': software_release,
        'computer_id': computer_guid,
        'state': state})
    except NotFoundError:
      raise NotFoundError("Computer %s has not been found by SlapOS Master."
          % computer_guid)
Łukasz Nowak's avatar
Łukasz Nowak committed
222

223
class OpenOrder(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
224 225 226 227

  zope.interface.implements(interface.IOpenOrder)

  def request(self, software_release, partition_reference,
228
      partition_parameter_kw=None, software_type=None, filter_kw=None,
229
      state=None, shared=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
230 231
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
232 233
    if filter_kw is None:
      filter_kw = {}
Łukasz Nowak's avatar
Łukasz Nowak committed
234 235 236 237
    request_dict = {
        'software_release': software_release,
        'partition_reference': partition_reference,
        'partition_parameter_xml': xml_marshaller.dumps(partition_parameter_kw),
238
        'filter_xml': xml_marshaller.dumps(filter_kw),
239 240
        # XXX Cedric: Why state and shared are marshalled? First is a string
        #             And second is a boolean.
241
        'state': xml_marshaller.dumps(state),
242
        'shared_xml': xml_marshaller.dumps(shared),
Łukasz Nowak's avatar
Łukasz Nowak committed
243 244 245
      }
    if software_type is not None:
      request_dict['software_type'] = software_type
246 247 248
    else:
      # Let's enforce a default software type
      request_dict['software_type'] = DEFAULT_SOFTWARE_TYPE
249
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
250

251 252 253 254 255 256 257 258 259 260 261
  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
    self._connection_helper.POST('/requestComputer',
      {'computer_title': computer_reference})
    xml = self._connection_helper.response.read()
    computer = xml_marshaller.loads(xml)
    computer._connection_helper = self._connection_helper
    return computer

Łukasz Nowak's avatar
Łukasz Nowak committed
262 263 264 265 266
def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
267 268
    if getattr(self, '_synced', 0):
      return func(self, *args, **kw)
269
    computer = self._connection_helper.getFullComputerInformation(self._computer_id)
Łukasz Nowak's avatar
Łukasz Nowak committed
270 271 272 273 274 275
    for key, value in computer.__dict__.items():
      if isinstance(value, unicode):
        # convert unicode to utf-8
        setattr(self, key, value.encode('utf-8'))
      else:
        setattr(self, key, value)
276 277 278
    setattr(self, '_synced', True)
    for computer_partition in self.getComputerPartitionList():
      setattr(computer_partition, '_synced', True)
Łukasz Nowak's avatar
Łukasz Nowak committed
279
    return func(self, *args, **kw)
280
  return decorated
Łukasz Nowak's avatar
Łukasz Nowak committed
281 282 283 284 285

class Computer(SlapDocument):

  zope.interface.implements(interface.IComputer)

286 287
  def __init__(self, computer_id, connection_helper=None):
    SlapDocument.__init__(self, connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
288 289 290 291 292 293 294 295 296 297 298 299 300
    self._computer_id = computer_id

  def __getinitargs__(self):
    return (self._computer_id, )

  @_syncComputerInformation
  def getSoftwareReleaseList(self):
    """
    Returns the list of software release which has to be supplied by the
    computer.

    Raise an INotFoundError if computer_guid doesn't exist.
    """
301 302
    for software_relase in self._software_release_list:
      software_relase._connection_helper = self._connection_helper
Łukasz Nowak's avatar
Łukasz Nowak committed
303 304 305 306
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
307 308
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
309
    return [x for x in self._computer_partition_list ]
Łukasz Nowak's avatar
Łukasz Nowak committed
310

311 312
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
313 314 315
      return
    self._connection_helper.POST('/useComputer', {
      'computer_id': self._computer_id,
316
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
317 318 319 320 321 322

  def updateConfiguration(self, xml):
    self._connection_helper.POST(
        '/loadComputerConfigurationFromXML', { 'xml' : xml })
    return self._connection_helper.response.read()

Łukasz Nowak's avatar
Łukasz Nowak committed
323 324 325 326 327
  def bang(self, message):
    self._connection_helper.POST('/computerBang', {
      'computer_id': self._computer_id,
      'message': message})

328 329 330 331 332
  def getStatus(self):
    self._connection_helper.GET(
        '/getComputerStatus?computer_id=%s' % self._computer_id)
    return xml_marshaller.loads(self._connection_helper.response.read())

333 334 335 336 337 338 339 340 341 342
  def revokeCertificate(self):
    self._connection_helper.POST('/revokeComputerCertificate', {
      'computer_id': self._computer_id})

  def generateCertificate(self):
    self._connection_helper.POST('/generateComputerCertificate', {
      'computer_id': self._computer_id})
    xml = self._connection_helper.response.read()
    return xml_marshaller.loads(xml)

343

344 345 346 347 348 349 350 351 352 353 354 355 356
def parsed_error_message(status, body, path):
  m = re.search('(Error Value:\n.*)', body, re.MULTILINE)
  if m:
    match = ' '.join(line.strip() for line in m.group(0).split('\n'))
    return '%s (status %s while calling %s)' % (
                saxutils.unescape(match),
                status,
                path
            )
  else:
    return 'Server responded with wrong code %s with %s' % (status, path)


357
class ComputerPartition(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
358 359 360

  zope.interface.implements(interface.IComputerPartition)

361 362 363
  def __init__(self, computer_id=None, partition_id=None, request_dict=None,
      connection_helper=None):
    SlapDocument.__init__(self, connection_helper)
364 365 366 367 368 369 370
    if request_dict is not None and (computer_id is not None or
        partition_id is not None):
      raise TypeError('request_dict conflicts with computer_id and '
        'partition_id')
    if request_dict is None and (computer_id is None or partition_id is None):
      raise TypeError('computer_id and partition_id or request_dict are '
        'required')
Łukasz Nowak's avatar
Łukasz Nowak committed
371 372
    self._computer_id = computer_id
    self._partition_id = partition_id
373
    self._request_dict = request_dict
Łukasz Nowak's avatar
Łukasz Nowak committed
374 375 376 377 378

  def __getinitargs__(self):
    return (self._computer_id, self._partition_id, )

  def request(self, software_release, software_type, partition_reference,
379 380
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
381 382 383 384 385 386 387 388 389 390 391 392
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" % \
                       partition_parameter_kw)

    if filter_kw is None:
      filter_kw = {}
    elif not isinstance(filter_kw, dict):
      raise ValueError("Unexpected type of filter_kw '%s'" % \
                       filter_kw)

393 394 395 396
    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

397 398
    request_dict = {
        'computer_id': self._computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
399 400 401 402 403 404 405 406
        'computer_partition_id': self._partition_id,
        'software_release': software_release,
        'software_type': software_type,
        'partition_reference': partition_reference,
        'shared_xml': xml_marshaller.dumps(shared),
        'partition_parameter_xml': xml_marshaller.dumps(
                                        partition_parameter_kw),
        'filter_xml': xml_marshaller.dumps(filter_kw),
407
        'state': xml_marshaller.dumps(state),
408 409
    }
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
410 411 412 413

  def building(self):
    self._connection_helper.POST('/buildingComputerPartition', {
      'computer_id': self._computer_id,
414
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
415 416 417 418

  def available(self):
    self._connection_helper.POST('/availableComputerPartition', {
      'computer_id': self._computer_id,
419
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
420 421 422 423

  def destroyed(self):
    self._connection_helper.POST('/destroyedComputerPartition', {
      'computer_id': self._computer_id,
424
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
425 426 427 428 429
      })

  def started(self):
    self._connection_helper.POST('/startedComputerPartition', {
      'computer_id': self._computer_id,
430
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
431 432 433 434 435
      })

  def stopped(self):
    self._connection_helper.POST('/stoppedComputerPartition', {
      'computer_id': self._computer_id,
436
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
437 438
      })

439
  def error(self, error_log, logger=None):
440 441 442 443 444 445
    try:
      self._connection_helper.POST('/softwareInstanceError', {
        'computer_id': self._computer_id,
        'computer_partition_id': self.getId(),
        'error_log': error_log})
    except Exception:
446
      (logger or fallback_logger).error(traceback.format_exc())
Łukasz Nowak's avatar
Łukasz Nowak committed
447

Łukasz Nowak's avatar
Łukasz Nowak committed
448 449 450
  def bang(self, message):
    self._connection_helper.POST('/softwareInstanceBang', {
      'computer_id': self._computer_id,
451
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
452 453
      'message': message})

454
  def rename(self, new_name, slave_reference=None):
455 456
    post_dict = dict(
      computer_id=self._computer_id,
457
      computer_partition_id=self.getId(),
458 459 460 461 462
      new_name=new_name,
    )
    if slave_reference is not None:
      post_dict.update(slave_reference=slave_reference)
    self._connection_helper.POST('/softwareInstanceRename', post_dict)
463

Łukasz Nowak's avatar
Łukasz Nowak committed
464
  def getId(self):
465
    if not getattr(self, '_partition_id', None):
466
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
467 468
    return self._partition_id

469
  def getInstanceGuid(self):
470
    """Return instance_guid. Raise ResourceNotReady if it doesn't exist."""
471 472 473 474
    if not getattr(self, '_instance_guid', None):
      raise ResourceNotReady()
    return self._instance_guid

Łukasz Nowak's avatar
Łukasz Nowak committed
475
  def getState(self):
476
    """return _requested_state. Raise ResourceNotReady if it doesn't exist."""
477 478
    if not getattr(self, '_requested_state', None):
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
479 480 481 482 483
    return self._requested_state

  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

484 485 486
  def getConnectionParameterDict(self):
    return getattr(self, '_connection_dict', None) or {}

Łukasz Nowak's avatar
Łukasz Nowak committed
487 488 489 490
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
491
    if not getattr(self, '_software_release_document', None):
Łukasz Nowak's avatar
Łukasz Nowak committed
492 493 494 495 496
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

497
  def setConnectionDict(self, connection_dict, slave_reference=None):
498 499 500 501 502 503
    if self.getConnectionParameterDict() != connection_dict:
      self._connection_helper.POST('/setComputerPartitionConnectionXml', {
          'computer_id': self._computer_id,
          'computer_partition_id': self._partition_id,
          'connection_xml': xml_marshaller.dumps(connection_dict),
          'slave_reference': slave_reference})
Łukasz Nowak's avatar
Łukasz Nowak committed
504

505 506 507 508 509 510 511
  def getInstanceParameter(self, key):
    parameter_dict = getattr(self, '_parameter_dict', None) or {}
    if key in parameter_dict:
      return parameter_dict[key]
    else:
      raise NotFoundError("%s not found" % key)

Łukasz Nowak's avatar
Łukasz Nowak committed
512 513 514 515 516 517 518 519 520 521 522 523 524 525 526
  def getConnectionParameter(self, key):
    connection_dict = getattr(self, '_connection_dict', None) or {}
    if key in connection_dict:
      return connection_dict[key]
    else:
      raise NotFoundError("%s not found" % key)

  def setUsage(self, usage_log):
    # XXX: this implementation has not been reviewed
    self.usage = usage_log

  def getCertificate(self):
    self._connection_helper.GET(
        '/getComputerPartitionCertificate?computer_id=%s&'
        'computer_partition_id=%s' % (self._computer_id, self._partition_id))
527 528 529 530 531 532
    return xml_marshaller.loads(self._connection_helper.response.read())

  def getStatus(self):
    self._connection_helper.GET(
        '/getComputerPartitionStatus?computer_id=%s&'
        'computer_partition_id=%s' % (self._computer_id, self._partition_id))
Łukasz Nowak's avatar
Łukasz Nowak committed
533 534 535
    return xml_marshaller.loads(self._connection_helper.response.read())

class ConnectionHelper:
536
  error_message_timeout = "\nThe connection timed out. Please try again later."
537 538 539 540
  error_message_connect_fail = "Couldn't connect to the server. Please " \
      "double check given master-url argument, and make sure that IPv6 is " \
      "enabled on your machine and that the server is available. The " \
      "original error was: "
541
  ssl_error_message_connect_fail = "\nCouldn't authenticate computer. Please "\
542
      "check that certificate and key exist and are valid. "
Łukasz Nowak's avatar
Łukasz Nowak committed
543
  def __init__(self, connection_wrapper, host, path, key_file=None,
544
      cert_file=None, master_ca_file=None, timeout=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
545 546 547 548 549 550
    self.connection_wrapper = connection_wrapper
    self.host = host
    self.path = path
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
551
    self.timeout = timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
552 553 554 555 556

  def getComputerInformation(self, computer_id):
    self.GET('/getComputerInformation?computer_id=%s' % computer_id)
    return xml_marshaller.loads(self.response.read())

557
  def getFullComputerInformation(self, computer_id):
558 559 560 561 562 563 564 565
    """
    Retrieve from SlapOS Master Computer instance containing all needed
    informations (Software Releases, Computer Partitions, ...).
    """
    method = '/getFullComputerInformation?computer_id=%s' % computer_id
    if not computer_id:
      # XXX-Cedric: should raise something smarter than "NotFound".
      raise NotFoundError(method)
566 567 568 569 570 571 572
    try:
      self.GET(method)
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
      self.GET('/getComputerInformation?computer_id=%s' % computer_id)

573 574
    return xml_marshaller.loads(self.response.read())

Łukasz Nowak's avatar
Łukasz Nowak committed
575 576 577 578 579 580 581 582 583 584 585 586 587
  def connect(self):
    connection_dict = dict(
        host=self.host)
    if self.key_file and self.cert_file:
      connection_dict.update(
        key_file=self.key_file,
        cert_file=self.cert_file)
    if self.master_ca_file is not None:
      connection_dict.update(ca_file=self.master_ca_file)
    self.connection = self.connection_wrapper(**connection_dict)

  def GET(self, path):
    try:
588 589 590 591 592 593
      default_timeout = socket.getdefaulttimeout()
      socket.setdefaulttimeout(self.timeout)
      try:
        self.connect()
        self.connection.request('GET', self.path + path)
        self.response = self.connection.getresponse()
594
      # If ssl error : may come from bad configuration
595
      except ssl.SSLError, e:
596 597 598
        if e.message == "The read operation timed out":
          raise socket.error(str(e) + self.error_message_timeout)
        raise ssl.SSLError(str(e) + self.ssl_error_message_connect_fail)
599
      except socket.error, e:
600 601
        if e.message == "timed out":
          raise socket.error(str(e) + self.error_message_timeout)
602 603 604 605 606 607 608 609 610 611
        raise socket.error(self.error_message_connect_fail + str(e))
      # check self.response.status and raise exception early
      if self.response.status == httplib.REQUEST_TIMEOUT:
        # resource is not ready
        raise ResourceNotReady(path)
      elif self.response.status == httplib.NOT_FOUND:
        raise NotFoundError(path)
      elif self.response.status == httplib.FORBIDDEN:
        raise Unauthorized(path)
      elif self.response.status != httplib.OK:
612 613 614
        message = parsed_error_message(self.response.status,
                                       self.response.read(),
                                       path)
615 616 617
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
618 619 620 621

  def POST(self, path, parameter_dict,
      content_type="application/x-www-form-urlencoded"):
    try:
622 623 624 625 626 627 628
      default_timeout = socket.getdefaulttimeout()
      socket.setdefaulttimeout(self.timeout)
      try:
        self.connect()
        header_dict = {'Content-type': content_type}
        self.connection.request("POST", self.path + path,
            urllib.urlencode(parameter_dict), header_dict)
629 630 631
      # If ssl error : must come from bad configuration
      except ssl.SSLError, e:
        raise ssl.SSLError(self.ssl_error_message_connect_fail + str(e))
632 633 634 635 636 637 638 639 640 641 642 643
      except socket.error, e:
        raise socket.error(self.error_message_connect_fail + str(e))
      self.response = self.connection.getresponse()
      # check self.response.status and raise exception early
      if self.response.status == httplib.REQUEST_TIMEOUT:
        # resource is not ready
        raise ResourceNotReady("%s - %s" % (path, parameter_dict))
      elif self.response.status == httplib.NOT_FOUND:
        raise NotFoundError("%s - %s" % (path, parameter_dict))
      elif self.response.status == httplib.FORBIDDEN:
        raise Unauthorized("%s - %s" % (path, parameter_dict))
      elif self.response.status != httplib.OK:
644 645 646
        message = parsed_error_message(self.response.status,
                                       self.response.read(),
                                       path)
647 648 649
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
650 651 652 653 654 655

class slap:

  zope.interface.implements(interface.slap)

  def initializeConnection(self, slapgrid_uri, key_file=None, cert_file=None,
656
      master_ca_file=None, timeout=60):
Łukasz Nowak's avatar
Łukasz Nowak committed
657
    scheme, netloc, path, query, fragment = urlparse.urlsplit(
658
        slapgrid_uri)
Łukasz Nowak's avatar
Łukasz Nowak committed
659 660
    if not(query == '' and fragment == ''):
      raise AttributeError('Passed URL %r issue: not parseable'%
661
          slapgrid_uri)
Łukasz Nowak's avatar
Łukasz Nowak committed
662 663 664

    if scheme == 'http':
      connection_wrapper = httplib.HTTPConnection
665
    elif scheme == 'https':
Łukasz Nowak's avatar
Łukasz Nowak committed
666 667 668 669
      if master_ca_file is not None:
        connection_wrapper = HTTPSConnectionCA
      else:
        connection_wrapper = httplib.HTTPSConnection
670 671 672
    else:
      raise AttributeError('Passed URL %r issue: there is no support for %r p'
          'rotocol' % (slapgrid_uri, scheme))
673
    self._connection_helper = ConnectionHelper(connection_wrapper,
674
          netloc, path, key_file, cert_file, master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
675

676
  # XXX-Cedric: this method is never used and thus should be removed.
Łukasz Nowak's avatar
Łukasz Nowak committed
677 678 679 680 681
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
682
    return SoftwareRelease(software_release=software_release,
683
      connection_helper=self._connection_helper
684
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
685 686 687 688 689 690

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
691
    return Computer(computer_guid, connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
692 693 694 695 696 697

  def registerComputerPartition(self, computer_guid, partition_id):
    """
    Registers connected representation of computer partition and
    returns Computer Partition class object
    """
698 699 700 701
    if not computer_guid or not partition_id:
      # XXX-Cedric: should raise something smarter than NotFound
      raise NotFoundError

Łukasz Nowak's avatar
Łukasz Nowak committed
702 703 704
    self._connection_helper.GET('/registerComputerPartition?' \
        'computer_reference=%s&computer_partition_reference=%s' % (
          computer_guid, partition_id))
705 706 707 708 709
    result = xml_marshaller.loads(self._connection_helper.response.read())
    # XXX: dirty hack to make computer partition usable. xml_marshaller is too
    # low-level for our needs here.
    result._connection_helper = self._connection_helper
    return result
Łukasz Nowak's avatar
Łukasz Nowak committed
710 711

  def registerOpenOrder(self):
712
    return OpenOrder(connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
713 714

  def registerSupply(self):
715
    return Supply(connection_helper=self._connection_helper)