SlapTool.py 58 KB
Newer Older
Łukasz Nowak's avatar
Łukasz Nowak committed
1 2 3
# -*- coding: utf-8 -*-
##############################################################################
#
Łukasz Nowak's avatar
Łukasz Nowak committed
4
# Copyright (c) 2010-2011 Nexedi SA and Contributors. All Rights Reserved.
Łukasz Nowak's avatar
Łukasz Nowak committed
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
#                    Łukasz Nowak <luke@nexedi.com>
#                    Romain Courteaud <romain@nexedi.com>
#
# 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 advised 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 2
# 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.
#
##############################################################################

from AccessControl import ClassSecurityInfo
from AccessControl import Unauthorized
33 34
from AccessControl.Permissions import access_contents_information
from AccessControl import getSecurityManager
35
from Products.ERP5Type.UnrestrictedMethod import UnrestrictedMethod
Łukasz Nowak's avatar
Łukasz Nowak committed
36 37 38 39 40
from OFS.Traversable import NotFound
from Products.DCWorkflow.DCWorkflow import ValidationFailed
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions
41
from Products.ERP5Type.Cache import DEFAULT_CACHE_SCOPE
Łukasz Nowak's avatar
Łukasz Nowak committed
42
from Products.ERP5Type.Cache import CachingMethod
Łukasz Nowak's avatar
Łukasz Nowak committed
43
from lxml import etree
44
import time
Łukasz Nowak's avatar
Łukasz Nowak committed
45
from Products.ERP5Type.tests.utils import DummyMailHostMixin
Łukasz Nowak's avatar
Łukasz Nowak committed
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
try:
  from slapos.slap.slap import Computer
  from slapos.slap.slap import ComputerPartition as SlapComputerPartition
  from slapos.slap.slap import SoftwareInstance
  from slapos.slap.slap import SoftwareRelease
except ImportError:
  # Do no prevent instance from starting
  # if libs are not installed
  class Computer:
    def __init__(self):
      raise ImportError
  class SlapComputerPartition:
    def __init__(self):
      raise ImportError
  class SoftwareInstance:
    def __init__(self):
      raise ImportError
  class SoftwareRelease:
    def __init__(self):
      raise ImportError

from zLOG import LOG, INFO
import xml_marshaller
69 70
import StringIO
import pkg_resources
Łukasz Nowak's avatar
Łukasz Nowak committed
71
from Products.Vifib.Conduit import VifibConduit
72 73 74
import json
from DateTime import DateTime
from App.Common import rfc1123_date
Łukasz Nowak's avatar
Łukasz Nowak committed
75 76 77 78 79 80 81 82 83 84 85 86 87
class SoftwareInstanceNotReady(Exception):
  pass

def convertToREST(function):
  """
  Wrap the method to create a log entry for each invocation to the zope logger
  """
  def wrapper(self, *args, **kwd):
    """
    Log the call, and the result of the call
    """
    try:
      retval = function(self, *args, **kwd)
88
    except (ValueError, AttributeError), log:
Łukasz Nowak's avatar
Łukasz Nowak committed
89 90 91 92 93
      LOG('SlapTool', INFO, 'Converting ValueError to NotFound, real error:',
          error=True)
      raise NotFound(log)
    except SoftwareInstanceNotReady, log:
      self.REQUEST.response.setStatus(408)
94
      self.REQUEST.response.setHeader('Cache-Control', 'private')
Łukasz Nowak's avatar
Łukasz Nowak committed
95 96
      return self.REQUEST.response
    except ValidationFailed:
Łukasz Nowak's avatar
Łukasz Nowak committed
97 98
      LOG('SlapTool', INFO, 'Converting ValidationFailed to ValidationFailed,'\
        ' real error:',
Łukasz Nowak's avatar
Łukasz Nowak committed
99 100
          error=True)
      raise ValidationFailed
101 102 103 104 105
    except Unauthorized:
      LOG('SlapTool', INFO, 'Converting Unauthorized to Unauthorized,'\
        ' real error:',
          error=True)
      raise Unauthorized
Łukasz Nowak's avatar
Łukasz Nowak committed
106

Łukasz Nowak's avatar
Łukasz Nowak committed
107
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
Łukasz Nowak's avatar
Łukasz Nowak committed
108 109 110 111
    return '%s' % retval
  wrapper.__doc__ = function.__doc__
  return wrapper

112
def _assertACI(document):
113 114
  sm = getSecurityManager()
  if sm.checkPermission(access_contents_information,
115 116
      document):
    return document
117
  raise Unauthorized('User %r has no access to %r' % (sm.getUser(), document))
118 119


120 121
_MARKER = []

Łukasz Nowak's avatar
Łukasz Nowak committed
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
class SlapTool(BaseTool):
  """SlapTool"""

  # TODO:
  #   * catch and convert exceptions to HTTP codes (be restful)

  id = 'portal_slap'
  meta_type = 'ERP5 Slap Tool'
  portal_type = 'Slap Tool'
  security = ClassSecurityInfo()
  allowed_types = ()

  security.declarePrivate('manage_afterAdd')
  def manage_afterAdd(self, item, container) :
    """Init permissions right after creation.

    Permissions in slap tool are simple:
     o Each member can access the tool.
     o Only manager can view and create.
     o Anonymous can not access
    """
    item.manage_permission(Permissions.AddPortalContent,
          ['Manager'])
    item.manage_permission(Permissions.AccessContentsInformation,
          ['Member', 'Manager'])
    item.manage_permission(Permissions.View,
          ['Manager',])
    BaseTool.inheritedAttribute('manage_afterAdd')(self, item, container)

  ####################################################
  # Public GET methods
  ####################################################

Łukasz Nowak's avatar
Łukasz Nowak committed
155
  def _isTestRun(self):
Łukasz Nowak's avatar
Łukasz Nowak committed
156
    if issubclass(self.getPortalObject().MailHost.__class__, DummyMailHostMixin) \
157
        or self.REQUEST.get('test_list'):
Łukasz Nowak's avatar
Łukasz Nowak committed
158 159 160
      return True
    return False

161 162 163 164 165
  def _getCachePlugin(self):
    return self.getPortalObject().portal_caches\
      .getRamCacheRoot().get('computer_information_cache_factory')\
      .getCachePluginList()[0]

Łukasz Nowak's avatar
Łukasz Nowak committed
166
  def _getCacheComputerInformation(self, computer_id, user):
167
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
168
    slap_computer = Computer(computer_id.decode("UTF-8"))
169 170 171 172
    parent_uid = self._getComputerUidByReference(computer_id)

    slap_computer._computer_partition_list = []
    slap_computer._software_release_list = \
Łukasz Nowak's avatar
Łukasz Nowak committed
173
       self._getSoftwareReleaseValueListForComputer(computer_id)
174
    for computer_partition in self.getPortalObject().portal_catalog.unrestrictedSearchResults(
175 176 177 178
                    parent_uid=parent_uid,
                    validation_state="validated",
                    portal_type="Computer Partition"):
      slap_computer._computer_partition_list.append(
179
          self._getSlapPartitionByPackingList(_assertACI(computer_partition.getObject())))
Łukasz Nowak's avatar
Łukasz Nowak committed
180 181
    return xml_marshaller.xml_marshaller.dumps(slap_computer)

Łukasz Nowak's avatar
Łukasz Nowak committed
182 183
  def _fillComputerInformationCache(self, computer_id, user):
    key = '%s_%s' % (computer_id, user)
Łukasz Nowak's avatar
Łukasz Nowak committed
184 185 186 187
    try:
      self._getCachePlugin().set(key, DEFAULT_CACHE_SCOPE,
        dict (
          time=time.time(),
Łukasz Nowak's avatar
Łukasz Nowak committed
188
          data=self._getCacheComputerInformation(computer_id, user),
Łukasz Nowak's avatar
Łukasz Nowak committed
189 190 191 192 193
        ),
        cache_duration=self.getPortalObject().portal_caches\
            .getRamCacheRoot().get('computer_information_cache_factory'\
              ).cache_duration
        )
194 195
    except (Unauthorized, IndexError):
      # XXX: Unauthorized hack. Race condition of not ready setup delivery which provides
Łukasz Nowak's avatar
Łukasz Nowak committed
196 197
      # security information shall not make this method fail, as it will be
      # called later anyway
198 199
      # Note: IndexError ignored, as it happend in case if full reindex is
      # called on site
Łukasz Nowak's avatar
Łukasz Nowak committed
200
      pass
201

202 203 204 205
  def _storeLastData(self, key, value):
    cache_plugin = self.getPortalObject().portal_caches\
      .getRamCacheRoot().get('last_stored_data_cache_factory')\
      .getCachePluginList()[0]
Łukasz Nowak's avatar
Łukasz Nowak committed
206 207 208
    cache_plugin.set(key, DEFAULT_CACHE_SCOPE, value,
      cache_duration=self.getPortalObject().portal_caches\
      .getRamCacheRoot().get('last_stored_data_cache_factory').cache_duration)
209 210 211 212 213 214 215 216 217

  def _getLastData(self, key):
    cache_plugin = self.getPortalObject().portal_caches\
      .getRamCacheRoot().get('last_stored_data_cache_factory')\
      .getCachePluginList()[0]
    try:
      entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
    except KeyError:
      entry = None
Łukasz Nowak's avatar
Łukasz Nowak committed
218 219
    else:
      entry = entry.getValue()
220 221
    return entry

Łukasz Nowak's avatar
Łukasz Nowak committed
222 223
  def _activateFillComputerInformationCache(self, computer_id, user):
    tag = 'computer_information_cache_fill_%s_%s' % (computer_id, user)
224 225
    if self.getPortalObject().portal_activities.countMessageWithTag(tag) == 0:
      self.activate(activity='SQLQueue', tag=tag)._fillComputerInformationCache(
Łukasz Nowak's avatar
Łukasz Nowak committed
226
        computer_id, user)
227

Łukasz Nowak's avatar
Łukasz Nowak committed
228
  def _getComputerInformation(self, computer_id, user):
229 230
    user_document = _assertACI(self.getPortalObject().portal_catalog.unrestrictedGetResultValue(
      reference=user, portal_type=['Person', 'Computer', 'Software Instance']))
Łukasz Nowak's avatar
Łukasz Nowak committed
231
    user_type = user_document.getPortalType()
232
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
233
    slap_computer = Computer(computer_id.decode("UTF-8"))
Łukasz Nowak's avatar
Łukasz Nowak committed
234 235 236 237
    parent_uid = self._getComputerUidByReference(computer_id)

    slap_computer._computer_partition_list = []
    if user_type in ('Computer', 'Person'):
238 239 240
      if not self._isTestRun():
        cache_plugin = self._getCachePlugin()
        try:
Łukasz Nowak's avatar
Łukasz Nowak committed
241
          key = '%s_%s' % (computer_id, user)
Łukasz Nowak's avatar
Łukasz Nowak committed
242
          entry = cache_plugin.get(key, DEFAULT_CACHE_SCOPE)
243 244 245 246
        except KeyError:
          entry = None
        if entry is not None and type(entry.getValue()) == type({}):
          result = entry.getValue()['data']
Łukasz Nowak's avatar
Łukasz Nowak committed
247
          self._activateFillComputerInformationCache(computer_id, user)
248 249
          return result
        else:
Łukasz Nowak's avatar
Łukasz Nowak committed
250
          self._activateFillComputerInformationCache(computer_id, user)
251 252 253
          self.REQUEST.response.setStatus(503)
          return self.REQUEST.response
      else:
Łukasz Nowak's avatar
Łukasz Nowak committed
254 255
        return self._getCacheComputerInformation(computer_id, user)
#      return self._getCacheComputerInformation(computer_id, user)
Łukasz Nowak's avatar
Łukasz Nowak committed
256 257
    else:
      slap_computer._software_release_list = []
258
    if user_type == 'Software Instance':
259 260 261 262 263 264
      computer = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
        portal_type='Computer', reference=computer_id,
        validation_state="validated")[0].getObject()
      computer_partition_list = computer.contentValues(
        portal_type="Computer Partition",
        checked_permission="View")
265 266
    else:
      computer_partition_list = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
Łukasz Nowak's avatar
Łukasz Nowak committed
267 268
                    parent_uid=parent_uid,
                    validation_state="validated",
269 270
                    portal_type="Computer Partition")
    for computer_partition in computer_partition_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
271
      slap_computer._computer_partition_list.append(
272
          self._getSlapPartitionByPackingList(_assertACI(computer_partition.getObject())))
Łukasz Nowak's avatar
Łukasz Nowak committed
273 274
    return xml_marshaller.xml_marshaller.dumps(slap_computer)

275 276 277 278 279 280 281 282 283
  security.declareProtected(Permissions.AccessContentsInformation,
    'getFullComputerInformation')
  def getFullComputerInformation(self, computer_id):
    """Returns marshalled XML of all needed information for computer

    Includes Software Releases, which may contain Software Instances.

    Reuses slap library for easy marshalling.
    """
284
    user = self.getPortalObject().portal_membership.getAuthenticatedMember().getUserName()
285 286
    if str(user) == computer_id:
      self._logAccess(user, user, '#access %s' % computer_id)
Łukasz Nowak's avatar
Łukasz Nowak committed
287
    result = self._getComputerInformation(computer_id, user)
288

289 290 291 292 293 294 295 296 297 298 299
    if self.REQUEST.response.getStatus() == 200:
      # Keep in cache server for 7 days
      self.REQUEST.response.setHeader('Cache-Control',
                                      'public, max-age=1, stale-if-error=604800')
      self.REQUEST.response.setHeader('Vary',
                                      'REMOTE_USER')
      self.REQUEST.response.setHeader('Last-Modified', rfc1123_date(DateTime()))
      self.REQUEST.response.setBody(result)
      return self.REQUEST.response
    else:
      return result
300

Łukasz Nowak's avatar
Łukasz Nowak committed
301 302
  security.declareProtected(Permissions.AccessContentsInformation,
    'getComputerPartitionCertificate')
Łukasz Nowak's avatar
Łukasz Nowak committed
303 304
  def getComputerPartitionCertificate(self, computer_id, computer_partition_id):
    """Method to fetch certificate"""
305
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
Łukasz Nowak's avatar
Łukasz Nowak committed
306 307 308 309 310 311
    software_instance = self._getSoftwareInstanceForComputerPartition(
      computer_id, computer_partition_id)
    certificate_dict = dict(
      key=software_instance.getSslKey(),
      certificate=software_instance.getSslCertificate()
    )
312 313 314 315 316 317 318 319 320 321 322
    result = xml_marshaller.xml_marshaller.dumps(certificate_dict)
    # Cache with revalidation
    self.REQUEST.response.setStatus(200)
    self.REQUEST.response.setHeader('Cache-Control',
                                    'public, max-age=0, must-revalidate')
    self.REQUEST.response.setHeader('Vary',
                                    'REMOTE_USER')
    self.REQUEST.response.setHeader('Last-Modified',
                                    rfc1123_date(software_instance.getModificationDate()))
    self.REQUEST.response.setBody(result)
    return self.REQUEST.response
Łukasz Nowak's avatar
Łukasz Nowak committed
323

Łukasz Nowak's avatar
Łukasz Nowak committed
324 325 326 327
  security.declareProtected(Permissions.AccessContentsInformation,
    'getComputerInformation')
  getComputerInformation = getFullComputerInformation

328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355
  security.declareProtected(Permissions.AccessContentsInformation,
    'getComputerPartitionStatus')
  def getComputerPartitionStatus(self, computer_id, computer_partition_id):
    """
    Get the connection status of the partition
    """
    try:
      instance = self._getSoftwareInstanceForComputerPartition(
          computer_id,
          computer_partition_id)
    except NotFound:
      return self._getAccessStatus(None)
    else:
      return self._getAccessStatus(instance.getReference())

  security.declareProtected(Permissions.AccessContentsInformation,
    'getComputerStatus')
  def getComputerStatus(self, computer_id):
    """
    Get the connection status of the partition
    """
    computer = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
      portal_type='Computer', reference=computer_id,
      validation_state="validated")[0].getObject()
    # Be sure to prevent accessing information to disallowed users
    computer = _assertACI(computer)
    return self._getAccessStatus(computer_id)

356 357
  security.declareProtected(Permissions.AccessContentsInformation,
    'getSoftwareReleaseListFromSoftwareProduct')
358 359
  def getSoftwareReleaseListFromSoftwareProduct(self,
      software_product_reference=None, software_release_url=None):
360
    """
361 362 363
    Get the list of all published Software Releases related to one of either:
      * A given Software Product as aggregate
      * Another Software Release from the same Software Product as aggregate,
364
    sorted by descending age (latest first).
365 366 367
    If both software_product_reference/software_release_url are defined, raise.
    If referenced Software Product does not exist, return empty list.
    If referenced Software Release does not exist, raise.
368
    """
369 370 371 372 373 374 375 376 377 378
    if software_product_reference is None:
      assert(software_release_url is not None)
      software_product_reference = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
        portal_type='Software Release',
        url_string=software_release_url
      )[0].getObject().getAggregateValue().getReference()
    else:
      # Don't accept both parameters
      assert(software_release_url is None)

379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    software_product_list = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
      portal_type='Software Product',
      reference=software_product_reference,
      validation_state='published')
    if len(software_product_list) is 0:
      return xml_marshaller.xml_marshaller.dumps([])
    if len(software_product_list) > 1:
      raise NotImplementedError('Several Software Product with the same title.')
    software_release_list = \
        software_product_list[0].getObject().getAggregateRelatedValueList()
    software_release_list = sorted(
        software_release_list,
        key=lambda software_release: software_release.getCreationDate(),
        reverse=True,
    )
    return xml_marshaller.xml_marshaller.dumps(
      [software_release.getUrlString()
        for software_release in software_release_list
          if software_release.getValidationState() == 'published'])


Łukasz Nowak's avatar
Łukasz Nowak committed
400 401 402 403
  ####################################################
  # Public POST methods
  ####################################################

Łukasz Nowak's avatar
Łukasz Nowak committed
404 405
  security.declareProtected(Permissions.AccessContentsInformation,
    'setComputerPartitionConnectionXml')
Łukasz Nowak's avatar
Łukasz Nowak committed
406 407
  def setComputerPartitionConnectionXml(self, computer_id,
                                        computer_partition_id,
408
                                        connection_xml, slave_reference=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
409 410 411
    """
    Set instance parameter informations on the slagrid server
    """
412 413 414
    # When None is passed in POST, it is converted to string
    if slave_reference is not None and slave_reference.lower() == "none":
      slave_reference = None
Łukasz Nowak's avatar
Łukasz Nowak committed
415 416
    return self._setComputerPartitionConnectionXml(computer_id,
                                                   computer_partition_id,
417 418
                                                   connection_xml,
                                                   slave_reference)
Łukasz Nowak's avatar
Łukasz Nowak committed
419

420 421
  security.declareProtected(Permissions.AccessContentsInformation,
    'supplySupply')
422
  def supplySupply(self, url, computer_id, state='available'):
423 424 425
    """
    Request Software Release installation
    """
426
    return self._supplySupply(url, computer_id, state)
427

428 429 430 431 432
  @convertToREST
  def _requestComputer(self, computer_title):
    portal = self.getPortalObject()
    person = portal.ERP5Site_getAuthenticatedMemberPersonValue()
    person.requestComputer(computer_title=computer_title)
433
    computer = Computer(self.REQUEST.get('computer_reference').decode("UTF-8"))
434 435 436 437 438 439 440 441 442 443
    return xml_marshaller.xml_marshaller.dumps(computer)

  security.declareProtected(Permissions.AccessContentsInformation,
    'requestComputer')
  def requestComputer(self, computer_title):
    """
    Request Computer
    """
    return self._requestComputer(computer_title)

Łukasz Nowak's avatar
Łukasz Nowak committed
444 445
  security.declareProtected(Permissions.AccessContentsInformation,
    'buildingSoftwareRelease')
Łukasz Nowak's avatar
Łukasz Nowak committed
446 447 448 449 450 451
  def buildingSoftwareRelease(self, url, computer_id):
    """
    Reports that Software Release is being build
    """
    return self._buildingSoftwareRelease(url, computer_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
452 453
  security.declareProtected(Permissions.AccessContentsInformation,
    'availableSoftwareRelease')
Łukasz Nowak's avatar
Łukasz Nowak committed
454 455 456 457 458 459
  def availableSoftwareRelease(self, url, computer_id):
    """
    Reports that Software Release is available
    """
    return self._availableSoftwareRelease(url, computer_id)

460 461 462 463 464 465 466 467
  security.declareProtected(Permissions.AccessContentsInformation,
    'destroyedSoftwareRelease')
  def destroyedSoftwareRelease(self, url, computer_id):
    """
    Reports that Software Release is available
    """
    return self._destroyedSoftwareRelease(url, computer_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
468 469
  security.declareProtected(Permissions.AccessContentsInformation,
    'softwareReleaseError')
Łukasz Nowak's avatar
Łukasz Nowak committed
470 471 472 473
  def softwareReleaseError(self, url, computer_id, error_log):
    """
    Add an error for a software Release workflow
    """
474
    return self._softwareReleaseError(url, computer_id, error_log)
Łukasz Nowak's avatar
Łukasz Nowak committed
475

Łukasz Nowak's avatar
Łukasz Nowak committed
476 477
  security.declareProtected(Permissions.AccessContentsInformation,
    'buildingComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
478 479 480 481 482 483
  def buildingComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is being build
    """
    return self._buildingComputerPartition(computer_id, computer_partition_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
484 485
  security.declareProtected(Permissions.AccessContentsInformation,
    'availableComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
486 487 488 489 490 491
  def availableComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is available
    """
    return self._availableComputerPartition(computer_id, computer_partition_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
492 493
  security.declareProtected(Permissions.AccessContentsInformation,
    'softwareInstanceError')
Łukasz Nowak's avatar
Łukasz Nowak committed
494 495 496 497 498 499 500 501
  def softwareInstanceError(self, computer_id,
                            computer_partition_id, error_log):
    """
    Add an error for the software Instance Workflow
    """
    return self._softwareInstanceError(computer_id, computer_partition_id,
                                       error_log)

502 503 504 505
  security.declareProtected(Permissions.AccessContentsInformation,
    'softwareInstanceRename')
  def softwareInstanceRename(self, new_name, computer_id,
                             computer_partition_id, slave_reference=None):
506 507 508
    """
    Change the title of a Software Instance using Workflow.
    """
509 510 511 512
    return self._softwareInstanceRename(new_name, computer_id,
                                        computer_partition_id,
                                        slave_reference)

Łukasz Nowak's avatar
Łukasz Nowak committed
513 514 515 516 517 518 519 520 521 522
  security.declareProtected(Permissions.AccessContentsInformation,
    'softwareInstanceBang')
  def softwareInstanceBang(self, computer_id,
                            computer_partition_id, message):
    """
    Fire up bang on this Software Instance
    """
    return self._softwareInstanceBang(computer_id, computer_partition_id,
                                       message)

Łukasz Nowak's avatar
Łukasz Nowak committed
523 524
  security.declareProtected(Permissions.AccessContentsInformation,
    'startedComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
525 526 527 528 529 530
  def startedComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is started
    """
    return self._startedComputerPartition(computer_id, computer_partition_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
531 532
  security.declareProtected(Permissions.AccessContentsInformation,
    'stoppedComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
533 534 535 536 537 538
  def stoppedComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is stopped
    """
    return self._stoppedComputerPartition(computer_id, computer_partition_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
539 540
  security.declareProtected(Permissions.AccessContentsInformation,
    'destroyedComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
541 542 543 544 545 546
  def destroyedComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is destroyed
    """
    return self._destroyedComputerPartition(computer_id, computer_partition_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
547 548
  security.declareProtected(Permissions.AccessContentsInformation,
    'requestComputerPartition')
549 550
  def requestComputerPartition(self, computer_id=None,
      computer_partition_id=None, software_release=None, software_type=None,
551
      partition_reference=None, partition_parameter_xml=None,
552
      filter_xml=None, state=None, shared_xml=_MARKER):
Łukasz Nowak's avatar
Łukasz Nowak committed
553 554 555 556 557 558 559 560 561 562 563 564
    """
    Asynchronously requests creation of computer partition for assigned
    parameters

    Returns XML representation of partition with HTTP code 200 OK

    In case if this request is still being processed data contain
    "Computer Partition is being processed" and HTTP code is 408 Request Timeout

    In any other case returns not important data and HTTP code is 403 Forbidden
    """
    return self._requestComputerPartition(computer_id, computer_partition_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
565
        software_release, software_type, partition_reference,
566
        shared_xml, partition_parameter_xml, filter_xml, state)
Łukasz Nowak's avatar
Łukasz Nowak committed
567

Łukasz Nowak's avatar
Łukasz Nowak committed
568 569
  security.declareProtected(Permissions.AccessContentsInformation,
    'useComputer')
Łukasz Nowak's avatar
Łukasz Nowak committed
570
  def useComputer(self, computer_id, use_string):
571 572 573 574 575 576 577
    """
    Entry point to reporting usage of a computer.
    """
    computer_consumption_model = \
      pkg_resources.resource_string(
        'slapos.slap',
        'doc/computer_consumption.xsd')
578 579

    if self._validateXML(use_string, computer_consumption_model):
580 581 582 583 584 585 586 587
      computer = self._getComputerDocument(computer_id)
      tree = etree.fromstring(use_string)
      source_reference = \
          tree.find('transaction').find('reference').text or ""
      source_reference = source_reference.encode("UTF-8")
      computer.Computer_reportComputerConsumption(
        source_reference,
        use_string)
Romain Courteaud's avatar
Romain Courteaud committed
588 589 590
      self.REQUEST.response.setStatus(200)
      self.REQUEST.response.setBody("OK")
      return self.REQUEST.response
591
    else:
592 593 594
      self.REQUEST.response.setStatus(400)
      self.REQUEST.response.setBody("")
      return self.REQUEST.response
Łukasz Nowak's avatar
Łukasz Nowak committed
595

Łukasz Nowak's avatar
Łukasz Nowak committed
596 597 598 599 600
  @convertToREST
  def _computerBang(self, computer_id, message):
    """
    Fire up bung on Computer
    """
601 602 603
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
    self._logAccess(user, computer_id, '#error bang')
Łukasz Nowak's avatar
Łukasz Nowak committed
604 605 606 607 608 609 610 611 612 613 614
    return self._getComputerDocument(computer_id).reportComputerBang(
                                     comment=message)

  security.declareProtected(Permissions.AccessContentsInformation,
    'computerBang')
  def computerBang(self, computer_id, message):
    """
    Fire up bang on this Software Instance
    """
    return self._computerBang(computer_id, message)

Łukasz Nowak's avatar
Łukasz Nowak committed
615 616
  security.declareProtected(Permissions.AccessContentsInformation,
    'loadComputerConfigurationFromXML')
Łukasz Nowak's avatar
Łukasz Nowak committed
617 618 619 620 621 622 623
  def loadComputerConfigurationFromXML(self, xml):
    "Load the given xml as configuration for the computer object"
    computer_dict = xml_marshaller.xml_marshaller.loads(xml)
    computer = self._getComputerDocument(computer_dict['reference'])
    computer.Computer_updateFromDict(computer_dict)
    return 'Content properly posted.'

Łukasz Nowak's avatar
Łukasz Nowak committed
624 625 626 627
  security.declareProtected(Permissions.AccessContentsInformation,
    'useComputerPartition')
  def useComputerPartition(self, computer_id, computer_partition_id,
    use_string):
Łukasz Nowak's avatar
Łukasz Nowak committed
628 629 630 631 632 633 634 635 636 637
    """Warning : deprecated method."""
    computer_document = self._getComputerDocument(computer_id)
    computer_partition_document = self._getComputerPartitionDocument(
      computer_document.getReference(), computer_partition_id)
    # easy way to start to store usage messages sent by client in related Web
    # Page text_content...
    self._reportUsage(computer_partition_document, use_string)
    return """Content properly posted.
              WARNING : this method is deprecated. Please use useComputer."""

638
  @convertToREST
639 640 641
  def _generateComputerCertificate(self, computer_id):
    self._getComputerDocument(computer_id).generateCertificate()
    result = {
642 643
     'certificate': self.REQUEST.get('computer_certificate').decode("UTF-8"),
     'key': self.REQUEST.get('computer_key').decode("UTF-8")
644 645
     }
    return xml_marshaller.xml_marshaller.dumps(result)
646 647

  security.declareProtected(Permissions.AccessContentsInformation,
648 649
    'generateComputerCertificate')
  def generateComputerCertificate(self, computer_id):
650
    """Fetches new computer certificate"""
651
    return self._generateComputerCertificate(computer_id)
652 653 654 655 656 657 658 659 660 661 662

  @convertToREST
  def _revokeComputerCertificate(self, computer_id):
    self._getComputerDocument(computer_id).revokeCertificate()

  security.declareProtected(Permissions.AccessContentsInformation,
    'revokeComputerCertificate')
  def revokeComputerCertificate(self, computer_id):
    """Revokes existing computer certificate"""
    return self._revokeComputerCertificate(computer_id)

Łukasz Nowak's avatar
Łukasz Nowak committed
663 664
  security.declareProtected(Permissions.AccessContentsInformation,
    'registerComputerPartition')
Łukasz Nowak's avatar
Łukasz Nowak committed
665 666 667 668 669 670 671 672
  def registerComputerPartition(self, computer_reference,
                                computer_partition_reference):
    """
    Registers connected representation of computer partition and
    returns Computer Partition class object
    """
    # Try to get the computer partition to raise an exception if it doesn't
    # exist
673 674
    portal = self.getPortalObject()
    computer_partition_document = self._getComputerPartitionDocument(
Łukasz Nowak's avatar
Łukasz Nowak committed
675
          computer_reference, computer_partition_reference)
676 677
    slap_partition = SlapComputerPartition(computer_reference.decode("UTF-8"),
        computer_partition_reference.decode("UTF-8"))
678 679 680 681 682 683
    slap_partition._software_release_document = None
    slap_partition._requested_state = 'destroyed'
    slap_partition._need_modification = 0
    software_instance = None

    if computer_partition_document.getSlapState() == 'busy':
684
      software_instance_list = portal.portal_catalog.unrestrictedSearchResults(
685 686 687 688 689 690 691
          portal_type="Software Instance",
          default_aggregate_uid=computer_partition_document.getUid(),
          validation_state="validated",
          limit=2,
          )
      software_instance_count = len(software_instance_list)
      if software_instance_count == 1:
692
        software_instance = _assertACI(software_instance_list[0].getObject())
693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
      elif software_instance_count > 1:
        # XXX do not prevent the system to work if one partition is broken
        raise NotImplementedError, "Too many instances %s linked to %s" % \
          ([x.path for x in software_instance_list],
           computer_partition_document.getRelativeUrl())

    if software_instance is not None:
      # trick client side, that data has been synchronised already for given
      # document
      slap_partition._synced = True
      state = software_instance.getSlapState()
      if state == "stop_requested":
        slap_partition._requested_state = 'stopped'
      if state == "start_requested":
        slap_partition._requested_state = 'started'

      slap_partition._software_release_document = SoftwareRelease(
710 711 712 713
            software_release=software_instance.getUrlString().decode("UTF-8"),
            computer_guid=computer_reference.decode("UTF-8"))
      slap_partition._software_release_document._software_release = \
        slap_partition._software_release_document._software_release.decode("UTF-8")
714 715 716 717 718 719 720 721 722 723

      slap_partition._need_modification = 1

      parameter_dict = self._getSoftwareInstanceAsParameterDict(
                                                       software_instance)
      # software instance has to define an xml parameter
      slap_partition._parameter_dict = self._instanceXmlToDict(
        parameter_dict.pop('xml'))
      slap_partition._connection_dict = self._instanceXmlToDict(
        parameter_dict.pop('connection_xml'))
724
      slap_partition._instance_guid = parameter_dict.pop('instance_guid')
725 726 727 728 729 730 731 732
      for slave_instance_dict in parameter_dict.get("slave_instance_list", []):
        if slave_instance_dict.has_key("connection_xml"):
          slave_instance_dict.update(self._instanceXmlToDict(
            slave_instance_dict.pop("connection_xml")))
        if slave_instance_dict.has_key("xml"):
          slave_instance_dict.update(self._instanceXmlToDict(
            slave_instance_dict.pop("xml")))
      slap_partition._parameter_dict.update(parameter_dict)
733 734
    result = xml_marshaller.xml_marshaller.dumps(slap_partition)

Łukasz Nowak's avatar
Łukasz Nowak committed
735
    # Keep in cache server for 7 days
736 737
    self.REQUEST.response.setStatus(200)
    self.REQUEST.response.setHeader('Cache-Control',
738
                                    'public, max-age=1, stale-if-error=604800')
739 740
    self.REQUEST.response.setHeader('Vary',
                                    'REMOTE_USER')
Łukasz Nowak's avatar
Łukasz Nowak committed
741
    self.REQUEST.response.setHeader('Last-Modified', rfc1123_date(DateTime()))
Łukasz Nowak's avatar
Łukasz Nowak committed
742
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
743 744
    self.REQUEST.response.setBody(result)
    return self.REQUEST.response
Łukasz Nowak's avatar
Łukasz Nowak committed
745 746 747 748 749

  ####################################################
  # Internal methods
  ####################################################

750 751 752 753 754 755 756 757 758 759 760 761 762 763
  def _getMemcachedDict(self):
    return self.getPortalObject().portal_memcached.getMemcachedDict(
      key_prefix='slap_tool',
      plugin_path='portal_memcached/default_memcached_plugin')

  def _logAccess(self, user_reference, context_reference, text):
    memcached_dict = self._getMemcachedDict()
    value = json.dumps({
      'user': '%s' % user_reference,
      'created_at': '%s' % rfc1123_date(DateTime()),
      'text': '%s' % text,
    })
    memcached_dict[context_reference] = value

764 765 766 767 768 769 770 771 772 773 774 775 776 777
  def _validateXML(self, to_be_validated, xsd_model):
    """Will validate the xml file"""
    #We parse the XSD model
    xsd_model = StringIO.StringIO(xsd_model)
    xmlschema_doc = etree.parse(xsd_model)
    xmlschema = etree.XMLSchema(xmlschema_doc)

    string_to_validate = StringIO.StringIO(to_be_validated)

    try:
      document = etree.parse(string_to_validate)
    except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
      LOG('SlapTool::_validateXML', INFO, 
        'Failed to parse this XML reports : %s\n%s' % \
Łukasz Nowak's avatar
Łukasz Nowak committed
778
          (to_be_validated, e))
779 780 781 782 783 784 785
      return False

    if xmlschema.validate(document):
      return True

    return False

Łukasz Nowak's avatar
Łukasz Nowak committed
786 787
  def _instanceXmlToDict(self, xml):
    result_dict = {}
788 789
    try:
      if xml is not None and xml != '':
790
        tree = etree.fromstring(xml)
791 792 793 794 795 796 797 798 799 800 801
        for element in tree.findall('parameter'):
          key = element.get('id')
          value = result_dict.get(key, None)
          if value is not None:
            value = value + ' ' + element.text
          else:
            value = element.text
          result_dict[key] = value
    except (etree.XMLSchemaError, etree.XMLSchemaParseError,
      etree.XMLSchemaValidateError, etree.XMLSyntaxError):
      LOG('SlapTool', INFO, 'Issue during parsing xml:', error=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
802 803 804 805 806 807 808
    return result_dict

  def _getSlapPartitionByPackingList(self, computer_partition_document):
    computer = computer_partition_document
    portal = self.getPortalObject()
    while computer.getPortalType() != 'Computer':
      computer = computer.getParentValue()
809
    computer_id = computer.getReference().decode("UTF-8")
Łukasz Nowak's avatar
Łukasz Nowak committed
810
    slap_partition = SlapComputerPartition(computer_id,
811
      computer_partition_document.getReference().decode("UTF-8"))
Łukasz Nowak's avatar
Łukasz Nowak committed
812 813 814 815 816

    slap_partition._software_release_document = None
    slap_partition._requested_state = 'destroyed'
    slap_partition._need_modification = 0

817 818 819
    software_instance = None

    if computer_partition_document.getSlapState() == 'busy':
820
      software_instance_list = portal.portal_catalog.unrestrictedSearchResults(
821 822 823 824 825 826 827
          portal_type="Software Instance",
          default_aggregate_uid=computer_partition_document.getUid(),
          validation_state="validated",
          limit=2,
          )
      software_instance_count = len(software_instance_list)
      if software_instance_count == 1:
828
        software_instance = _assertACI(software_instance_list[0].getObject())
829 830 831 832 833 834 835 836 837 838 839 840 841 842
      elif software_instance_count > 1:
        # XXX do not prevent the system to work if one partition is broken
        raise NotImplementedError, "Too many instances %s linked to %s" % \
          ([x.path for x in software_instance_list],
           computer_partition_document.getRelativeUrl())

    if software_instance is not None:
      state = software_instance.getSlapState()
      if state == "stop_requested":
        slap_partition._requested_state = 'stopped'
      if state == "start_requested":
        slap_partition._requested_state = 'started'

      slap_partition._software_release_document = SoftwareRelease(
843
            software_release=software_instance.getUrlString().decode("UTF-8"),
Łukasz Nowak's avatar
Łukasz Nowak committed
844
            computer_guid=computer_id)
845 846
      slap_partition._software_release_document._software_release = \
        slap_partition._software_release_document._software_release.decode("UTF-8")
847 848 849 850 851

      slap_partition._need_modification = 1

      parameter_dict = self._getSoftwareInstanceAsParameterDict(
                                                       software_instance)
Łukasz Nowak's avatar
Łukasz Nowak committed
852 853 854 855 856
      # software instance has to define an xml parameter
      slap_partition._parameter_dict = self._instanceXmlToDict(
        parameter_dict.pop('xml'))
      slap_partition._connection_dict = self._instanceXmlToDict(
        parameter_dict.pop('connection_xml'))
857
      slap_partition._instance_guid = parameter_dict.pop('instance_guid')
858 859 860 861 862 863 864
      for slave_instance_dict in parameter_dict.get("slave_instance_list", []):
        if slave_instance_dict.has_key("connection_xml"):
          slave_instance_dict.update(self._instanceXmlToDict(
            slave_instance_dict.pop("connection_xml")))
        if slave_instance_dict.has_key("xml"):
          slave_instance_dict.update(self._instanceXmlToDict(
            slave_instance_dict.pop("xml")))
Łukasz Nowak's avatar
Łukasz Nowak committed
865 866 867 868
      slap_partition._parameter_dict.update(parameter_dict)

    return slap_partition

869
  @convertToREST
870
  def _supplySupply(self, url, computer_id, state):
871 872 873 874
    """
    Request Software Release installation
    """
    computer_document = self._getComputerDocument(computer_id)
875
    computer_document.requestSoftwareRelease(software_release_url=url, state=state)
876

Łukasz Nowak's avatar
Łukasz Nowak committed
877 878 879
  @convertToREST
  def _buildingSoftwareRelease(self, url, computer_id):
    """
880
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
881
    """
882 883 884
    user = self.getPortalObject().portal_membership.\
        getAuthenticatedMember().getUserName()
    self._logAccess(user, user, 'building software release %s' % url)
Łukasz Nowak's avatar
Łukasz Nowak committed
885 886 887 888

  @convertToREST
  def _availableSoftwareRelease(self, url, computer_id):
    """
889
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
890
    """
891 892 893 894
    user = self.getPortalObject().portal_membership.\
        getAuthenticatedMember().getUserName()
    self._logAccess(user, user, '#access software release %s available' % \
        url)
Łukasz Nowak's avatar
Łukasz Nowak committed
895

896 897 898
  @convertToREST
  def _destroyedSoftwareRelease(self, url, computer_id):
    """
899
    Reports that Software Release is destroyed
900 901
    """
    computer_document = self._getComputerDocument(computer_id)
902 903
    software_installation = self._getSoftwareInstallationForComputer(url,
      computer_document)
904 905
    if software_installation.getSlapState() != 'destroy_requested':
      raise NotFound
906
    if self.getPortalObject().portal_workflow.isTransitionPossible(software_installation,
907
        'invalidate'):
908 909
      software_installation.invalidate(
        comment="Software Release destroyed report.")
910

Łukasz Nowak's avatar
Łukasz Nowak committed
911 912 913
  @convertToREST
  def _buildingComputerPartition(self, computer_id, computer_partition_id):
    """
914
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
915
    """
916 917 918 919 920 921 922
    instance = self._getSoftwareInstanceForComputerPartition(
        computer_id,
        computer_partition_id)
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
    self._logAccess(user, instance.getReference(), 
                    'building the instance')
Łukasz Nowak's avatar
Łukasz Nowak committed
923 924 925 926

  @convertToREST
  def _availableComputerPartition(self, computer_id, computer_partition_id):
    """
927
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
928
    """
929 930 931 932 933 934 935
    instance = self._getSoftwareInstanceForComputerPartition(
        computer_id,
        computer_partition_id)
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
    self._logAccess(user, instance.getReference(), 
                    '#access instance available')
Łukasz Nowak's avatar
Łukasz Nowak committed
936 937 938 939 940 941 942

  @convertToREST
  def _softwareInstanceError(self, computer_id,
                            computer_partition_id, error_log):
    """
    Add an error for the software Instance Workflow
    """
943
    instance = self._getSoftwareInstanceForComputerPartition(
Łukasz Nowak's avatar
Łukasz Nowak committed
944
        computer_id,
945 946 947 948 949
        computer_partition_id)
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
    self._logAccess(user, instance.getReference(), 
                    '#error while instanciating')
950
    #return instance.reportComputerPartitionError()
Łukasz Nowak's avatar
Łukasz Nowak committed
951

952 953 954 955 956 957
  @convertToREST
  def _softwareInstanceRename(self, new_name, computer_id,
                              computer_partition_id, slave_reference):
    software_instance = self._getSoftwareInstanceForComputerPartition(
      computer_id, computer_partition_id,
      slave_reference)
958 959 960 961 962
    hosting = software_instance.getSpecialise()
    for name in [software_instance.getTitle(), new_name]:
      # reset request cache
      key = '_'.join([hosting, name])
      self._storeLastData(key, {})
963 964 965
    return software_instance.rename(new_name=new_name,
      comment="Rename %s into %s" % (software_instance.title, new_name))

Łukasz Nowak's avatar
Łukasz Nowak committed
966 967 968 969 970 971 972
  @convertToREST
  def _softwareInstanceBang(self, computer_id,
                            computer_partition_id, message):
    """
    Fire up bang on Software Instance
    Add an error for the software Instance Workflow
    """
973
    software_instance = self._getSoftwareInstanceForComputerPartition(
Łukasz Nowak's avatar
Łukasz Nowak committed
974
        computer_id,
975 976 977 978 979
        computer_partition_id)
    user = self.getPortalObject().portal_membership.\
        getAuthenticatedMember().getUserName()
    self._logAccess(user, software_instance.getReference(),
                    '#error bang called')
980 981 982 983 984 985
    timestamp = str(int(software_instance.getModificationDate()))
    key = "%s_bangstamp" % software_instance.getReference()

    transition = self.getPortalObject().portal_workflow.getInfoFor(
      software_instance, 'action', wf_id='instance_slap_interface_workflow')

986
    if (self._getLastData(key) != timestamp):
987
      software_instance.bang(bang_tree=True, comment=message)
988
      self._storeLastData(key, str(int(software_instance.getModificationDate())))
989
    return "OK"
990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010

  def _getAccessStatus(self, context_reference):
    memcached_dict = self._getMemcachedDict()
    try:
      if context_reference is None:
        raise KeyError
      else:
        d = memcached_dict[context_reference]
    except KeyError:
      if context_reference is None:
        d = {
          "user": "SlapOS Master",
          'created_at': '%s' % rfc1123_date(DateTime()),
          "text": "#error no data found"
        }
      else:
        d = {
          "user": "SlapOS Master",
          'created_at': '%s' % rfc1123_date(DateTime()),
          "text": "#error no data found for %s" % context_reference
        }
1011 1012 1013
      # Prepare for xml marshalling
      d["user"] = d["user"].decode("UTF-8")
      d["text"] = d["text"].decode("UTF-8")
1014 1015
    else:
      d = json.loads(d)
1016

1017 1018 1019 1020 1021 1022 1023
    # Keep in cache server for 7 days
    self.REQUEST.response.setStatus(200)
    self.REQUEST.response.setHeader('Cache-Control',
                                    'public, max-age=60, stale-if-error=604800')
    self.REQUEST.response.setHeader('Vary',
                                    'REMOTE_USER')
    self.REQUEST.response.setHeader('Last-Modified', rfc1123_date(DateTime()))
Łukasz Nowak's avatar
Łukasz Nowak committed
1024
    self.REQUEST.response.setHeader('Content-Type', 'text/xml; charset=utf-8')
1025 1026
    self.REQUEST.response.setBody(xml_marshaller.xml_marshaller.dumps(d))
    return self.REQUEST.response
Łukasz Nowak's avatar
Łukasz Nowak committed
1027

Łukasz Nowak's avatar
Łukasz Nowak committed
1028 1029 1030
  @convertToREST
  def _startedComputerPartition(self, computer_id, computer_partition_id):
    """
1031
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
1032
    """
1033 1034 1035 1036 1037
    instance = self._getSoftwareInstanceForComputerPartition(
        computer_id,
        computer_partition_id)
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
1038 1039
    self._logAccess(user, instance.getReference(),
                    '#access Instance correctly started')
Łukasz Nowak's avatar
Łukasz Nowak committed
1040 1041 1042 1043

  @convertToREST
  def _stoppedComputerPartition(self, computer_id, computer_partition_id):
    """
1044
    Log the computer status
Łukasz Nowak's avatar
Łukasz Nowak committed
1045
    """
1046 1047 1048 1049 1050
    instance = self._getSoftwareInstanceForComputerPartition(
        computer_id,
        computer_partition_id)
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
1051 1052
    self._logAccess(user, instance.getReference(),
                    '#access Instance correctly stopped')
Łukasz Nowak's avatar
Łukasz Nowak committed
1053 1054 1055 1056 1057 1058

  @convertToREST
  def _destroyedComputerPartition(self, computer_id, computer_partition_id):
    """
    Reports that Computer Partition is destroyed
    """
1059
    instance = self._getSoftwareInstanceForComputerPartition(
Łukasz Nowak's avatar
Łukasz Nowak committed
1060 1061
        computer_id,
        computer_partition_id)
1062 1063 1064 1065 1066 1067 1068 1069 1070
    if instance.getSlapState() == 'destroy_requested':
      # remove certificate from SI
      if instance.getSslKey() is not None or instance.getSslCertificate() is not None:
        instance.edit(
          ssl_key=None,
          ssl_certificate=None,
        )
      if instance.getValidationState() == 'validated':
        instance.invalidate()
Łukasz Nowak's avatar
Łukasz Nowak committed
1071

Romain Courteaud's avatar
Romain Courteaud committed
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083
      # XXX Integrate with REST API
      # Code duplication will be needed until SlapTool is removed
      # revoke certificate
      portal = self.getPortalObject()
      try:
        portal.portal_certificate_authority\
          .revokeCertificate(instance.getDestinationReference())
      except ValueError:
        # Ignore already revoked certificates, as OpenSSL backend is
        # non transactional, so it is ok to allow multiple tries to destruction
        # even if certificate was already revoked
        pass
1084 1085


Łukasz Nowak's avatar
Łukasz Nowak committed
1086 1087 1088
  @convertToREST
  def _setComputerPartitionConnectionXml(self, computer_id,
                                         computer_partition_id,
1089 1090
                                         connection_xml,
                                         slave_reference=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
1091 1092 1093 1094 1095
    """
    Sets Computer Partition connection Xml
    """
    software_instance = self._getSoftwareInstanceForComputerPartition(
        computer_id,
1096 1097
        computer_partition_id,
        slave_reference)
Łukasz Nowak's avatar
Łukasz Nowak committed
1098 1099 1100 1101 1102 1103 1104 1105
    partition_parameter_kw = xml_marshaller.xml_marshaller.loads(
                                              connection_xml)
    instance = etree.Element('instance')
    for parameter_id, parameter_value in partition_parameter_kw.iteritems():
      etree.SubElement(instance, "parameter",
                       attrib={'id':parameter_id}).text = parameter_value
    connection_xml = etree.tostring(instance, pretty_print=True,
                                  xml_declaration=True, encoding='utf-8')
1106 1107
    reference = software_instance.getReference()
    if self._getLastData(reference) != connection_xml:
1108
      software_instance.updateConnection(
1109 1110 1111
        connection_xml=connection_xml,
      )
      self._storeLastData(reference, connection_xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
1112 1113 1114

  @convertToREST
  def _requestComputerPartition(self, computer_id, computer_partition_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
1115
        software_release, software_type, partition_reference,
1116
        shared_xml, partition_parameter_xml, filter_xml, state):
Łukasz Nowak's avatar
Łukasz Nowak committed
1117 1118 1119 1120 1121 1122 1123
    """
    Asynchronously requests creation of computer partition for assigned
    parameters

    Returns XML representation of partition with HTTP code 200 OK

    In case if this request is still being processed data contain
Łukasz Nowak's avatar
Łukasz Nowak committed
1124 1125
    "Computer Partition is being processed" and HTTP code is 408 Request
    Timeout
Łukasz Nowak's avatar
Łukasz Nowak committed
1126 1127 1128

    In any other case returns not important data and HTTP code is 403 Forbidden
    """
1129 1130
    if state:
      state = xml_marshaller.xml_marshaller.loads(state)
Łukasz Nowak's avatar
Łukasz Nowak committed
1131
    if state is None:
Łukasz Nowak's avatar
Łukasz Nowak committed
1132
      state = 'started'
1133 1134
    if shared_xml is not _MARKER:
      shared = xml_marshaller.xml_marshaller.loads(shared_xml)
Łukasz Nowak's avatar
Łukasz Nowak committed
1135
    else:
1136
      shared = False
Łukasz Nowak's avatar
Łukasz Nowak committed
1137 1138 1139 1140 1141 1142 1143 1144 1145
    if partition_parameter_xml:
      partition_parameter_kw = xml_marshaller.xml_marshaller.loads(
                                              partition_parameter_xml)
    else:
      partition_parameter_kw = dict()
    if filter_xml:
      filter_kw = xml_marshaller.xml_marshaller.loads(filter_xml)
    else:
      filter_kw = dict()
Łukasz Nowak's avatar
Łukasz Nowak committed
1146

Łukasz Nowak's avatar
Łukasz Nowak committed
1147 1148 1149 1150 1151 1152
    instance = etree.Element('instance')
    for parameter_id, parameter_value in partition_parameter_kw.iteritems():
      # cast everything to string
      parameter_value = str(parameter_value)
      etree.SubElement(instance, "parameter",
                       attrib={'id':parameter_id}).text = parameter_value
Łukasz Nowak's avatar
Łukasz Nowak committed
1153
    instance_xml = etree.tostring(instance, pretty_print=True,
Łukasz Nowak's avatar
Łukasz Nowak committed
1154 1155
                                  xml_declaration=True, encoding='utf-8')

1156 1157 1158 1159 1160 1161 1162 1163 1164
    instance = etree.Element('instance')
    for parameter_id, parameter_value in filter_kw.iteritems():
      # cast everything to string
      parameter_value = str(parameter_value)
      etree.SubElement(instance, "parameter",
                       attrib={'id':parameter_id}).text = parameter_value
    sla_xml = etree.tostring(instance, pretty_print=True,
                                  xml_declaration=True, encoding='utf-8')

1165
    portal = self.getPortalObject()
1166 1167 1168 1169
    if computer_id and computer_partition_id:
      # requested by Software Instance, there is already top part of tree
      software_instance_document = self.\
        _getSoftwareInstanceForComputerPartition(computer_id,
Łukasz Nowak's avatar
Łukasz Nowak committed
1170
        computer_partition_id)
1171
      kw = dict(software_release=software_release,
1172
              software_type=software_type,
Romain Courteaud's avatar
Romain Courteaud committed
1173
              software_title=partition_reference,
1174
              instance_xml=instance_xml,
1175
              shared=shared,
Łukasz Nowak's avatar
Łukasz Nowak committed
1176 1177
              sla_xml=sla_xml,
              state=state)
1178 1179 1180 1181 1182
      key = '_'.join([software_instance_document.getSpecialise(), partition_reference])
      value = dict(
        hash='_'.join([software_instance_document.getRelativeUrl(), str(kw)]),
        )
      last_data = self._getLastData(key)
1183 1184 1185
      requested_software_instance = None
      if last_data is not None and type(last_data) == type({}):
        requested_software_instance = portal.restrictedTraverse(
1186 1187 1188 1189 1190 1191 1192 1193 1194 1195
          last_data.get('request_instance'), None)
      if last_data is None or type(last_data) != type(value) or \
          last_data.get('hash') != value['hash'] or \
          requested_software_instance is None:
        software_instance_document.requestInstance(**kw)
        requested_software_instance = self.REQUEST.get('request_instance')
        if requested_software_instance is not None:
          value['request_instance'] = requested_software_instance\
            .getRelativeUrl()
          self._storeLastData(key, value)
1196 1197
    else:
      # requested as root, so done by human
1198
      person = portal.ERP5Site_getAuthenticatedMemberPersonValue()
1199
      kw = dict(software_release=software_release,
1200 1201
              software_type=software_type,
              software_title=partition_reference,
1202
              shared=shared,
1203
              instance_xml=instance_xml,
Łukasz Nowak's avatar
Łukasz Nowak committed
1204 1205
              sla_xml=sla_xml,
              state=state)
1206 1207 1208 1209 1210
      key = '_'.join([person.getRelativeUrl(), partition_reference])
      value = dict(
        hash=str(kw)
      )
      last_data = self._getLastData(key)
1211 1212
      if last_data is not None and type(last_data) == type({}):
        requested_software_instance = portal.restrictedTraverse(
1213 1214 1215 1216 1217 1218 1219 1220 1221 1222
          last_data.get('request_instance'), None)
      if last_data is None or type(last_data) != type(value) or \
        last_data.get('hash') != value['hash'] or \
        requested_software_instance is None:
        person.requestSoftwareInstance(**kw)
        requested_software_instance = self.REQUEST.get('request_instance')
        if requested_software_instance is not None:
          value['request_instance'] = requested_software_instance\
            .getRelativeUrl()
          self._storeLastData(key, value)
1223

1224
    if requested_software_instance is None:
Łukasz Nowak's avatar
Łukasz Nowak committed
1225 1226
      raise SoftwareInstanceNotReady
    else:
1227
      if not requested_software_instance.getAggregate(portal_type="Computer Partition"):
1228
        raise SoftwareInstanceNotReady
1229
      else:
1230
        parameter_dict = self._getSoftwareInstanceAsParameterDict(requested_software_instance)
1231 1232 1233 1234 1235 1236

        # software instance has to define an xml parameter
        xml = self._instanceXmlToDict(
          parameter_dict.pop('xml'))
        connection_xml = self._instanceXmlToDict(
          parameter_dict.pop('connection_xml'))
1237
        instance_guid = parameter_dict.pop('instance_guid')
1238

1239
        software_instance = SoftwareInstance(**parameter_dict)
1240 1241
        software_instance._parameter_dict = xml
        software_instance._connection_dict = connection_xml
1242 1243
        software_instance._requested_state = state
        software_instance._instance_guid = instance_guid
1244
        return xml_marshaller.xml_marshaller.dumps(software_instance)
Łukasz Nowak's avatar
Łukasz Nowak committed
1245 1246 1247 1248 1249 1250 1251 1252

  ####################################################
  # Internals methods
  ####################################################

  def _getDocument(self, **kwargs):
    # No need to get all results if an error is raised when at least 2 objects
    # are found
1253
    l = self.getPortalObject().portal_catalog.unrestrictedSearchResults(limit=2, **kwargs)
Łukasz Nowak's avatar
Łukasz Nowak committed
1254 1255 1256
    if len(l) != 1:
      raise NotFound, "No document found with parameters: %s" % kwargs
    else:
1257
      return _assertACI(l[0].getObject())
Łukasz Nowak's avatar
Łukasz Nowak committed
1258

Łukasz Nowak's avatar
Łukasz Nowak committed
1259
  def _getNonCachedComputerDocument(self, computer_reference):
Łukasz Nowak's avatar
Łukasz Nowak committed
1260 1261 1262 1263
    return self._getDocument(
        portal_type='Computer',
        # XXX Hardcoded validation state
        validation_state="validated",
Łukasz Nowak's avatar
Łukasz Nowak committed
1264 1265 1266 1267 1268 1269 1270 1271 1272 1273
        reference=computer_reference).getRelativeUrl()

  def _getComputerDocument(self, computer_reference):
    """
    Get the validated computer with this reference.
    """
    result = CachingMethod(self._getNonCachedComputerDocument,
        id='_getComputerDocument',
        cache_factory='slap_cache_factory')(computer_reference)
    return self.getPortalObject().restrictedTraverse(result)
Łukasz Nowak's avatar
Łukasz Nowak committed
1274

1275 1276
  @UnrestrictedMethod
  def _getComputerUidByReference(self, computer_reference):
1277 1278 1279
    return self.getPortalObject().portal_catalog.unrestrictedSearchResults(
      portal_type='Computer', reference=computer_reference,
      validation_state="validated")[0].UID
1280

Łukasz Nowak's avatar
Łukasz Nowak committed
1281 1282 1283 1284 1285 1286
  def _getComputerPartitionDocument(self, computer_reference,
                                    computer_partition_reference):
    """
    Get the computer partition defined in an available computer
    """
    # Related key might be nice
1287
    return self._getDocument(portal_type='Computer Partition',
Łukasz Nowak's avatar
Łukasz Nowak committed
1288
                             reference=computer_partition_reference,
1289 1290
                             parent_uid=self._getComputerUidByReference(
                                computer_reference))
Łukasz Nowak's avatar
Łukasz Nowak committed
1291

1292
  def _getSoftwareInstallationForComputer(self, url, computer_document):
1293
    software_installation_list = self.getPortalObject().portal_catalog.unrestrictedSearchResults(
1294 1295 1296 1297 1298 1299
      portal_type='Software Installation',
      default_aggregate_uid=computer_document.getUid(),
      validation_state='validated',
      limit=2,
      url_string={'query': url, 'key': 'ExactMatch'},
    )
1300

1301 1302
    l = len(software_installation_list)
    if l == 1:
1303
      return _assertACI(software_installation_list[0].getObject())
1304 1305 1306 1307 1308 1309 1310 1311 1312
    elif l == 0:
      raise NotFound('No software release %r found on computer %r' % (url,
        computer_document.getReference()))
    else:
      raise ValueError('Wrong list of software releases on %r: %s' % (
        computer_document.getReference(), ', '.join([q.getRelativeUrl() for q \
          in software_installation_list])
      ))

Łukasz Nowak's avatar
Łukasz Nowak committed
1313
  def _getSoftwareInstanceForComputerPartition(self, computer_id,
1314
      computer_partition_id, slave_reference=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
1315 1316 1317 1318 1319 1320 1321 1322 1323
    computer_partition_document = self._getComputerPartitionDocument(
      computer_id, computer_partition_id)
    if computer_partition_document.getSlapState() != 'busy':
      LOG('SlapTool::_getSoftwareInstanceForComputerPartition', INFO,
          'Computer partition %s shall be busy, is free' %
          computer_partition_document.getRelativeUrl())
      raise NotFound, "No software instance found for: %s - %s" % (computer_id,
          computer_partition_id)
    else:
1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
      query_kw = {
        'validation_state': 'validated',
        'portal_type': 'Slave Instance',
        'default_aggregate_uid': computer_partition_document.getUid(),
      }
      if slave_reference is None:
        query_kw['portal_type'] = "Software Instance"
      else:
        query_kw['reference'] = slave_reference

1334
      software_instance = _assertACI(self.getPortalObject().portal_catalog.unrestrictedGetResultValue(**query_kw))
1335
      if software_instance is None:
Łukasz Nowak's avatar
Łukasz Nowak committed
1336 1337
        raise NotFound, "No software instance found for: %s - %s" % (
          computer_id, computer_partition_id)
Łukasz Nowak's avatar
Łukasz Nowak committed
1338
      else:
1339
        return software_instance
Łukasz Nowak's avatar
Łukasz Nowak committed
1340

1341
  @UnrestrictedMethod
1342 1343 1344
  def _getSoftwareInstanceAsParameterDict(self, software_instance):
    portal = software_instance.getPortalObject()
    computer_partition = software_instance.getAggregateValue(portal_type="Computer Partition")
1345 1346
    timestamp = int(computer_partition.getModificationDate())

Romain Courteaud's avatar
Romain Courteaud committed
1347
    newtimestamp = int(software_instance.getBangTimestamp(int(software_instance.getModificationDate())))
1348 1349
    if (newtimestamp > timestamp):
      timestamp = newtimestamp
1350 1351 1352

    ip_list = []
    for internet_protocol_address in computer_partition.contentValues(portal_type='Internet Protocol Address'):
1353 1354 1355
      ip_list.append((
        internet_protocol_address.getNetworkInterface('').decode("UTF-8"),
        internet_protocol_address.getIpAddress().decode("UTF-8")))
1356 1357 1358 1359

    slave_instance_list = []
    if (software_instance.getPortalType() == "Software Instance"):
      append = slave_instance_list.append
1360
      slave_instance_sql_list = portal.portal_catalog.unrestrictedSearchResults(
1361 1362 1363 1364 1365
        default_aggregate_uid=computer_partition.getUid(),
        portal_type='Slave Instance',
        validation_state="validated",
      )
      for slave_instance in slave_instance_sql_list:
1366
        slave_instance = _assertACI(slave_instance.getObject())
1367 1368 1369
        # XXX Use catalog to filter more efficiently
        if slave_instance.getSlapState() == "start_requested":
          append({
1370 1371 1372
            'slave_title': slave_instance.getTitle().decode("UTF-8"),
            'slap_software_type': \
                slave_instance.getSourceReference().decode("UTF-8"),
1373
            'slave_reference': slave_instance.getReference().decode("UTF-8"),
1374 1375 1376
            'xml': slave_instance.getTextContent(),
            'connection_xml': slave_instance.getConnectionXml(),
          })
Romain Courteaud's avatar
Romain Courteaud committed
1377 1378
          newtimestamp = int(slave_instance.getBangTimestamp(int(software_instance.getModificationDate())))                  
          if (newtimestamp > timestamp):                                            
1379
            timestamp = newtimestamp
1380
    return {
1381
      'instance_guid': software_instance.getReference().decode("UTF-8"),
1382 1383
      'xml': software_instance.getTextContent(),
      'connection_xml': software_instance.getConnectionXml(),
1384 1385 1386 1387 1388 1389 1390 1391
      'slap_computer_id': \
        computer_partition.getParentValue().getReference().decode("UTF-8"),
      'slap_computer_partition_id': \
        computer_partition.getReference().decode("UTF-8"),
      'slap_software_type': \
        software_instance.getSourceReference().decode("UTF-8"),
      'slap_software_release_url': \
        software_instance.getUrlString().decode("UTF-8"),
1392 1393
      'slave_instance_list': slave_instance_list,
      'ip_list': ip_list,
Romain Courteaud's avatar
Romain Courteaud committed
1394
      'timestamp': "%i" % timestamp,
1395
    }
Łukasz Nowak's avatar
Łukasz Nowak committed
1396

1397
  @UnrestrictedMethod
Łukasz Nowak's avatar
Łukasz Nowak committed
1398
  def _getSoftwareReleaseValueListForComputer(self, computer_reference):
Łukasz Nowak's avatar
Łukasz Nowak committed
1399
    """Returns list of Software Releases documentsfor computer"""
1400
    computer_document = self._getComputerDocument(computer_reference)
Łukasz Nowak's avatar
Łukasz Nowak committed
1401 1402
    portal = self.getPortalObject()
    software_release_list = []
1403
    for software_installation in portal.portal_catalog.unrestrictedSearchResults(
1404 1405 1406 1407
      portal_type='Software Installation',
      default_aggregate_uid=computer_document.getUid(),
      validation_state='validated',
      ):
1408
      software_installation = _assertACI(software_installation.getObject())
Łukasz Nowak's avatar
Łukasz Nowak committed
1409
      software_release_response = SoftwareRelease(
1410 1411 1412 1413
          software_release=software_installation.getUrlString().decode('UTF-8'),
          computer_guid=computer_reference.decode('UTF-8'))
      software_release_response._software_release = \
        software_release_response._software_release.decode("UTF-8")
Łukasz Nowak's avatar
Typo.  
Łukasz Nowak committed
1414
      if software_installation.getSlapState() == 'destroy_requested':
1415 1416 1417
        software_release_response._requested_state = 'destroyed'
      else:
        software_release_response._requested_state = 'available'
Łukasz Nowak's avatar
Łukasz Nowak committed
1418
      software_release_list.append(software_release_response)
Łukasz Nowak's avatar
Łukasz Nowak committed
1419 1420
    return software_release_list

1421 1422 1423 1424 1425 1426 1427 1428 1429 1430
  @convertToREST
  def _softwareReleaseError(self, url, computer_id, error_log):
    """
    Log the computer status
    """
    user = self.getPortalObject().portal_membership.getAuthenticatedMember()\
                                                   .getUserName()
    self._logAccess(
        user, computer_id, '#error while installing %s' % url)

Łukasz Nowak's avatar
Łukasz Nowak committed
1431
InitializeClass(SlapTool)