1#
2# Copyright (c) 2011 Thomas Graf <tgraf@suug.ch>
3#
4
5"""Module providing access to network links
6
7This module provides an interface to view configured network links,
8modify them and to add and delete virtual network links.
9
10The following is a basic example:
11    import netlink.core as netlink
12    import netlink.route.link as link
13
14    sock = netlink.Socket()
15    sock.connect(netlink.NETLINK_ROUTE)
16
17    cache = link.LinkCache()	# create new empty link cache
18    cache.refill(sock)		# fill cache with all configured links
19    eth0 = cache['eth0']		# lookup link "eth0"
20    print eth0			# print basic configuration
21
22The module contains the following public classes:
23
24  - Link -- Represents a network link. Instances can be created directly
25        via the constructor (empty link objects) or via the refill()
26        method of a LinkCache.
27  - LinkCache -- Derived from netlink.Cache, holds any number of
28         network links (Link instances). Main purpose is to keep
29         a local list of all network links configured in the
30         kernel.
31
32The following public functions exist:
33  - get_from_kernel(socket, name)
34
35"""
36
37from __future__ import absolute_import
38
39__version__ = '0.1'
40__all__ = [
41    'LinkCache',
42    'Link',
43    'get_from_kernel',
44]
45
46import socket
47from .. import core as netlink
48from .. import capi as core_capi
49from .  import capi as capi
50from .links  import inet as inet
51from .. import util as util
52
53# Link statistics definitions
54RX_PACKETS = 0
55TX_PACKETS = 1
56RX_BYTES = 2
57TX_BYTES = 3
58RX_ERRORS = 4
59TX_ERRORS = 5
60RX_DROPPED = 6
61TX_DROPPED = 7
62RX_COMPRESSED = 8
63TX_COMPRESSED = 9
64RX_FIFO_ERR = 10
65TX_FIFO_ERR = 11
66RX_LEN_ERR = 12
67RX_OVER_ERR = 13
68RX_CRC_ERR = 14
69RX_FRAME_ERR = 15
70RX_MISSED_ERR = 16
71TX_ABORT_ERR = 17
72TX_CARRIER_ERR = 18
73TX_HBEAT_ERR = 19
74TX_WIN_ERR = 20
75COLLISIONS = 21
76MULTICAST = 22
77IP6_INPKTS = 23
78IP6_INHDRERRORS = 24
79IP6_INTOOBIGERRORS = 25
80IP6_INNOROUTES = 26
81IP6_INADDRERRORS = 27
82IP6_INUNKNOWNPROTOS = 28
83IP6_INTRUNCATEDPKTS = 29
84IP6_INDISCARDS = 30
85IP6_INDELIVERS = 31
86IP6_OUTFORWDATAGRAMS = 32
87IP6_OUTPKTS = 33
88IP6_OUTDISCARDS = 34
89IP6_OUTNOROUTES = 35
90IP6_REASMTIMEOUT = 36
91IP6_REASMREQDS = 37
92IP6_REASMOKS = 38
93IP6_REASMFAILS = 39
94IP6_FRAGOKS = 40
95IP6_FRAGFAILS = 41
96IP6_FRAGCREATES = 42
97IP6_INMCASTPKTS = 43
98IP6_OUTMCASTPKTS = 44
99IP6_INBCASTPKTS = 45
100IP6_OUTBCASTPKTS = 46
101IP6_INOCTETS = 47
102IP6_OUTOCTETS = 48
103IP6_INMCASTOCTETS = 49
104IP6_OUTMCASTOCTETS = 50
105IP6_INBCASTOCTETS = 51
106IP6_OUTBCASTOCTETS = 52
107ICMP6_INMSGS = 53
108ICMP6_INERRORS = 54
109ICMP6_OUTMSGS = 55
110ICMP6_OUTERRORS = 56
111
112class LinkCache(netlink.Cache):
113    """Cache of network links"""
114
115    def __init__(self, family=socket.AF_UNSPEC, cache=None):
116        if not cache:
117            cache = self._alloc_cache_name('route/link')
118
119        self._info_module = None
120        self._protocol = netlink.NETLINK_ROUTE
121        self._nl_cache = cache
122        self._set_arg1(family)
123
124    def __getitem__(self, key):
125        if type(key) is int:
126            link = capi.rtnl_link_get(self._nl_cache, key)
127        else:
128            link = capi.rtnl_link_get_by_name(self._nl_cache, key)
129
130        if link is None:
131            raise KeyError()
132        else:
133            return Link.from_capi(link)
134
135    @staticmethod
136    def _new_object(obj):
137        return Link(obj)
138
139    def _new_cache(self, cache):
140        return LinkCache(family=self.arg1, cache=cache)
141
142class Link(netlink.Object):
143    """Network link"""
144
145    def __init__(self, obj=None):
146        netlink.Object.__init__(self, 'route/link', 'link', obj)
147        self._rtnl_link = self._obj2type(self._nl_object)
148
149        if self.type:
150            self._module_lookup('netlink.route.links.' + self.type)
151
152        self.inet = inet.InetLink(self)
153        self.af = {'inet' : self.inet }
154
155    def __enter__(self):
156        return self
157
158    def __exit__(self, exc_type, exc_value, tb):
159        if exc_type is None:
160            self.change()
161        else:
162            return false
163
164    @classmethod
165    def from_capi(cls, obj):
166        return cls(capi.link2obj(obj))
167
168    @staticmethod
169    def _obj2type(obj):
170        return capi.obj2link(obj)
171
172    def __cmp__(self, other):
173        return self.ifindex - other.ifindex
174
175    @staticmethod
176    def _new_instance(obj):
177        if not obj:
178            raise ValueError()
179
180        return Link(obj)
181
182    @property
183    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
184    def ifindex(self):
185        """interface index"""
186        return capi.rtnl_link_get_ifindex(self._rtnl_link)
187
188    @ifindex.setter
189    def ifindex(self, value):
190        capi.rtnl_link_set_ifindex(self._rtnl_link, int(value))
191
192        # ifindex is immutable but we assume that if _orig does not
193        # have an ifindex specified, it was meant to be given here
194        if capi.rtnl_link_get_ifindex(self._orig) == 0:
195            capi.rtnl_link_set_ifindex(self._orig, int(value))
196
197    @property
198    @netlink.nlattr(type=str, fmt=util.bold)
199    def name(self):
200        """Name of link"""
201        return capi.rtnl_link_get_name(self._rtnl_link)
202
203    @name.setter
204    def name(self, value):
205        capi.rtnl_link_set_name(self._rtnl_link, value)
206
207        # name is the secondary identifier, if _orig does not have
208        # the name specified yet, assume it was meant to be specified
209        # here. ifindex will always take priority, therefore if ifindex
210        # is specified as well, this will be ignored automatically.
211        if capi.rtnl_link_get_name(self._orig) is None:
212            capi.rtnl_link_set_name(self._orig, value)
213
214    @property
215    @netlink.nlattr(type=str, fmt=util.string)
216    def flags(self):
217        """Flags
218        Setting this property will *Not* reset flags to value you supply in
219        Examples:
220        link.flags = '+xxx' # add xxx flag
221        link.flags = 'xxx'  # exactly the same
222        link.flags = '-xxx' # remove xxx flag
223        link.flags = [ '+xxx', '-yyy' ] # list operation
224        """
225        flags = capi.rtnl_link_get_flags(self._rtnl_link)
226        return capi.rtnl_link_flags2str(flags, 256)[0].split(',')
227
228    def _set_flag(self, flag):
229        if flag.startswith('-'):
230            i = capi.rtnl_link_str2flags(flag[1:])
231            capi.rtnl_link_unset_flags(self._rtnl_link, i)
232        elif flag.startswith('+'):
233            i = capi.rtnl_link_str2flags(flag[1:])
234            capi.rtnl_link_set_flags(self._rtnl_link, i)
235        else:
236            i = capi.rtnl_link_str2flags(flag)
237            capi.rtnl_link_set_flags(self._rtnl_link, i)
238
239    @flags.setter
240    def flags(self, value):
241        if not (type(value) is str):
242            for flag in value:
243                self._set_flag(flag)
244        else:
245            self._set_flag(value)
246
247    @property
248    @netlink.nlattr(type=int, fmt=util.num)
249    def mtu(self):
250        """Maximum Transmission Unit"""
251        return capi.rtnl_link_get_mtu(self._rtnl_link)
252
253    @mtu.setter
254    def mtu(self, value):
255        capi.rtnl_link_set_mtu(self._rtnl_link, int(value))
256
257    @property
258    @netlink.nlattr(type=int, immutable=True, fmt=util.num)
259    def family(self):
260        """Address family"""
261        return capi.rtnl_link_get_family(self._rtnl_link)
262
263    @family.setter
264    def family(self, value):
265        capi.rtnl_link_set_family(self._rtnl_link, value)
266
267    @property
268    @netlink.nlattr(type=str, fmt=util.addr)
269    def address(self):
270        """Hardware address (MAC address)"""
271        a = capi.rtnl_link_get_addr(self._rtnl_link)
272        return netlink.AbstractAddress(a)
273
274    @address.setter
275    def address(self, value):
276        capi.rtnl_link_set_addr(self._rtnl_link, value._addr)
277
278    @property
279    @netlink.nlattr(type=str, fmt=util.addr)
280    def broadcast(self):
281        """Hardware broadcast address"""
282        a = capi.rtnl_link_get_broadcast(self._rtnl_link)
283        return netlink.AbstractAddress(a)
284
285    @broadcast.setter
286    def broadcast(self, value):
287        capi.rtnl_link_set_broadcast(self._rtnl_link, value._addr)
288
289    @property
290    @netlink.nlattr(type=str, immutable=True, fmt=util.string)
291    def qdisc(self):
292        """Name of qdisc (cannot be changed)"""
293        return capi.rtnl_link_get_qdisc(self._rtnl_link)
294
295    @qdisc.setter
296    def qdisc(self, value):
297        capi.rtnl_link_set_qdisc(self._rtnl_link, value)
298
299    @property
300    @netlink.nlattr(type=int, fmt=util.num)
301    def txqlen(self):
302        """Length of transmit queue"""
303        return capi.rtnl_link_get_txqlen(self._rtnl_link)
304
305    @txqlen.setter
306    def txqlen(self, value):
307        capi.rtnl_link_set_txqlen(self._rtnl_link, int(value))
308
309    @property
310    @netlink.nlattr(type=str, immutable=True, fmt=util.string)
311    def arptype(self):
312        """Type of link (cannot be changed)"""
313        type_ = capi.rtnl_link_get_arptype(self._rtnl_link)
314        return core_capi.nl_llproto2str(type_, 64)[0]
315
316    @arptype.setter
317    def arptype(self, value):
318        i = core_capi.nl_str2llproto(value)
319        capi.rtnl_link_set_arptype(self._rtnl_link, i)
320
321    @property
322    @netlink.nlattr(type=str, immutable=True, fmt=util.string, title='state')
323    def operstate(self):
324        """Operational status"""
325        operstate = capi.rtnl_link_get_operstate(self._rtnl_link)
326        return capi.rtnl_link_operstate2str(operstate, 32)[0]
327
328    @operstate.setter
329    def operstate(self, value):
330        i = capi.rtnl_link_str2operstate(value)
331        capi.rtnl_link_set_operstate(self._rtnl_link, i)
332
333    @property
334    @netlink.nlattr(type=str, immutable=True, fmt=util.string)
335    def mode(self):
336        """Link mode"""
337        mode = capi.rtnl_link_get_linkmode(self._rtnl_link)
338        return capi.rtnl_link_mode2str(mode, 32)[0]
339
340    @mode.setter
341    def mode(self, value):
342        i = capi.rtnl_link_str2mode(value)
343        capi.rtnl_link_set_linkmode(self._rtnl_link, i)
344
345    @property
346    @netlink.nlattr(type=str, fmt=util.string)
347    def alias(self):
348        """Interface alias (SNMP)"""
349        return capi.rtnl_link_get_ifalias(self._rtnl_link)
350
351    @alias.setter
352    def alias(self, value):
353        capi.rtnl_link_set_ifalias(self._rtnl_link, value)
354
355    @property
356    @netlink.nlattr(type=str, fmt=util.string)
357    def type(self):
358        """Link type"""
359        return capi.rtnl_link_get_type(self._rtnl_link)
360
361    @type.setter
362    def type(self, value):
363        if capi.rtnl_link_set_type(self._rtnl_link, value) < 0:
364            raise NameError('unknown info type')
365
366        self._module_lookup('netlink.route.links.' + value)
367
368    def get_stat(self, stat):
369        """Retrieve statistical information"""
370        if type(stat) is str:
371            stat = capi.rtnl_link_str2stat(stat)
372            if stat < 0:
373                raise NameError('unknown name of statistic')
374
375        return capi.rtnl_link_get_stat(self._rtnl_link, stat)
376
377    def enslave(self, slave, sock=None):
378        if not sock:
379            sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
380
381        return capi.rtnl_link_enslave(sock._sock, self._rtnl_link, slave._rtnl_link)
382
383    def release(self, slave, sock=None):
384        if not sock:
385            sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
386
387        return capi.rtnl_link_release(sock._sock, self._rtnl_link, slave._rtnl_link)
388
389    def add(self, sock=None, flags=None):
390        if not sock:
391            sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
392
393        if not flags:
394            flags = netlink.NLM_F_CREATE
395
396        ret = capi.rtnl_link_add(sock._sock, self._rtnl_link, flags)
397        if ret < 0:
398            raise netlink.KernelError(ret)
399
400    def change(self, sock=None, flags=0):
401        """Commit changes made to the link object"""
402        if sock is None:
403            sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
404
405        if not self._orig:
406            raise netlink.NetlinkError('Original link not available')
407        ret = capi.rtnl_link_change(sock._sock, self._orig, self._rtnl_link, flags)
408        if ret < 0:
409            raise netlink.KernelError(ret)
410
411    def delete(self, sock=None):
412        """Attempt to delete this link in the kernel"""
413        if sock is None:
414            sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
415
416        ret = capi.rtnl_link_delete(sock._sock, self._rtnl_link)
417        if ret < 0:
418            raise netlink.KernelError(ret)
419
420    ###################################################################
421    # private properties
422    #
423    # Used for formatting output. USE AT OWN RISK
424    @property
425    def _state(self):
426        if 'up' in self.flags:
427            buf = util.good('up')
428            if 'lowerup' not in self.flags:
429                buf += ' ' + util.bad('no-carrier')
430        else:
431            buf = util.bad('down')
432        return buf
433
434    @property
435    def _brief(self):
436        return self._module_brief() + self._foreach_af('brief')
437
438    @property
439    def _flags(self):
440        ignore = [
441            'up',
442            'running',
443            'lowerup',
444        ]
445        return ','.join([flag for flag in self.flags if flag not in ignore])
446
447    def _foreach_af(self, name, args=None):
448        buf = ''
449        for af in self.af:
450            try:
451                func = getattr(self.af[af], name)
452                s = str(func(args))
453                if len(s) > 0:
454                    buf += ' ' + s
455            except AttributeError:
456                pass
457        return buf
458
459    def format(self, details=False, stats=False, indent=''):
460        """Return link as formatted text"""
461        fmt = util.MyFormatter(self, indent)
462
463        buf = fmt.format('{a|ifindex} {a|name} {a|arptype} {a|address} '\
464                 '{a|_state} <{a|_flags}> {a|_brief}')
465
466        if details:
467            buf += fmt.nl('\t{t|mtu} {t|txqlen} {t|weight} '\
468                      '{t|qdisc} {t|operstate}')
469            buf += fmt.nl('\t{t|broadcast} {t|alias}')
470
471            buf += self._foreach_af('details', fmt)
472
473        if stats:
474            l = [['Packets', RX_PACKETS, TX_PACKETS],
475                 ['Bytes', RX_BYTES, TX_BYTES],
476                 ['Errors', RX_ERRORS, TX_ERRORS],
477                 ['Dropped', RX_DROPPED, TX_DROPPED],
478                 ['Compressed', RX_COMPRESSED, TX_COMPRESSED],
479                 ['FIFO Errors', RX_FIFO_ERR, TX_FIFO_ERR],
480                 ['Length Errors', RX_LEN_ERR, None],
481                 ['Over Errors', RX_OVER_ERR, None],
482                 ['CRC Errors', RX_CRC_ERR, None],
483                 ['Frame Errors', RX_FRAME_ERR, None],
484                 ['Missed Errors', RX_MISSED_ERR, None],
485                 ['Abort Errors', None, TX_ABORT_ERR],
486                 ['Carrier Errors', None, TX_CARRIER_ERR],
487                 ['Heartbeat Errors', None, TX_HBEAT_ERR],
488                 ['Window Errors', None, TX_WIN_ERR],
489                 ['Collisions', None, COLLISIONS],
490                 ['Multicast', None, MULTICAST],
491                 ['', None, None],
492                 ['Ipv6:', None, None],
493                 ['Packets', IP6_INPKTS, IP6_OUTPKTS],
494                 ['Bytes', IP6_INOCTETS, IP6_OUTOCTETS],
495                 ['Discards', IP6_INDISCARDS, IP6_OUTDISCARDS],
496                 ['Multicast Packets', IP6_INMCASTPKTS, IP6_OUTMCASTPKTS],
497                 ['Multicast Bytes', IP6_INMCASTOCTETS, IP6_OUTMCASTOCTETS],
498                 ['Broadcast Packets', IP6_INBCASTPKTS, IP6_OUTBCASTPKTS],
499                 ['Broadcast Bytes', IP6_INBCASTOCTETS, IP6_OUTBCASTOCTETS],
500                 ['Delivers', IP6_INDELIVERS, None],
501                 ['Forwarded', None, IP6_OUTFORWDATAGRAMS],
502                 ['No Routes', IP6_INNOROUTES, IP6_OUTNOROUTES],
503                 ['Header Errors', IP6_INHDRERRORS, None],
504                 ['Too Big Errors', IP6_INTOOBIGERRORS, None],
505                 ['Address Errors', IP6_INADDRERRORS, None],
506                 ['Unknown Protocol', IP6_INUNKNOWNPROTOS, None],
507                 ['Truncated Packets', IP6_INTRUNCATEDPKTS, None],
508                 ['Reasm Timeouts', IP6_REASMTIMEOUT, None],
509                 ['Reasm Requests', IP6_REASMREQDS, None],
510                 ['Reasm Failures', IP6_REASMFAILS, None],
511                 ['Reasm OK', IP6_REASMOKS, None],
512                 ['Frag Created', None, IP6_FRAGCREATES],
513                 ['Frag Failures', None, IP6_FRAGFAILS],
514                 ['Frag OK', None, IP6_FRAGOKS],
515                 ['', None, None],
516                 ['ICMPv6:', None, None],
517                 ['Messages', ICMP6_INMSGS, ICMP6_OUTMSGS],
518                 ['Errors', ICMP6_INERRORS, ICMP6_OUTERRORS]]
519
520            buf += '\n\t%s%s%s%s\n' % (33 * ' ', util.title('RX'),
521                           15 * ' ', util.title('TX'))
522
523            for row in l:
524                row[0] = util.kw(row[0])
525                row[1] = self.get_stat(row[1]) if row[1] else ''
526                row[2] = self.get_stat(row[2]) if row[2] else ''
527                buf += '\t{0[0]:27} {0[1]:>16} {0[2]:>16}\n'.format(row)
528
529            buf += self._foreach_af('stats')
530
531        return buf
532
533def get(name, sock=None):
534    """Lookup Link object directly from kernel"""
535    if not name:
536        raise ValueError()
537
538    if not sock:
539        sock = netlink.lookup_socket(netlink.NETLINK_ROUTE)
540
541    link = capi.get_from_kernel(sock._sock, 0, name)
542    if not link:
543        return None
544
545    return Link.from_capi(link)
546
547_link_cache = LinkCache()
548
549def resolve(name):
550    _link_cache.refill()
551    return _link_cache[name]
552