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