slap.py 26 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 42 43
import socket
import ssl
import urllib
import urlparse
44

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

50 51 52
from slapos.slap import schlamar_requests as requests


Marco Mariani's avatar
Marco Mariani committed
53
# XXX fallback_logger to be deprecated together with the old CLI entry points.
Marco Mariani's avatar
Marco Mariani committed
54
fallback_logger = logging.getLogger(__name__)
55 56 57
fallback_handler = logging.StreamHandler()
fallback_logger.setLevel(logging.INFO)
fallback_logger.addHandler(fallback_handler)
Łukasz Nowak's avatar
Łukasz Nowak committed
58 59


60
DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
61

Marco Mariani's avatar
Marco Mariani committed
62

Łukasz Nowak's avatar
Łukasz Nowak committed
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
# 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:
85 86 87 88 89
  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
90

Marco Mariani's avatar
Marco Mariani committed
91

92 93 94 95 96 97
class SlapRequester(SlapDocument):
  """
  Abstract class that allow to factor method for subclasses that use "request()"
  """
  def _requestComputerPartition(self, request_dict):
    try:
98
      xml = self._connection_helper.POST('/requestComputerPartition', request_dict)
99 100 101 102 103
    except ResourceNotReady:
      return ComputerPartition(
        request_dict=request_dict,
        connection_helper=self._connection_helper,
      )
104 105 106 107 108 109 110
    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
111 112 113 114
    # XXX Should be removed by correctly specifying difference between
    # ComputerPartition and SoftwareInstance
    computer_partition.__dict__ = dict(computer_partition.__dict__.items() +
                                       software_instance.__dict__.items())
115 116 117 118 119 120
    # 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
121 122


Łukasz Nowak's avatar
Łukasz Nowak committed
123 124 125 126 127 128 129 130 131 132 133 134
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
    """
135
    SlapDocument.__init__(self, kw.pop('connection_helper', None))
Łukasz Nowak's avatar
Łukasz Nowak committed
136 137 138 139 140 141 142 143 144
    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, )

145 146 147 148 149 150 151 152 153 154 155 156
  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

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

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

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

177 178
  def destroyed(self):
    self._connection_helper.POST('/destroyedSoftwareRelease', {
179 180
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
181

182 183 184
  def getState(self):
    return getattr(self, '_requested_state', 'available')

Marco Mariani's avatar
Marco Mariani committed
185

Łukasz Nowak's avatar
Łukasz Nowak committed
186 187 188 189 190 191 192 193 194 195 196 197 198
# 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)

Marco Mariani's avatar
Marco Mariani committed
199

Łukasz Nowak's avatar
Łukasz Nowak committed
200
"""Exposed exceptions"""
Marco Mariani's avatar
Marco Mariani committed
201 202


Łukasz Nowak's avatar
Łukasz Nowak committed
203 204
# XXX Why do we need to expose exceptions?
class ResourceNotReady(Exception):
205
  zope.interface.implements(interface.IResourceNotReady)
Łukasz Nowak's avatar
Łukasz Nowak committed
206

Marco Mariani's avatar
Marco Mariani committed
207

Łukasz Nowak's avatar
Łukasz Nowak committed
208
class ServerError(Exception):
209
  zope.interface.implements(interface.IServerError)
Łukasz Nowak's avatar
Łukasz Nowak committed
210

Marco Mariani's avatar
Marco Mariani committed
211

Łukasz Nowak's avatar
Łukasz Nowak committed
212 213 214
class NotFoundError(Exception):
  zope.interface.implements(interface.INotFoundError)

Marco Mariani's avatar
Marco Mariani committed
215

Łukasz Nowak's avatar
Łukasz Nowak committed
216 217 218 219
class Unauthorized(Exception):
  zope.interface.implements(interface.IUnauthorized)


Marco Mariani's avatar
Marco Mariani committed
220
class Supply(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
221 222
  zope.interface.implements(interface.ISupply)

223
  def supply(self, software_release, computer_guid=None, state='available'):
224 225 226 227 228 229 230 231
    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
232 233


Marco Mariani's avatar
Marco Mariani committed
234
class OpenOrder(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
235 236 237
  zope.interface.implements(interface.IOpenOrder)

  def request(self, software_release, partition_reference,
Marco Mariani's avatar
Marco Mariani committed
238 239
              partition_parameter_kw=None, software_type=None,
              filter_kw=None, state=None, shared=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
240 241
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
242 243
    if filter_kw is None:
      filter_kw = {}
Łukasz Nowak's avatar
Łukasz Nowak committed
244 245 246 247
    request_dict = {
        'software_release': software_release,
        'partition_reference': partition_reference,
        'partition_parameter_xml': xml_marshaller.dumps(partition_parameter_kw),
248
        'filter_xml': xml_marshaller.dumps(filter_kw),
249 250
        # XXX Cedric: Why state and shared are marshalled? First is a string
        #             And second is a boolean.
251
        'state': xml_marshaller.dumps(state),
252
        'shared_xml': xml_marshaller.dumps(shared),
Marco Mariani's avatar
Marco Mariani committed
253
    }
Łukasz Nowak's avatar
Łukasz Nowak committed
254 255
    if software_type is not None:
      request_dict['software_type'] = software_type
256 257 258
    else:
      # Let's enforce a default software type
      request_dict['software_type'] = DEFAULT_SOFTWARE_TYPE
259
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
260

261 262 263 264
  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
265
    xml = self._connection_helper.POST('/requestComputer',
266 267 268 269 270
      {'computer_title': computer_reference})
    computer = xml_marshaller.loads(xml)
    computer._connection_helper = self._connection_helper
    return computer

Marco Mariani's avatar
Marco Mariani committed
271

Łukasz Nowak's avatar
Łukasz Nowak committed
272 273 274 275 276
def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
277 278
    if getattr(self, '_synced', 0):
      return func(self, *args, **kw)
279
    computer = self._connection_helper.getFullComputerInformation(self._computer_id)
Łukasz Nowak's avatar
Łukasz Nowak committed
280 281 282 283 284 285
    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)
286 287 288
    setattr(self, '_synced', True)
    for computer_partition in self.getComputerPartitionList():
      setattr(computer_partition, '_synced', True)
Łukasz Nowak's avatar
Łukasz Nowak committed
289
    return func(self, *args, **kw)
290
  return decorated
Łukasz Nowak's avatar
Łukasz Nowak committed
291 292


Marco Mariani's avatar
Marco Mariani committed
293
class Computer(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
294 295
  zope.interface.implements(interface.IComputer)

296 297
  def __init__(self, computer_id, connection_helper=None):
    SlapDocument.__init__(self, connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
298 299 300 301 302 303 304 305 306 307 308 309 310
    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.
    """
311 312
    for software_relase in self._software_release_list:
      software_relase._connection_helper = self._connection_helper
Łukasz Nowak's avatar
Łukasz Nowak committed
313 314 315 316
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
317 318
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
Marco Mariani's avatar
Marco Mariani committed
319
    return [x for x in self._computer_partition_list]
Łukasz Nowak's avatar
Łukasz Nowak committed
320

321 322
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
323 324 325
      return
    self._connection_helper.POST('/useComputer', {
      'computer_id': self._computer_id,
326
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
327 328

  def updateConfiguration(self, xml):
329
    return self._connection_helper.POST(
Marco Mariani's avatar
Marco Mariani committed
330
        '/loadComputerConfigurationFromXML', {'xml': xml})
Łukasz Nowak's avatar
Łukasz Nowak committed
331

Łukasz Nowak's avatar
Łukasz Nowak committed
332 333 334 335 336
  def bang(self, message):
    self._connection_helper.POST('/computerBang', {
      'computer_id': self._computer_id,
      'message': message})

337
  def getStatus(self):
338
    xml = self._connection_helper.GET(
339
        '/getComputerStatus?computer_id=%s' % self._computer_id)
340
    return xml_marshaller.loads(xml)
341

342 343 344 345 346
  def revokeCertificate(self):
    self._connection_helper.POST('/revokeComputerCertificate', {
      'computer_id': self._computer_id})

  def generateCertificate(self):
347
    xml = self._connection_helper.POST('/generateComputerCertificate', {
348 349 350
      'computer_id': self._computer_id})
    return xml_marshaller.loads(xml)

351

352 353 354 355 356 357 358 359 360 361 362 363 364
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)


365
class ComputerPartition(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
366 367
  zope.interface.implements(interface.IComputerPartition)

Marco Mariani's avatar
Marco Mariani committed
368 369
  def __init__(self, computer_id=None, partition_id=None,
               request_dict=None, connection_helper=None):
370
    SlapDocument.__init__(self, connection_helper)
371 372 373 374 375 376 377
    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
378 379
    self._computer_id = computer_id
    self._partition_id = partition_id
380
    self._request_dict = request_dict
Łukasz Nowak's avatar
Łukasz Nowak committed
381 382 383 384 385

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

  def request(self, software_release, software_type, partition_reference,
386 387
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
388 389 390
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
Marco Mariani's avatar
Marco Mariani committed
391
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" %
Łukasz Nowak's avatar
Łukasz Nowak committed
392 393 394 395 396
                       partition_parameter_kw)

    if filter_kw is None:
      filter_kw = {}
    elif not isinstance(filter_kw, dict):
Marco Mariani's avatar
Marco Mariani committed
397
      raise ValueError("Unexpected type of filter_kw '%s'" %
Łukasz Nowak's avatar
Łukasz Nowak committed
398 399
                       filter_kw)

400 401 402 403
    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

404 405
    request_dict = {
        'computer_id': self._computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
406 407 408 409 410 411 412 413
        '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),
414
        'state': xml_marshaller.dumps(state),
415 416
    }
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
417 418 419 420

  def building(self):
    self._connection_helper.POST('/buildingComputerPartition', {
      'computer_id': self._computer_id,
421
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
422 423 424 425

  def available(self):
    self._connection_helper.POST('/availableComputerPartition', {
      'computer_id': self._computer_id,
426
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
427 428 429 430

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

  def started(self):
    self._connection_helper.POST('/startedComputerPartition', {
      'computer_id': self._computer_id,
437
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
438 439 440 441 442
      })

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

446
  def error(self, error_log, logger=None):
447 448 449 450 451 452
    try:
      self._connection_helper.POST('/softwareInstanceError', {
        'computer_id': self._computer_id,
        'computer_partition_id': self.getId(),
        'error_log': error_log})
    except Exception:
453
      (logger or fallback_logger).exception('')
Łukasz Nowak's avatar
Łukasz Nowak committed
454

Łukasz Nowak's avatar
Łukasz Nowak committed
455 456 457
  def bang(self, message):
    self._connection_helper.POST('/softwareInstanceBang', {
      'computer_id': self._computer_id,
458
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
459 460
      'message': message})

461
  def rename(self, new_name, slave_reference=None):
462 463 464 465 466 467 468
    post_dict = {
            'computer_id': self._computer_id,
            'computer_partition_id': self.getId(),
            'new_name': new_name,
            }
    if slave_reference:
      post_dict['slave_reference'] = slave_reference
469
    self._connection_helper.POST('/softwareInstanceRename', post_dict)
470

Łukasz Nowak's avatar
Łukasz Nowak committed
471
  def getId(self):
472
    if not getattr(self, '_partition_id', None):
473
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
474 475
    return self._partition_id

476
  def getInstanceGuid(self):
477
    """Return instance_guid. Raise ResourceNotReady if it doesn't exist."""
478 479 480 481
    if not getattr(self, '_instance_guid', None):
      raise ResourceNotReady()
    return self._instance_guid

Łukasz Nowak's avatar
Łukasz Nowak committed
482
  def getState(self):
483
    """return _requested_state. Raise ResourceNotReady if it doesn't exist."""
484 485
    if not getattr(self, '_requested_state', None):
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
486 487
    return self._requested_state

488 489 490 491 492 493 494 495 496 497 498 499
  def getType(self):
    """
    return the Software Type of the instance.
    Raise RessourceNotReady if not present.
    """
    # XXX: software type should not belong to the parameter dict.
    software_type = self.getInstanceParameterDict().get(
        'slap_software_type', None)
    if not software_type:
      raise ResourceNotReady()
    return software_type

Łukasz Nowak's avatar
Łukasz Nowak committed
500 501 502
  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

503 504 505
  def getConnectionParameterDict(self):
    return getattr(self, '_connection_dict', None) or {}

Łukasz Nowak's avatar
Łukasz Nowak committed
506 507 508 509
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
510
    if not getattr(self, '_software_release_document', None):
Łukasz Nowak's avatar
Łukasz Nowak committed
511 512 513 514 515
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

516
  def setConnectionDict(self, connection_dict, slave_reference=None):
517 518 519 520 521 522
    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
523

524 525 526 527 528 529 530
  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
531 532 533 534 535 536 537 538 539 540 541 542
  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):
543
    xml = self._connection_helper.GET(
Łukasz Nowak's avatar
Łukasz Nowak committed
544 545
        '/getComputerPartitionCertificate?computer_id=%s&'
        'computer_partition_id=%s' % (self._computer_id, self._partition_id))
546
    return xml_marshaller.loads(xml)
547 548

  def getStatus(self):
549
    xml = self._connection_helper.GET(
550 551
        '/getComputerPartitionStatus?computer_id=%s&'
        'computer_partition_id=%s' % (self._computer_id, self._partition_id))
552
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
553

Marco Mariani's avatar
Marco Mariani committed
554

Łukasz Nowak's avatar
Łukasz Nowak committed
555
class ConnectionHelper:
556
  error_message_timeout = "\nThe connection timed out. Please try again later."
557 558 559 560
  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: "
561
  ssl_error_message_connect_fail = "\nCouldn't authenticate computer. Please "\
562
      "check that certificate and key exist and are valid. "
Marco Mariani's avatar
Marco Mariani committed
563

Łukasz Nowak's avatar
Łukasz Nowak committed
564
  def __init__(self, connection_wrapper, host, path, key_file=None,
Marco Mariani's avatar
Marco Mariani committed
565
               cert_file=None, master_ca_file=None, timeout=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
566 567 568 569 570 571
    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
572
    self.timeout = timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
573 574

  def getComputerInformation(self, computer_id):
575 576
    xml = self.GET('/getComputerInformation?computer_id=%s' % computer_id)
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
577

578
  def getFullComputerInformation(self, computer_id):
579 580 581 582 583 584 585 586
    """
    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)
587
    try:
588
      xml = self.GET(method)
589 590 591
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
592
      xml = self.GET('/getComputerInformation?computer_id=%s' % computer_id)
593

594
    return xml_marshaller.loads(xml)
595

Łukasz Nowak's avatar
Łukasz Nowak committed
596
  def connect(self):
597 598 599
    connection_dict = {
            'host': self.host
            }
Łukasz Nowak's avatar
Łukasz Nowak committed
600
    if self.key_file and self.cert_file:
601 602 603 604
      connection_dict['key_file'] = self.key_file
      connection_dict['cert_file'] = self.cert_file
    if self.master_ca_file:
      connection_dict['ca_file'] = self.master_ca_file
Łukasz Nowak's avatar
Łukasz Nowak committed
605 606 607 608
    self.connection = self.connection_wrapper(**connection_dict)

  def GET(self, path):
    try:
609 610 611 612 613
      default_timeout = socket.getdefaulttimeout()
      socket.setdefaulttimeout(self.timeout)
      try:
        self.connect()
        self.connection.request('GET', self.path + path)
614
        response = self.connection.getresponse()
615
      # If ssl error : may come from bad configuration
616 617 618 619 620 621 622 623
      except ssl.SSLError as exc:
        if exc.message == 'The read operation timed out':
          raise socket.error(str(exc) + self.error_message_timeout)
        raise ssl.SSLError(str(exc) + self.ssl_error_message_connect_fail)
      except socket.error as exc:
        if exc.message == 'timed out':
          raise socket.error(str(exc) + self.error_message_timeout)
        raise socket.error(self.error_message_connect_fail + str(exc))
624

625
      # check self.response.status and raise exception early
626
      if response.status == httplib.REQUEST_TIMEOUT:
627 628
        # resource is not ready
        raise ResourceNotReady(path)
629
      elif response.status == httplib.NOT_FOUND:
630
        raise NotFoundError(path)
631
      elif response.status == httplib.FORBIDDEN:
632
        raise Unauthorized(path)
633 634 635
      elif response.status != httplib.OK:
        message = parsed_error_message(response.status,
                                       response.read(),
636
                                       path)
637 638 639
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
640

641 642
    return response.read()

Łukasz Nowak's avatar
Łukasz Nowak committed
643
  def POST(self, path, parameter_dict,
Marco Mariani's avatar
Marco Mariani committed
644
           content_type='application/x-www-form-urlencoded'):
Łukasz Nowak's avatar
Łukasz Nowak committed
645
    try:
646 647 648 649 650 651 652
      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)
653
      # If ssl error : must come from bad configuration
654 655 656 657
      except ssl.SSLError as exc:
        raise ssl.SSLError(self.ssl_error_message_connect_fail + str(exc))
      except socket.error as exc:
        raise socket.error(self.error_message_connect_fail + str(exc))
658 659

      response = self.connection.getresponse()
660
      # check self.response.status and raise exception early
661
      if response.status == httplib.REQUEST_TIMEOUT:
662 663
        # resource is not ready
        raise ResourceNotReady("%s - %s" % (path, parameter_dict))
664
      elif response.status == httplib.NOT_FOUND:
665
        raise NotFoundError("%s - %s" % (path, parameter_dict))
666
      elif response.status == httplib.FORBIDDEN:
667
        raise Unauthorized("%s - %s" % (path, parameter_dict))
668 669 670
      elif response.status != httplib.OK:
        message = parsed_error_message(response.status,
                                       response.read(),
671
                                       path)
672 673 674
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
675

676 677 678
    return response.read()


Łukasz Nowak's avatar
Łukasz Nowak committed
679 680 681 682
class slap:
  zope.interface.implements(interface.slap)

  def initializeConnection(self, slapgrid_uri, key_file=None, cert_file=None,
Marco Mariani's avatar
Marco Mariani committed
683 684 685 686
                           master_ca_file=None, timeout=60):
    scheme, netloc, path, query, fragment = urlparse.urlsplit(slapgrid_uri)
    if not (query == '' and fragment == ''):
      raise AttributeError('Passed URL %r issue: not parseable' % slapgrid_uri)
Łukasz Nowak's avatar
Łukasz Nowak committed
687 688 689

    if scheme == 'http':
      connection_wrapper = httplib.HTTPConnection
690
    elif scheme == 'https':
Łukasz Nowak's avatar
Łukasz Nowak committed
691 692 693 694
      if master_ca_file is not None:
        connection_wrapper = HTTPSConnectionCA
      else:
        connection_wrapper = httplib.HTTPSConnection
695
    else:
Marco Mariani's avatar
Marco Mariani committed
696 697
      raise AttributeError('Passed URL %r issue: there is no support '
                           'for %r protocol' % (slapgrid_uri, scheme))
698
    self._connection_helper = ConnectionHelper(connection_wrapper,
699
          netloc, path, key_file, cert_file, master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
700

701
  # XXX-Cedric: this method is never used and thus should be removed.
Łukasz Nowak's avatar
Łukasz Nowak committed
702 703 704 705 706
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
707
    return SoftwareRelease(software_release=software_release,
708
      connection_helper=self._connection_helper
709
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
710 711 712 713 714 715

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
716
    return Computer(computer_guid, connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
717 718 719 720 721 722

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

727
    xml = self._connection_helper.GET('/registerComputerPartition?' \
Łukasz Nowak's avatar
Łukasz Nowak committed
728 729
        'computer_reference=%s&computer_partition_reference=%s' % (
          computer_guid, partition_id))
730
    result = xml_marshaller.loads(xml)
731 732 733 734
    # 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
735 736

  def registerOpenOrder(self):
737
    return OpenOrder(connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
738 739

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