Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / resources.py @ e2212007

History | View | Annotate | Download (13 kB)

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
  })