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
__all__ = ["slap", "ComputerPartition", "Computer", "SoftwareRelease",
34
           "SoftwareProductCollection",
35
           "Supply", "OpenOrder", "NotFoundError",
36
           "ResourceNotReady", "ServerError"]
Łukasz Nowak's avatar
Łukasz Nowak committed
37

38
import logging
39
import re
Łukasz Nowak's avatar
Łukasz Nowak committed
40
import urlparse
41
from util import xml2dict
42

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

48 49 50 51 52 53
import requests
# silence messages like 'Starting connection' that are logged with INFO
urllib3_logger = logging.getLogger('requests.packages.urllib3')
urllib3_logger.setLevel(logging.WARNING)


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


61
DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
62

Marco Mariani's avatar
Marco Mariani committed
63

64 65
class AuthenticationError(Exception):
  pass
Łukasz Nowak's avatar
Łukasz Nowak committed
66 67


68 69
class ConnectionError(Exception):
  pass
Łukasz Nowak's avatar
Łukasz Nowak committed
70 71 72


class SlapDocument:
73 74 75 76 77
  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
78

Marco Mariani's avatar
Marco Mariani committed
79

80 81 82 83 84 85
class SlapRequester(SlapDocument):
  """
  Abstract class that allow to factor method for subclasses that use "request()"
  """
  def _requestComputerPartition(self, request_dict):
    try:
86
      xml = self._connection_helper.POST('requestComputerPartition', data=request_dict)
87 88 89 90 91
    except ResourceNotReady:
      return ComputerPartition(
        request_dict=request_dict,
        connection_helper=self._connection_helper,
      )
92 93 94 95 96 97 98
    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
99 100 101 102
    # XXX Should be removed by correctly specifying difference between
    # ComputerPartition and SoftwareInstance
    computer_partition.__dict__ = dict(computer_partition.__dict__.items() +
                                       software_instance.__dict__.items())
103 104 105 106 107 108
    # 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
109 110


Łukasz Nowak's avatar
Łukasz Nowak committed
111 112 113 114 115 116 117 118 119 120 121 122
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
    """
123
    SlapDocument.__init__(self, kw.pop('connection_helper', None))
Łukasz Nowak's avatar
Łukasz Nowak committed
124 125 126 127 128 129 130 131 132
    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, )

133 134 135 136 137 138 139 140 141 142 143 144
  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

145
  def error(self, error_log, logger=None):
146 147
    try:
      # Does not follow interface
148
      self._connection_helper.POST('softwareReleaseError', data={
149
        'url': self.getURI(),
Marco Mariani's avatar
Marco Mariani committed
150
        'computer_id': self.getComputerId(),
151 152
        'error_log': error_log})
    except Exception:
153
      (logger or fallback_logger).exception('')
Łukasz Nowak's avatar
Łukasz Nowak committed
154 155

  def available(self):
156
    self._connection_helper.POST('availableSoftwareRelease', data={
157 158
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
159 160

  def building(self):
161
    self._connection_helper.POST('buildingSoftwareRelease', data={
162 163
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
164

165
  def destroyed(self):
166
    self._connection_helper.POST('destroyedSoftwareRelease', data={
167 168
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
169

170 171 172
  def getState(self):
    return getattr(self, '_requested_state', 'available')

Marco Mariani's avatar
Marco Mariani committed
173

174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
class SoftwareProductCollection(object):
  zope.interface.implements(interface.ISoftwareProductCollection)

  def __init__(self, logger, slap):
    self.logger = logger
    self.slap = slap
    self.__getattr__ = self.get
  def get(self, software_product):
      self.logger.info('Getting best Software Release corresponging to '
                       'this Software Product...')
      software_release_list = \
          self.slap.getSoftwareReleaseListFromSoftwareProduct(software_product)
      try:
          software_release_url = software_release_list[0] # First is best one.
          self.logger.info('Found as %s.' % software_release_url)
          return software_release_url
      except IndexError:
          raise AttributeError('No Software Release corresponding to this '
                           'Software Product has been found.')


Łukasz Nowak's avatar
Łukasz Nowak committed
195 196 197 198 199 200 201 202 203 204 205 206 207
# 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
208

Łukasz Nowak's avatar
Łukasz Nowak committed
209
"""Exposed exceptions"""
Marco Mariani's avatar
Marco Mariani committed
210 211


Łukasz Nowak's avatar
Łukasz Nowak committed
212 213
# XXX Why do we need to expose exceptions?
class ResourceNotReady(Exception):
214
  zope.interface.implements(interface.IResourceNotReady)
Łukasz Nowak's avatar
Łukasz Nowak committed
215

Marco Mariani's avatar
Marco Mariani committed
216

Łukasz Nowak's avatar
Łukasz Nowak committed
217
class ServerError(Exception):
218
  zope.interface.implements(interface.IServerError)
Łukasz Nowak's avatar
Łukasz Nowak committed
219

Marco Mariani's avatar
Marco Mariani committed
220

Łukasz Nowak's avatar
Łukasz Nowak committed
221 222 223
class NotFoundError(Exception):
  zope.interface.implements(interface.INotFoundError)

Marco Mariani's avatar
Marco Mariani committed
224 225

class Supply(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
226 227
  zope.interface.implements(interface.ISupply)

228
  def supply(self, software_release, computer_guid=None, state='available'):
229
    try:
230
      self._connection_helper.POST('supplySupply', data={
231 232 233 234 235 236
        '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
237 238


Marco Mariani's avatar
Marco Mariani committed
239
class OpenOrder(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
240 241 242
  zope.interface.implements(interface.IOpenOrder)

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

266 267 268 269
  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
270
    xml = self._connection_helper.POST('requestComputer', data={'computer_title': computer_reference})
271 272 273 274
    computer = xml_marshaller.loads(xml)
    computer._connection_helper = self._connection_helper
    return computer

Marco Mariani's avatar
Marco Mariani committed
275

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


Marco Mariani's avatar
Marco Mariani committed
297
class Computer(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
298 299
  zope.interface.implements(interface.IComputer)

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

  @_syncComputerInformation
  def getComputerPartitionList(self):
321 322
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
Marco Mariani's avatar
Marco Mariani committed
323
    return [x for x in self._computer_partition_list]
Łukasz Nowak's avatar
Łukasz Nowak committed
324

325 326
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
327
      return
328
    self._connection_helper.POST('useComputer', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
329
      'computer_id': self._computer_id,
330
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
331 332

  def updateConfiguration(self, xml):
333
    return self._connection_helper.POST('loadComputerConfigurationFromXML', data={'xml': xml})
Łukasz Nowak's avatar
Łukasz Nowak committed
334

Łukasz Nowak's avatar
Łukasz Nowak committed
335
  def bang(self, message):
336
    self._connection_helper.POST('computerBang', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
337 338 339
      'computer_id': self._computer_id,
      'message': message})

340
  def getStatus(self):
341
    xml = self._connection_helper.GET('getComputerStatus', {'computer_id': self._computer_id})
342
    return xml_marshaller.loads(xml)
343

344
  def revokeCertificate(self):
345
    self._connection_helper.POST('revokeComputerCertificate', data={
346 347 348
      'computer_id': self._computer_id})

  def generateCertificate(self):
349
    xml = self._connection_helper.POST('generateComputerCertificate', data={
350 351 352
      'computer_id': self._computer_id})
    return xml_marshaller.loads(xml)

353

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

463
  def rename(self, new_name, slave_reference=None):
464 465 466 467 468 469 470
    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
471
    self._connection_helper.POST('softwareInstanceRename', data=post_dict)
472

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

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

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

490 491 492 493 494 495 496 497 498 499 500 501
  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
502 503 504
  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

505
  def getConnectionParameterDict(self):
506 507 508 509
    connection_dict = getattr(self, '_connection_dict', None)
    if connection_dict is None:
      # XXX Backward compatibility for older slapproxy (<= 1.0.0)
      connection_dict = xml2dict(getattr(self, 'connection_xml', '')) 
510

511 512
    return connection_dict or {}
      
Łukasz Nowak's avatar
Łukasz Nowak committed
513 514 515 516
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
517
    if not getattr(self, '_software_release_document', None):
Łukasz Nowak's avatar
Łukasz Nowak committed
518 519 520 521 522
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

523
  def setConnectionDict(self, connection_dict, slave_reference=None):
524
    if self.getConnectionParameterDict() != connection_dict:
525
      self._connection_helper.POST('setComputerPartitionConnectionXml', data={
526 527 528 529
          '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
530

531 532 533 534 535 536 537
  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
538
  def getConnectionParameter(self, key):
539
    connection_dict = self.getConnectionParameterDict()
Łukasz Nowak's avatar
Łukasz Nowak committed
540 541 542 543 544 545 546 547 548 549
    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):
550 551 552 553 554 555
    xml = self._connection_helper.GET('getComputerPartitionCertificate',
            {
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
556
    return xml_marshaller.loads(xml)
557 558

  def getStatus(self):
559 560 561 562 563 564
    xml = self._connection_helper.GET('getComputerPartitionStatus',
            {
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
565
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
566

Marco Mariani's avatar
Marco Mariani committed
567

Łukasz Nowak's avatar
Łukasz Nowak committed
568
class ConnectionHelper:
569
  def __init__(self, master_url, key_file=None,
Marco Mariani's avatar
Marco Mariani committed
570
               cert_file=None, master_ca_file=None, timeout=None):
571 572 573 574 575
    if master_url.endswith('/'):
        self.slapgrid_uri = master_url
    else:
        # add a slash or the last path segment will be ignored by urljoin
        self.slapgrid_uri = master_url + '/'
Łukasz Nowak's avatar
Łukasz Nowak committed
576 577 578
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
579
    self.timeout = timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
580 581

  def getComputerInformation(self, computer_id):
582
    xml = self.GET('getComputerInformation', {'computer_id': computer_id})
583
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
584

585
  def getFullComputerInformation(self, computer_id):
586 587 588 589
    """
    Retrieve from SlapOS Master Computer instance containing all needed
    informations (Software Releases, Computer Partitions, ...).
    """
590 591
    path = 'getFullComputerInformation'
    params = {'computer_id': computer_id}
592 593
    if not computer_id:
      # XXX-Cedric: should raise something smarter than "NotFound".
594
      raise NotFoundError('%r %r' (path, params))
595
    try:
596
      xml = self.GET(path, params)
597 598 599
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
600
      xml = self.GET('getComputerInformation', {'computer_id': computer_id})
601

602
    return xml_marshaller.loads(xml)
603

604 605 606 607 608
  def do_request(self, method, path, params=None, data=None, headers=None):
    url = urlparse.urljoin(self.slapgrid_uri, path)
    if path.startswith('/'):
      raise ValueError('method path should be relative: %s' % path)

Łukasz Nowak's avatar
Łukasz Nowak committed
609
    try:
610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638
      if url.startswith('https'):
        cert = (self.cert_file, self.key_file)
      else:
        cert = None

      # XXX TODO: handle host cert verify

      req = method(url=url,
                   params=params,
                   cert=cert,
                   verify=False,
                   data=data,
                   headers=headers,
                   timeout=self.timeout)
      req.raise_for_status()

    except (requests.Timeout, requests.ConnectionError) as exc:
      raise ConnectionError("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:\n%s" % exc)
    except requests.HTTPError as exc:
      if exc.response.status_code == requests.status_codes.codes.not_found:
        msg = url
        if params:
            msg += ' - %s' % params
        raise NotFoundError(msg)
      elif exc.response.status_code == requests.status_codes.codes.request_timeout:
        # this is explicitly returned by SlapOS master, and does not really mean timeout
639
        raise ResourceNotReady(path)
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663
        # XXX TODO test request timeout and resource not found
      else:
        # we don't know how or don't want to handle these (including Unauthorized)
        req.raise_for_status()
    except requests.exceptions.SSLError as exc:
      raise AuthenticationError("%s\nCouldn't authenticate computer. Please "
                                "check that certificate and key exist and are valid." % exc)

#    XXX TODO parse server messages for client configure and node register
#    elif response.status != httplib.OK:
#      message = parsed_error_message(response.status,
#                                     response.read(),
#                                     path)
#      raise ServerError(message)

    return req

  def GET(self, path, params=None):
    req = self.do_request(requests.get,
                          path=path,
                          params=params)
    return req.text

  def POST(self, path, params=None, data=None,
Marco Mariani's avatar
Marco Mariani committed
664
           content_type='application/x-www-form-urlencoded'):
665 666 667 668 669 670
    req = self.do_request(requests.post,
                          path=path,
                          params=params,
                          data=data,
                          headers={'Content-type': content_type})
    return req.text
671 672


Łukasz Nowak's avatar
Łukasz Nowak committed
673 674 675 676
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
677
                           master_ca_file=None, timeout=60):
678 679 680 681
    if master_ca_file:
      raise NotImplementedError('Master certificate not verified in this version: %s' % master_ca_file)

    self._connection_helper = ConnectionHelper(slapgrid_uri, key_file, cert_file, master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
682

683
  # XXX-Cedric: this method is never used and thus should be removed.
Łukasz Nowak's avatar
Łukasz Nowak committed
684 685 686 687 688
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
689
    return SoftwareRelease(software_release=software_release,
690
      connection_helper=self._connection_helper
691
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
692 693 694 695 696 697

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
698
    return Computer(computer_guid, connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
699 700 701 702 703 704

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

709 710 711 712 713 714
    xml = self._connection_helper.GET('registerComputerPartition',
            {
                'computer_reference': computer_guid,
                'computer_partition_reference': partition_id,
                }
            )
715
    result = xml_marshaller.loads(xml)
716 717 718 719
    # 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
720 721

  def registerOpenOrder(self):
722
    return OpenOrder(connection_helper=self._connection_helper)
Łukasz Nowak's avatar
Łukasz Nowak committed
723 724

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

727 728 729 730
  def getSoftwareReleaseListFromSoftwareProduct(self,
      software_product_reference=None, software_release_url=None):
    url = '/getSoftwareReleaseListFromSoftwareProduct?'
    if software_product_reference:
731 732 733
      if software_release_url is not None:
        raise AttributeError('Both software_product_reference and '
                             'software_release_url parameters are specified.')
734 735
      url += 'software_product_reference=%s' % software_product_reference
    else:
736 737 738
      if software_release_url is None:
        raise AttributeError('None of software_product_reference and '
                             'software_release_url parameters are specified.')
739
      url += 'software_release_url=%s' % software_release_url
740 741 742 743

    result = xml_marshaller.loads(self._connection_helper.GET(url))
    assert(type(result) == list)
    return result