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