Initially working version of PlanetLab testbed implementation.
[nepi.git] / src / nepi / testbeds / planetlab / metadata_v01.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 from constants import TESTBED_ID
5 from nepi.core import metadata
6 from nepi.core.attributes import Attribute
7 from nepi.util import validation
8 from nepi.util.constants import STATUS_NOT_STARTED, STATUS_RUNNING, \
9         STATUS_FINISHED
10
11 NODE = "Node"
12 NODEIFACE = "NodeInterface"
13 TUNIFACE = "TunInterface"
14 APPLICATION = "Application"
15 INTERNET = "Internet"
16
17 PL_TESTBED_ID = "planetlab"
18
19 ### Connection functions ####
20
21 def connect_node_iface_node(testbed_instance, node, iface):
22     iface.node = node
23
24 def connect_node_iface_inet(testbed_instance, iface, inet):
25     iface.has_internet = True
26
27 def connect_tun_iface_node(testbed_instance, node, iface):
28     iface.node = node
29
30 def connect_app(testbed_instance, node, app):
31     app.node = node
32
33 ### Creation functions ###
34
35 def create_node(testbed_instance, guid):
36     parameters = testbed_instance._get_parameters(guid)
37     
38     # create element with basic attributes
39     element = testbed_instance._make_node(parameters)
40     
41     # add constraint on number of (real) interfaces
42     # by counting connected devices
43     dev_guids = testbed_instance.get_connected(guid, "node", "devs")
44     num_open_ifaces = sum( # count True values
45         TUNEIFACE == testbed_instance._get_factory_id(guid)
46         for guid in dev_guids )
47     element.min_num_external_ifaces = num_open_ifaces
48     
49     testbed_instance.elements[guid] = element
50
51 def create_nodeiface(testbed_instance, guid):
52     parameters = testbed_instance._get_parameters(guid)
53     element = testbed_instance._make_node_iface(parameters)
54     testbed_instance.elements[guid] = element
55
56 def create_tuniface(testbed_instance, guid):
57     parameters = testbed_instance._get_parameters(guid)
58     element = testbed_instance._make_tun_iface(parameters)
59     testbed_instance.elements[guid] = element
60
61 def create_application(testbed_instance, guid):
62     parameters = testbed_instance._get_parameters(guid)
63     element = testbed_instance._make_application(parameters)
64     testbed_instance.elements[guid] = element
65
66 def create_internet(testbed_instance, guid):
67     parameters = testbed_instance._get_parameters(guid)
68     element = testbed_instance._make_internet(parameters)
69     testbed_instance.elements[guid] = element
70
71 ### Start/Stop functions ###
72
73 def start_application(testbed_instance, guid):
74     parameters = testbed_instance._get_parameters(guid)
75     traces = testbed_instance._get_traces(guid)
76     app = testbed_instance.elements[guid]
77     
78     app.stdout = "stdout" in traces
79     app.stderr = "stderr" in traces
80     
81     app.start()
82
83 def stop_application(testbed_instance, guid):
84     app = testbed_instance.elements[guid]
85     app.kill()
86
87 ### Status functions ###
88
89 def status_application(testbed_instance, guid):
90     if guid not in testbed_instance.elements.keys():
91         return STATUS_NOT_STARTED
92     
93     app = testbed_instance.elements[guid]
94     return app.status()
95
96 ### Configure functions ###
97
98 def configure_nodeiface(testbed_instance, guid):
99     element = testbed_instance._elements[guid]
100     
101     # Cannot explicitly configure addresses
102     if guid in testbed_instance._add_address:
103         del testbed_instance._add_address[guid]
104     
105     # Get siblings
106     node_guid = testbed_instance.get_connected(guid, "node", "devs")[0]
107     dev_guids = testbed_instance.get_connected(node_guid, "node", "devs")
108     siblings = [ self._element[dev_guid] 
109                  for dev_guid in dev_guids
110                  if dev_guid != guid ]
111     
112     # Fetch address from PLC api
113     element.pick_iface(siblings)
114     
115     # Do some validations
116     element.validate()
117
118 def configure_tuniface(testbed_instance, guid):
119     element = testbed_instance._elements[guid]
120     if not guid in testbed_instance._add_address:
121         return
122     
123     addresses = testbed_instance._add_address[guid]
124     for address in addresses:
125         (address, netprefix, broadcast) = address
126         raise NotImplementedError, "C'mon... TUNs are hard..."
127     
128     # Do some validations
129     element.validate()
130
131 def configure_node(testbed_instance, guid):
132     node = testbed_instance._elements[guid]
133     
134     # If we have only one candidate, simply use it
135     candidates = node.find_candidates(
136         filter_slice_id = testbed_instance.slice_id)
137     if len(candidates) == 1:
138         node.assign_node_id(iter(candidates).next())
139     
140     # Do some validations
141     node.validate()
142
143 def configure_application(testbed_instance, guid):
144     app = testbed_instance._elements[guid]
145     
146     # Just inject configuration stuff
147     app.home_path = "nepi-app-%s" % (guid,)
148     app.ident_path = testbed_instance.sliceSSHKey
149     app.slicename = testbed_instance.slicename
150     
151     # Do some validations
152     app.validate()
153     
154     # Install stuff
155     app.setup()
156
157 ### Factory information ###
158
159 connector_types = dict({
160     "apps": dict({
161                 "help": "Connector from node to applications", 
162                 "name": "apps",
163                 "max": -1, 
164                 "min": 0
165             }),
166     "devs": dict({
167                 "help": "Connector from node to network interfaces", 
168                 "name": "devs",
169                 "max": -1, 
170                 "min": 0
171             }),
172     "inet": dict({
173                 "help": "Connector from network interfaces to the internet", 
174                 "name": "inet",
175                 "max": 1, 
176                 "min": 1
177             }),
178     "node": dict({
179                 "help": "Connector to a Node", 
180                 "name": "node",
181                 "max": 1, 
182                 "min": 1
183             }),
184    })
185
186 connections = [
187     dict({
188         "from": (TESTBED_ID, NODE, "devs"),
189         "to":   (TESTBED_ID, NODEIFACE, "node"),
190         "code": connect_node_iface_node,
191         "can_cross": False
192     }),
193     dict({
194         "from": (TESTBED_ID, NODE, "devs"),
195         "to":   (TESTBED_ID, TUNIFACE, "node"),
196         "code": connect_tun_iface_node,
197         "can_cross": False
198     }),
199     dict({
200         "from": (TESTBED_ID, NODEIFACE, "inet"),
201         "to":   (TESTBED_ID, INTERNET, "devs"),
202         "code": connect_node_iface_inet,
203         "can_cross": False
204     }),
205     dict({
206         "from": (TESTBED_ID, NODE, "apps"),
207         "to":   (TESTBED_ID, APPLICATION, "node"),
208         "code": connect_app,
209         "can_cross": False
210     })
211 ]
212
213 attributes = dict({
214     "forward_X11": dict({      
215                 "name": "forward_X11",
216                 "help": "Forward x11 from main namespace to the node",
217                 "type": Attribute.BOOL, 
218                 "value": False,
219                 "flags": Attribute.DesignOnly,
220                 "validation_function": validation.is_bool,
221             }),
222     "hostname": dict({      
223                 "name": "hostname",
224                 "help": "Constrain hostname during resource discovery. May use wildcards.",
225                 "type": Attribute.STRING, 
226                 "flags": Attribute.DesignOnly,
227                 "validation_function": validation.is_string,
228             }),
229     "architecture": dict({      
230                 "name": "architecture",
231                 "help": "Constrain architexture during resource discovery.",
232                 "type": Attribute.ENUM, 
233                 "flags": Attribute.DesignOnly,
234                 "allowed": ["x86_64",
235                             "i386"],
236                 "validation_function": validation.is_enum,
237             }),
238     "operating_system": dict({      
239                 "name": "operatingSystem",
240                 "help": "Constrain operating system during resource discovery.",
241                 "type": Attribute.ENUM, 
242                 "flags": Attribute.DesignOnly,
243                 "allowed": ["f8",
244                             "f12",
245                             "f14",
246                             "centos",
247                             "other"],
248                 "validation_function": validation.is_enum,
249             }),
250     "site": dict({      
251                 "name": "site",
252                 "help": "Constrain the PlanetLab site this node should reside on.",
253                 "type": Attribute.ENUM, 
254                 "flags": Attribute.DesignOnly,
255                 "allowed": ["PLE",
256                             "PLC",
257                             "PLJ"],
258                 "validation_function": validation.is_enum,
259             }),
260     "emulation": dict({      
261                 "name": "emulation",
262                 "help": "Enable emulation on this node. Enables NetfilterRoutes, bridges, and a host of other functionality.",
263                 "type": Attribute.BOOL,
264                 "value": False, 
265                 "flags": Attribute.DesignOnly,
266                 "validation_function": validation.is_bool,
267             }),
268     "min_reliability": dict({
269                 "name": "minReliability",
270                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies a lower acceptable bound.",
271                 "type": Attribute.DOUBLE,
272                 "range": (0,100),
273                 "flags": Attribute.DesignOnly,
274                 "validation_function": validation.is_double,
275             }),
276     "max_reliability": dict({
277                 "name": "maxReliability",
278                 "help": "Constrain reliability while picking PlanetLab nodes. Specifies an upper acceptable bound.",
279                 "type": Attribute.DOUBLE,
280                 "range": (0,100),
281                 "flags": Attribute.DesignOnly,
282                 "validation_function": validation.is_double,
283             }),
284     "min_bandwidth": dict({
285                 "name": "minBandwidth",
286                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies a lower acceptable bound.",
287                 "type": Attribute.DOUBLE,
288                 "range": (0,2**31),
289                 "flags": Attribute.DesignOnly,
290                 "validation_function": validation.is_double,
291             }),
292     "max_bandwidth": dict({
293                 "name": "maxBandwidth",
294                 "help": "Constrain available bandwidth while picking PlanetLab nodes. Specifies an upper acceptable bound.",
295                 "type": Attribute.DOUBLE,
296                 "range": (0,2**31),
297                 "flags": Attribute.DesignOnly,
298                 "validation_function": validation.is_double,
299             }),
300             
301     "up": dict({
302                 "name": "up",
303                 "help": "Link up",
304                 "type": Attribute.BOOL,
305                 "value": False,
306                 "validation_function": validation.is_bool
307             }),
308     "primary": dict({
309                 "name": "primary",
310                 "help": "This is the primary interface for the attached node",
311                 "type": Attribute.BOOL,
312                 "value": True,
313                 "validation_function": validation.is_bool
314             }),
315     "device_name": dict({
316                 "name": "name",
317                 "help": "Device name",
318                 "type": Attribute.STRING,
319                 "flags": Attribute.DesignOnly,
320                 "validation_function": validation.is_string
321             }),
322     "mtu":  dict({
323                 "name": "mtu", 
324                 "help": "Maximum transmition unit for device",
325                 "type": Attribute.INTEGER,
326                 "range": (0,1500),
327                 "validation_function": validation.is_integer_range(0,1500)
328             }),
329     "mask":  dict({
330                 "name": "mask", 
331                 "help": "Network mask for the device (eg: 24 for /24 network)",
332                 "type": Attribute.INTEGER,
333                 "validation_function": validation.is_integer_range(8,24)
334             }),
335     "snat":  dict({
336                 "name": "snat", 
337                 "help": "Enable SNAT (source NAT to the internet) no this device",
338                 "type": Attribute.BOOL,
339                 "value": False,
340                 "validation_function": validation.is_bool
341             }),
342             
343     "command": dict({
344                 "name": "command",
345                 "help": "Command line string",
346                 "type": Attribute.STRING,
347                 "flags": Attribute.DesignOnly,
348                 "validation_function": validation.is_string
349             }),
350     "sudo": dict({
351                 "name": "user",
352                 "help": "System user",
353                 "type": Attribute.BOOL,
354                 "flags": Attribute.DesignOnly,
355                 "value": False,
356                 "validation_function": validation.is_bool
357             }),
358     "stdin": dict({
359                 "name": "stdin",
360                 "help": "Standard input",
361                 "type": Attribute.STRING,
362                 "flags": Attribute.DesignOnly,
363                 "validation_function": validation.is_string
364             }),
365     })
366
367 traces = dict({
368     "stdout": dict({
369                 "name": "stdout",
370                 "help": "Standard output stream"
371               }),
372     "stderr": dict({
373                 "name": "stderr",
374                 "help": "Application standard error",
375               }) 
376     })
377
378 create_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, APPLICATION ]
379
380 configure_order = [ INTERNET, NODE, NODEIFACE, TUNIFACE, APPLICATION ]
381
382 factories_info = dict({
383     NODE: dict({
384             "allow_routes": False,
385             "help": "Virtualized Node (V-Server style)",
386             "category": "topology",
387             "create_function": create_node,
388             "preconfigure_function": configure_node,
389             "box_attributes": [
390                 "forward_X11",
391                 "hostname",
392                 "architecture",
393                 "operating_system",
394                 "site",
395                 "emulation",
396                 "min_reliability",
397                 "max_reliability",
398                 "min_bandwidth",
399                 "max_bandwidth",
400             ],
401             "connector_types": ["devs", "apps"]
402        }),
403     NODEIFACE: dict({
404             "allow_addresses": True,
405             "help": "External network interface - they cannot be brought up or down, and they MUST be connected to the internet.",
406             "category": "devices",
407             "create_function": create_nodeiface,
408             "preconfigure_function": configure_nodeiface,
409             "box_attributes": [ ],
410             "connector_types": ["node", "inet"]
411         }),
412     TUNIFACE: dict({
413             "allow_addresses": True,
414             "help": "Virtual TUN network interface",
415             "category": "devices",
416             "create_function": create_tuniface,
417             "preconfigure_function": configure_tuniface,
418             "box_attributes": [
419                 "up", "device_name", "mtu", "snat",
420             ],
421             "connector_types": ["node"]
422         }),
423     APPLICATION: dict({
424             "help": "Generic executable command line application",
425             "category": "applications",
426             "create_function": create_application,
427             "start_function": start_application,
428             "status_function": status_application,
429             "stop_function": stop_application,
430             "configure_function": configure_application,
431             "box_attributes": ["command", "sudo", "stdin"],
432             "connector_types": ["node"],
433             "traces": ["stdout", "stderr"]
434         }),
435     INTERNET: dict({
436             "help": "Internet routing",
437             "category": "topology",
438             "create_function": create_internet,
439             "connector_types": ["devs"],
440         }),
441 })
442
443 testbed_attributes = dict({
444         "slice": dict({
445             "name": "slice",
446             "help": "The name of the PlanetLab slice to use",
447             "type": Attribute.STRING,
448             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
449             "validation_function": validation.is_string
450         }),
451         "auth_user": dict({
452             "name": "authUser",
453             "help": "The name of the PlanetLab user to use for API calls - it must have at least a User role.",
454             "type": Attribute.STRING,
455             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
456             "validation_function": validation.is_string
457         }),
458         "auth_pass": dict({
459             "name": "authPass",
460             "help": "The PlanetLab user's password.",
461             "type": Attribute.STRING,
462             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
463             "validation_function": validation.is_string
464         }),
465         "slice_ssh_key": dict({
466             "name": "sliceSSHKey",
467             "help": "The controller-local path to the slice user's ssh private key. "
468                     "It is the user's responsability to deploy this file where the controller "
469                     "will run, it won't be done automatically because it's sensitive information. "
470                     "It is recommended that a NEPI-specific user be created for this purpose and "
471                     "this purpose alone.",
472             "type": Attribute.STRING,
473             "flags": Attribute.DesignOnly | Attribute.HasNoDefaultValue,
474             "validation_function": validation.is_string
475         }),
476     })
477
478 class VersionedMetadataInfo(metadata.VersionedMetadataInfo):
479     @property
480     def connector_types(self):
481         return connector_types
482
483     @property
484     def connections(self):
485         return connections
486
487     @property
488     def attributes(self):
489         return attributes
490
491     @property
492     def traces(self):
493         return traces
494
495     @property
496     def create_order(self):
497         return create_order
498
499     @property
500     def configure_order(self):
501         return configure_order
502
503     @property
504     def factories_info(self):
505         return factories_info
506
507     @property
508     def testbed_attributes(self):
509         return testbed_attributes
510