slap.py 21.6 KB
Newer Older
Łukasz Nowak's avatar
Łukasz Nowak committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
# -*- coding: utf-8 -*-
##############################################################################
#
# Copyright (c) 2010 Vifib SARL and Contributors. All Rights Reserved.
#
# 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
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# 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.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
##############################################################################
__all__ = ["slap", "ComputerPartition", "Computer", "SoftwareRelease",
           "Supply", "OpenOrder", "NotFoundError", "Unauthorized",
           "ResourceNotReady"]

from interface import slap as interface
from xml_marshaller import xml_marshaller
import httplib
import socket
import ssl
import urllib
import urlparse
import zope.interface

"""
Simple, easy to (un)marshall classes for slap client/server communication
"""

# httplib.HTTPSConnection with key verification
class HTTPSConnectionCA(httplib.HTTPSConnection):
  """Patched version of HTTPSConnection which verifies server certificate"""
  def __init__(self, *args, **kwargs):
    self.ca_file = kwargs.pop('ca_file')
    if self.ca_file is None:
      raise ValueError('ca_file is required argument.')
    httplib.HTTPSConnection.__init__(self, *args, **kwargs)

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

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


class SlapDocument:
  pass

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
    """
    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, )

  def error(self, error_log):
    # Does not follow interface
    self._connection_helper.POST('/softwareReleaseError', {
      'url': self._software_release,
      'computer_id' : self._computer_guid,
      'error_log': error_log})

  def getURI(self):
    return self._software_release

  def available(self):
    self._connection_helper.POST('/availableSoftwareRelease', {
      'url': self._software_release, 
      'computer_id': self._computer_guid})

  def building(self):
    self._connection_helper.POST('/buildingSoftwareRelease', {
      'url': self._software_release, 
      'computer_id': self._computer_guid})

# XXX What is this SoftwareInstance class?
class SoftwareInstance(SlapDocument):
  """
  Contains Software Instance information
  """

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

"""Exposed exceptions"""
# XXX Why do we need to expose exceptions?
class ResourceNotReady(Exception):
  pass

class ServerError(Exception):
  pass

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

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

class Supply(SlapDocument):

  zope.interface.implements(interface.ISupply)

  def supply(self, software_release, computer_guid=None):
    self._connection_helper.POST('/supplySupply', {
Łukasz Nowak's avatar
Łukasz Nowak committed
144
      'url': software_release,
Łukasz Nowak's avatar
Łukasz Nowak committed
145 146 147 148 149 150 151
      'computer_id': computer_guid})

class OpenOrder(SlapDocument):

  zope.interface.implements(interface.IOpenOrder)

  def request(self, software_release, partition_reference,
152
      partition_parameter_kw=None, software_type=None, filter_kw=None,
153
      state=None, shared=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
154 155
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
156 157
    if filter_kw is None:
      filter_kw = {}
Łukasz Nowak's avatar
Łukasz Nowak committed
158 159 160 161
    request_dict = {
        'software_release': software_release,
        'partition_reference': partition_reference,
        'partition_parameter_xml': xml_marshaller.dumps(partition_parameter_kw),
162
        'filter_xml': xml_marshaller.dumps(filter_kw),
163
        'state': xml_marshaller.dumps(state),
164
        'shared_xml': xml_marshaller.dumps(shared),
Łukasz Nowak's avatar
Łukasz Nowak committed
165 166 167
      }
    if software_type is not None:
      request_dict['software_type'] = software_type
168 169 170 171 172 173 174 175 176 177
    try:
      self._connection_helper.POST('/requestComputerPartition', request_dict)
    except ResourceNotReady:
      return ComputerPartition(request_dict=request_dict)
    else:
      xml = self._connection_helper.response.read()
      software_instance = xml_marshaller.loads(xml)
      return ComputerPartition(
        software_instance.slap_computer_id.encode('UTF-8'),
        software_instance.slap_computer_partition_id.encode('UTF-8'))
Łukasz Nowak's avatar
Łukasz Nowak committed
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217

def _syncComputerInformation(func):
  """
  Synchronize computer object with server information
  """
  def decorated(self, *args, **kw):
    computer = self._connection_helper.getComputerInformation(self._computer_id)
    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)
    return func(self, *args, **kw)
  return decorated 

class Computer(SlapDocument):

  zope.interface.implements(interface.IComputer)

  def __init__(self, computer_id):
    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.
    """
    return self._software_release_list

  @_syncComputerInformation
  def getComputerPartitionList(self):
    return [x for x in self._computer_partition_list if x._need_modification]

218 219
  def reportUsage(self, computer_usage):
    if computer_usage == "":
Łukasz Nowak's avatar
Łukasz Nowak committed
220 221 222
      return
    self._connection_helper.POST('/useComputer', {
      'computer_id': self._computer_id,
223
      'use_string': computer_usage})
Łukasz Nowak's avatar
Łukasz Nowak committed
224 225 226 227 228 229

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

Łukasz Nowak's avatar
Łukasz Nowak committed
230 231 232 233 234
  def bang(self, message):
    self._connection_helper.POST('/computerBang', {
      'computer_id': self._computer_id,
      'message': message})

Łukasz Nowak's avatar
Łukasz Nowak committed
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261
def _syncComputerPartitionInformation(func):
  """
  Synchronize computer partition object with server information
  """
  def decorated(self, *args, **kw):
    computer = self._connection_helper.getComputerInformation(self._computer_id)
    found_computer_partition = None
    for computer_partition in computer._computer_partition_list:
      if computer_partition.getId() == self.getId():
        found_computer_partition = computer_partition
        break
    if found_computer_partition is None:
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      for key, value in found_computer_partition.__dict__.items():
        if isinstance(value, unicode):
          # convert unicode to utf-8
          setattr(self, key, value.encode('utf-8'))
        if isinstance(value, dict):
          new_dict = {}
          for ink, inv in value.iteritems():
            if isinstance(inv, (list, tuple)):
              new_inv = []
              for elt in inv:
                if isinstance(elt, (list, tuple)):
                  new_inv.append([x.encode('utf-8') for x in elt])
262 263 264
                elif isinstance(elt, dict):
                  new_inv.append(dict([(x.encode('utf-8'),
                    y and y.encode("utf-8")) for x,y in elt.iteritems()]))
Łukasz Nowak's avatar
Łukasz Nowak committed
265 266 267 268 269 270 271 272 273 274 275 276 277
                else:
                  new_inv.append(elt.encode('utf-8'))
              new_dict[ink.encode('utf-8')] = new_inv
            elif inv is None:
              new_dict[ink.encode('utf-8')] = None
            else:
              new_dict[ink.encode('utf-8')] = inv.encode('utf-8')
          setattr(self, key, new_dict)
        else:
          setattr(self, key, value)
    return func(self, *args, **kw)
  return decorated

278

Łukasz Nowak's avatar
Łukasz Nowak committed
279 280 281 282
class ComputerPartition(SlapDocument):

  zope.interface.implements(interface.IComputerPartition)

283 284 285 286 287 288 289 290
  def __init__(self, computer_id=None, partition_id=None, request_dict=None):
    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
291 292
    self._computer_id = computer_id
    self._partition_id = partition_id
293
    self._request_dict = request_dict
Łukasz Nowak's avatar
Łukasz Nowak committed
294 295 296 297 298 299 300 301 302 303 304

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

  # XXX: As request is decorated with _syncComputerPartitionInformation it
  #      will raise ResourceNotReady really early -- just after requesting,
  #      and not when try to access to real partition is required.
  #      To have later raising (like in case of calling methods), the way how
  #      Computer Partition data are fetch from server shall be delayed
  @_syncComputerPartitionInformation
  def request(self, software_release, software_type, partition_reference,
305 306
              shared=False, partition_parameter_kw=None, filter_kw=None,
              state=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
307 308 309 310 311 312 313 314 315 316 317 318
    if partition_parameter_kw is None:
      partition_parameter_kw = {}
    elif not isinstance(partition_parameter_kw, dict):
      raise ValueError("Unexpected type of partition_parameter_kw '%s'" % \
                       partition_parameter_kw)

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

319
    request_dict = { 'computer_id': self._computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
320 321 322 323 324 325 326 327
        '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),
328
        'state': xml_marshaller.dumps(state),
329
      }
330 331 332 333 334 335 336 337 338 339
    try:
      self._connection_helper.POST('/requestComputerPartition', request_dict)
    except ResourceNotReady:
      return ComputerPartition(request_dict=request_dict)
    else:
      xml = self._connection_helper.response.read()
      software_instance = xml_marshaller.loads(xml)
      return ComputerPartition(
        software_instance.slap_computer_id.encode('UTF-8'),
        software_instance.slap_computer_partition_id.encode('UTF-8'))
Łukasz Nowak's avatar
Łukasz Nowak committed
340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374

  def building(self):
    self._connection_helper.POST('/buildingComputerPartition', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id})

  def available(self):
    self._connection_helper.POST('/availableComputerPartition', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id})

  def destroyed(self):
    self._connection_helper.POST('/destroyedComputerPartition', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
      })

  def started(self):
    self._connection_helper.POST('/startedComputerPartition', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
      })

  def stopped(self):
    self._connection_helper.POST('/stoppedComputerPartition', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
      })

  def error(self, error_log):
    self._connection_helper.POST('/softwareInstanceError', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
      'error_log': error_log})

Łukasz Nowak's avatar
Łukasz Nowak committed
375 376 377 378 379 380
  def bang(self, message):
    self._connection_helper.POST('/softwareInstanceBang', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
      'message': message})

381
  def rename(self, new_name, slave_reference=None):
382 383 384 385 386 387 388 389
    post_dict = dict(
      computer_id=self._computer_id,
      computer_partition_id=self._partition_id,
      new_name=new_name,
    )
    if slave_reference is not None:
      post_dict.update(slave_reference=slave_reference)
    self._connection_helper.POST('/softwareInstanceRename', post_dict)
390

Łukasz Nowak's avatar
Łukasz Nowak committed
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
  def getId(self):
    return self._partition_id

  @_syncComputerPartitionInformation
  def getState(self):
    return self._requested_state

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

  @_syncComputerPartitionInformation
  def getSoftwareRelease(self):
    """
    Returns the software release associate to the computer partition.
    """
    if self._software_release_document is None:
      raise NotFoundError("No software release information for partition %s" %
          self.getId())
    else:
      return self._software_release_document

413
  def setConnectionDict(self, connection_dict, slave_reference=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
414 415 416
    self._connection_helper.POST('/setComputerPartitionConnectionXml', {
      'computer_id': self._computer_id,
      'computer_partition_id': self._partition_id,
417 418
      'connection_xml': xml_marshaller.dumps(connection_dict),
      'slave_reference': slave_reference})
Łukasz Nowak's avatar
Łukasz Nowak committed
419 420 421 422 423 424 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 451 452 453 454 455 456 457

  @_syncComputerPartitionInformation
  def getConnectionParameter(self, key):
    connection_dict = getattr(self, '_connection_dict', None) or {}
    if key in connection_dict:
      return connection_dict[key]
    else:
      raise NotFoundError("%s not found" % key)

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

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

# def lazyMethod(func):
#   """
#   Return a function which stores a computed value in an instance
#   at the first call.
#   """
#   key = '_cache_' + str(id(func))
#   def decorated(self, *args, **kw):
#     try:
#       return getattr(self, key)
#     except AttributeError:
#       result = func(self, *args, **kw)
#       setattr(self, key, result)
#       return result
#   return decorated 

class ConnectionHelper:
  error_message_connect_fail = "Couldn't connect to the server. Please double \
check given master-url argument, and make sure that IPv6 is enabled on \
your machine and that the server is available. The original error was:"
  def __init__(self, connection_wrapper, host, path, key_file=None,
458
      cert_file=None, master_ca_file=None, timeout=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
459 460 461 462 463 464
    self.connection_wrapper = connection_wrapper
    self.host = host
    self.path = path
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
465
    self.timeout = timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483

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

  def connect(self):
    connection_dict = dict(
        host=self.host)
    if self.key_file and self.cert_file:
      connection_dict.update(
        key_file=self.key_file,
        cert_file=self.cert_file)
    if self.master_ca_file is not None:
      connection_dict.update(ca_file=self.master_ca_file)
    self.connection = self.connection_wrapper(**connection_dict)

  def GET(self, path):
    try:
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
      default_timeout = socket.getdefaulttimeout()
      socket.setdefaulttimeout(self.timeout)
      try:
        self.connect()
        self.connection.request('GET', self.path + path)
        self.response = self.connection.getresponse()
      except socket.error, e:
        raise socket.error(self.error_message_connect_fail + str(e))
      # check self.response.status and raise exception early
      if self.response.status == httplib.REQUEST_TIMEOUT:
        # resource is not ready
        raise ResourceNotReady(path)
      elif self.response.status == httplib.NOT_FOUND:
        raise NotFoundError(path)
      elif self.response.status == httplib.FORBIDDEN:
        raise Unauthorized(path)
      elif self.response.status != httplib.OK:
        message = 'Server responded with wrong code %s with %s' % \
                                           (self.response.status, path)
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
506 507 508 509

  def POST(self, path, parameter_dict,
      content_type="application/x-www-form-urlencoded"):
    try:
510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
      default_timeout = socket.getdefaulttimeout()
      socket.setdefaulttimeout(self.timeout)
      try:
        self.connect()
        header_dict = {'Content-type': content_type}
        self.connection.request("POST", self.path + path,
            urllib.urlencode(parameter_dict), header_dict)
      except socket.error, e:
        raise socket.error(self.error_message_connect_fail + str(e))
      self.response = self.connection.getresponse()
      # check self.response.status and raise exception early
      if self.response.status == httplib.REQUEST_TIMEOUT:
        # resource is not ready
        raise ResourceNotReady("%s - %s" % (path, parameter_dict))
      elif self.response.status == httplib.NOT_FOUND:
        raise NotFoundError("%s - %s" % (path, parameter_dict))
      elif self.response.status == httplib.FORBIDDEN:
        raise Unauthorized("%s - %s" % (path, parameter_dict))
      elif self.response.status != httplib.OK:
        message = 'Server responded with wrong code %s with %s' % \
                                           (self.response.status, path)
        raise ServerError(message)
    finally:
      socket.setdefaulttimeout(default_timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
534 535 536 537 538 539

class slap:

  zope.interface.implements(interface.slap)

  def initializeConnection(self, slapgrid_uri, key_file=None, cert_file=None,
540
      master_ca_file=None, timeout=60):
Łukasz Nowak's avatar
Łukasz Nowak committed
541
    self._initialiseConnectionHelper(slapgrid_uri, key_file, cert_file,
542
        master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
543 544

  def _initialiseConnectionHelper(self, slapgrid_uri, key_file, cert_file,
545
      master_ca_file, timeout):
Łukasz Nowak's avatar
Łukasz Nowak committed
546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564
    SlapDocument._slapgrid_uri = slapgrid_uri
    scheme, netloc, path, query, fragment = urlparse.urlsplit(
        SlapDocument._slapgrid_uri)
    if not(query == '' and fragment == ''):
      raise AttributeError('Passed URL %r issue: not parseable'%
          SlapDocument._slapgrid_uri)
    if scheme not in ('http', 'https'):
      raise AttributeError('Passed URL %r issue: there is no support for %r p'
          'rotocol' % (SlapDocument._slapgrid_uri, scheme))

    if scheme == 'http':
      connection_wrapper = httplib.HTTPConnection
    else:
      if master_ca_file is not None:
        connection_wrapper = HTTPSConnectionCA
      else:
        connection_wrapper = httplib.HTTPSConnection
    slap._connection_helper = \
      SlapDocument._connection_helper = ConnectionHelper(connection_wrapper,
565
          netloc, path, key_file, cert_file, master_ca_file, timeout)
Łukasz Nowak's avatar
Łukasz Nowak committed
566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606

  def _register(self, klass, *registration_argument_list):
    if len(registration_argument_list) == 1 and type(
        registration_argument_list[0]) == type([]):
      # in case if list is explicitly passed and there is only one
      # argument in registration convert it to list
      registration_argument_list = registration_argument_list[0]
    document = klass(*registration_argument_list)
    return document

  def registerSoftwareRelease(self, software_release):
    """
    Registers connected representation of software release and
    returns SoftwareRelease class object
    """
    return SoftwareRelease(software_release=software_release)

  def registerComputer(self, computer_guid):
    """
    Registers connected representation of computer and
    returns Computer class object
    """
    self.computer_guid = computer_guid
    return self._register(Computer, computer_guid)

  def registerComputerPartition(self, computer_guid, partition_id):
    """
    Registers connected representation of computer partition and
    returns Computer Partition class object
    """
    self._connection_helper.GET('/registerComputerPartition?' \
        'computer_reference=%s&computer_partition_reference=%s' % (
          computer_guid, partition_id))
    xml = self._connection_helper.response.read()
    return xml_marshaller.loads(xml)

  def registerOpenOrder(self):
    return OpenOrder()

  def registerSupply(self):
    return Supply()