slapgrid.py 51.3 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
Łukasz Nowak's avatar
Łukasz Nowak committed
33 34 35
import logging
import os
import pkg_resources
36
import random
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
37 38
import socket
import StringIO
39
import subprocess
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
40 41 42 43
import sys
import tempfile
import time
import traceback
Łukasz Nowak's avatar
Łukasz Nowak committed
44 45
import warnings
if sys.version_info < (2, 6):
Marco Mariani's avatar
Marco Mariani committed
46
  warnings.warn('Used python version (%s) is old and has problems with'
Łukasz Nowak's avatar
Łukasz Nowak committed
47 48
      ' IPv6 connections' % sys.version.split('\n')[0])

Marco Mariani's avatar
Marco Mariani committed
49 50
from lxml import etree

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
51 52
from slapos.slap.slap import NotFoundError
from slapos.slap.slap import ServerError
Marco Mariani's avatar
Marco Mariani committed
53 54
from slapos.grid.exception import BuildoutFailedError
from slapos.grid.SlapObject import Software, Partition, WrongPermissionError, \
Łukasz Nowak's avatar
Łukasz Nowak committed
55
    PathDoesNotExistError
Marco Mariani's avatar
Marco Mariani committed
56
from slapos.grid.svcbackend import launchSupervisord
57
from slapos.grid.utils import (md5digest, createPrivateDirectory, dropPrivileges,
Marco Mariani's avatar
Marco Mariani committed
58 59
                               setRunning, setFinished, SlapPopen, updateFile)
import slapos.slap
Łukasz Nowak's avatar
Łukasz Nowak committed
60 61 62 63 64 65 66 67

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

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
68
# XXX: should be moved to SLAP library
69
COMPUTER_PARTITION_DESTROYED_STATE = 'destroyed'
70 71
COMPUTER_PARTITION_STARTED_STATE = 'started'
COMPUTER_PARTITION_STOPPED_STATE = 'stopped'
Łukasz Nowak's avatar
Łukasz Nowak committed
72

73 74 75 76 77
# Global variables about return state of slapgrid
SLAPGRID_SUCCESS = 0
SLAPGRID_FAIL = 1
SLAPGRID_PROMISE_FAIL = 2

78
# XXX hardcoded watchdog_path
79
WATCHDOG_PATH = '/opt/slapos/bin/slapos-watchdog'
80

81 82
COMPUTER_PARTITION_TIMESTAMP_FILENAME = '.timestamp'

83 84 85 86 87

class _formatXMLError(Exception):
  pass


Łukasz Nowak's avatar
Łukasz Nowak committed
88 89 90 91 92 93 94 95
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
96
      help="The instance root directory location.")
Łukasz Nowak's avatar
Łukasz Nowak committed
97
  parser.add_argument("--software-root",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
98
      help="The software_root directory location.")
Łukasz Nowak's avatar
Łukasz Nowak committed
99
  parser.add_argument("--master-url",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
100
      help="The master server URL. Mandatory.")
Łukasz Nowak's avatar
Łukasz Nowak committed
101
  parser.add_argument("--computer-id",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
102
      help="The computer id defined in the server.")
Łukasz Nowak's avatar
Łukasz Nowak committed
103
  parser.add_argument("--supervisord-socket",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
104
      help="The socket supervisor will use.")
Łukasz Nowak's avatar
Łukasz Nowak committed
105
  parser.add_argument("--supervisord-configuration-path",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
106 107 108
      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
109
  parser.add_argument("--pidfile",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
110
      help="The location where pidfile will be created.")
Łukasz Nowak's avatar
Łukasz Nowak committed
111
  parser.add_argument("--logfile",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
112
      help="The location where slapgrid logfile will be created.")
Łukasz Nowak's avatar
Łukasz Nowak committed
113 114 115
  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
116 117 118 119
  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
120 121 122
  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,
123
      help="Deprecated, doesn't do anything.")
Łukasz Nowak's avatar
Łukasz Nowak committed
124 125
  parser.add_argument("-v", "--verbose", action="store_true", default=False,
      help="Be verbose.")
126 127
  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
128 129
  parser.add_argument("--promise-timeout", type=int, default=3,
      help="Promise timeout in seconds.")
Yingjie Xu's avatar
Yingjie Xu committed
130
  parser.add_argument("--now", action="store_true", default=False,
131
      help="Launch slapgrid without delay. Default behavior.")
132
  parser.add_argument("--all", action="store_true", default=False,
Jérome Perrin's avatar
Jérome Perrin committed
133
      help="Launch slapgrid to process all Softare Releases "
134
           "and/or Computer Partitions.")
135
  parser.add_argument("--only-sr",
Jérome Perrin's avatar
Jérome Perrin committed
136 137
      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
138
           "sofware releases be ignored.")
139
  parser.add_argument("--only-cp",
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
140 141 142
      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
143

144 145 146 147 148 149 150 151 152 153 154 155 156
  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.")

Łukasz Nowak's avatar
Łukasz Nowak committed
157
  # Parses arguments
Marco Mariani's avatar
Marco Mariani committed
158
  if not argument_tuple:
Łukasz Nowak's avatar
Łukasz Nowak committed
159 160 161
    # No arguments given to entry point : we parse sys.argv.
    argument_option_instance = parser.parse_args()
  else:
Marco Mariani's avatar
Marco Mariani committed
162
    argument_option_instance = parser.parse_args(list(argument_tuple))
Łukasz Nowak's avatar
Łukasz Nowak committed
163
  # Parses arguments from config file, if needed, then merge previous arguments
Marco Mariani's avatar
Marco Mariani committed
164
  options = {}
Łukasz Nowak's avatar
Łukasz Nowak committed
165 166 167 168 169
  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
Marco Mariani's avatar
Marco Mariani committed
170
  options = dict(slapgrid_configuration.items("slapos"))
171
  if slapgrid_configuration.has_section("networkcache"):
Marco Mariani's avatar
Marco Mariani committed
172
    options.update(dict(slapgrid_configuration.items("networkcache")))
Łukasz Nowak's avatar
Łukasz Nowak committed
173 174 175
  for argument_key, argument_value in vars(argument_option_instance
      ).iteritems():
    if argument_value is not None:
Marco Mariani's avatar
Marco Mariani committed
176
      options.update({argument_key: argument_value})
Łukasz Nowak's avatar
Łukasz Nowak committed
177
  # Configures logger.
Marco Mariani's avatar
Marco Mariani committed
178
  if options['verbose']:
Łukasz Nowak's avatar
Łukasz Nowak committed
179 180 181
    level = logging.DEBUG
  else:
    level = logging.INFO
182 183 184
  logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s',
                      level=level,
                      datefmt='%Y-%m-%dT%H:%M:%S')
Marco Mariani's avatar
Marco Mariani committed
185 186
  if options.get('logfile'):
    console = logging.FileHandler(options['logfile'])
187 188 189 190 191
    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
192 193
  missing_mandatory_parameter_list = []
  for mandatory_parameter in MANDATORY_PARAMETER_LIST:
Marco Mariani's avatar
Marco Mariani committed
194
    if not mandatory_parameter in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
195 196
      missing_mandatory_parameter_list.append(mandatory_parameter)

Marco Mariani's avatar
Marco Mariani committed
197
  if options.get('all'):
Marco Mariani's avatar
Marco Mariani committed
198
    options['develop'] = True
199

Marco Mariani's avatar
Marco Mariani committed
200 201
  if options.get('maximum_periodicity') is not None:
    options['force_periodicity'] = True
202

Łukasz Nowak's avatar
Łukasz Nowak committed
203
  repository_required = False
Marco Mariani's avatar
Marco Mariani committed
204
  if 'key_file' in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
205
    repository_required = True
Marco Mariani's avatar
Marco Mariani committed
206
    if not 'cert_file' in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
207
      missing_mandatory_parameter_list.append('cert_file')
Marco Mariani's avatar
Marco Mariani committed
208
  if 'cert_file' in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
209
    repository_required = True
Marco Mariani's avatar
Marco Mariani committed
210
    if not 'key_file' in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
211 212
      missing_mandatory_parameter_list.append('key_file')
  if repository_required:
Marco Mariani's avatar
Marco Mariani committed
213
    if 'certificate_repository_path' not in options:
Łukasz Nowak's avatar
Łukasz Nowak committed
214 215 216 217 218 219
      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))

Marco Mariani's avatar
Marco Mariani committed
220 221 222 223
  key_file = options.get('key_file')
  cert_file = options.get('cert_file')
  master_ca_file = options.get('master_ca_file')
  signature_private_key_file = options.get('signature_private_key_file')
224 225 226 227 228 229 230

  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)

231 232
  for k in ['shacache-cert-file', 'shacache-key-file', 'shadir-cert-file',
      'shadir-key-file']:
Marco Mariani's avatar
Marco Mariani committed
233
    mandatory_file_list.append(options.get(k, None))
234

235
  for f in mandatory_file_list:
236 237
    if f is not None:
      if not os.path.exists(f):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
238
        parser.error('File %r does not exist.' % f)
Łukasz Nowak's avatar
Łukasz Nowak committed
239

Marco Mariani's avatar
Marco Mariani committed
240
  certificate_repository_path = options.get('certificate_repository_path')
Marco Mariani's avatar
Marco Mariani committed
241
  if certificate_repository_path:
Łukasz Nowak's avatar
Łukasz Nowak committed
242
    if not os.path.isdir(certificate_repository_path):
Marco Mariani's avatar
Marco Mariani committed
243
      parser.error('Directory %r does not exist' % certificate_repository_path)
Łukasz Nowak's avatar
Łukasz Nowak committed
244 245

  # Supervisord configuration location
Marco Mariani's avatar
Marco Mariani committed
246 247 248
  if not options.get('supervisord_configuration_path'):
    options['supervisord_configuration_path'] = \
      os.path.join(options['instance_root'], 'etc', 'supervisord.conf')
Łukasz Nowak's avatar
Łukasz Nowak committed
249
  # Supervisord socket
Marco Mariani's avatar
Marco Mariani committed
250 251 252
  if not options.get('supervisord_socket'):
    options['supervisord_socket'] = \
      os.path.join(options['instance_root'], 'supervisord.socket')
Yingjie Xu's avatar
Yingjie Xu committed
253 254

  signature_certificate_list_string = \
Marco Mariani's avatar
Marco Mariani committed
255
    options.get('signature-certificate-list', None)
Yingjie Xu's avatar
Yingjie Xu committed
256 257
  if signature_certificate_list_string is not None:
    cert_marker = "-----BEGIN CERTIFICATE-----"
Marco Mariani's avatar
Marco Mariani committed
258 259 260 261 262
    signature_certificate_list = [
            cert_marker + '\n' + q.strip()
            for q in signature_certificate_list_string.split(cert_marker)
            if q.strip()
            ]
Yingjie Xu's avatar
Yingjie Xu committed
263 264 265
  else:
    signature_certificate_list = None

266 267
  # Parse cache / binary cache options
  # Backward compatibility about "binary-cache-url-blacklist" deprecated option
Marco Mariani's avatar
Marco Mariani committed
268 269 270 271 272 273
  if options.get("binary-cache-url-blacklist") and not \
      options.get("download-from-binary-cache-url-blacklist"):
    options["download-from-binary-cache-url-blacklist"] = \
        options["binary-cache-url-blacklist"]
  options["download-from-binary-cache-url-blacklist"] = [
      url.strip() for url in options.get(
274
          "download-from-binary-cache-url-blacklist", "").split('\n') if url]
Marco Mariani's avatar
Marco Mariani committed
275 276
  options["upload-to-binary-cache-url-blacklist"] = [
      url.strip() for url in options.get(
277
          "upload-to-binary-cache-url-blacklist", "").split('\n') if url]
278

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

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


def realRun(argument_tuple, method_list):
Marco Mariani's avatar
Marco Mariani committed
338
  slapgrid_object, options = \
Łukasz Nowak's avatar
Łukasz Nowak committed
339
      parseArgumentTupleAndReturnSlapgridObject(*argument_tuple)
Marco Mariani's avatar
Marco Mariani committed
340
  pidfile = options.get('pidfile')
Łukasz Nowak's avatar
Łukasz Nowak committed
341 342 343
  if pidfile:
    setRunning(pidfile)
  try:
344 345
    failed = False
    failed_promise = False
Łukasz Nowak's avatar
Łukasz Nowak committed
346
    for method in method_list:
347 348 349 350 351 352
      # 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
353 354 355
  finally:
    if pidfile:
      setFinished(pidfile)
356 357 358 359 360
  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
361 362 363 364 365 366 367 368 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


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
395 396 397 398

  class PromiseError(Exception):
    pass

Łukasz Nowak's avatar
Łukasz Nowak committed
399 400 401 402 403 404 405
  def __init__(self,
               software_root,
               instance_root,
               master_url,
               computer_id,
               supervisord_socket,
               supervisord_configuration_path,
406
               buildout,
407 408
               force_periodicity=False,
               maximum_periodicity=86400,
Łukasz Nowak's avatar
Łukasz Nowak committed
409 410
               key_file=None,
               cert_file=None,
411
               signature_private_key_file=None,
Yingjie Xu's avatar
Yingjie Xu committed
412 413 414
               signature_certificate_list=None,
               download_binary_cache_url=None,
               upload_binary_cache_url=None,
415 416
               download_from_binary_cache_url_blacklist=None,
               upload_to_binary_cache_url_blacklist=None,
417
               upload_cache_url=None,
Yingjie Xu's avatar
Yingjie Xu committed
418 419
               download_binary_dir_url=None,
               upload_binary_dir_url=None,
420
               upload_dir_url=None,
Łukasz Nowak's avatar
Łukasz Nowak committed
421 422
               master_ca_file=None,
               certificate_repository_path=None,
423 424 425 426
               promise_timeout=3,
               shacache_cert_file=None,
               shacache_key_file=None,
               shadir_cert_file=None,
427
               shadir_key_file=None,
428
               develop=False,
429
               software_release_filter_list=None,
430 431
               computer_partition_filter_list=None,
               ):
Łukasz Nowak's avatar
Łukasz Nowak committed
432 433 434 435 436 437 438 439 440 441 442 443
    """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
444
    self.signature_private_key_file = signature_private_key_file
Yingjie Xu's avatar
Yingjie Xu committed
445 446 447
    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
448 449 450 451
    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
452
    self.upload_cache_url = upload_cache_url
Yingjie Xu's avatar
Yingjie Xu committed
453 454
    self.download_binary_dir_url = download_binary_dir_url
    self.upload_binary_dir_url = upload_binary_dir_url
455
    self.upload_dir_url = upload_dir_url
456 457 458 459
    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
460 461 462
    # Configures logger
    self.logger = logging.getLogger('Slapgrid')
    # Creates objects from slap module
Marco Mariani's avatar
Marco Mariani committed
463
    self.slap = slapos.slap.slap()
Łukasz Nowak's avatar
Łukasz Nowak committed
464 465 466 467 468 469 470
    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')
471
    self.buildout = buildout
472
    self.promise_timeout = promise_timeout
473
    self.develop = develop
474
    if software_release_filter_list is not None:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
475 476
      self.software_release_filter_list = \
          software_release_filter_list.split(",")
477
    else:
478
      self.software_release_filter_list = []
479 480
    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
481 482
      self.computer_partition_filter_list = \
          computer_partition_filter_list.split(",")
483 484
    self.maximum_periodicity = maximum_periodicity
    self.force_periodicity = force_periodicity
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
485 486

  def getWatchdogLine(self):
487
    invocation_list = [WATCHDOG_PATH]
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
488
    invocation_list.append("--master-url '%s' " % self.master_url)
489 490 491
    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
492 493
    invocation_list.append("--computer-id '%s'" % self.computer_id)
    return ' '.join(invocation_list)
Łukasz Nowak's avatar
Łukasz Nowak committed
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516

  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__,
Marco Mariani's avatar
Marco Mariani committed
517 518 519 520 521 522 523 524 525 526 527
          'templates/supervisord.conf.in').read() % {
              'supervisord_configuration_directory': self.supervisord_configuration_directory,
              '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',
              'watchdog_command': self.getWatchdogLine(),
              })
Łukasz Nowak's avatar
Łukasz Nowak committed
528 529 530 531 532 533 534 535
    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)
536
      raise
Łukasz Nowak's avatar
Łukasz Nowak committed
537 538 539 540 541 542 543 544
    return computer_partition_list

  def processSoftwareReleaseList(self):
    """Will process each Software Release.
    """
    self.checkEnvironmentAndCreateStructure()
    logger = logging.getLogger('SoftwareReleases')
    logger.info("Processing software releases...")
545
    # Boolean to know if every instance has correctly been deployed
Łukasz Nowak's avatar
Łukasz Nowak committed
546 547
    clean_run = True
    for software_release in self.computer.getSoftwareReleaseList():
Łukasz Nowak's avatar
Łukasz Nowak committed
548
      state = software_release.getState()
Łukasz Nowak's avatar
Łukasz Nowak committed
549 550
      try:
        software_release_uri = software_release.getURI()
Marco Mariani's avatar
Marco Mariani committed
551
        url_hash = md5digest(software_release_uri)
552
        software_path = os.path.join(self.software_root, url_hash)
Łukasz Nowak's avatar
Łukasz Nowak committed
553 554
        software = Software(url=software_release_uri,
            software_root=self.software_root,
555
            buildout=self.buildout,
556
            signature_private_key_file=self.signature_private_key_file,
Yingjie Xu's avatar
Yingjie Xu committed
557 558 559
            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,
560 561 562 563
            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,
564
            upload_cache_url=self.upload_cache_url,
Yingjie Xu's avatar
Yingjie Xu committed
565 566
            download_binary_dir_url=self.download_binary_dir_url,
            upload_binary_dir_url=self.upload_binary_dir_url,
567 568 569 570
            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
571 572
            shadir_key_file=self.shadir_key_file)
        if state == 'available':
573
          completed_tag = os.path.join(software_path, '.completed')
574
          if self.develop or (not os.path.exists(completed_tag) and \
575
                 len(self.software_release_filter_list) == 0) or \
576
                 url_hash in self.software_release_filter_list or \
Marco Mariani's avatar
Marco Mariani committed
577
                 url_hash in (md5digest(uri) for uri in self.software_release_filter_list):
578 579 580 581 582
            try:
              software_release.building()
            except NotFoundError:
              pass
            software.install()
Marco Mariani's avatar
Marco Mariani committed
583 584
            with open(completed_tag, 'w') as fout:
              fout.write(time.asctime())
Łukasz Nowak's avatar
Łukasz Nowak committed
585
        elif state == 'destroyed':
586 587 588 589
          if os.path.exists(software_path):
            logger.info('Destroying %r...' % software_release_uri)
            software.destroy()
            logger.info('Destroyed %r.' % software_release_uri)
590
      # Send log before exiting
Łukasz Nowak's avatar
Łukasz Nowak committed
591 592 593 594
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        software_release.error(exception)
        raise
595 596

      # Buildout failed: send log but don't print it to output (already done)
597
      except BuildoutFailedError as exception:
598 599 600 601 602 603 604 605 606 607 608
        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
609 610 611 612 613 614
      except Exception:
        exception = traceback.format_exc()
        logger.error(exception)
        software_release.error(exception)
        clean_run = False
      else:
Łukasz Nowak's avatar
Łukasz Nowak committed
615
        if state == 'available':
616 617 618 619
          try:
            software_release.available()
          except NotFoundError:
            pass
Łukasz Nowak's avatar
Łukasz Nowak committed
620
        elif state == 'destroyed':
621 622
          try:
            software_release.destroyed()
623 624
          except (NotFoundError, ServerError):
            print traceback.format_exc()
625
    logger.info("Finished software releases.")
626 627 628 629 630

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

632

Łukasz Nowak's avatar
Łukasz Nowak committed
633 634
  def _launchSupervisord(self):
    launchSupervisord(self.supervisord_socket,
Marco Mariani's avatar
Marco Mariani committed
635 636
                      self.supervisord_configuration_path,
                      logger=self.logger)
Łukasz Nowak's avatar
Łukasz Nowak committed
637

Antoine Catton's avatar
Antoine Catton committed
638
  def _checkPromises(self, computer_partition):
639
    self.logger.info("Checking promises...")
Marco Mariani's avatar
Marco Mariani committed
640
    instance_path = os.path.join(self.instance_root, computer_partition.getId())
Antoine Catton's avatar
Antoine Catton committed
641 642 643 644 645 646 647 648

    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

649
    promise_present = False
Antoine Catton's avatar
Antoine Catton committed
650 651 652 653 654 655 656 657
    # 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:
658
        promise_present = True
Antoine Catton's avatar
Antoine Catton committed
659

Antoine Catton's avatar
Antoine Catton committed
660 661 662
        command = [os.path.join(promise_dir, promise)]

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

665
        process_handler = subprocess.Popen(command,
Marco Mariani's avatar
Marco Mariani committed
666 667 668
                                           preexec_fn=lambda: dropPrivileges(uid, gid),
                                           cwd=cwd,
                                           env=None if sys.platform == 'cygwin' else {},
Marco Mariani's avatar
Marco Mariani committed
669 670 671
                                           stdout=subprocess.PIPE,
                                           stderr=subprocess.PIPE,
                                           stdin=subprocess.PIPE)
672 673 674
        process_handler.stdin.flush()
        process_handler.stdin.close()
        process_handler.stdin = None
Antoine Catton's avatar
Antoine Catton committed
675 676 677 678

        time.sleep(self.promise_timeout)

        if process_handler.poll() is None:
679
          process_handler.terminate()
Antoine Catton's avatar
Antoine Catton committed
680 681 682 683 684
          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
685 686
          else:
            stderr = 'Promise %r:' % promise + stderr
Antoine Catton's avatar
Antoine Catton committed
687 688
          raise Slapgrid.PromiseError(stderr)

Antoine Catton's avatar
Antoine Catton committed
689 690 691
    if not promise_present:
      self.logger.info("No promise.")

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
  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

711 712
    logger.info('Processing Computer Partition %s...' % computer_partition_id)

713 714 715
    instance_path = os.path.join(self.instance_root, computer_partition_id)

    # Try to get partition timestamp (last modification date)
716 717 718 719
    timestamp_path = os.path.join(
        instance_path,
        COMPUTER_PARTITION_TIMESTAMP_FILENAME
    )
720 721 722 723 724 725
    parameter_dict = computer_partition.getInstanceParameterDict()
    if 'timestamp' in parameter_dict:
      timestamp = parameter_dict['timestamp']
    else:
      timestamp = None

726 727 728 729 730 731 732
    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:
Marco Mariani's avatar
Marco Mariani committed
733
      software_path = os.path.join(self.software_root, md5digest(software_url))
734 735 736 737 738
    except TypeError:
      # Problem with instance: SR URI not set.
      # Try to process it anyway, it may need to be deleted.
      software_path = None

739
    periodicity = self.maximum_periodicity
740
    if software_path:
741 742
      # Get periodicity from periodicity file if not forced
      if not self.force_periodicity:
743
        periodicity_path = os.path.join(software_path, 'periodicity')
744 745
        if os.path.exists(periodicity_path):
          try:
746
            periodicity = int(open(periodicity_path).read())
747 748 749 750
          except ValueError:
            os.remove(periodicity_path)
            exception = traceback.format_exc()
            logger.error(exception)
751 752 753 754 755 756 757 758 759 760 761

    # 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):
762 763
            if computer_partition.getState() != COMPUTER_PARTITION_STARTED_STATE:
              return
764 765 766 767
            # 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):
768
              self.logger.info('Partition already up-to-date, skipping.')
769
              return
770 771 772 773
            else:
              # Periodicity forced processing this partition. Removing
              # the timestamp file in case it fails.
              os.remove(timestamp_path)
774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
        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,
792
      buildout=self.buildout)
793 794

    computer_partition_state = computer_partition.getState()
795
    if computer_partition_state == COMPUTER_PARTITION_STARTED_STATE:
796 797 798 799 800
      local_partition.install()
      computer_partition.available()
      local_partition.start()
      self._checkPromises(computer_partition)
      computer_partition.started()
801
    elif computer_partition_state == COMPUTER_PARTITION_STOPPED_STATE:
802 803 804 805 806 807 808 809
      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()
810
      computer_partition.stopped()
811
    elif computer_partition_state == COMPUTER_PARTITION_DESTROYED_STATE:
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828
      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:
829 830 831 832
      timestamp_path = os.path.join(
          instance_path,
          COMPUTER_PARTITION_TIMESTAMP_FILENAME
      )
833 834
      open(timestamp_path, 'w').write(timestamp)

835
  def FilterComputerPartitionList(self, computer_partition_list):
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
836
    """
837
    Try to filter valid partitions to be processed from free partitions.
Łukasz Nowak's avatar
Łukasz Nowak committed
838 839
    """
    logger = logging.getLogger('ComputerPartitionProcessing')
840 841
    filtered_computer_partition_list = []
    for computer_partition in computer_partition_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
842
      try:
843 844 845 846 847 848 849
        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"
850
        # partition, and check if it has some Software information.
851 852 853
        # 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()
854 855 856 857
        try:
          software_url = computer_partition.getSoftwareRelease().getURI()
        except (NotFoundError, TypeError, NameError):
          software_url = None
858
        if computer_partition_state == COMPUTER_PARTITION_DESTROYED_STATE and \
859 860
           os.listdir(computer_partition_path) == [] and \
           not software_url:
861
          continue
862

863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907
        # 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()
908 909

    # Boolean to know if every instance has correctly been deployed
910
    clean_run = True
911 912
    # Boolean to know if every promises correctly passed
    clean_run_promise = True
913 914 915 916 917 918 919 920 921 922

    # 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:
923
        # Process the partition itself
924
        self.processComputerPartition(computer_partition)
925

926
      # Send log before exiting
Łukasz Nowak's avatar
Łukasz Nowak committed
927 928 929 930
      except (SystemExit, KeyboardInterrupt):
        exception = traceback.format_exc()
        computer_partition.error(exception)
        raise
931

932 933 934
      except Slapgrid.PromiseError as exception:
        clean_run_promise = False
        try:
935
          logger.error(exception)
936 937 938 939 940 941 942 943
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

944 945 946 947 948 949 950 951 952 953 954 955 956
      # 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.
957
      except Exception as exception:
Łukasz Nowak's avatar
Łukasz Nowak committed
958
        clean_run = False
959
        logger.error(traceback.format_exc())
960 961 962 963 964 965 966 967 968
        try:
          computer_partition.error(exception)
        except (SystemExit, KeyboardInterrupt):
          raise
        except Exception:
          exception = traceback.format_exc()
          logger.error('Problem during reporting error, continuing:\n' +
            exception)

969
    logger.info("Finished computer partitions.")
970 971 972 973 974 975 976

    # 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
977

978

979 980
  def validateXML(self, to_be_validated, xsd_model):
    """Validates a given xml file"""
Łukasz Nowak's avatar
Łukasz Nowak committed
981

982 983 984 985 986
    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
987 988
    xmlschema = etree.XMLSchema(xmlschema_doc)

989
    try:
990
      document = etree.fromstring(to_be_validated)
991
    except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
992 993 994
      logger.info('Failed to parse this XML report :  %s\n%s' % \
        (to_be_validated, _formatXMLError(e)))
      logger.error(_formatXMLError(e))
995 996
      return False

Łukasz Nowak's avatar
Łukasz Nowak committed
997 998 999 1000 1001
    if xmlschema.validate(document):
      return True

    return False

1002 1003 1004
  def asXML(self, computer_partition_usage_list):
    """Generates a XML report from computer partition usage list
    """
1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018
    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>']
1019 1020 1021

    for computer_partition_usage in computer_partition_usage_list:
      try:
1022
        root = etree.fromstring(computer_partition_usage.usage)
1023
      except UnicodeError, e:
1024 1025 1026
        self.logger.info("Failed to read %s." % (
            computer_partition_usage.usage))
        self.logger.error(UnicodeError)
1027
        raise UnicodeError("Failed to read %s: %s" % (computer_partition_usage.usage, e))
1028
      except (etree.XMLSyntaxError, etree.DocumentInvalid) as e:
Cédric de Saint Martin's avatar
YATTA  
Cédric de Saint Martin committed
1029
        self.logger.info("Failed to parse %s." % (computer_partition_usage.usage))
1030
        self.logger.error(e)
1031
        raise _formatXMLError(e)
1032 1033
      except Exception, e:
        raise Exception("Failed to generate XML report: %s" % e)
1034 1035

      for movement in root.findall('movement'):
1036 1037 1038 1039
        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))
1040
          else:
1041 1042
            xml.append('<%s>%s</%s>' % (child.tag, child.text, child.tag))
        xml.append('</movement>')
1043

1044
    xml.append('</transaction></journal>')
1045

1046
    return ''.join(xml)
1047

Łukasz Nowak's avatar
Łukasz Nowak committed
1048 1049 1050
  def agregateAndSendUsage(self):
    """Will agregate usage from each Computer Partition.
    """
1051 1052 1053 1054
    # Prepares environment
    self.checkEnvironmentAndCreateStructure()
    self._launchSupervisord()

Łukasz Nowak's avatar
Łukasz Nowak committed
1055 1056 1057 1058 1059
    slap_computer_usage = self.slap.registerComputer(self.computer_id)
    computer_partition_usage_list = []
    logger = logging.getLogger('UsageReporting')
    logger.info("Aggregating and sending usage reports...")

1060 1061 1062 1063 1064 1065 1066 1067 1068
    #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(
1069
          __name__,
1070 1071 1072 1073 1074 1075 1076 1077 1078 1079
          '../../../../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(
1080
          __name__,
1081 1082
          '../../../../slapos/slap/doc/partition_consumption.xsd')

Łukasz Nowak's avatar
Łukasz Nowak committed
1083
    clean_run = True
1084 1085 1086
    # Loop on the different computer partitions
    computer_partition_list = self.FilterComputerPartitionList(
       slap_computer_usage.getComputerPartitionList())
1087

1088
    for computer_partition in computer_partition_list:
1089 1090
      try:
        computer_partition_id = computer_partition.getId()
1091

1092 1093 1094 1095 1096 1097 1098 1099
        #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 = []
Marco Mariani's avatar
Marco Mariani committed
1100

1101 1102 1103 1104 1105 1106
        #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)
Marco Mariani's avatar
Marco Mariani committed
1107

1108 1109 1110 1111 1112 1113 1114 1115 1116
        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)
Marco Mariani's avatar
Marco Mariani committed
1117

1118 1119 1120 1121 1122 1123 1124 1125
          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
          process_handler = SlapPopen(invocation_list,
Marco Mariani's avatar
Marco Mariani committed
1126 1127 1128
                                      preexec_fn=lambda: dropPrivileges(uid, gid),
                                      cwd=os.path.join(instance_path, 'etc', 'report'),
                                      env=None,
Marco Mariani's avatar
Marco Mariani committed
1129 1130
                                      stdout=subprocess.PIPE,
                                      stderr=subprocess.STDOUT)
1131 1132 1133 1134
          if process_handler.returncode is None:
            process_handler.kill()
          if process_handler.returncode != 0:
            clean_run = False
1135 1136
            failed_script_list.append("Script %r failed." % script)
            logger.warning("Failed to run %r" % invocation_list)
1137 1138 1139 1140 1141 1142 1143 1144 1145
          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
1146

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
1147
    #Now we loop through the different computer partitions to report
Łukasz Nowak's avatar
Łukasz Nowak committed
1148
    report_usage_issue_cp_list = []
1149
    for computer_partition in computer_partition_list:
1150 1151 1152 1153 1154 1155 1156 1157 1158
      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
1159
        else:
1160 1161 1162
          filename_list = []
        #logger.debug('name List %s' % filename_list)
        usage = ''
Marco Mariani's avatar
Marco Mariani committed
1163

1164
        for filename in filename_list:
Marco Mariani's avatar
Marco Mariani committed
1165

1166 1167
          file_path = os.path.join(dir_reports, filename)
          if os.path.exists(file_path):
Marco Mariani's avatar
Marco Mariani committed
1168
            usage = open(file_path, 'r').read()
Marco Mariani's avatar
Marco Mariani committed
1169

1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184
            #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
1185

1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
        #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)
1197 1198

    for computer_partition_usage in computer_partition_usage_list:
Marco Mariani's avatar
Marco Mariani committed
1199
      logger.info('computer_partition_usage_list: %s - %s' % \
1200 1201 1202 1203
        (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
1204
      try:
1205 1206 1207
        #We generate the final XML report with asXML method
        computer_consumption = self.asXML(computer_partition_usage_list)

Marco Mariani's avatar
Marco Mariani committed
1208
        logger.info('Final xml report: %s' % computer_consumption)
1209 1210 1211 1212 1213 1214 1215

        #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 !')
1216
          raise ValueError('XML file generated by asXML is not valid !')
Łukasz Nowak's avatar
Łukasz Nowak committed
1217 1218 1219 1220 1221 1222 1223 1224 1225
      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)

1226
    for computer_partition in computer_partition_list:
1227
      if computer_partition.getState() == COMPUTER_PARTITION_DESTROYED_STATE:
Łukasz Nowak's avatar
Łukasz Nowak committed
1228
        try:
1229
          computer_partition_id = computer_partition.getId()
1230
          try:
1231
             software_url = computer_partition.getSoftwareRelease().getURI()
Marco Mariani's avatar
Marco Mariani committed
1232
             software_path = os.path.join(self.software_root, md5digest(software_url))
1233 1234 1235
          except (NotFoundError, TypeError):
            software_url = None
            software_path = None
1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
          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,
1250
            buildout=self.buildout,
1251
            )
Łukasz Nowak's avatar
Łukasz Nowak committed
1252 1253 1254 1255 1256 1257 1258 1259 1260
          local_partition.stop()
          try:
            computer_partition.stopped()
          except (SystemExit, KeyboardInterrupt):
            exception = traceback.format_exc()
            computer_partition.error(exception)
            raise
          except Exception:
            pass
1261 1262 1263 1264 1265
          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
1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
        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()
Marco Mariani's avatar
Marco Mariani committed
1277
        except NotFoundError:
Łukasz Nowak's avatar
Łukasz Nowak committed
1278 1279 1280
          logger.debug('Ignored slap error while trying to inform about '
              'destroying not fully configured Computer Partition %r' %
                  computer_partition.getId())
1281 1282
        except ServerError as server_error:
          logger.debug('Ignored server error while trying to inform about '
Marco Mariani's avatar
Marco Mariani committed
1283
              'destroying Computer Partition %r. Error is:\n%r' %
1284
                  (computer_partition.getId(), server_error.args[0]))
Łukasz Nowak's avatar
Łukasz Nowak committed
1285

1286
    logger.info("Finished usage reports.")
1287 1288 1289 1290 1291

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