format.py 46.4 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 32 33
from optparse import OptionParser, Option
from xml_marshaller import xml_marshaller
import ConfigParser
34 35
import errno
import fcntl
Łukasz Nowak's avatar
Łukasz Nowak committed
36
import grp
37
import json
Łukasz Nowak's avatar
Łukasz Nowak committed
38 39 40 41
import logging
import netaddr
import netifaces
import os
Łukasz Nowak's avatar
Łukasz Nowak committed
42
import pwd
Łukasz Nowak's avatar
Łukasz Nowak committed
43 44
import random
import slapos.slap as slap
45
import shutil
Łukasz Nowak's avatar
Łukasz Nowak committed
46
import socket
47
import struct
Łukasz Nowak's avatar
Łukasz Nowak committed
48 49
import subprocess
import sys
50
import threading
Łukasz Nowak's avatar
Łukasz Nowak committed
51
import time
52
import traceback
Marco Mariani's avatar
Marco Mariani committed
53 54 55
import zipfile

import lxml.etree
56
from slapos.version import version
Marco Mariani's avatar
Marco Mariani committed
57 58


Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
59 60 61 62 63
# set up logging
logger = logging.getLogger("slapformat")
logger.setLevel(logging.INFO)


Marco Mariani's avatar
Marco Mariani committed
64 65 66 67
def prettify_xml(xml):
  root = lxml.etree.fromstring(xml)
  return lxml.etree.tostring(root, pretty_print=True)

Łukasz Nowak's avatar
Łukasz Nowak committed
68

69
from slapos.util import mkdir_p
70
from slapos.util import chownDirectory
71

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

Vincent Pelletier's avatar
Vincent Pelletier committed
75 76 77
  _os = os

  def __init__(self, config):
78
    self._dry_run = config.dry_run
Vincent Pelletier's avatar
Vincent Pelletier committed
79 80 81 82 83 84 85 86 87 88 89 90
    self._verbose = config.verbose
    self._logger = config.logger
    add = self._addWrapper
    add('chown')
    add('chmod')
    add('makedirs')
    add('mkdir')

  def _addWrapper(self, name):
    def wrapper(*args, **kw):
      if self._verbose:
        arg_list = [repr(x) for x in args] + [
Marco Mariani's avatar
Marco Mariani committed
91 92 93
                '%s=%r' % (x, y) for x, y in kw.iteritems()
                ]
        self._logger.debug('%s(%s)' % (name, ', '.join(arg_list)))
94 95
      if not self._dry_run:
        getattr(self._os, name)(*args, **kw)
Vincent Pelletier's avatar
Vincent Pelletier committed
96 97 98 99
    setattr(self, name, wrapper)

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

101

102 103 104
class UsageError(Exception):
  pass

105

106
class NoAddressOnInterface(Exception):
Łukasz Nowak's avatar
Łukasz Nowak committed
107
  """
Marco Mariani's avatar
Marco Mariani committed
108
  Exception raised if there is no address on the interface to construct IPv6
109 110 111
  address with.

  Attributes:
112
    brige: String, the name of the interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
113 114
  """

115 116
  def __init__(self, interface):
    super(NoAddressOnInterface, self).__init__(
Marco Mariani's avatar
Marco Mariani committed
117
      'No IPv6 found on interface %s to construct IPv6 with.' % interface
118
    )
Łukasz Nowak's avatar
Łukasz Nowak committed
119

120

121 122 123
class AddressGenerationError(Exception):
  """
  Exception raised if the generation of an IPv6 based on the prefix obtained
124
  from the interface failed.
125 126 127 128 129 130 131 132

  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
133

134

Łukasz Nowak's avatar
Łukasz Nowak committed
135
def callAndRead(argument_list, raise_on_error=True):
Marco Mariani's avatar
Marco Mariani committed
136 137 138
  popen = subprocess.Popen(argument_list,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
Łukasz Nowak's avatar
Łukasz Nowak committed
139 140
  result = popen.communicate()[0]
  if raise_on_error and popen.returncode != 0:
141
    raise ValueError('Issue while invoking %r, result was:\n%s' % (
Marco Mariani's avatar
Marco Mariani committed
142
                     argument_list, result))
Łukasz Nowak's avatar
Łukasz Nowak committed
143 144
  return popen.returncode, result

145

Łukasz Nowak's avatar
Łukasz Nowak committed
146 147 148 149 150 151
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()

152

Łukasz Nowak's avatar
Łukasz Nowak committed
153 154 155 156 157
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)]

158

Łukasz Nowak's avatar
Łukasz Nowak committed
159 160 161 162 163
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)]

164

Łukasz Nowak's avatar
Łukasz Nowak committed
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
def _getDict(instance):
  """
  Serialize an object instance into dictionaries. List and dict will remains
  the same, basic type too. But encapsulated object will be returned as dict.
  Set, collections and other aren't handle for now.

  Args:
    instance: an object of any type.

  Returns:
    A dictionary if the given object wasn't a list, a list otherwise.
  """
  if isinstance(instance, list):
    return [_getDict(item) for item in instance]

  elif isinstance(instance, dict):
    result = {}
182
    for key in instance:
Łukasz Nowak's avatar
Łukasz Nowak committed
183 184 185 186 187
      result[key] = _getDict(instance[key])
    return result

  else:
    try:
188
      dikt = instance.__dict__
Łukasz Nowak's avatar
Łukasz Nowak committed
189 190
    except AttributeError:
      return instance
191 192 193 194
    result = {}
    for key, value in dikt.iteritems():
      result[key] = _getDict(value)
    return result
Łukasz Nowak's avatar
Łukasz Nowak committed
195

196

Vincent Pelletier's avatar
Vincent Pelletier committed
197
class Computer(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
198
  "Object representing the computer"
199 200
  instance_root = None
  software_root = None
Łukasz Nowak's avatar
Łukasz Nowak committed
201

202
  def __init__(self, reference, interface=None, addr=None, netmask=None,
Marco Mariani's avatar
Marco Mariani committed
203
               ipv6_interface=None, software_user='slapsoft'):
Łukasz Nowak's avatar
Łukasz Nowak committed
204 205 206
    """
    Attributes:
      reference: String, the reference of the computer.
207
      interface: String, the name of the computer's used interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
208 209
    """
    self.reference = str(reference)
210
    self.interface = interface
Łukasz Nowak's avatar
Łukasz Nowak committed
211 212 213
    self.partition_list = []
    self.address = addr
    self.netmask = netmask
Łukasz Nowak's avatar
Łukasz Nowak committed
214
    self.ipv6_interface = ipv6_interface
215
    self.software_user = software_user
Łukasz Nowak's avatar
Łukasz Nowak committed
216 217

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

220
  def getAddress(self, allow_tap=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
221
    """
Marco Mariani's avatar
Marco Mariani committed
222
    Return a list of the interface address not attributed to any partition (which
Łukasz Nowak's avatar
Łukasz Nowak committed
223 224 225
    are therefore free for the computer itself).

    Returns:
226
      False if the interface isn't available, else the list of the free addresses.
Łukasz Nowak's avatar
Łukasz Nowak committed
227
    """
228
    if self.interface is None:
Marco Mariani's avatar
Marco Mariani committed
229
      return {'addr': self.address, 'netmask': self.netmask}
Łukasz Nowak's avatar
Łukasz Nowak committed
230 231 232 233 234 235

    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'])
236
    # Going through addresses of the computer's interface
237
    for address_dict in self.interface.getGlobalScopeAddressList():
Łukasz Nowak's avatar
Łukasz Nowak committed
238 239 240 241
      # Comparing with computer's partition addresses
      if address_dict['addr'] not in computer_partition_address_list:
        return address_dict

242
    if allow_tap:
Marco Mariani's avatar
Marco Mariani committed
243
      # all addresses on interface are for partition, so let's add new one
244 245 246 247 248 249
      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
250
    raise NoAddressOnInterface('No valid IPv6 found on %s.' % self.interface.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
251 252 253 254 255 256 257 258 259

  def send(self, config):
    """
    Send a marshalled dictionary of the computer object serialized via_getDict.
    """

    slap_instance = slap.slap()
    connection_dict = {}
    if config.key_file and config.cert_file:
Marco Mariani's avatar
Marco Mariani committed
260 261
      connection_dict['key_file'] = config.key_file
      connection_dict['cert_file'] = config.cert_file
Łukasz Nowak's avatar
Łukasz Nowak committed
262
    slap_instance.initializeConnection(config.master_url,
Marco Mariani's avatar
Marco Mariani committed
263
                                       **connection_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
264
    slap_computer = slap_instance.registerComputer(self.reference)
Marco Mariani's avatar
Marco Mariani committed
265

266 267
    if config.dry_run:
      return
268
    try:
269
      slap_computer.updateConfiguration(xml_marshaller.dumps(_getDict(self)))
270 271 272 273
    except slap.NotFoundError as error:
      raise slap.NotFoundError("%s\nERROR : This SlapOS node is not recognised by "
          "SlapOS Master. Please make sure computer_id of slapos.cfg looks "
          "like 'COMP-123' and is correct.\nError is : 404 Not Found." % error)
Łukasz Nowak's avatar
Łukasz Nowak committed
274

275
  def dump(self, path_to_xml, path_to_json):
Łukasz Nowak's avatar
Łukasz Nowak committed
276 277 278 279 280
    """
    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
281
      path_to_json: String, path to the JSON version to save.
Łukasz Nowak's avatar
Łukasz Nowak committed
282 283 284
    """

    computer_dict = _getDict(self)
285 286 287 288 289

    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
290 291 292 293 294 295 296 297 298 299 300 301
    new_xml = xml_marshaller.dumps(computer_dict)
    new_pretty_xml = prettify_xml(new_xml)

    path_to_archive = path_to_xml + '.zip'

    if os.path.exists(path_to_archive):
      # 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

302
    if os.path.exists(path_to_xml):
303 304 305 306 307 308 309 310 311 312 313 314
      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,
                    path_to_archive+time.strftime('_broken_%Y%m%d-%H:%M'))
        try:
          self.backup_xml(path_to_archive, path_to_xml)
        except:
          # give up trying
          logger.warning("Can't backup %s: %s" %
                           (path_to_xml, traceback.format_exc()))
Marco Mariani's avatar
Marco Mariani committed
315

Marco Mariani's avatar
Marco Mariani committed
316 317
    with open(path_to_xml, 'wb') as fout:
      fout.write(new_pretty_xml)
Marco Mariani's avatar
Marco Mariani committed
318 319 320


  def backup_xml(self, path_to_archive, path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
321 322 323
    """
    Stores a copy of the current xml file to an historical archive.
    """
Marco Mariani's avatar
Marco Mariani committed
324
    xml_content = open(path_to_xml).read()
Marco Mariani's avatar
typo  
Marco Mariani committed
325
    saved_filename = os.path.basename(path_to_xml) + time.strftime('.%Y%m%d-%H:%M')
Marco Mariani's avatar
Marco Mariani committed
326 327 328 329

    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
330 331

  @classmethod
Łukasz Nowak's avatar
Łukasz Nowak committed
332
  def load(cls, path_to_xml, reference, ipv6_interface):
Łukasz Nowak's avatar
Łukasz Nowak committed
333 334 335 336 337 338 339 340
    """
    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:
341
      A Computer object.
Łukasz Nowak's avatar
Łukasz Nowak committed
342 343 344 345 346 347 348 349 350
    """

    dumped_dict = xml_marshaller.loads(open(path_to_xml).read())

    # Reconstructing the computer object from the xml
    computer = Computer(
        reference = reference,
        addr = dumped_dict['address'],
        netmask = dumped_dict['netmask'],
Marco Mariani's avatar
Marco Mariani committed
351 352
        ipv6_interface = ipv6_interface,
        software_user = dumped_dict.get('software_user', 'slapsoft'),
Łukasz Nowak's avatar
Łukasz Nowak committed
353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380
    )

    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(
          reference = partition_dict['reference'],
          path = partition_dict['path'],
          user = user,
          address_list = address_list,
          tap = tap,
      )

      computer.partition_list.append(partition)

    return computer

381
  def construct(self, alter_user=True, alter_network=True, create_tap=True):
Łukasz Nowak's avatar
Łukasz Nowak committed
382 383 384 385
    """
    Construct the computer object as it is.
    """
    if alter_network and self.address is not None:
386
      self.interface.addAddr(self.address, self.netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
387

388
    for path in self.instance_root, self.software_root:
Łukasz Nowak's avatar
Łukasz Nowak committed
389 390 391 392 393
      if not os.path.exists(path):
        os.makedirs(path, 0755)
      else:
        os.chmod(path, 0755)

394 395
    # own self.software_root by software user
    slapsoft = User(self.software_user)
Łukasz Nowak's avatar
Łukasz Nowak committed
396 397 398
    slapsoft.path = self.software_root
    if alter_user:
      slapsoft.create()
Łukasz Nowak's avatar
Łukasz Nowak committed
399
      slapsoft_pw = pwd.getpwnam(slapsoft.name)
400
      chownDirectory(path, uid, gid)
Łukasz Nowak's avatar
Łukasz Nowak committed
401 402
    os.chmod(self.software_root, 0755)

403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    # Speed hack:
    # Blindly add all IPs from existing configuration, just to speed up actual
    # computer configuration later on.
    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),
                       'dev', self.interface.name])
      except ValueError:
        pass

419 420 421 422 423 424 425 426 427 428 429 430 431 432 433
    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')

434
        if alter_network and create_tap:
435
          # In case it has to be  attached to the TAP network device, only one
436 437
          # is necessary for the interface to assert carrier
          if self.interface.attach_to_tap and partition_index == 0:
438
            partition.tap.createWithOwner(owner, attach_to_tap=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
439
          else:
440 441
            partition.tap.createWithOwner(owner)

442
          self.interface.addTap(partition.tap)
443 444 445 446 447 448 449 450 451 452

        # 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
        if len(partition.address_list) == 0:
          # regenerate
453 454
          partition.address_list.append(self.interface.addIPv4LocalAddress())
          partition.address_list.append(self.interface.addAddr())
455 456 457 458 459
        elif alter_network:
          # regenerate list of addresses
          old_partition_address_list = partition.address_list
          partition.address_list = []
          if len(old_partition_address_list) != 2:
460 461 462
            raise ValueError(
              'There should be exactly 2 stored addresses. Got: %r' %
              (old_partition_address_list,))
Vincent Pelletier's avatar
Vincent Pelletier committed
463 464
          if not any([netaddr.valid_ipv6(q['addr'])
              for q in old_partition_address_list]):
465
            raise ValueError('Not valid ipv6 addresses loaded')
Vincent Pelletier's avatar
Vincent Pelletier committed
466 467
          if not any([netaddr.valid_ipv4(q['addr'])
              for q in old_partition_address_list]):
468
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
469

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

487

Vincent Pelletier's avatar
Vincent Pelletier committed
488
class Partition(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511
  "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
512 513
    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
514 515
    """

516
    self.path = os.path.abspath(self.path)
Łukasz Nowak's avatar
Łukasz Nowak committed
517
    owner = self.user if self.user else User('root')
518 519
    if not os.path.exists(self.path):
      os.mkdir(self.path, 0750)
Łukasz Nowak's avatar
Łukasz Nowak committed
520
    if alter_user:
Łukasz Nowak's avatar
Łukasz Nowak committed
521
      owner_pw = pwd.getpwnam(owner.name)
522
      chownDirectory(path, uid, gid)
523
    os.chmod(self.path, 0750)
Łukasz Nowak's avatar
Łukasz Nowak committed
524

525

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

529
  path = None
Łukasz Nowak's avatar
Łukasz Nowak committed
530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555

  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
556
    grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
557
    try:
Jondy Zhao's avatar
Jondy Zhao committed
558
      grp.getgrnam(grpname)
Łukasz Nowak's avatar
Łukasz Nowak committed
559
    except KeyError:
Jondy Zhao's avatar
Jondy Zhao committed
560
      callAndRead(['groupadd', grpname])
Łukasz Nowak's avatar
Łukasz Nowak committed
561

Vincent Pelletier's avatar
Vincent Pelletier committed
562 563
    user_parameter_list = ['-d', self.path, '-g', self.name, '-s',
      '/bin/false']
Łukasz Nowak's avatar
Łukasz Nowak committed
564 565 566 567
    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
568
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
569
    except KeyError:
570
      user_parameter_list.append('-r')
Łukasz Nowak's avatar
Łukasz Nowak committed
571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586
      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
587
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
588 589 590 591
      return True
    except KeyError:
      return False

592

Vincent Pelletier's avatar
Vincent Pelletier committed
593
class Tap(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
594
  "Tap represent a tap interface on the system"
595 596 597
  IFF_TAP = 0x0002
  TUNSETIFF = 0x400454ca
  KEEP_TAP_ATTACHED_EVENT = threading.Event()
Łukasz Nowak's avatar
Łukasz Nowak committed
598 599 600 601 602 603 604 605 606 607 608 609

  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,)

610 611 612
  def attach(self):
    """
    Attach to the TAP interface, meaning  that it just opens the TAP interface
Marco Mariani's avatar
Marco Mariani committed
613
    and waits for the caller to notify that it can be safely detached.
614 615 616 617 618 619 620 621

    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).

622
    In case of bridge:
623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640
    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))

641
    except IOError as error:
642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
      # 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
663 664 665 666 667 668 669 670 671
    """
    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):
672
      owner_id = open(check_file).read().strip()
Łukasz Nowak's avatar
Łukasz Nowak committed
673
      try:
674 675
        owner_id = int(owner_id)
      except ValueError:
Łukasz Nowak's avatar
Łukasz Nowak committed
676
        pass
677
    if owner_id != pwd.getpwnam(owner.name).pw_uid:
Łukasz Nowak's avatar
Łukasz Nowak committed
678 679 680
      callAndRead(['tunctl', '-t', self.name, '-u', owner.name])
    callAndRead(['ip', 'link', 'set', self.name, 'up'])

681 682 683
    if attach_to_tap:
      threading.Thread(target=self.attach).start()

684

685
class Interface(object):
Marco Mariani's avatar
Marco Mariani committed
686
  """Represent a network interface on the system"""
Łukasz Nowak's avatar
Łukasz Nowak committed
687

688
  def __init__(self, name, ipv4_local_network, ipv6_interface=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
689 690
    """
    Attributes:
691
        name: String, the name of the interface
Łukasz Nowak's avatar
Łukasz Nowak committed
692 693 694 695
    """

    self.name = str(name)
    self.ipv4_local_network = ipv4_local_network
Łukasz Nowak's avatar
Łukasz Nowak committed
696
    self.ipv6_interface = ipv6_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
697

698
    # Attach to TAP  network interface, only if the  interface interface does not
699
    # report carrier
700
    _, result = callAndRead(['ip', 'addr', 'list', self.name])
701 702
    self.attach_to_tap = 'DOWN' in result.split('\n', 1)[0]

Łukasz Nowak's avatar
Łukasz Nowak committed
703 704 705 706
  def __getinitargs__(self):
    return (self.name,)

  def getIPv4LocalAddressList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
707 708 709 710
    """
    Returns currently configured local IPv4 addresses which are in
    ipv4_local_network
    """
Łukasz Nowak's avatar
Łukasz Nowak committed
711 712 713 714 715 716 717 718 719
    if not socket.AF_INET in netifaces.ifaddresses(self.name):
      return []
    return [dict(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))]

  def getGlobalScopeAddressList(self):
    """Returns currently configured global scope IPv6 addresses"""
Łukasz Nowak's avatar
Łukasz Nowak committed
720 721 722 723
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
724
    try:
Vincent Pelletier's avatar
Vincent Pelletier committed
725 726 727
      address_list = [q
        for q in netifaces.ifaddresses(interface_name)[socket.AF_INET6]
        if isGlobalScopeAddress(q['addr'].split('%')[0])]
728 729
    except KeyError:
      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()
Vincent Pelletier's avatar
Vincent Pelletier committed
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:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
857
        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 864 865
    else:
      # confirmed to be configured
      return dict(addr=addr, netmask=netmask)

  def addAddr(self, addr = None, netmask = None):
    """
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:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
915
            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)
Vincent Pelletier's avatar
Vincent Pelletier committed
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

Łukasz Nowak's avatar
Łukasz Nowak committed
935 936 937 938
class Parser(OptionParser):
  """
  Parse all arguments.
  """
939
  def __init__(self, usage=None, version=version):
Łukasz Nowak's avatar
Łukasz Nowak committed
940 941 942 943 944 945 946 947 948 949
    """
    Initialize all options possibles.
    """
    OptionParser.__init__(self, usage=usage, version=version,
                          option_list=[
      Option("-x", "--computer_xml",
             help="Path to file with computer's XML. If does not exists, "
                  "will be created",
             default=None,
             type=str),
950 951 952 953
      Option("--computer_json",
             help="Path to a JSON version of the computer's XML (for development only).",
             default=None,
             type=str),
Łukasz Nowak's avatar
Łukasz Nowak committed
954 955 956 957 958 959 960 961 962 963 964 965 966 967
      Option("-l", "--log_file",
             help="The path to the log file used by the script.",
             type=str),
      Option("-i", "--input_definition_file",
             help="Path to file to read definition of computer instead of "
             "declaration. Using definition file allows to disable "
             "'discovery' of machine services and allows to define computer "
             "configuration in fully controlled manner.",
             type=str),
      Option("-o", "--output_definition_file",
             help="Path to file to write definition of computer from "
             "declaration.",
             type=str),
      Option("-n", "--dry_run",
968 969 970
             help="Don't actually do anything.",
             default=False,
             action="store_true"),
Łukasz Nowak's avatar
Łukasz Nowak committed
971 972 973 974 975 976 977 978 979 980 981 982
      Option("-v", "--verbose",
             default=False,
             action="store_true",
             help="Verbose output."),
      Option("-c", "--console",
             default=False,
             action="store_true",
             help="Console output."),
      Option('--alter_user', choices=['True', 'False'],
        help="Shall slapformat alter user database [default: True]"),
      Option('--alter_network', choices=['True', 'False'],
        help="Shall slapformat alter network configuration [default: True]"),
983
      Option('--now',
984 985 986
             help="Launch slapformat without delay",
             default=False,
             action="store_true"),
Łukasz Nowak's avatar
Łukasz Nowak committed
987 988
      ])

989
  def check_args(self, args):
Łukasz Nowak's avatar
Łukasz Nowak committed
990 991 992
    """
    Check arguments
    """
993 994 995 996
    if args:
      (options, args) = self.parse_args(list(args))
    else:
      (options, args) = self.parse_args()
Łukasz Nowak's avatar
Łukasz Nowak committed
997 998 999 1000
    if len(args) != 1:
      self.error("Incorrect number of arguments")
    return options, args[0]

1001

Marco Mariani's avatar
Marco Mariani committed
1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012

def parse_computer_definition(config, definition_path):
  config.logger.info('Using definition file %r' % definition_path)
  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
1013
    address, netmask = computer_definition.get('computer', 'address').split('/')
Marco Mariani's avatar
Marco Mariani committed
1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
  if config.alter_network and config.interface_name is not None \
      and config.ipv4_local_network is not None:
    interface = Interface(config.interface_name, config.ipv4_local_network,
      config.ipv6_interface)
  computer = Computer(
      reference=config.computer_id,
      interface=interface,
      addr=address,
      netmask=netmask,
      ipv6_interface=config.ipv6_interface,
      software_user=computer_definition.get('computer', 'software_user'),
    )
  partition_list = []
  for partition_number in range(int(config.partition_amount)):
    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
1035 1036 1037 1038 1039 1040 1041
    partition = Partition(reference=computer_definition.get(section, 'pathname'),
                          path=os.path.join(config.instance_root,
                                            computer_definition.get(section, 'pathname')),
                          user=user,
                          address_list=address_list,
                          tap=tap)
    partition_list.append(partition)
Marco Mariani's avatar
Marco Mariani committed
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053
  computer.partition_list = partition_list
  return computer


def parse_computer_xml(config, xml_path):
  if os.path.exists(xml_path):
    config.logger.info('Loading previous computer data from %r' % xml_path)
    computer = Computer.load(xml_path,
                             reference=config.computer_id,
                             ipv6_interface=config.ipv6_interface)
    # Connect to the interface defined by the configuration
    computer.interface = Interface(config.interface_name, config.ipv4_local_network,
1054
        config.ipv6_interface)
Marco Mariani's avatar
Marco Mariani committed
1055 1056
  else:
    # If no pre-existent configuration found, create a new computer object
Marco Mariani's avatar
Marco Mariani committed
1057
    config.logger.warning('Creating new data computer with id %r' % config.computer_id)
1058
    computer = Computer(
Marco Mariani's avatar
Marco Mariani committed
1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
      reference=config.computer_id,
      interface=Interface(config.interface_name, config.ipv4_local_network,
        config.ipv6_interface),
      addr=None,
      netmask=None,
      ipv6_interface=config.ipv6_interface,
      software_user=config.software_user,
    )

  partition_amount = int(config.partition_amount)
  existing_partition_amount = len(computer.partition_list)
  if existing_partition_amount > partition_amount:
    raise ValueError('Requested amount of computer partitions (%s) is lower '
        'then already configured (%s), cannot continue' % (partition_amount,
          len(computer.partition_list)))

  config.logger.info('Adding %s new partitions' %
      (partition_amount-existing_partition_amount))
  for nb_iter in range(existing_partition_amount, partition_amount):
    # add new ones
    user = User("%s%s" % (config.user_base_name, nb_iter))

    tap = Tap("%s%s" % (config.tap_base_name, nb_iter))

    path = os.path.join(config.instance_root, "%s%s" % (
                         config.partition_base_name, nb_iter))
    computer.partition_list.append(
      Partition(
        reference="%s%s" % (config.partition_base_name, nb_iter),
        path=path,
        user=user,
        address_list=None,
        tap=tap,
        ))

  return computer


1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
def write_computer_definition(config, computer):
  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)
  computer_definition.write(open(config.output_definition_file, 'w'))
  config.logger.info('Stored computer definition in %r' % config.output_definition_file)


Marco Mariani's avatar
Marco Mariani committed
1117 1118 1119
def run(config):
  if config.input_definition_file:
    computer = parse_computer_definition(config, config.input_definition_file)
1120 1121
  else:
    # no definition file, figure out computer
Marco Mariani's avatar
Marco Mariani committed
1122
    computer = parse_computer_xml(config, config.computer_xml)
1123 1124 1125 1126

  computer.instance_root = config.instance_root
  computer.software_root = config.software_root
  config.logger.info('Updating computer')
1127
  address = computer.getAddress(config.create_tap)
1128 1129 1130 1131
  computer.address = address['addr']
  computer.netmask = address['netmask']

  if config.output_definition_file:
1132
    write_computer_definition(config, computer)
Marco Mariani's avatar
Marco Mariani committed
1133

1134
  computer.construct(alter_user=config.alter_user,
Marco Mariani's avatar
Marco Mariani committed
1135 1136
                     alter_network=config.alter_network,
                     create_tap=config.create_tap)
1137

1138
  if getattr(config, 'certificate_repository_path', None):
1139 1140
    mkdir_p(config.certificate_repository_path, mode=0o700)

1141 1142
  # Dumping and sending to the erp5 the current configuration
  if not config.dry_run:
1143 1144
    computer.dump(path_to_xml=config.computer_xml,
                  path_to_json=config.computer_json)
1145 1146
  config.logger.info('Posting information to %r' % config.master_url)
  computer.send(config)
1147
  config.logger.info('slapformat successfully prepared computer.')
Łukasz Nowak's avatar
Łukasz Nowak committed
1148

1149

Vincent Pelletier's avatar
Vincent Pelletier committed
1150
class Config(object):
1151 1152 1153 1154
  key_file = None
  cert_file = None
  alter_network = None
  alter_user = None
1155
  create_tap = None
1156
  computer_xml = None
1157
  computer_json = None
Marco Mariani's avatar
Marco Mariani committed
1158
  input_definition_file = None
1159 1160
  logger = None
  log_file = None
Marco Mariani's avatar
Marco Mariani committed
1161
  output_definition_file = None
1162 1163 1164
  verbose = None
  dry_run = None
  console = None
1165
  software_user = None
1166 1167 1168

  @staticmethod
  def checkRequiredBinary(binary_list):
Łukasz Nowak's avatar
Łukasz Nowak committed
1169 1170
    missing_binary_list = []
    for b in binary_list:
1171 1172
      if type(b) != type([]):
        b = [b]
Łukasz Nowak's avatar
Łukasz Nowak committed
1173
      try:
1174
        callAndRead(b)
Łukasz Nowak's avatar
Łukasz Nowak committed
1175 1176 1177
      except ValueError:
        pass
      except OSError:
Jondy Zhao's avatar
Jondy Zhao committed
1178
        missing_binary_list.append(b[0])
Łukasz Nowak's avatar
Łukasz Nowak committed
1179
    if missing_binary_list:
Vincent Pelletier's avatar
Vincent Pelletier committed
1180 1181
      raise UsageError('Some required binaries are missing or not '
          'functional: %s' % (','.join(missing_binary_list), ))
Łukasz Nowak's avatar
Łukasz Nowak committed
1182 1183 1184 1185 1186 1187 1188

  def setConfig(self, option_dict, configuration_file_path):
    """
    Set options given by parameters.
    """
    self.key_file = None
    self.cert_file = None
1189 1190

    # set up logging
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
1191 1192
    # XXX-Cedric: change code to use global logger
    self.logger = logger
1193

Łukasz Nowak's avatar
Łukasz Nowak committed
1194 1195 1196 1197 1198 1199
    # Set options parameters
    for option, value in option_dict.__dict__.items():
      setattr(self, option, value)

    # Load configuration file
    configuration_parser = ConfigParser.SafeConfigParser()
1200 1201 1202
    if configuration_parser.read(configuration_file_path) != [configuration_file_path]:
      raise UsageError('Cannot find or parse configuration file: %s' % configuration_file_path)

Łukasz Nowak's avatar
Łukasz Nowak committed
1203 1204 1205 1206 1207 1208 1209 1210
    # Merges the arguments and configuration
    for section in ("slapformat", "slapos"):
      configuration_dict = dict(configuration_parser.items(section))
      for key in configuration_dict:
        if not getattr(self, key, None):
          setattr(self, key, configuration_dict[key])

    # setup some nones
1211
    for parameter in ['interface_name', 'partition_base_name', 'user_base_name',
Łukasz Nowak's avatar
Łukasz Nowak committed
1212
        'tap_base_name', 'ipv4_local_network', 'ipv6_interface']:
Łukasz Nowak's avatar
Łukasz Nowak committed
1213 1214
      if getattr(self, parameter, None) is None:
        setattr(self, parameter, None)
1215

1216 1217 1218 1219
    # Backward compatibility
    if not getattr(self, "interface_name", None) \
        and getattr(self, "bridge_name", None):
      setattr(self, "interface_name", self.bridge_name)
1220 1221
      self.logger.warning('bridge_name option is deprecated and should be '
          'replaced by interface_name.')
1222 1223 1224
    if not getattr(self, "create_tap", None) \
        and getattr(self, "no_bridge", None):
      setattr(self, "create_tap", not self.no_bridge)
1225 1226
      self.logger.warning('no_bridge option is deprecated and should be '
          'replaced by create_tap.')
Łukasz Nowak's avatar
Łukasz Nowak committed
1227 1228 1229 1230 1231 1232

    # Set defaults lately
    if self.alter_network is None:
      self.alter_network = 'True'
    if self.alter_user is None:
      self.alter_user = 'True'
1233 1234
    if self.software_user is None:
      self.software_user = 'slapsoft'
1235 1236
    if self.create_tap is None:
      self.create_tap = True
Łukasz Nowak's avatar
Łukasz Nowak committed
1237

1238
    # Configure logging
Łukasz Nowak's avatar
Łukasz Nowak committed
1239 1240 1241 1242
    if self.console:
      self.logger.addHandler(logging.StreamHandler())

    # Convert strings to booleans
1243
    for o in ['alter_network', 'alter_user', 'create_tap']:
1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
      attr = getattr(self, o)
      if isinstance(attr, str):
        if attr.lower() == 'true':
          root_needed = True
          setattr(self, o, True)
        elif attr.lower() == 'false':
          setattr(self, o, False)
        else:
          message = 'Option %r needs to be "True" or "False", wrong value: ' \
              '%r' % (o, getattr(self, o))
          self.logger.error(message)
          raise UsageError(message)
Łukasz Nowak's avatar
Łukasz Nowak committed
1256

1257 1258 1259
    if not self.dry_run:
      if self.alter_user:
        self.checkRequiredBinary(['groupadd', 'useradd', 'usermod'])
1260
      if self.create_tap:
1261
        self.checkRequiredBinary([['tunctl', '-d']])
1262
      if self.alter_network:
1263
        self.checkRequiredBinary(['ip'])
Marco Mariani's avatar
Marco Mariani committed
1264

1265
    # Required, even for dry run
1266
    if self.alter_network and self.create_tap:
1267
      self.checkRequiredBinary(['brctl'])
Łukasz Nowak's avatar
Łukasz Nowak committed
1268

1269 1270 1271 1272
    # Check if root is needed
    if (self.alter_network or self.alter_user) and not self.dry_run:
      root_needed = True
    else:
1273
      root_needed = False
1274

Łukasz Nowak's avatar
Łukasz Nowak committed
1275 1276 1277 1278
    # check root
    if root_needed and os.getuid() != 0:
      message = "Root rights are needed"
      self.logger.error(message)
1279 1280
      sys.stderr.write(message+'\n')
      sys.exit()
Łukasz Nowak's avatar
Łukasz Nowak committed
1281 1282 1283 1284 1285 1286 1287 1288 1289

    if self.log_file:
      if not os.path.isdir(os.path.dirname(self.log_file)):
        # fallback to console only if directory for logs does not exists and
        # continue to run
        raise ValueError('Please create directory %r to store %r log file' % (
          os.path.dirname(self.log_file), self.log_file))
      else:
        file_handler = logging.FileHandler(self.log_file)
Vincent Pelletier's avatar
Vincent Pelletier committed
1290 1291
        file_handler.setFormatter(logging.Formatter("%(asctime)s - "
          "%(name)s - %(levelname)s - %(message)s"))
Łukasz Nowak's avatar
Łukasz Nowak committed
1292 1293
        self.logger.addHandler(file_handler)
        self.logger.info('Configured logging to file %r' % self.log_file)
1294

Łukasz Nowak's avatar
Łukasz Nowak committed
1295 1296 1297 1298 1299 1300
    # 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)

1301 1302 1303 1304 1305
    # 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):
Marco Mariani's avatar
Marco Mariani committed
1306
          self.logger.fatal('File %r does not exist or is not readable.' %
1307 1308 1309
              file_location)
          sys.exit(1)

Łukasz Nowak's avatar
Łukasz Nowak committed
1310 1311 1312 1313
    self.logger.info("Started.")
    if self.verbose:
      self.logger.setLevel(logging.DEBUG)
      self.logger.debug("Verbose mode enabled.")
1314 1315
    if self.dry_run:
      self.logger.info("Dry-run mode enabled.")
1316
    if self.create_tap:
Cédric de Saint Martin's avatar
Cédric de Saint Martin committed
1317
      self.logger.info("Tap creation mode enabled.")
Łukasz Nowak's avatar
Łukasz Nowak committed
1318 1319 1320 1321

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

Marco Mariani's avatar
Marco Mariani committed
1322 1323 1324 1325 1326 1327
    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
1328

1329 1330 1331

def tracing_monkeypatch(config):
  """Substitute os module and callAndRead function with tracing wrappers."""
Vincent Pelletier's avatar
Vincent Pelletier committed
1332 1333
  global os
  global callAndRead
1334

Vincent Pelletier's avatar
Vincent Pelletier committed
1335
  real_callAndRead = callAndRead
Łukasz Nowak's avatar
Łukasz Nowak committed
1336

1337 1338 1339 1340 1341 1342 1343 1344 1345
  os = OS(config)
  if config.dry_run:
    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
    def fake_getpwnam(user):
Vincent Pelletier's avatar
Vincent Pelletier committed
1346
      class result(object):
1347 1348 1349 1350 1351 1352
        pw_uid = 12345
        pw_gid = 54321
      return result
    pwd.getpwnam = fake_getpwnam
  else:
    dry_callAndRead = real_callAndRead
1353

1354 1355 1356 1357 1358
  if config.verbose:
    def logging_callAndRead(argument_list, raise_on_error=True):
      config.logger.debug(' '.join(argument_list))
      return dry_callAndRead(argument_list, raise_on_error)
    callAndRead = logging_callAndRead
1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376


def main(*args):
  "Run default configuration."

  # Parse arguments
  usage = "usage: %s [options] CONFIGURATION_FILE" % sys.argv[0]
  options, configuration_file_path = Parser(usage=usage).check_args(args)
  config = Config()
  try:
    config.setConfig(options, configuration_file_path)
  except UsageError as err:
    sys.stderr.write(err.message + '\n')
    sys.stderr.write("For help use --help\n")
    sys.exit(1)

  tracing_monkeypatch(config)

1377
  # Add delay between 0 and 1 hour
1378 1379
  # XXX should be the contrary: now by default, and cron should have
  # --maximal-delay=3600
1380 1381 1382 1383 1384
  if not config.now:
    duration = float(60*60) * random.random()
    print("Sleeping for %s seconds. To disable this feature, " \
                    "use with --now parameter in manual." % duration)
    time.sleep(duration)
1385 1386 1387 1388 1389
  try:
    run(config)
  except:
    config.logger.exception('Uncaught exception:')
    raise
1390