slapgrid.py 51.6 KB
Newer Older
1
# -*- coding: utf-8 -*-
2
# vim: set et sts=2:
Łukasz Nowak's avatar
Łukasz Nowak committed
3 4
##############################################################################
#
5 6
# Copyright (c) 2010, 2011, 2012 Vifib SARL and Contributors.
# All Rights Reserved.
Łukasz Nowak's avatar
Łukasz Nowak committed
7 8 9 10 11
#
# 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
12
# guarantees and support are strongly advised to contract a Free Software
Łukasz Nowak's avatar
Łukasz Nowak committed
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
# 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.
#
##############################################################################
30

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
31 32
import argparse
import ConfigParser
33
from exception import BuildoutFailedError
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
34 35
from hashlib import md5
from lxml import etree
Łukasz Nowak's avatar
Łukasz Nowak committed
36 37 38
import logging
import os
import pkg_resources
39
import random
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
40 41
import socket
import StringIO
42
import subprocess
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
43 44 45 46
import sys
import tempfile
import time
import traceback
Łukasz Nowak's avatar
Łukasz Nowak committed
47 48 49 50 51
import warnings
if sys.version_info < (2, 6):
  warnings.warn('Used python version (%s) is old and have problems with'
      ' IPv6 connections' % sys.version.split('\n')[0])

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
52 53
from slapos.slap.slap import NotFoundError
from slapos.slap.slap import ServerError
Łukasz Nowak's avatar
Łukasz Nowak committed
54 55
from SlapObject import Software, Partition, WrongPermissionError, \
    PathDoesNotExistError
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
56
from svcbackend import launchSupervisord
Łukasz Nowak's avatar
Łukasz Nowak committed
57
from utils import createPrivateDirectory
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
58 59
from utils import dropPrivileges
from utils import getSoftwareUrlHash
Łukasz Nowak's avatar
Łukasz Nowak committed
60 61 62
from utils import setRunning
from utils import setFinished
from utils import SlapPopen
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
63 64
from utils import updateFile
from slapos import slap
Łukasz Nowak's avatar
Łukasz Nowak committed
65 66 67 68 69 70 71 72

MANDATORY_PARAMETER_LIST = [
    'computer_id',
    'instance_root',
    'master_url',
    'software_root',
]

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
73
# XXX: should be moved to SLAP library
74
COMPUTER_PARTITION_DESTROYED_STATE = 'destroyed'
75 76
COMPUTER_PARTITION_STARTED_STATE = 'started'
COMPUTER_PARTITION_STOPPED_STATE = 'stopped'
Łukasz Nowak's avatar
Łukasz Nowak committed
77

78 79 80 81 82
# Global variables about return state of slapgrid
SLAPGRID_SUCCESS = 0
SLAPGRID_FAIL = 1
SLAPGRID_PROMISE_FAIL = 2

83
# XXX hardcoded watchdog_path
84
WATCHDOG_PATH = '/opt/slapos/bin/slapos-watchdog'
85

86 87
COMPUTER_PARTITION_TIMESTAMP_FILENAME = '.timestamp'

88 89 90 91 92

class _formatXMLError(Exception):
  pass


Łukasz Nowak's avatar
Łukasz Nowak committed
93 94 95 96 97 98 99 100
def parseArgumentTupleAndReturnSlapgridObject(*argument_tuple):
  """Parses arguments either from command line, from method parameters or from
     config file. Then returns a new instance of slapgrid.Slapgrid with those
     parameters. Also returns the options dict and unused variable list, and
     configures logger.
  """
  parser = argparse.ArgumentParser()
  parser.add_argument("--instance-root",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
101
      help="The instance root directory location.")
Łukasz Nowak's avatar
Łukasz Nowak committed
102
  parser.add_argument("--software-root",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
103
      help="The software_root directory location.")
Łukasz Nowak's avatar
Łukasz Nowak committed
104
  parser.add_argument("--master-url",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
105
      help="The master server URL. Mandatory.")
Łukasz Nowak's avatar
Łukasz Nowak committed
106
  parser.add_argument("--computer-id",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
107
      help="The computer id defined in the server.")
Łukasz Nowak's avatar
Łukasz Nowak committed
108
  parser.add_argument("--supervisord-socket",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
109
      help="The socket supervisor will use.")
Łukasz Nowak's avatar
Łukasz Nowak committed
110
  parser.add_argument("--supervisord-configuration-path",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
111 112 113
      help="The location where supervisord configuration will be stored.")
  parser.add_argument("--buildout", default=None,
      help="Location of buildout binary.")
Łukasz Nowak's avatar
Łukasz Nowak committed
114
  parser.add_argument("--pidfile",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
115
      help="The location where pidfile will be created.")
Łukasz Nowak's avatar
Łukasz Nowak committed
116
  parser.add_argument("--logfile",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
117
      help="The location where slapgrid logfile will be created.")
Łukasz Nowak's avatar
Łukasz Nowak committed
118 119 120
  parser.add_argument("--key_file", help="SSL Authorisation key file.")
  parser.add_argument("--cert_file",
      help="SSL Authorisation certificate file.")
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
121 122 123 124
  parser.add_argument("--signature_private_key_file",
      help="Signature private key file.")
  parser.add_argument("--master_ca_file",
      help="Root certificate of SlapOS master key.")
Łukasz Nowak's avatar
Łukasz Nowak committed
125 126 127
  parser.add_argument("--certificate_repository_path",
      help="Path to directory where downloaded certificates would be stored.")
  parser.add_argument("-c", "--console", action="store_true", default=False,
128
      help="Deprecated, doesn't do anything.")
Łukasz Nowak's avatar
Łukasz Nowak committed
129 130
  parser.add_argument("-v", "--verbose", action="store_true", default=False,
      help="Be verbose.")
131 132
  parser.add_argument("--maximum-periodicity", type=int, default=None,
      help="Periodicity at which buildout should be run in instance.")
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
133 134
  parser.add_argument("--promise-timeout", type=int, default=3,
      help="Promise timeout in seconds.")
Yingjie Xu's avatar
Yingjie Xu committed
135
  parser.add_argument("--now", action="store_true", default=False,
136
      help="Launch slapgrid without delay. Default behavior.")
137
  parser.add_argument("--all", action="store_true", default=False,
Jérome Perrin's avatar
Jérome Perrin committed
138
      help="Launch slapgrid to process all Softare Releases "
139
           "and/or Computer Partitions.")
140
  parser.add_argument("--only-sr",
Jérome Perrin's avatar
Jérome Perrin committed
141 142
      help="Force the update of a single software release (use url hash), "
           "even if is already installed. This option will make all others "
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
143
           "sofware releases be ignored.")
144
  parser.add_argument("--only-cp",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
145 146 147
      help="Update a single or a list of computer partitions "
           "(ie.:slappartX, slappartY),"
           "this option will make all others computer partitions be ignored.")
Łukasz Nowak's avatar
Łukasz Nowak committed
148

149 150 151 152 153 154 155 156 157 158 159 160 161
  parser.add_argument("configuration_file", nargs=1, type=argparse.FileType(),
      help="SlapOS configuration file.")

  # Deprecated options
  parser.add_argument("--develop", action="store_true", default=False,
      help="Deprecated, same as --all.")
  parser.add_argument("--only_sr",
      help="Deprecated, same as --only-sr.")
  parser.add_argument("--only_cp",
      help="Deprecated, same as --only-cp.")
  parser.add_argument("--maximal_delay",
      help="Deprecated. Will only work from configuration file in the future.")

162

Łukasz Nowak's avatar
Łukasz Nowak committed
163
  # Parses arguments
Marco Mariani's avatar
Marco Mariani committed
164
  if not argument_tuple:
Łukasz Nowak's avatar
Łukasz Nowak committed
165 166 167 168 169 170 171 172 173 174 175 176 177
    # No arguments given to entry point : we parse sys.argv.
    argument_option_instance = parser.parse_args()
  else:
    argument_option_instance = \
      parser.parse_args(list(argument_tuple))
  # Parses arguments from config file, if needed, then merge previous arguments
  option_dict = {}
  configuration_file = argument_option_instance.configuration_file[0]
  # Loads config (if config specified)
  slapgrid_configuration = ConfigParser.SafeConfigParser()
  slapgrid_configuration.readfp(configuration_file)
  # Merges the two dictionnaries
  option_dict = dict(slapgrid_configuration.items("slapos"))
178 179
  if slapgrid_configuration.has_section("networkcache"):
    option_dict.update(dict(slapgrid_configuration.items("networkcache")))
Łukasz Nowak's avatar
Łukasz Nowak committed
180 181 182 183 184 185 186 187 188
  for argument_key, argument_value in vars(argument_option_instance
      ).iteritems():
    if argument_value is not None:
      option_dict.update({argument_key: argument_value})
  # Configures logger.
  if option_dict['verbose']:
    level = logging.DEBUG
  else:
    level = logging.INFO
189 190 191
  logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
                      level=level,
                      datefmt='%Y-%m-%dT%H:%M:%S')
Łukasz Nowak's avatar
Łukasz Nowak committed
192
  if option_dict.get('logfile'):
193 194 195 196 197 198
    console = logging.FileHandler(option_dict['logfile'])
    console.setLevel(level)
    console.setFormatter(logging.Formatter(
        '%(asctime)s %(name)-18s: %(levelname)-8s %(message)s'))
    logging.getLogger('').addHandler(console)

Łukasz Nowak's avatar
Łukasz Nowak committed
199 200 201 202 203
  missing_mandatory_parameter_list = []
  for mandatory_parameter in MANDATORY_PARAMETER_LIST:
    if not mandatory_parameter in option_dict:
      missing_mandatory_parameter_list.append(mandatory_parameter)

204
  if option_dict.get('all') is True:
205
    option_dict['develop'] = True
206

207 208 209
  if option_dict.get('maximum_periodicity') is not None:
    option_dict['force_periodicity'] = True

Łukasz Nowak's avatar
Łukasz Nowak committed
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
  repository_required = False
  if 'key_file' in option_dict:
    repository_required = True
    if not 'cert_file' in option_dict:
      missing_mandatory_parameter_list.append('cert_file')
  if 'cert_file' in option_dict:
    repository_required = True
    if not 'key_file' in option_dict:
      missing_mandatory_parameter_list.append('key_file')
  if repository_required:
    if 'certificate_repository_path' not in option_dict:
      missing_mandatory_parameter_list.append('certificate_repository_path')

  if len(missing_mandatory_parameter_list) > 0:
    parser.error('Missing mandatory parameters:\n%s' % '\n'.join(
      missing_mandatory_parameter_list))

  key_file = option_dict.get('key_file')
  cert_file = option_dict.get('cert_file')
  master_ca_file = option_dict.get('master_ca_file')
230 231 232 233 234 235 236 237
  signature_private_key_file = option_dict.get('signature_private_key_file')

  mandatory_file_list = [key_file, cert_file, master_ca_file]
  # signature_private_key_file is not mandatory, we must be able to run
  # slapgrid scripts without this parameter.
  if signature_private_key_file:
    mandatory_file_list.append(signature_private_key_file)

238 239 240 241
  for k in ['shacache-cert-file', 'shacache-key-file', 'shadir-cert-file',
      'shadir-key-file']:
    mandatory_file_list.append(option_dict.get(k, None))

242
  for f in mandatory_file_list:
243 244
    if f is not None:
      if not os.path.exists(f):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
245
        parser.error('File %r does not exist.' % f)
Łukasz Nowak's avatar
Łukasz Nowak committed
246 247 248 249

  certificate_repository_path = option_dict.get('certificate_repository_path')
  if certificate_repository_path is not None:
    if not os.path.isdir(certificate_repository_path):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
250
      parser.error('Directory %r does not exist' %
Łukasz Nowak's avatar
Łukasz Nowak committed
251 252 253 254 255 256 257 258 259 260
          certificate_repository_path)

  # Supervisord configuration location
  if not option_dict.get('supervisord_configuration_path'):
    option_dict['supervisord_configuration_path'] = \
      os.path.join(option_dict['instance_root'], 'etc', 'supervisord.conf')
  # Supervisord socket
  if not option_dict.get('supervisord_socket'):
    option_dict['supervisord_socket'] = \
      os.path.join(option_dict['instance_root'], 'supervisord.socket')
Yingjie Xu's avatar
Yingjie Xu committed
261 262 263 264 265 266 267 268 269 270 271

  signature_certificate_list_string = \
    option_dict.get('signature-certificate-list', None)
  if signature_certificate_list_string is not None:
    cert_marker = "-----BEGIN CERTIFICATE-----"
    signature_certificate_list = [cert_marker + '\n' + q.strip() \
      for q in signature_certificate_list_string.split(cert_marker) \
        if q.strip()]
  else:
    signature_certificate_list = None

272 273 274 275 276 277 278 279 280 281 282 283
  # Parse cache / binary cache options
  # Backward compatibility about "binary-cache-url-blacklist" deprecated option
  if option_dict.get("binary-cache-url-blacklist") and not \
      option_dict.get("download-from-binary-cache-url-blacklist"):
    option_dict["download-from-binary-cache-url-blacklist"] = \
        option_dict["binary-cache-url-blacklist"]
  option_dict["download-from-binary-cache-url-blacklist"] = [
      url.strip() for url in option_dict.get(
          "download-from-binary-cache-url-blacklist", "").split('\n') if url]
  option_dict["upload-to-binary-cache-url-blacklist"] = [
      url.strip() for url in option_dict.get(
          "upload-to-binary-cache-url-blacklist", "").split('\n') if url]
284

285 286
  # Sleep for a random time to avoid SlapOS Master being DDOSed by an army of
  # SlapOS Nodes configured with cron.
Yingjie Xu's avatar
Yingjie Xu committed
287
  if option_dict["now"]:
288
    # XXX-Cedric: deprecate "--now"
Yingjie Xu's avatar
Yingjie Xu committed
289 290
    maximal_delay = 0
  else:
291
    maximal_delay = int(option_dict.get("maximal_delay", "0"))
292
  if maximal_delay > 0:
293
    duration = random.randint(1, maximal_delay)
294
    logging.info("Sleeping for %s seconds. To disable this feature, " \
295
                    "check --now parameter in slapgrid help." % duration)
296 297
    time.sleep(duration)

298
  # Return new Slapgrid instance and options
Łukasz Nowak's avatar
Łukasz Nowak committed
299 300 301 302 303 304 305 306 307 308 309
  return ([Slapgrid(software_root=option_dict['software_root'],
            instance_root=option_dict['instance_root'],
            master_url=option_dict['master_url'],
            computer_id=option_dict['computer_id'],
            supervisord_socket=option_dict['supervisord_socket'],
            supervisord_configuration_path=option_dict[
              'supervisord_configuration_path'],
            key_file=key_file,
            cert_file=cert_file,
            master_ca_file=master_ca_file,
            certificate_repository_path=certificate_repository_path,
310
            signature_private_key_file=signature_private_key_file,
Yingjie Xu's avatar
Yingjie Xu committed
311 312 313 314 315
            signature_certificate_list=signature_certificate_list,
            download_binary_cache_url=\
              option_dict.get('download-binary-cache-url', None),
            upload_binary_cache_url=\
              option_dict.get('upload-binary-cache-url', None),
316 317 318 319
            download_from_binary_cache_url_blacklist=\
                option_dict.get('download-from-binary-cache-url-blacklist', []),
            upload_to_binary_cache_url_blacklist=\
                option_dict.get('upload-to-binary-cache-url-blacklist', []),
320
            upload_cache_url=option_dict.get('upload-cache-url', None),
Yingjie Xu's avatar
Yingjie Xu committed
321 322 323 324
            download_binary_dir_url=\
              option_dict.get('download-binary-dir-url', None),
            upload_binary_dir_url=\
              option_dict.get('upload-binary-dir-url', None),
325
            upload_dir_url=option_dict.get('upload-dir-url', None),
326
            buildout=option_dict.get('buildout'),
327 328 329 330 331
            promise_timeout=option_dict['promise_timeout'],
            shacache_cert_file=option_dict.get('shacache-cert-file', None),
            shacache_key_file=option_dict.get('shacache-key-file', None),
            shadir_cert_file=option_dict.get('shadir-cert-file', None),
            shadir_key_file=option_dict.get('shadir-key-file', None),
332
            develop=option_dict.get('develop', False),
333 334 335 336 337 338
            software_release_filter_list=option_dict.get('only-sr',
                # Try to fetch from deprecated argument
                option_dict.get('only_sr', None)),
            computer_partition_filter_list=option_dict.get('only-cp',
                # Try to fetch from deprecated argument
                option_dict.get('only_cp', None)),
339 340
            force_periodicity = option_dict.get('force_periodicity', False),
            maximum_periodicity = option_dict.get('maximum_periodicity', 86400),
341
            ),
Łukasz Nowak's avatar
Łukasz Nowak committed
342 343 344 345 346 347 348 349 350 351
          option_dict])


def realRun(argument_tuple, method_list):
  slapgrid_object, option_dict = \
      parseArgumentTupleAndReturnSlapgridObject(*argument_tuple)
  pidfile = option_dict.get('pidfile')
  if pidfile:
    setRunning(pidfile)
  try:
352 353
    failed = False
    failed_promise = False
Łukasz Nowak's avatar
Łukasz Nowak committed
354
    for method in method_list:
355 356 357 358 359 360
      # Quite complicated way to figure out if everything went fine
      return_value = getattr(slapgrid_object, method)()
      if return_value == SLAPGRID_FAIL:
        failed = True
      if return_value == SLAPGRID_PROMISE_FAIL:
        failed_promise = True
Łukasz Nowak's avatar
Łukasz Nowak committed
361 362 363
  finally:
    if pidfile:
      setFinished(pidfile)
364 365 366 367 368
  if failed:
    sys.exit(SLAPGRID_FAIL)
  if failed_promise:
    sys.exit(SLAPGRID_PROMISE_FAIL)
  sys.exit(SLAPGRID_SUCCESS)
Łukasz Nowak's avatar
Łukasz Nowak committed
369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402


def run(*argument_tuple):
  """Hooks for generic entry point to proces Software Releases (sr),
     Computer Partitions (cp) and Usage Reports (ur)
     Will run one by one each task (sr, cp, ur). If specified,
     will run in the user wanted order.
  """
  realRun(argument_tuple, ['processSoftwareReleaseList',
    'processComputerPartitionList', 'agregateAndSendUsage'])


def runSoftwareRelease(*argument_tuple):
  """Hook for entry point to process Software Releases only
  """
  realRun(argument_tuple, ['processSoftwareReleaseList'])


def runComputerPartition(*argument_tuple):
  """Hook for entry point to process Computer Partitions only
  """
  realRun(argument_tuple, ['processComputerPartitionList'])


def runUsageReport(*argument_tuple):
  """Hook for entry point to process Usage Reports only
  """
  realRun(argument_tuple, ['agregateAndSendUsage'])


class Slapgrid(object):
  """ Main class for SlapGrid. Fetches and processes informations from master
  server and pushes usage information to master server.
  """
Antoine Catton's avatar
Antoine Catton committed
403 404 405 406

  class PromiseError(Exception):
    pass

Łukasz Nowak's avatar
Łukasz Nowak committed
407 408 409 410 411 412 413
  def __init__(self,
               software_root,
               instance_root,
               master_url,
               computer_id,
               supervisord_socket,
               supervisord_configuration_path,
414
               buildout,
415 416
               force_periodicity=False,
               maximum_periodicity=86400,
Łukasz Nowak's avatar
Łukasz Nowak committed
417 418
               key_file=None,
               cert_file=None,
419
               signature_private_key_file=None,
Yingjie Xu's avatar
Yingjie Xu committed
420 421 422
               signature_certificate_list=None,
               download_binary_cache_url=None,
               upload_binary_cache_url=None,
423 424
               download_from_binary_cache_url_blacklist=None,
               upload_to_binary_cache_url_blacklist=None,
425
               upload_cache_url=None,
Yingjie Xu's avatar
Yingjie Xu committed
426 427
               download_binary_dir_url=None,
               upload_binary_dir_url=None,
428
               upload_dir_url=None,
Łukasz Nowak's avatar
Łukasz Nowak committed
429 430
               master_ca_file=None,
               certificate_repository_path=None,
431 432 433 434
               promise_timeout=3,
               shacache_cert_file=None,
               shacache_key_file=None,
               shadir_cert_file=None,
435
               shadir_key_file=None,
436
               develop=False,
437
               software_release_filter_list=None,
438 439
               computer_partition_filter_list=None,
               ):
Łukasz Nowak's avatar
Łukasz Nowak committed
440 441 442 443 444 445 446 447 448 449 450 451
    """Makes easy initialisation of class parameters"""
    # Parses arguments
    self.software_root = os.path.abspath(software_root)
    self.instance_root = os.path.abspath(instance_root)
    self.master_url = master_url
    self.computer_id = computer_id
    self.supervisord_socket = supervisord_socket
    self.supervisord_configuration_path = supervisord_configuration_path
    self.key_file = key_file
    self.cert_file = cert_file
    self.master_ca_file = master_ca_file
    self.certificate_repository_path = certificate_repository_path
452
    self.signature_private_key_file = signature_private_key_file
Yingjie Xu's avatar
Yingjie Xu committed
453 454 455
    self.signature_certificate_list = signature_certificate_list
    self.download_binary_cache_url = download_binary_cache_url
    self.upload_binary_cache_url = upload_binary_cache_url
456 457 458 459
    self.download_from_binary_cache_url_blacklist = \
        download_from_binary_cache_url_blacklist
    self.upload_to_binary_cache_url_blacklist = \
        upload_to_binary_cache_url_blacklist
460
    self.upload_cache_url = upload_cache_url
Yingjie Xu's avatar
Yingjie Xu committed
461 462
    self.download_binary_dir_url = download_binary_dir_url
    self.upload_binary_dir_url = upload_binary_dir_url
463
    self.upload_dir_url = upload_dir_url
464 465 466 467
    self.shacache_cert_file = shacache_cert_file
    self.shacache_key_file = shacache_key_file
    self.shadir_cert_file = shadir_cert_file
    self.shadir_key_file = shadir_key_file
Łukasz Nowak's avatar
Łukasz Nowak committed
468 469 470 471 472 473 474 475 476 477 478
    # Configures logger
    self.logger = logging.getLogger('Slapgrid')
    # Creates objects from slap module
    self.slap = slap.slap()
    self.slap.initializeConnection(self.master_url, key_file=self.key_file,
        cert_file=self.cert_file, master_ca_file=self.master_ca_file)
    self.computer = self.slap.registerComputer(self.computer_id)
    # Defines all needed paths
    self.instance_etc_directory = os.path.join(self.instance_root, 'etc')
    self.supervisord_configuration_directory = \
        os.path.join(self.instance_etc_directory, 'supervisord.conf.d')
479
    self.buildout = buildout
480
    self.promise_timeout = promise_timeout
481
    self.develop = develop
482
    if software_release_filter_list is not None:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
483 484
      self.software_release_filter_list = \
          software_release_filter_list.split(",")
485
    else:
486
      self.software_release_filter_list = []
487 488
    self.computer_partition_filter_list = []
    if computer_partition_filter_list is not None:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
489 490
      self.computer_partition_filter_list = \
          computer_partition_filter_list.split(",")
491 492
    self.maximum_periodicity = maximum_periodicity
    self.force_periodicity = force_periodicity
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
493 494

  def getWatchdogLine(self):
495
    invocation_list = [WATCHDOG_PATH]
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
496
    invocation_list.append("--master-url '%s' " % self.master_url)
497 498 499
    if self.certificate_repository_path is not None:
      invocation_list.append("--certificate-repository-path '%s'" \
                               % self.certificate_repository_path)
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
500 501
    invocation_list.append("--computer-id '%s'" % self.computer_id)
    return ' '.join(invocation_list)
Łukasz Nowak's avatar
Łukasz Nowak committed
502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525

  def checkEnvironmentAndCreateStructure(self):
    """Checks for software_root and instance_root existence, then creates
       needed files and directories.
    """
    # Checks for software_root and instance_root existence
    if not os.path.isdir(self.software_root):
      error = "%s does not exist." % self.software_root
      raise OSError(error)
    if not os.path.isdir(self.instance_root):
      error = "%s does not exist." % self.instance_root
      raise OSError(error)
    # Creates everything needed
    try:
      # Creates instance_root structure
      createPrivateDirectory(self.instance_etc_directory)
      createPrivateDirectory(os.path.join(self.instance_root, 'var'))
      createPrivateDirectory(os.path.join(self.instance_root, 'var', 'log'))
      createPrivateDirectory(os.path.join(self.instance_root, 'var', 'run'))
      createPrivateDirectory(self.supervisord_configuration_directory)
      # Creates supervisord configuration
      updateFile(self.supervisord_configuration_path,
        pkg_resources.resource_stream(__name__,
          'templates/supervisord.conf.in').read() % dict(
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
526 527
            supervisord_configuration_directory=\
                self.supervisord_configuration_directory,
Łukasz Nowak's avatar
Łukasz Nowak committed
528 529 530 531 532 533 534 535 536
            supervisord_socket=os.path.abspath(self.supervisord_socket),
            supervisord_loglevel='info',
            supervisord_logfile=os.path.abspath(os.path.join(
              self.instance_root, 'var', 'log', 'supervisord.log')),
            supervisord_logfile_maxbytes='50MB',
            supervisord_nodaemon='false',
            supervisord_pidfile=os.path.abspath(os.path.join(
              self.instance_root, 'var', 'run', 'supervisord.pid')),
            supervisord_logfile_backups='10',
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
537
            watchdog_command = self.getWatchdogLine(),
Łukasz Nowak's avatar
Łukasz Nowak committed
538 539 540 541 542 543 544 545 546
          ))
    except (WrongPermissionError, PathDoesNotExistError) as error:
      raise error

  def getComputerPartitionList(self):
    try:
      computer_partition_list = self.computer.getComputerPartitionList()
    except socket.error as error:
      self.logger.fatal(error)
547
      raise
Łukasz Nowak's avatar
Łukasz Nowak committed
548 549 550 551 552 553 554 555
    return computer_partition_list

  def processSoftwareReleaseList(self):
    """Will process each Software Release.
    """
    self.checkEnvironmentAndCreateStructure()
    logger = logging.getLogger('SoftwareReleases')
    logger.info("Processing software releases...")
556
    # Boolean to know if every instance has correctly been deployed
Łukasz Nowak's avatar
Łukasz Nowak committed
557 558
    clean_run = True
    for software_release in self.computer.getSoftwareReleaseList():
Łukasz Nowak's avatar
Łukasz Nowak committed
559
      state = software_release.getState()
Łukasz Nowak's avatar
Łukasz Nowak committed
560 561
      try:
        software_release_uri = software_release.getURI()
562 563
        url_hash = md5(software_release_uri).hexdigest()
        software_path = os.path.join(self.software_root, url_hash)
Łukasz Nowak's avatar
Łukasz Nowak committed
564 565
        software = Software(url=software_release_uri,
            software_root=self.software_root,
566
            buildout=self.buildout,
567
            signature_private_key_file=self.signature_private_key_file,
Yingjie Xu's avatar
Yingjie Xu committed
568 569 570
            signature_certificate_list=self.signature_certificate_list,
            download_binary_cache_url=self.download_binary_cache_url,
            upload_binary_cache_url=self.upload_binary_cache_url,
571 572 573 574
            download_from_binary_cache_url_blacklist=\
                self.download_from_binary_cache_url_blacklist,
            upload_to_binary_cache_url_blacklist=\
                self.upload_to_binary_cache_url_blacklist,
575
            upload_cache_url=self.upload_cache_url,
Yingjie Xu's avatar
Yingjie Xu committed
576 577
            download_binary_dir_url=self.download_binary_dir_url,
            upload_binary_dir_url=self.upload_binary_dir_url,
578 579 580 581
            upload_dir_url=self.upload_dir_url,
            shacache_cert_file=self.shacache_cert_file,
            shacache_key_file=self.shacache_key_file,
            shadir_cert_file=self.shadir_cert_file,
Łukasz Nowak's avatar
Łukasz Nowak committed
582 583
            shadir_key_file=self.shadir_key_file)
        if state == 'available':
584
          completed_tag = os.path.join(software_path, '.completed')
585
          if self.develop or (not os.path.exists(completed_tag) and \
586
                 len(self.software_release_filter_list) == 0) or \
587 588
                 url_hash in self.software_release_filter_list or \
                 url_hash in (md5(uri).hexdigest() for uri in self.software_release_filter_list):
589 590 591 592 593 594 595 596
            try:
              software_release.building()
            except NotFoundError:
              pass
            software.install()
            file_descriptor = open(completed_tag, 'w')
            file_descriptor.write(time.asctime())
            file_descriptor.close()
Łukasz Nowak's avatar
Łukasz Nowak committed
597
        elif state == 'destroyed':
598 599 600 601
          if os.path.exists(software_path):
            logger.info('Destroying %r...' % software_release_uri)
            software.destroy()
            logger.info('Destroyed %r.' % software_release_uri)
602
      # Send log before exiting
Łukasz Nowak's avatar
Łukasz Nowak committed
603 604 605 606
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        software_release.error(exception)
        raise
607 608

      # Buildout failed: send log but don't print it to output (already done)
609
      except BuildoutFailedError as exception:
610 611 612 613 614 615 616 617 618 619 620
        clean_run = False
        try:
          software_release.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

      # For everything else: log it, send it, continue.
Łukasz Nowak's avatar
Łukasz Nowak committed
621 622 623 624 625 626
      except Exception:
        exception = traceback.format_exc()
        logger.error(exception)
        software_release.error(exception)
        clean_run = False
      else:
Łukasz Nowak's avatar
Łukasz Nowak committed
627
        if state == 'available':
628 629 630 631
          try:
            software_release.available()
          except NotFoundError:
            pass
Łukasz Nowak's avatar
Łukasz Nowak committed
632
        elif state == 'destroyed':
633 634
          try:
            software_release.destroyed()
635 636
          except (NotFoundError, ServerError):
            print traceback.format_exc()
637
    logger.info("Finished software releases.")
638 639 640 641 642

    # Return success value
    if not clean_run:
      return SLAPGRID_FAIL
    return SLAPGRID_SUCCESS
Łukasz Nowak's avatar
Łukasz Nowak committed
643

644

Łukasz Nowak's avatar
Łukasz Nowak committed
645 646
  def _launchSupervisord(self):
    launchSupervisord(self.supervisord_socket,
Marco Mariani's avatar
Marco Mariani committed
647 648
        self.supervisord_configuration_path,
        logger=self.logger)
Łukasz Nowak's avatar
Łukasz Nowak committed
649

Antoine Catton's avatar
Antoine Catton committed
650
  def _checkPromises(self, computer_partition):
651
    self.logger.info("Checking promises...")
Antoine Catton's avatar
Antoine Catton committed
652 653 654 655 656 657 658 659 660 661
    instance_path = os.path.join(self.instance_root,
        computer_partition.getId())

    uid, gid = None, None
    stat_info = os.stat(instance_path)

    #stat sys call to get statistics informations
    uid = stat_info.st_uid
    gid = stat_info.st_gid

662
    promise_present = False
Antoine Catton's avatar
Antoine Catton committed
663 664 665 666 667 668 669 670
    # Get the list of promises
    promise_dir = os.path.join(instance_path, 'etc', 'promise')
    if os.path.exists(promise_dir) and os.path.isdir(promise_dir):
      cwd = instance_path
      promises_list = os.listdir(promise_dir)

      # Check whether every promise is kept
      for promise in promises_list:
671
        promise_present = True
Antoine Catton's avatar
Antoine Catton committed
672

Antoine Catton's avatar
Antoine Catton committed
673 674 675
        command = [os.path.join(promise_dir, promise)]

        promise = os.path.basename(command[0])
676
        self.logger.info("Checking promise %r.", promise)
Antoine Catton's avatar
Antoine Catton committed
677

678 679
        kw = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            stdin=subprocess.PIPE)
Antoine Catton's avatar
Antoine Catton committed
680

681
        process_handler = subprocess.Popen(command,
Antoine Catton's avatar
Antoine Catton committed
682 683
          preexec_fn=lambda: dropPrivileges(uid, gid),
          cwd=cwd,
Jondy Zhao's avatar
Jondy Zhao committed
684
          env=None if sys.platform == 'cygwin' else {}, **kw)
685 686 687
        process_handler.stdin.flush()
        process_handler.stdin.close()
        process_handler.stdin = None
Antoine Catton's avatar
Antoine Catton committed
688 689 690 691

        time.sleep(self.promise_timeout)

        if process_handler.poll() is None:
692
          process_handler.terminate()
Antoine Catton's avatar
Antoine Catton committed
693 694 695 696 697
          raise Slapgrid.PromiseError("The promise %r timed out" % promise)
        elif process_handler.poll() != 0:
          stderr = process_handler.communicate()[1]
          if stderr is None:
            stderr = 'No error output from %r.' % promise
Antoine Catton's avatar
Antoine Catton committed
698 699
          else:
            stderr = 'Promise %r:' % promise + stderr
Antoine Catton's avatar
Antoine Catton committed
700 701
          raise Slapgrid.PromiseError(stderr)

Antoine Catton's avatar
Antoine Catton committed
702 703 704
    if not promise_present:
      self.logger.info("No promise.")

705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
  def processComputerPartition(self, computer_partition):
    """
    Process a Computer Partition, depending on its state
    """
    logger = logging.getLogger('ComputerPartitionProcessing')

    computer_partition_id = computer_partition.getId()

    # Sanity checks before processing
    # Those values should not be None or empty string or any falsy value
    if not computer_partition_id:
      raise ValueError('Computer Partition id is empty.')

    # Check if we defined explicit list of partitions to process.
    # If so, if current partition not in this list, skip.
    if len(self.computer_partition_filter_list) > 0 and \
         (computer_partition_id not in self.computer_partition_filter_list):
      return

724 725
    logger.info('Processing Computer Partition %s...' % computer_partition_id)

726 727 728
    instance_path = os.path.join(self.instance_root, computer_partition_id)

    # Try to get partition timestamp (last modification date)
729 730 731 732
    timestamp_path = os.path.join(
        instance_path,
        COMPUTER_PARTITION_TIMESTAMP_FILENAME
    )
733 734 735 736 737 738
    parameter_dict = computer_partition.getInstanceParameterDict()
    if 'timestamp' in parameter_dict:
      timestamp = parameter_dict['timestamp']
    else:
      timestamp = None

739 740 741 742 743 744 745 746
    try:
      software_url = computer_partition.getSoftwareRelease().getURI()
    except NotFoundError:
      # Problem with instance: SR URI not set.
      # Try to process it anyway, it may need to be deleted.
      software_url = None
    try:
      software_path = os.path.join(self.software_root,
747
          getSoftwareUrlHash(software_url))
748 749 750 751 752
    except TypeError:
      # Problem with instance: SR URI not set.
      # Try to process it anyway, it may need to be deleted.
      software_path = None

753
    periodicity = self.maximum_periodicity
754
    if software_path:
755 756
      # Get periodicity from periodicity file if not forced
      if not self.force_periodicity:
757
        periodicity_path = os.path.join(software_path, 'periodicity')
758 759
        if os.path.exists(periodicity_path):
          try:
760
            periodicity = int(open(periodicity_path).read())
761 762 763 764
          except ValueError:
            os.remove(periodicity_path)
            exception = traceback.format_exc()
            logger.error(exception)
765 766 767 768 769 770 771 772 773 774 775

    # Check if timestamp from server is more recent than local one.
    # If not: it's not worth processing this partition (nothing has
    # changed).
    if computer_partition_id not in self.computer_partition_filter_list and \
        (not self.develop) and os.path.exists(timestamp_path):
      old_timestamp = open(timestamp_path).read()
      last_runtime = int(os.path.getmtime(timestamp_path))
      if timestamp:
        try:
          if int(timestamp) <= int(old_timestamp):
776 777
            if computer_partition.getState() != COMPUTER_PARTITION_STARTED_STATE:
              return
778 779 780 781
            # Check periodicity, i.e if periodicity is one day, partition
            # should be processed at least every day.
            # Only do it for "started" instances
            if int(time.time()) <= (last_runtime + periodicity):
782
              self.logger.info('Partition already up-to-date, skipping.')
783
              return
784 785 786 787
            else:
              # Periodicity forced processing this partition. Removing
              # the timestamp file in case it fails.
              os.remove(timestamp_path)
788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805
        except ValueError:
          os.remove(timestamp_path)
          exception = traceback.format_exc()
          logger.error(exception)

    local_partition = Partition(
      software_path=software_path,
      instance_path=instance_path,
      supervisord_partition_configuration_path=os.path.join(
        self.supervisord_configuration_directory, '%s.conf' %
        computer_partition_id),
      supervisord_socket=self.supervisord_socket,
      computer_partition=computer_partition,
      computer_id=self.computer_id,
      partition_id=computer_partition_id,
      server_url=self.master_url,
      software_release_url=software_url,
      certificate_repository_path=self.certificate_repository_path,
806
      buildout=self.buildout)
807 808

    computer_partition_state = computer_partition.getState()
809
    if computer_partition_state == COMPUTER_PARTITION_STARTED_STATE:
810 811 812 813 814
      local_partition.install()
      computer_partition.available()
      local_partition.start()
      self._checkPromises(computer_partition)
      computer_partition.started()
815
    elif computer_partition_state == COMPUTER_PARTITION_STOPPED_STATE:
816 817 818 819 820 821 822 823
      try:
        local_partition.install()
        computer_partition.available()
      except Exception:
        raise
      finally:
        # Instance has to be stopped even if buildout/reporting is wrong.
        local_partition.stop()
824
      computer_partition.stopped()
825
    elif computer_partition_state == COMPUTER_PARTITION_DESTROYED_STATE:
826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
      local_partition.stop()
      try:
        computer_partition.stopped()
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        computer_partition.error(exception)
        raise
      except Exception:
        pass
    else:
      error_string = "Computer Partition %r has unsupported state: %s" % \
        (computer_partition_id, computer_partition_state)
      computer_partition.error(error_string)
      raise NotImplementedError(error_string)

    # If partition has been successfully processed, write timestamp
    if timestamp:
843 844 845 846
      timestamp_path = os.path.join(
          instance_path,
          COMPUTER_PARTITION_TIMESTAMP_FILENAME
      )
847 848
      open(timestamp_path, 'w').write(timestamp)

849
  def FilterComputerPartitionList(self, computer_partition_list):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
850
    """
851
    Try to filter valid partitions to be processed from free partitions.
Łukasz Nowak's avatar
Łukasz Nowak committed
852 853
    """
    logger = logging.getLogger('ComputerPartitionProcessing')
854 855
    filtered_computer_partition_list = []
    for computer_partition in computer_partition_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
856
      try:
857 858 859 860 861 862 863
        computer_partition_path = os.path.join(self.instance_root,
            computer_partition.getId())
        if not os.path.exists(computer_partition_path):
          raise NotFoundError('Partition directory %s does not exist.' %
              computer_partition_path)
        # Check state of partition. If it is in "destroyed" state, check if it
        # partition is actually installed in the Computer or if it is "free"
864
        # partition, and check if it has some Software information.
865 866 867
        # XXX-Cedric: Temporary AND ugly solution to check if an instance
        # is in the partition. Dangerous because not 100% sure it is empty
        computer_partition_state = computer_partition.getState()
868 869 870 871
        try:
          software_url = computer_partition.getSoftwareRelease().getURI()
        except (NotFoundError, TypeError, NameError):
          software_url = None
872
        if computer_partition_state == COMPUTER_PARTITION_DESTROYED_STATE and \
873 874
           os.listdir(computer_partition_path) == [] and \
           not software_url:
875
          continue
876

877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921
        # Everything seems fine
        filtered_computer_partition_list.append(computer_partition)

      # XXX-Cedric: factor all this error handling

      # Send log before exiting
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        computer_partition.error(exception)
        raise

      # Buildout failed: send log but don't print it to output (already done)
      except BuildoutFailedError, exception:
        try:
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

      # For everything else: log it, send it, continue.
      except Exception as exception:
        logger.error(traceback.format_exc())
        try:
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

    return filtered_computer_partition_list

  def processComputerPartitionList(self):
    """
    Will start supervisord and process each Computer Partition.
    """
    logger = logging.getLogger('ComputerPartitionProcessing')
    logger.info('Processing computer partitions...')
    # Prepares environment
    self.checkEnvironmentAndCreateStructure()
    self._launchSupervisord()
922 923

    # Boolean to know if every instance has correctly been deployed
924
    clean_run = True
925 926
    # Boolean to know if every promises correctly passed
    clean_run_promise = True
927 928 929 930 931 932 933 934 935 936

    # Filter all dummy / empty partitions
    computer_partition_list = self.FilterComputerPartitionList(
        self.getComputerPartitionList())

    for computer_partition in computer_partition_list:
      # Nothing should raise outside of the current loop iteration, so that
      # even if something is terribly wrong while processing an instance, it
      # won't prevent processing other ones.
      try:
937
        # Process the partition itself
938
        self.processComputerPartition(computer_partition)
939

940
      # Send log before exiting
Łukasz Nowak's avatar
Łukasz Nowak committed
941 942 943 944
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        computer_partition.error(exception)
        raise
945

946 947 948
      except Slapgrid.PromiseError as exception:
        clean_run_promise = False
        try:
949
          logger.error(exception)
950 951 952 953 954 955 956 957
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

958 959 960 961 962 963 964 965 966 967 968 969 970
      # Buildout failed: send log but don't print it to output (already done)
      except BuildoutFailedError, exception:
        clean_run = False
        try:
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

      # For everything else: log it, send it, continue.
971
      except Exception as exception:
Łukasz Nowak's avatar
Łukasz Nowak committed
972
        clean_run = False
973
        logger.error(traceback.format_exc())
974 975 976 977 978 979 980 981 982
        try:
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

983
    logger.info("Finished computer partitions.")
984 985 986 987 988 989 990

    # Return success value
    if not clean_run:
      return SLAPGRID_FAIL
    if not clean_run_promise:
      return SLAPGRID_PROMISE_FAIL
    return SLAPGRID_SUCCESS
Łukasz Nowak's avatar
Łukasz Nowak committed
991

992

993 994
  def validateXML(self, to_be_validated, xsd_model):
    """Validates a given xml file"""
Łukasz Nowak's avatar
Łukasz Nowak committed
995

996 997 998 999 1000
    logger = logging.getLogger('XMLValidating')

    #We retrieve the xsd model
    xsd_model = StringIO.StringIO(xsd_model)
    xmlschema_doc = etree.parse(xsd_model)
Łukasz Nowak's avatar
Łukasz Nowak committed
1001 1002
    xmlschema = etree.XMLSchema(xmlschema_doc)

1003
    try:
1004
      document = etree.fromstring(to_be_validated)
1005
    except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
1006 1007 1008
      logger.info('Failed to parse this XML report :  %s\n%s' % \
        (to_be_validated, _formatXMLError(e)))
      logger.error(_formatXMLError(e))
1009 1010
      return False

Łukasz Nowak's avatar
Łukasz Nowak committed
1011 1012 1013 1014 1015
    if xmlschema.validate(document):
      return True

    return False

1016 1017 1018
  def asXML(self, computer_partition_usage_list):
    """Generates a XML report from computer partition usage list
    """
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032
    xml = ['<?xml version="1.0"?>',
           '<journal>',
           '<transaction type="Sale Packing List">',
           '<title>Resource consumptions</title>',
           '<start_date></start_date>',
           '<stop_date>%s</stop_date>' % time.strftime("%Y-%m-%d at %H:%M:%S"),
           '<reference>%s</reference>' % self.computer_id,
           '<currency></currency>',
           '<payment_mode></payment_mode>',
           '<category></category>',
           '<arrow type="Administration">',
           '<source></source>',
           '<destination></destination>',
           '</arrow>']
1033 1034 1035

    for computer_partition_usage in computer_partition_usage_list:
      try:
1036
        root = etree.fromstring(computer_partition_usage.usage)
1037
      except UnicodeError, e:
1038 1039 1040
        self.logger.info("Failed to read %s." % (
            computer_partition_usage.usage))
        self.logger.error(UnicodeError)
1041
        raise UnicodeError("Failed to read %s: %s" % (computer_partition_usage.usage, e))
1042
      except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
Cédric de Saint Martin's avatar
YATTA  
Cédric de Saint Martin committed
1043
        self.logger.info("Failed to parse %s." % (computer_partition_usage.usage))
1044
        self.logger.error(e)
1045
        raise _formatXMLError(e)
1046 1047
      except Exception, e:
        raise Exception("Failed to generate XML report: %s" % e)
1048 1049

      for movement in root.findall('movement'):
1050 1051 1052 1053
        xml.append('<movement>')
        for child in movement.getchildren():
          if child.tag == "reference":
            xml.append('<%s>%s</%s>' % (child.tag, computer_partition_usage.getId(), child.tag))
1054
          else:
1055 1056
            xml.append('<%s>%s</%s>' % (child.tag, child.text, child.tag))
        xml.append('</movement>')
1057

1058
    xml.append('</transaction></journal>')
1059

1060
    return ''.join(xml)
1061

Łukasz Nowak's avatar
Łukasz Nowak committed
1062 1063 1064
  def agregateAndSendUsage(self):
    """Will agregate usage from each Computer Partition.
    """
1065 1066 1067 1068
    # Prepares environment
    self.checkEnvironmentAndCreateStructure()
    self._launchSupervisord()

Łukasz Nowak's avatar
Łukasz Nowak committed
1069 1070 1071 1072 1073
    slap_computer_usage = self.slap.registerComputer(self.computer_id)
    computer_partition_usage_list = []
    logger = logging.getLogger('UsageReporting')
    logger.info("Aggregating and sending usage reports...")

1074 1075 1076 1077 1078 1079 1080 1081 1082
    #We retrieve XSD models
    try:
      computer_consumption_model = \
        pkg_resources.resource_string(
          'slapos.slap',
          'doc/computer_consumption.xsd')
    except IOError:
      computer_consumption_model = \
        pkg_resources.resource_string(
1083
          __name__,
1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
          '../../../../slapos/slap/doc/computer_consumption.xsd')

    try:
      partition_consumption_model = \
        pkg_resources.resource_string(
          'slapos.slap',
          'doc/partition_consumption.xsd')
    except IOError:
      partition_consumption_model = \
        pkg_resources.resource_string(
1094
          __name__,
1095 1096
          '../../../../slapos/slap/doc/partition_consumption.xsd')

Łukasz Nowak's avatar
Łukasz Nowak committed
1097
    clean_run = True
1098 1099 1100
    # Loop on the different computer partitions
    computer_partition_list = self.FilterComputerPartitionList(
       slap_computer_usage.getComputerPartitionList())
1101

1102
    for computer_partition in computer_partition_list:
1103 1104
      try:
        computer_partition_id = computer_partition.getId()
1105

1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
        #We want execute all the script in the report folder
        instance_path = os.path.join(self.instance_root,
            computer_partition.getId())
        report_path = os.path.join(instance_path, 'etc', 'report')
        if os.path.isdir(report_path):
          script_list_to_run = os.listdir(report_path)
        else:
          script_list_to_run = []
        
        #We now generate the pseudorandom name for the xml file
        # and we add it in the invocation_list
        f = tempfile.NamedTemporaryFile()
        name_xml = '%s.%s' % ('slapreport', os.path.basename(f.name))
        path_to_slapreport = os.path.join(instance_path, 'var', 'xml_report',
            name_xml)
        
        failed_script_list = []
        for script in script_list_to_run:
          invocation_list = []
          invocation_list.append(os.path.join(instance_path, 'etc', 'report',
            script))
          #We add the xml_file name in the invocation_list
          #f = tempfile.NamedTemporaryFile()
          #name_xml = '%s.%s' % ('slapreport', os.path.basename(f.name))
          #path_to_slapreport = os.path.join(instance_path, 'var', name_xml)
        
          invocation_list.append(path_to_slapreport)
          #Dropping privileges
          uid, gid = None, None
          stat_info = os.stat(instance_path)
          #stat sys call to get statistics informations
          uid = stat_info.st_uid
          gid = stat_info.st_gid
1139
          kw = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1140 1141 1142 1143 1144 1145 1146 1147
          process_handler = SlapPopen(invocation_list,
            preexec_fn=lambda: dropPrivileges(uid, gid),
            cwd=os.path.join(instance_path, 'etc', 'report'),
            env=None, **kw)
          if process_handler.returncode is None:
            process_handler.kill()
          if process_handler.returncode != 0:
            clean_run = False
1148 1149
            failed_script_list.append("Script %r failed." % script)
            logger.warning("Failed to run %r" % invocation_list)
1150 1151 1152 1153 1154 1155 1156 1157 1158
          if len(failed_script_list):
            computer_partition.error('\n'.join(failed_script_list))
      # Whatever happens, don't stop processing other instances
      except Exception:
        computer_partition_id = computer_partition.getId()
        exception = traceback.format_exc()
        issue = "Cannot run usage script(s) for %r: %s" % (
            computer_partition_id, exception)
        logger.info(issue)
Łukasz Nowak's avatar
Łukasz Nowak committed
1159

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
1160
    #Now we loop through the different computer partitions to report
Łukasz Nowak's avatar
Łukasz Nowak committed
1161
    report_usage_issue_cp_list = []
1162
    for computer_partition in computer_partition_list:
1163 1164 1165 1166 1167 1168 1169 1170 1171
      try:
        filename_delete_list = []
        computer_partition_id = computer_partition.getId()
        instance_path = os.path.join(self.instance_root, computer_partition_id)
        dir_reports = os.path.join(instance_path, 'var', 'xml_report')
        #The directory xml_report contain a number of files equal
        #to the number of software instance running inside the same partition
        if os.path.isdir(dir_reports):
          filename_list = os.listdir(dir_reports)
Łukasz Nowak's avatar
Łukasz Nowak committed
1172
        else:
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199
          filename_list = []
        #logger.debug('name List %s' % filename_list)
        usage = ''
        
        for filename in filename_list:
        
          file_path = os.path.join(dir_reports, filename)
          if os.path.exists(file_path):
            usage_file = open(file_path, 'r')
            usage = usage_file.read()
            usage_file.close()
        
            #We check the validity of xml content of each reports
            if not self.validateXML(usage, partition_consumption_model):
              logger.info('WARNING: The XML file %s generated by slapreport is '
                  'not valid - This report is left as is at %s where you can '
                  'inspect what went wrong ' % (filename, dir_reports))
              # Warn the SlapOS Master that a partition generates corrupted xml
              # report
            else:
              computer_partition_usage = self.slap.registerComputerPartition(
                      self.computer_id, computer_partition_id)
              computer_partition_usage.setUsage(usage)
              computer_partition_usage_list.append(computer_partition_usage)
              filename_delete_list.append(filename)
          else:
            logger.debug("Usage report %r not found, ignored" % file_path)
Łukasz Nowak's avatar
Łukasz Nowak committed
1200

1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211
        #After sending the aggregated file we remove all the valid xml reports
        for filename in filename_delete_list:
          os.remove(os.path.join(dir_reports, filename))

      # Whatever happens, don't stop processing other instances
      except Exception:
        computer_partition_id = computer_partition.getId()
        exception = traceback.format_exc()
        issue = "Cannot run usage script(s) for %r: %s" % (
            computer_partition_id, exception)
        logger.info(issue)
1212 1213 1214 1215 1216 1217 1218

    for computer_partition_usage in computer_partition_usage_list:
      logger.info('computer_partition_usage_list : %s - %s' % \
        (computer_partition_usage.usage, computer_partition_usage.getId()))

    #If there is, at least, one report
    if computer_partition_usage_list != []:
Łukasz Nowak's avatar
Łukasz Nowak committed
1219
      try:
1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230
        #We generate the final XML report with asXML method
        computer_consumption = self.asXML(computer_partition_usage_list)

        logger.info('Final xml report : %s' % computer_consumption)

        #We test the XML report before sending it
        if self.validateXML(computer_consumption, computer_consumption_model):
          logger.info('XML file generated by asXML is valid')
          slap_computer_usage.reportUsage(computer_consumption)
        else:
          logger.info('XML file generated by asXML is not valid !')
1231
          raise ValueError('XML file generated by asXML is not valid !')
Łukasz Nowak's avatar
Łukasz Nowak committed
1232 1233 1234 1235 1236 1237 1238 1239 1240
      except Exception:
        computer_partition_id = computer_partition.getId()
        exception = traceback.format_exc()
        issue = "Cannot report usage for %r: %s" % (computer_partition_id,
          exception)
        logger.info(issue)
        computer_partition.error(issue)
        report_usage_issue_cp_list.append(computer_partition_id)

1241
    for computer_partition in computer_partition_list:
1242
      if computer_partition.getState() == COMPUTER_PARTITION_DESTROYED_STATE:
Łukasz Nowak's avatar
Łukasz Nowak committed
1243
        try:
1244
          computer_partition_id = computer_partition.getId()
1245
          try:
1246 1247 1248
             software_url = computer_partition.getSoftwareRelease().getURI()
             software_path = os.path.join(self.software_root,
                 getSoftwareUrlHash(software_url))
1249 1250 1251
          except (NotFoundError, TypeError):
            software_url = None
            software_path = None
1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
          local_partition = Partition(
            software_path=software_path,
            instance_path=os.path.join(self.instance_root,
                computer_partition.getId()),
            supervisord_partition_configuration_path=os.path.join(
              self.supervisord_configuration_directory, '%s.conf' %
              computer_partition_id),
            supervisord_socket=self.supervisord_socket,
            computer_partition=computer_partition,
            computer_id=self.computer_id,
            partition_id=computer_partition_id,
            server_url=self.master_url,
            software_release_url=software_url,
            certificate_repository_path=self.certificate_repository_path,
1266
            buildout=self.buildout,
1267
            )
Łukasz Nowak's avatar
Łukasz Nowak committed
1268 1269 1270 1271 1272 1273 1274 1275 1276
          local_partition.stop()
          try:
            computer_partition.stopped()
          except (SystemExit, KeyboardInterrupt):
            exception = traceback.format_exc()
            computer_partition.error(exception)
            raise
          except Exception:
            pass
1277 1278 1279 1280 1281
          if computer_partition.getId() in report_usage_issue_cp_list:
            logger.info('Ignoring destruction of %r, as not report usage was '
              'sent' % computer_partition.getId())
            continue
          local_partition.destroy()
Łukasz Nowak's avatar
Łukasz Nowak committed
1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296
        except (SystemExit, KeyboardInterrupt):
          exception = traceback.format_exc()
          computer_partition.error(exception)
          raise
        except Exception:
          clean_run = False
          exception = traceback.format_exc()
          computer_partition.error(exception)
          logger.error(exception)
        try:
          computer_partition.destroyed()
        except slap.NotFoundError:
          logger.debug('Ignored slap error while trying to inform about '
              'destroying not fully configured Computer Partition %r' %
                  computer_partition.getId())
1297 1298 1299 1300
        except ServerError as server_error:
          logger.debug('Ignored server error while trying to inform about '
              'destroying Computer Partition %r. Error is :\n%r' %
                  (computer_partition.getId(), server_error.args[0]))
Łukasz Nowak's avatar
Łukasz Nowak committed
1301

1302
    logger.info("Finished usage reports.")
1303 1304 1305 1306 1307

    # Return success value
    if not clean_run:
      return SLAPGRID_FAIL
    return SLAPGRID_SUCCESS