SlapObject.py 26.9 KB
Newer Older
1
# -*- coding: utf-8 -*-
Marco Mariani's avatar
Marco Mariani committed
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

Łukasz Nowak's avatar
Łukasz Nowak committed
31 32
import os
import pkg_resources
Marco Mariani's avatar
typos  
Marco Mariani committed
33
import pwd
Marco Mariani's avatar
Marco Mariani committed
34
import shutil
Łukasz Nowak's avatar
Łukasz Nowak committed
35
import stat
Marco Mariani's avatar
Marco Mariani committed
36
import subprocess
Marco Mariani's avatar
typos  
Marco Mariani committed
37
import tarfile
38
import tempfile
39
import textwrap
Marco Mariani's avatar
typos  
Marco Mariani committed
40 41 42
import xmlrpclib

from supervisor import xmlrpc
Marco Mariani's avatar
Marco Mariani committed
43

44 45
from slapos.grid.utils import (md5digest, getCleanEnvironment,
                               SlapPopen, dropPrivileges, updateFile)
Marco Mariani's avatar
Marco Mariani committed
46
from slapos.grid import utils  # for methods that could be mocked, access them through the module
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
47
from slapos.slap.slap import NotFoundError
Marco Mariani's avatar
Marco Mariani committed
48 49 50 51 52
from slapos.grid.svcbackend import getSupervisorRPC
from slapos.grid.exception import (BuildoutFailedError, WrongPermissionError,
                                   PathDoesNotExistError)
from slapos.grid.networkcache import download_network_cached, upload_network_cached
from slapos.grid.watchdog import getWatchdogID
Łukasz Nowak's avatar
Łukasz Nowak committed
53

Marco Mariani's avatar
Marco Mariani committed
54
REQUIRED_COMPUTER_PARTITION_PERMISSION = 0o750
Łukasz Nowak's avatar
Łukasz Nowak committed
55

56 57 58
# XXX not very clean. this is changed when testing
PROGRAM_PARTITION_TEMPLATE = pkg_resources.resource_stream(__name__,
            'templates/program_partition_supervisord.conf.in').read()
Łukasz Nowak's avatar
Łukasz Nowak committed
59

60

Łukasz Nowak's avatar
Łukasz Nowak committed
61
class Software(object):
Marco Mariani's avatar
typos  
Marco Mariani committed
62
  """This class is responsible for installing a software release"""
Marco Mariani's avatar
Marco Mariani committed
63

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
64 65
  # XXX: "url" parameter should be named "key", "target" or alike to be more generic.
  #      The key is an url in the case of Buildout.
Marco Mariani's avatar
Marco Mariani committed
66
  def __init__(self, url, software_root, buildout, logger,
Marco Mariani's avatar
Marco Mariani committed
67 68 69 70 71
               signature_private_key_file=None, signature_certificate_list=None,
               upload_cache_url=None, upload_dir_url=None, shacache_cert_file=None,
               shacache_key_file=None, shadir_cert_file=None, shadir_key_file=None,
               download_binary_cache_url=None, upload_binary_cache_url=None,
               download_binary_dir_url=None, upload_binary_dir_url=None,
Marco Mariani's avatar
Marco Mariani committed
72 73
               download_from_binary_cache_url_blacklist=None,
               upload_to_binary_cache_url_blacklist=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
74 75
    """Initialisation of class parameters
    """
Marco Mariani's avatar
Marco Mariani committed
76 77 78 79 80 81 82

    if download_from_binary_cache_url_blacklist is None:
      download_from_binary_cache_url_blacklist = []

    if upload_to_binary_cache_url_blacklist is None:
      upload_to_binary_cache_url_blacklist = []

Łukasz Nowak's avatar
Łukasz Nowak committed
83 84
    self.url = url
    self.software_root = software_root
85
    self.software_url_hash = md5digest(self.url)
Łukasz Nowak's avatar
Łukasz Nowak committed
86
    self.software_path = os.path.join(self.software_root,
Yingjie Xu's avatar
Yingjie Xu committed
87
                                      self.software_url_hash)
88
    self.buildout = buildout
Marco Mariani's avatar
Marco Mariani committed
89
    self.logger = logger
90
    self.signature_private_key_file = signature_private_key_file
Yingjie Xu's avatar
Yingjie Xu committed
91
    self.signature_certificate_list = signature_certificate_list
92 93
    self.upload_cache_url = upload_cache_url
    self.upload_dir_url = upload_dir_url
94 95 96 97
    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
Yingjie Xu's avatar
Yingjie Xu committed
98 99 100 101
    self.download_binary_cache_url = download_binary_cache_url
    self.upload_binary_cache_url = upload_binary_cache_url
    self.download_binary_dir_url = download_binary_dir_url
    self.upload_binary_dir_url = upload_binary_dir_url
102 103 104 105
    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
Łukasz Nowak's avatar
Łukasz Nowak committed
106

107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
  def download_binary_cache(self, tarpath):
    """
    returns True after a successful download
    """
    return download_network_cached(
        self.download_binary_cache_url,
        self.download_binary_dir_url,
        self.url,
        self.software_root,
        self.software_url_hash,
        tarpath,
        self.logger,
        self.signature_certificate_list,
        self.download_from_binary_cache_url_blacklist
    )

Łukasz Nowak's avatar
Łukasz Nowak committed
123
  def install(self):
Yingjie Xu's avatar
Yingjie Xu committed
124 125 126
    """ Fetches binary cache if possible.
    Installs from buildout otherwise.
    """
127
    self.logger.info("Installing software release %s..." % self.url)
Yingjie Xu's avatar
Yingjie Xu committed
128
    cache_dir = tempfile.mkdtemp()
129 130 131
    try:
      tarpath = os.path.join(cache_dir, self.software_url_hash)
      # Check if we can download from cache
132 133
      if (not os.path.exists(self.software_path)
            and self.download_binary_cache(tarpath)):
134 135
        self.logger.info("Extracting archive of cached software release...")
        tarfile.open(tarpath).extractall(self.software_root)
136 137
      else:
        self._install_from_buildout()
138
        self.update_binary_cache(tarpath)
139 140
    finally:
      shutil.rmtree(cache_dir)
141

142 143 144 145 146 147 148 149 150 151 152 153 154
  def _set_ownership(self, path):
    """
    If running as root: copy ownership of software_root to path
    If not running as root: do nothing
    """
    if os.getuid():
      return
    root_stat = os.stat(self.software_root)
    path_stat = os.stat(path)
    if (root_stat.st_uid != path_stat.st_uid or
          root_stat.st_gid != path_stat.st_gid):
      os.chown(path, root_stat.st_uid, root_stat.st_gid)

155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
  def _additional_buildout_parameters(self, extends_cache):
    yield 'buildout:extends-cache=%s' % extends_cache
    yield 'buildout:directory=%s' % self.software_path

    if (self.signature_private_key_file or
          self.upload_cache_url or
          self.upload_dir_url):
      yield 'buildout:networkcache-section=networkcache'

    for networkcache_option, value in [
        ('signature-private-key-file', self.signature_private_key_file),
        ('upload-cache-url', self.upload_cache_url),
        ('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),
        ('shadir-key-file', self.shadir_key_file)
    ]:
      if value:
        yield 'networkcache:%s=%s' % (networkcache_option, value)

Yingjie Xu's avatar
Yingjie Xu committed
176
  def _install_from_buildout(self):
Łukasz Nowak's avatar
Łukasz Nowak committed
177 178 179
    """ Fetches buildout configuration from the server, run buildout with
    it. If it fails, we notify the server.
    """
180
    root_stat = os.stat(self.software_root)
Marco Mariani's avatar
Marco Mariani committed
181
    os.environ = getCleanEnvironment(logger=self.logger,
182
                                     home_path=pwd.getpwuid(root_stat.st_uid).pw_dir)
Łukasz Nowak's avatar
Łukasz Nowak committed
183 184
    if not os.path.isdir(self.software_path):
      os.mkdir(self.software_path)
185 186
      self._set_ownership(self.software_path)

187
    extends_cache = tempfile.mkdtemp()
188 189
    self._set_ownership(extends_cache)

190
    try:
191
      buildout_cfg = os.path.join(self.software_path, 'buildout.cfg')
192 193 194 195 196
      if not os.path.exists(buildout_cfg):
        self._create_buildout_profile(buildout_cfg, self.url)

      additional_parameters = list(self._additional_buildout_parameters(extends_cache))
      additional_parameters.extend(['-c', buildout_cfg])
197

Marco Mariani's avatar
Marco Mariani committed
198 199 200
      utils.bootstrapBuildout(path=self.software_path,
                              buildout=self.buildout,
                              logger=self.logger,
201 202
                              additional_buildout_parameter_list=additional_parameters)

Marco Mariani's avatar
Marco Mariani committed
203 204 205
      utils.launchBuildout(path=self.software_path,
                           buildout_binary=os.path.join(self.software_path, 'bin', 'buildout'),
                           logger=self.logger,
206
                           additional_buildout_parameter_list=additional_parameters)
207 208
    finally:
      shutil.rmtree(extends_cache)
Łukasz Nowak's avatar
Łukasz Nowak committed
209

210 211 212 213 214 215 216 217 218 219
  def _create_buildout_profile(self, buildout_cfg, url):
    with open(buildout_cfg, 'wb') as fout:
      fout.write(textwrap.dedent("""\
          # Created by slapgrid. extends {url}
          # but you can change it for development purposes.

          [buildout]
          extends = {url}
          """.format(url=url)))
    self._set_ownership(buildout_cfg)
220

221 222 223 224 225 226 227 228 229 230 231 232 233
  def update_binary_cache(self, tarpath):
    """
    Upload to binary cache if possible and allowed
    """
    if all([self.software_root, self.url, self.software_url_hash,
            self.upload_binary_cache_url, self.upload_binary_dir_url]):
      if any(self.url.startswith(url)
              for url in self.upload_to_binary_cache_url_blacklist):
          self.logger.info("Can't upload to binary cache: "
                           "Software Release URL is blacklisted.")
      else:
        self.uploadSoftwareRelease(tarpath)

234 235 236 237 238 239
  def uploadSoftwareRelease(self, tarpath):
    """
    Try to tar and upload an installed Software Release.
    """
    self.logger.info("Creating archive of software release...")
    tar = tarfile.open(tarpath, "w:gz")
240 241
    tar.add(self.software_path, arcname=self.software_url_hash)
    tar.close()
242 243 244 245 246 247 248 249 250 251 252 253 254
    self.logger.info("Trying to upload archive of software release...")
    upload_network_cached(
        self.software_root,
        self.url, self.software_url_hash,
        self.upload_binary_cache_url,
        self.upload_binary_dir_url,
        tarpath, self.logger,
        self.signature_private_key_file,
        self.shacache_cert_file,
        self.shacache_key_file,
        self.shadir_cert_file,
        self.shadir_key_file)

Łukasz Nowak's avatar
Łukasz Nowak committed
255 256 257
  def destroy(self):
    """Removes software release."""
    def retry(func, path, exc):
Marco Mariani's avatar
typos  
Marco Mariani committed
258
      # inspired by slapos.buildout hard remover
Łukasz Nowak's avatar
Łukasz Nowak committed
259 260 261
      if func == os.path.islink:
        os.unlink(path)
      else:
Marco Mariani's avatar
Marco Mariani committed
262
        os.chmod(path, 0o600)
Łukasz Nowak's avatar
Łukasz Nowak committed
263
        func(path)
Łukasz Nowak's avatar
Łukasz Nowak committed
264
    try:
265
      if os.path.exists(self.software_path):
Łukasz Nowak's avatar
Łukasz Nowak committed
266
        self.logger.info('Removing path %r' % self.software_path)
267
        shutil.rmtree(self.software_path, onerror=retry)
Łukasz Nowak's avatar
Łukasz Nowak committed
268 269 270
      else:
        self.logger.info('Path %r does not exists, no need to remove.' %
            self.software_path)
271 272
    except IOError as exc:
      raise IOError("I/O error while removing software (%s): %s" % (self.url, exc))
Łukasz Nowak's avatar
Łukasz Nowak committed
273 274 275


class Partition(object):
Marco Mariani's avatar
Marco Mariani committed
276
  """This class is responsible of the installation of an instance
Łukasz Nowak's avatar
Łukasz Nowak committed
277
  """
Marco Mariani's avatar
Marco Mariani committed
278

Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
279 280
  # XXX: we should give the url (or the "key") instead of the software_path
  #      then compute the path from it, like in Software.
Łukasz Nowak's avatar
Łukasz Nowak committed
281 282 283 284 285 286 287 288 289 290
  def __init__(self,
               software_path,
               instance_path,
               supervisord_partition_configuration_path,
               supervisord_socket,
               computer_partition,
               computer_id,
               partition_id,
               server_url,
               software_release_url,
291
               buildout,
Marco Mariani's avatar
Marco Mariani committed
292
               logger,
Łukasz Nowak's avatar
Łukasz Nowak committed
293 294 295
               certificate_repository_path=None,
               ):
    """Initialisation of class parameters"""
296
    self.buildout = buildout
Marco Mariani's avatar
Marco Mariani committed
297
    self.logger = logger
Łukasz Nowak's avatar
Łukasz Nowak committed
298 299 300
    self.software_path = software_path
    self.instance_path = instance_path
    self.run_path = os.path.join(self.instance_path, 'etc', 'run')
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
301
    self.service_path = os.path.join(self.instance_path, 'etc', 'service')
Łukasz Nowak's avatar
Łukasz Nowak committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
    self.supervisord_partition_configuration_path = \
        supervisord_partition_configuration_path
    self.supervisord_socket = supervisord_socket
    self.computer_partition = computer_partition
    self.computer_id = computer_id
    self.partition_id = partition_id
    self.server_url = server_url
    self.software_release_url = software_release_url

    self.key_file = ''
    self.cert_file = ''
    if certificate_repository_path is not None:
      self.key_file = os.path.join(certificate_repository_path,
          self.partition_id + '.key')
      self.cert_file = os.path.join(certificate_repository_path,
          self.partition_id + '.crt')
      self._updateCertificate()

  def _updateCertificate(self):
321 322 323 324 325 326 327 328
    try:
      partition_certificate = self.computer_partition.getCertificate()
    except NotFoundError:
      raise NotFoundError('Partition %s is not known by SlapOS Master.' %
          self.partition_id)

    uid, gid = self.getUserGroupId()

Marco Mariani's avatar
Marco Mariani committed
329
    for name, path in [('certificate', self.cert_file), ('key', self.key_file)]:
330 331 332 333 334 335 336 337 338 339 340 341 342 343 344
      new_content = partition_certificate[name]
      old_content = None
      if os.path.exists(path):
        old_content = open(path).read()

      if old_content != new_content:
        if old_content is None:
          self.logger.info('Missing %s file. Creating %r' % (name, path))
        else:
          self.logger.info('Changed %s content. Updating %r' % (name, path))

        with os.fdopen(os.open(path, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o400), 'wb') as fout:
          fout.write(new_content)
        os.chown(path, uid, gid)

Łukasz Nowak's avatar
Łukasz Nowak committed
345
  def getUserGroupId(self):
346
    """Returns (uid, gid) tuple of partition owner"""
Łukasz Nowak's avatar
Łukasz Nowak committed
347 348 349
    stat_info = os.stat(self.instance_path)
    uid = stat_info.st_uid
    gid = stat_info.st_gid
350
    return uid, gid
Łukasz Nowak's avatar
Łukasz Nowak committed
351

Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
352
  def addServiceToGroup(self, partition_id,
Marco Mariani's avatar
Marco Mariani committed
353
                        runner_list, path, extension=''):
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
354 355 356
    uid, gid = self.getUserGroupId()
    for runner in runner_list:
      self.partition_supervisor_configuration += '\n' + \
357
          PROGRAM_PARTITION_TEMPLATE % {
Marco Mariani's avatar
Marco Mariani committed
358 359 360 361 362 363 364 365 366 367 368
              'program_id': '_'.join([partition_id, runner]),
              'program_directory': self.instance_path,
              'program_command': os.path.join(path, runner),
              'program_name': runner + extension,
              'instance_path': self.instance_path,
              'user_id': uid,
              'group_id': gid,
              # As supervisord has no environment to inherit, setup a minimalistic one
              'HOME': pwd.getpwuid(uid).pw_dir,
              'USER': pwd.getpwuid(uid).pw_name,
          }
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
369

370 371 372 373 374 375 376 377 378
  @property
  def software_release_link(self):
    return os.path.join(self.instance_path, 'software_release')

  def update_link_to_sr(self):
    link = self.software_release_link
    if os.path.lexists(link):
      if not os.path.islink(link):
        self.logger.debug('Not a symlink: %s, has been ignored', link)
379
        return
380 381 382 383 384 385 386 387 388
      os.unlink(link)
    os.symlink(self.software_path, link)
    uid, gid = self.getUserGroupId()
    os.lchown(link, uid, gid)

  def remove_link_to_sr(self):
    link = self.software_release_link
    if os.path.islink(link):
      os.unlink(link)
389

Łukasz Nowak's avatar
Łukasz Nowak committed
390 391 392 393
  def install(self):
    """ Creates configuration file from template in software_path, then
    installs the software partition with the help of buildout
    """
Marco Mariani's avatar
Marco Mariani committed
394
    self.logger.info("Installing Computer Partition %s..."
Łukasz Nowak's avatar
Łukasz Nowak committed
395 396 397 398 399 400
        % self.computer_partition.getId())
    # Checks existence and permissions of Partition directory
    # Note : Partitions have to be created and configured before running slapgrid
    if not os.path.isdir(self.instance_path):
      raise PathDoesNotExistError('Please create partition directory %s'
                                           % self.instance_path)
401

402
    self.update_link_to_sr()
403

404
    instance_stat_info = os.stat(self.instance_path)
Marco Mariani's avatar
Marco Mariani committed
405
    permission = stat.S_IMODE(instance_stat_info.st_mode)
Łukasz Nowak's avatar
Łukasz Nowak committed
406
    if permission != REQUIRED_COMPUTER_PARTITION_PERMISSION:
Marco Mariani's avatar
Marco Mariani committed
407 408 409 410
      raise WrongPermissionError('Wrong permissions in %s: actual '
                                 'permissions are: 0%o, wanted are 0%o' %
                                 (self.instance_path, permission,
                                  REQUIRED_COMPUTER_PARTITION_PERMISSION))
Marco Mariani's avatar
Marco Mariani committed
411 412
    os.environ = getCleanEnvironment(logger=self.logger,
                                     home_path=pwd.getpwuid(instance_stat_info.st_uid).pw_dir)
413

414 415 416 417
    # Check that Software Release directory is present
    if not os.path.exists(self.software_path):
      # XXX What should it raise?
      raise IOError('Software Release %s is not present on system.\n'
Marco Mariani's avatar
Marco Mariani committed
418
                    'Cannot deploy instance.' % self.software_release_url)
419

420
    # Generate buildout instance profile from template in Software Release
421 422
    instance_cfg = os.path.join(self.software_path, 'instance.cfg')
    if not os.path.exists(instance_cfg):
423 424
      # Backward compatibility: "instance.cfg" file was named "template.cfg".
      if os.path.exists(os.path.join(self.software_path, 'template.cfg')):
425
        instance_cfg = os.path.join(self.software_path, 'template.cfg')
426 427 428 429
      else:
        # No template: Software Release is either inconsistent or not correctly installed.
        # XXX What should it raise?
        raise IOError('Software Release %s is not correctly installed.\nMissing file: %s' % (
430 431 432 433 434 435 436 437 438 439 440
            self.software_release_url, instance_cfg))

    buildout_cfg = os.path.join(self.instance_path, 'buildout.cfg')
    self.logger.debug("Copying %r to %r" % (instance_cfg, buildout_cfg))

    with open(buildout_cfg, 'w') as fout:
        fout.write(open(instance_cfg).read())
        fout.write('\n\n')
        # fill generated buildout with additional information
        fout.write(pkg_resources.resource_string(__name__,
          'templates/buildout-tail.cfg.in') % {
Marco Mariani's avatar
Marco Mariani committed
441 442 443 444 445 446
            'computer_id': self.computer_id,
            'partition_id': self.partition_id,
            'server_url': self.server_url,
            'software_release_url': self.software_release_url,
            'key_file': self.key_file,
            'cert_file': self.cert_file,
447 448 449
          })
    os.chmod(buildout_cfg, 0o640)

Łukasz Nowak's avatar
Łukasz Nowak committed
450 451 452 453 454 455 456 457 458 459 460 461
    # Try to find the best possible buildout:
    #  *) if software_root/bin/bootstrap exists use this one to bootstrap
    #     locally
    #  *) as last resort fallback to buildout binary from software_path
    bootstrap_candidate_dir = os.path.abspath(os.path.join(self.software_path,
      'bin'))
    if os.path.isdir(bootstrap_candidate_dir):
      bootstrap_candidate_list = [q for q in os.listdir(bootstrap_candidate_dir)
        if q.startswith('bootstrap')]
    else:
      bootstrap_candidate_list = []
    uid, gid = self.getUserGroupId()
462
    os.chown(buildout_cfg, -1, int(gid))
Łukasz Nowak's avatar
Łukasz Nowak committed
463 464 465 466 467 468
    if len(bootstrap_candidate_list) == 0:
      buildout_binary = os.path.join(self.software_path, 'bin', 'buildout')
      self.logger.warning("Falling back to default buildout %r" %
        buildout_binary)
    else:
      if len(bootstrap_candidate_list) != 1:
Marco Mariani's avatar
typos  
Marco Mariani committed
469
        raise ValueError('More than one bootstrap candidate found.')
Łukasz Nowak's avatar
Łukasz Nowak committed
470 471 472 473
      # Reads uid/gid of path, launches buildout with thoses privileges
      bootstrap_file = os.path.abspath(os.path.join(bootstrap_candidate_dir,
        bootstrap_candidate_list[0]))

Marco Mariani's avatar
Marco Mariani committed
474
      first_line = open(bootstrap_file, 'r').readline()
Łukasz Nowak's avatar
Łukasz Nowak committed
475
      invocation_list = []
Marco Mariani's avatar
Marco Mariani committed
476 477
      if first_line.startswith('#!'):
        invocation_list = first_line[2:].split()
Łukasz Nowak's avatar
Łukasz Nowak committed
478
      invocation_list.append(bootstrap_file)
Marco Mariani's avatar
Marco Mariani committed
479

Łukasz Nowak's avatar
Łukasz Nowak committed
480 481
      self.logger.debug('Invoking %r in %r' % (' '.join(invocation_list),
        self.instance_path))
Marco Mariani's avatar
Marco Mariani committed
482
      process_handler = SlapPopen(invocation_list,
Marco Mariani's avatar
Marco Mariani committed
483
                                  preexec_fn=lambda: dropPrivileges(uid, gid, logger=self.logger),
Marco Mariani's avatar
Marco Mariani committed
484
                                  cwd=self.instance_path,
Marco Mariani's avatar
Marco Mariani committed
485 486
                                  env=getCleanEnvironment(logger=self.logger,
                                                          home_path=pwd.getpwuid(uid).pw_dir),
Marco Mariani's avatar
Marco Mariani committed
487
                                  stdout=subprocess.PIPE,
Marco Mariani's avatar
Marco Mariani committed
488 489
                                  stderr=subprocess.STDOUT,
                                  logger=self.logger)
Łukasz Nowak's avatar
Łukasz Nowak committed
490
      if process_handler.returncode is None or process_handler.returncode != 0:
491
        message = 'Failed to bootstrap buildout in %r.' % (self.instance_path)
492 493
        self.logger.error(message)
        raise BuildoutFailedError('%s:\n%s\n' % (message, process_handler.output))
Łukasz Nowak's avatar
Łukasz Nowak committed
494 495 496 497
      buildout_binary = os.path.join(self.instance_path, 'sbin', 'buildout')

    if not os.path.exists(buildout_binary):
      # use own buildout generation
Marco Mariani's avatar
Marco Mariani committed
498 499 500 501 502 503
      utils.bootstrapBuildout(path=self.instance_path,
                              buildout=self.buildout,
                              logger=self.logger,
                              additional_buildout_parameter_list=
                                ['buildout:bin-directory=%s' %
                                    os.path.join(self.instance_path, 'sbin')])
Łukasz Nowak's avatar
Łukasz Nowak committed
504
      buildout_binary = os.path.join(self.instance_path, 'sbin', 'buildout')
505

Marco Mariani's avatar
Marco Mariani committed
506 507 508
    utils.launchBuildout(path=self.instance_path,
                         buildout_binary=buildout_binary,
                         logger=self.logger)
509 510 511 512 513 514 515 516 517 518 519
    self.generateSupervisorConfigurationFile()

  def generateSupervisorConfigurationFile(self):
    """
    Generates supervisord configuration file from template.

    check if CP/etc/run exists and it is a directory
    iterate over each file in CP/etc/run
    iterate over each file in CP/etc/service adding WatchdogID to their name
    if at least one is not 0o750 raise -- partition has something funny
    """
Łukasz Nowak's avatar
Łukasz Nowak committed
520
    runner_list = []
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
521
    service_list = []
Łukasz Nowak's avatar
Łukasz Nowak committed
522 523 524
    if os.path.exists(self.run_path):
      if os.path.isdir(self.run_path):
        runner_list = os.listdir(self.run_path)
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
525 526 527 528 529
    if os.path.exists(self.service_path):
      if os.path.isdir(self.service_path):
        service_list = os.listdir(self.service_path)
    if len(runner_list) == 0 and len(service_list) == 0:
      self.logger.warning('No runners nor services found for partition %r' %
Łukasz Nowak's avatar
Łukasz Nowak committed
530 531 532 533 534 535 536
          self.partition_id)
      if os.path.exists(self.supervisord_partition_configuration_path):
        os.unlink(self.supervisord_partition_configuration_path)
    else:
      partition_id = self.computer_partition.getId()
      group_partition_template = pkg_resources.resource_stream(__name__,
          'templates/group_partition_supervisord.conf.in').read()
Marco Mariani's avatar
Marco Mariani committed
537
      self.partition_supervisor_configuration = group_partition_template % {
Marco Mariani's avatar
Marco Mariani committed
538 539 540 541
          'instance_id': partition_id,
          'program_list': ','.join(['_'.join([partition_id, runner])
                                    for runner in runner_list + service_list])
      }
Marco Mariani's avatar
Marco Mariani committed
542
      # Same method to add to service and run
Marco Mariani's avatar
Marco Mariani committed
543 544
      self.addServiceToGroup(partition_id, runner_list, self.run_path)
      self.addServiceToGroup(partition_id, service_list, self.service_path,
Cédric Le Ninivin's avatar
Cédric Le Ninivin committed
545
                             extension=getWatchdogID())
Marco Mariani's avatar
Marco Mariani committed
546
      updateFile(self.supervisord_partition_configuration_path,
Marco Mariani's avatar
Marco Mariani committed
547
                 self.partition_supervisor_configuration)
Łukasz Nowak's avatar
Łukasz Nowak committed
548 549 550 551 552 553
    self.updateSupervisor()

  def start(self):
    """Asks supervisord to start the instance. If this instance is not
    installed, we install it.
    """
554
    supervisor = getSupervisorRPC(self.supervisord_socket)
Łukasz Nowak's avatar
Łukasz Nowak committed
555
    partition_id = self.computer_partition.getId()
556 557
    try:
      supervisor.startProcessGroup(partition_id, False)
558 559
    except xmlrpclib.Fault as exc:
      if exc.faultString.startswith('BAD_NAME:'):
Marco Mariani's avatar
Marco Mariani committed
560
        self.logger.info("Nothing to start on %s..." %
561 562 563
                         self.computer_partition.getId())
    else:
      self.logger.info("Requested start of %s..." % self.computer_partition.getId())
Łukasz Nowak's avatar
Łukasz Nowak committed
564 565 566 567 568

  def stop(self):
    """Asks supervisord to stop the instance."""
    partition_id = self.computer_partition.getId()
    try:
569
      supervisor = getSupervisorRPC(self.supervisord_socket)
Łukasz Nowak's avatar
Łukasz Nowak committed
570
      supervisor.stopProcessGroup(partition_id, False)
571 572
    except xmlrpclib.Fault as exc:
      if exc.faultString.startswith('BAD_NAME:'):
Łukasz Nowak's avatar
Łukasz Nowak committed
573 574 575 576
        self.logger.info('Partition %s not known in supervisord, ignoring' % partition_id)
    else:
      self.logger.info("Requested stop of %s..." % self.computer_partition.getId())

577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592
  def _pre_destroy(self, destroy_binary):
    uid, gid = self.getUserGroupId()
    self.logger.debug('Invoking %r', destroy_binary)
    process_handler = SlapPopen([destroy_binary],
                                preexec_fn=lambda: dropPrivileges(uid, gid, logger=self.logger),
                                cwd=self.instance_path,
                                env=getCleanEnvironment(logger=self.logger,
                                                        home_path=pwd.getpwuid(uid).pw_dir),
                                stdout=subprocess.PIPE,
                                stderr=subprocess.STDOUT,
                                logger=self.logger)
    if process_handler.returncode is None or process_handler.returncode != 0:
      message = 'Failed to destroy Computer Partition in %r.' % self.instance_path
      self.logger.error(message)
      raise subprocess.CalledProcessError(message, process_handler.output)

Łukasz Nowak's avatar
Łukasz Nowak committed
593 594 595
  def destroy(self):
    """Destroys the partition and makes it available for subsequent use."
    """
Marco Mariani's avatar
Marco Mariani committed
596
    self.logger.info("Destroying Computer Partition %s..."
Łukasz Nowak's avatar
Łukasz Nowak committed
597
        % self.computer_partition.getId())
598

Łukasz Nowak's avatar
Łukasz Nowak committed
599
    # Launches "destroy" binary if exists
600 601 602
    destroy_binary = os.path.join(self.instance_path, 'sbin', 'destroy')
    if os.path.exists(destroy_binary):
      self._pre_destroy(destroy_binary)
603

Łukasz Nowak's avatar
Łukasz Nowak committed
604 605 606 607 608 609
    # Manually cleans what remains
    try:
      for f in [self.key_file, self.cert_file]:
        if f:
          if os.path.exists(f):
            os.unlink(f)
Marco Mariani's avatar
Marco Mariani committed
610 611

      # better to manually remove symlinks because rmtree might choke on them
612
      self.remove_link_to_sr()
Marco Mariani's avatar
Marco Mariani committed
613

Łukasz Nowak's avatar
Łukasz Nowak committed
614 615 616 617 618
      for root, dirs, file_list in os.walk(self.instance_path):
        for directory in dirs:
          shutil.rmtree(os.path.join(self.instance_path, directory))
        for file in file_list:
          os.remove(os.path.join(self.instance_path, file))
619 620 621 622

      if os.path.exists(self.supervisord_partition_configuration_path):
        os.remove(self.supervisord_partition_configuration_path)
      self.updateSupervisor()
623 624
    except IOError as exc:
      raise IOError("I/O error while freeing partition (%s): %s" % (self.instance_path, exc))
Łukasz Nowak's avatar
Łukasz Nowak committed
625 626 627 628 629 630 631 632 633

  def fetchInformations(self):
    """Fetch usage informations with buildout, returns it.
    """
    raise NotImplementedError

  def updateSupervisor(self):
    """Forces supervisord to reload its configuration"""
    # Note: This method shall wait for results from supervisord
634
    #       In future it will not be needed, as update command
Łukasz Nowak's avatar
Łukasz Nowak committed
635 636
    #       is going to be implemented on server side.
    self.logger.debug('Updating supervisord')
637
    supervisor = getSupervisorRPC(self.supervisord_socket)
Łukasz Nowak's avatar
Łukasz Nowak committed
638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664
    # took from supervisord.supervisorctl.do_update
    result = supervisor.reloadConfig()
    added, changed, removed = result[0]

    for gname in removed:
      results = supervisor.stopProcessGroup(gname)
      fails = [res for res in results
               if res['status'] == xmlrpc.Faults.FAILED]
      if fails:
        self.logger.warning('Problem while stopping process %r, will try later' % gname)
      else:
        self.logger.info('Stopped %r' % gname)
      supervisor.removeProcessGroup(gname)
      self.logger.info('Removed %r' % gname)

    for gname in changed:
      results = supervisor.stopProcessGroup(gname)
      self.logger.info('Stopped %r' % gname)

      supervisor.removeProcessGroup(gname)
      supervisor.addProcessGroup(gname)
      self.logger.info('Updated %r' % gname)

    for gname in added:
      supervisor.addProcessGroup(gname)
      self.logger.info('Updated %r' % gname)
    self.logger.debug('Supervisord updated')