Fix TUN<->TunChannel connection in netns: must set TunChan in non-ethernet mode
[nepi.git] / src / nepi / testbeds / netns / metadata.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID, TESTBED_VERSION
5 from nepi.core import metadata
6 from nepi.core.attributes import Attribute
7 from nepi.util import tags, validation
8 from nepi.util.constants import ApplicationStatus as AS, \
9         FactoryCategories as FC, DeploymentConfiguration as DC
10
11 from nepi.util.tunchannel_impl import \
12     preconfigure_tunchannel, postconfigure_tunchannel, \
13     prestart_tunchannel, create_tunchannel, \
14     crossconnect_tunchannel_peer_init, \
15     crossconnect_tunchannel_peer_compl
16
17 import functools
18
19 # Factories
20 NODE = "Node"
21 P2PIFACE = "P2PNodeInterface"
22 TAPIFACE = "TapNodeInterface"
23 TUNIFACE = "TunNodeInterface"
24 NODEIFACE = "NodeInterface"
25 SWITCH = "Switch"
26 APPLICATION = "Application"
27 TUNCHANNEL = "TunChannel"
28
29 NS3_TESTBED_ID = "ns3"
30 FDNETDEV = "ns3::FdNetDevice"
31
32 def _follow_trace(testbed_instance, guid, trace_id, filename):
33     filepath = testbed_instance.trace_filepath(guid, trace_id, filename)
34     trace = open(filepath, "wb")
35     testbed_instance.follow_trace(guid, trace_id, trace, filename)
36     return trace
37
38 ### Connection functions ####
39
40 def connect_switch(testbed_instance, switch_guid, interface_guid):
41     switch = testbed_instance._elements[switch_guid]
42     interface = testbed_instance._elements[interface_guid]
43     switch.connect(interface)
44    
45 def connect_fd(testbed_instance, tap_guid, cross_data):
46     import passfd
47     import socket
48     tap = testbed_instance._elements[tap_guid]
49     address = cross_data["tun_addr"]
50     sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
51     sock.connect(address)
52     passfd.sendfd(sock, tap.fd, '0')
53     # TODO: after succesful transfer, the tap device should close the fd
54
55 def connect_tunchannel_tun(testbed_instance, chan_guid, tap_guid):
56     connect_tunchannel_tap(testbed_instance, chan_guid, tap_guid, ethernet_mode=False)
57
58 def connect_tunchannel_tap(testbed_instance, chan_guid, tap_guid, ethernet_mode=True):
59     tap = testbed_instance._elements[tap_guid]
60     chan = testbed_instance._elements[chan_guid]
61
62     # Create a file object for the tap's interface device 
63     # and send it to the channel. It should comply with all the
64     # requirements for the channel's tun_socket.
65     import os
66     chan.tun_socket = os.fdopen(tap.fd)
67     
68     # Set the channel to ethernet mode (it's a tap)
69     chan.ethernet_mode = ethernet_mode
70     
71     # Check to see if the device uses PI headers
72     # It's normally so
73     with_pi = True
74     try:
75         import fcntl
76         import struct
77         TUNGETIFF = 0x800454d2
78         IFF_NO_PI = 0x00001000
79         struct_ifreq = "x"*16+"H"+"x"*22
80         flags = struct.unpack(struct_ifreq,
81             fcntl.ioctl(tap.fd, TUNGETIFF, struct.pack(struct_ifreq,0)) )[0]
82         with_pi = (0 == (flags & IFF_NO_PI))
83     except:
84         # maybe the kernel doesn't support the IOCTL,
85         # in which case, we assume it uses PI headers (as is usual)
86         pass
87     chan.with_pi = with_pi
88
89 ### Trace functions ###
90
91 def nodepcap_trace(testbed_instance, guid, trace_id):
92     node = testbed_instance._elements[guid]
93     parameters = testbed_instance._get_parameters(guid)
94     filename = "%d-pcap.stdout" % guid
95     stdout = _follow_trace(testbed_instance, guid, "pcap_stdout", filename)
96     filename = "%d-pcap.stderr" % guid
97     stderr = _follow_trace(testbed_instance, guid, "pcap_stderr", filename)
98     filename = "%d-node.pcap" % guid
99     filepath = testbed_instance.trace_filepath(guid, trace_id, filename)
100     command = "tcpdump -i 'any' -w %s" % filepath
101     user = "root"
102     trace = node.Popen(command, shell = True, stdout = stdout, 
103             stderr = stderr, user = user)
104     testbed_instance.follow_trace(guid, trace_id, trace, filename)
105
106 trace_functions = dict({
107     "pcap": nodepcap_trace,
108     })
109
110 ### Creation functions ###
111
112 def create_node(testbed_instance, guid):
113     parameters = testbed_instance._get_parameters(guid)
114     forward_X11 = False
115     if "forward_X11" in parameters:
116         forward_X11 = parameters["forward_X11"]
117         del parameters["forward_X11"]
118     element = testbed_instance.netns.Node(forward_X11 = forward_X11)
119     testbed_instance.elements[guid] = element
120
121 def create_p2piface(testbed_instance, guid):
122     if guid in testbed_instance.elements:
123         # The interface pair was already instantiated
124         return
125     # search for the node asociated with the p2piface
126     node1_guid = testbed_instance.get_connected(guid, "node", "devs")
127     if len(node1_guid) == 0:
128         raise RuntimeError("Can't instantiate interface %d outside netns \
129                 node" % guid)
130     node1 = testbed_instance.elements[node1_guid[0]]
131     # search for the pair p2piface
132     p2p_guid = testbed_instance.get_connected(guid, "p2p","p2p")
133     if len(p2p_guid) == 0:
134         raise RuntimeError("Can't instantiate p2p interface %d. \
135                 Missing interface pair" % guid)
136     guid2 = p2p_guid[0]
137     node2_guid = testbed_instance.get_connected(guid2, "node", "devs")
138     if len(node2_guid) == 0:
139         raise RuntimeError("Can't instantiate interface %d outside netns \
140                 node" % guid2)
141     node2 = testbed_instance.elements[node2_guid[0]]
142     element1, element2 = testbed_instance.netns.P2PInterface.create_pair(
143         node1, node2)
144     testbed_instance.elements[guid] = element1
145     testbed_instance.elements[guid2] = element2
146
147 def create_tapiface(testbed_instance, guid):
148     node_guid = testbed_instance.get_connected(guid, "node", "devs")
149     if len(node_guid) == 0:
150         raise RuntimeError("Can't instantiate interface %d outside netns \
151                 node" % guid)
152     node = testbed_instance.elements[node_guid[0]]
153     element = node.add_tap()
154     testbed_instance.elements[guid] = element
155
156 def create_tuniface(testbed_instance, guid):
157     node_guid = testbed_instance.get_connected(guid, "node", "devs")
158     if len(node_guid) == 0:
159         raise RuntimeError("Can't instantiate interface %d outside netns \
160                 node" % guid)
161     node = testbed_instance.elements[node_guid[0]]
162     element = node.add_tun()
163     testbed_instance.elements[guid] = element
164
165 def create_nodeiface(testbed_instance, guid):
166     node_guid = testbed_instance.get_connected(guid, "node", "devs")
167     if len(node_guid) == 0:
168         raise RuntimeError("Can't instantiate interface %d outside netns \
169                 node" % guid)
170     node = testbed_instance.elements[node_guid[0]]
171     element = node.add_if()
172     testbed_instance.elements[guid] = element
173
174 def create_switch(testbed_instance, guid):
175     element = testbed_instance.netns.Switch()
176     testbed_instance.elements[guid] = element
177
178 def create_application(testbed_instance, guid):
179     testbed_instance.elements[guid] = None # Delayed construction 
180
181 ### Start/Stop functions ###
182
183 def start_application(testbed_instance, guid):
184     parameters = testbed_instance._get_parameters(guid)
185     traces = testbed_instance._get_traces(guid)
186     command = parameters["command"]
187     user = None
188     if "user" in parameters:
189         user = parameters["user"]
190     stdout = stderr = None
191     if "stdout" in traces:
192         filename = "%d-stdout.trace" % guid
193         stdout = _follow_trace(testbed_instance, guid, "stdout", filename)
194     if "stderr" in traces:
195         filename = "%d-stderr.trace" % guid
196         stderr = _follow_trace(testbed_instance, guid, "stderr", filename)
197     node_guid = testbed_instance.get_connected(guid, "node", "apps")
198     if len(node_guid) == 0:
199         raise RuntimeError("Can't instantiate interface %d outside netns \
200                 node" % guid)
201     node = testbed_instance.elements[node_guid[0]]
202     element  = node.Popen(command, shell = True, stdout = stdout, 
203             stderr = stderr, user = user)
204     testbed_instance.elements[guid] = element
205
206 def stop_application(testbed_instance, guid):
207     #app = testbed_instance.elements[guid]
208     #app.signal()
209     pass
210
211 ### Status functions ###
212
213 def status_application(testbed_instance, guid):
214     if guid not in testbed_instance.elements.keys():
215         return AS.STATUS_NOT_STARTED
216     app = testbed_instance.elements[guid]
217     if app.poll() == None:
218         return AS.STATUS_RUNNING
219     return AS.STATUS_FINISHED
220
221 ### Configure functions ###
222
223 def configure_traces(testbed_instance, guid):
224     traces = testbed_instance._get_traces(guid)
225     for trace_id in traces:
226         if trace_id not in trace_functions:
227             continue
228         trace_func = trace_functions[trace_id]
229         trace_func(testbed_instance, guid, trace_id)
230
231 def configure_device(testbed_instance, guid):
232     configure_traces(testbed_instance, guid)
233     element = testbed_instance._elements[guid]
234     if not guid in testbed_instance._add_address:
235         return
236     addresses = testbed_instance._add_address[guid]
237     for address in addresses:
238         (address, netprefix, broadcast) = address
239         # TODO: Decide if we should add a ipv4 or ipv6 address
240         element.add_v4_address(address, netprefix)
241
242 def configure_node(testbed_instance, guid):
243     configure_traces(testbed_instance, guid)
244     element = testbed_instance._elements[guid]
245     if not guid in testbed_instance._add_route:
246         return
247     routes = testbed_instance._add_route[guid]
248     for route in routes:
249         (destination, netprefix, nexthop, metric) = route
250         element.add_route(prefix = destination, prefix_len = netprefix,
251             nexthop = nexthop, metric = metric)
252
253 ### Factory information ###
254
255 connector_types = dict({
256     "apps": dict({
257                 "help": "Connector from node to applications", 
258                 "name": "apps",
259                 "max": -1, 
260                 "min": 0
261             }),
262     "devs": dict({
263                 "help": "Connector from node to network interfaces", 
264                 "name": "devs",
265                 "max": -1, 
266                 "min": 0
267             }),
268     "node": dict({
269                 "help": "Connector to a Node", 
270                 "name": "node",
271                 "max": 1, 
272                 "min": 1
273             }),
274     "p2p": dict({
275                 "help": "Connector to a P2PInterface", 
276                 "name": "p2p",
277                 "max": 1, 
278                 "min": 0
279             }),
280     "->fd": dict({
281                 "help": "File descriptor receptor for devices with file descriptors",
282                 "name": "->fd",
283                 "max": 1,
284                 "min": 0
285             }),
286     "fd->": dict({
287                 "help": "File descriptor provider for devices with file descriptors",
288                 "name": "fd->",
289                 "max": 1,
290                 "min": 0
291             }),
292     "switch": dict({
293                 "help": "Connector to a switch", 
294                 "name": "switch",
295                 "max": 1, 
296                 "min": 0
297             }),
298     "tcp": dict({
299                 "help": "ip-ip tunneling over TCP link", 
300                 "name": "tcp",
301                 "max": 1, 
302                 "min": 0
303             }),
304     "udp": dict({
305                 "help": "ip-ip tunneling over UDP datagrams", 
306                 "name": "udp",
307                 "max": 1, 
308                 "min": 0
309             }),
310    })
311
312 connections = [
313     dict({
314         "from": (TESTBED_ID, NODE, "devs"),
315         "to":   (TESTBED_ID, P2PIFACE, "node"),
316         "can_cross": False
317     }),
318     dict({
319         "from": (TESTBED_ID, NODE, "devs"),
320         "to":   (TESTBED_ID, TAPIFACE, "node"),
321         "can_cross": False
322     }),
323     dict({
324         "from": (TESTBED_ID, NODE, "devs"),
325         "to":   (TESTBED_ID, TUNIFACE, "node"),
326         "can_cross": False
327     }),
328     dict({
329         "from": (TESTBED_ID, NODE, "devs"),
330         "to":   (TESTBED_ID, NODEIFACE, "node"),
331         "can_cross": False
332     }),
333     dict({
334         "from": (TESTBED_ID, P2PIFACE, "p2p"),
335         "to":   (TESTBED_ID, P2PIFACE, "p2p"),
336         "can_cross": False
337     }),
338     dict({
339         "from": (TESTBED_ID, TAPIFACE, "fd->"),
340         "to":   (None, None, "->fd"),
341         "compl_code": connect_fd,
342         "can_cross": True
343     }),
344     dict({
345         "from": (TESTBED_ID, TUNIFACE, "fd->"),
346         "to":   (None, None, "->fd"),
347         "compl_code": connect_fd,
348         "can_cross": True
349     }),
350      dict({
351         "from": (TESTBED_ID, SWITCH, "devs"),
352         "to":   (TESTBED_ID, NODEIFACE, "switch"),
353         "init_code": connect_switch,
354         "can_cross": False
355     }),
356     dict({
357         "from": (TESTBED_ID, NODE, "apps"),
358         "to":   (TESTBED_ID, APPLICATION, "node"),
359         "can_cross": False
360     }),
361     dict({
362         "from": (TESTBED_ID, TUNCHANNEL, "->fd" ),
363         "to":   (TESTBED_ID, TAPIFACE, "fd->" ),
364         "init_code": connect_tunchannel_tap,
365         "can_cross": False
366     }),
367     dict({
368         "from": (TESTBED_ID, TUNCHANNEL, "->fd" ),
369         "to":   (TESTBED_ID, TUNIFACE, "fd->" ),
370         "init_code": connect_tunchannel_tun,
371         "can_cross": False
372     }),
373     dict({
374         "from": (TESTBED_ID, TUNCHANNEL, "tcp"),
375         "to":   (None, None, "tcp"),
376         "init_code": functools.partial(crossconnect_tunchannel_peer_init,"tcp"),
377         "compl_code": functools.partial(crossconnect_tunchannel_peer_compl,"tcp"),
378         "can_cross": True
379     }),
380     dict({
381         "from": (TESTBED_ID, TUNCHANNEL, "udp"),
382         "to":   (None, None, "udp"),
383         "init_code": functools.partial(crossconnect_tunchannel_peer_init,"udp"),
384         "compl_code": functools.partial(crossconnect_tunchannel_peer_compl,"udp"),
385         "can_cross": True
386     }),
387 ]
388
389 attributes = dict({
390     "forward_X11": dict({      
391                 "name": "forward_X11",
392                 "help": "Forward x11 from main namespace to the node",
393                 "type": Attribute.BOOL, 
394                 "value": False,
395                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
396                 "validation_function": validation.is_bool
397             }),
398     "lladdr": dict({      
399                 "name": "lladdr", 
400                 "help": "Mac address", 
401                 "type": Attribute.STRING,
402                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
403                 "validation_function": validation.is_mac_address
404             }),
405     "up": dict({
406                 "name": "up",
407                 "help": "Link up",
408                 "type": Attribute.BOOL,
409                 "value": False,
410                 "validation_function": validation.is_bool
411             }),
412     "device_name": dict({
413                 "name": "name",
414                 "help": "Device name",
415                 "type": Attribute.STRING,
416                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
417                 "validation_function": validation.is_string
418             }),
419     "mtu":  dict({
420                 "name": "mtu", 
421                 "help": "Maximum transmition unit for device",
422                 "type": Attribute.INTEGER,
423                 "validation_function": validation.is_integer
424             }),
425     "broadcast": dict({ 
426                 "name": "broadcast",
427                 "help": "Broadcast address",
428                 "type": Attribute.STRING,
429                 "validation_function": validation.is_string # TODO: should be is address!
430             }),
431     "multicast": dict({      
432                 "name": "multicast",
433                 "help": "Multicast enabled",
434                 "type": Attribute.BOOL,
435                 "value": False,
436                 "validation_function": validation.is_bool
437             }),
438     "arp": dict({
439                 "name": "arp",
440                 "help": "ARP enabled",
441                 "type": Attribute.BOOL,
442                 "value": False,
443                 "validation_function": validation.is_bool
444             }),
445     "command": dict({
446                 "name": "command",
447                 "help": "Command line string",
448                 "type": Attribute.STRING,
449                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
450                 "validation_function": validation.is_string
451             }),
452     "user": dict({
453                 "name": "user",
454                 "help": "System user",
455                 "type": Attribute.STRING,
456                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
457                 "validation_function": validation.is_string
458             }),
459     "stdin": dict({
460                 "name": "stdin",
461                 "help": "Standard input",
462                 "type": Attribute.STRING,
463                 "flags": Attribute.ExecReadOnly | Attribute.ExecImmutable,
464                 "validation_function": validation.is_string
465             }),
466     })
467
468 traces = dict({
469     "stdout": dict({
470                 "name": "stdout",
471                 "help": "Standard output stream"
472               }),
473     "stderr": dict({
474                 "name": "stderr",
475                 "help": "Application standard error",
476         }),
477     "node_pcap": dict({
478                 "name": "pcap",
479                 "help": "tcpdump at all node interfaces",
480         }) 
481     })
482
483 create_order = [ NODE, P2PIFACE, NODEIFACE, TAPIFACE, 
484         TUNIFACE, TUNCHANNEL, SWITCH,
485         APPLICATION ]
486
487 configure_order = [ P2PIFACE, NODEIFACE, TAPIFACE, 
488         TUNIFACE, TUNCHANNEL, SWITCH, 
489         NODE, APPLICATION ]
490
491 factories_info = dict({
492     NODE: dict({
493             "help": "Emulated Node with virtualized network stack",
494             "category": FC.CATEGORY_NODES,
495             "create_function": create_node,
496             "configure_function": configure_node,
497             "box_attributes": ["forward_X11"],
498             "connector_types": ["devs", "apps"],
499             "traces": ["node_pcap"],
500             "tags": [tags.NODE, tags.ALLOW_ROUTES],
501        }),
502     P2PIFACE: dict({
503             "help": "Point to point network interface",
504             "category": FC.CATEGORY_DEVICES,
505             "create_function": create_p2piface,
506             "configure_function": configure_device,
507             "box_attributes": ["lladdr", "up", "device_name", "mtu", 
508                 "multicast", "broadcast", "arp"],
509             "connector_types": ["node", "p2p"],
510             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
511        }),
512     TAPIFACE: dict({
513             "help": "Tap device network interface",
514             "category": FC.CATEGORY_DEVICES,
515             "create_function": create_tapiface,
516             "configure_function": configure_device,
517             "box_attributes": ["lladdr", "up", "device_name", "mtu", 
518                 "multicast", "broadcast", "arp"],
519             "connector_types": ["node", "fd->"],
520             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
521         }),
522     TUNIFACE: dict({
523             "help": "Tun device network interface",
524             "category": FC.CATEGORY_DEVICES,
525             "create_function": create_tuniface,
526             "configure_function": configure_device,
527             "box_attributes": ["lladdr", "up", "device_name", "mtu", 
528                 "multicast", "broadcast", "arp"],
529             "connector_types": ["node", "fd->"],
530             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
531         }),
532     NODEIFACE: dict({
533             "help": "Node network interface",
534             "category": FC.CATEGORY_DEVICES,
535             "create_function": create_nodeiface,
536             "configure_function": configure_device,
537             "box_attributes": ["lladdr", "up", "device_name", "mtu", 
538                 "multicast", "broadcast", "arp"],
539             "connector_types": ["node", "switch"],
540             "tags": [tags.INTERFACE, tags.ALLOW_ADDRESSES],
541         }),
542     SWITCH: dict({
543             "display_name": "Switch",
544             "help": "Switch interface",
545             "category": FC.CATEGORY_DEVICES,
546             "create_function": create_switch,
547             "box_attributes": ["up", "device_name", "mtu", "multicast"],
548              #TODO: Add attribute ("Stp", help, type, value, range, allowed, readonly, validation_function),
549              #TODO: Add attribute ("ForwarddDelay", help, type, value, range, allowed, readonly, validation_function),
550              #TODO: Add attribute ("HelloTime", help, type, value, range, allowed, readonly, validation_function),
551              #TODO: Add attribute ("AgeingTime", help, type, value, range, allowed, readonly, validation_function),
552              #TODO: Add attribute ("MaxAge", help, type, value, range, allowed, readonly, validation_function)
553             "connector_types": ["devs"],
554             "tags": [tags.SWITCH],
555         }),
556     APPLICATION: dict({
557             "help": "Generic executable command line application",
558             "category": FC.CATEGORY_APPLICATIONS,
559             "create_function": create_application,
560             "start_function": start_application,
561             "stop_function": stop_application,
562             "status_function": status_application,
563             "box_attributes": ["command", "user"],
564             "connector_types": ["node"],
565             "traces": ["stdout", "stderr"],
566             "tags": [tags.APPLICATION],
567         }),
568      TUNCHANNEL : dict({
569             "category": FC.CATEGORY_TUNNELS,
570             "create_function": create_tunchannel,
571             "preconfigure_function": preconfigure_tunchannel,
572             "configure_function": postconfigure_tunchannel,
573             "prestart_function": prestart_tunchannel,
574             "help": "Channel to forward "+TAPIFACE+" data to "
575                 "other TAP interfaces supporting the NEPI tunneling protocol.",
576             "connector_types": ["->fd", "udp", "tcp"],
577             "allow_addresses": False,
578             "box_attributes": ["tun_proto", "tun_addr", "tun_port", "tun_key", "tun_cipher"],
579             "tags": [tags.TUNNEL],
580     }),
581 })
582
583 testbed_attributes = dict({
584         "enable_debug": dict({
585                 "name": "enableDebug",
586                 "help": "Enable netns debug output",
587                 "type": Attribute.BOOL,
588                 "value": False,
589                 "validation_function": validation.is_bool
590             }),
591     })
592
593 supported_recovery_policies = [
594         DC.POLICY_FAIL,
595     ]
596
597 class MetadataInfo(metadata.MetadataInfo):
598     @property
599     def connector_types(self):
600         return connector_types
601
602     @property
603     def connections(self):
604         return connections
605
606     @property
607     def attributes(self):
608         return attributes
609
610     @property
611     def traces(self):
612         return traces
613
614     @property
615     def create_order(self):
616         return create_order
617
618     @property
619     def configure_order(self):
620         return configure_order
621
622     @property
623     def factories_info(self):
624         return factories_info
625
626     @property
627     def testbed_attributes(self):
628         return testbed_attributes
629
630     @property
631     def testbed_id(self):
632         return TESTBED_ID
633
634     @property
635     def testbed_version(self):
636         return TESTBED_VERSION
637     
638     @property
639     def supported_recover_policies(self):
640         return supported_recovery_policies
641