format.py 49.2 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
40
import glob
Łukasz Nowak's avatar
Łukasz Nowak committed
41
import pwd
Łukasz Nowak's avatar
Łukasz Nowak committed
42
import random
43
import shutil
Łukasz Nowak's avatar
Łukasz Nowak committed
44
import socket
45
import struct
Łukasz Nowak's avatar
Łukasz Nowak committed
46 47
import subprocess
import sys
48
import threading
Łukasz Nowak's avatar
Łukasz Nowak committed
49
import time
50
import traceback
Marco Mariani's avatar
Marco Mariani committed
51 52 53
import zipfile

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

56
import slapos.util
57 58
from slapos.util import mkdir_p
import slapos.slap as slap
59 60


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

Łukasz Nowak's avatar
Łukasz Nowak committed
65

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

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

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

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

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

93

94 95 96
class UsageError(Exception):
  pass

97

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

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

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

112

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

  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
125

126

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

137

Łukasz Nowak's avatar
Łukasz Nowak committed
138 139 140 141 142 143
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()

144

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

150

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

156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
def getIfaceAddressIPv4(iface):
  """return dict containing ipv4 address netmask, network and broadcast address 
  of interface"""
  if not iface in netifaces.interfaces():
    raise ValueError('Could not find interface called %s to use as gateway ' \
                      'for tap network' % iface)
  try:
    addresses_list = netifaces.ifaddresses(iface)[socket.AF_INET]
    if len (addresses_list) > 0:
      
      addresses = addresses_list[0].copy()
      addresses['network'] = str(netaddr.IPNetwork('%s/%s' % (addresses['addr'],
                                          addresses['netmask'])).cidr.network)
      return addresses
    else:
      return {}
  except KeyError:
    raise KeyError('Could not find IPv4 adress on interface %s.' % iface)

def getIPv4SubnetAddressRange(ip_address, mask, size):
  """Check if a given ipaddress can be used to create 'size' 
  host ip address, then return list of ip address in the subnet"""
  ip = netaddr.IPNetwork('%s/%s' % (ip_address, mask))
  # Delete network and default ip_address from the list
180
  ip_list = [x for x in sorted(list(ip))
181 182 183 184 185
              if str(x) != ip_address and x.value != ip.cidr.network.value]
  if len(ip_list) < size:
    raise ValueError('Could not create %s tap interfaces from address %s.' % (
              size, ip_address))
  return ip_list
186

187
def _getDict(obj):
Łukasz Nowak's avatar
Łukasz Nowak committed
188
  """
189
  Serialize an object into dictionaries. List and dict will remains
Łukasz Nowak's avatar
Łukasz Nowak committed
190 191 192 193
  the same, basic type too. But encapsulated object will be returned as dict.
  Set, collections and other aren't handle for now.

  Args:
194
    obj: an object of any type.
Łukasz Nowak's avatar
Łukasz Nowak committed
195 196 197 198

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

202 203
  if isinstance(obj, dict):
    dikt = obj
Łukasz Nowak's avatar
Łukasz Nowak committed
204 205
  else:
    try:
206
      dikt = obj.__dict__
Łukasz Nowak's avatar
Łukasz Nowak committed
207
    except AttributeError:
208 209 210
      return obj

  return {
211 212
    key: _getDict(value) \
    for key, value in dikt.iteritems() \
213 214 215
    # 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
216

217

218
class Computer(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
219
  "Object representing the computer"
220 221
  instance_root = None
  software_root = None
222
  instance_storage_home = None
Łukasz Nowak's avatar
Łukasz Nowak committed
223

224
  def __init__(self, reference, interface=None, addr=None, netmask=None,
225 226
               ipv6_interface=None, software_user='slapsoft',
               tap_gateway_interface=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
227 228 229
    """
    Attributes:
      reference: String, the reference of the computer.
230
      interface: String, the name of the computer's used interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
231 232
    """
    self.reference = str(reference)
233
    self.interface = interface
Łukasz Nowak's avatar
Łukasz Nowak committed
234 235 236
    self.partition_list = []
    self.address = addr
    self.netmask = netmask
237
    self.ipv6_interface = ipv6_interface
238
    self.software_user = software_user
239
    self.tap_gateway_interface = tap_gateway_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
240 241

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

244
  def getAddress(self, allow_tap=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
245
    """
Marco Mariani's avatar
Marco Mariani committed
246
    Return a list of the interface address not attributed to any partition (which
Łukasz Nowak's avatar
Łukasz Nowak committed
247 248 249
    are therefore free for the computer itself).

    Returns:
250
      False if the interface isn't available, else the list of the free addresses.
Łukasz Nowak's avatar
Łukasz Nowak committed
251
    """
252
    if self.interface is None:
Marco Mariani's avatar
Marco Mariani committed
253
      return {'addr': self.address, 'netmask': self.netmask}
Łukasz Nowak's avatar
Łukasz Nowak committed
254 255 256 257 258 259

    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'])
260
    # Going through addresses of the computer's interface
261
    for address_dict in self.interface.getGlobalScopeAddressList():
Łukasz Nowak's avatar
Łukasz Nowak committed
262 263 264 265
      # Comparing with computer's partition addresses
      if address_dict['addr'] not in computer_partition_address_list:
        return address_dict

266
    if allow_tap:
Marco Mariani's avatar
Marco Mariani committed
267
      # all addresses on interface are for partition, so let's add new one
268 269 270 271 272 273
      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
274
    raise NoAddressOnInterface('No valid IPv6 found on %s.' % self.interface.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
275

276
  def send(self, conf):
Łukasz Nowak's avatar
Łukasz Nowak committed
277 278 279 280 281 282
    """
    Send a marshalled dictionary of the computer object serialized via_getDict.
    """

    slap_instance = slap.slap()
    connection_dict = {}
283 284 285 286
    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
287
                                       **connection_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
288
    slap_computer = slap_instance.registerComputer(self.reference)
Marco Mariani's avatar
Marco Mariani committed
289

290
    if conf.dry_run:
291
      return
292
    try:
293
      slap_computer.updateConfiguration(xml_marshaller.xml_marshaller.dumps(_getDict(self)))
294
    except slap.NotFoundError as error:
295 296 297
      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 "
298
          "like 'COMP-123' and is correct.\nError is : 404 Not Found." % error)
Łukasz Nowak's avatar
Łukasz Nowak committed
299

300
  def dump(self, path_to_xml, path_to_json, logger):
Łukasz Nowak's avatar
Łukasz Nowak committed
301 302 303 304 305
    """
    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
306
      path_to_json: String, path to the JSON version to save.
Łukasz Nowak's avatar
Łukasz Nowak committed
307 308 309
    """

    computer_dict = _getDict(self)
310 311 312 313 314

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

315
    new_xml = xml_marshaller.xml_marshaller.dumps(computer_dict)
Marco Mariani's avatar
Marco Mariani committed
316 317 318 319
    new_pretty_xml = prettify_xml(new_xml)

    path_to_archive = path_to_xml + '.zip'

320
    if os.path.exists(path_to_archive) and os.path.exists(path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
321 322 323 324 325 326
      # 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

327
    if os.path.exists(path_to_xml):
328 329 330 331 332
      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
333
                    path_to_archive + time.strftime('_broken_%Y%m%d-%H:%M'))
334 335 336 337
        try:
          self.backup_xml(path_to_archive, path_to_xml)
        except:
          # give up trying
338
          logger.exception("Can't backup %s:", path_to_xml)
Marco Mariani's avatar
Marco Mariani committed
339

Marco Mariani's avatar
Marco Mariani committed
340 341
    with open(path_to_xml, 'wb') as fout:
      fout.write(new_pretty_xml)
Marco Mariani's avatar
Marco Mariani committed
342 343

  def backup_xml(self, path_to_archive, path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
344 345 346
    """
    Stores a copy of the current xml file to an historical archive.
    """
Marco Mariani's avatar
Marco Mariani committed
347
    xml_content = open(path_to_xml).read()
Marco Mariani's avatar
Marco Mariani committed
348
    saved_filename = os.path.basename(path_to_xml) + time.strftime('.%Y%m%d-%H:%M')
Marco Mariani's avatar
Marco Mariani committed
349 350 351 352

    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
353
  @classmethod
354
  def load(cls, path_to_xml, reference, ipv6_interface, tap_gateway_interface):
Łukasz Nowak's avatar
Łukasz Nowak committed
355 356 357 358 359 360 361 362
    """
    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:
363
      A Computer object.
Łukasz Nowak's avatar
Łukasz Nowak committed
364 365
    """

366
    dumped_dict = xml_marshaller.xml_marshaller.loads(open(path_to_xml).read())
Łukasz Nowak's avatar
Łukasz Nowak committed
367 368 369

    # Reconstructing the computer object from the xml
    computer = Computer(
370 371 372 373 374
        reference=reference,
        addr=dumped_dict['address'],
        netmask=dumped_dict['netmask'],
        ipv6_interface=ipv6_interface,
        software_user=dumped_dict.get('software_user', 'slapsoft'),
375
        tap_gateway_interface=tap_gateway_interface,
Łukasz Nowak's avatar
Łukasz Nowak committed
376 377 378 379 380 381 382 383 384 385 386
    )

    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'])
387 388 389 390 391
        if tap_gateway_interface:
          tap.ipv4_addr = partition_dict['tap'].get('ipv4_addr', '')
          tap.ipv4_netmask = partition_dict['tap'].get('ipv4_netmask', '')
          tap.ipv4_gateway = partition_dict['tap'].get('ipv4_gateway', '')
          tap.ipv4_network = partition_dict['tap'].get('ipv4_network', '')
Łukasz Nowak's avatar
Łukasz Nowak committed
392 393 394 395
      else:
        tap = Tap(partition_dict['reference'])

      address_list = partition_dict['address_list']
396
      external_storage_list = partition_dict.get('external_storage_list', [])
Łukasz Nowak's avatar
Łukasz Nowak committed
397 398

      partition = Partition(
399 400 401 402 403
          reference=partition_dict['reference'],
          path=partition_dict['path'],
          user=user,
          address_list=address_list,
          tap=tap,
404
          external_storage_list=external_storage_list,
Łukasz Nowak's avatar
Łukasz Nowak committed
405 406 407 408 409 410
      )

      computer.partition_list.append(partition)

    return computer

411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
  def _speedHackAddAllOldIpsToInterface(self):
    """
    Speed hack:
    Blindly add all IPs from existing configuration, just to speed up actual
    computer configuration later on.
    """
    # XXX-TODO: only add an address if it doesn't already exist.
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    elif self.interface:
      interface_name = self.interface.name
    else:
      return

    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', interface_name])
      except ValueError:
        pass

438 439 440 441 442 443 444
  def _addUniqueLocalAddressIpv6(self, interface_name):
    """
    Create a unique local address in the interface interface_name, so that
    slapformat can build upon this.
    See https://en.wikipedia.org/wiki/Unique_local_address.
    """
    command = 'ip address add dev %s fd00::1/64' % interface_name
445
    callAndRead(command.split())
446

447
  def construct(self, alter_user=True, alter_network=True, create_tap=True, use_unique_local_address_block=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
448 449 450 451
    """
    Construct the computer object as it is.
    """
    if alter_network and self.address is not None:
452
      self.interface.addAddr(self.address, self.netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
453

454
    if use_unique_local_address_block and alter_network:
455 456 457
      if self.ipv6_interface:
        network_interface_name = self.ipv6_interface
      else:
458
        network_interface_name = self.interface.name
459 460
      self._addUniqueLocalAddressIpv6(network_interface_name)

461
    for path in self.instance_root, self.software_root:
Łukasz Nowak's avatar
Łukasz Nowak committed
462
      if not os.path.exists(path):
Marco Mariani's avatar
Marco Mariani committed
463
        os.makedirs(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
464
      else:
Marco Mariani's avatar
Marco Mariani committed
465
        os.chmod(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
466

467 468
    # own self.software_root by software user
    slapsoft = User(self.software_user)
Łukasz Nowak's avatar
Łukasz Nowak committed
469 470 471
    slapsoft.path = self.software_root
    if alter_user:
      slapsoft.create()
Łukasz Nowak's avatar
Łukasz Nowak committed
472
      slapsoft_pw = pwd.getpwnam(slapsoft.name)
473
      os.chown(slapsoft.path, slapsoft_pw.pw_uid, slapsoft_pw.pw_gid)
Marco Mariani's avatar
Marco Mariani committed
474
    os.chmod(self.software_root, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
475

476 477 478
    # get list of instance external storage if exist
    instance_external_list = []
    if self.instance_storage_home:
479 480 481 482 483 484 485
      # get all /XXX/dataN where N is a digit
      data_list = glob.glob(os.path.join(self.instance_storage_home, 'data*'))
      for i in range(0, len(data_list)):
        data_path = data_list.pop()
        the_digit = os.path.basename(data_path).split('data')[-1]
        if the_digit.isdigit():
          instance_external_list.append(data_path)
486

487 488 489 490 491 492 493 494
    tap_address_list = []
    if alter_network and self.tap_gateway_interface and create_tap:
      gateway_addr_dict = getIfaceAddressIPv4(self.tap_gateway_interface)
      tap_address_list = getIPv4SubnetAddressRange(gateway_addr_dict['addr'],
                              gateway_addr_dict['netmask'],
                              len(self.partition_list))
      assert(len(self.partition_list) <= len(tap_address_list))

495 496
    if alter_network:
      self._speedHackAddAllOldIpsToInterface()
497

498 499 500 501 502 503
    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]
504 505
        partition.external_storage_list = ['%s/%s' % (path, partition.reference)
                                            for path in instance_external_list]
506 507 508 509 510 511 512 513 514
        if alter_user:
          partition.user.create()

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

515
        if alter_network and create_tap:
516
          # In case it has to be  attached to the TAP network device, only one
517 518
          # is necessary for the interface to assert carrier
          if self.interface.attach_to_tap and partition_index == 0:
519
            partition.tap.createWithOwner(owner, attach_to_tap=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
520
          else:
521
            partition.tap.createWithOwner(owner)
522 523 524 525 526 527
          # If tap_gateway_interface is specified, we don't add tap to bridge
          # but we create route for this tap
          if not self.tap_gateway_interface:
            self.interface.addTap(partition.tap)
          else:
            next_ipv4_addr = '%s' % tap_address_list.pop(0)
528 529 530 531 532 533 534
            if not partition.tap.ipv4_addr:
              # define new ipv4 address for this tap
              partition.tap.ipv4_addr = next_ipv4_addr
              partition.tap.ipv4_netmask = gateway_addr_dict['netmask']
              partition.tap.ipv4_gateway = gateway_addr_dict['addr']
              partition.tap.ipv4_network = gateway_addr_dict['network']
            partition.tap.createRoutes()
535 536 537

        # Reconstructing partition's directory
        partition.createPath(alter_user)
538
        partition.createExternalPath(alter_user)
539 540 541 542 543

        # 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
544
        if not partition.address_list:
545
          # regenerate
546 547
          partition.address_list.append(self.interface.addIPv4LocalAddress())
          partition.address_list.append(self.interface.addAddr())
548 549 550 551 552
        elif alter_network:
          # regenerate list of addresses
          old_partition_address_list = partition.address_list
          partition.address_list = []
          if len(old_partition_address_list) != 2:
553 554 555
            raise ValueError(
              'There should be exactly 2 stored addresses. Got: %r' %
              (old_partition_address_list,))
Marco Mariani's avatar
Marco Mariani committed
556 557
          if not any(netaddr.valid_ipv6(q['addr'])
                     for q in old_partition_address_list):
558
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
559 560
          if not any(netaddr.valid_ipv4(q['addr'])
                     for q in old_partition_address_list):
561
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
562

563 564
          for address in old_partition_address_list:
            if netaddr.valid_ipv6(address['addr']):
565
              partition.address_list.append(self.interface.addAddr(
Vincent Pelletier's avatar
Vincent Pelletier committed
566 567
                address['addr'],
                address['netmask']))
568
            elif netaddr.valid_ipv4(address['addr']):
569
              partition.address_list.append(self.interface.addIPv4LocalAddress(
Vincent Pelletier's avatar
Vincent Pelletier committed
570
                address['addr']))
571 572 573
            else:
              raise ValueError('Address %r is incorrect' % address['addr'])
    finally:
574
      if alter_network and create_tap and self.interface.attach_to_tap:
575 576 577 578
        try:
          self.partition_list[0].tap.detach()
        except IndexError:
          pass
Łukasz Nowak's avatar
Łukasz Nowak committed
579

580

581
class Partition(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
582 583
  "Represent a computer partition"

584
  def __init__(self, reference, path, user, address_list, tap, external_storage_list=[]):
Łukasz Nowak's avatar
Łukasz Nowak committed
585 586 587 588 589 590 591
    """
    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.
592
      external_storage_list: Base path list of folder to format for data storage
Łukasz Nowak's avatar
Łukasz Nowak committed
593 594 595 596 597 598 599
    """

    self.reference = str(reference)
    self.path = str(path)
    self.user = user
    self.address_list = address_list or []
    self.tap = tap
600
    self.external_storage_list = []
Łukasz Nowak's avatar
Łukasz Nowak committed
601 602 603 604 605 606

  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
607 608
    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
609 610
    """

611
    self.path = os.path.abspath(self.path)
Łukasz Nowak's avatar
Łukasz Nowak committed
612
    owner = self.user if self.user else User('root')
613
    if not os.path.exists(self.path):
Marco Mariani's avatar
Marco Mariani committed
614
      os.mkdir(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
615
    if alter_user:
Łukasz Nowak's avatar
Łukasz Nowak committed
616
      owner_pw = pwd.getpwnam(owner.name)
617
      os.chown(self.path, owner_pw.pw_uid, owner_pw.pw_gid)
618
    os.chmod(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
619

620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
  def createExternalPath(self, alter_user=True):
    """
    Create and external directory of the partition, assign to the partition user
    and give it the 750 permission. In case if path exists just modifies it.
    """

    for path in self.external_storage_list:
      storage_path = os.path.abspath(path)
      owner = self.user if self.user else User('root')
      if not os.path.exists(storage_path):
        os.mkdir(storage_path, 0o750)
      if alter_user:
        owner_pw = pwd.getpwnam(owner.name)
        os.chown(storage_path, owner_pw.pw_uid, owner_pw.pw_gid)
      os.chmod(storage_path, 0o750)
635

636
class User(object):
Marco Mariani's avatar
Marco Mariani committed
637 638
  """User: represent and manipulate a user on the system."""

639
  path = None
Łukasz Nowak's avatar
Łukasz Nowak committed
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665

  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
666
    grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
667
    try:
Jondy Zhao's avatar
Jondy Zhao committed
668
      grp.getgrnam(grpname)
Łukasz Nowak's avatar
Łukasz Nowak committed
669
    except KeyError:
Jondy Zhao's avatar
Jondy Zhao committed
670
      callAndRead(['groupadd', grpname])
Łukasz Nowak's avatar
Łukasz Nowak committed
671

672
    user_parameter_list = ['-d', self.path, '-g', self.name]
Łukasz Nowak's avatar
Łukasz Nowak committed
673 674 675 676
    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
677
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
678
    except KeyError:
679
      user_parameter_list.append('-r')
Łukasz Nowak's avatar
Łukasz Nowak committed
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695
      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
696
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
697 698 699 700
      return True
    except KeyError:
      return False

701

702
class Tap(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
703
  "Tap represent a tap interface on the system"
704 705 706
  IFF_TAP = 0x0002
  TUNSETIFF = 0x400454ca
  KEEP_TAP_ATTACHED_EVENT = threading.Event()
Łukasz Nowak's avatar
Łukasz Nowak committed
707 708 709 710 711

  def __init__(self, tap_name):
    """
    Attributes:
        tap_name: String, the name of the tap interface.
712 713 714
        ipv4_address: String, local ipv4 to route to this tap
        ipv4_network: String, netmask to use when configure route for this tap
        gateway_ipv4: String, ipv4 of gateway to be used to reach local network
Łukasz Nowak's avatar
Łukasz Nowak committed
715 716 717
    """

    self.name = str(tap_name)
718 719 720 721
    self.ipv4_addr = ""
    self.ipv4_netmask = ""
    self.ipv4_gateway = ""
    self.ipv4_network = ""
Łukasz Nowak's avatar
Łukasz Nowak committed
722 723 724 725

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

726 727 728
  def attach(self):
    """
    Attach to the TAP interface, meaning  that it just opens the TAP interface
Marco Mariani's avatar
Marco Mariani committed
729
    and waits for the caller to notify that it can be safely detached.
730 731 732 733 734 735 736 737

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

738
    In case of bridge:
739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
    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))

757
    except IOError as error:
758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778
      # 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
779 780 781 782 783 784 785 786 787
    """
    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):
788
      owner_id = open(check_file).read().strip()
Łukasz Nowak's avatar
Łukasz Nowak committed
789
      try:
790 791
        owner_id = int(owner_id)
      except ValueError:
Łukasz Nowak's avatar
Łukasz Nowak committed
792
        pass
793
    if owner_id != pwd.getpwnam(owner.name).pw_uid:
Łukasz Nowak's avatar
Łukasz Nowak committed
794 795 796
      callAndRead(['tunctl', '-t', self.name, '-u', owner.name])
    callAndRead(['ip', 'link', 'set', self.name, 'up'])

797 798 799
    if attach_to_tap:
      threading.Thread(target=self.attach).start()

800
  def createRoutes(self):
801 802 803
    """
    Configure ipv4 route to reach this interface from local network
    """
804 805 806 807 808 809 810 811 812
    if self.ipv4_addr:
      # Check if this route exits
      code, result = callAndRead(['ip', 'route', 'show', self.ipv4_addr])
      if code == 0 and self.ipv4_addr in result and self.name in result:
        return
      callAndRead(['route', 'add', '-host', self.ipv4_addr, 'dev', self.name])
    else:
      raise ValueError("%s should not be empty. No ipv4 address assigned to %s" %
                         (self.ipv4_addr, self.name))
813

814
class Interface(object):
Marco Mariani's avatar
Marco Mariani committed
815
  """Represent a network interface on the system"""
Łukasz Nowak's avatar
Łukasz Nowak committed
816

817
  def __init__(self, logger, name, ipv4_local_network, ipv6_interface=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
818 819
    """
    Attributes:
820
        name: String, the name of the interface
Łukasz Nowak's avatar
Łukasz Nowak committed
821 822
    """

823
    self.logger = logger
Łukasz Nowak's avatar
Łukasz Nowak committed
824 825
    self.name = str(name)
    self.ipv4_local_network = ipv4_local_network
826
    self.ipv6_interface = ipv6_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
827

828
    # Attach to TAP  network interface, only if the  interface interface does not
829
    # report carrier
830
    _, result = callAndRead(['ip', 'addr', 'list', self.name])
831 832
    self.attach_to_tap = 'DOWN' in result.split('\n', 1)[0]

833
  # XXX no __getinitargs__, as instances of this class are never deserialized.
Łukasz Nowak's avatar
Łukasz Nowak committed
834 835

  def getIPv4LocalAddressList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
836 837 838 839
    """
    Returns currently configured local IPv4 addresses which are in
    ipv4_local_network
    """
Łukasz Nowak's avatar
Łukasz Nowak committed
840 841
    if not socket.AF_INET in netifaces.ifaddresses(self.name):
      return []
Marco Mariani's avatar
Marco Mariani committed
842 843 844 845 846 847 848 849 850
    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
851 852 853

  def getGlobalScopeAddressList(self):
    """Returns currently configured global scope IPv6 addresses"""
854 855 856 857
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
858
    try:
Marco Mariani's avatar
Marco Mariani committed
859
      address_list = [
860 861 862 863
          q
          for q in netifaces.ifaddresses(interface_name)[socket.AF_INET6]
          if isGlobalScopeAddress(q['addr'].split('%')[0])
      ]
864
    except KeyError:
865
      raise ValueError("%s must have at least one IPv6 address assigned" %
866
                         interface_name)
Jondy Zhao's avatar
Jondy Zhao committed
867 868
    if sys.platform == 'cygwin':
      for q in address_list:
869
        q.setdefault('netmask', 'FFFF:FFFF:FFFF:FFFF::')
Łukasz Nowak's avatar
Łukasz Nowak committed
870 871 872 873 874 875 876 877 878 879
    # 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):
880
    """Returns list of interfaces already present on bridge"""
Łukasz Nowak's avatar
Łukasz Nowak committed
881
    interface_list = []
882
    _, result = callAndRead(['brctl', 'show'])
883
    in_interface = False
Łukasz Nowak's avatar
Łukasz Nowak committed
884 885 886 887
    for line in result.split('\n'):
      if len(line.split()) > 1:
        if self.name in line:
          interface_list.append(line.split()[-1])
888
          in_interface = True
Łukasz Nowak's avatar
Łukasz Nowak committed
889
          continue
890
        if in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
891
          break
892
      elif in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908
        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):
909
    """Adds system address to interface
910

Łukasz Nowak's avatar
Łukasz Nowak committed
911 912 913 914 915 916 917
    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
918 919 920 921
      if self.ipv6_interface:
        interface_name = self.ipv6_interface
      else:
        interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
922 923 924
    else:
      af = socket.AF_INET
      address_string = '%s/%s' % (address, netmaskToPrefixIPv4(netmask))
925
      interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
926 927 928

    # check if address is already took by any other interface
    for interface in netifaces.interfaces():
929
      if interface != interface_name:
Łukasz Nowak's avatar
Łukasz Nowak committed
930 931
        address_dict = netifaces.ifaddresses(interface)
        if af in address_dict:
932
          if address in [q['addr'].split('%')[0] for q in address_dict[af]]:
Łukasz Nowak's avatar
Łukasz Nowak committed
933 934
            return False

Vincent Pelletier's avatar
Vincent Pelletier committed
935 936
    if not af in netifaces.ifaddresses(interface_name) \
        or not address in [q['addr'].split('%')[0]
Marco Mariani's avatar
Marco Mariani committed
937 938
                           for q in netifaces.ifaddresses(interface_name)[af]
                           ]:
Łukasz Nowak's avatar
Łukasz Nowak committed
939
      # add an address
940
      callAndRead(['ip', 'addr', 'add', address_string, 'dev', interface_name])
941 942 943 944 945

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

Łukasz Nowak's avatar
Łukasz Nowak committed
946 947
      # wait few moments
      time.sleep(2)
948 949 950 951 952 953

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

    # check existence on interface for ipv6
954
    _, result = callAndRead(['ip', '-6', 'addr', 'list', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
955 956 957 958
    for l in result.split('\n'):
      if address in l:
        if 'tentative' in l:
          # duplicate, remove
Marco Mariani's avatar
Marco Mariani committed
959
          callAndRead(['ip', 'addr', 'del', address_string, 'dev', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
960 961 962 963 964 965 966 967 968 969 970 971 972
          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()
973 974
      if (dict(addr=addr, netmask=netmask) not in
            self.getIPv4LocalAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
975 976 977 978 979 980 981 982 983
        # 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"""
984
    netmask = str(netaddr.IPNetwork(self.ipv4_local_network).netmask) if sys.platform == 'cygwin' \
985
             else '255.255.255.255'
Łukasz Nowak's avatar
Łukasz Nowak committed
986 987 988 989
    local_address_list = self.getIPv4LocalAddressList()
    if addr is None:
      return self._generateRandomIPv4Address(netmask)
    elif dict(addr=addr, netmask=netmask) not in local_address_list:
990 991 992
      if self._addSystemAddress(addr, netmask, False):
        return dict(addr=addr, netmask=netmask)
      else:
993
        self.logger.warning('Impossible to add old local IPv4 %s. Generating '
994
            'new IPv4 address.' % addr)
995
        return self._generateRandomIPv4Address(netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
996 997 998 999
    else:
      # confirmed to be configured
      return dict(addr=addr, netmask=netmask)

Marco Mariani's avatar
Marco Mariani committed
1000
  def addAddr(self, addr=None, netmask=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
1001
    """
1002
    Adds IP address to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
1003

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

1006
    If addr is specified and does not exists on interface, tries to add given
Vincent Pelletier's avatar
Vincent Pelletier committed
1007 1008
    address. If it is not possible (ex. because network changed) calculates new
    address.
Łukasz Nowak's avatar
Łukasz Nowak committed
1009 1010

    Args:
1011
      addr: Wished address to be added to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
1012 1013 1014 1015 1016 1017 1018
      netmask: Wished netmask to be used.

    Returns:
      Tuple of (address, netmask).

    Raises:
      AddressGenerationError: Couldn't construct valid address with existing
1019 1020
          one's on the interface.
      NoAddressOnInterface: There's no address on the interface to construct
Łukasz Nowak's avatar
Łukasz Nowak committed
1021 1022
          an address with.
    """
1023
    # Getting one address of the interface as base of the next addresses
1024 1025 1026 1027
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
1028
    interface_addr_list = self.getGlobalScopeAddressList()
Łukasz Nowak's avatar
Łukasz Nowak committed
1029 1030

    # No address found
1031 1032 1033
    if len(interface_addr_list) == 0:
      raise NoAddressOnInterface(interface_name)
    address_dict = interface_addr_list[0]
Łukasz Nowak's avatar
Łukasz Nowak committed
1034 1035

    if addr is not None:
1036
      if dict(addr=addr, netmask=netmask) in interface_addr_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
1037 1038 1039 1040
        # 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
1041
        interface_network = netaddr.ip.IPNetwork('%s/%s' % (address_dict['addr'],
Łukasz Nowak's avatar
Łukasz Nowak committed
1042
          netmaskToPrefixIPv6(address_dict['netmask'])))
Vincent Pelletier's avatar
Vincent Pelletier committed
1043 1044
        requested_network = netaddr.ip.IPNetwork('%s/%s' % (addr,
          netmaskToPrefixIPv6(netmask)))
1045
        if interface_network.network == requested_network.network:
Łukasz Nowak's avatar
Łukasz Nowak committed
1046 1047 1048 1049
          # same network, try to add
          if self._addSystemAddress(addr, netmask):
            # succeed, return it
            return dict(addr=addr, netmask=netmask)
1050
          else:
1051
            self.logger.warning('Impossible to add old public IPv6 %s. '
1052
                'Generating new IPv6 address.' % addr)
Łukasz Nowak's avatar
Łukasz Nowak committed
1053 1054 1055 1056 1057

    # 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
1058 1059
      addr = ':'.join(address_dict['addr'].split(':')[:-1] + ['%x' % (
        random.randint(1, 65000), )])
Łukasz Nowak's avatar
Łukasz Nowak committed
1060
      socket.inet_pton(socket.AF_INET6, addr)
1061 1062
      if (dict(addr=addr, netmask=netmask) not in
            self.getGlobalScopeAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
1063 1064 1065 1066 1067 1068 1069
        # Checking the validity of the IPv6 address
        if self._addSystemAddress(addr, netmask):
          return dict(addr=addr, netmask=netmask)
        try_num -= 1

    raise AddressGenerationError(addr)

1070

1071 1072
def parse_computer_definition(conf, definition_path):
  conf.logger.info('Using definition file %r' % definition_path)
1073 1074 1075 1076 1077 1078 1079 1080
  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
1081
    address, netmask = computer_definition.get('computer', 'address').split('/')
1082 1083
  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
1084 1085 1086 1087
    interface = Interface(logger=conf.logger,
                          name=conf.interface_name,
                          ipv4_local_network=conf.ipv4_local_network,
                          ipv6_interface=conf.ipv6_interface)
1088
  computer = Computer(
1089
      reference=conf.computer_id,
1090 1091 1092
      interface=interface,
      addr=address,
      netmask=netmask,
1093
      ipv6_interface=conf.ipv6_interface,
1094
      software_user=computer_definition.get('computer', 'software_user'),
1095
      tap_gateway_interface=conf.tap_gateway_interface,
1096
  )
1097
  partition_list = []
1098
  for partition_number in range(int(conf.partition_amount)):
1099 1100 1101 1102 1103 1104 1105
    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
1106
    partition = Partition(reference=computer_definition.get(section, 'pathname'),
1107
                          path=os.path.join(conf.instance_root,
Marco Mariani's avatar
Marco Mariani committed
1108 1109 1110 1111 1112
                                            computer_definition.get(section, 'pathname')),
                          user=user,
                          address_list=address_list,
                          tap=tap)
    partition_list.append(partition)
1113 1114 1115 1116
  computer.partition_list = partition_list
  return computer


1117
def parse_computer_xml(conf, xml_path):
Marco Mariani's avatar
Marco Mariani committed
1118 1119 1120 1121 1122
  interface = Interface(logger=conf.logger,
                        name=conf.interface_name,
                        ipv4_local_network=conf.ipv4_local_network,
                        ipv6_interface=conf.ipv6_interface)

1123
  if os.path.exists(xml_path):
1124
    conf.logger.debug('Loading previous computer data from %r' % xml_path)
1125
    computer = Computer.load(xml_path,
1126
                             reference=conf.computer_id,
1127 1128
                             ipv6_interface=conf.ipv6_interface,
                             tap_gateway_interface=conf.tap_gateway_interface)
1129
    # Connect to the interface defined by the configuration
Marco Mariani's avatar
Marco Mariani committed
1130
    computer.interface = interface
1131 1132
  else:
    # If no pre-existent configuration found, create a new computer object
1133
    conf.logger.warning('Creating new computer data with id %r', conf.computer_id)
1134
    computer = Computer(
1135
      reference=conf.computer_id,
Marco Mariani's avatar
Marco Mariani committed
1136
      interface=interface,
1137 1138
      addr=None,
      netmask=None,
1139 1140
      ipv6_interface=conf.ipv6_interface,
      software_user=conf.software_user,
1141
      tap_gateway_interface=conf.tap_gateway_interface,
1142 1143
    )

1144
  partition_amount = int(conf.partition_amount)
1145 1146
  existing_partition_amount = len(computer.partition_list)

1147 1148 1149 1150 1151 1152 1153 1154
  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
1155 1156 1157 1158

  for i in range(existing_partition_amount, partition_amount):
    # add new partitions
    partition = Partition(
1159 1160 1161 1162 1163 1164 1165
        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
1166
    computer.partition_list.append(partition)
1167 1168 1169 1170

  return computer


1171
def write_computer_definition(conf, computer):
1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186
  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)
1187 1188
  computer_definition.write(open(conf.output_definition_file, 'w'))
  conf.logger.info('Stored computer definition in %r' % conf.output_definition_file)
1189 1190


1191
def random_delay(conf):
1192 1193 1194
  # Add delay between 0 and 1 hour
  # XXX should be the contrary: now by default, and cron should have
  # --maximal-delay=3600
1195
  if not conf.now:
1196
    duration = float(60 * 60) * random.random()
Marco Mariani's avatar
Marco Mariani committed
1197 1198
    conf.logger.info('Sleeping for %s seconds. To disable this feature, '
                     'use with --now parameter in manual.' % duration)
1199 1200 1201
    time.sleep(duration)


1202 1203
def do_format(conf):
  random_delay(conf)
1204

1205 1206
  if conf.input_definition_file:
    computer = parse_computer_definition(conf, conf.input_definition_file)
1207 1208
  else:
    # no definition file, figure out computer
1209
    computer = parse_computer_xml(conf, conf.computer_xml)
1210

1211 1212
  computer.instance_root = conf.instance_root
  computer.software_root = conf.software_root
1213
  computer.instance_storage_home = conf.instance_storage_home
1214 1215
  conf.logger.info('Updating computer')
  address = computer.getAddress(conf.create_tap)
1216 1217 1218
  computer.address = address['addr']
  computer.netmask = address['netmask']

1219 1220
  if conf.output_definition_file:
    write_computer_definition(conf, computer)
1221
  
1222 1223
  computer.construct(alter_user=conf.alter_user,
                     alter_network=conf.alter_network,
1224
                     create_tap=conf.create_tap,
1225
                     use_unique_local_address_block=conf.use_unique_local_address_block)
1226

1227 1228
  if getattr(conf, 'certificate_repository_path', None):
    mkdir_p(conf.certificate_repository_path, mode=0o700)
1229

1230
  # Dumping and sending to the erp5 the current configuration
1231 1232 1233 1234 1235 1236
  if not conf.dry_run:
    computer.dump(path_to_xml=conf.computer_xml,
                  path_to_json=conf.computer_json,
                  logger=conf.logger)
  conf.logger.info('Posting information to %r' % conf.master_url)
  computer.send(conf)
1237
  conf.logger.info('slapos successfully prepared the computer.')
Łukasz Nowak's avatar
Łukasz Nowak committed
1238

1239

1240
class FormatConfig(object):
1241 1242 1243 1244
  key_file = None
  cert_file = None
  alter_network = None
  alter_user = None
1245
  create_tap = None
1246
  computer_xml = None
1247
  computer_json = None
Marco Mariani's avatar
Marco Mariani committed
1248
  input_definition_file = None
1249
  log_file = None
Marco Mariani's avatar
Marco Mariani committed
1250
  output_definition_file = None
1251
  dry_run = None
1252
  software_user = None
1253
  tap_gateway_interface = None
1254
  use_unique_local_address_block = None
1255
  instance_storage_home = None
1256

1257 1258 1259
  def __init__(self, logger):
    self.logger = logger