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