Added attribute MaxAddresses for interface factories to design
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 import time
5
6 from constants import TESTBED_ID
7 from nepi.core import metadata
8 from nepi.core.attributes import Attribute
9 from nepi.util import validation
10 from nepi.util.constants import ApplicationStatus as AS, \
11         FactoryCategories as FC, \
12         ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP
13
14 import functools
15 import os
16 import os.path
17
18 NODE = "Node"
19 NODEIFACE = "NodeInterface"
20 TUNIFACE = "TunInterface"
21 TAPIFACE = "TapInterface"
22 APPLICATION = "Application"
23 DEPENDENCY = "Dependency"
24 NEPIDEPENDENCY = "NepiDependency"
25 NS3DEPENDENCY = "NS3Dependency"
26 INTERNET = "Internet"
27 NETPIPE = "NetPipe"
28
29 PL_TESTBED_ID = "planetlab"
30
31
32 ### Custom validation functions ###
33 def is_addrlist(attribute, value):
34     if not validation.is_string(attribute, value):
35         return False
36     
37     if not value:
38         # No empty strings
39         return False
40     
41     components = value.split(',')
42     
43     for component in components:
44         if '/' in component:
45             addr, mask = component.split('/',1)
46         else:
47             addr, mask = component, '32'
48         
49         if mask is not None and not (mask and mask.isdigit()):
50             # No empty or nonnumeric masks
51             return False
52         
53         if not validation.is_ip4_address(attribute, addr):
54             # Address part must be ipv4
55             return False
56         
57     return True
58
59 def is_portlist(attribute, value):
60     if not validation.is_string(attribute, value):
61         return False
62     
63     if not value:
64         # No empty strings
65         return False
66     
67     components = value.split(',')
68     
69     for component in components:
70         if '-' in component:
71             pfrom, pto = component.split('-',1)
72         else:
73             pfrom = pto = component
74         
75         if not pfrom or not pto or not pfrom.isdigit() or not pto.isdigit():
76             # No empty or nonnumeric ports
77             return False
78         
79     return True
80
81
82 ### Connection functions ####
83
84 def connect_node_iface_node(testbed_instance, node_guid, iface_guid):
85     node = testbed_instance._elements[node_guid]
86     iface = testbed_instance._elements[iface_guid]
87     iface.node = node
88
89 def connect_node_iface_inet(testbed_instance, iface_guid, inet_guid):
90     iface = testbed_instance._elements[iface_guid]
91     iface.has_internet = True
92
93 def connect_tun_iface_node(testbed_instance, node_guid, iface_guid):
94     node = testbed_instance._elements[node_guid]
95     iface = testbed_instance._elements[iface_guid]
96     if not node.emulation:
97         raise RuntimeError, "Use of TUN interfaces requires emulation"
98     iface.node = node
99     node.required_vsys.update(('fd_tuntap', 'vif_up'))
100     node.required_packages.update(('python', 'python-crypto', 'python-setuptools', 'gcc'))
101
102 def connect_tun_iface_peer(proto, testbed_instance, iface_guid, peer_iface_guid):
103     iface = testbed_instance._elements[iface_guid]
104     peer_iface = testbed_instance._elements[peer_iface_guid]
105     iface.peer_iface = peer_iface
106     iface.peer_proto = \
107     iface.tun_proto = proto
108     iface.tun_key = peer_iface.tun_key
109
110 def crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data):
111     iface = testbed_instance._elements[iface_guid]
112     iface.peer_iface = None
113     iface.peer_addr = peer_iface_data.get("tun_addr")
114     iface.peer_proto = peer_iface_data.get("tun_proto") or proto
115     iface.peer_port = peer_iface_data.get("tun_port")
116     iface.tun_key = min(iface.tun_key, peer_iface_data.get("tun_key"))
117     iface.tun_proto = proto
118     
119     preconfigure_tuniface(testbed_instance, iface_guid)
120
121 def crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data):
122     # refresh (refreshable) attributes for second-phase
123     iface = testbed_instance._elements[iface_guid]
124     iface.peer_addr = peer_iface_data.get("tun_addr")
125     iface.peer_proto = peer_iface_data.get("tun_proto") or proto
126     iface.peer_port = peer_iface_data.get("tun_port")
127     
128     postconfigure_tuniface(testbed_instance, iface_guid)
129
130 def crossconnect_tun_iface_peer_both(proto, testbed_instance, iface_guid, peer_iface_data):
131     crossconnect_tun_iface_peer_init(proto, testbed_instance, iface_guid, peer_iface_data)
132     crossconnect_tun_iface_peer_compl(proto, testbed_instance, iface_guid, peer_iface_data)
133
134 def connect_dep(testbed_instance, node_guid, app_guid):
135     node = testbed_instance._elements[node_guid]
136     app = testbed_instance._elements[app_guid]
137     app.node = node
138     
139     if app.depends:
140         node.required_packages.update(set(
141             app.depends.split() ))
142     
143     if app.add_to_path:
144         if app.home_path and app.home_path not in node.pythonpath:
145             node.pythonpath.append(app.home_path)
146     
147     if app.env:
148         for envkey, envval in app.env.iteritems():
149             envval = app._replace_paths(envval)
150             node.env[envkey].append(envval)
151     
152     if app.rpmFusion:
153         node.rpmFusion = True
154
155 def connect_node_netpipe(testbed_instance, node_guid, netpipe_guid):
156     node = testbed_instance._elements[node_guid]
157     netpipe = testbed_instance._elements[netpipe_guid]
158     if not node.emulation:
159         raise RuntimeError, "Use of NetPipes requires emulation"
160     netpipe.node = node
161     
162
163 ### Creation functions ###
164
165 def create_node(testbed_instance, guid):
166     parameters = testbed_instance._get_parameters(guid)
167     
168     # create element with basic attributes
169     element = testbed_instance._make_node(parameters)
170     
171     # add constraint on number of (real) interfaces
172     # by counting connected devices
173     dev_guids = testbed_instance.get_connected(guid, "devs", "node")
174     num_open_ifaces = sum( # count True values
175         NODEIFACE == testbed_instance._get_factory_id(guid)
176         for guid in dev_guids )
177     element.min_num_external_ifaces = num_open_ifaces
178     
179     # require vroute vsys if we have routes to set up
180     routes = testbed_instance._add_route.get(guid)
181     if routes:
182         element.required_vsys.add("vroute")
183     
184     testbed_instance.elements[guid] = element
185
186 def create_nodeiface(testbed_instance, guid):
187     parameters = testbed_instance._get_parameters(guid)
188     element = testbed_instance._make_node_iface(parameters)
189     testbed_instance.elements[guid] = element
190
191 def create_tuniface(testbed_instance, guid):
192     parameters = testbed_instance._get_parameters(guid)
193     element = testbed_instance._make_tun_iface(parameters)
194     
195     # Set custom addresses, if there are any already
196     # Setting this early helps set up P2P links
197     if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
198         addresses = testbed_instance._add_address[guid]
199         for address in addresses:
200             (address, netprefix, broadcast) = address
201             element.add_address(address, netprefix, broadcast)
202     
203     testbed_instance.elements[guid] = element
204
205 def create_tapiface(testbed_instance, guid):
206     parameters = testbed_instance._get_parameters(guid)
207     element = testbed_instance._make_tap_iface(parameters)
208     
209     # Set custom addresses, if there are any already
210     # Setting this early helps set up P2P links
211     if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
212         addresses = testbed_instance._add_address[guid]
213         for address in addresses:
214             (address, netprefix, broadcast) = address
215             element.add_address(address, netprefix, broadcast)
216     
217     testbed_instance.elements[guid] = element
218
219 def create_application(testbed_instance, guid):
220     parameters = testbed_instance._get_parameters(guid)
221     element = testbed_instance._make_application(parameters)
222     
223     # Just inject configuration stuff
224     element.home_path = "nepi-app-%s" % (guid,)
225     
226     testbed_instance.elements[guid] = element
227
228 def create_dependency(testbed_instance, guid):
229     parameters = testbed_instance._get_parameters(guid)
230     element = testbed_instance._make_dependency(parameters)
231     
232     # Just inject configuration stuff
233     element.home_path = "nepi-dep-%s" % (guid,)
234     
235     testbed_instance.elements[guid] = element
236
237 def create_nepi_dependency(testbed_instance, guid):
238     parameters = testbed_instance._get_parameters(guid)
239     element = testbed_instance._make_nepi_dependency(parameters)
240     
241     # Just inject configuration stuff
242     element.home_path = "nepi-nepi-%s" % (guid,)
243     
244     testbed_instance.elements[guid] = element
245
246 def create_ns3_dependency(testbed_instance, guid):
247     parameters = testbed_instance._get_parameters(guid)
248     element = testbed_instance._make_ns3_dependency(parameters)
249     
250     # Just inject configuration stuff
251     element.home_path = "nepi-ns3-%s" % (guid,)
252     
253     testbed_instance.elements[guid] = element
254
255 def create_internet(testbed_instance, guid):
256     parameters = testbed_instance._get_parameters(guid)
257     element = testbed_instance._make_internet(parameters)
258     testbed_instance.elements[guid] = element
259
260 def create_netpipe(testbed_instance, guid):
261     parameters = testbed_instance._get_parameters(guid)
262     element = testbed_instance._make_netpipe(parameters)
263     testbed_instance.elements[guid] = element
264
265 ### Start/Stop functions ###
266
267 def start_application(testbed_instance, guid):
268     parameters = testbed_instance._get_parameters(guid)
269     traces = testbed_instance._get_traces(guid)
270     app = testbed_instance.elements[guid]
271     
272     app.stdout = "stdout" in traces
273     app.stderr = "stderr" in traces
274     app.buildlog = "buildlog" in traces
275     
276     app.start()
277
278 def stop_application(testbed_instance, guid):
279     app = testbed_instance.elements[guid]
280     app.kill()
281
282 ### Status functions ###
283
284 def status_application(testbed_instance, guid):
285     if guid not in testbed_instance.elements.keys():
286         return AS.STATUS_NOT_STARTED
287     
288     app = testbed_instance.elements[guid]
289     return app.status()
290
291 ### Configure functions ###
292
293 def configure_nodeiface(testbed_instance, guid):
294     element = testbed_instance._elements[guid]
295     
296     # Cannot explicitly configure addresses
297     if guid in testbed_instance._add_address:
298         raise ValueError, "Cannot explicitly set address of public PlanetLab interface"
299     
300     # Get siblings
301     node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
302     dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
303     siblings = [ self._element[dev_guid] 
304                  for dev_guid in dev_guids
305                  if dev_guid != guid ]
306     
307     # Fetch address from PLC api
308     element.pick_iface(siblings)
309     
310     # Do some validations
311     element.validate()
312
313 def preconfigure_tuniface(testbed_instance, guid):
314     element = testbed_instance._elements[guid]
315     
316     # Set custom addresses if any, and if not set already
317     if guid in testbed_instance._add_address and not (element.address or element.netmask or element.netprefix):
318         addresses = testbed_instance._add_address[guid]
319         for address in addresses:
320             (address, netprefix, broadcast) = address
321             element.add_address(address, netprefix, broadcast)
322     
323     # Link to external interface, if any
324     for iface in testbed_instance._elements.itervalues():
325         if isinstance(iface, testbed_instance._interfaces.NodeIface) and iface.node is element.node and iface.has_internet:
326             element.external_iface = iface
327             break
328
329     # Set standard TUN attributes
330     if (not element.tun_addr or not element.tun_port) and element.external_iface:
331         element.tun_addr = element.external_iface.address
332         element.tun_port = 15000 + int(guid)
333
334     # Set enabled traces
335     traces = testbed_instance._get_traces(guid)
336     element.capture = 'packets' in traces
337     
338     # Do some validations
339     element.validate()
340     
341     # First-phase setup
342     if element.peer_proto:
343         if element.peer_iface and isinstance(element.peer_iface, testbed_instance._interfaces.TunIface):
344             # intra tun
345             listening = id(element) < id(element.peer_iface)
346         else:
347             # cross tun
348             if not element.tun_addr or not element.tun_port:
349                 listening = True
350             elif not element.peer_addr or not element.peer_port:
351                 listening = True
352             else:
353                 # both have addresses...
354                 # ...the one with the lesser address listens
355                 listening = element.tun_addr < element.peer_addr
356         element.prepare( 
357             'tun-%s' % (guid,),
358              listening)
359
360 def postconfigure_tuniface(testbed_instance, guid):
361     element = testbed_instance._elements[guid]
362     
363     # Second-phase setup
364     element.setup()
365     
366 def wait_tuniface(testbed_instance, guid):
367     element = testbed_instance._elements[guid]
368     
369     # Second-phase setup
370     element.async_launch_wait()
371     
372
373 def configure_node(testbed_instance, guid):
374     node = testbed_instance._elements[guid]
375     
376     # Just inject configuration stuff
377     node.home_path = "nepi-node-%s" % (guid,)
378     node.ident_path = testbed_instance.sliceSSHKey
379     node.slicename = testbed_instance.slicename
380     
381     # Do some validations
382     node.validate()
383     
384     # this will be done in parallel in all nodes
385     # this call only spawns the process
386     node.install_dependencies()
387
388 def configure_node_routes(testbed_instance, guid):
389     node = testbed_instance._elements[guid]
390     routes = testbed_instance._add_route.get(guid)
391     
392     if routes:
393         devs = [ dev
394             for dev_guid in testbed_instance.get_connected(guid, "devs", "node")
395             for dev in ( testbed_instance._elements.get(dev_guid) ,)
396             if dev and isinstance(dev, testbed_instance._interfaces.TunIface) ]
397         
398         node.configure_routes(routes, devs)
399
400 def configure_application(testbed_instance, guid):
401     app = testbed_instance._elements[guid]
402     
403     # Do some validations
404     app.validate()
405     
406     # Wait for dependencies
407     app.node.wait_dependencies()
408     
409     # Install stuff
410     app.async_setup()
411
412 def configure_dependency(testbed_instance, guid):
413     dep = testbed_instance._elements[guid]
414     
415     # Do some validations
416     dep.validate()
417     
418     # Wait for dependencies
419     dep.node.wait_dependencies()
420     
421     # Install stuff
422     dep.async_setup()
423
424 def configure_netpipe(testbed_instance, guid):
425     netpipe = testbed_instance._elements[guid]
426     
427     # Do some validations
428     netpipe.validate()
429     
430     # Wait for dependencies
431     netpipe.node.wait_dependencies()
432     
433     # Install rules
434     netpipe.configure()
435
436 ### Factory information ###
437
438 connector_types = dict({
439     "apps": dict({
440                 "help": "Connector from node to applications", 
441                 "name": "apps",
442                 "max": -1, 
443                 "min": 0
444             }),
445     "devs": dict({
446                 "help": "Connector from node to network interfaces", 
447                 "name": "devs",
448                 "max": -1, 
449                 "min": 0
450             }),
451     "deps": dict({
452                 "help": "Connector from node to application dependencies "
453                         "(packages and applications that need to be installed)", 
454                 "name": "deps",
455                 "max": -1, 
456                 "min": 0
457             }),
458     "inet": dict({
459                 "help": "Connector from network interfaces to the internet", 
460                 "name": "inet",
461                 "max": 1, 
462                 "min": 1
463             }),
464     "node": dict({
465                 "help": "Connector to a Node", 
466                 "name": "node",
467                 "max": 1, 
468                 "min": 1
469             }),
470     "pipes": dict({
471                 "help": "Connector to a NetPipe", 
472                 "name": "pipes",
473                 "max": 2, 
474                 "min": 0
475             }),
476     
477     "tcp": dict({
478                 "help": "ip-ip tunneling over TCP link", 
479                 "name": "tcp",
480                 "max": 1, 
481                 "min": 0
482             }),
483     "udp": dict({
484                 "help": "ip-ip tunneling over UDP datagrams", 
485                 "name": "udp",
486                 "max": 1, 
487                 "min": 0
488             }),
489     "fd->": dict({
490                 "help": "TUN device file descriptor provider", 
491                 "name": "fd->",
492                 "max": 1, 
493                 "min": 0
494             }),
495    })
496
497 connections = [
498     dict({
499         "from": (TESTBED_ID, NODE, "devs"),
500         "to":   (TESTBED_ID, NODEIFACE, "node"),
501         "init_code": connect_node_iface_node,
502         "can_cross": False
503     }),
504     dict({
505         "from": (TESTBED_ID, NODE, "devs"),
506         "to":   (TESTBED_ID, TUNIFACE, "node"),
507         "init_code": connect_tun_iface_node,
508         "can_cross": False
509     }),
510     dict({
511         "from": (TESTBED_ID, NODE, "devs"),
512         "to":   (TESTBED_ID, TAPIFACE, "node"),
513         "init_code": connect_tun_iface_node,
514         "can_cross": False
515     }),
516     dict({
517         "from": (TESTBED_ID, NODEIFACE, "inet"),
518         "to":   (TESTBED_ID, INTERNET, "devs"),
519         "init_code": connect_node_iface_inet,
520         "can_cross": False
521     }),
522     dict({
523         "from": (TESTBED_ID, NODE, "apps"),
524         "to":   (TESTBED_ID, APPLICATION, "node"),
525         "init_code": connect_dep,
526         "can_cross": False
527     }),
528     dict({
529         "from": (TESTBED_ID, NODE, "deps"),
530         "to":   (TESTBED_ID, DEPENDENCY, "node"),
531         "init_code": connect_dep,
532         "can_cross": False
533     }),
534     dict({
535         "from": (TESTBED_ID, NODE, "deps"),
536         "to":   (TESTBED_ID, NEPIDEPENDENCY, "node"),
537         "init_code": connect_dep,
538         "can_cross": False
539     }),
540     dict({
541         "from": (TESTBED_ID, NODE, "deps"),
542         "to":   (TESTBED_ID, NS3DEPENDENCY, "node"),
543         "init_code": connect_dep,
544         "can_cross": False
545     }),
546     dict({
547         "from": (TESTBED_ID, NODE, "pipes"),
548         "to":   (TESTBED_ID, NETPIPE, "node"),
549         "init_code": connect_node_netpipe,
550         "can_cross": False
551     }),
552     dict({
553         "from": (TESTBED_ID, TUNIFACE, "tcp"),
554         "to":   (TESTBED_ID, TUNIFACE, "tcp"),
555         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
556         "can_cross": False
557     }),
558     dict({
559         "from": (TESTBED_ID, TUNIFACE, "udp"),
560         "to":   (TESTBED_ID, TUNIFACE, "udp"),
561         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
562         "can_cross": False
563     }),
564     dict({
565         "from": (TESTBED_ID, TAPIFACE, "tcp"),
566         "to":   (TESTBED_ID, TAPIFACE, "tcp"),
567         "init_code": functools.partial(connect_tun_iface_peer,"tcp"),
568         "can_cross": False
569     }),
570     dict({
571         "from": (TESTBED_ID, TAPIFACE, "udp"),
572         "to":   (TESTBED_ID, TAPIFACE, "udp"),
573         "init_code": functools.partial(connect_tun_iface_peer,"udp"),
574         "can_cross": False
575     }),
576     dict({
577         "from": (TESTBED_ID, TUNIFACE, "tcp"),
578         "to":   (None, None, "tcp"),
579         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
580         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
581         "can_cross": True
582     }),
583     dict({
584         "from": (TESTBED_ID, TUNIFACE, "udp"),
585         "to":   (None, None, "udp"),
586         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
587         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
588         "can_cross": True
589     }),
590     dict({
591         "from": (TESTBED_ID, TUNIFACE, "fd->"),
592         "to":   (None, None, "->fd"),
593         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
594         "can_cross": True
595     }),
596     dict({
597         "from": (TESTBED_ID, TAPIFACE, "tcp"),
598         "to":   (None, None, "tcp"),
599         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"tcp"),
600         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"tcp"),
601         "can_cross": True
602     }),
603     dict({
604         "from": (TESTBED_ID, TAPIFACE, "udp"),
605         "to":   (None, None, "udp"),
606         "init_code": functools.partial(crossconnect_tun_iface_peer_init,"udp"),
607         "compl_code": functools.partial(crossconnect_tun_iface_peer_compl,"udp"),
608         "can_cross": True
609     }),
610     dict({
611         "from": (TESTBED_ID, TAPIFACE, "fd->"),
612         "to":   (None, None, "->fd"),
613         "compl_code": functools.partial(crossconnect_tun_iface_peer_both,"fd"),
614         "can_cross": True
615     }),
616 ]
617
618 attributes = dict({
619     "forward_X11": dict({      
620                 "name": "forward_X11",
621                 "help": "Forward x11 from main namespace to the node",
622                 "type": Attribute.BOOL, 
623                 "value": False,
624                 "flags": Attribute.DesignOnly,
625                 "validation_function": validation.is_bool,
626             }),
627     "hostname": dict({      
628                 "name": "hostname",
629                 "help": "Constrain hostname during resource discovery. May use wildcards.",
630                 "type": Attribute.STRING, 
631                 "flags": Attribute.DesignOnly,
632                 "validation_function": validation.is_string,
633             }),
634     "architecture": dict({      
635                 "name": "architecture",
636                 "help": "Constrain architexture during resource discovery.",
637                 "type": Attribute.ENUM, 
638                 "flags": Attribute.DesignOnly,
639                 "allowed": ["x86_64",
640                             "i386"],
641                 "validation_function": validation.is_enum,
642             }),
643     "operating_system": dict({      
644                 "name": "operatingSystem",
645                 "help": "Constrain operating system during resource discovery.",
646                 "type": Attribute.ENUM, 
647                 "flags": Attribute.DesignOnly,
648                 "allowed": ["f8",
649                             "f12",
650                             "f14",
651                             "centos",
652                             "other"],
653                 "validation_function": validation.is_enum,
654             }),
655     "site": dict({      
656                 "name": "site",
657                 "help": "Constrain the PlanetLab site this node should reside on.",
658                 "type": Attribute.ENUM, 
659                 "flags": Attribute.DesignOnly,
660                 "allowed": ["PLE",
661                             "PLC",
662                             "PLJ"],
663                 "validation_function": validation.is_enum,
664             }),
665     "emulation": dict({      
666                 "name": "emulation",
667                 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
668                 "type": Attribute.BOOL,
669                 "value": False, 
670                 "flags": Attribute.DesignOnly,
671                 "validation_function": validation.is_bool,
672             }),
673     "min_reliability": dict({
674                 "name": "minReliability",
675                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
676                 "type": Attribute.DOUBLE,
677                 "range": (0,100),
678                 "flags": Attribute.DesignOnly,
679                 "validation_function": validation.is_double,
680             }),
681     "max_reliability": dict({
682                 "name": "maxReliability",
683                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
684                 "type": Attribute.DOUBLE,
685                 "range": (0,100),
686                 "flags": Attribute.DesignOnly,
687                 "validation_function": validation.is_double,
688             }),
689     "min_bandwidth": dict({
690                 "name": "minBandwidth",
691                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
692                 "type": Attribute.DOUBLE,
693                 "range": (0,2**31),
694                 "flags": Attribute.DesignOnly,
695                 "validation_function": validation.is_double,
696             }),
697     "max_bandwidth": dict({
698                 "name": "maxBandwidth",
699                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
700                 "type": Attribute.DOUBLE,
701                 "range": (0,2**31),
702                 "flags": Attribute.DesignOnly,
703                 "validation_function": validation.is_double,
704             }),
705             
706     "up": dict({
707                 "name": "up",
708                 "help": "Link up",
709                 "type": Attribute.BOOL,
710                 "value": False,
711                 "validation_function": validation.is_bool
712             }),
713     "primary": dict({
714                 "name": "primary",
715                 "help": "This is the primary interface for the attached node",
716                 "type": Attribute.BOOL,
717                 "value": True,
718                 "validation_function": validation.is_bool
719             }),
720     "device_name": dict({
721                 "name": "name",
722                 "help": "Device name",
723                 "type": Attribute.STRING,
724                 "flags": Attribute.DesignOnly,
725                 "validation_function": validation.is_string
726             }),
727     "mtu":  dict({
728                 "name": "mtu", 
729                 "help": "Maximum transmition unit for device",
730                 "type": Attribute.INTEGER,
731                 "range": (0,1500),
732                 "validation_function": validation.is_integer_range(0,1500)
733             }),
734     "mask":  dict({
735                 "name": "mask", 
736                 "help": "Network mask for the device (eg: 24 for /24 network)",
737                 "type": Attribute.INTEGER,
738                 "validation_function": validation.is_integer_range(8,24)
739             }),
740     "snat":  dict({
741                 "name": "snat", 
742                 "help": "Enable SNAT (source NAT to the internet) no this device",
743                 "type": Attribute.BOOL,
744                 "value": False,
745                 "validation_function": validation.is_bool
746             }),
747     "pointopoint":  dict({
748                 "name": "pointopoint", 
749                 "help": "If the interface is a P2P link, the remote endpoint's IP "
750                         "should be set on this attribute.",
751                 "type": Attribute.STRING,
752                 "flags": Attribute.DesignOnly,
753                 "validation_function": validation.is_string
754             }),
755     "txqueuelen":  dict({
756                 "name": "mask", 
757                 "help": "Transmission queue length (in packets)",
758                 "type": Attribute.INTEGER,
759                 "flags": Attribute.DesignOnly,
760                 "range" : (1,10000),
761                 "validation_function": validation.is_integer
762             }),
763             
764     "command": dict({
765                 "name": "command",
766                 "help": "Command line string",
767                 "type": Attribute.STRING,
768                 "flags": Attribute.DesignOnly,
769                 "validation_function": validation.is_string
770             }),
771     "sudo": dict({
772                 "name": "sudo",
773                 "help": "Run with root privileges",
774                 "type": Attribute.BOOL,
775                 "flags": Attribute.DesignOnly,
776                 "value": False,
777                 "validation_function": validation.is_bool
778             }),
779     "stdin": dict({
780                 "name": "stdin",
781                 "help": "Standard input",
782                 "type": Attribute.STRING,
783                 "flags": Attribute.DesignOnly,
784                 "validation_function": validation.is_string
785             }),
786             
787     "depends": dict({
788                 "name": "depends",
789                 "help": "Space-separated list of packages required to run the application",
790                 "type": Attribute.STRING,
791                 "flags": Attribute.DesignOnly,
792                 "validation_function": validation.is_string
793             }),
794     "build-depends": dict({
795                 "name": "buildDepends",
796                 "help": "Space-separated list of packages required to build the application",
797                 "type": Attribute.STRING,
798                 "flags": Attribute.DesignOnly,
799                 "validation_function": validation.is_string
800             }),
801     "rpm-fusion": dict({
802                 "name": "rpmFusion",
803                 "help": "True if required packages can be found in the RpmFusion repository",
804                 "type": Attribute.BOOL,
805                 "flags": Attribute.DesignOnly,
806                 "value": False,
807                 "validation_function": validation.is_bool
808             }),
809     "sources": dict({
810                 "name": "sources",
811                 "help": "Space-separated list of regular files to be deployed in the working path prior to building. "
812                         "Archives won't be expanded automatically.",
813                 "type": Attribute.STRING,
814                 "flags": Attribute.DesignOnly,
815                 "validation_function": validation.is_string
816             }),
817     "build": dict({
818                 "name": "build",
819                 "help": "Build commands to execute after deploying the sources. "
820                         "Sources will be in the ${SOURCES} folder. "
821                         "Example: tar xzf ${SOURCES}/my-app.tgz && cd my-app && ./configure && make && make clean.\n"
822                         "Try to make the commands return with a nonzero exit code on error.\n"
823                         "Also, do not install any programs here, use the 'install' attribute. This will "
824                         "help keep the built files constrained to the build folder (which may "
825                         "not be the home folder), and will result in faster deployment. Also, "
826                         "make sure to clean up temporary files, to reduce bandwidth usage between "
827                         "nodes when transferring built packages.",
828                 "type": Attribute.STRING,
829                 "flags": Attribute.DesignOnly,
830                 "validation_function": validation.is_string
831             }),
832     "install": dict({
833                 "name": "install",
834                 "help": "Commands to transfer built files to their final destinations. "
835                         "Sources will be in the initial working folder, and a special "
836                         "tag ${SOURCES} can be used to reference the experiment's "
837                         "home folder (where the application commands will run).\n"
838                         "ALL sources and targets needed for execution must be copied there, "
839                         "if building has been enabled.\n"
840                         "That is, 'slave' nodes will not automatically get any source files. "
841                         "'slave' nodes don't get build dependencies either, so if you need "
842                         "make and other tools to install, be sure to provide them as "
843                         "actual dependencies instead.",
844                 "type": Attribute.STRING,
845                 "flags": Attribute.DesignOnly,
846                 "validation_function": validation.is_string
847             }),
848     
849     "netpipe_mode": dict({      
850                 "name": "mode",
851                 "help": "Link mode:\n"
852                         " * SERVER: applies to incoming connections\n"
853                         " * CLIENT: applies to outgoing connections\n"
854                         " * SERVICE: applies to both",
855                 "type": Attribute.ENUM, 
856                 "flags": Attribute.DesignOnly,
857                 "allowed": ["SERVER",
858                             "CLIENT",
859                             "SERVICE"],
860                 "validation_function": validation.is_enum,
861             }),
862     "port_list":  dict({
863                 "name": "portList", 
864                 "help": "Port list or range. Eg: '22', '22,23,27', '20-2000'",
865                 "type": Attribute.STRING,
866                 "validation_function": is_portlist,
867             }),
868     "addr_list":  dict({
869                 "name": "addrList", 
870                 "help": "Address list or range. Eg: '127.0.0.1', '127.0.0.1,127.0.1.1', '127.0.0.1/8'",
871                 "type": Attribute.STRING,
872                 "validation_function": is_addrlist,
873             }),
874     "bw_in":  dict({
875                 "name": "bwIn", 
876                 "help": "Inbound bandwidth limit (in Mbit/s)",
877                 "type": Attribute.DOUBLE,
878                 "validation_function": validation.is_double,
879             }),
880     "bw_out":  dict({
881                 "name": "bwOut", 
882                 "help": "Outbound bandwidth limit (in Mbit/s)",
883                 "type": Attribute.DOUBLE,
884                 "validation_function": validation.is_double,
885             }),
886     "plr_in":  dict({
887                 "name": "plrIn", 
888                 "help": "Inbound packet loss rate (0 = no loss, 1 = 100% loss)",
889                 "type": Attribute.DOUBLE,
890                 "validation_function": validation.is_double,
891             }),
892     "plr_out":  dict({
893                 "name": "plrOut", 
894                 "help": "Outbound packet loss rate (0 = no loss, 1 = 100% loss)",
895                 "type": Attribute.DOUBLE,
896                 "validation_function": validation.is_double,
897             }),
898     "delay_in":  dict({
899                 "name": "delayIn", 
900                 "help": "Inbound packet delay (in milliseconds)",
901                 "type": Attribute.INTEGER,
902                 "range": (0,60000),
903                 "validation_function": validation.is_integer,
904             }),
905     "delay_out":  dict({
906                 "name": "delayOut", 
907                 "help": "Outbound packet delay (in milliseconds)",
908                 "type": Attribute.INTEGER,
909                 "range": (0,60000),
910                 "validation_function": validation.is_integer,
911             }),
912     })
913
914 traces = dict({
915     "stdout": dict({
916                 "name": "stdout",
917                 "help": "Standard output stream"
918               }),
919     "stderr": dict({
920                 "name": "stderr",
921                 "help": "Application standard error",
922               }),
923     "buildlog": dict({
924                 "name": "buildlog",
925                 "help": "Output of the build process",
926               }), 
927     
928     "netpipe_stats": dict({
929                 "name": "netpipeStats",
930                 "help": "Information about rule match counters, packets dropped, etc.",
931               }),
932
933     "packets": dict({
934                 "name": "packets",
935                 "help": "Detailled log of all packets going through the interface",
936               }),
937     })
938
939 create_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
940
941 configure_order = [ INTERNET, NODE, NODEIFACE, TAPIFACE, TUNIFACE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
942
943 # Start (and prestart) node after ifaces, because the node needs the ifaces in order to set up routes
944 start_order = [ INTERNET, NODEIFACE, TAPIFACE, TUNIFACE, NODE, NETPIPE, NEPIDEPENDENCY, NS3DEPENDENCY, DEPENDENCY, APPLICATION ]
945
946 factories_info = dict({
947     NODE: dict({
948             "allow_routes": True,
949             "help": "Virtualized Node (V-Server style)",
950             "category": FC.CATEGORY_NODES,
951             "create_function": create_node,
952             "preconfigure_function": configure_node,
953             "prestart_function": configure_node_routes,
954             "box_attributes": [
955                 "forward_X11",
956                 "hostname",
957                 "architecture",
958                 "operating_system",
959                 "site",
960                 "emulation",
961                 "min_reliability",
962                 "max_reliability",
963                 "min_bandwidth",
964                 "max_bandwidth",
965                 
966                 # NEPI-in-NEPI attributes
967                 ATTR_NEPI_TESTBED_ENVIRONMENT_SETUP,
968             ],
969             "connector_types": ["devs", "apps", "pipes", "deps"]
970        }),
971     NODEIFACE: dict({
972             "has_addresses": True,
973             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
974             "category": FC.CATEGORY_DEVICES,
975             "create_function": create_nodeiface,
976             "preconfigure_function": configure_nodeiface,
977             "box_attributes": [ ],
978             "connector_types": ["node", "inet"]
979         }),
980     TUNIFACE: dict({
981             "allow_addresses": True,
982             "help": "Virtual TUN network interface (layer 3)",
983             "category": FC.CATEGORY_DEVICES,
984             "create_function": create_tuniface,
985             "preconfigure_function": preconfigure_tuniface,
986             "configure_function": postconfigure_tuniface,
987             "prestart_function": wait_tuniface,
988             "box_attributes": [
989                 "up", "device_name", "mtu", "snat", "pointopoint",
990                 "txqueuelen",
991                 "tun_proto", "tun_addr", "tun_port", "tun_key"
992             ],
993             "traces": ["packets"],
994             "connector_types": ["node","udp","tcp","fd->"]
995         }),
996     TAPIFACE: dict({
997             "allow_addresses": True,
998             "help": "Virtual TAP network interface (layer 2)",
999             "category": FC.CATEGORY_DEVICES,
1000             "create_function": create_tapiface,
1001             "preconfigure_function": preconfigure_tuniface,
1002             "configure_function": postconfigure_tuniface,
1003             "prestart_function": wait_tuniface,
1004             "box_attributes": [
1005                 "up", "device_name", "mtu", "snat", "pointopoint",
1006                 "txqueuelen",
1007                 "tun_proto", "tun_addr", "tun_port", "tun_key"
1008             ],
1009             "traces": ["packets"],
1010             "connector_types": ["node","udp","tcp","fd->"]
1011         }),
1012     APPLICATION: dict({
1013             "help": "Generic executable command line application",
1014             "category": FC.CATEGORY_APPLICATIONS,
1015             "create_function": create_application,
1016             "start_function": start_application,
1017             "status_function": status_application,
1018             "stop_function": stop_application,
1019             "configure_function": configure_application,
1020             "box_attributes": ["command", "sudo", "stdin",
1021                                "depends", "build-depends", "build", "install",
1022                                "sources", "rpm-fusion" ],
1023             "connector_types": ["node"],
1024             "traces": ["stdout", "stderr", "buildlog"]
1025         }),
1026     DEPENDENCY: dict({
1027             "help": "Requirement for package or application to be installed on some node",
1028             "category": FC.CATEGORY_APPLICATIONS,
1029             "create_function": create_dependency,
1030             "preconfigure_function": configure_dependency,
1031             "box_attributes": ["depends", "build-depends", "build", "install",
1032                                "sources", "rpm-fusion" ],
1033             "connector_types": ["node"],
1034             "traces": ["buildlog"]
1035         }),
1036     NEPIDEPENDENCY: dict({
1037             "help": "Requirement for NEPI inside NEPI - required to run testbed instances inside a node",
1038             "category": FC.CATEGORY_APPLICATIONS,
1039             "create_function": create_nepi_dependency,
1040             "preconfigure_function": configure_dependency,
1041             "box_attributes": [ ],
1042             "connector_types": ["node"],
1043             "traces": ["buildlog"]
1044         }),
1045     NS3DEPENDENCY: dict({
1046             "help": "Requirement for NS3 inside NEPI - required to run NS3 testbed instances inside a node. It also needs NepiDependency.",
1047             "category": FC.CATEGORY_APPLICATIONS,
1048             "create_function": create_ns3_dependency,
1049             "preconfigure_function": configure_dependency,
1050             "box_attributes": [ ],
1051             "connector_types": ["node"],
1052             "traces": ["buildlog"]
1053         }),
1054     INTERNET: dict({
1055             "help": "Internet routing",
1056             "category": FC.CATEGORY_CHANNELS,
1057             "create_function": create_internet,
1058             "connector_types": ["devs"],
1059         }),
1060     NETPIPE: dict({
1061             "help": "Link emulation",
1062             "category": FC.CATEGORY_CHANNELS,
1063             "create_function": create_netpipe,
1064             "configure_function": configure_netpipe,
1065             "box_attributes": ["netpipe_mode",
1066                                "addr_list", "port_list",
1067                                "bw_in","plr_in","delay_in",
1068                                "bw_out","plr_out","delay_out"],
1069             "connector_types": ["node"],
1070             "traces": ["netpipe_stats"]
1071         }),
1072 })
1073
1074 testbed_attributes = dict({
1075         "slice": dict({
1076             "name": "slice",
1077             "help": "The name of the PlanetLab slice to use",
1078             "type": Attribute.STRING,
1079             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1080             "validation_function": validation.is_string
1081         }),
1082         "auth_user": dict({
1083             "name": "authUser",
1084             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
1085             "type": Attribute.STRING,
1086             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1087             "validation_function": validation.is_string
1088         }),
1089         "auth_pass": dict({
1090             "name": "authPass",
1091             "help": "The PlanetLab user's password.",
1092             "type": Attribute.STRING,
1093             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1094             "validation_function": validation.is_string
1095         }),
1096         "plc_host": dict({
1097             "name": "plcHost",
1098             "help": "The PlanetLab PLC API host",
1099             "type": Attribute.STRING,
1100             "value": "www.planet-lab.eu",
1101             "flags": Attribute.DesignOnly,
1102             "validation_function": validation.is_string
1103         }),
1104         "plc_url": dict({
1105             "name": "plcUrl",
1106             "help": "The PlanetLab PLC API url pattern - %(hostname)s is replaced by plcHost.",
1107             "type": Attribute.STRING,
1108             "value": "https://%(hostname)s:443/PLCAPI/",
1109             "flags": Attribute.DesignOnly,
1110             "validation_function": validation.is_string
1111         }),
1112         "slice_ssh_key": dict({
1113             "name": "sliceSSHKey",
1114             "help": "The controller-local path to the slice user's ssh private key. "
1115                     "It is the user's responsability to deploy this file where the controller "
1116                     "will run, it won't be done automatically because it's sensitive information. "
1117                     "It is recommended that a NEPI-specific user be created for this purpose and "
1118                     "this purpose alone.",
1119             "type": Attribute.STRING,
1120             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
1121             "validation_function": validation.is_string
1122         }),
1123     })
1124
1125 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
1126     @property
1127     def connector_types(self):
1128         return connector_types
1129
1130     @property
1131     def connections(self):
1132         return connections
1133
1134     @property
1135     def attributes(self):
1136         return attributes
1137
1138     @property
1139     def traces(self):
1140         return traces
1141
1142     @property
1143     def create_order(self):
1144         return create_order
1145
1146     @property
1147     def configure_order(self):
1148         return configure_order
1149
1150     @property
1151     def prestart_order(self):
1152         return start_order
1153
1154     @property
1155     def start_order(self):
1156         return start_order
1157
1158     @property
1159     def factories_info(self):
1160         return factories_info
1161
1162     @property
1163     def testbed_attributes(self):
1164         return testbed_attributes
1165