Initial copy of RAPI filebase to the trunk
[ganeti-local] / lib / rapi / resources.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API resources.
23
24 """
25
26 import cgi
27 import re
28
29 import ganeti.opcodes
30 import ganeti.errors
31 import ganeti.cli
32
33 from ganeti import constants
34 from ganeti import utils
35 from ganeti.rapi import httperror
36
37
38 # Initialized at the end of this file.
39 _CONNECTOR = {}
40
41
42 def BuildUriList(names, uri_format):
43   """Builds a URI list as used by index resources.
44
45   Args:
46   - names: List of names as strings
47   - uri_format: Format to be applied for URI
48
49   """
50   def _MapName(name):
51     return { "name": name, "uri": uri_format % name, }
52
53   # Make sure the result is sorted, makes it nicer to look at and simplifies
54   # unittests.
55   names.sort()
56
57   return map(_MapName, names)
58
59
60 def ExtractField(sequence, index):
61   """Creates a list containing one column out of a list of lists.
62
63   Args:
64   - sequence: Sequence of lists
65   - index: Index of field
66
67   """
68   return map(lambda item: item[index], sequence)
69
70
71 def MapFields(names, data):
72   """Maps two lists into one dictionary.
73
74   Args:
75   - names: Field names (list of strings)
76   - data: Field data (list)
77
78   Example:
79   >>> MapFields(["a", "b"], ["foo", 123])
80   {'a': 'foo', 'b': 123}
81
82   """
83   if len(names) != len(data):
84     raise AttributeError("Names and data must have the same length")
85   return dict([(names[i], data[i]) for i in range(len(names))])
86
87
88 def RequireLock(name='cmd'):
89   """Function decorator to automatically acquire locks.
90
91   PEP-318 style function decorator.
92
93   """
94   def wrapper(fn):
95     def new_f(*args, **kwargs):
96       try:
97         utils.Lock(name, max_retries=15)
98         try:
99           # Call real function
100           return fn(*args, **kwargs)
101         finally:
102           utils.Unlock(name)
103           utils.LockCleanup()
104       except ganeti.errors.LockError, err:
105         raise httperror.HTTPServiceUnavailable(message=str(err))
106
107     # Override function metadata
108     new_f.func_name = fn.func_name
109     new_f.func_doc = fn.func_doc
110
111     return new_f
112
113   return wrapper
114
115
116 def _Tags_GET(kind, name=None):
117   """Helper function to retrieve tags.
118
119   """
120   if name is None:
121     # Do not cause "missing parameter" error, which happens if a parameter
122     # is None.
123     name = ""
124   op = ganeti.opcodes.OpGetTags(kind=kind, name=name)
125   tags = ganeti.cli.SubmitOpCode(op)
126   return list(tags)
127
128
129 class Mapper:
130   """Map resource to method.
131
132   """
133   def __init__(self, connector=_CONNECTOR):
134     """Resource mapper constructor.
135
136     Args:
137       con: a dictionary, mapping method name with URL path regexp
138
139     """
140     self._connector = connector
141
142   def getController(self, uri):
143     """Find method for a given URI.
144
145     Args:
146       uri: string with URI
147
148     Returns:
149       None if no method is found or a tuple containing the following fields:
150         methd: name of method mapped to URI
151         items: a list of variable intems in the path
152         args: a dictionary with additional parameters from URL
153
154     """
155     if '?' in uri:
156       (path, query) = uri.split('?', 1)
157       args = cgi.parse_qs(query)
158     else:
159       path = uri
160       query = None
161       args = {}
162
163     result = None
164
165     for key, handler in self._connector.iteritems():
166       # Regex objects
167       if hasattr(key, "match"):
168         m = key.match(path)
169         if m:
170           result = (handler, list(m.groups()), args)
171           break
172
173       # String objects
174       elif key == path:
175         result = (handler, [], args)
176         break
177
178     if result is not None:
179       return result
180     else:
181       raise httperror.HTTPNotFound()
182
183
184 class R_Generic(object):
185   """Generic class for resources.
186
187   """
188   def __init__(self, request, items, queryargs):
189     """Generic resource constructor.
190
191     Args:
192       request: HTTPRequestHandler object
193       items: a list with variables encoded in the URL
194       queryargs: a dictionary with additional options from URL
195
196     """
197     self.request = request
198     self.items = items
199     self.queryargs = queryargs
200
201
202 class R_root(R_Generic):
203   """/ resource.
204
205   """
206   DOC_URI = "/"
207
208   def GET(self):
209     """Show the list of mapped resources.
210     
211     Returns:
212       A dictionary with 'name' and 'uri' keys for each of them.
213
214     """
215     root_pattern = re.compile('^R_([a-zA-Z0-9]+)$')
216
217     rootlist = []
218     for handler in _CONNECTOR.values():
219       m = root_pattern.match(handler.__name__)
220       if m:
221         name = m.group(1)
222         if name != 'root':
223           rootlist.append(name)
224
225     return BuildUriList(rootlist, "/%s")
226
227
228 class R_version(R_Generic):
229   """/version resource.
230
231   This resource should be used to determine the remote API version and to adapt
232   clients accordingly.
233
234   """
235   DOC_URI = "/version"
236
237   def GET(self):
238     """Returns the remote API version.
239
240     """
241     return constants.RAPI_VERSION
242
243
244 class R_tags(R_Generic):
245   """/tags resource.
246
247   Manages cluster tags.
248
249   """
250   DOC_URI = "/tags"
251
252   def GET(self):
253     """Returns a list of all cluster tags.
254
255     Example: ["tag1", "tag2", "tag3"]
256
257     """
258     return _Tags_GET(constants.TAG_CLUSTER)
259
260
261 class R_info(R_Generic):
262   """Cluster info.
263
264   """
265   DOC_URI = "/info"
266
267   def GET(self):
268     """Returns cluster information.
269
270     Example: {
271       "config_version": 3,
272       "name": "cluster1.example.com",
273       "software_version": "1.2.4",
274       "os_api_version": 5,
275       "export_version": 0,
276       "master": "node1.example.com",
277       "architecture": [
278         "64bit",
279         "x86_64"
280       ],
281       "hypervisor_type": "xen-3.0",
282       "protocol_version": 12
283     }
284
285     """
286     op = ganeti.opcodes.OpQueryClusterInfo()
287     return ganeti.cli.SubmitOpCode(op)
288
289
290 class R_nodes(R_Generic):
291   """/nodes resource.
292
293   """
294   DOC_URI = "/nodes"
295
296   @RequireLock()
297   def _GetDetails(self, nodeslist):
298     """Returns detailed instance data for bulk output.
299
300     Args:
301       instance: A list of nodes names.
302
303     Returns:
304       A list of nodes properties
305
306     """
307     fields = ["name","dtotal", "dfree",
308               "mtotal", "mnode", "mfree",
309               "pinst_cnt", "sinst_cnt", "tags"]
310
311     op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
312                                      names=nodeslist)
313     result = ganeti.cli.SubmitOpCode(op)
314
315     nodes_details = []
316     for node in result:
317       mapped = MapFields(fields, node)
318       nodes_details.append(mapped)
319     return nodes_details
320  
321   def GET(self):
322     """Returns a list of all nodes.
323     
324     Returns:
325       A dictionary with 'name' and 'uri' keys for each of them.
326
327     Example: [
328         {
329           "name": "node1.example.com",
330           "uri": "\/instances\/node1.example.com"
331         },
332         {
333           "name": "node2.example.com",
334           "uri": "\/instances\/node2.example.com"
335         }]
336
337     If the optional 'bulk' argument is provided and set to 'true' 
338     value (i.e '?bulk=1'), the output contains detailed
339     information about nodes as a list. Note: Lock required.
340
341     Example: [
342         {
343           "pinst_cnt": 1,
344           "mfree": 31280,
345           "mtotal": 32763,
346           "name": "www.example.com",
347           "tags": [],
348           "mnode": 512,
349           "dtotal": 5246208,
350           "sinst_cnt": 2,
351           "dfree": 5171712
352         },
353         ...
354     ]
355
356     """
357     op = ganeti.opcodes.OpQueryNodes(output_fields=["name"], names=[])
358     nodeslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
359     
360     if 'bulk' in self.queryargs:
361       return self._GetDetails(nodeslist)
362
363     return BuildUriList(nodeslist, "/nodes/%s")
364
365
366 class R_nodes_name(R_Generic):
367   """/nodes/[node_name] resources.
368
369   """
370   DOC_URI = "/nodes/[node_name]"
371
372   @RequireLock()
373   def GET(self):
374     """Send information about a node. 
375
376     """
377     node_name = self.items[0]
378     fields = ["name","dtotal", "dfree",
379               "mtotal", "mnode", "mfree",
380               "pinst_cnt", "sinst_cnt", "tags"]
381
382     op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
383                                      names=[node_name])
384     result = ganeti.cli.SubmitOpCode(op)
385
386     return MapFields(fields, result[0])
387
388
389 class R_nodes_name_tags(R_Generic):
390   """/nodes/[node_name]/tags resource.
391
392   Manages per-node tags.
393
394   """
395   DOC_URI = "/nodes/[node_name]/tags"
396
397   def GET(self):
398     """Returns a list of node tags.
399
400     Example: ["tag1", "tag2", "tag3"]
401
402     """
403     return _Tags_GET(constants.TAG_NODE, name=self.items[0])
404
405
406 class R_instances(R_Generic):
407   """/instances resource.
408
409   """
410   DOC_URI = "/instances"
411
412   @RequireLock()
413   def _GetDetails(self, instanceslist):
414     """Returns detailed instance data for bulk output.
415
416     Args:
417       instance: A list of instances names.
418
419     Returns:
420       A list with instances properties.
421
422     """
423     fields = ["name", "os", "pnode", "snodes",
424               "admin_state", "admin_ram",
425               "disk_template", "ip", "mac", "bridge",
426               "sda_size", "sdb_size", "vcpus",
427               "oper_state", "status", "tags"]
428
429     op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
430                                          names=instanceslist)
431     result = ganeti.cli.SubmitOpCode(op)
432
433     instances_details = []
434     for instance in result:
435       mapped = MapFields(fields, instance)
436       instances_details.append(mapped)
437     return instances_details
438    
439   def GET(self):
440     """Returns a list of all available instances.
441     
442     Returns:
443        A dictionary with 'name' and 'uri' keys for each of them.
444
445     Example: [
446         {
447           "name": "web.example.com",
448           "uri": "\/instances\/web.example.com"
449         },
450         {
451           "name": "mail.example.com",
452           "uri": "\/instances\/mail.example.com"
453         }]
454
455     If the optional 'bulk' argument is provided and set to 'true' 
456     value (i.e '?bulk=1'), the output contains detailed
457     information about instances as a list. Note: Lock required.
458
459     Example: [
460         {
461            "status": "running",
462            "bridge": "xen-br0",
463            "name": "web.example.com",
464            "tags": ["tag1", "tag2"],
465            "admin_ram": 512,
466            "sda_size": 20480,
467            "pnode": "node1.example.com",
468            "mac": "01:23:45:67:89:01",
469            "sdb_size": 4096,
470            "snodes": ["node2.example.com"],
471            "disk_template": "drbd",
472            "ip": null,
473            "admin_state": true,
474            "os": "debian-etch",
475            "vcpus": 2,
476            "oper_state": true
477         },
478         ...
479     ]
480
481     """
482     op = ganeti.opcodes.OpQueryInstances(output_fields=["name"], names=[])
483     instanceslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
484     
485     if 'bulk' in self.queryargs:
486       return self._GetDetails(instanceslist)  
487
488     else:
489       return BuildUriList(instanceslist, "/instances/%s")
490
491
492 class R_instances_name(R_Generic):
493   """/instances/[instance_name] resources.
494
495   """
496   DOC_URI = "/instances/[instance_name]"
497
498   @RequireLock()
499   def GET(self):
500     """Send information about an instance.
501
502     """
503     instance_name = self.items[0]
504     fields = ["name", "os", "pnode", "snodes",
505               "admin_state", "admin_ram",
506               "disk_template", "ip", "mac", "bridge",
507               "sda_size", "sdb_size", "vcpus",
508               "oper_state", "status", "tags"]
509
510     op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
511                                          names=[instance_name])
512     result = ganeti.cli.SubmitOpCode(op)
513
514     return MapFields(fields, result[0])
515
516
517 class R_instances_name_tags(R_Generic):
518   """/instances/[instance_name]/tags resource.
519
520   Manages per-instance tags.
521
522   """
523   DOC_URI = "/instances/[instance_name]/tags"
524
525   def GET(self):
526     """Returns a list of instance tags.
527
528     Example: ["tag1", "tag2", "tag3"]
529
530     """
531     return _Tags_GET(constants.TAG_INSTANCE, name=self.items[0])
532
533
534 class R_os(R_Generic):
535   """/os resource.
536
537   """
538   DOC_URI = "/os"
539
540   @RequireLock()
541   def GET(self):
542     """Return a list of all OSes.
543
544     Can return error 500 in case of a problem.
545
546     Example: ["debian-etch"]
547
548     """
549     op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
550                                      names=[])
551     diagnose_data = ganeti.cli.SubmitOpCode(op)
552
553     if not isinstance(diagnose_data, list):
554       raise httperror.HTTPInternalError(message="Can't get OS list")
555
556     return [row[0] for row in diagnose_data if row[1]]
557
558
559 _CONNECTOR.update({
560   "/": R_root,
561
562   "/version": R_version,
563
564   "/tags": R_tags,
565   "/info": R_info,
566
567   "/nodes": R_nodes,
568   re.compile(r'^/nodes/([\w\._-]+)$'): R_nodes_name,
569   re.compile(r'^/nodes/([\w\._-]+)/tags$'): R_nodes_name_tags,
570
571   "/instances": R_instances,
572   re.compile(r'^/instances/([\w\._-]+)$'): R_instances_name,
573   re.compile(r'^/instances/([\w\._-]+)/tags$'): R_instances_name_tags,
574
575   "/os": R_os,
576   })