slap.py 30.5 KB
Newer Older
Łukasz Nowak's avatar
Łukasz Nowak committed
1
# -*- coding: utf-8 -*-
2
# vim: set et sts=2:
Łukasz Nowak's avatar
Łukasz Nowak committed
3 4
##############################################################################
#
5 6
# Copyright (c) 2010, 2011, 2012 Vifib SARL and Contributors.
# All Rights Reserved.
Łukasz Nowak's avatar
Łukasz Nowak committed
7 8 9 10 11 12 13 14 15
#
# 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
16 17
# 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
18 19 20 21 22 23 24
# 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.
#
25
# You should have received a copy of the GNU Lesser General Public License
Łukasz Nowak's avatar
Łukasz Nowak committed
26 27 28 29
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
30 31 32 33
"""
Simple, easy to (un)marshall classes for slap client/server communication
"""

Łukasz Nowak's avatar
Łukasz Nowak committed
34
__all__ = ["slap", "ComputerPartition", "Computer", "SoftwareRelease",
35
           "SoftwareInstance", "SoftwareProductCollection",
36
           "Supply", "OpenOrder", "NotFoundError", "Token",
37
           "ResourceNotReady", "ServerError", "ConnectionError"]
Łukasz Nowak's avatar
Łukasz Nowak committed
38

39
import os
40
import logging
41
import re
Bryton Lacquement's avatar
Bryton Lacquement committed
42 43 44 45
from functools import wraps

import six

46 47 48
from .exception import ResourceNotReady, ServerError, NotFoundError, \
          ConnectionError
from .hateoas import SlapHateoasNavigator, ConnectionHelper
49
from slapos.util import loads, dumps, bytes2str, xml2dict, dict2xml, calculate_dict_hash
50

51
from xml.sax import saxutils
Bryton Lacquement's avatar
Bryton Lacquement committed
52 53
from zope.interface import implementer
from .interface import slap as interface
54

55

56
import requests
57 58
# silence messages like 'Unverified HTTPS request is being made'
requests.packages.urllib3.disable_warnings()
59 60 61 62 63
# 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
64
# XXX fallback_logger to be deprecated together with the old CLI entry points.
Marco Mariani's avatar
Marco Mariani committed
65
fallback_logger = logging.getLogger(__name__)
66 67 68
fallback_handler = logging.StreamHandler()
fallback_logger.setLevel(logging.INFO)
fallback_logger.addHandler(fallback_handler)
Łukasz Nowak's avatar
Łukasz Nowak committed
69 70


71
DEFAULT_SOFTWARE_TYPE = 'RootSoftwareInstance'
72
COMPUTER_PARTITION_REQUEST_LIST_TEMPLATE_FILENAME = '.slapos-request-transaction-%s'
73

Łukasz Nowak's avatar
Łukasz Nowak committed
74
class SlapDocument:
75
  def __init__(self, connection_helper=None, hateoas_navigator=None):
76 77 78 79
    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
80
      self._hateoas_navigator = hateoas_navigator
Łukasz Nowak's avatar
Łukasz Nowak committed
81

Marco Mariani's avatar
Marco Mariani committed
82

83 84 85 86
class SlapRequester(SlapDocument):
  """
  Abstract class that allow to factor method for subclasses that use "request()"
  """
87

88 89
  def _requestComputerPartition(self, request_dict):
    try:
90
      xml = self._connection_helper.POST('requestComputerPartition', data=request_dict)
91 92 93 94 95
    except ResourceNotReady:
      return ComputerPartition(
        request_dict=request_dict,
        connection_helper=self._connection_helper,
      )
Bryton Lacquement's avatar
Bryton Lacquement committed
96
    software_instance = loads(xml)
97
    computer_partition = ComputerPartition(
Bryton Lacquement's avatar
Bryton Lacquement committed
98 99
      software_instance.slap_computer_id,
      software_instance.slap_computer_partition_id,
100 101 102
      connection_helper=self._connection_helper,
    )
    # Hack to give all object attributes to the ComputerPartition instance
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
103 104
    # XXX Should be removed by correctly specifying difference between
    # ComputerPartition and SoftwareInstance
Bryton Lacquement's avatar
Bryton Lacquement committed
105
    computer_partition.__dict__.update(software_instance.__dict__)
106
    # XXX not generic enough.
Bryton Lacquement's avatar
Bryton Lacquement committed
107
    if loads(request_dict['shared_xml']):
108 109 110 111
      computer_partition._synced = True
      computer_partition._connection_dict = software_instance._connection_dict
      computer_partition._parameter_dict = software_instance._parameter_dict
    return computer_partition
112 113


Bryton Lacquement's avatar
Bryton Lacquement committed
114
@implementer(interface.ISoftwareRelease)
Łukasz Nowak's avatar
Łukasz Nowak committed
115 116 117 118 119
class SoftwareRelease(SlapDocument):
  """
  Contains Software Release information
  """

120
  def __init__(self, software_release=None, computer_guid=None, requested_state='available', **kw):
Łukasz Nowak's avatar
Łukasz Nowak committed
121 122 123 124 125
    """
    Makes easy initialisation of class parameters

    XXX **kw args only kept for compatibility
    """
126 127
    SlapDocument.__init__(self, kw.pop('connection_helper', None),
                                kw.pop('hateoas_navigator', None))
Łukasz Nowak's avatar
Łukasz Nowak committed
128 129 130 131 132 133 134
    self._software_instance_list = []
    self._software_release = software_release
    self._computer_guid = computer_guid

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

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

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

  def available(self):
158 159 160 161 162
    if getattr(self, '_known_state', 'unknown') != "available":
      # Not required to repost if not needed.
      self._connection_helper.POST('availableSoftwareRelease', data={
        'url': self.getURI(),
        'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
163 164

  def building(self):
165 166 167 168
    if getattr(self, '_known_state', 'unknown') != "building":
      self._connection_helper.POST('buildingSoftwareRelease', data={
        'url': self.getURI(),
        'computer_id': self.getComputerId()})
Łukasz Nowak's avatar
Łukasz Nowak committed
169

170
  def destroyed(self):
171
    self._connection_helper.POST('destroyedSoftwareRelease', data={
172 173
      'url': self.getURI(),
      'computer_id': self.getComputerId()})
174

175 176 177
  def getState(self):
    return getattr(self, '_requested_state', 'available')

Marco Mariani's avatar
Marco Mariani committed
178

Bryton Lacquement's avatar
Bryton Lacquement committed
179
@implementer(interface.ISoftwareProductCollection)
180 181 182 183 184
class SoftwareProductCollection(object):

  def __init__(self, logger, slap):
    self.logger = logger
    self.slap = slap
185 186 187
    self.get = self.__getattr__

  def __getattr__(self, software_product):
Sebastien Robin's avatar
Sebastien Robin committed
188
      self.logger.info('Getting best Software Release corresponding to '
189 190 191 192 193 194 195 196 197 198 199 200
                       '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
201
# XXX What is this SoftwareInstance class?
Bryton Lacquement's avatar
Bryton Lacquement committed
202
@implementer(interface.ISoftwareInstance)
Łukasz Nowak's avatar
Łukasz Nowak committed
203 204 205 206 207
class SoftwareInstance(SlapDocument):
  """
  Contains Software Instance information
  """

Bryton Lacquement's avatar
Bryton Lacquement committed
208
  def __init__(self, **kw):
Łukasz Nowak's avatar
Łukasz Nowak committed
209 210 211
    """
    Makes easy initialisation of class parameters
    """
Bryton Lacquement's avatar
Bryton Lacquement committed
212
    self.__dict__.update(kw)
Łukasz Nowak's avatar
Łukasz Nowak committed
213

Marco Mariani's avatar
Marco Mariani committed
214 215


Bryton Lacquement's avatar
Bryton Lacquement committed
216
@implementer(interface.ISupply)
Marco Mariani's avatar
Marco Mariani committed
217
class Supply(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
218

219
  def supply(self, software_release, computer_guid=None, state='available'):
220
    try:
221
      self._connection_helper.POST('supplySupply', data={
222 223 224 225 226 227
        '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
228

229 230 231 232
@implementer(interface.IToken)
class Token(SlapDocument):
  def request(self):
    return self._hateoas_navigator.getToken()
Łukasz Nowak's avatar
Łukasz Nowak committed
233

Bryton Lacquement's avatar
Bryton Lacquement committed
234
@implementer(interface.IOpenOrder)
Marco Mariani's avatar
Marco Mariani committed
235
class OpenOrder(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
236 237

  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):
240

Łukasz Nowak's avatar
Łukasz Nowak committed
241 242
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
243 244 245 246
    elif not isinstance(partition_parameter_kw, dict):
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" %
                       partition_parameter_kw)

247 248
    if filter_kw is None:
      filter_kw = {}
249 250 251 252 253 254 255 256
    elif not isinstance(filter_kw, dict):
      raise ValueError("Unexpected type of filter_kw '%s'" %
                       filter_kw)

    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

Łukasz Nowak's avatar
Łukasz Nowak committed
257 258 259
    request_dict = {
        'software_release': software_release,
        'partition_reference': partition_reference,
Bryton Lacquement's avatar
Bryton Lacquement committed
260 261
        'partition_parameter_xml': dumps(partition_parameter_kw),
        'filter_xml': dumps(filter_kw),
262
        'software_type': software_type,
263 264
        # XXX Cedric: Why state and shared are marshalled? First is a string
        #             And second is a boolean.
Bryton Lacquement's avatar
Bryton Lacquement committed
265 266
        'state': dumps(state),
        'shared_xml': dumps(shared),
Marco Mariani's avatar
Marco Mariani committed
267
    }
268
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
269

270 271
  def getInformation(self, partition_reference):
    if not getattr(self, '_hateoas_navigator', None):
272
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
273 274 275
    raw_information = self._hateoas_navigator.getHostingSubscriptionRootSoftwareInstanceInformation(partition_reference)
    software_instance = SoftwareInstance()
    # XXX redefine SoftwareInstance to be more consistent
276
    for key, value in six.iteritems(raw_information["data"]):
277 278 279
      if key in ['_links']:
        continue
      setattr(software_instance, '_%s' % key, value)
280 281 282 283 284 285 286 287 288

    if raw_information["data"].get("text_content", None) is not None:
      setattr(software_instance, '_parameter_dict', xml2dict(raw_information["data"]['text_content']))
    else:
      setattr(software_instance, '_parameter_dict', {})

    setattr(software_instance, '_requested_state', raw_information["data"]['slap_state'])
    setattr(software_instance, '_connection_dict', raw_information["data"]['connection_parameter_list'])
    setattr(software_instance, '_software_release_url', raw_information["data"]['url_string'])
289 290
    return software_instance

291 292 293 294
  def requestComputer(self, computer_reference):
    """
    Requests a computer.
    """
295
    xml = self._connection_helper.POST('requestComputer', data={'computer_title': computer_reference})
Bryton Lacquement's avatar
Bryton Lacquement committed
296
    computer = loads(xml)
297
    computer._connection_helper = self._connection_helper
298
    computer._hateoas_navigator = self._hateoas_navigator
299 300
    return computer

Marco Mariani's avatar
Marco Mariani committed
301

Łukasz Nowak's avatar
Łukasz Nowak committed
302 303 304 305 306
def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
Bryton Lacquement's avatar
Bryton Lacquement committed
307 308 309 310 311 312 313
    if not getattr(self, '_synced', 0):
      computer = self._connection_helper.getFullComputerInformation(
        self._computer_id)
      self.__dict__.update(computer.__dict__)
      self._synced = True
      for computer_partition in self.getComputerPartitionList():
        computer_partition._synced = True
Łukasz Nowak's avatar
Łukasz Nowak committed
314
    return func(self, *args, **kw)
Bryton Lacquement's avatar
Bryton Lacquement committed
315
  return wraps(func)(decorated)
Łukasz Nowak's avatar
Łukasz Nowak committed
316 317


Bryton Lacquement's avatar
Bryton Lacquement committed
318
@implementer(interface.IComputer)
Marco Mariani's avatar
Marco Mariani committed
319
class Computer(SlapDocument):
Łukasz Nowak's avatar
Łukasz Nowak committed
320

321 322
  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
323 324 325 326 327 328 329 330 331 332 333 334 335
    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.
    """
336 337
    for software_relase in self._software_release_list:
      software_relase._connection_helper = self._connection_helper
338
      software_relase._hateoas_navigator = self._hateoas_navigator
Łukasz Nowak's avatar
Łukasz Nowak committed
339 340 341 342
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
343 344
    for computer_partition in self._computer_partition_list:
      computer_partition._connection_helper = self._connection_helper
345
      computer_partition._hateoas_navigator = self._hateoas_navigator
Marco Mariani's avatar
Marco Mariani committed
346
    return [x for x in self._computer_partition_list]
Łukasz Nowak's avatar
Łukasz Nowak committed
347

348 349
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
350
      return
351
    self._connection_helper.POST('useComputer', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
352
      'computer_id': self._computer_id,
353
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
354 355

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

Łukasz Nowak's avatar
Łukasz Nowak committed
358
  def bang(self, message):
359
    self._connection_helper.POST('computerBang', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
360 361 362
      'computer_id': self._computer_id,
      'message': message})

363
  def getStatus(self):
364
    xml = self._connection_helper.GET('getComputerStatus', params={'computer_id': self._computer_id})
Bryton Lacquement's avatar
Bryton Lacquement committed
365
    return loads(xml)
366

367
  def revokeCertificate(self):
368
    self._connection_helper.POST('revokeComputerCertificate', data={
369 370 371
      'computer_id': self._computer_id})

  def generateCertificate(self):
372
    xml = self._connection_helper.POST('generateComputerCertificate', data={
373
      'computer_id': self._computer_id})
Bryton Lacquement's avatar
Bryton Lacquement committed
374
    return loads(xml)
375

376 377 378 379 380 381 382 383 384 385 386
  def getInformation(self):
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
    raw_information = self._hateoas_navigator._getComputer(reference=self._computer_id)
    computer = Computer(self._computer_id) 
    for key, value in six.iteritems(raw_information["data"]):
      if key in ['_links']:
        continue
      setattr(computer, '_%s' % key, value)
    return computer

387

388 389 390 391 392 393 394 395 396 397 398 399 400
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)


Bryton Lacquement's avatar
Bryton Lacquement committed
401
@implementer(interface.IComputerPartition)
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
402
class ComputerPartition(SlapRequester):
Łukasz Nowak's avatar
Łukasz Nowak committed
403

Marco Mariani's avatar
Marco Mariani committed
404
  def __init__(self, computer_id=None, partition_id=None,
405 406 407
               request_dict=None, connection_helper=None,
               hateoas_navigator=None):
    SlapDocument.__init__(self, connection_helper, hateoas_navigator)
408 409 410 411 412 413 414
    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
415 416
    self._computer_id = computer_id
    self._partition_id = partition_id
417
    self._request_dict = request_dict
Łukasz Nowak's avatar
Łukasz Nowak committed
418

419 420 421
    # Just create an empty file (for nothing requested yet)
    self._updateTransactionFile(partition_reference=None)

Łukasz Nowak's avatar
Łukasz Nowak committed
422 423 424
  def __getinitargs__(self):
    return (self._computer_id, self._partition_id, )

425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450
  def _updateTransactionFile(self, partition_reference=None):
    """
    Store reference to all Instances requested by this Computer Parition
    """
    # Environ variable set by Slapgrid while processing this partition
    instance_root = os.environ.get('SLAPGRID_INSTANCE_ROOT', '')
    if not instance_root or not self._partition_id:
      return

    transaction_file_name = COMPUTER_PARTITION_REQUEST_LIST_TEMPLATE_FILENAME % self._partition_id
    transaction_file_path = os.path.join(instance_root, self._partition_id,
                                        transaction_file_name)

    try:
      if partition_reference is None:
        if os.access(os.path.join(instance_root, self._partition_id), os.W_OK):
          if os.path.exists(transaction_file_path):
            return
          transac_file = open(transaction_file_path, 'w')
          transac_file.close()
      else:
        with open(transaction_file_path, 'a') as transac_file:
          transac_file.write('%s\n' % partition_reference)
    except OSError:
      return

Łukasz Nowak's avatar
Łukasz Nowak committed
451
  def request(self, software_release, software_type, partition_reference,
452 453
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
454 455 456
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
Marco Mariani's avatar
Marco Mariani committed
457
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" %
Łukasz Nowak's avatar
Łukasz Nowak committed
458 459 460 461 462
                       partition_parameter_kw)

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

466 467 468 469
    # Let enforce a default software type
    if software_type is None:
      software_type = DEFAULT_SOFTWARE_TYPE

470 471
    request_dict = {
        'computer_id': self._computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
472 473 474 475
        'computer_partition_id': self._partition_id,
        'software_release': software_release,
        'software_type': software_type,
        'partition_reference': partition_reference,
Bryton Lacquement's avatar
Bryton Lacquement committed
476 477 478 479
        'shared_xml': dumps(shared),
        'partition_parameter_xml': dumps(partition_parameter_kw),
        'filter_xml': dumps(filter_kw),
        'state': dumps(state),
480
    }
481
    self._updateTransactionFile(partition_reference)
482
    return self._requestComputerPartition(request_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
483 484

  def destroyed(self):
485
    self._connection_helper.POST('destroyedComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
486
      'computer_id': self._computer_id,
487
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
488 489 490
      })

  def started(self):
491
    self._connection_helper.POST('startedComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
492
      'computer_id': self._computer_id,
493
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
494 495 496
      })

  def stopped(self):
497
    self._connection_helper.POST('stoppedComputerPartition', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
498
      'computer_id': self._computer_id,
499
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
500 501
      })

502
  def error(self, error_log, logger=None):
503
    try:
504
      self._connection_helper.POST('softwareInstanceError', data={
505 506 507 508
        'computer_id': self._computer_id,
        'computer_partition_id': self.getId(),
        'error_log': error_log})
    except Exception:
509
      (logger or fallback_logger).exception('')
Łukasz Nowak's avatar
Łukasz Nowak committed
510

Łukasz Nowak's avatar
Łukasz Nowak committed
511
  def bang(self, message):
512
    self._connection_helper.POST('softwareInstanceBang', data={
Łukasz Nowak's avatar
Łukasz Nowak committed
513
      'computer_id': self._computer_id,
514
      'computer_partition_id': self.getId(),
Łukasz Nowak's avatar
Łukasz Nowak committed
515 516
      'message': message})

517
  def rename(self, new_name, slave_reference=None):
518 519 520 521 522 523 524
    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
525
    self._connection_helper.POST('softwareInstanceRename', data=post_dict)
526

527 528 529 530 531 532
  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):
533
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
534

535 536 537
    instance_url = self.getMeUrl()
    raw_information = self._hateoas_navigator.getRelatedInstanceInformation(
      instance_url, partition_reference)
538
    software_instance = SoftwareInstance()
539
    for key, value in six.iteritems(raw_information["data"]):
540 541 542
      if key in ['_links']:
        continue
      setattr(software_instance, '_%s' % key, value)
543 544 545 546 547 548 549 550 551

    if raw_information["data"].get("text_content", None) is not None:
      setattr(software_instance, '_parameter_dict', xml2dict(raw_information["data"]['text_content']))
    else:
      setattr(software_instance, '_parameter_dict', {})

    setattr(software_instance, '_requested_state', raw_information["data"]['slap_state'])
    setattr(software_instance, '_connection_dict', raw_information["data"]['connection_parameter_list'])
    setattr(software_instance, '_software_release_url', raw_information["data"]['url_string'])
552 553
    return software_instance

Łukasz Nowak's avatar
Łukasz Nowak committed
554
  def getId(self):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
555
    if not getattr(self, '_partition_id', None):
556
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
557 558
    return self._partition_id

559
  def getInstanceGuid(self):
560
    """Return instance_guid. Raise ResourceNotReady if it doesn't exist."""
561 562 563 564
    if not getattr(self, '_instance_guid', None):
      raise ResourceNotReady()
    return self._instance_guid

Łukasz Nowak's avatar
Łukasz Nowak committed
565
  def getState(self):
566
    """return _requested_state. Raise ResourceNotReady if it doesn't exist."""
567 568
    if not getattr(self, '_requested_state', None):
      raise ResourceNotReady()
Łukasz Nowak's avatar
Łukasz Nowak committed
569 570
    return self._requested_state

571 572 573 574
  def getAccessStatus(self):
    """Get latest computer partition Access message state"""
    return getattr(self, '_access_status', None)

575 576 577 578 579 580 581 582 583 584 585 586
  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
587 588 589
  def getInstanceParameterDict(self):
    return getattr(self, '_parameter_dict', None) or {}

590
  def getConnectionParameterDict(self):
591 592 593
    connection_dict = getattr(self, '_connection_dict', None)
    if connection_dict is None:
      # XXX Backward compatibility for older slapproxy (<= 1.0.0)
594
      connection_dict = xml2dict(getattr(self, 'connection_xml', ''))
595

596
    return connection_dict or {}
597

Łukasz Nowak's avatar
Łukasz Nowak committed
598 599 600 601
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
602
    if not getattr(self, '_software_release_document', None):
Łukasz Nowak's avatar
Łukasz Nowak committed
603 604 605 606 607
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

608
  def setConnectionDict(self, connection_dict, slave_reference=None):
609 610
    # recreate and stabilise connection_dict that it would became the same as on server
    connection_dict = xml2dict(dict2xml(connection_dict))
611 612 613
    if self.getConnectionParameterDict() == connection_dict:
      return

614
    if slave_reference is not None:
615
      # check the connection parameters from the slave
616

617 618 619
      # Should we check existence?
      slave_parameter_list = self.getInstanceParameter("slave_instance_list")
      slave_connection_dict = {}
620
      connection_parameter_hash = None
621 622 623
      for slave_parameter_dict in slave_parameter_list:
        if slave_parameter_dict.get("slave_reference") == slave_reference:
          connection_parameter_hash = slave_parameter_dict.get("connection-parameter-hash", None)
624 625 626
          break

      # Skip as nothing changed for the slave
627
      if connection_parameter_hash is not None and \
628
        connection_parameter_hash == calculate_dict_hash(connection_dict):
629
        return
630

631
    self._connection_helper.POST('setComputerPartitionConnectionXml', data={
632 633
          'computer_id': self._computer_id,
          'computer_partition_id': self._partition_id,
Bryton Lacquement's avatar
Bryton Lacquement committed
634
          'connection_xml': dumps(connection_dict),
635
          'slave_reference': slave_reference})
Łukasz Nowak's avatar
Łukasz Nowak committed
636

637 638
  def getInstanceParameter(self, key):
    parameter_dict = getattr(self, '_parameter_dict', None) or {}
639
    try:
640
      return parameter_dict[key]
641
    except KeyError:
642 643
      raise NotFoundError("%s not found" % key)

Łukasz Nowak's avatar
Łukasz Nowak committed
644
  def getConnectionParameter(self, key):
645
    connection_dict = self.getConnectionParameterDict()
646
    try:
Łukasz Nowak's avatar
Łukasz Nowak committed
647
      return connection_dict[key]
648
    except KeyError:
Łukasz Nowak's avatar
Łukasz Nowak committed
649 650 651 652 653 654 655
      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):
656
    xml = self._connection_helper.GET('getComputerPartitionCertificate',
657
            params={
658 659 660 661
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
Bryton Lacquement's avatar
Bryton Lacquement committed
662
    return loads(xml)
663 664

  def getStatus(self):
665
    xml = self._connection_helper.GET('getComputerPartitionStatus',
666
            params={
667 668 669 670
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
Bryton Lacquement's avatar
Bryton Lacquement committed
671
    return loads(xml)
672 673 674 675 676 677 678 679
  
  def getFullHostingIpAddressList(self):
    xml = self._connection_helper.GET('getHostingSubscriptionIpList',
            params={
                'computer_id': self._computer_id,
                'computer_partition_id': self._partition_id,
                }
            )
Bryton Lacquement's avatar
Bryton Lacquement committed
680
    return loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
681

682 683 684 685 686
  def setComputerPartitionRelatedInstanceList(self, instance_reference_list):
    self._connection_helper.POST('updateComputerPartitionRelatedInstanceList',
        data={
          'computer_id': self._computer_id,
          'computer_partition_id': self._partition_id,
Bryton Lacquement's avatar
Bryton Lacquement committed
687
          'instance_reference_xml': dumps(instance_reference_list)
688 689 690
          }
        )

691
class SlapConnectionHelper(ConnectionHelper):
692

Łukasz Nowak's avatar
Łukasz Nowak committed
693
  def getComputerInformation(self, computer_id):
694
    xml = self.GET('getComputerInformation', params={'computer_id': computer_id})
Bryton Lacquement's avatar
Bryton Lacquement committed
695
    return loads(xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
696

697
  def getFullComputerInformation(self, computer_id):
698 699 700 701
    """
    Retrieve from SlapOS Master Computer instance containing all needed
    informations (Software Releases, Computer Partitions, ...).
    """
702 703
    path = 'getFullComputerInformation'
    params = {'computer_id': computer_id}
704 705
    if not computer_id:
      # XXX-Cedric: should raise something smarter than "NotFound".
706
      raise NotFoundError('%r %r' % (path, params))
707
    try:
708
      xml = self.GET(path, params=params)
709 710 711
    except NotFoundError:
      # XXX: This is a ugly way to keep backward compatibility,
      # We should stablise slap library soon.
712
      xml = self.GET('getComputerInformation', params=params)
713

Bryton Lacquement's avatar
Bryton Lacquement committed
714
    return loads(xml)
715

716
getHateoasUrl_cache = {}
Bryton Lacquement's avatar
Bryton Lacquement committed
717
@implementer(interface.slap)
Łukasz Nowak's avatar
Łukasz Nowak committed
718 719
class slap:

Alain Takoudjou's avatar
Alain Takoudjou committed
720
  def initializeConnection(self, slapgrid_uri,
721 722 723 724
                           key_file=None, cert_file=None,
                           master_ca_file=None,
                           timeout=60,
                           slapgrid_rest_uri=None):
725 726 727
    if master_ca_file:
      raise NotImplementedError('Master certificate not verified in this version: %s' % master_ca_file)

728 729
    self._connection_helper = SlapConnectionHelper(
            slapgrid_uri, key_file, cert_file, master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
730

731
    if not slapgrid_rest_uri:
732
      getHateoasUrl_cache_key = (slapgrid_uri, key_file, cert_file, master_ca_file, timeout)
733
      try:
734 735 736 737 738
        slapgrid_rest_uri = getHateoasUrl_cache[getHateoasUrl_cache_key]
      except KeyError:
        pass
    if not slapgrid_rest_uri:
      try:
Bryton Lacquement's avatar
Bryton Lacquement committed
739 740
        slapgrid_rest_uri = getHateoasUrl_cache[getHateoasUrl_cache_key] = \
          bytes2str(self._connection_helper.GET('getHateoasUrl'))
741 742
      except:
        pass
743
    if slapgrid_rest_uri:
744
      self._hateoas_navigator = SlapHateoasNavigator(
745 746 747 748 749 750 751
          slapgrid_rest_uri,
          key_file, cert_file,
          master_ca_file, timeout
      )
    else:
      self._hateoas_navigator = None

752
  # XXX-Cedric: this method is never used and thus should be removed.
Łukasz Nowak's avatar
Łukasz Nowak committed
753 754 755 756 757
  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
758
    return SoftwareRelease(software_release=software_release,
759 760
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
761
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
762

763 764 765 766 767 768 769 770 771 772 773 774 775 776
  def registerToken(self):
    """
    Registers connected represenation of token and
    return Token class object
    """
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')

    return Token(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
    )


Łukasz Nowak's avatar
Łukasz Nowak committed
777 778 779 780 781
  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
782 783 784 785
    return Computer(computer_guid,
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
786 787 788 789 790 791

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

796
    xml = self._connection_helper.GET('registerComputerPartition',
797
            params = {
798 799 800 801
                'computer_reference': computer_guid,
                'computer_partition_reference': partition_id,
                }
            )
Bryton Lacquement's avatar
Bryton Lacquement committed
802
    result = loads(xml)
803 804 805
    # XXX: dirty hack to make computer partition usable. xml_marshaller is too
    # low-level for our needs here.
    result._connection_helper = self._connection_helper
806
    result._hateoas_navigator = self._hateoas_navigator
807
    return result
Łukasz Nowak's avatar
Łukasz Nowak committed
808 809

  def registerOpenOrder(self):
810 811 812 813
    return OpenOrder(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )
Łukasz Nowak's avatar
Łukasz Nowak committed
814 815

  def registerSupply(self):
816 817 818 819
    return Supply(
      connection_helper=self._connection_helper,
      hateoas_navigator=self._hateoas_navigator
  )
820

821 822
  def getSoftwareReleaseListFromSoftwareProduct(self,
      software_product_reference=None, software_release_url=None):
823 824
    url = 'getSoftwareReleaseListFromSoftwareProduct'
    params = {}
825
    if software_product_reference:
826 827 828
      if software_release_url is not None:
        raise AttributeError('Both software_product_reference and '
                             'software_release_url parameters are specified.')
829
      params['software_product_reference'] = software_product_reference
830
    else:
831 832 833
      if software_release_url is None:
        raise AttributeError('None of software_product_reference and '
                             'software_release_url parameters are specified.')
834
      params['software_release_url'] = software_release_url
835

836
    xml = self._connection_helper.GET(url, params=params)
Bryton Lacquement's avatar
Bryton Lacquement committed
837
    result = loads(xml)
838 839
    assert(type(result) == list)
    return result
840 841 842

  def getOpenOrderDict(self):
    if not getattr(self, '_hateoas_navigator', None):
843
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
844
    return self._hateoas_navigator.getHostingSubscriptionDict()
845 846 847 848 849 850

  def getComputerDict(self): 
    if not getattr(self, '_hateoas_navigator', None):
      raise Exception('SlapOS Master Hateoas API required for this operation is not availble.')
    return self._hateoas_navigator.getComputerDict()