format.py 57.5 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
import logging
37
import math
Łukasz Nowak's avatar
Łukasz Nowak committed
38 39 40
import netaddr
import netifaces
import os
41
import glob
Łukasz Nowak's avatar
Łukasz Nowak committed
42
import pwd
Łukasz Nowak's avatar
Łukasz Nowak committed
43
import random
44
import shutil
Łukasz Nowak's avatar
Łukasz Nowak committed
45
import socket
46
import struct
Łukasz Nowak's avatar
Łukasz Nowak committed
47 48
import subprocess
import sys
49
import threading
Łukasz Nowak's avatar
Łukasz Nowak committed
50
import time
51
import traceback
Marco Mariani's avatar
Marco Mariani committed
52
import zipfile
53 54
import platform
from urllib2 import urlopen
Marco Mariani's avatar
Marco Mariani committed
55 56

import lxml.etree
57
import xml_marshaller.xml_marshaller
Marco Mariani's avatar
Marco Mariani committed
58

59
import slapos.util
60 61
from slapos.util import mkdir_p
import slapos.slap as slap
62
from slapos import version
63
from slapos import manager as slapmanager
64

65 66 67 68

logger = logging.getLogger("slapos.format")


Marco Mariani's avatar
Marco Mariani committed
69 70 71 72
def prettify_xml(xml):
  root = lxml.etree.fromstring(xml)
  return lxml.etree.tostring(root, pretty_print=True)

Łukasz Nowak's avatar
Łukasz Nowak committed
73

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

Vincent Pelletier's avatar
Vincent Pelletier committed
77 78
  _os = os

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

  def _addWrapper(self, name):
    def wrapper(*args, **kw):
90
      arg_list = [repr(x) for x in args] + [
91 92
          '%s=%r' % (x, y) for x, y in kw.iteritems()
      ]
93
      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

135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
def getPublicIPv4Address():
  test_list = [
    { "url": 'https://api.ipify.org/?format=json' , "json_key": "ip"},
    { "url": 'http://httpbin.org/ip', "json_key": "origin"},
    { "url": 'http://jsonip.com', "json_key": "ip"}]
  previous = None
  ipv4 = None
  for test in test_list:
    if ipv4 is not None:
      previous = ipv4
    try:
      ipv4 = json.load(urlopen(test["url"]))[test["json_key"]]
    except:
      ipv4 = None
    if ipv4 is not None and ipv4 == previous:
      return ipv4

Łukasz Nowak's avatar
Łukasz Nowak committed
152
def callAndRead(argument_list, raise_on_error=True):
Marco Mariani's avatar
Marco Mariani committed
153 154 155
  popen = subprocess.Popen(argument_list,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.STDOUT)
Łukasz Nowak's avatar
Łukasz Nowak committed
156 157
  result = popen.communicate()[0]
  if raise_on_error and popen.returncode != 0:
158
    raise ValueError('Issue while invoking %r, result was:\n%s' % (
Marco Mariani's avatar
Marco Mariani committed
159
                     argument_list, result))
Łukasz Nowak's avatar
Łukasz Nowak committed
160 161
  return popen.returncode, result

162

Łukasz Nowak's avatar
Łukasz Nowak committed
163 164 165 166 167 168
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()

169

Łukasz Nowak's avatar
Łukasz Nowak committed
170 171 172 173 174
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)]

175

Łukasz Nowak's avatar
Łukasz Nowak committed
176 177 178 179 180
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)]

181
def getIfaceAddressIPv4(iface):
182
  """return dict containing ipv4 address netmask, network and broadcast address
183 184 185 186 187 188 189
  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:
190

191 192 193 194 195 196 197 198 199 200
      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):
201
  """Check if a given ipaddress can be used to create 'size'
202 203 204
  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
205
  ip_list = [x for x in sorted(list(ip))
206 207 208 209 210
              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
211

212
def _getDict(obj):
Łukasz Nowak's avatar
Łukasz Nowak committed
213
  """
214
  Serialize an object into dictionaries. List and dict will remains
Łukasz Nowak's avatar
Łukasz Nowak committed
215 216 217 218
  the same, basic type too. But encapsulated object will be returned as dict.
  Set, collections and other aren't handle for now.

  Args:
219
    obj: an object of any type.
Łukasz Nowak's avatar
Łukasz Nowak committed
220 221 222 223

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

227 228
  if isinstance(obj, dict):
    dikt = obj
Łukasz Nowak's avatar
Łukasz Nowak committed
229 230
  else:
    try:
231
      dikt = obj.__dict__
Łukasz Nowak's avatar
Łukasz Nowak committed
232
    except AttributeError:
233 234 235
      return obj

  return {
236 237
    key: _getDict(value) \
    for key, value in dikt.iteritems() \
238
    # do not attempt to serialize logger: it is both useless and recursive.
239 240
    # do not serialize attributes starting with "_", let the classes have some privacy
    if not key.startswith("_")
241
  }
Łukasz Nowak's avatar
Łukasz Nowak committed
242

243

244
class Computer(object):
245
  """Object representing the computer"""
Łukasz Nowak's avatar
Łukasz Nowak committed
246

247
  def __init__(self, reference, interface=None, addr=None, netmask=None,
248
               ipv6_interface=None, software_user='slapsoft',
249 250
               tap_gateway_interface=None,
               instance_root=None, software_root=None, instance_storage_home=None,
251
               partition_list=None, config=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
252 253
    """
    Attributes:
254 255
      reference: str, the reference of the computer.
      interface: str, the name of the computer's used interface.
256 257

      :param config: dict-like, holds raw data from configuration file
Łukasz Nowak's avatar
Łukasz Nowak committed
258 259
    """
    self.reference = str(reference)
260
    self.interface = interface
261
    self.partition_list = partition_list or []
Łukasz Nowak's avatar
Łukasz Nowak committed
262 263
    self.address = addr
    self.netmask = netmask
264
    self.ipv6_interface = ipv6_interface
265
    self.software_user = software_user
266
    self.tap_gateway_interface = tap_gateway_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
267

268 269 270 271 272 273 274 275
    # Used to be static attributes of the class object - didn't make sense (Marco again)
    assert instance_root is not None and software_root is not None, \
           "Computer's instance_root and software_root must not be empty!"
    self.software_root = software_root
    self.instance_root = instance_root
    self.instance_storage_home = instance_storage_home

    # The following properties are updated on update() method
276 277 278 279 280
    self.public_ipv4_address = None
    self.os_type = None
    self.python_version = None
    self.slapos_version = None

281 282
    # attributes starting with '_' are saved from serialization
    # monkey-patch use of class instead of dictionary
283 284 285 286
    if config is None:
      logger.warning("Computer needs config in constructor to allow managers.")

    self._config = config if config is None or isinstance(config, dict) else config.__dict__
287
    self._manager_list = slapmanager.from_config(self._config)
288

Łukasz Nowak's avatar
Łukasz Nowak committed
289
  def __getinitargs__(self):
290
    return (self.reference, self.interface)
Łukasz Nowak's avatar
Łukasz Nowak committed
291

292
  def getAddress(self, allow_tap=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
293
    """
Marco Mariani's avatar
Marco Mariani committed
294
    Return a list of the interface address not attributed to any partition (which
Łukasz Nowak's avatar
Łukasz Nowak committed
295 296 297
    are therefore free for the computer itself).

    Returns:
298
      False if the interface isn't available, else the list of the free addresses.
Łukasz Nowak's avatar
Łukasz Nowak committed
299
    """
300
    if self.interface is None:
Marco Mariani's avatar
Marco Mariani committed
301
      return {'addr': self.address, 'netmask': self.netmask}
Łukasz Nowak's avatar
Łukasz Nowak committed
302 303 304 305 306 307

    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'])
308
    # Going through addresses of the computer's interface
309
    for address_dict in self.interface.getGlobalScopeAddressList():
Łukasz Nowak's avatar
Łukasz Nowak committed
310 311 312 313
      # Comparing with computer's partition addresses
      if address_dict['addr'] not in computer_partition_address_list:
        return address_dict

314
    if allow_tap:
Marco Mariani's avatar
Marco Mariani committed
315
      # all addresses on interface are for partition, so let's add new one
316 317 318 319 320 321
      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
322
    raise NoAddressOnInterface('No valid IPv6 found on %s.' % self.interface.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
323

324
  def update(self):
325
    """Collect environmental hardware/network information."""
326 327 328 329 330
    self.public_ipv4_address = getPublicIPv4Address()
    self.slapos_version = version.version
    self.python_version = platform.python_version()
    self.os_type = platform.platform()

331
  def send(self, conf):
Łukasz Nowak's avatar
Łukasz Nowak committed
332 333 334 335 336
    """
    Send a marshalled dictionary of the computer object serialized via_getDict.
    """
    slap_instance = slap.slap()
    connection_dict = {}
337 338 339 340
    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
341
                                       **connection_dict)
Łukasz Nowak's avatar
Łukasz Nowak committed
342
    slap_computer = slap_instance.registerComputer(self.reference)
Marco Mariani's avatar
Marco Mariani committed
343

344
    if conf.dry_run:
345
      return
346
    try:
347
      slap_computer.updateConfiguration(xml_marshaller.xml_marshaller.dumps(_getDict(self)))
348
    except slap.NotFoundError as error:
349 350 351
      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 "
352
          "like 'COMP-123' and is correct.\nError is : 404 Not Found." % error)
Łukasz Nowak's avatar
Łukasz Nowak committed
353

354
  def dump(self, path_to_xml, path_to_json, logger):
Łukasz Nowak's avatar
Łukasz Nowak committed
355 356 357 358 359
    """
    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
360
      path_to_json: String, path to the JSON version to save.
Łukasz Nowak's avatar
Łukasz Nowak committed
361 362 363
    """

    computer_dict = _getDict(self)
364 365 366 367 368

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

369
    new_xml = xml_marshaller.xml_marshaller.dumps(computer_dict)
Marco Mariani's avatar
Marco Mariani committed
370 371 372 373
    new_pretty_xml = prettify_xml(new_xml)

    path_to_archive = path_to_xml + '.zip'

374
    if os.path.exists(path_to_archive) and os.path.exists(path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
375 376 377 378 379 380
      # 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

381
    if os.path.exists(path_to_xml):
382 383 384 385 386
      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
387
                    path_to_archive + time.strftime('_broken_%Y%m%d-%H:%M'))
388 389 390 391
        try:
          self.backup_xml(path_to_archive, path_to_xml)
        except:
          # give up trying
392
          logger.exception("Can't backup %s:", path_to_xml)
Marco Mariani's avatar
Marco Mariani committed
393

Marco Mariani's avatar
Marco Mariani committed
394 395
    with open(path_to_xml, 'wb') as fout:
      fout.write(new_pretty_xml)
Marco Mariani's avatar
Marco Mariani committed
396

397 398 399
    for partition in self.partition_list:
      partition.dump()

Marco Mariani's avatar
Marco Mariani committed
400
  def backup_xml(self, path_to_archive, path_to_xml):
Marco Mariani's avatar
Marco Mariani committed
401 402 403
    """
    Stores a copy of the current xml file to an historical archive.
    """
Marco Mariani's avatar
Marco Mariani committed
404
    xml_content = open(path_to_xml).read()
Marco Mariani's avatar
Marco Mariani committed
405
    saved_filename = os.path.basename(path_to_xml) + time.strftime('.%Y%m%d-%H:%M')
Marco Mariani's avatar
Marco Mariani committed
406 407 408 409

    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
410
  @classmethod
411
  def load(cls, path_to_xml, reference, ipv6_interface, tap_gateway_interface,
412
           instance_root=None, software_root=None, config=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
413 414 415 416 417 418 419 420
    """
    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:
421
      A Computer object.
Łukasz Nowak's avatar
Łukasz Nowak committed
422
    """
423 424
    with open(path_to_xml, "rb") as fi:
      dumped_dict = xml_marshaller.xml_marshaller.load(fi)
Łukasz Nowak's avatar
Łukasz Nowak committed
425 426 427

    # Reconstructing the computer object from the xml
    computer = Computer(
428 429 430 431 432
        reference=reference,
        addr=dumped_dict['address'],
        netmask=dumped_dict['netmask'],
        ipv6_interface=ipv6_interface,
        software_user=dumped_dict.get('software_user', 'slapsoft'),
433
        tap_gateway_interface=tap_gateway_interface,
434 435
        software_root=dumped_dict.get('software_root', software_root),
        instance_root=dumped_dict.get('instance_root', instance_root),
436
        config=config,
Łukasz Nowak's avatar
Łukasz Nowak committed
437 438
    )

439
    partition_amount = int(config.partition_amount)
440
    for partition_index, partition_dict in enumerate(dumped_dict['partition_list']):
Łukasz Nowak's avatar
Łukasz Nowak committed
441 442 443 444 445 446 447 448

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

      if partition_dict['tap']:
        tap = Tap(partition_dict['tap']['name'])
449 450 451 452 453
        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
454 455 456
      else:
        tap = Tap(partition_dict['reference'])

457
      if partition_dict.get('tun') is not None and partition_dict['tun'].get('ipv4_addr') is not None:
458
        tun = Tun(partition_dict['tun']['name'], partition_index, partition_amount)
459 460
        tun.ipv4_addr = partition_dict['tun']['ipv4_addr']
      else:
461
        tun = Tun("slaptun" + str(partition_index), partition_index, partition_amount)
462

Łukasz Nowak's avatar
Łukasz Nowak committed
463
      address_list = partition_dict['address_list']
464
      external_storage_list = partition_dict.get('external_storage_list', [])
Łukasz Nowak's avatar
Łukasz Nowak committed
465 466

      partition = Partition(
467 468 469 470 471
          reference=partition_dict['reference'],
          path=partition_dict['path'],
          user=user,
          address_list=address_list,
          tap=tap,
472
          tun=tun if config.create_tun else None,
473
          external_storage_list=external_storage_list,
Łukasz Nowak's avatar
Łukasz Nowak committed
474 475 476 477 478 479
      )

      computer.partition_list.append(partition)

    return computer

480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506
  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

507 508 509 510 511 512 513
  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
514
    callAndRead(command.split())
515

516 517 518 519 520 521 522 523 524
  @property
  def software_gid(self):
    """Return GID for self.software_user.

    Has to be dynamic because __init__ happens before ``format`` where we 
    effectively create the user and group."""
    return pwd.getpwnam(self.software_user)[3]

  def format(self, alter_user=True, alter_network=True, create_tap=True, use_unique_local_address_block=False):
Łukasz Nowak's avatar
Łukasz Nowak committed
525
    """
526 527 528 529 530 531
    Setup underlaying OS so it reflects this instance (``self``).

    - setup interfaces and addresses
    - setup TAP and TUN interfaces
    - add groups and users
    - construct partitions inside slapgrid
Łukasz Nowak's avatar
Łukasz Nowak committed
532 533
    """
    if alter_network and self.address is not None:
534
      self.interface.addAddr(self.address, self.netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
535

536
    if use_unique_local_address_block and alter_network:
537 538 539
      if self.ipv6_interface:
        network_interface_name = self.ipv6_interface
      else:
540
        network_interface_name = self.interface.name
541 542
      self._addUniqueLocalAddressIpv6(network_interface_name)

543
    for path in self.instance_root, self.software_root:
Łukasz Nowak's avatar
Łukasz Nowak committed
544
      if not os.path.exists(path):
Marco Mariani's avatar
Marco Mariani committed
545
        os.makedirs(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
546
      else:
Marco Mariani's avatar
Marco Mariani committed
547
        os.chmod(path, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
548

549 550
    # own self.software_root by software user
    slapsoft = User(self.software_user)
Łukasz Nowak's avatar
Łukasz Nowak committed
551 552 553
    slapsoft.path = self.software_root
    if alter_user:
      slapsoft.create()
Łukasz Nowak's avatar
Łukasz Nowak committed
554
      slapsoft_pw = pwd.getpwnam(slapsoft.name)
555
      os.chown(slapsoft.path, slapsoft_pw.pw_uid, slapsoft_pw.pw_gid)
Marco Mariani's avatar
Marco Mariani committed
556
    os.chmod(self.software_root, 0o755)
Łukasz Nowak's avatar
Łukasz Nowak committed
557

558 559
    # Iterate over all managers and let them `format` the computer too
    for manager in self._manager_list:
560
      manager.format(self)
561

562 563 564
    # get list of instance external storage if exist
    instance_external_list = []
    if self.instance_storage_home:
565 566 567 568 569 570 571
      # 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)
572

573 574 575 576 577 578 579 580
    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))

581 582
    if alter_network:
      self._speedHackAddAllOldIpsToInterface()
583

584 585 586 587 588 589
    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]
590 591
        partition.external_storage_list = ['%s/%s' % (path, partition.reference)
                                            for path in instance_external_list]
592 593 594 595 596 597 598 599 600
        if alter_user:
          partition.user.create()

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

601
        if alter_network and create_tap:
602
          # In case it has to be  attached to the TAP network device, only one
603 604
          # is necessary for the interface to assert carrier
          if self.interface.attach_to_tap and partition_index == 0:
605
            partition.tap.createWithOwner(owner, attach_to_tap=True)
Łukasz Nowak's avatar
Łukasz Nowak committed
606
          else:
607
            partition.tap.createWithOwner(owner)
608 609 610 611 612 613
          # 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)
614 615 616 617 618 619 620
            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()
621

622 623 624 625 626
        if alter_network and partition.tun is not None:
          # create TUN interface per partition as well
          partition.tun.createWithOwner(owner)
          partition.tun.createRoutes()

627 628
        # Reconstructing partition's directory
        partition.createPath(alter_user)
629
        partition.createExternalPath(alter_user)
630 631 632 633 634

        # 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
635
        if not partition.address_list:
636
          # regenerate
637 638
          partition.address_list.append(self.interface.addIPv4LocalAddress())
          partition.address_list.append(self.interface.addAddr())
639 640 641 642 643
        elif alter_network:
          # regenerate list of addresses
          old_partition_address_list = partition.address_list
          partition.address_list = []
          if len(old_partition_address_list) != 2:
644 645 646
            raise ValueError(
              'There should be exactly 2 stored addresses. Got: %r' %
              (old_partition_address_list,))
Marco Mariani's avatar
Marco Mariani committed
647 648
          if not any(netaddr.valid_ipv6(q['addr'])
                     for q in old_partition_address_list):
649
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
650 651
          if not any(netaddr.valid_ipv4(q['addr'])
                     for q in old_partition_address_list):
652
            raise ValueError('Not valid ipv6 addresses loaded')
Marco Mariani's avatar
Marco Mariani committed
653

654 655
          for address in old_partition_address_list:
            if netaddr.valid_ipv6(address['addr']):
656
              partition.address_list.append(self.interface.addAddr(
Vincent Pelletier's avatar
Vincent Pelletier committed
657 658
                address['addr'],
                address['netmask']))
659
            elif netaddr.valid_ipv4(address['addr']):
660
              partition.address_list.append(self.interface.addIPv4LocalAddress(
Vincent Pelletier's avatar
Vincent Pelletier committed
661
                address['addr']))
662 663 664
            else:
              raise ValueError('Address %r is incorrect' % address['addr'])
    finally:
665
      if alter_network and create_tap and self.interface.attach_to_tap:
666 667 668 669
        try:
          self.partition_list[0].tap.detach()
        except IndexError:
          pass
Łukasz Nowak's avatar
Łukasz Nowak committed
670

671

672
class Partition(object):
673 674 675
  """Represent a computer partition."""

  resource_file = ".slapos-resource"
Łukasz Nowak's avatar
Łukasz Nowak committed
676

677 678
  def __init__(self, reference, path, user, address_list, 
               tap, external_storage_list=[], tun=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
679 680 681 682 683 684
    """
    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.
685 686
      tap: Tap, the tap interface linked to this partition e.g. used as a bridge for kvm
      tun: Tun interface used for special apps simulating ethernet connections
687
      external_storage_list: Base path list of folder to format for data storage
Łukasz Nowak's avatar
Łukasz Nowak committed
688 689 690 691 692 693 694
    """

    self.reference = str(reference)
    self.path = str(path)
    self.user = user
    self.address_list = address_list or []
    self.tap = tap
695
    self.tun = tun
696
    self.external_storage_list = []
Łukasz Nowak's avatar
Łukasz Nowak committed
697 698

  def __getinitargs__(self):
699
    return (self.reference, self.path, self.user, self.address_list, self.tap, self.tun)
Łukasz Nowak's avatar
Łukasz Nowak committed
700 701 702

  def createPath(self, alter_user=True):
    """
Vincent Pelletier's avatar
Vincent Pelletier committed
703 704
    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
705 706
    """

707
    self.path = os.path.abspath(self.path)
Łukasz Nowak's avatar
Łukasz Nowak committed
708
    owner = self.user if self.user else User('root')
709
    if not os.path.exists(self.path):
Marco Mariani's avatar
Marco Mariani committed
710
      os.mkdir(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
711
    if alter_user:
Łukasz Nowak's avatar
Łukasz Nowak committed
712
      owner_pw = pwd.getpwnam(owner.name)
713
      os.chown(self.path, owner_pw.pw_uid, owner_pw.pw_gid)
714
    os.chmod(self.path, 0o750)
Łukasz Nowak's avatar
Łukasz Nowak committed
715

716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
  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)
731

732 733 734 735 736 737 738 739 740
  def dump(self):
    """Dump available resources into ~partition_home/.slapos-resource."""
    file_path = os.path.join(self.path, self.resource_file)
    logger.info("Partition resources saved to {}".format(
      self.reference, file_path))
    data = _getDict(self)
    with open(file_path, "wb") as fo:
      json.dump(data, fo, sort_keys=True, indent=4)
    owner_pw = pwd.getpwnam(self.user.name)
741
    os.chmod(file_path, 0o644)
742 743


744
class User(object):
Marco Mariani's avatar
Marco Mariani committed
745 746
  """User: represent and manipulate a user on the system."""

747
  path = None
Łukasz Nowak's avatar
Łukasz Nowak committed
748 749 750 751 752 753 754

  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)
755
    self.shell = '/bin/sh'
Łukasz Nowak's avatar
Łukasz Nowak committed
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
    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
775
    grpname = 'grp_' + self.name if sys.platform == 'cygwin' else self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
776
    try:
Jondy Zhao's avatar
Jondy Zhao committed
777
      grp.getgrnam(grpname)
Łukasz Nowak's avatar
Łukasz Nowak committed
778
    except KeyError:
Jondy Zhao's avatar
Jondy Zhao committed
779
      callAndRead(['groupadd', grpname])
Łukasz Nowak's avatar
Łukasz Nowak committed
780

781
    user_parameter_list = ['-d', self.path, '-g', self.name, '-s', self.shell]
Łukasz Nowak's avatar
Łukasz Nowak committed
782 783 784 785
    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
786
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
787
    except KeyError:
788
      user_parameter_list.append('-r')
Łukasz Nowak's avatar
Łukasz Nowak committed
789 790
      callAndRead(['useradd'] + user_parameter_list)
    else:
791 792
      # if the user is already created and used we should not fail
      callAndRead(['usermod'] + user_parameter_list, raise_on_error=False)
793 794
    # lock the password of user
    callAndRead(['passwd', '-l', self.name])
Łukasz Nowak's avatar
Łukasz Nowak committed
795 796 797 798 799 800 801 802 803 804 805 806 807

    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
808
      pwd.getpwnam(self.name)
Łukasz Nowak's avatar
Łukasz Nowak committed
809 810 811 812
      return True
    except KeyError:
      return False

813

814
class Tap(object):
Łukasz Nowak's avatar
Łukasz Nowak committed
815
  "Tap represent a tap interface on the system"
816 817 818
  IFF_TAP = 0x0002
  TUNSETIFF = 0x400454ca
  KEEP_TAP_ATTACHED_EVENT = threading.Event()
819
  MODE = "tap"
Łukasz Nowak's avatar
Łukasz Nowak committed
820 821 822 823 824

  def __init__(self, tap_name):
    """
    Attributes:
        tap_name: String, the name of the tap interface.
825 826 827
        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
828 829 830
    """

    self.name = str(tap_name)
831 832 833 834
    self.ipv4_addr = ""
    self.ipv4_netmask = ""
    self.ipv4_gateway = ""
    self.ipv4_network = ""
Łukasz Nowak's avatar
Łukasz Nowak committed
835 836 837 838

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

839 840 841
  def attach(self):
    """
    Attach to the TAP interface, meaning  that it just opens the TAP interface
Marco Mariani's avatar
Marco Mariani committed
842
    and waits for the caller to notify that it can be safely detached.
843 844 845 846 847 848 849 850

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

851
    In case of bridge:
852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869
    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))

870
    except IOError as error:
871 872
      # If  EBUSY, it  means another  program is  already attached,  thus just
      # ignore it...
873
      logger.warning("Cannot create interface " + self.name + ". Does it exist already?")
874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891
      if error.errno != errno.EBUSY:
        os.close(tap_fd)
    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
892
    """
893
    Create a tap interface on the system if it doesn't exist yet.
Łukasz Nowak's avatar
Łukasz Nowak committed
894 895 896 897
    """
    check_file = '/sys/devices/virtual/net/%s/owner' % self.name
    owner_id = None
    if os.path.exists(check_file):
898 899
      with open(check_file) as fx:
        owner_id = fx.read().strip()
Łukasz Nowak's avatar
Łukasz Nowak committed
900
      try:
901 902
        owner_id = int(owner_id)
      except ValueError:
903 904 905 906 907 908 909 910 911 912 913
        owner_id = pwd.getpwnam(owner_id).pw_uid
      #
      if owner_id != pwd.getpwnam(owner.name).pw_uid:
        logger.warning("Wrong owner of TUN/TAP interface {}! Not touching it."
                       "Expected {:d} got {:d}".format(
          self.name, pwd.getpwnam(owner.name).pw_uid, owner_id))
      # if the interface already exists - don't do anything
      return

    callAndRead(['ip', 'tuntap', 'add', 'dev', self.name, 'mode',
                 self.MODE, 'user', owner.name])
Łukasz Nowak's avatar
Łukasz Nowak committed
914 915
    callAndRead(['ip', 'link', 'set', self.name, 'up'])

916 917 918
    if attach_to_tap:
      threading.Thread(target=self.attach).start()

919
  def createRoutes(self):
920 921 922
    """
    Configure ipv4 route to reach this interface from local network
    """
923 924
    if self.ipv4_addr:
      # Check if this route exits
925 926
      code, result = callAndRead(['ip', 'route', 'show', self.ipv4_addr],
                                 raise_on_error=False)
927 928
      if code == 0 and self.ipv4_addr in result and self.name in result:
        return
929
      callAndRead(['ip', 'route', 'add', self.ipv4_addr, 'dev', self.name])
930 931 932
    else:
      raise ValueError("%s should not be empty. No ipv4 address assigned to %s" %
                         (self.ipv4_addr, self.name))
933

934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985

class Tun(Tap):
  """Represent TUN interface which might be many per user."""
 
  MODE = "tun"
  BASE_MASK = 12
  BASE_NETWORK = "172.16.0.0"

  def __init__(self, name, sequence=None, partitions=None):
    """Create TUN interface with subnet according to the optional ``sequence`` number.

    :param name: name which will appear in ``ip list`` afterwards
    :param sequence: {int} position of this TUN among all ``partitions``
    """
    super(Tun, self).__init__(name)
    if sequence is not None:
      assert 0 <= sequence < partitions, "0 <= {} < {}".format(sequence, partitions)
      # create base IPNetwork
      ip_network = netaddr.IPNetwork(Tun.BASE_NETWORK + "/" + str(Tun.BASE_MASK))
      # compute shift in BITS to separate ``partitions`` networks into subset
      # example: for 30 partitions we need log2(30) = 8 BITS
      mask_shift = int(math.ceil(math.log(int(partitions), 2.0)))
      # IPNetwork.subnet returns iterator over all possible subnets of given mask
      ip_subnets = list(ip_network.subnet(Tun.BASE_MASK + mask_shift))
      subnet = ip_subnets[sequence]
      # For serialization purposes, convert directly to ``str``
      self.ipv4_network = str(subnet)
      self.ipv4_addr = str(subnet.ip)
      self.ipv4_netmask = str(subnet.netmask)

  def createRoutes(self):
    """Extend for physical addition of network address because TAP let this on external class."""
    if self.ipv4_network:
      # add an address
      code, _ = callAndRead(['ip', 'addr', 'add', self.ipv4_network, 'dev', self.name],
                            raise_on_error=False)
      if code == 0:
        # address added to the interface - wait
        time.sleep(1)
    else:
      raise RuntimeError("Cannot setup address on interface {}. "
                         "Address is missing.".format(self.name))
    # create routes
    super(Tun, self).createRoutes()
    # add iptables rule to accept connections from this interface
    chain_rule = ['INPUT', '-i', self.name, '-j', 'ACCEPT']
    code, _ = callAndRead(['iptables', '-C'] + chain_rule, raise_on_error=False)
    if code == 0:
      # 0 means the rule does not exits so we are free to insert it
      callAndRead(['iptables', '-I'] + chain_rule)


986
class Interface(object):
Marco Mariani's avatar
Marco Mariani committed
987
  """Represent a network interface on the system"""
Łukasz Nowak's avatar
Łukasz Nowak committed
988

989
  def __init__(self, logger, name, ipv4_local_network, ipv6_interface=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
990 991
    """
    Attributes:
992
        name: String, the name of the interface
Łukasz Nowak's avatar
Łukasz Nowak committed
993 994
    """

995
    self._logger = logger
Łukasz Nowak's avatar
Łukasz Nowak committed
996 997
    self.name = str(name)
    self.ipv4_local_network = ipv4_local_network
998
    self.ipv6_interface = ipv6_interface
Łukasz Nowak's avatar
Łukasz Nowak committed
999

1000
    # Attach to TAP  network interface, only if the  interface interface does not
1001
    # report carrier
1002
    _, result = callAndRead(['ip', 'addr', 'list', self.name], raise_on_error=False)
1003
    self.attach_to_tap = self.isBridge() and ('DOWN' in result.split('\n', 1)[0])
1004

1005
  # XXX no __getinitargs__, as instances of this class are never deserialized.
Łukasz Nowak's avatar
Łukasz Nowak committed
1006 1007

  def getIPv4LocalAddressList(self):
Vincent Pelletier's avatar
Vincent Pelletier committed
1008 1009 1010 1011
    """
    Returns currently configured local IPv4 addresses which are in
    ipv4_local_network
    """
Łukasz Nowak's avatar
Łukasz Nowak committed
1012 1013
    if not socket.AF_INET in netifaces.ifaddresses(self.name):
      return []
Marco Mariani's avatar
Marco Mariani committed
1014 1015 1016 1017 1018 1019 1020 1021 1022
    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
1023 1024 1025

  def getGlobalScopeAddressList(self):
    """Returns currently configured global scope IPv6 addresses"""
1026 1027 1028 1029
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
1030
    try:
Marco Mariani's avatar
Marco Mariani committed
1031
      address_list = [
1032 1033 1034 1035
          q
          for q in netifaces.ifaddresses(interface_name)[socket.AF_INET6]
          if isGlobalScopeAddress(q['addr'].split('%')[0])
      ]
1036
    except KeyError:
1037
      raise ValueError("%s must have at least one IPv6 address assigned" %
1038
                         interface_name)
Jondy Zhao's avatar
Jondy Zhao committed
1039 1040
    if sys.platform == 'cygwin':
      for q in address_list:
1041
        q.setdefault('netmask', 'FFFF:FFFF:FFFF:FFFF::')
Łukasz Nowak's avatar
Łukasz Nowak committed
1042 1043 1044 1045 1046 1047 1048 1049 1050
    # 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

1051
  def isBridge(self):
1052 1053 1054 1055 1056 1057 1058
    try:
      _, result = callAndRead(['brctl', 'show'])
      return any(line.startswith(self.name) for line in result.split("\n"))
    except Exception as e:
      # the binary "brctl" itself does not exist - bridge is imposible to exist
      logger.warning(str(e))
      return False
1059

Łukasz Nowak's avatar
Łukasz Nowak committed
1060
  def getInterfaceList(self):
1061
    """Returns list of interfaces already present on bridge"""
Łukasz Nowak's avatar
Łukasz Nowak committed
1062
    interface_list = []
1063
    _, result = callAndRead(['brctl', 'show'])
1064
    in_interface = False
Łukasz Nowak's avatar
Łukasz Nowak committed
1065 1066 1067 1068
    for line in result.split('\n'):
      if len(line.split()) > 1:
        if self.name in line:
          interface_list.append(line.split()[-1])
1069
          in_interface = True
Łukasz Nowak's avatar
Łukasz Nowak committed
1070
          continue
1071
        if in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
1072
          break
1073
      elif in_interface:
Łukasz Nowak's avatar
Łukasz Nowak committed
1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
        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():
1087 1088 1089
      if self.isBridge():
        callAndRead(['brctl', 'addif', self.name, tap.name])
      else:
1090
        logger.warning("Interface slapos.cfg:interface_name={} is not a bridge. "
1091 1092
                       "TUN/TAP interface {} might not have internet connection."
                       "".format(self.name, tap.name))
Łukasz Nowak's avatar
Łukasz Nowak committed
1093 1094

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

Łukasz Nowak's avatar
Łukasz Nowak committed
1097 1098 1099 1100 1101 1102 1103
    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
1104 1105 1106 1107
      if self.ipv6_interface:
        interface_name = self.ipv6_interface
      else:
        interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
1108 1109 1110
    else:
      af = socket.AF_INET
      address_string = '%s/%s' % (address, netmaskToPrefixIPv4(netmask))
1111
      interface_name = self.name
Łukasz Nowak's avatar
Łukasz Nowak committed
1112 1113 1114

    # check if address is already took by any other interface
    for interface in netifaces.interfaces():
1115
      if interface != interface_name:
Łukasz Nowak's avatar
Łukasz Nowak committed
1116 1117
        address_dict = netifaces.ifaddresses(interface)
        if af in address_dict:
1118
          if address in [q['addr'].split('%')[0] for q in address_dict[af]]:
Łukasz Nowak's avatar
Łukasz Nowak committed
1119 1120
            return False

Vincent Pelletier's avatar
Vincent Pelletier committed
1121 1122
    if not af in netifaces.ifaddresses(interface_name) \
        or not address in [q['addr'].split('%')[0]
Marco Mariani's avatar
Marco Mariani committed
1123 1124
                           for q in netifaces.ifaddresses(interface_name)[af]
                           ]:
Łukasz Nowak's avatar
Łukasz Nowak committed
1125
      # add an address
1126
      callAndRead(['ip', 'addr', 'add', address_string, 'dev', interface_name])
1127 1128 1129 1130 1131

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

Łukasz Nowak's avatar
Łukasz Nowak committed
1132 1133
      # wait few moments
      time.sleep(2)
1134 1135 1136 1137 1138 1139

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

    # check existence on interface for ipv6
1140
    _, result = callAndRead(['ip', '-6', 'addr', 'list', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
1141 1142 1143 1144
    for l in result.split('\n'):
      if address in l:
        if 'tentative' in l:
          # duplicate, remove
Marco Mariani's avatar
Marco Mariani committed
1145
          callAndRead(['ip', 'addr', 'del', address_string, 'dev', interface_name])
Łukasz Nowak's avatar
Łukasz Nowak committed
1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158
          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()
1159 1160
      if (dict(addr=addr, netmask=netmask) not in
            self.getIPv4LocalAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
1161 1162 1163 1164 1165 1166 1167 1168 1169
        # 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"""
1170
    netmask = str(netaddr.IPNetwork(self.ipv4_local_network).netmask) if sys.platform == 'cygwin' \
1171
             else '255.255.255.255'
Łukasz Nowak's avatar
Łukasz Nowak committed
1172 1173 1174 1175
    local_address_list = self.getIPv4LocalAddressList()
    if addr is None:
      return self._generateRandomIPv4Address(netmask)
    elif dict(addr=addr, netmask=netmask) not in local_address_list:
1176 1177 1178
      if self._addSystemAddress(addr, netmask, False):
        return dict(addr=addr, netmask=netmask)
      else:
1179
        self._logger.warning('Impossible to add old local IPv4 %s. Generating '
1180
            'new IPv4 address.' % addr)
1181
        return self._generateRandomIPv4Address(netmask)
Łukasz Nowak's avatar
Łukasz Nowak committed
1182 1183 1184 1185
    else:
      # confirmed to be configured
      return dict(addr=addr, netmask=netmask)

Marco Mariani's avatar
Marco Mariani committed
1186
  def addAddr(self, addr=None, netmask=None):
Łukasz Nowak's avatar
Łukasz Nowak committed
1187
    """
1188
    Adds IP address to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
1189

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

1192
    If addr is specified and does not exists on interface, tries to add given
Vincent Pelletier's avatar
Vincent Pelletier committed
1193 1194
    address. If it is not possible (ex. because network changed) calculates new
    address.
Łukasz Nowak's avatar
Łukasz Nowak committed
1195 1196

    Args:
1197
      addr: Wished address to be added to interface.
Łukasz Nowak's avatar
Łukasz Nowak committed
1198 1199 1200 1201 1202 1203 1204
      netmask: Wished netmask to be used.

    Returns:
      Tuple of (address, netmask).

    Raises:
      AddressGenerationError: Couldn't construct valid address with existing
1205 1206
          one's on the interface.
      NoAddressOnInterface: There's no address on the interface to construct
Łukasz Nowak's avatar
Łukasz Nowak committed
1207 1208
          an address with.
    """
1209
    # Getting one address of the interface as base of the next addresses
1210 1211 1212 1213
    if self.ipv6_interface:
      interface_name = self.ipv6_interface
    else:
      interface_name = self.name
1214
    interface_addr_list = self.getGlobalScopeAddressList()
Łukasz Nowak's avatar
Łukasz Nowak committed
1215 1216

    # No address found
1217 1218 1219
    if len(interface_addr_list) == 0:
      raise NoAddressOnInterface(interface_name)
    address_dict = interface_addr_list[0]
Łukasz Nowak's avatar
Łukasz Nowak committed
1220 1221

    if addr is not None:
1222
      if dict(addr=addr, netmask=netmask) in interface_addr_list:
Łukasz Nowak's avatar
Łukasz Nowak committed
1223 1224 1225 1226
        # 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
1227
        interface_network = netaddr.ip.IPNetwork('%s/%s' % (address_dict['addr'],
Łukasz Nowak's avatar
Łukasz Nowak committed
1228
          netmaskToPrefixIPv6(address_dict['netmask'])))
Vincent Pelletier's avatar
Vincent Pelletier committed
1229 1230
        requested_network = netaddr.ip.IPNetwork('%s/%s' % (addr,
          netmaskToPrefixIPv6(netmask)))
1231
        if interface_network.network == requested_network.network:
Łukasz Nowak's avatar
Łukasz Nowak committed
1232 1233 1234 1235
          # same network, try to add
          if self._addSystemAddress(addr, netmask):
            # succeed, return it
            return dict(addr=addr, netmask=netmask)
1236
          else:
1237
            self._logger.warning('Impossible to add old public IPv6 %s. '
1238
                'Generating new IPv6 address.' % addr)
Łukasz Nowak's avatar
Łukasz Nowak committed
1239 1240 1241 1242 1243

    # 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
1244 1245
      addr = ':'.join(address_dict['addr'].split(':')[:-1] + ['%x' % (
        random.randint(1, 65000), )])
Łukasz Nowak's avatar
Łukasz Nowak committed
1246
      socket.inet_pton(socket.AF_INET6, addr)
1247 1248
      if (dict(addr=addr, netmask=netmask) not in
            self.getGlobalScopeAddressList()):
Łukasz Nowak's avatar
Łukasz Nowak committed
1249 1250 1251 1252 1253 1254 1255
        # Checking the validity of the IPv6 address
        if self._addSystemAddress(addr, netmask):
          return dict(addr=addr, netmask=netmask)
        try_num -= 1

    raise AddressGenerationError(addr)

1256

1257 1258
def parse_computer_definition(conf, definition_path):
  conf.logger.info('Using definition file %r' % definition_path)
1259 1260 1261 1262 1263 1264 1265 1266
  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
1267
    address, netmask = computer_definition.get('computer', 'address').split('/')
1268 1269
  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
1270 1271 1272 1273
    interface = Interface(logger=conf.logger,
                          name=conf.interface_name,
                          ipv4_local_network=conf.ipv4_local_network,
                          ipv6_interface=conf.ipv6_interface)
1274
  computer = Computer(
1275
      reference=conf.computer_id,