iproute.py 31.7 KB
Newer Older
Martín Ferrari's avatar
Martín Ferrari committed
1
# vim:ts=4:sw=4:et:ai:sts=4
Martín Ferrari's avatar
Martín Ferrari committed
2
import copy, fcntl, os, re, socket, struct, subprocess, sys
Martín Ferrari's avatar
Martín Ferrari committed
3
from netns.environ import *
4

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
# helpers
def _any_to_bool(any):
    if isinstance(any, bool):
        return any
    if isinstance(any, int):
        return any != 0
    if isinstance(any, str):
        if any.isdigit():
            return int(any) != 0
        if any.lower() == "true":
            return True
        if any.lower() == "false":
            return False
        return any != ""
    return bool(any)

def _positive(val):
    v = int(val)
    if v <= 0:
        raise ValueError("Invalid value: %d" % v)
    return v

Martín Ferrari's avatar
Martín Ferrari committed
27
def _non_empty_str(val):
28
    if val == "":
Martín Ferrari's avatar
Martín Ferrari committed
29 30 31 32
        return None
    else:
        return str(val)

33 34
def _fix_lladdr(addr):
    foo = addr.lower()
35
    if ":" in addr:
36
        # Verify sanity and split
37
        m = re.search("^" + ":".join(["([0-9a-f]{1,2})"] * 6) + "$", foo)
38 39 40
        if m is None:
            raise ValueError("Invalid address: `%s'." % addr)
        # Fill missing zeros and glue again
41
        return ":".join(("0" * (2 - len(x)) + x for x in m.groups()))
42 43

    # Fill missing zeros
44
    foo = "0" * (12 - len(foo)) + foo
45
    # Verify sanity and split
46
    m = re.search("^" + "([0-9a-f]{2})" * 6 + "$", foo)
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
    if m is None:
        raise ValueError("Invalid address: `%s'." % addr)
    # Glue
    return ":".join(m.groups())

def _make_getter(attr, conv = lambda x: x):
    def getter(self):
        return conv(getattr(self, attr))
    return getter

def _make_setter(attr, conv = lambda x: x):
    def setter(self, value):
        if value == None:
            setattr(self, attr, None)
        else:
            setattr(self, attr, conv(value))
    return setter

# classes for internal use
class interface(object):
    """Class for internal use. It is mostly a data container used to easily
    pass information around; with some convenience methods."""

    # information for other parts of the code
    changeable_attributes = ["name", "mtu", "lladdr", "broadcast", "up",
            "multicast", "arp"]

    # Index should be read-only
    index = property(_make_getter("_index"))
    up = property(_make_getter("_up"), _make_setter("_up", _any_to_bool))
    mtu = property(_make_getter("_mtu"), _make_setter("_mtu", _positive))
    lladdr = property(_make_getter("_lladdr"),
            _make_setter("_lladdr", _fix_lladdr))
    arp = property(_make_getter("_arp"), _make_setter("_arp", _any_to_bool))
    multicast = property(_make_getter("_mc"), _make_setter("_mc", _any_to_bool))

    def __init__(self, index = None, name = None, up = None, mtu = None,
            lladdr = None, broadcast = None, multicast = None, arp = None):
        self._index     = _positive(index) if index is not None else None
        self.name       = name
        self.up         = up
        self.mtu        = mtu
        self.lladdr     = lladdr
        self.broadcast  = broadcast
        self.multicast  = multicast
        self.arp        = arp

    def __repr__(self):
        s = "%s.%s(index = %s, name = %s, up = %s, mtu = %s, lladdr = %s, "
        s += "broadcast = %s, multicast = %s, arp = %s)"
        return s % (self.__module__, self.__class__.__name__,
                self.index.__repr__(), self.name.__repr__(),
                self.up.__repr__(), self.mtu.__repr__(),
                self.lladdr.__repr__(), self.broadcast.__repr__(),
                self.multicast.__repr__(), self.arp.__repr__())

    def __sub__(self, o):
        """Compare attributes and return a new object with just the attributes
        that differ set (with the value they have in the first operand). The
        index remains equal to the first operand."""
        name        = None if self.name == o.name else self.name
        up          = None if self.up == o.up else self.up
        mtu         = None if self.mtu == o.mtu else self.mtu
        lladdr      = None if self.lladdr == o.lladdr else self.lladdr
        broadcast   = None if self.broadcast == o.broadcast else self.broadcast
        multicast   = None if self.multicast == o.multicast else self.multicast
        arp         = None if self.arp == o.arp else self.arp
        return self.__class__(self.index, name, up, mtu, lladdr, broadcast,
                multicast, arp)

117 118 119
    def copy(self):
        return copy.copy(self)

120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
class bridge(interface):
    changeable_attributes = interface.changeable_attributes + ["stp",
            "forward_delay", "hello_time", "ageing_time", "max_age"]

    # Index should be read-only
    stp = property(_make_getter("_stp"), _make_setter("_stp", _any_to_bool))
    forward_delay = property(_make_getter("_forward_delay"),
            _make_setter("_forward_delay", float))
    hello_time = property(_make_getter("_hello_time"),
            _make_setter("_hello_time", float))
    ageing_time = property(_make_getter("_ageing_time"),
            _make_setter("_ageing_time", float))
    max_age = property(_make_getter("_max_age"),
            _make_setter("_max_age", float))

    @classmethod
    def upgrade(cls, iface, *kargs, **kwargs):
        """Upgrade a interface to a bridge."""
        return cls(iface.index, iface.name, iface.up, iface.mtu, iface.lladdr,
                iface.broadcast, iface.multicast, iface.arp, *kargs, **kwargs)

    def __init__(self, index = None, name = None, up = None, mtu = None,
            lladdr = None, broadcast = None, multicast = None, arp = None,
            stp = None, forward_delay = None, hello_time = None,
            ageing_time = None, max_age = None):
        super(bridge, self).__init__(index, name, up, mtu, lladdr, broadcast,
                multicast, arp)
        self.stp            = stp
        self.forward_delay  = forward_delay
        self.hello_time     = hello_time
        self.ageing_time    = ageing_time
        self.max_age        = max_age

    def __repr__(self):
        s = "%s.%s(index = %s, name = %s, up = %s, mtu = %s, lladdr = %s, "
        s += "broadcast = %s, multicast = %s, arp = %s, stp = %s, "
        s += "forward_delay = %s, hello_time = %s, ageing_time = %s, "
        s += "max_age = %s)"
        return s % (self.__module__, self.__class__.__name__,
                self.index.__repr__(), self.name.__repr__(),
                self.up.__repr__(), self.mtu.__repr__(),
                self.lladdr.__repr__(), self.broadcast.__repr__(),
                self.multicast.__repr__(), self.arp.__repr__(),
                self.stp.__repr__(), self.forward_delay.__repr__(),
                self.hello_time.__repr__(), self.ageing_time.__repr__(),
                self.max_age.__repr__())

    def __sub__(self, o):
        r = super(bridge, self).__sub__(o)
        if type(o) == interface:
            return r
        r.stp           = None if self.stp == o.stp else self.stp
        r.hello_time    = None if self.hello_time == o.hello_time else \
                self.hello_time
        r.forward_delay = None if self.forward_delay == o.forward_delay else \
                self.forward_delay
        r.ageing_time   = None if self.ageing_time == o.ageing_time else \
                self.ageing_time
        r.max_age       = None if self.max_age == o.max_age else self.max_age
        return r

class address(object):
    """Class for internal use. It is mostly a data container used to easily
Martín Ferrari's avatar
Martín Ferrari committed
183 184 185
    pass information around; with some convenience methods. __eq__ and
    __hash__ are defined just to be able to easily find duplicated
    addresses."""
186 187 188 189 190 191 192 193 194 195 196
    # broadcast is not taken into account for differentiating addresses
    def __eq__(self, o):
        if not isinstance(o, address):
            return False
        return (self.family == o.family and self.address == o.address and
                self.prefix_len == o.prefix_len)

    def __hash__(self):
        h = (self.address.__hash__() ^ self.prefix_len.__hash__() ^
                self.family.__hash__())
        return h
197

198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
class ipv4address(address):
    def __init__(self, address, prefix_len, broadcast):
        self.address = address
        self.prefix_len = int(prefix_len)
        self.broadcast = broadcast
        self.family = socket.AF_INET

    def __repr__(self):
        s = "%s.%s(address = %s, prefix_len = %d, broadcast = %s)"
        return s % (self.__module__, self.__class__.__name__,
                self.address.__repr__(), self.prefix_len,
                self.broadcast.__repr__())

class ipv6address(address):
    def __init__(self, address, prefix_len):
        self.address = address
        self.prefix_len = int(prefix_len)
        self.family = socket.AF_INET6

    def __repr__(self):
        s = "%s.%s(address = %s, prefix_len = %d)"
        return s % (self.__module__, self.__class__.__name__,
                self.address.__repr__(), self.prefix_len)
221

Martín Ferrari's avatar
Martín Ferrari committed
222
class route(object):
223 224
    tipes = ["unicast", "local", "broadcast", "multicast", "throw",
            "unreachable", "prohibit", "blackhole", "nat"]
Martín Ferrari's avatar
Martín Ferrari committed
225 226 227 228 229 230

    tipe = property(_make_getter("_tipe", tipes.__getitem__),
            _make_setter("_tipe", tipes.index))
    prefix = property(_make_getter("_prefix"),
            _make_setter("_prefix", _non_empty_str))
    prefix_len = property(_make_getter("_plen"),
231
            lambda s, v: setattr(s, "_plen", int(v or 0)))
Martín Ferrari's avatar
Martín Ferrari committed
232 233
    nexthop = property(_make_getter("_nexthop"),
            _make_setter("_nexthop", _non_empty_str))
234 235
    interface = property(_make_getter("_interface"),
            _make_setter("_interface", _positive))
Martín Ferrari's avatar
Martín Ferrari committed
236
    metric = property(_make_getter("_metric"),
237
            lambda s, v: setattr(s, "_metric", int(v or 0)))
Martín Ferrari's avatar
Martín Ferrari committed
238

239
    def __init__(self, tipe = "unicast", prefix = None, prefix_len = 0,
240
            nexthop = None, interface = None, metric = 0):
Martín Ferrari's avatar
Martín Ferrari committed
241 242 243 244
        self.tipe = tipe
        self.prefix = prefix
        self.prefix_len = prefix_len
        self.nexthop = nexthop
245
        self.interface = interface
Martín Ferrari's avatar
Martín Ferrari committed
246
        self.metric = metric
247
        assert nexthop or interface
Martín Ferrari's avatar
Martín Ferrari committed
248 249 250

    def __repr__(self):
        s = "%s.%s(tipe = %s, prefix = %s, prefix_len = %s, nexthop = %s, "
251
        s += "interface = %s, metric = %s)"
Martín Ferrari's avatar
Martín Ferrari committed
252 253 254
        return s % (self.__module__, self.__class__.__name__,
                self.tipe.__repr__(), self.prefix.__repr__(),
                self.prefix_len.__repr__(), self.nexthop.__repr__(),
255
                self.interface.__repr__(), self.metric.__repr__())
Martín Ferrari's avatar
Martín Ferrari committed
256 257 258 259 260 261

    def __eq__(self, o):
        if not isinstance(o, route):
            return False
        return (self.tipe == o.tipe and self.prefix == o.prefix and
                self.prefix_len == o.prefix_len and self.nexthop == o.nexthop
262
                and self.interface == o.interface and self.metric == o.metric)
Martín Ferrari's avatar
Martín Ferrari committed
263

264 265 266 267 268 269 270 271 272
# helpers
def _get_if_name(iface):
    if isinstance(iface, interface):
        if iface.name != None:
            return iface.name
    if isinstance(iface, str):
        return iface
    return get_if(iface).name

Martín Ferrari's avatar
Martín Ferrari committed
273
# XXX: ideally this should be replaced by netlink communication
Martín Ferrari's avatar
Martín Ferrari committed
274
# Interface handling
Martín Ferrari's avatar
Martín Ferrari committed
275 276

# FIXME: try to lower the amount of calls to retrieve data!!
277
def get_if_data():
278 279 280
    """Gets current interface information. Returns a tuple (byidx, bynam) in
    which each element is a dictionary with the same data, but using different
    keys: interface indexes and interface names.
281 282 283

    In each dictionary, values are interface objects.
    """
Martín Ferrari's avatar
Martín Ferrari committed
284
    ipdata = backticks([ip_path, "-o", "link", "list"])
285 286 287 288 289 290 291

    byidx = {}
    bynam = {}
    for line in ipdata.split("\n"):
        if line == "":
            continue
        match = re.search(r'^(\d+):\s+(.*)', line)
292
        idx = int(match.group(1))
293
        match = re.search(r'^(\d+): (\S+): <(\S+)> mtu (\d+) qdisc \S+' +
294
                r'.*link/\S+(?: ([0-9a-f:]+) brd ([0-9a-f:]+))?', line)
295
        flags = match.group(3).split(",")
296
        i = interface(
297 298 299 300 301 302 303 304
                index   = match.group(1),
                name    = match.group(2),
                up      = "UP" in flags,
                mtu     = match.group(4),
                lladdr  = match.group(5),
                arp     = not ("NOARP" in flags),
                broadcast = match.group(6),
                multicast = "MULTICAST" in flags)
305 306
        byidx[idx] = bynam[i.name] = i
    return byidx, bynam
307

Martín Ferrari's avatar
Martín Ferrari committed
308 309
def get_if(iface):
    ifdata = get_if_data()
310
    if isinstance(iface, interface):
Martín Ferrari's avatar
Martín Ferrari committed
311 312 313 314 315 316 317 318
        if iface.index != None:
            return ifdata[0][iface.index]
        else:
            return ifdata[1][iface.name]
    if isinstance(iface, int):
        return ifdata[0][iface]
    return ifdata[1][iface]

319 320 321 322 323 324 325 326
def create_if_pair(if1, if2):
    assert if1.name and if2.name

    cmd = [[], []]
    iface = [if1, if2]
    for i in (0, 1):
        cmd[i] = ["name", iface[i].name]
        if iface[i].lladdr:
327
            cmd[i] += ["address", iface[i].lladdr]
328
        if iface[i].broadcast:
329
            cmd[i] += ["broadcast", iface[i].broadcast]
330
        if iface[i].mtu:
331
            cmd[i] += ["mtu", str(iface[i].mtu)]
332

Martín Ferrari's avatar
Martín Ferrari committed
333
    cmd = [ip_path, "link", "add"] + cmd[0] + ["type", "veth", "peer"] + cmd[1]
Martín Ferrari's avatar
Martín Ferrari committed
334
    execute(cmd)
335 336 337 338 339 340 341 342 343 344 345 346 347 348 349
    try:
        set_if(if1)
        set_if(if2)
    except:
        (t, v, bt) = sys.exc_info()
        try:
            del_if(if1)
            del_if(if2)
        except:
            pass
        raise t, v, bt
    interfaces = get_if_data()[1]
    return interfaces[if1.name], interfaces[if2.name]

def del_if(iface):
Martín Ferrari's avatar
Martín Ferrari committed
350
    ifname = _get_if_name(iface)
Martín Ferrari's avatar
Martín Ferrari committed
351
    execute([ip_path, "link", "del", ifname])
352 353

def set_if(iface, recover = True):
354 355 356
    def do_cmds(cmds, orig_iface):
        for c in cmds:
            try:
Martín Ferrari's avatar
Martín Ferrari committed
357
                execute(c)
358 359 360 361 362
            except:
                if recover:
                    set_if(orig_iface, recover = False) # rollback
                    raise

Martín Ferrari's avatar
Martín Ferrari committed
363
    orig_iface = get_if(iface)
364
    diff = iface - orig_iface # Only set what's needed
365 366

    # Name goes first
367
    if diff.name:
Martín Ferrari's avatar
Martín Ferrari committed
368
        _ils = [ip_path, "link", "set", "dev"]
369 370 371 372 373 374 375 376
        cmds = [_ils + [orig_iface.name, "name", diff.name]]
        if orig_iface.up:
            # iface needs to be down
            cmds = [_ils + [orig_iface.name, "down"], cmds[0],
                    _ils + [diff.name, "up"]]
        do_cmds(cmds, orig_iface)

    # I need to use the new name after a name change, duh!
Martín Ferrari's avatar
Martín Ferrari committed
377
    _ils = [ip_path, "link", "set", "dev", diff.name or orig_iface.name]
378
    cmds = []
379
    if diff.lladdr:
380 381 382
        if orig_iface.up:
            # iface needs to be down
            cmds.append(_ils + ["down"])
383
        cmds.append(_ils + ["address", diff.lladdr])
384 385 386
        if orig_iface.up and diff.up == None:
            # restore if it was up and it's not going to be set later
            cmds.append(_ils + ["up"])
387 388
    if diff.mtu:
        cmds.append(_ils + ["mtu", str(diff.mtu)])
389
    if diff.broadcast:
390 391 392 393 394
        cmds.append(_ils + ["broadcast", diff.broadcast])
    if diff.multicast != None:
        cmds.append(_ils + ["multicast", "on" if diff.multicast else "off"])
    if diff.arp != None:
        cmds.append(_ils + ["arp", "on" if diff.arp else "off"])
395 396
    if diff.up != None:
        cmds.append(_ils + ["up" if diff.up else "down"])
397

398
    do_cmds(cmds, orig_iface)
399

400
def change_netns(iface, netns):
Martín Ferrari's avatar
Martín Ferrari committed
401
    ifname = _get_if_name(iface)
Martín Ferrari's avatar
Martín Ferrari committed
402
    execute([ip_path, "link", "set", "dev", ifname, "netns", str(netns)])
403

Martín Ferrari's avatar
Martín Ferrari committed
404 405
# Address handling

Martín Ferrari's avatar
Martín Ferrari committed
406
def get_addr_data():
Martín Ferrari's avatar
Martín Ferrari committed
407
    ipdata = backticks([ip_path, "-o", "addr", "list"])
Martín Ferrari's avatar
Martín Ferrari committed
408 409 410 411 412 413 414 415 416 417 418 419 420 421

    byidx = {}
    bynam = {}
    for line in ipdata.split("\n"):
        if line == "":
            continue
        match = re.search(r'^(\d+):\s+(\S+?)(:?)\s+(.*)', line)
        if not match:
            raise RuntimeError("Invalid `ip' command output")
        idx = int(match.group(1))
        name = match.group(2)
        if match.group(3):
            bynam[name] = byidx[idx] = []
            continue # link info
422
        bynam[name].append(_parse_ip_addr(match.group(4)))
Martín Ferrari's avatar
Martín Ferrari committed
423 424
    return byidx, bynam

425 426 427
def _parse_ip_addr(line):
    match = re.search(r'^inet ([0-9.]+)/(\d+)(?: brd ([0-9.]+))?', line)
    if match != None:
428
        return ipv4address(
429 430 431 432
                address     = match.group(1),
                prefix_len  = match.group(2),
                broadcast   = match.group(3))

433
    match = re.search(r'^inet6 ([0-9a-f:]+)/(\d+)', line)
434
    if match != None:
435
        return ipv6address(
436 437 438
                address     = match.group(1),
                prefix_len  = match.group(2))

439
    raise RuntimeError("Problems parsing ip command output")
440

441
def add_addr(iface, address):
Martín Ferrari's avatar
Martín Ferrari committed
442
    ifname = _get_if_name(iface)
443 444 445
    addresses = get_addr_data()[1][ifname]
    assert address not in addresses

Martín Ferrari's avatar
Martín Ferrari committed
446
    cmd = [ip_path, "addr", "add", "dev", ifname, "local",
447 448 449
            "%s/%d" % (address.address, int(address.prefix_len))]
    if hasattr(address, "broadcast"):
        cmd += ["broadcast", address.broadcast if address.broadcast else "+"]
Martín Ferrari's avatar
Martín Ferrari committed
450
    execute(cmd)
451 452

def del_addr(iface, address):
Martín Ferrari's avatar
Martín Ferrari committed
453
    ifname = _get_if_name(iface)
454 455 456
    addresses = get_addr_data()[1][ifname]
    assert address in addresses

Martín Ferrari's avatar
Martín Ferrari committed
457
    cmd = [ip_path, "addr", "del", "dev", ifname, "local",
458
            "%s/%d" % (address.address, int(address.prefix_len))]
Martín Ferrari's avatar
Martín Ferrari committed
459
    execute(cmd)
460

461
def set_addr(iface, addresses, recover = True):
Martín Ferrari's avatar
Martín Ferrari committed
462
    ifname = _get_if_name(iface)
463 464 465
    addresses = get_addr_data()[1][ifname]
    to_remove = set(orig_addresses) - set(addresses)
    to_add = set(addresses) - set(orig_addresses)
466 467 468

    for a in to_remove:
        try:
469
            del_addr(ifname, a)
470 471
        except:
            if recover:
472
                set_addr(orig_addresses, recover = False) # rollback
473 474 475 476
                raise

    for a in to_add:
        try:
477
            add_addr(ifname, a)
478 479
        except:
            if recover:
480
                set_addr(orig_addresses, recover = False) # rollback
481 482
                raise

Martín Ferrari's avatar
Martín Ferrari committed
483
# Bridge handling
484
def _sysfs_read_br(brname):
485 486 487
    def readval(fname):
        f = file(fname)
        return f.readline().strip()
Martín Ferrari's avatar
Martín Ferrari committed
488

489 490
    p = "/sys/class/net/%s/bridge/" % brname
    p2 = "/sys/class/net/%s/brif/" % brname
491 492 493 494 495
    try:
        os.stat(p)
    except:
        return None
    return dict(
496 497 498 499 500
            stp             = readval(p + "stp_state"),
            forward_delay   = float(readval(p + "forward_delay")) / 100,
            hello_time      = float(readval(p + "hello_time")) / 100,
            ageing_time     = float(readval(p + "ageing_time")) / 100,
            max_age         = float(readval(p + "max_age")) / 100,
501 502 503
            ports           = os.listdir(p2))

def get_bridge_data():
Martín Ferrari's avatar
Martín Ferrari committed
504 505
    # brctl stinks too much; it is better to directly use sysfs, it is
    # probably stable by now
Martín Ferrari's avatar
Martín Ferrari committed
506 507 508
    byidx = {}
    bynam = {}
    ports = {}
509
    ifdata = get_if_data()
510 511 512
    for iface in ifdata[0].values():
        brdata = _sysfs_read_br(iface.name)
        if brdata == None:
Martín Ferrari's avatar
Martín Ferrari committed
513
            continue
514 515
        ports[iface.index] = [ifdata[1][x].index for x in brdata["ports"]]
        del brdata["ports"]
516
        bynam[iface.name] = byidx[iface.index] = \
517
                bridge.upgrade(iface, **brdata)
Martín Ferrari's avatar
Martín Ferrari committed
518 519
    return byidx, bynam, ports

520 521 522
def get_bridge(br):
    iface = get_if(br)
    brdata = _sysfs_read_br(iface.name)
523 524
    #ports = [ifdata[1][x].index for x in brdata["ports"]]
    del brdata["ports"]
525
    return bridge.upgrade(iface, **brdata)
526

Martín Ferrari's avatar
Martín Ferrari committed
527 528
def create_bridge(br):
    if isinstance(br, str):
529
        br = interface(name = br)
Martín Ferrari's avatar
Martín Ferrari committed
530
    assert br.name
Martín Ferrari's avatar
Martín Ferrari committed
531
    execute([brctl_path, "addbr", br.name])
Martín Ferrari's avatar
Martín Ferrari committed
532 533 534 535 536 537 538 539 540 541 542 543 544
    try:
        set_if(br)
    except:
        (t, v, bt) = sys.exc_info()
        try:
            del_bridge(br)
        except:
            pass
        raise t, v, bt
    return get_if_data()[1][br.name]

def del_bridge(br):
    brname = _get_if_name(br)
Martín Ferrari's avatar
Martín Ferrari committed
545
    execute([brctl_path, "delbr", brname])
Martín Ferrari's avatar
Martín Ferrari committed
546

547 548
def set_bridge(br, recover = True):
    def saveval(fname, val):
549
        f = file(fname, "w")
550 551 552 553 554 555 556 557 558 559 560 561
        f.write(str(val))
        f.close()
    def do_cmds(basename, cmds, orig_br):
        for n, v in cmds:
            try:
                saveval(basename + n, v)
            except:
                if recover:
                    set_bridge(orig_br, recover = False) # rollback
                    set_if(orig_br, recover = False) # rollback
                    raise

562
    orig_br = get_bridge(br)
563 564 565 566
    diff = br - orig_br # Only set what's needed

    cmds = []
    if diff.stp != None:
567
        cmds.append(("stp_state", int(diff.stp)))
568
    if diff.forward_delay != None:
569
        cmds.append(("forward_delay", int(diff.forward_delay)))
570
    if diff.hello_time != None:
571
        cmds.append(("hello_time", int(diff.hello_time)))
572
    if diff.ageing_time != None:
573
        cmds.append(("ageing_time", int(diff.ageing_time)))
574
    if diff.max_age != None:
575
        cmds.append(("max_age", int(diff.max_age)))
576 577

    set_if(diff)
578
    name = diff.name if diff.name != None else orig_br.name
579
    do_cmds("/sys/class/net/%s/bridge/" % name, cmds, orig_br)
580

Martín Ferrari's avatar
Martín Ferrari committed
581 582 583
def add_bridge_port(br, iface):
    ifname = _get_if_name(iface)
    brname = _get_if_name(br)
Martín Ferrari's avatar
Martín Ferrari committed
584
    execute([brctl_path, "addif", brname, ifname])
Martín Ferrari's avatar
Martín Ferrari committed
585 586 587 588

def del_bridge_port(br, iface):
    ifname = _get_if_name(iface)
    brname = _get_if_name(br)
Martín Ferrari's avatar
Martín Ferrari committed
589
    execute([brctl_path, "delif", brname, ifname])
Martín Ferrari's avatar
Martín Ferrari committed
590

Martín Ferrari's avatar
Martín Ferrari committed
591 592
# Routing

Martín Ferrari's avatar
Martín Ferrari committed
593
def get_all_route_data():
Martín Ferrari's avatar
Martín Ferrari committed
594 595
    ipdata = backticks([ip_path, "-o", "route", "list"]) # "table", "all"
    ipdata += backticks([ip_path, "-o", "-f", "inet6", "route", "list"])
Martín Ferrari's avatar
Martín Ferrari committed
596 597 598 599 600 601 602 603

    ifdata = get_if_data()[1]
    ret = []
    for line in ipdata.split("\n"):
        if line == "":
            continue
        match = re.match(r'(?:(unicast|local|broadcast|multicast|throw|' +
                r'unreachable|prohibit|blackhole|nat) )?' +
604
                r'(\S+)(?: via (\S+))? dev (\S+).*(?: metric (\d+))?', line)
Martín Ferrari's avatar
Martín Ferrari committed
605
        if not match:
606
            raise RuntimeError("Invalid output from `ip route': `%s'" % line)
607
        tipe = match.group(1) or "unicast"
Martín Ferrari's avatar
Martín Ferrari committed
608 609
        prefix = match.group(2)
        nexthop = match.group(3)
610 611
        interface = ifdata[match.group(4)]
        metric = match.group(5)
612
        if prefix == "default" or re.search(r'/0$', prefix):
Martín Ferrari's avatar
Martín Ferrari committed
613
            prefix = None
614 615
            prefix_len = 0
        else:
616 617 618 619 620
            match = re.match(r'([0-9a-f:.]+)(?:/(\d+))?$', prefix)
            prefix = match.group(1)
            prefix_len = int(match.group(2) or 32)
        ret.append(route(tipe, prefix, prefix_len, nexthop, interface.index,
            metric))
Martín Ferrari's avatar
Martín Ferrari committed
621
    return ret
Martín Ferrari's avatar
Martín Ferrari committed
622 623 624

def get_route_data():
    # filter out non-unicast routes
625
    return [x for x in get_all_route_data() if x.tipe == "unicast"]
Martín Ferrari's avatar
Martín Ferrari committed
626

Martín Ferrari's avatar
Martín Ferrari committed
627
def add_route(route):
628 629
    # Cannot really test this
    #if route in get_all_route_data():
630 631
    #    raise ValueError("Route already exists")
    _add_del_route("add", route)
Martín Ferrari's avatar
Martín Ferrari committed
632

Martín Ferrari's avatar
Martín Ferrari committed
633
def del_route(route):
634 635
    # Cannot really test this
    #if route not in get_all_route_data():
636 637
    #    raise ValueError("Route does not exist")
    _add_del_route("del", route)
Martín Ferrari's avatar
Martín Ferrari committed
638

Martín Ferrari's avatar
Martín Ferrari committed
639
def _add_del_route(action, route):
Martín Ferrari's avatar
Martín Ferrari committed
640
    cmd = [ip_path, "route", action]
641
    if route.tipe != "unicast":
Martín Ferrari's avatar
Martín Ferrari committed
642 643 644
        cmd += [route.tipe]
    if route.prefix:
        cmd += ["%s/%d" % (route.prefix, route.prefix_len)]
Martín Ferrari's avatar
Martín Ferrari committed
645
    else:
646
        cmd += ["default"]
Martín Ferrari's avatar
Martín Ferrari committed
647
    if route.nexthop:
648
        cmd += ["via", route.nexthop]
649
    if route.interface:
650
        cmd += ["dev", _get_if_name(route.interface)]
Martín Ferrari's avatar
Martín Ferrari committed
651
    execute(cmd)
652 653 654 655

# TC stuff

def get_tc_tree():
Martín Ferrari's avatar
Martín Ferrari committed
656
    tcdata = backticks([tc_path, "qdisc", "show"])
657 658 659 660 661

    data = {}
    for line in tcdata.split("\n"):
        if line == "":
            continue
Martín Ferrari's avatar
Martín Ferrari committed
662 663
        match = re.match(r'qdisc (\S+) ([0-9a-f]+):[0-9a-f]* dev (\S+) ' +
                r'(?:parent ([0-9a-f]+):[0-9a-f]*|root)\s*(.*)', line)
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
        if not match:
            raise RuntimeError("Invalid output from `tc qdisc': `%s'" % line)
        qdisc = match.group(1)
        handle = match.group(2)
        iface = match.group(3)
        parent = match.group(4) # or None
        extra = match.group(5)
        if iface not in data:
            data[iface] = {}
        if parent not in data[iface]:
            data[iface][parent] = []
        data[iface][parent] += [[handle, qdisc, parent, extra]]

    tree = {}
    for iface in data:
        def gen_tree(data, data_node):
            children = []
            node = {"handle": data_node[0],
                    "qdisc": data_node[1],
                    "extra": data_node[3],
                    "children": []}
            if data_node[0] in data:
                for h in data[data_node[0]]:
                    node["children"].append(gen_tree(data, h))
            return node
        tree[iface] = gen_tree(data[iface], data[iface][None][0])
    return tree

692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712
_multipliers = {"M": 1000000, "K": 1000}
_dividers = {"m": 1000, "u": 1000000}
def _parse_netem_delay(line):
    ret = {}
    match = re.search(r'delay ([\d.]+)([mu]?)s(?: +([\d.]+)([mu]?)s)?' +
            r'(?: *([\d.]+)%)?(?: *distribution (\S+))?', line)
    if not match:
        return ret

    delay = float(match.group(1))
    if match.group(2):
        delay /= _dividers[match.group(2)]
    ret["delay"] = delay

    if match.group(3):
        delay_jitter = float(match.group(3))
        if match.group(4):
            delay_jitter /= _dividers[match.group(4)]
        ret["delay_jitter"] = delay_jitter

    if match.group(5):
713
        ret["delay_correlation"] = float(match.group(5)) / 100
714 715 716 717 718 719 720 721 722 723 724 725

    if match.group(6):
        ret["delay_distribution"] = match.group(6)

    return ret

def _parse_netem_loss(line):
    ret = {}
    match = re.search(r'loss ([\d.]+)%(?: *([\d.]+)%)?', line)
    if not match:
        return ret

726
    ret["loss"] = float(match.group(1)) / 100
727
    if match.group(2):
728
        ret["loss_correlation"] = float(match.group(2)) / 100
729 730 731 732 733 734 735 736
    return ret

def _parse_netem_dup(line):
    ret = {}
    match = re.search(r'duplicate ([\d.]+)%(?: *([\d.]+)%)?', line)
    if not match:
        return ret

737
    ret["dup"] = float(match.group(1)) / 100
738
    if match.group(2):
739
        ret["dup_correlation"] = float(match.group(2)) / 100
740 741 742 743 744 745 746 747
    return ret

def _parse_netem_corrupt(line):
    ret = {}
    match = re.search(r'corrupt ([\d.]+)%(?: *([\d.]+)%)?', line)
    if not match:
        return ret

748
    ret["corrupt"] = float(match.group(1)) / 100
749
    if match.group(2):
750
        ret["corrupt_correlation"] = float(match.group(2)) / 100
751 752
    return ret

753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
def get_tc_data():
    tree = get_tc_tree()
    ifdata = get_if_data()

    ret = {}
    for i in ifdata[0]:
        ret[i] = {"qdiscs": {}}
        if ifdata[0][i].name not in tree:
            continue
        node = tree[ifdata[0][i].name]
        if not node["children"]:
            if node["qdisc"] == "mq" or node["qdisc"] == "pfifo_fast" \
                    or node["qdisc"][1:] == "fifo":
                continue

            if node["qdisc"] == "netem":
                tbf = None
                netem = node["extra"], node["handle"]
            elif node["qdisc"] == "tbf":
                tbf = node["extra"], node["handle"]
                netem = None
            else:
                ret[i] = "foreign"
                continue
        else:
            if node["qdisc"] != "tbf" or len(node["children"]) != 1 or \
                    node["children"][0]["qdisc"] != "netem" or \
                    node["children"][0]["children"]:
                ret[i] = "foreign"
                continue
            tbf = node["extra"], node["handle"]
            netem = node["children"][0]["extra"], \
                    node["children"][0]["handle"]

        if tbf:
            ret[i]["qdiscs"]["tbf"] = tbf[1]
            match = re.search(r'rate (\d+)([MK]?)bit', tbf[0])
            if not match:
                ret[i] = "foreign"
                continue
            bandwidth = int(match.group(1))
            if match.group(2):
795
                bandwidth *= _multipliers[match.group(2)]
796 797 798 799
            ret[i]["bandwidth"] = bandwidth

        if netem:
            ret[i]["qdiscs"]["netem"] = netem[1]
800 801 802 803
            ret[i].update(_parse_netem_delay(netem[0]))
            ret[i].update(_parse_netem_loss(netem[0]))
            ret[i].update(_parse_netem_dup(netem[0]))
            ret[i].update(_parse_netem_corrupt(netem[0]))
804 805 806 807 808 809 810 811
    return ret, ifdata[0], ifdata[1]

def clear_tc(iface):
    iface = get_if(iface)
    tcdata = get_tc_data()[0]
    if tcdata[iface.index] == None:
        return
    # Any other case, we clean
Martín Ferrari's avatar
Martín Ferrari committed
812
    execute([tc_path, "qdisc", "del", "dev", iface.name, "root"])
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827

def set_tc(iface, bandwidth = None, delay = None, delay_jitter = None,
        delay_correlation = None, delay_distribution = None,
        loss = None, loss_correlation = None,
        dup = None, dup_correlation = None,
        corrupt = None, corrupt_correlation = None):
    use_netem = bool(delay or delay_jitter or delay_correlation or
            delay_distribution or loss or loss_correlation or dup or
            dup_correlation or corrupt or corrupt_correlation)

    iface = get_if(iface)
    tcdata, ifdata = get_tc_data()[0:2]
    commands = []
    if tcdata[iface.index] == 'foreign':
        # Avoid the overhead of calling tc+ip again
Martín Ferrari's avatar
Martín Ferrari committed
828
        commands.append([tc_path, "qdisc", "del", "dev", iface.name, "root"])
829
        tcdata[iface.index] = {'qdiscs':  []}
830 831 832 833 834 835 836 837 838 839 840 841 842 843

    has_netem = 'netem' in tcdata[iface.index]['qdiscs']
    has_tbf = 'tbf' in tcdata[iface.index]['qdiscs']

    if not bandwidth and not use_netem:
        if has_netem or has_tbf:
            clear_tc(iface)
        return

    if has_netem == use_netem and has_tbf == bool(bandwidth):
        cmd = "change"
    else:
        # Too much work to do better :)
        if has_netem or has_tbf:
Martín Ferrari's avatar
Martín Ferrari committed
844 845
            commands.append([tc_path, "qdisc", "del", "dev", iface.name,
                "root"])
846 847 848
        cmd = "add"

    if bandwidth:
849
        rate = "%dbit" % int(bandwidth)
850
        mtu = ifdata[iface.index].mtu
Martín Ferrari's avatar
Martín Ferrari committed
851
        burst = max(mtu, int(bandwidth) / hz)
852 853 854 855
        limit = burst * 2 # FIXME?
        handle = "1:"
        if cmd == "change":
            handle = "%d:" % int(tcdata[iface.index]["qdiscs"]["tbf"])
Martín Ferrari's avatar
Martín Ferrari committed
856
        command = [tc_path, "qdisc", cmd, "dev", iface.name, "root", "handle",
857 858 859 860 861 862 863 864
                handle, "tbf", "rate", rate, "limit", str(limit), "burst",
                str(burst)]
        commands.append(command)

    if use_netem:
        handle = "2:"
        if cmd == "change":
            handle = "%d:" % int(tcdata[iface.index]["qdiscs"]["netem"])
Martín Ferrari's avatar
Martín Ferrari committed
865
        command = [tc_path, "qdisc", cmd, "dev", iface.name, "handle", handle]
866 867 868 869 870 871 872 873 874 875 876 877 878
        if bandwidth:
            parent = "1:"
            if cmd == "change":
                parent = "%d:" % int(tcdata[iface.index]["qdiscs"]["tbf"])
            command += ["parent", parent]
        else:
            command += ["root"]
        command += ["netem"]
        if delay:
            command += ["delay", "%fs" % delay]
            if delay_jitter:
                command += ["%fs" % delay_jitter]
            if delay_correlation:
879 880
                if not delay_jitter:
                    raise ValueError("delay_correlation requires delay_jitter")
881
                command += ["%f%%" % (delay_correlation * 100)]
882
            if delay_distribution:
883
                if not delay_jitter:
884 885 886
                    raise ValueError("delay_distribution requires delay_jitter")
                command += ["distribution", delay_distribution]
        if loss:
887
            command += ["loss", "%f%%" % (loss * 100)]
888
            if loss_correlation:
889
                command += ["%f%%" % (loss_correlation * 100)]
890
        if dup:
891
            command += ["duplicate", "%f%%" % (dup * 100)]
892
            if dup_correlation:
893
                command += ["%f%%" % (dup_correlation * 100)]
894
        if corrupt:
895
            command += ["corrupt", "%f%%" % (corrupt * 100)]
896
            if corrupt_correlation:
897
                command += ["%f%%" % (corrupt_correlation * 100)]
898 899 900
        commands.append(command)

    for c in commands:
Martín Ferrari's avatar
Martín Ferrari committed
901
        execute(c)
902

903 904
def create_tap(iface, use_pi = False, tun = False):
    """Creates a tap/tun device and returns the associated file descriptor"""
905 906 907 908
    if isinstance(iface, str):
        iface = interface(name = iface)
    assert iface.name

909
    IFF_TUN     = 0x0001
910 911
    IFF_TAP     = 0x0002
    IFF_NO_PI   = 0x1000
912
    TUNSETIFF   = 0x400454ca
913 914 915 916
    if tun:
        mode = IFF_TUN
    else:
        mode = IFF_TAP
917 918 919
    if not use_pi:
        mode |= IFF_NO_PI

920
    fd = os.open("/dev/net/tun", os.O_RDWR)
921

Martín Ferrari's avatar
Martín Ferrari committed
922
    err = fcntl.ioctl(fd, TUNSETIFF, struct.pack("16sH", iface.name, mode))
923 924 925
    if err < 0:
        os.close(fd)
        raise RuntimeError("Could not configure device %s" % iface.name)
926 927 928 929

    try:
        set_if(iface)
    except:
Martín Ferrari's avatar
Martín Ferrari committed
930 931
        os.close(fd)
        raise
932 933
    interfaces = get_if_data()[1]
    return interfaces[iface.name], fd
934