slap.py 32.2 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
           "SoftwareInstance", "SoftwareProductCollection",
35
           "Supply", "OpenOrder", "NotFoundError",
36
           "ResourceNotReady", "ServerError", "ConnectionError"]
Łukasz Nowak's avatar
Łukasz Nowak committed
37

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

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

49 50 51 52 53 54
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
55
# XXX fallback_logger to be deprecated together with the old CLI entry points.
Marco Mariani's avatar
Marco Mariani committed
56
fallback_logger = logging.getLogger(__name__)
57 58 59
fallback_handler = logging.StreamHandler()
fallback_logger.setLevel(logging.INFO)
fallback_logger.addHandler(fallback_handler)
Łukasz Nowak's avatar
Łukasz Nowak committed
60 61


62
DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
63

Łukasz Nowak's avatar
Łukasz Nowak committed
64
class SlapDocument:
65
  def __init__(self, connection_helper=None, hateoas_navigator=None):
66 67 68 69
    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
70
      self._hateoas_navigator = hateoas_navigator
Łukasz Nowak's avatar
Łukasz Nowak committed
71

Marco Mariani's avatar
Marco Mariani committed
72

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


Łukasz Nowak's avatar
Łukasz Nowak committed
104 105 106 107 108 109 110 111 112 113 114 115
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
    """
116 117
    SlapDocument.__init__(self, kw.pop('connection_helper', None),
                                kw.pop('hateoas_navigator', None))
Łukasz Nowak's avatar
Łukasz Nowak committed
118 119 120 121 122 123 124 125 126
    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, )

127 128 129 130 131 132 133 134 135 136 137 138
  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

139
  def error(self, error_log, logger=None):
140 141
    try:
      # Does not follow interface
142
      self._connection_helper.POST('softwareReleaseError', data={
143
        'url': self.getURI(),
Marco Mariani's avatar
Marco Mariani committed
144
        'computer_id': self.getComputerId(),
145 146
        'error_log': error_log})
    except Exception:
147
      (logger or fallback_logger).exception('')
Łukasz Nowak's avatar
Łukasz Nowak committed
148 149

  def available(self):
150
    self._connection_helper.POST('availableSoftwareRelease', data={
151 152
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
153 154

  def building(self):
155
    self._connection_helper.POST('buildingSoftwareRelease', data={
156 157
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
158

159
  def destroyed(self):
160
    self._connection_helper.POST('destroyedSoftwareRelease', data={
161 162
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
163

164 165 166
  def getState(self):
    return getattr(self, '_requested_state', 'available')

Marco Mariani's avatar
Marco Mariani committed
167

168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
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
189 190 191 192 193
# XXX What is this SoftwareInstance class?
class SoftwareInstance(SlapDocument):
  """
  Contains Software Instance information
  """
194
  zope.interface.implements(interface.ISoftwareInstance)
Łukasz Nowak's avatar
Łukasz Nowak committed
195 196 197 198 199 200 201 202

  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
203

Łukasz Nowak's avatar
Łukasz Nowak committed
204 205
"""Exposed exceptions"""
class ResourceNotReady(Exception):
206
  zope.interface.implements(interface.IResourceNotReady)
Łukasz Nowak's avatar
Łukasz Nowak committed
207 208

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

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

214 215 216 217 218 219 220
class AuthenticationError(Exception):
  pass


class ConnectionError(Exception):
  zope.interface.implements(interface.IConnectionError)

Marco Mariani's avatar
Marco Mariani committed
221 222

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

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


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

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

263 264 265 266 267 268 269 270 271 272 273 274 275
  def getInformation(self, partition_reference):
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master REST URL (master_rest_url) has not been configured.')
    raw_information = self._hateoas_navigator.getHostingSubscriptionRootSoftwareInstanceInformation(partition_reference)
    software_instance = SoftwareInstance()
    # XXX redefine SoftwareInstance to be more consistent
    for key, value in raw_information.iteritems():
      if key in ['_links']:
        continue
      setattr(software_instance, '_%s' % key, value)
    setattr(software_instance, '_software_release_url', raw_information['_links']['software_release'])
    return software_instance

276 277 278 279
  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
280
    xml = self._connection_helper.POST('requestComputer', data={'computer_title': computer_reference})
281 282
    computer = xml_marshaller.loads(xml)
    computer._connection_helper = self._connection_helper
283
    computer._hateoas_navigator = self._hateoas_navigator
284 285
    return computer

Marco Mariani's avatar
Marco Mariani committed
286

Łukasz Nowak's avatar
Łukasz Nowak committed
287 288 289 290 291
def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
292 293
    if getattr(self, '_synced', 0):
      return func(self, *args, **kw)
294
    computer = self._connection_helper.getFullComputerInformation(self._computer_id)
Łukasz Nowak's avatar
Łukasz Nowak committed
295 296 297 298 299 300
    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)
301 302 303
    setattr(self, '_synced', True)
    for computer_partition in self.getComputerPartitionList():
      setattr(computer_partition, '_synced', True)
Łukasz Nowak's avatar
Łukasz Nowak committed
304
    return func(self, *args, **kw)
305
  return decorated
Łukasz Nowak's avatar
Łukasz Nowak committed
306 307


Marco Mariani's avatar
Marco Mariani committed
308
class Computer(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
309 310
  zope.interface.implements(interface.IComputer)

311 312
  def __init__(self, computer_id, connection_helper=None, hateoas_navigator=None):
    SlapDocument.__init__(self, connection_helper, hateoas_navigator)
Łukasz Nowak's avatar
Łukasz Nowak committed
313 314 315 316 317 318 319 320 321 322 323 324 325
    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.
    """
326 327
    for software_relase in self._software_release_list:
      software_relase._connection_helper = self._connection_helper
328
      software_relase._hateoas_navigator = self._hateoas_navigator
Łukasz Nowak's avatar
Łukasz Nowak committed
329 330 331 332
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
333 334
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
335
      computer_partition._hateoas_navigator = self._hateoas_navigator
Marco Mariani's avatar
Marco Mariani committed
336
    return [x for x in self._computer_partition_list]
Łukasz Nowak's avatar
Łukasz Nowak committed
337

338 339
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
340
      return
341
    self._connection_helper.POST('useComputer', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
342
      'computer_id': self._computer_id,
343
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
344 345

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

Łukasz Nowak's avatar
Łukasz Nowak committed
348
  def bang(self, message):
349
    self._connection_helper.POST('computerBang', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
350 351 352
      'computer_id': self._computer_id,
      'message': message})

353
  def getStatus(self):
354
    xml = self._connection_helper.GET('getComputerStatus', params={'computer_id': self._computer_id})
355
    return xml_marshaller.loads(xml)
356

357
  def revokeCertificate(self):
358
    self._connection_helper.POST('revokeComputerCertificate', data={
359 360 361
      'computer_id': self._computer_id})

  def generateCertificate(self):
362
    xml = self._connection_helper.POST('generateComputerCertificate', data={
363 364 365
      'computer_id': self._computer_id})
    return xml_marshaller.loads(xml)

366

367 368 369 370 371 372 373 374 375 376 377 378 379
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)


380
class ComputerPartition(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
381 382
  zope.interface.implements(interface.IComputerPartition)

Marco Mariani's avatar
Marco Mariani committed
383
  def __init__(self, computer_id=None, partition_id=None,
384 385 386
               request_dict=None, connection_helper=None,
               hateoas_navigator=None):
    SlapDocument.__init__(self, connection_helper, hateoas_navigator)
387 388 389 390 391 392 393
    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
394 395
    self._computer_id = computer_id
    self._partition_id = partition_id
396
    self._request_dict = request_dict
Łukasz Nowak's avatar
Łukasz Nowak committed
397 398 399 400 401

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

  def request(self, software_release, software_type, partition_reference,
402 403
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
404 405 406
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
Marco Mariani's avatar
Marco Mariani committed
407
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" %
Łukasz Nowak's avatar
Łukasz Nowak committed
408 409 410 411 412
                       partition_parameter_kw)

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

416 417 418 419
    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

420 421
    request_dict = {
        'computer_id': self._computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
422 423 424 425 426 427 428 429
        '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),
430
        'state': xml_marshaller.dumps(state),
431 432
    }
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
433 434

  def building(self):
435
    self._connection_helper.POST('buildingComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
436
      'computer_id': self._computer_id,
437
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
438 439

  def available(self):
440
    self._connection_helper.POST('availableComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
441
      'computer_id': self._computer_id,
442
      'computer_partition_id': self.getId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
443 444

  def destroyed(self):
445
    self._connection_helper.POST('destroyedComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
446
      'computer_id': self._computer_id,
447
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
448 449 450
      })

  def started(self):
451
    self._connection_helper.POST('startedComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
452
      'computer_id': self._computer_id,
453
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
454 455 456
      })

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

462
  def error(self, error_log, logger=None):
463
    try:
464
      self._connection_helper.POST('softwareInstanceError', data={
465 466 467 468
        'computer_id': self._computer_id,
        'computer_partition_id': self.getId(),
        'error_log': error_log})
    except Exception:
469
      (logger or fallback_logger).exception('')
Łukasz Nowak's avatar
Łukasz Nowak committed
470

Łukasz Nowak's avatar
Łukasz Nowak committed
471
  def bang(self, message):
472
    self._connection_helper.POST('softwareInstanceBang', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
473
      'computer_id': self._computer_id,
474
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
475 476
      'message': message})

477
  def rename(self, new_name, slave_reference=None):
478 479 480 481 482 483 484
    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
485
    self._connection_helper.POST('softwareInstanceRename', data=post_dict)
486

487 488 489 490 491 492 493 494 495
  def getInformation(self, partition_reference):
    """
    Return all needed informations about an existing Computer Partition
    in the Instance tree of the current Computer Partition.
    """
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master REST URL (master_rest_url) has not been configured.')
    return self._hateoas_navigator.getSoftwareReleaseInformation(partition_reference)

Łukasz Nowak's avatar
Łukasz Nowak committed
496
  def getId(self):
497
    if not getattr(self, '_partition_id', None):
498
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
499 500
    return self._partition_id

501
  def getInstanceGuid(self):
502
    """Return instance_guid. Raise ResourceNotReady if it doesn't exist."""
503 504 505 506
    if not getattr(self, '_instance_guid', None):
      raise ResourceNotReady()
    return self._instance_guid

Łukasz Nowak's avatar
Łukasz Nowak committed
507
  def getState(self):
508
    """return _requested_state. Raise ResourceNotReady if it doesn't exist."""
509 510
    if not getattr(self, '_requested_state', None):
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
511 512
    return self._requested_state

513 514 515 516 517 518 519 520 521 522 523 524
  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
525 526 527
  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

528
  def getConnectionParameterDict(self):
529 530 531
    connection_dict = getattr(self, '_connection_dict', None)
    if connection_dict is None:
      # XXX Backward compatibility for older slapproxy (<= 1.0.0)
532
      connection_dict = xml2dict(getattr(self, 'connection_xml', ''))
533

534
    return connection_dict or {}
535

Łukasz Nowak's avatar
Łukasz Nowak committed
536 537 538 539
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
540
    if not getattr(self, '_software_release_document', None):
Łukasz Nowak's avatar
Łukasz Nowak committed
541 542 543 544 545
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

546
  def setConnectionDict(self, connection_dict, slave_reference=None):
547
    if self.getConnectionParameterDict() != connection_dict:
548
      self._connection_helper.POST('setComputerPartitionConnectionXml', data={
549 550 551 552
          '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
553

554 555 556 557 558 559 560
  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
561
  def getConnectionParameter(self, key):
562
    connection_dict = self.getConnectionParameterDict()
Łukasz Nowak's avatar
Łukasz Nowak committed
563 564 565 566 567 568 569 570 571 572
    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):
573
    xml = self._connection_helper.GET('getComputerPartitionCertificate',
574
            params={
575 576 577 578
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
579
    return xml_marshaller.loads(xml)
580 581

  def getStatus(self):
582
    xml = self._connection_helper.GET('getComputerPartitionStatus',
583
            params={
584 585 586 587
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
588
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
589

Marco Mariani's avatar
Marco Mariani committed
590

Łukasz Nowak's avatar
Łukasz Nowak committed
591
class ConnectionHelper:
592
  def __init__(self, master_url, key_file=None,
Marco Mariani's avatar
Marco Mariani committed
593
               cert_file=None, master_ca_file=None, timeout=None):
594 595 596 597 598
    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
599 600 601
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
602
    self.timeout = timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
603 604

  def getComputerInformation(self, computer_id):
605
    xml = self.GET('getComputerInformation', params={'computer_id': computer_id})
606
    return xml_marshaller.loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
607

608
  def getFullComputerInformation(self, computer_id):
609 610 611 612
    """
    Retrieve from SlapOS Master Computer instance containing all needed
    informations (Software Releases, Computer Partitions, ...).
    """
613 614
    path = 'getFullComputerInformation'
    params = {'computer_id': computer_id}
615 616
    if not computer_id:
      # XXX-Cedric: should raise something smarter than "NotFound".
617
      raise NotFoundError('%r %r' % (path, params))
618
    try:
619
      xml = self.GET(path, params=params)
620 621 622
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
623
      xml = self.GET('getComputerInformation', params=params)
624

625
    return xml_marshaller.loads(xml)
626

627 628
  def do_request(self, method, path, params=None, data=None, headers=None):
    url = urlparse.urljoin(self.slapgrid_uri, path)
629 630 631
    if headers is None:
      headers = {}
    headers.setdefault('Accept', '*/*')
632
    if path.startswith('/'):
633 634
      path = path[1:]
#      raise ValueError('method path should be relative: %s' % path)
635

Łukasz Nowak's avatar
Łukasz Nowak committed
636
    try:
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
      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
666
        raise ResourceNotReady(path)
667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
        # 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)
688
    return req.text.encode('utf-8')
689 690

  def POST(self, path, params=None, data=None,
Marco Mariani's avatar
Marco Mariani committed
691
           content_type='application/x-www-form-urlencoded'):
692 693 694 695 696
    req = self.do_request(requests.post,
                          path=path,
                          params=params,
                          data=data,
                          headers={'Content-type': content_type})
697
    return req.text.encode('utf-8')
698 699


Łukasz Nowak's avatar
Łukasz Nowak committed
700 701 702
class slap:
  zope.interface.implements(interface.slap)

703 704 705 706 707
  def initializeConnection(self, slapgrid_uri,
                           key_file=None, cert_file=None,
                           master_ca_file=None,
                           timeout=60,
                           slapgrid_rest_uri=None):
708 709 710 711
    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
712

713 714 715 716 717 718 719 720 721
    if slapgrid_rest_uri:
      self._hateoas_navigator = HateoasNavigator(
          slapgrid_rest_uri,
          key_file, cert_file,
          master_ca_file, timeout
      )
    else:
      self._hateoas_navigator = None

722
  # XXX-Cedric: this method is never used and thus should be removed.
Łukasz Nowak's avatar
Łukasz Nowak committed
723 724 725 726 727
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
728
    return SoftwareRelease(software_release=software_release,
729 730
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
731
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
732 733 734 735 736 737

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
738 739 740 741
    return Computer(computer_guid,
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
742 743 744 745 746 747

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

752
    xml = self._connection_helper.GET('registerComputerPartition',
753
            params = {
754 755 756 757
                'computer_reference': computer_guid,
                'computer_partition_reference': partition_id,
                }
            )
758
    result = xml_marshaller.loads(xml)
759 760 761
    # XXX: dirty hack to make computer partition usable. xml_marshaller is too
    # low-level for our needs here.
    result._connection_helper = self._connection_helper
762
    result._hateoas_navigator = self._hateoas_navigator
763
    return result
Łukasz Nowak's avatar
Łukasz Nowak committed
764 765

  def registerOpenOrder(self):
766 767 768 769
    return OpenOrder(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )
Łukasz Nowak's avatar
Łukasz Nowak committed
770 771

  def registerSupply(self):
772 773 774 775
    return Supply(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )
776

777 778
  def getSoftwareReleaseListFromSoftwareProduct(self,
      software_product_reference=None, software_release_url=None):
779 780
    url = 'getSoftwareReleaseListFromSoftwareProduct'
    params = {}
781
    if software_product_reference:
782 783 784
      if software_release_url is not None:
        raise AttributeError('Both software_product_reference and '
                             'software_release_url parameters are specified.')
785
      params['software_product_reference'] = software_product_reference
786
    else:
787 788 789
      if software_release_url is None:
        raise AttributeError('None of software_product_reference and '
                             'software_release_url parameters are specified.')
790
      params['software_release_url'] = software_release_url
791

792
    result = xml_marshaller.loads(self._connection_helper.GET(url, params=params))
793 794
    assert(type(result) == list)
    return result
795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891

  def getOpenOrderDict(self):
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master REST URL (master_rest_url) has not been configured.')
    return self._hateoas_navigator.getHostingSubscriptionDict()


class HateoasNavigator(object):
  # XXX: needs to be designed for real. For now, just a mockup.
  def __init__(self, slapgrid_uri,
               key_file=None, cert_file=None,
               master_ca_file=None, timeout=60):
    self.slapos_master_hateoas_uri = slapgrid_uri
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
    self.timeout = timeout

  def GET(self, uri):
    # XXX hack
    connection_helper = ConnectionHelper(
        uri, self.key_file, self.cert_file, self.master_ca_file, self.timeout)
    return connection_helper.GET(uri)

  def _hateoasGetMaster(self):
    result = self.GET('%s/Base_getHateoasMaster' % self.slapos_master_hateoas_uri)
    return json.loads(result)

  def _hateoasGetPerson(self):
    person_link = self._hateoasGetMaster()['_links']['action_object_jump']['href']
    result = self.GET(person_link)
    return json.loads(result)

  def _hateoas_getHostingSubscriptionDict(self):
    action_object_slap_list = self._hateoasGetPerson()['_links']['action_object_slap']
    for action in action_object_slap_list:
      if action.get('title') == 'getHateoasHostingSubscriptionList':
        getter_link = action['href']
        break
    else:
      raise Exception('Hosting subscription not found.')
    result = self.GET(getter_link)
    return json.loads(result)['_links']['content']

  # XXX static method
  def _hateoas_getActionObjectSlap(self, action_object_slap_list, action_title):
    for action in action_object_slap_list:
      if action.get('title') == action_title:
        return action['href']
    else:
      raise NotFoundError('Action %s not found.' % action)

  def _hateoasGetInformation(self, url):
    result = self.GET(url)
    result = json.loads(result)
    object_link = self._hateoas_getActionObjectSlap(
      result['_links']['action_object_slap'],
      'getHateoasInformation'
    )
    result = self.GET(object_link)
    return json.loads(result)

  def getHostingSubscriptionDict(self):
    hosting_subscription_link_list = self._hateoas_getHostingSubscriptionDict()
    hosting_subscription_dict = {}
    for hosting_subscription_link in hosting_subscription_link_list:
      raw_information = self.getHostingSubscriptionRootSoftwareInstanceInformation(hosting_subscription_link['title'])
      software_instance = SoftwareInstance()
      # XXX redefine SoftwareInstance to be more consistent
      for key, value in raw_information.iteritems():
        if key in ['_links']:
          continue
        setattr(software_instance, '_%s' % key, value)
      setattr(software_instance, '_software_release_url', raw_information['_links']['software_release'])
      hosting_subscription_dict[software_instance._title] = software_instance

    return hosting_subscription_dict

  def getHostingSubscriptionRootSoftwareInstanceInformation(self, reference):
    hosting_subscription_list = self._hateoas_getHostingSubscriptionDict()
    for hosting_subscription in hosting_subscription_list:
      if hosting_subscription.get('title') == reference:
        hosting_subscription_url = hosting_subscription['href']
        break
    else:
      raise NotFoundError('This document does not exist.')

    hosting_subscription = json.loads(self.GET(hosting_subscription_url))

    software_instance_url = self._hateoas_getActionObjectSlap(
        hosting_subscription['_links']['action_object_slap'],
        'getHateoasRootInstance'
    )
    response = self.GET(software_instance_url)
    response = json.loads(response)
    software_instance_url = response['_links']['content'][0]['href']
    return self._hateoasGetInformation(software_instance_url)