format.py 42.9 KB
Newer Older
Łukasz Nowak's avatar
Łukasz Nowak committed
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 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly advised to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 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
import ConfigParser
32 33
import errno
import fcntl
Łukasz Nowak's avatar
Łukasz Nowak committed
34
import grp
35
import json
Łukasz Nowak's avatar
Łukasz Nowak committed
36 37 38 39
import logging
import netaddr
import netifaces
import os
Łukasz Nowak's avatar
Łukasz Nowak committed
40
import pwd
Łukasz Nowak's avatar
Łukasz Nowak committed
41
import random
42
import shutil
Łukasz Nowak's avatar
Łukasz Nowak committed
43
import socket
44
import struct
Łukasz Nowak's avatar
Łukasz Nowak committed
45 46
import subprocess
import sys
47
import threading
Łukasz Nowak's avatar
Łukasz Nowak committed
48
import time
49
import traceback
Marco Mariani's avatar
Marco Mariani committed
50 51 52
import zipfile

import lxml.etree
Marco Mariani's avatar
Marco Mariani committed
53
import xml_marshaller.xml_marshaller
Marco Mariani's avatar
Marco Mariani committed
54

55
from slapos.util import chownDirectory
Marco Mariani's avatar
Marco Mariani committed
56 57
from slapos.util import mkdir_p
import slapos.slap as slap
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
58 59


Marco Mariani's avatar
Marco Mariani committed
60 61 62 63
def prettify_xml(xml):
  root = lxml.etree.fromstring(xml)
  return lxml.etree.tostring(root, pretty_print=True)

Łukasz Nowak's avatar
Łukasz Nowak committed
64

Vincent Pelletier's avatar
Vincent Pelletier committed
65
class OS(object):
66 67
  """Wrap parts of the 'os' module to provide logging of performed actions."""

Vincent Pelletier's avatar
Vincent Pelletier committed
68 69
  _os = os

70 71 72
  def __init__(self, conf):
    self._dry_run = conf.dry_run
    self._logger = conf.logger
Vincent Pelletier's avatar
Vincent Pelletier committed
73 74 75 76 77 78 79 80
    add = self._addWrapper
    add('chown')
    add('chmod')
    add('makedirs')
    add('mkdir')

  def _addWrapper(self, name):
    def wrapper(*args, **kw):
Marco Mariani's avatar
Marco Mariani committed
81
      arg_list = [repr(x) for x in args] + [
82 83
          '%s=%r' % (x, y) for x, y in kw.iteritems()
      ]
84
      self._logger.debug('%s(%s)', name, ', '.join(arg_list))
85 86
      if not self._dry_run:
        getattr(self._os, name)(*args, **kw)
Vincent Pelletier's avatar
Vincent Pelletier committed
87 88 89 90
    setattr(self, name, wrapper)

  def __getattr__(self, name):
    return getattr(self._os, name)
Łukasz Nowak's avatar
Łukasz Nowak committed
91

92

93 94 95
class UsageError(Exception):
  pass

96

97
class NoAddressOnInterface(Exception):
Łukasz Nowak's avatar
Łukasz Nowak committed
98
  """
Marco Mariani's avatar
Marco Mariani committed
99
  Exception raised if there is no address on the interface to construct IPv6
100 101 102
  address with.

  Attributes:
103
    brige: String, the name of the interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
104 105
  """

106 107
  def __init__(self, interface):
    super(NoAddressOnInterface, self).__init__(
Marco Mariani's avatar
Marco Mariani committed
108
      'No IPv6 found on interface %s to construct IPv6 with.' % interface
109
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
110

111

112 113 114
class AddressGenerationError(Exception):
  """
  Exception raised if the generation of an IPv6 based on the prefix obtained
115
  from the interface failed.
116 117 118 119 120 121 122 123

  Attributes:
    addr: String, the invalid address the exception is raised for.
  """
  def __init__(self, addr):
    super(AddressGenerationError, self).__init__(
      'Generated IPv6 %s seems not to be a valid IP.' % addr
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
124

125

Łukasz Nowak's avatar
Łukasz Nowak committed
126
def callAndRead(argument_list, raise_on_error=True):
Marco Mariani's avatar
Marco Mariani committed
127 128 129
  popen = subprocess.Popen(argument_list,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
Łukasz Nowak's avatar
Łukasz Nowak committed
130 131
  result = popen.communicate()[0]
  if raise_on_error and popen.returncode != 0:
132
    raise ValueError('Issue while invoking %r, result was:\n%s' % (
Marco Mariani's avatar
Marco Mariani committed
133
                     argument_list, result))
Łukasz Nowak's avatar
Łukasz Nowak committed
134 135
  return popen.returncode, result

136

Łukasz Nowak's avatar
Łukasz Nowak committed
137 138 139 140 141 142
def isGlobalScopeAddress(a):
  """Returns True if a is global scope IP v4/6 address"""
  ip = netaddr.IPAddress(a)
  return not ip.is_link_local() and not ip.is_loopback() and \
      not ip.is_reserved() and ip.is_unicast()

143

Łukasz Nowak's avatar
Łukasz Nowak committed
144 145 146 147 148
def netmaskToPrefixIPv4(netmask):
  """Convert string represented netmask to its integer prefix"""
  return netaddr.strategy.ipv4.netmask_to_prefix[
          netaddr.strategy.ipv4.str_to_int(netmask)]

149

Łukasz Nowak's avatar
Łukasz Nowak committed
150 151 152 153 154
def netmaskToPrefixIPv6(netmask):
  """Convert string represented netmask to its integer prefix"""
  return netaddr.strategy.ipv6.netmask_to_prefix[
          netaddr.strategy.ipv6.str_to_int(netmask)]

155

156
def _getDict(obj):
Łukasz Nowak's avatar
Łukasz Nowak committed
157
  """
158
  Serialize an object into dictionaries. List and dict will remains
Łukasz Nowak's avatar
Łukasz Nowak committed
159 160 161 162
  the same, basic type too. But encapsulated object will be returned as dict.
  Set, collections and other aren't handle for now.

  Args:
163
    obj: an object of any type.
Łukasz Nowak's avatar
Łukasz Nowak committed
164 165 166 167

  Returns:
    A dictionary if the given object wasn't a list, a list otherwise.
  """
168 169
  if isinstance(obj, list):
    return [_getDict(item) for item in obj]
Łukasz Nowak's avatar
Łukasz Nowak committed
170

171 172
  if isinstance(obj, dict):
    dikt = obj
Łukasz Nowak's avatar
Łukasz Nowak committed
173 174
  else:
    try:
175
      dikt = obj.__dict__
Łukasz Nowak's avatar
Łukasz Nowak committed
176
    except AttributeError:
177 178 179
      return obj

  return {
180 181 182 183 184
    key: _getDict(value)
    for key, value in dikt.iteritems()
    # do not attempt to serialize logger: it is both useless and recursive.
    if not isinstance(value, logging.Logger)
  }
Łukasz Nowak's avatar
Łukasz Nowak committed
185

186

Vincent Pelletier's avatar
Vincent Pelletier committed
187
class Computer(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
188
  "Object representing the computer"
189 190
  instance_root = None
  software_root = None
Łukasz Nowak's avatar
Łukasz Nowak committed
191

192
  def __init__(self, reference, interface=None, addr=None, netmask=None,
Marco Mariani's avatar
Marco Mariani committed
193
               ipv6_interface=None, software_user='slapsoft'):
Łukasz Nowak's avatar
Łukasz Nowak committed
194 195 196
    """
    Attributes:
      reference: String, the reference of the computer.
197
      interface: String, the name of the computer's used interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
198 199
    """
    self.reference = str(reference)
200
    self.interface = interface
Łukasz Nowak's avatar
Łukasz Nowak committed
201 202 203
    self.partition_list = []
    self.address = addr
    self.netmask = netmask
Łukasz Nowak's avatar
Łukasz Nowak committed
204
    self.ipv6_interface = ipv6_interface
205
    self.software_user = software_user
Łukasz Nowak's avatar
Łukasz Nowak committed
206 207

  def __getinitargs__(self):
208
    return (self.reference, self.interface)
Łukasz Nowak's avatar
Łukasz Nowak committed
209

210
  def getAddress(self, allow_tap=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
211
    """
Marco Mariani's avatar
Marco Mariani committed
212
    Return a list of the interface address not attributed to any partition (which
Łukasz Nowak's avatar
Łukasz Nowak committed
213 214 215
    are therefore free for the computer itself).

    Returns:
216
      False if the interface isn't available, else the list of the free addresses.
Łukasz Nowak's avatar
Łukasz Nowak committed
217
    """
218
    if self.interface is None:
Marco Mariani's avatar
Marco Mariani committed
219
      return {'addr': self.address, 'netmask': self.netmask}
Łukasz Nowak's avatar
Łukasz Nowak committed
220 221 222 223 224 225

    computer_partition_address_list = []
    for partition in self.partition_list:
      for address in partition.address_list:
        if netaddr.valid_ipv6(address['addr']):
          computer_partition_address_list.append(address['addr'])
226
    # Going through addresses of the computer's interface
227
    for address_dict in self.interface.getGlobalScopeAddressList():
Łukasz Nowak's avatar
Łukasz Nowak committed
228 229 230 231
      # Comparing with computer's partition addresses
      if address_dict['addr'] not in computer_partition_address_list:
        return address_dict

232
    if allow_tap:
Marco Mariani's avatar
Marco Mariani committed
233
      # all addresses on interface are for partition, so let's add new one
234 235 236 237 238 239
      computer_tap = Tap('compdummy')
      computer_tap.createWithOwner(User('root'), attach_to_tap=True)
      self.interface.addTap(computer_tap)
      return self.interface.addAddr()

    # Can't find address
Marco Mariani's avatar
Marco Mariani committed
240
    raise NoAddressOnInterface('No valid IPv6 found on %s.' % self.interface.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
241

242
  def send(self, conf):
Łukasz Nowak's avatar
Łukasz Nowak committed
243 244 245 246 247 248
    """
    Send a marshalled dictionary of the computer object serialized via_getDict.
    """

    slap_instance = slap.slap()
    connection_dict = {}
249 250 251 252
    if conf.key_file and conf.cert_file:
      connection_dict['key_file'] = conf.key_file
      connection_dict['cert_file'] = conf.cert_file
    slap_instance.initializeConnection(conf.master_url,
Marco Mariani's avatar
Marco Mariani committed
253
                                       **connection_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
254
    slap_computer = slap_instance.registerComputer(self.reference)
Marco Mariani's avatar
Marco Mariani committed
255

256
    if conf.dry_run:
257
      return
258
    try:
Marco Mariani's avatar
Marco Mariani committed
259
      slap_computer.updateConfiguration(xml_marshaller.xml_marshaller.dumps(_getDict(self)))
260
    except slap.NotFoundError as error:
261 262 263
      raise slap.NotFoundError("%s\nERROR: This SlapOS node is not recognised by "
          "SlapOS Master and/or computer_id and certificates don't match. "
          "Please make sure computer_id of slapos.cfg looks "
264
          "like 'COMP-123' and is correct.\nError is : 404 Not Found." % error)
Łukasz Nowak's avatar
Łukasz Nowak committed
265

266
  def dump(self, path_to_xml, path_to_json, logger):
Łukasz Nowak's avatar
Łukasz Nowak committed
267 268 269 270 271
    """
    Dump the computer object to an xml file via xml_marshaller.

    Args:
      path_to_xml: String, path to the file to load.
Marco Mariani's avatar
Marco Mariani committed
272
      path_to_json: String, path to the JSON version to save.
Łukasz Nowak's avatar
Łukasz Nowak committed
273 274 275
    """

    computer_dict = _getDict(self)
276 277 278 279 280

    if path_to_json:
      with open(path_to_json, 'wb') as fout:
        fout.write(json.dumps(computer_dict, sort_keys=True, indent=2))

Marco Mariani's avatar
Marco Mariani committed
281
    new_xml = xml_marshaller.xml_marshaller.dumps(computer_dict)
Marco Mariani's avatar
Marco Mariani committed
282 283 284 285
    new_pretty_xml = prettify_xml(new_xml)

    path_to_archive = path_to_xml + '.zip'

286
    if os.path.exists(path_to_archive) and os.path.exists(path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
287 288 289 290 291 292
      # the archive file exists, we only backup if something has changed
      with open(path_to_xml, 'rb') as fin:
        if fin.read() == new_pretty_xml:
          # computer configuration did not change, nothing to write
          return

293
    if os.path.exists(path_to_xml):
294 295 296 297 298
      try:
        self.backup_xml(path_to_archive, path_to_xml)
      except:
        # might be a corrupted zip file. let's move it out of the way and retry.
        shutil.move(path_to_archive,
Marco Mariani's avatar
Marco Mariani committed
299
                    path_to_archive + time.strftime('_broken_%Y%m%d-%H:%M'))
300 301 302 303
        try:
          self.backup_xml(path_to_archive, path_to_xml)
        except:
          # give up trying
304
          logger.exception("Can't backup %s:", path_to_xml)
Marco Mariani's avatar
Marco Mariani committed
305

Marco Mariani's avatar
Marco Mariani committed
306 307
    with open(path_to_xml, 'wb') as fout:
      fout.write(new_pretty_xml)
Marco Mariani's avatar
Marco Mariani committed
308 309

  def backup_xml(self, path_to_archive, path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
310 311 312
    """
    Stores a copy of the current xml file to an historical archive.
    """
Marco Mariani's avatar
Marco Mariani committed
313
    xml_content = open(path_to_xml).read()
Marco Mariani's avatar
typo  
Marco Mariani committed
314
    saved_filename = os.path.basename(path_to_xml) + time.strftime('.%Y%m%d-%H:%M')
Marco Mariani's avatar
Marco Mariani committed
315 316 317 318

    with zipfile.ZipFile(path_to_archive, 'a') as archive:
      archive.writestr(saved_filename, xml_content, zipfile.ZIP_DEFLATED)

Łukasz Nowak's avatar
Łukasz Nowak committed
319
  @classmethod
Łukasz Nowak's avatar
Łukasz Nowak committed
320
  def load(cls, path_to_xml, reference, ipv6_interface):
Łukasz Nowak's avatar
Łukasz Nowak committed
321 322 323 324 325 326 327 328
    """
    Create a computer object from a valid xml file.

    Arg:
      path_to_xml: String, a path to a valid file containing
          a valid configuration.

    Return:
329
      A Computer object.
Łukasz Nowak's avatar
Łukasz Nowak committed
330 331
    """

Marco Mariani's avatar
Marco Mariani committed
332
    dumped_dict = xml_marshaller.xml_marshaller.loads(open(path_to_xml).read())
Łukasz Nowak's avatar
Łukasz Nowak committed
333 334 335

    # Reconstructing the computer object from the xml
    computer = Computer(
336 337 338 339 340
        reference=reference,
        addr=dumped_dict['address'],
        netmask=dumped_dict['netmask'],
        ipv6_interface=ipv6_interface,
        software_user=dumped_dict.get('software_user', 'slapsoft'),
Łukasz Nowak's avatar
Łukasz Nowak committed
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
    )

    for partition_dict in dumped_dict['partition_list']:

      if partition_dict['user']:
        user = User(partition_dict['user']['name'])
      else:
        user = User('root')

      if partition_dict['tap']:
        tap = Tap(partition_dict['tap']['name'])
      else:
        tap = Tap(partition_dict['reference'])

      address_list = partition_dict['address_list']

      partition = Partition(
358 359 360 361 362
          reference=partition_dict['reference'],
          path=partition_dict['path'],
          user=user,
          address_list=address_list,
          tap=tap,
Łukasz Nowak's avatar
Łukasz Nowak committed
363 364 365 366 367 368
      )

      computer.partition_list.append(partition)

    return computer

369
  def construct(self, alter_user=True, alter_network=True, create_tap=True):
Łukasz Nowak's avatar
Łukasz Nowak committed
370 371 372 373
    """
    Construct the computer object as it is.
    """
    if alter_network and self.address is not None:
374
      self.interface.addAddr(self.address, self.netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
375

376
    for path in self.instance_root, self.software_root:
Łukasz Nowak's avatar
Łukasz Nowak committed
377
      if not os.path.exists(path):
Marco Mariani's avatar
Marco Mariani committed
378
        os.makedirs(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
379
      else:
Marco Mariani's avatar
Marco Mariani committed
380
        os.chmod(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
381

382 383
    # own self.software_root by software user
    slapsoft = User(self.software_user)
Łukasz Nowak's avatar
Łukasz Nowak committed
384 385 386
    slapsoft.path = self.software_root
    if alter_user:
      slapsoft.create()
Łukasz Nowak's avatar
Łukasz Nowak committed
387
      slapsoft_pw = pwd.getpwnam(slapsoft.name)
388
      chownDirectory(slapsoft.path, slapsoft_pw.pw_uid, slapsoft_pw.pw_gid)
Marco Mariani's avatar
Marco Mariani committed
389
    os.chmod(self.software_root, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
390

391 392 393
    # Speed hack:
    # Blindly add all IPs from existing configuration, just to speed up actual
    # computer configuration later on.
394 395 396 397
    # XXX-TODO: only add an address if it doesn't already exist.
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
398
      interface_name = self.interface.name
399 400 401 402 403 404 405 406 407
    for partition in self.partition_list:
      try:
        for address in partition.address_list:
          try:
            netmask = netmaskToPrefixIPv6(address['netmask'])
          except:
            continue
          callAndRead(['ip', 'addr', 'add',
                       '%s/%s' % (address['addr'], netmask),
408
                       'dev', interface_name])
409 410 411
      except ValueError:
        pass

412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
    try:
      for partition_index, partition in enumerate(self.partition_list):
        # Reconstructing User's
        partition.path = os.path.join(self.instance_root, partition.reference)
        partition.user.setPath(partition.path)
        partition.user.additional_group_list = [slapsoft.name]
        if alter_user:
          partition.user.create()

        # Reconstructing Tap
        if partition.user and partition.user.isAvailable():
          owner = partition.user
        else:
          owner = User('root')

427
        if alter_network and create_tap:
428
          # In case it has to be  attached to the TAP network device, only one
429 430
          # is necessary for the interface to assert carrier
          if self.interface.attach_to_tap and partition_index == 0:
431
            partition.tap.createWithOwner(owner, attach_to_tap=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
432
          else:
433 434
            partition.tap.createWithOwner(owner)

435
          self.interface.addTap(partition.tap)
436 437 438 439 440 441 442 443

        # Reconstructing partition's directory
        partition.createPath(alter_user)

        # Reconstructing partition's address
        # There should be two addresses on each Computer Partition:
        #  * global IPv6
        #  * local IPv4, took from slapformat:ipv4_local_network
Marco Mariani's avatar
Marco Mariani committed
444
        if not partition.address_list:
445
          # regenerate
446 447
          partition.address_list.append(self.interface.addIPv4LocalAddress())
          partition.address_list.append(self.interface.addAddr())
448 449 450 451 452
        elif alter_network:
          # regenerate list of addresses
          old_partition_address_list = partition.address_list
          partition.address_list = []
          if len(old_partition_address_list) != 2:
453 454 455
            raise ValueError(
              'There should be exactly 2 stored addresses. Got: %r' %
              (old_partition_address_list,))
Marco Mariani's avatar
Marco Mariani committed
456 457
          if not any(netaddr.valid_ipv6(q['addr'])
                     for q in old_partition_address_list):
458
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
459 460
          if not any(netaddr.valid_ipv4(q['addr'])
                     for q in old_partition_address_list):
461
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
462

463 464
          for address in old_partition_address_list:
            if netaddr.valid_ipv6(address['addr']):
465
              partition.address_list.append(self.interface.addAddr(
Vincent Pelletier's avatar
Vincent Pelletier committed
466 467
                address['addr'],
                address['netmask']))
468
            elif netaddr.valid_ipv4(address['addr']):
469
              partition.address_list.append(self.interface.addIPv4LocalAddress(
Vincent Pelletier's avatar
Vincent Pelletier committed
470
                address['addr']))
471 472 473
            else:
              raise ValueError('Address %r is incorrect' % address['addr'])
    finally:
474
      if alter_network and create_tap and self.interface.attach_to_tap:
475 476 477 478
        try:
          self.partition_list[0].tap.detach()
        except IndexError:
          pass
Łukasz Nowak's avatar
Łukasz Nowak committed
479

480

Vincent Pelletier's avatar
Vincent Pelletier committed
481
class Partition(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504
  "Represent a computer partition"

  def __init__(self, reference, path, user, address_list, tap):
    """
    Attributes:
      reference: String, the name of the partition.
      path: String, the path to the partition folder.
      user: User, the user linked to this partition.
      address_list: List of associated IP addresses.
      tap: Tap, the tap interface linked to this partition.
    """

    self.reference = str(reference)
    self.path = str(path)
    self.user = user
    self.address_list = address_list or []
    self.tap = tap

  def __getinitargs__(self):
    return (self.reference, self.path, self.user, self.address_list, self.tap)

  def createPath(self, alter_user=True):
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
505 506
    Create the directory of the partition, assign to the partition user and
    give it the 750 permission. In case if path exists just modifies it.
Łukasz Nowak's avatar
Łukasz Nowak committed
507 508
    """

509
    self.path = os.path.abspath(self.path)
Łukasz Nowak's avatar
Łukasz Nowak committed
510
    owner = self.user if self.user else User('root')
511
    if not os.path.exists(self.path):
Marco Mariani's avatar
Marco Mariani committed
512
      os.mkdir(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
513
    if alter_user:
Łukasz Nowak's avatar
Łukasz Nowak committed
514
      owner_pw = pwd.getpwnam(owner.name)
515
      chownDirectory(self.path, owner_pw.pw_uid, owner_pw.pw_gid)
516
    os.chmod(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
517

518

Vincent Pelletier's avatar
Vincent Pelletier committed
519
class User(object):
Marco Mariani's avatar
Marco Mariani committed
520 521
  """User: represent and manipulate a user on the system."""

522
  path = None
Łukasz Nowak's avatar
Łukasz Nowak committed
523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548

  def __init__(self, user_name, additional_group_list=None):
    """
    Attributes:
        user_name: string, the name of the user, who will have is home in
    """
    self.name = str(user_name)
    self.additional_group_list = additional_group_list

  def __getinitargs__(self):
    return (self.name,)

  def setPath(self, path):
    self.path = path

  def create(self):
    """
    Create a user on the system who will be named after the self.name with its
    own group and directory.

    Returns:
        True: if the user creation went right
    """
    # XXX: This method shall be no-op in case if all is correctly setup
    #      This method shall check if all is correctly done
    #      This method shall not reset groups, just add them
Jondy Zhao's avatar
Jondy Zhao committed
549
    grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
550
    try:
Jondy Zhao's avatar
Jondy Zhao committed
551
      grp.getgrnam(grpname)
Łukasz Nowak's avatar
Łukasz Nowak committed
552
    except KeyError:
Jondy Zhao's avatar
Jondy Zhao committed
553
      callAndRead(['groupadd', grpname])
Łukasz Nowak's avatar
Łukasz Nowak committed
554

Vincent Pelletier's avatar
Vincent Pelletier committed
555 556
    user_parameter_list = ['-d', self.path, '-g', self.name, '-s',
      '/bin/false']
Łukasz Nowak's avatar
Łukasz Nowak committed
557 558 559 560
    if self.additional_group_list is not None:
      user_parameter_list.extend(['-G', ','.join(self.additional_group_list)])
    user_parameter_list.append(self.name)
    try:
Łukasz Nowak's avatar
Łukasz Nowak committed
561
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
562
    except KeyError:
563
      user_parameter_list.append('-r')
Łukasz Nowak's avatar
Łukasz Nowak committed
564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579
      callAndRead(['useradd'] + user_parameter_list)
    else:
      callAndRead(['usermod'] + user_parameter_list)

    return True

  def isAvailable(self):
    """
    Determine the availability of a user on the system

    Return:
        True: if available
        False: otherwise
    """

    try:
Łukasz Nowak's avatar
Łukasz Nowak committed
580
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
581 582 583 584
      return True
    except KeyError:
      return False

585

Vincent Pelletier's avatar
Vincent Pelletier committed
586
class Tap(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
587
  "Tap represent a tap interface on the system"
588 589 590
  IFF_TAP = 0x0002
  TUNSETIFF = 0x400454ca
  KEEP_TAP_ATTACHED_EVENT = threading.Event()
Łukasz Nowak's avatar
Łukasz Nowak committed
591 592 593 594 595 596 597 598 599 600 601 602

  def __init__(self, tap_name):
    """
    Attributes:
        tap_name: String, the name of the tap interface.
    """

    self.name = str(tap_name)

  def __getinitargs__(self):
    return (self.name,)

603 604 605
  def attach(self):
    """
    Attach to the TAP interface, meaning  that it just opens the TAP interface
Marco Mariani's avatar
Marco Mariani committed
606
    and waits for the caller to notify that it can be safely detached.
607 608 609 610 611 612 613 614

    Linux  distinguishes administrative  and operational  state of  an network
    interface.  The  former can be set  manually by running ``ip  link set dev
    <dev> up|down'', whereas the latter states that the interface can actually
    transmit  data (for  a wired  network interface,  it basically  means that
    there is  carrier, e.g.  the network  cable is plugged  into a  switch for
    example).

615
    In case of bridge:
616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633
    In order  to be able to check  the uniqueness of IPv6  address assigned to
    the bridge, the network interface  must be up from an administrative *and*
    operational point of view.

    However,  from  Linux  2.6.39,  the  bridge  reflects  the  state  of  the
    underlying device (e.g.  the bridge asserts carrier if at least one of its
    ports has carrier) whereas it  always asserted carrier before. This should
    work fine for "real" network interface,  but will not work properly if the
    bridge only binds TAP interfaces, which, from 2.6.36, reports carrier only
    and only if an userspace program is attached.
    """
    tap_fd = os.open("/dev/net/tun", os.O_RDWR)

    try:
      # Attach to the TAP interface which has previously been created
      fcntl.ioctl(tap_fd, self.TUNSETIFF,
                  struct.pack("16sI", self.name, self.IFF_TAP))

634
    except IOError as error:
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655
      # If  EBUSY, it  means another  program is  already attached,  thus just
      # ignore it...
      if error.errno != errno.EBUSY:
        os.close(tap_fd)
        raise
    else:
      # Block until the  caller send an event stating that  the program can be
      # now detached safely,  thus bringing down the TAP  device (from 2.6.36)
      # and the bridge at the same time (from 2.6.39)
      self.KEEP_TAP_ATTACHED_EVENT.wait()
    finally:
      os.close(tap_fd)

  def detach(self):
    """
    Detach to the  TAP network interface by notifying  the thread which attach
    to the TAP and closing the TAP file descriptor
    """
    self.KEEP_TAP_ATTACHED_EVENT.set()

  def createWithOwner(self, owner, attach_to_tap=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
656 657 658 659 660 661 662 663 664
    """
    Create a tap interface on the system.
    """

    # some systems does not have -p switch for tunctl
    #callAndRead(['tunctl', '-p', '-t', self.name, '-u', owner.name])
    check_file = '/sys/devices/virtual/net/%s/owner' % self.name
    owner_id = None
    if os.path.exists(check_file):
665
      owner_id = open(check_file).read().strip()
Łukasz Nowak's avatar
Łukasz Nowak committed
666
      try:
667 668
        owner_id = int(owner_id)
      except ValueError:
Łukasz Nowak's avatar
Łukasz Nowak committed
669
        pass
670
    if owner_id != pwd.getpwnam(owner.name).pw_uid:
Łukasz Nowak's avatar
Łukasz Nowak committed
671 672 673
      callAndRead(['tunctl', '-t', self.name, '-u', owner.name])
    callAndRead(['ip', 'link', 'set', self.name, 'up'])

674 675 676
    if attach_to_tap:
      threading.Thread(target=self.attach).start()

677

678
class Interface(object):
Marco Mariani's avatar
Marco Mariani committed
679
  """Represent a network interface on the system"""
Łukasz Nowak's avatar
Łukasz Nowak committed
680

681
  def __init__(self, logger, name, ipv4_local_network, ipv6_interface=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
682 683
    """
    Attributes:
684
        name: String, the name of the interface
Łukasz Nowak's avatar
Łukasz Nowak committed
685 686
    """

Marco Mariani's avatar
Marco Mariani committed
687
    self.logger = logger
Łukasz Nowak's avatar
Łukasz Nowak committed
688 689
    self.name = str(name)
    self.ipv4_local_network = ipv4_local_network
Łukasz Nowak's avatar
Łukasz Nowak committed
690
    self.ipv6_interface = ipv6_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
691

692
    # Attach to TAP  network interface, only if the  interface interface does not
693
    # report carrier
694
    _, result = callAndRead(['ip', 'addr', 'list', self.name])
695 696
    self.attach_to_tap = 'DOWN' in result.split('\n', 1)[0]

697
  # XXX no __getinitargs__, as instances of this class are never deserialized.
Łukasz Nowak's avatar
Łukasz Nowak committed
698 699

  def getIPv4LocalAddressList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
700 701 702 703
    """
    Returns currently configured local IPv4 addresses which are in
    ipv4_local_network
    """
Łukasz Nowak's avatar
Łukasz Nowak committed
704 705
    if not socket.AF_INET in netifaces.ifaddresses(self.name):
      return []
Marco Mariani's avatar
Marco Mariani committed
706 707 708 709 710 711 712 713 714
    return [
            {
                'addr': q['addr'],
                'netmask': q['netmask']
                }
            for q in netifaces.ifaddresses(self.name)[socket.AF_INET]
            if netaddr.IPAddress(q['addr'], 4) in netaddr.glob_to_iprange(
                netaddr.cidr_to_glob(self.ipv4_local_network))
            ]
Łukasz Nowak's avatar
Łukasz Nowak committed
715 716 717

  def getGlobalScopeAddressList(self):
    """Returns currently configured global scope IPv6 addresses"""
Łukasz Nowak's avatar
Łukasz Nowak committed
718 719 720 721
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
722
    try:
Marco Mariani's avatar
Marco Mariani committed
723
      address_list = [
724 725 726 727
          q
          for q in netifaces.ifaddresses(interface_name)[socket.AF_INET6]
          if isGlobalScopeAddress(q['addr'].split('%')[0])
      ]
728
    except KeyError:
729
      raise ValueError("%s must have at least one IPv6 address assigned" %
Łukasz Nowak's avatar
Łukasz Nowak committed
730
                         interface_name)
Jondy Zhao's avatar
Jondy Zhao committed
731 732 733
    if sys.platform == 'cygwin':
      for q in address_list:
        q.setdefault('netmask', 'FFFF:FFFF:FFFF:FFFF::')
Łukasz Nowak's avatar
Łukasz Nowak committed
734 735 736 737 738 739 740 741 742 743
    # XXX: Missing implementation of Unique Local IPv6 Unicast Addresses as
    # defined in http://www.rfc-editor.org/rfc/rfc4193.txt
    # XXX: XXX: XXX: IT IS DISALLOWED TO IMPLEMENT link-local addresses as
    # Linux and BSD are possibly wrongly implementing it -- it is "too local"
    # it is impossible to listen or access it on same node
    # XXX: IT IS DISALLOWED to implement ad hoc solution like inventing node
    # local addresses or anything which does not exists in RFC!
    return address_list

  def getInterfaceList(self):
744
    """Returns list of interfaces already present on bridge"""
Łukasz Nowak's avatar
Łukasz Nowak committed
745
    interface_list = []
746
    _, result = callAndRead(['brctl', 'show'])
747
    in_interface = False
Łukasz Nowak's avatar
Łukasz Nowak committed
748 749 750 751
    for line in result.split('\n'):
      if len(line.split()) > 1:
        if self.name in line:
          interface_list.append(line.split()[-1])
752
          in_interface = True
Łukasz Nowak's avatar
Łukasz Nowak committed
753
          continue
754
        if in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
755
          break
756
      elif in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772
        if line.strip():
          interface_list.append(line.strip())

    return interface_list

  def addTap(self, tap):
    """
    Add the tap interface tap to the bridge.

    Args:
      tap: Tap, the tap interface.
    """
    if tap.name not in self.getInterfaceList():
      callAndRead(['brctl', 'addif', self.name, tap.name])

  def _addSystemAddress(self, address, netmask, ipv6=True):
773
    """Adds system address to interface
774

Łukasz Nowak's avatar
Łukasz Nowak committed
775 776 777 778 779 780 781
    Returns True if address was added successfully.

    Returns False if there was issue.
    """
    if ipv6:
      address_string = '%s/%s' % (address, netmaskToPrefixIPv6(netmask))
      af = socket.AF_INET6
Łukasz Nowak's avatar
Łukasz Nowak committed
782 783 784 785
      if self.ipv6_interface:
        interface_name = self.ipv6_interface
      else:
        interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
786 787 788
    else:
      af = socket.AF_INET
      address_string = '%s/%s' % (address, netmaskToPrefixIPv4(netmask))
Łukasz Nowak's avatar
Łukasz Nowak committed
789
      interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
790 791 792

    # check if address is already took by any other interface
    for interface in netifaces.interfaces():
Łukasz Nowak's avatar
Łukasz Nowak committed
793
      if interface != interface_name:
Łukasz Nowak's avatar
Łukasz Nowak committed
794 795
        address_dict = netifaces.ifaddresses(interface)
        if af in address_dict:
796
          if address in [q['addr'].split('%')[0] for q in address_dict[af]]:
Łukasz Nowak's avatar
Łukasz Nowak committed
797 798
            return False

Vincent Pelletier's avatar
Vincent Pelletier committed
799 800
    if not af in netifaces.ifaddresses(interface_name) \
        or not address in [q['addr'].split('%')[0]
Marco Mariani's avatar
Marco Mariani committed
801 802
                           for q in netifaces.ifaddresses(interface_name)[af]
                           ]:
Łukasz Nowak's avatar
Łukasz Nowak committed
803
      # add an address
Łukasz Nowak's avatar
Łukasz Nowak committed
804
      callAndRead(['ip', 'addr', 'add', address_string, 'dev', interface_name])
805 806 807 808 809

      # Fake success for local ipv4
      if not ipv6:
        return True

Łukasz Nowak's avatar
Łukasz Nowak committed
810 811
      # wait few moments
      time.sleep(2)
812 813 814 815 816 817

    # Fake success for local ipv4
    if not ipv6:
      return True

    # check existence on interface for ipv6
818
    _, result = callAndRead(['ip', '-6', 'addr', 'list', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
819 820 821 822
    for l in result.split('\n'):
      if address in l:
        if 'tentative' in l:
          # duplicate, remove
Marco Mariani's avatar
Marco Mariani committed
823
          callAndRead(['ip', 'addr', 'del', address_string, 'dev', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
824 825 826 827 828 829 830 831 832 833 834 835 836
          return False
        # found and clean
        return True
    # even when added not found, this is bad...
    return False

  def _generateRandomIPv4Address(self, netmask):
    # no addresses found, generate new one
    # Try 10 times to add address, raise in case if not possible
    try_num = 10
    while try_num > 0:
      addr = random.choice([q for q in netaddr.glob_to_iprange(
        netaddr.cidr_to_glob(self.ipv4_local_network))]).format()
837 838
      if (dict(addr=addr, netmask=netmask) not in
            self.getIPv4LocalAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
839 840 841 842 843 844 845 846 847
        # Checking the validity of the IPv6 address
        if self._addSystemAddress(addr, netmask, False):
          return dict(addr=addr, netmask=netmask)
        try_num -= 1

    raise AddressGenerationError(addr)

  def addIPv4LocalAddress(self, addr=None):
    """Adds local IPv4 address in ipv4_local_network"""
Jondy Zhao's avatar
Jondy Zhao committed
848 849
    netmask = '255.255.255.254' if sys.platform == 'cygwin' \
             else '255.255.255.255'
Łukasz Nowak's avatar
Łukasz Nowak committed
850 851 852 853
    local_address_list = self.getIPv4LocalAddressList()
    if addr is None:
      return self._generateRandomIPv4Address(netmask)
    elif dict(addr=addr, netmask=netmask) not in local_address_list:
854 855 856
      if self._addSystemAddress(addr, netmask, False):
        return dict(addr=addr, netmask=netmask)
      else:
Marco Mariani's avatar
Marco Mariani committed
857
        self.logger.warning('Impossible to add old local IPv4 %s. Generating '
858
                            'new IPv4 address.', addr)
859
        return self._generateRandomIPv4Address(netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
860 861 862 863
    else:
      # confirmed to be configured
      return dict(addr=addr, netmask=netmask)

Marco Mariani's avatar
Marco Mariani committed
864
  def addAddr(self, addr=None, netmask=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
865
    """
866
    Adds IP address to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
867

868
    If addr is specified and exists already on interface does nothing.
Łukasz Nowak's avatar
Łukasz Nowak committed
869

870
    If addr is specified and does not exists on interface, tries to add given
Vincent Pelletier's avatar
Vincent Pelletier committed
871 872
    address. If it is not possible (ex. because network changed) calculates new
    address.
Łukasz Nowak's avatar
Łukasz Nowak committed
873 874

    Args:
875
      addr: Wished address to be added to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
876 877 878 879 880 881 882
      netmask: Wished netmask to be used.

    Returns:
      Tuple of (address, netmask).

    Raises:
      AddressGenerationError: Couldn't construct valid address with existing
883 884
          one's on the interface.
      NoAddressOnInterface: There's no address on the interface to construct
Łukasz Nowak's avatar
Łukasz Nowak committed
885 886
          an address with.
    """
887
    # Getting one address of the interface as base of the next addresses
Łukasz Nowak's avatar
Łukasz Nowak committed
888 889 890 891
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
892
    interface_addr_list = self.getGlobalScopeAddressList()
Łukasz Nowak's avatar
Łukasz Nowak committed
893 894

    # No address found
895 896 897
    if len(interface_addr_list) == 0:
      raise NoAddressOnInterface(interface_name)
    address_dict = interface_addr_list[0]
Łukasz Nowak's avatar
Łukasz Nowak committed
898 899

    if addr is not None:
900
      if dict(addr=addr, netmask=netmask) in interface_addr_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
901 902 903 904
        # confirmed to be configured
        return dict(addr=addr, netmask=netmask)
      if netmask == address_dict['netmask']:
        # same netmask, so there is a chance to add good one
905
        interface_network = netaddr.ip.IPNetwork('%s/%s' % (address_dict['addr'],
Łukasz Nowak's avatar
Łukasz Nowak committed
906
          netmaskToPrefixIPv6(address_dict['netmask'])))
Vincent Pelletier's avatar
Vincent Pelletier committed
907 908
        requested_network = netaddr.ip.IPNetwork('%s/%s' % (addr,
          netmaskToPrefixIPv6(netmask)))
909
        if interface_network.network == requested_network.network:
Łukasz Nowak's avatar
Łukasz Nowak committed
910 911 912 913
          # same network, try to add
          if self._addSystemAddress(addr, netmask):
            # succeed, return it
            return dict(addr=addr, netmask=netmask)
914
          else:
Marco Mariani's avatar
Marco Mariani committed
915
            self.logger.warning('Impossible to add old public IPv6 %s. '
916
                                'Generating new IPv6 address.', addr)
Łukasz Nowak's avatar
Łukasz Nowak committed
917 918 919 920 921

    # Try 10 times to add address, raise in case if not possible
    try_num = 10
    netmask = address_dict['netmask']
    while try_num > 0:
Vincent Pelletier's avatar
Vincent Pelletier committed
922 923
      addr = ':'.join(address_dict['addr'].split(':')[:-1] + ['%x' % (
        random.randint(1, 65000), )])
Łukasz Nowak's avatar
Łukasz Nowak committed
924
      socket.inet_pton(socket.AF_INET6, addr)
925 926
      if (dict(addr=addr, netmask=netmask) not in
            self.getGlobalScopeAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
927 928 929 930 931 932 933
        # Checking the validity of the IPv6 address
        if self._addSystemAddress(addr, netmask):
          return dict(addr=addr, netmask=netmask)
        try_num -= 1

    raise AddressGenerationError(addr)

934

935
def parse_computer_definition(conf, definition_path):
936
  conf.logger.info('Using definition file %r', definition_path)
Marco Mariani's avatar
Marco Mariani committed
937 938 939 940 941 942 943 944
  computer_definition = ConfigParser.RawConfigParser({
    'software_user': 'slapsoft',
  })
  computer_definition.read(definition_path)
  interface = None
  address = None
  netmask = None
  if computer_definition.has_option('computer', 'address'):
Marco Mariani's avatar
Marco Mariani committed
945
    address, netmask = computer_definition.get('computer', 'address').split('/')
946 947
  if (conf.alter_network and conf.interface_name is not None
        and conf.ipv4_local_network is not None):
Marco Mariani's avatar
Marco Mariani committed
948 949 950 951
    interface = Interface(logger=conf.logger,
                          name=conf.interface_name,
                          ipv4_local_network=conf.ipv4_local_network,
                          ipv6_interface=conf.ipv6_interface)
Marco Mariani's avatar
Marco Mariani committed
952
  computer = Computer(
953
      reference=conf.computer_id,
Marco Mariani's avatar
Marco Mariani committed
954 955 956
      interface=interface,
      addr=address,
      netmask=netmask,
957
      ipv6_interface=conf.ipv6_interface,
Marco Mariani's avatar
Marco Mariani committed
958
      software_user=computer_definition.get('computer', 'software_user'),
959
  )
Marco Mariani's avatar
Marco Mariani committed
960
  partition_list = []
961
  for partition_number in range(int(conf.partition_amount)):
Marco Mariani's avatar
Marco Mariani committed
962 963 964 965 966 967 968
    section = 'partition_%s' % partition_number
    user = User(computer_definition.get(section, 'user'))
    address_list = []
    for a in computer_definition.get(section, 'address').split():
      address, netmask = a.split('/')
      address_list.append(dict(addr=address, netmask=netmask))
    tap = Tap(computer_definition.get(section, 'network_interface'))
Marco Mariani's avatar
Marco Mariani committed
969
    partition = Partition(reference=computer_definition.get(section, 'pathname'),
970
                          path=os.path.join(conf.instance_root,
Marco Mariani's avatar
Marco Mariani committed
971 972 973 974 975
                                            computer_definition.get(section, 'pathname')),
                          user=user,
                          address_list=address_list,
                          tap=tap)
    partition_list.append(partition)
Marco Mariani's avatar
Marco Mariani committed
976 977 978 979
  computer.partition_list = partition_list
  return computer


980
def parse_computer_xml(conf, xml_path):
Marco Mariani's avatar
Marco Mariani committed
981 982 983 984 985
  interface = Interface(logger=conf.logger,
                        name=conf.interface_name,
                        ipv4_local_network=conf.ipv4_local_network,
                        ipv6_interface=conf.ipv6_interface)

Marco Mariani's avatar
Marco Mariani committed
986
  if os.path.exists(xml_path):
987
    conf.logger.debug('Loading previous computer data from %r', xml_path)
Marco Mariani's avatar
Marco Mariani committed
988
    computer = Computer.load(xml_path,
989 990
                             reference=conf.computer_id,
                             ipv6_interface=conf.ipv6_interface)
Marco Mariani's avatar
Marco Mariani committed
991
    # Connect to the interface defined by the configuration
Marco Mariani's avatar
Marco Mariani committed
992
    computer.interface = interface
Marco Mariani's avatar
Marco Mariani committed
993 994
  else:
    # If no pre-existent configuration found, create a new computer object
995
    conf.logger.warning('Creating new computer data with id %r', conf.computer_id)
996
    computer = Computer(
997
      reference=conf.computer_id,
Marco Mariani's avatar
Marco Mariani committed
998
      interface=interface,
Marco Mariani's avatar
Marco Mariani committed
999 1000
      addr=None,
      netmask=None,
1001 1002
      ipv6_interface=conf.ipv6_interface,
      software_user=conf.software_user,
Marco Mariani's avatar
Marco Mariani committed
1003 1004
    )

1005
  partition_amount = int(conf.partition_amount)
Marco Mariani's avatar
Marco Mariani committed
1006 1007
  existing_partition_amount = len(computer.partition_list)

1008 1009 1010 1011 1012 1013 1014 1015
  if partition_amount < existing_partition_amount:
    conf.logger.critical('Requested amount of computer partitions (%s) is lower '
                         'than already configured (%s), cannot continue',
                         partition_amount, existing_partition_amount)
    sys.exit(1)
  elif partition_amount > existing_partition_amount:
    conf.logger.info('Adding %s new partitions',
                     partition_amount - existing_partition_amount)
Marco Mariani's avatar
Marco Mariani committed
1016 1017 1018 1019

  for i in range(existing_partition_amount, partition_amount):
    # add new partitions
    partition = Partition(
1020 1021 1022 1023 1024 1025 1026
        reference='%s%s' % (conf.partition_base_name, i),
        path=os.path.join(conf.instance_root, '%s%s' % (
          conf.partition_base_name, i)),
        user=User('%s%s' % (conf.user_base_name, i)),
        address_list=None,
        tap=Tap('%s%s' % (conf.tap_base_name, i))
    )
Marco Mariani's avatar
Marco Mariani committed
1027
    computer.partition_list.append(partition)
Marco Mariani's avatar
Marco Mariani committed
1028 1029 1030 1031

  return computer


1032
def write_computer_definition(conf, computer):
1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047
  computer_definition = ConfigParser.RawConfigParser()
  computer_definition.add_section('computer')
  if computer.address is not None and computer.netmask is not None:
    computer_definition.set('computer', 'address', '/'.join(
      [computer.address, computer.netmask]))
  for partition_number, partition in enumerate(computer.partition_list):
    section = 'partition_%s' % partition_number
    computer_definition.add_section(section)
    address_list = []
    for address in partition.address_list:
      address_list.append('/'.join([address['addr'], address['netmask']]))
    computer_definition.set(section, 'address', ' '.join(address_list))
    computer_definition.set(section, 'user', partition.user.name)
    computer_definition.set(section, 'network_interface', partition.tap.name)
    computer_definition.set(section, 'pathname', partition.reference)
1048
  computer_definition.write(open(conf.output_definition_file, 'w'))
1049
  conf.logger.info('Stored computer definition in %r', conf.output_definition_file)
1050 1051


1052
def random_delay(conf):
Marco Mariani's avatar
Marco Mariani committed
1053 1054 1055
  # Add delay between 0 and 1 hour
  # XXX should be the contrary: now by default, and cron should have
  # --maximal-delay=3600
1056
  if not conf.now:
Marco Mariani's avatar
Marco Mariani committed
1057
    duration = float(60 * 60) * random.random()
Marco Mariani's avatar
Marco Mariani committed
1058
    conf.logger.info('Sleeping for %s seconds. To disable this feature, '
1059
                     'use with --now parameter in manual.', duration)
Marco Mariani's avatar
Marco Mariani committed
1060 1061 1062
    time.sleep(duration)


1063 1064
def do_format(conf):
  random_delay(conf)
Marco Mariani's avatar
Marco Mariani committed
1065

1066 1067
  if conf.input_definition_file:
    computer = parse_computer_definition(conf, conf.input_definition_file)
1068 1069
  else:
    # no definition file, figure out computer
1070
    computer = parse_computer_xml(conf, conf.computer_xml)
1071

1072 1073
  computer.instance_root = conf.instance_root
  computer.software_root = conf.software_root
1074
  conf.logger.info('Updating node %s', conf.computer_id)
1075
  address = computer.getAddress(conf.create_tap)
1076 1077 1078
  computer.address = address['addr']
  computer.netmask = address['netmask']

1079 1080
  if conf.output_definition_file:
    write_computer_definition(conf, computer)
Marco Mariani's avatar
Marco Mariani committed
1081

1082 1083 1084
  computer.construct(alter_user=conf.alter_user,
                     alter_network=conf.alter_network,
                     create_tap=conf.create_tap)
1085

1086 1087
  if getattr(conf, 'certificate_repository_path', None):
    mkdir_p(conf.certificate_repository_path, mode=0o700)
1088

1089
  # Dumping and sending to the erp5 the current configuration
1090 1091 1092 1093
  if not conf.dry_run:
    computer.dump(path_to_xml=conf.computer_xml,
                  path_to_json=conf.computer_json,
                  logger=conf.logger)
1094
  conf.logger.info('Posting information to %r', conf.master_url)
1095
  computer.send(conf)
1096
  conf.logger.info('slapos successfully prepared the computer.')
Łukasz Nowak's avatar
Łukasz Nowak committed
1097

1098

Marco Mariani's avatar
Marco Mariani committed
1099
class FormatConfig(object):
1100 1101 1102 1103
  key_file = None
  cert_file = None
  alter_network = None
  alter_user = None
1104
  create_tap = None
1105
  computer_xml = None
1106
  computer_json = None
Marco Mariani's avatar
Marco Mariani committed
1107
  input_definition_file = None
1108
  log_file = None
Marco Mariani's avatar
Marco Mariani committed
1109
  output_definition_file = None
1110
  dry_run = None
1111
  software_user = None
1112

Marco Mariani's avatar
Marco Mariani committed
1113 1114 1115
  def __init__(self, logger):
    self.logger = logger

1116 1117
  @staticmethod
  def checkRequiredBinary(binary_list):
Łukasz Nowak's avatar
Łukasz Nowak committed
1118 1119
    missing_binary_list = []
    for b in binary_list:
1120 1121
      if type(b) != type([]):
        b = [b]
Łukasz Nowak's avatar
Łukasz Nowak committed
1122
      try:
1123
        callAndRead(b)
Łukasz Nowak's avatar
Łukasz Nowak committed
1124 1125 1126
      except ValueError:
        pass
      except OSError:
Jondy Zhao's avatar
Jondy Zhao committed
1127
        missing_binary_list.append(b[0])
Łukasz Nowak's avatar
Łukasz Nowak committed
1128
    if missing_binary_list:
Vincent Pelletier's avatar
Vincent Pelletier committed
1129 1130
      raise UsageError('Some required binaries are missing or not '
          'functional: %s' % (','.join(missing_binary_list), ))
Łukasz Nowak's avatar
Łukasz Nowak committed
1131

1132
  def mergeConfig(self, args, configp):
Łukasz Nowak's avatar
Łukasz Nowak committed
1133 1134
    """
    Set options given by parameters.
1135
    Must be executed before setting up the logger.
Łukasz Nowak's avatar
Łukasz Nowak committed
1136 1137 1138
    """
    self.key_file = None
    self.cert_file = None
1139

1140 1141 1142
    # Set argument parameters
    for key, value in args.__dict__.items():
      setattr(self, key, value)
Łukasz Nowak's avatar
Łukasz Nowak committed
1143 1144 1145

    # Merges the arguments and configuration
    for section in ("slapformat", "slapos"):
1146
      configuration_dict = dict(configp.items(section))
Łukasz Nowak's avatar
Łukasz Nowak committed
1147 1148 1149 1150
      for key in configuration_dict:
        if not getattr(self, key, None):
          setattr(self, key, configuration_dict[key])

1151
  def setConfig(self):
Łukasz Nowak's avatar
Łukasz Nowak committed
1152
    # setup some nones
1153
    for parameter in ['interface_name', 'partition_base_name', 'user_base_name',
1154
          'tap_base_name', 'ipv4_local_network', 'ipv6_interface']:
Łukasz Nowak's avatar
Łukasz Nowak committed
1155 1156
      if getattr(self, parameter, None) is None:
        setattr(self, parameter, None)
1157

1158 1159
    # Backward compatibility
    if not getattr(self, "interface_name", None) \
1160
          and getattr(self, "bridge_name", None):
1161
      setattr(self, "interface_name", self.bridge_name)
1162
      self.logger.warning('bridge_name option is deprecated and should be '
1163
                          'replaced by interface_name.')
1164
    if not getattr(self, "create_tap", None) \
1165
          and getattr(self, "no_bridge", None):
1166
      setattr(self, "create_tap", not self.no_bridge)
1167
      self.logger.warning('no_bridge option is deprecated and should be '
1168
                          'replaced by create_tap.')
Łukasz Nowak's avatar
Łukasz Nowak committed
1169 1170 1171 1172 1173 1174

    # Set defaults lately
    if self.alter_network is None:
      self.alter_network = 'True'
    if self.alter_user is None:
      self.alter_user = 'True'
1175 1176
    if self.software_user is None:
      self.software_user = 'slapsoft'
1177 1178
    if self.create_tap is None:
      self.create_tap = True
Łukasz Nowak's avatar
Łukasz Nowak committed
1179 1180

    # Convert strings to booleans
Marco Mariani's avatar
Marco Mariani committed
1181 1182
    for option in ['alter_network', 'alter_user', 'create_tap']:
      attr = getattr(self, option)
1183 1184 1185
      if isinstance(attr, str):
        if attr.lower() == 'true':
          root_needed = True
Marco Mariani's avatar
Marco Mariani committed
1186
          setattr(self, option, True)
1187
        elif attr.lower() == 'false':
Marco Mariani's avatar
Marco Mariani committed
1188
          setattr(self, option, False)
1189 1190
        else:
          message = 'Option %r needs to be "True" or "False", wrong value: ' \
Marco Mariani's avatar
Marco Mariani committed
1191
              '%r' % (option, getattr(self, option))
1192 1193
          self.logger.error(message)
          raise UsageError(message)
Łukasz Nowak's avatar
Łukasz Nowak committed
1194

1195 1196 1197
    if not self.dry_run:
      if self.alter_user:
        self.checkRequiredBinary(['groupadd', 'useradd', 'usermod'])
1198
      if self.create_tap:
1199
        self.checkRequiredBinary([['tunctl', '-d']])
1200
      if self.alter_network:
1201
        self.checkRequiredBinary(['ip'])
Marco Mariani's avatar
Marco Mariani committed
1202

1203
    # Required, even for dry run
1204
    if self.alter_network and self.create_tap:
1205
      self.checkRequiredBinary(['brctl'])
Łukasz Nowak's avatar
Łukasz Nowak committed
1206

1207 1208 1209 1210
    # Check if root is needed
    if (self.alter_network or self.alter_user) and not self.dry_run:
      root_needed = True
    else:
1211
      root_needed = False
1212

Łukasz Nowak's avatar
Łukasz Nowak committed
1213
    # check root
Marco Mariani's avatar
Marco Mariani committed
1214
    # XXX in the new CLI, this is checked by the @must_be_root decorator.
Łukasz Nowak's avatar
Łukasz Nowak committed
1215 1216 1217
    if root_needed and os.getuid() != 0:
      message = "Root rights are needed"
      self.logger.error(message)
Marco Mariani's avatar
Marco Mariani committed
1218
      sys.stderr.write(message + '\n')
1219
      sys.exit(1)
Łukasz Nowak's avatar
Łukasz Nowak committed
1220 1221 1222 1223 1224 1225 1226

    # Check mandatory options
    for parameter in ('computer_id', 'instance_root', 'master_url',
                      'software_root', 'computer_xml'):
      if not getattr(self, parameter, None):
        raise UsageError("Parameter '%s' is not defined." % parameter)

1227 1228 1229 1230 1231
    # Check existence of SSL certificate files, if defined
    for attribute in ['key_file', 'cert_file', 'master_ca_file']:
      file_location = getattr(self, attribute, None)
      if file_location is not None:
        if not os.path.exists(file_location):
1232 1233
          self.logger.critical('File %r does not exist or is not readable.',
                               file_location)
1234 1235
          sys.exit(1)

1236
    self.logger.debug('Started.')
1237 1238
    if self.dry_run:
      self.logger.info("Dry-run mode enabled.")
1239
    if self.create_tap:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
1240
      self.logger.info("Tap creation mode enabled.")
Łukasz Nowak's avatar
Łukasz Nowak committed
1241 1242 1243 1244

    # Calculate path once
    self.computer_xml = os.path.abspath(self.computer_xml)

Marco Mariani's avatar
Marco Mariani committed
1245 1246 1247 1248 1249 1250
    if self.input_definition_file:
      self.input_definition_file = os.path.abspath(self.input_definition_file)

    if self.output_definition_file:
      self.output_definition_file = os.path.abspath(self.output_definition_file)

Łukasz Nowak's avatar
Łukasz Nowak committed
1251

1252
def tracing_monkeypatch(conf):
1253
  """Substitute os module and callAndRead function with tracing wrappers."""
Vincent Pelletier's avatar
Vincent Pelletier committed
1254 1255
  global os
  global callAndRead
1256

Vincent Pelletier's avatar
Vincent Pelletier committed
1257
  real_callAndRead = callAndRead
Łukasz Nowak's avatar
Łukasz Nowak committed
1258

1259 1260
  os = OS(conf)
  if conf.dry_run:
1261 1262 1263 1264 1265 1266
    def dry_callAndRead(argument_list, raise_on_error=True):
      if argument_list == ['brctl', 'show']:
        return real_callAndRead(argument_list, raise_on_error)
      else:
        return 0, ''
    callAndRead = dry_callAndRead
Marco Mariani's avatar
Marco Mariani committed
1267

1268
    def fake_getpwnam(user):
Vincent Pelletier's avatar
Vincent Pelletier committed
1269
      class result(object):
1270 1271 1272 1273 1274 1275
        pw_uid = 12345
        pw_gid = 54321
      return result
    pwd.getpwnam = fake_getpwnam
  else:
    dry_callAndRead = real_callAndRead
1276

Marco Mariani's avatar
Marco Mariani committed
1277
  def logging_callAndRead(argument_list, raise_on_error=True):
1278
    conf.logger.debug(' '.join(argument_list))
Marco Mariani's avatar
Marco Mariani committed
1279 1280
    return dry_callAndRead(argument_list, raise_on_error)
  callAndRead = logging_callAndRead