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