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