Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / resources.py @ bac5ffc3

History | View | Annotate | Download (13.6 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 luxi
35
from ganeti import utils
36
from ganeti.rapi import httperror
37

    
38

    
39
# Initialized at the end of this file.
40
_CONNECTOR = {}
41

    
42

    
43
def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
44
  """Builds a URI list as used by index resources.
45

46
  Args:
47
  - ids: List of ids as strings
48
  - uri_format: Format to be applied for URI
49
  - uri_fields: Optional parameter for field ids
50

51
  """
52
  (field_id, field_uri) = uri_fields
53
  
54
  def _MapId(m_id):
55
    return { field_id: m_id, field_uri: uri_format % m_id, }
56

    
57
  # Make sure the result is sorted, makes it nicer to look at and simplifies
58
  # unittests.
59
  ids.sort()
60

    
61
  return map(_MapId, ids)
62

    
63

    
64
def ExtractField(sequence, index):
65
  """Creates a list containing one column out of a list of lists.
66

67
  Args:
68
  - sequence: Sequence of lists
69
  - index: Index of field
70

71
  """
72
  return map(lambda item: item[index], sequence)
73

    
74

    
75
def MapFields(names, data):
76
  """Maps two lists into one dictionary.
77

78
  Args:
79
  - names: Field names (list of strings)
80
  - data: Field data (list)
81

82
  Example:
83
  >>> MapFields(["a", "b"], ["foo", 123])
84
  {'a': 'foo', 'b': 123}
85

86
  """
87
  if len(names) != len(data):
88
    raise AttributeError("Names and data must have the same length")
89
  return dict([(names[i], data[i]) for i in range(len(names))])
90

    
91

    
92
def _Tags_GET(kind, name=None):
93
  """Helper function to retrieve tags.
94

95
  """
96
  if name is None:
97
    # Do not cause "missing parameter" error, which happens if a parameter
98
    # is None.
99
    name = ""
100
  op = ganeti.opcodes.OpGetTags(kind=kind, name=name)
101
  tags = ganeti.cli.SubmitOpCode(op)
102
  return list(tags)
103

    
104

    
105
class Mapper:
106
  """Map resource to method.
107

108
  """
109
  def __init__(self, connector=_CONNECTOR):
110
    """Resource mapper constructor.
111

112
    Args:
113
      con: a dictionary, mapping method name with URL path regexp
114

115
    """
116
    self._connector = connector
117

    
118
  def getController(self, uri):
119
    """Find method for a given URI.
120

121
    Args:
122
      uri: string with URI
123

124
    Returns:
125
      None if no method is found or a tuple containing the following fields:
126
        methd: name of method mapped to URI
127
        items: a list of variable intems in the path
128
        args: a dictionary with additional parameters from URL
129

130
    """
131
    if '?' in uri:
132
      (path, query) = uri.split('?', 1)
133
      args = cgi.parse_qs(query)
134
    else:
135
      path = uri
136
      query = None
137
      args = {}
138

    
139
    result = None
140

    
141
    for key, handler in self._connector.iteritems():
142
      # Regex objects
143
      if hasattr(key, "match"):
144
        m = key.match(path)
145
        if m:
146
          result = (handler, list(m.groups()), args)
147
          break
148

    
149
      # String objects
150
      elif key == path:
151
        result = (handler, [], args)
152
        break
153

    
154
    if result is not None:
155
      return result
156
    else:
157
      raise httperror.HTTPNotFound()
158

    
159

    
160
class R_Generic(object):
161
  """Generic class for resources.
162

163
  """
164
  def __init__(self, request, items, queryargs):
165
    """Generic resource constructor.
166

167
    Args:
168
      request: HTTPRequestHandler object
169
      items: a list with variables encoded in the URL
170
      queryargs: a dictionary with additional options from URL
171

172
    """
173
    self.request = request
174
    self.items = items
175
    self.queryargs = queryargs
176

    
177

    
178
class R_root(R_Generic):
179
  """/ resource.
180

181
  """
182
  DOC_URI = "/"
183

    
184
  def GET(self):
185
    """Show the list of mapped resources.
186
    
187
    Returns:
188
      A dictionary with 'name' and 'uri' keys for each of them.
189

190
    """
191
    root_pattern = re.compile('^R_([a-zA-Z0-9]+)$')
192

    
193
    rootlist = []
194
    for handler in _CONNECTOR.values():
195
      m = root_pattern.match(handler.__name__)
196
      if m:
197
        name = m.group(1)
198
        if name != 'root':
199
          rootlist.append(name)
200

    
201
    return BuildUriList(rootlist, "/%s")
202

    
203

    
204
class R_version(R_Generic):
205
  """/version resource.
206

207
  This resource should be used to determine the remote API version and to adapt
208
  clients accordingly.
209

210
  """
211
  DOC_URI = "/version"
212

    
213
  def GET(self):
214
    """Returns the remote API version.
215

216
    """
217
    return constants.RAPI_VERSION
218

    
219

    
220
class R_tags(R_Generic):
221
  """/tags resource.
222

223
  Manages cluster tags.
224

225
  """
226
  DOC_URI = "/tags"
227

    
228
  def GET(self):
229
    """Returns a list of all cluster tags.
230

231
    Example: ["tag1", "tag2", "tag3"]
232

233
    """
234
    return _Tags_GET(constants.TAG_CLUSTER)
235

    
236

    
237
class R_info(R_Generic):
238
  """Cluster info.
239

240
  """
241
  DOC_URI = "/info"
242

    
243
  def GET(self):
244
    """Returns cluster information.
245

246
    Example: {
247
      "config_version": 3,
248
      "name": "cluster1.example.com",
249
      "software_version": "1.2.4",
250
      "os_api_version": 5,
251
      "export_version": 0,
252
      "master": "node1.example.com",
253
      "architecture": [
254
        "64bit",
255
        "x86_64"
256
      ],
257
      "hypervisor_type": "xen-3.0",
258
      "protocol_version": 12
259
    }
260

261
    """
262
    op = ganeti.opcodes.OpQueryClusterInfo()
263
    return ganeti.cli.SubmitOpCode(op)
264

    
265

    
266
class R_nodes(R_Generic):
267
  """/nodes resource.
268

269
  """
270
  DOC_URI = "/nodes"
271

    
272
  def _GetDetails(self, nodeslist):
273
    """Returns detailed instance data for bulk output.
274

275
    Args:
276
      instance: A list of nodes names.
277

278
    Returns:
279
      A list of nodes properties
280

281
    """
282
    fields = ["name","dtotal", "dfree",
283
              "mtotal", "mnode", "mfree",
284
              "pinst_cnt", "sinst_cnt", "tags"]
285

    
286
    op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
287
                                     names=nodeslist)
288
    result = ganeti.cli.SubmitOpCode(op)
289

    
290
    nodes_details = []
291
    for node in result:
292
      mapped = MapFields(fields, node)
293
      nodes_details.append(mapped)
294
    return nodes_details
295
 
296
  def GET(self):
297
    """Returns a list of all nodes.
298
    
299
    Returns:
300
      A dictionary with 'name' and 'uri' keys for each of them.
301

302
    Example: [
303
        {
304
          "name": "node1.example.com",
305
          "uri": "\/instances\/node1.example.com"
306
        },
307
        {
308
          "name": "node2.example.com",
309
          "uri": "\/instances\/node2.example.com"
310
        }]
311

312
    If the optional 'bulk' argument is provided and set to 'true' 
313
    value (i.e '?bulk=1'), the output contains detailed
314
    information about nodes as a list.
315

316
    Example: [
317
        {
318
          "pinst_cnt": 1,
319
          "mfree": 31280,
320
          "mtotal": 32763,
321
          "name": "www.example.com",
322
          "tags": [],
323
          "mnode": 512,
324
          "dtotal": 5246208,
325
          "sinst_cnt": 2,
326
          "dfree": 5171712
327
        },
328
        ...
329
    ]
330

331
    """
332
    op = ganeti.opcodes.OpQueryNodes(output_fields=["name"], names=[])
333
    nodeslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
334
    
335
    if 'bulk' in self.queryargs:
336
      return self._GetDetails(nodeslist)
337

    
338
    return BuildUriList(nodeslist, "/nodes/%s")
339

    
340

    
341
class R_nodes_name(R_Generic):
342
  """/nodes/[node_name] resources.
343

344
  """
345
  DOC_URI = "/nodes/[node_name]"
346

    
347
  def GET(self):
348
    """Send information about a node. 
349

350
    """
351
    node_name = self.items[0]
352
    fields = ["name","dtotal", "dfree",
353
              "mtotal", "mnode", "mfree",
354
              "pinst_cnt", "sinst_cnt", "tags"]
355

    
356
    op = ganeti.opcodes.OpQueryNodes(output_fields=fields,
357
                                     names=[node_name])
358
    result = ganeti.cli.SubmitOpCode(op)
359

    
360
    return MapFields(fields, result[0])
361

    
362

    
363
class R_nodes_name_tags(R_Generic):
364
  """/nodes/[node_name]/tags resource.
365

366
  Manages per-node tags.
367

368
  """
369
  DOC_URI = "/nodes/[node_name]/tags"
370

    
371
  def GET(self):
372
    """Returns a list of node tags.
373

374
    Example: ["tag1", "tag2", "tag3"]
375

376
    """
377
    return _Tags_GET(constants.TAG_NODE, name=self.items[0])
378

    
379

    
380
class R_instances(R_Generic):
381
  """/instances resource.
382

383
  """
384
  DOC_URI = "/instances"
385

    
386
  def _GetDetails(self, instanceslist):
387
    """Returns detailed instance data for bulk output.
388

389
    Args:
390
      instance: A list of instances names.
391

392
    Returns:
393
      A list with instances properties.
394

395
    """
396
    fields = ["name", "os", "pnode", "snodes",
397
              "admin_state", "admin_ram",
398
              "disk_template", "ip", "mac", "bridge",
399
              "sda_size", "sdb_size", "vcpus",
400
              "oper_state", "status", "tags"]
401

    
402
    op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
403
                                         names=instanceslist)
404
    result = ganeti.cli.SubmitOpCode(op)
405

    
406
    instances_details = []
407
    for instance in result:
408
      mapped = MapFields(fields, instance)
409
      instances_details.append(mapped)
410
    return instances_details
411
   
412
  def GET(self):
413
    """Returns a list of all available instances.
414
    
415
    Returns:
416
       A dictionary with 'name' and 'uri' keys for each of them.
417

418
    Example: [
419
        {
420
          "name": "web.example.com",
421
          "uri": "\/instances\/web.example.com"
422
        },
423
        {
424
          "name": "mail.example.com",
425
          "uri": "\/instances\/mail.example.com"
426
        }]
427

428
    If the optional 'bulk' argument is provided and set to 'true' 
429
    value (i.e '?bulk=1'), the output contains detailed
430
    information about instances as a list.
431

432
    Example: [
433
        {
434
           "status": "running",
435
           "bridge": "xen-br0",
436
           "name": "web.example.com",
437
           "tags": ["tag1", "tag2"],
438
           "admin_ram": 512,
439
           "sda_size": 20480,
440
           "pnode": "node1.example.com",
441
           "mac": "01:23:45:67:89:01",
442
           "sdb_size": 4096,
443
           "snodes": ["node2.example.com"],
444
           "disk_template": "drbd",
445
           "ip": null,
446
           "admin_state": true,
447
           "os": "debian-etch",
448
           "vcpus": 2,
449
           "oper_state": true
450
        },
451
        ...
452
    ]
453

454
    """
455
    op = ganeti.opcodes.OpQueryInstances(output_fields=["name"], names=[])
456
    instanceslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0)
457
    
458
    if 'bulk' in self.queryargs:
459
      return self._GetDetails(instanceslist)  
460

    
461
    else:
462
      return BuildUriList(instanceslist, "/instances/%s")
463

    
464

    
465
class R_instances_name(R_Generic):
466
  """/instances/[instance_name] resources.
467

468
  """
469
  DOC_URI = "/instances/[instance_name]"
470

    
471
  def GET(self):
472
    """Send information about an instance.
473

474
    """
475
    instance_name = self.items[0]
476
    fields = ["name", "os", "pnode", "snodes",
477
              "admin_state", "admin_ram",
478
              "disk_template", "ip", "mac", "bridge",
479
              "sda_size", "sdb_size", "vcpus",
480
              "oper_state", "status", "tags"]
481

    
482
    op = ganeti.opcodes.OpQueryInstances(output_fields=fields,
483
                                         names=[instance_name])
484
    result = ganeti.cli.SubmitOpCode(op)
485

    
486
    return MapFields(fields, result[0])
487

    
488

    
489
class R_instances_name_tags(R_Generic):
490
  """/instances/[instance_name]/tags resource.
491

492
  Manages per-instance tags.
493

494
  """
495
  DOC_URI = "/instances/[instance_name]/tags"
496

    
497
  def GET(self):
498
    """Returns a list of instance tags.
499

500
    Example: ["tag1", "tag2", "tag3"]
501

502
    """
503
    return _Tags_GET(constants.TAG_INSTANCE, name=self.items[0])
504

    
505

    
506
class R_os(R_Generic):
507
  """/os resource.
508

509
  """
510
  DOC_URI = "/os"
511

    
512
  def GET(self):
513
    """Return a list of all OSes.
514

515
    Can return error 500 in case of a problem.
516

517
    Example: ["debian-etch"]
518

519
    """
520
    op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
521
                                     names=[])
522
    diagnose_data = ganeti.cli.SubmitOpCode(op)
523

    
524
    if not isinstance(diagnose_data, list):
525
      raise httperror.HTTPInternalError(message="Can't get OS list")
526

    
527
    return [row[0] for row in diagnose_data if row[1]]
528

    
529

    
530
class R_2_jobs(R_Generic):
531
  """/2/jobs resource.
532

533
  """
534
  DOC_URI = "/2/jobs"
535

    
536
  def GET(self):
537
    """Returns a dictionary of jobs.
538

539
    Returns:
540
      A dictionary with jobs id and uri.
541
    
542
    """
543
    fields = ["id"]
544
    # Convert the list of lists to the list of ids
545
    result = [job_id for [job_id] in luxi.Client().QueryJobs(None, fields)]
546
    return BuildUriList(result, "/2/jobs/%s", uri_fields=("id", "uri"))
547

    
548

    
549
class R_2_jobs_id(R_Generic):
550
  """/2/jobs/[job_id] resource.
551

552
  """
553
  DOC_URI = "/2/jobs/[job_id]"
554

    
555
  def GET(self):
556
    """Returns a job status.
557

558
    Returns: 
559
      A dictionary with job parameters.
560

561
    The result includes:
562
      id - job ID as a number
563
      status - current job status as a string
564
      ops - involved OpCodes as a list of dictionaries for each opcodes in 
565
        the job
566
      opstatus - OpCodes status as a list
567
      opresult - OpCodes results as a list of lists
568
    
569
    """
570
    fields = ["id", "ops", "status", "opstatus", "opresult"]
571
    job_id = self.items[0]
572
    result = luxi.Client().QueryJobs([job_id,], fields)[0]
573
    return MapFields(fields, result)
574

    
575

    
576
_CONNECTOR.update({
577
  "/": R_root,
578

    
579
  "/version": R_version,
580

    
581
  "/tags": R_tags,
582
  "/info": R_info,
583

    
584
  "/nodes": R_nodes,
585
  re.compile(r'^/nodes/([\w\._-]+)$'): R_nodes_name,
586
  re.compile(r'^/nodes/([\w\._-]+)/tags$'): R_nodes_name_tags,
587

    
588
  "/instances": R_instances,
589
  re.compile(r'^/instances/([\w\._-]+)$'): R_instances_name,
590
  re.compile(r'^/instances/([\w\._-]+)/tags$'): R_instances_name_tags,
591

    
592
  "/os": R_os,
593

    
594
  "/2/jobs": R_2_jobs,
595
  re.compile(r'/2/jobs/(%s)$' % constants.JOB_ID_TEMPLATE): R_2_jobs_id,
596
  })