Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 82e186f8

History | View | Annotate | Download (20.1 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 version 2 baserlib.library.
23

24
"""
25

    
26
from ganeti import opcodes
27
from ganeti import http
28
from ganeti import constants
29
from ganeti import cli
30
from ganeti import rapi
31
from ganeti.rapi import baserlib
32

    
33

    
34
_COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
35
I_FIELDS = ["name", "admin_state", "os",
36
            "pnode", "snodes",
37
            "disk_template",
38
            "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
39
            "network_port",
40
            "disk.sizes", "disk_usage",
41
            "beparams", "hvparams",
42
            "oper_state", "oper_ram", "status",
43
            ] + _COMMON_FIELDS
44

    
45
N_FIELDS = ["name", "offline", "master_candidate", "drained",
46
            "dtotal", "dfree",
47
            "mtotal", "mnode", "mfree",
48
            "pinst_cnt", "sinst_cnt",
49
            "ctotal", "cnodes", "csockets",
50
            "pip", "sip", "role",
51
            "pinst_list", "sinst_list",
52
            ] + _COMMON_FIELDS
53

    
54
_NR_DRAINED = "drained"
55
_NR_MASTER_CANDIATE = "master-candidate"
56
_NR_MASTER = "master"
57
_NR_OFFLINE = "offline"
58
_NR_REGULAR = "regular"
59

    
60
_NR_MAP = {
61
  "M": _NR_MASTER,
62
  "C": _NR_MASTER_CANDIATE,
63
  "D": _NR_DRAINED,
64
  "O": _NR_OFFLINE,
65
  "R": _NR_REGULAR,
66
  }
67

    
68

    
69
class R_version(baserlib.R_Generic):
70
  """/version resource.
71

72
  This resource should be used to determine the remote API version and
73
  to adapt clients accordingly.
74

75
  """
76
  def GET(self):
77
    """Returns the remote API version.
78

79
    """
80
    return constants.RAPI_VERSION
81

    
82

    
83
class R_2_info(baserlib.R_Generic):
84
  """Cluster info.
85

86
  """
87
  def GET(self):
88
    """Returns cluster information.
89

90
    """
91
    client = baserlib.GetClient()
92
    return client.QueryClusterInfo()
93

    
94

    
95
class R_2_os(baserlib.R_Generic):
96
  """/2/os resource.
97

98
  """
99
  def GET(self):
100
    """Return a list of all OSes.
101

102
    Can return error 500 in case of a problem.
103

104
    Example: ["debian-etch"]
105

106
    """
107
    cl = baserlib.GetClient()
108
    op = opcodes.OpDiagnoseOS(output_fields=["name", "valid", "variants"],
109
                              names=[])
110
    job_id = baserlib.SubmitJob([op], cl)
111
    # we use custom feedback function, instead of print we log the status
112
    result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
113
    diagnose_data = result[0]
114

    
115
    if not isinstance(diagnose_data, list):
116
      raise http.HttpBadGateway(message="Can't get OS list")
117

    
118
    os_names = []
119
    for (name, valid, variants) in diagnose_data:
120
      if valid:
121
        os_names.extend(cli.CalculateOSNames(name, variants))
122

    
123
    return os_names
124

    
125

    
126
class R_2_redist_config(baserlib.R_Generic):
127
  """/2/redistribute-config resource.
128

129
  """
130
  def PUT(self):
131
    """Redistribute configuration to all nodes.
132

133
    """
134
    return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
135

    
136

    
137
class R_2_jobs(baserlib.R_Generic):
138
  """/2/jobs resource.
139

140
  """
141
  def GET(self):
142
    """Returns a dictionary of jobs.
143

144
    @return: a dictionary with jobs id and uri.
145

146
    """
147
    fields = ["id"]
148
    cl = baserlib.GetClient()
149
    # Convert the list of lists to the list of ids
150
    result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
151
    return baserlib.BuildUriList(result, "/2/jobs/%s",
152
                                 uri_fields=("id", "uri"))
153

    
154

    
155
class R_2_jobs_id(baserlib.R_Generic):
156
  """/2/jobs/[job_id] resource.
157

158
  """
159
  def GET(self):
160
    """Returns a job status.
161

162
    @return: a dictionary with job parameters.
163
        The result includes:
164
            - id: job ID as a number
165
            - status: current job status as a string
166
            - ops: involved OpCodes as a list of dictionaries for each
167
              opcodes in the job
168
            - opstatus: OpCodes status as a list
169
            - opresult: OpCodes results as a list of lists
170

171
    """
172
    fields = ["id", "ops", "status", "summary",
173
              "opstatus", "opresult", "oplog",
174
              "received_ts", "start_ts", "end_ts",
175
              ]
176
    job_id = self.items[0]
177
    result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
178
    if result is None:
179
      raise http.HttpNotFound()
180
    return baserlib.MapFields(fields, result)
181

    
182
  def DELETE(self):
183
    """Cancel not-yet-started job.
184

185
    """
186
    job_id = self.items[0]
187
    result = baserlib.GetClient().CancelJob(job_id)
188
    return result
189

    
190

    
191
class R_2_nodes(baserlib.R_Generic):
192
  """/2/nodes resource.
193

194
  """
195
  def GET(self):
196
    """Returns a list of all nodes.
197

198
    """
199
    client = baserlib.GetClient()
200

    
201
    if self.useBulk():
202
      bulkdata = client.QueryNodes([], N_FIELDS, False)
203
      return baserlib.MapBulkFields(bulkdata, N_FIELDS)
204
    else:
205
      nodesdata = client.QueryNodes([], ["name"], False)
206
      nodeslist = [row[0] for row in nodesdata]
207
      return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
208
                                   uri_fields=("id", "uri"))
209

    
210

    
211
class R_2_nodes_name(baserlib.R_Generic):
212
  """/2/nodes/[node_name] resources.
213

214
  """
215
  def GET(self):
216
    """Send information about a node.
217

218
    """
219
    node_name = self.items[0]
220
    client = baserlib.GetClient()
221
    result = client.QueryNodes(names=[node_name], fields=N_FIELDS,
222
                               use_locking=self.useLocking())
223

    
224
    return baserlib.MapFields(N_FIELDS, result[0])
225

    
226

    
227
class R_2_nodes_name_role(baserlib.R_Generic):
228
  """ /2/nodes/[node_name]/role resource.
229

230
  """
231
  def GET(self):
232
    """Returns the current node role.
233

234
    @return: Node role
235

236
    """
237
    node_name = self.items[0]
238
    client = baserlib.GetClient()
239
    result = client.QueryNodes(names=[node_name], fields=["role"],
240
                               use_locking=self.useLocking())
241

    
242
    return _NR_MAP[result[0][0]]
243

    
244
  def PUT(self):
245
    """Sets the node role.
246

247
    @return: a job id
248

249
    """
250
    if not isinstance(self.req.request_body, basestring):
251
      raise http.HttpBadRequest("Invalid body contents, not a string")
252

    
253
    node_name = self.items[0]
254
    role = self.req.request_body
255

    
256
    if role == _NR_REGULAR:
257
      candidate = False
258
      offline = False
259
      drained = False
260

    
261
    elif role == _NR_MASTER_CANDIATE:
262
      candidate = True
263
      offline = drained = None
264

    
265
    elif role == _NR_DRAINED:
266
      drained = True
267
      candidate = offline = None
268

    
269
    elif role == _NR_OFFLINE:
270
      offline = True
271
      candidate = drained = None
272

    
273
    else:
274
      raise http.HttpBadRequest("Can't set '%s' role" % role)
275

    
276
    op = opcodes.OpSetNodeParams(node_name=node_name,
277
                                 master_candidate=candidate,
278
                                 offline=offline,
279
                                 drained=drained,
280
                                 force=bool(self.useForce()))
281

    
282
    return baserlib.SubmitJob([op])
283

    
284

    
285
class R_2_nodes_name_evacuate(baserlib.R_Generic):
286
  """/2/nodes/[node_name]/evacuate resource.
287

288
  """
289
  def POST(self):
290
    """Evacuate all secondary instances off a node.
291

292
    """
293
    node_name = self.items[0]
294
    remote_node = self._checkStringVariable("remote_node", default=None)
295
    iallocator = self._checkStringVariable("iallocator", default=None)
296

    
297
    op = opcodes.OpEvacuateNode(node_name=node_name,
298
                                remote_node=remote_node,
299
                                iallocator=iallocator)
300

    
301
    return baserlib.SubmitJob([op])
302

    
303

    
304
class R_2_nodes_name_migrate(baserlib.R_Generic):
305
  """/2/nodes/[node_name]/migrate resource.
306

307
  """
308
  def POST(self):
309
    """Migrate all primary instances from a node.
310

311
    """
312
    node_name = self.items[0]
313
    live = bool(self._checkIntVariable("live", default=1))
314

    
315
    op = opcodes.OpMigrateNode(node_name=node_name, live=live)
316

    
317
    return baserlib.SubmitJob([op])
318

    
319

    
320
class R_2_nodes_name_storage(baserlib.R_Generic):
321
  """/2/nodes/[node_name]/storage ressource.
322

323
  """
324
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
325
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
326

    
327
  def GET(self):
328
    node_name = self.items[0]
329

    
330
    storage_type = self._checkStringVariable("storage_type", None)
331
    if not storage_type:
332
      raise http.HttpBadRequest("Missing the required 'storage_type'"
333
                                " parameter")
334

    
335
    output_fields = self._checkStringVariable("output_fields", None)
336
    if not output_fields:
337
      raise http.HttpBadRequest("Missing the required 'output_fields'"
338
                                " parameter")
339

    
340
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
341
                                    storage_type=storage_type,
342
                                    output_fields=output_fields.split(","))
343
    return baserlib.SubmitJob([op])
344

    
345

    
346
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
347
  """/2/nodes/[node_name]/storage/modify ressource.
348

349
  """
350
  def PUT(self):
351
    node_name = self.items[0]
352

    
353
    storage_type = self._checkStringVariable("storage_type", None)
354
    if not storage_type:
355
      raise http.HttpBadRequest("Missing the required 'storage_type'"
356
                                " parameter")
357

    
358
    name = self._checkStringVariable("name", None)
359
    if not name:
360
      raise http.HttpBadRequest("Missing the required 'name'"
361
                                " parameter")
362

    
363
    changes = {}
364

    
365
    if "allocatable" in self.queryargs:
366
      changes[constants.SF_ALLOCATABLE] = \
367
        bool(self._checkIntVariable("allocatable", default=1))
368

    
369
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
370
                                     storage_type=storage_type,
371
                                     name=name,
372
                                     changes=changes)
373
    return baserlib.SubmitJob([op])
374

    
375

    
376
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
377
  """/2/nodes/[node_name]/storage/repair ressource.
378

379
  """
380
  def PUT(self):
381
    node_name = self.items[0]
382

    
383
    storage_type = self._checkStringVariable("storage_type", None)
384
    if not storage_type:
385
      raise http.HttpBadRequest("Missing the required 'storage_type'"
386
                                " parameter")
387

    
388
    name = self._checkStringVariable("name", None)
389
    if not name:
390
      raise http.HttpBadRequest("Missing the required 'name'"
391
                                " parameter")
392

    
393
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
394
                                     storage_type=storage_type,
395
                                     name=name)
396
    return baserlib.SubmitJob([op])
397

    
398

    
399
class R_2_instances(baserlib.R_Generic):
400
  """/2/instances resource.
401

402
  """
403
  def GET(self):
404
    """Returns a list of all available instances.
405

406
    """
407
    client = baserlib.GetClient()
408

    
409
    use_locking = self.useLocking()
410
    if self.useBulk():
411
      bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
412
      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
413
    else:
414
      instancesdata = client.QueryInstances([], ["name"], use_locking)
415
      instanceslist = [row[0] for row in instancesdata]
416
      return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
417
                                   uri_fields=("id", "uri"))
418

    
419
  def POST(self):
420
    """Create an instance.
421

422
    @return: a job id
423

424
    """
425
    if not isinstance(self.req.request_body, dict):
426
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
427

    
428
    beparams = baserlib.MakeParamsDict(self.req.request_body,
429
                                       constants.BES_PARAMETERS)
430
    hvparams = baserlib.MakeParamsDict(self.req.request_body,
431
                                       constants.HVS_PARAMETERS)
432
    fn = self.getBodyParameter
433

    
434
    # disk processing
435
    disk_data = fn('disks')
436
    if not isinstance(disk_data, list):
437
      raise http.HttpBadRequest("The 'disks' parameter should be a list")
438
    disks = []
439
    for idx, d in enumerate(disk_data):
440
      if not isinstance(d, int):
441
        raise http.HttpBadRequest("Disk %d specification wrong: should"
442
                                  " be an integer" % idx)
443
      disks.append({"size": d})
444
    # nic processing (one nic only)
445
    nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
446
    if fn("ip", None) is not None:
447
      nics[0]["ip"] = fn("ip")
448
    if fn("mode", None) is not None:
449
      nics[0]["mode"] = fn("mode")
450
    if fn("link", None) is not None:
451
      nics[0]["link"] = fn("link")
452
    if fn("bridge", None) is not None:
453
      nics[0]["bridge"] = fn("bridge")
454

    
455
    op = opcodes.OpCreateInstance(
456
      mode=constants.INSTANCE_CREATE,
457
      instance_name=fn('name'),
458
      disks=disks,
459
      disk_template=fn('disk_template'),
460
      os_type=fn('os'),
461
      pnode=fn('pnode', None),
462
      snode=fn('snode', None),
463
      iallocator=fn('iallocator', None),
464
      nics=nics,
465
      start=fn('start', True),
466
      ip_check=fn('ip_check', True),
467
      wait_for_sync=True,
468
      hypervisor=fn('hypervisor', None),
469
      hvparams=hvparams,
470
      beparams=beparams,
471
      file_storage_dir=fn('file_storage_dir', None),
472
      file_driver=fn('file_driver', 'loop'),
473
      dry_run=bool(self.dryRun()),
474
      )
475

    
476
    return baserlib.SubmitJob([op])
477

    
478

    
479
class R_2_instances_name(baserlib.R_Generic):
480
  """/2/instances/[instance_name] resources.
481

482
  """
483
  def GET(self):
484
    """Send information about an instance.
485

486
    """
487
    client = baserlib.GetClient()
488
    instance_name = self.items[0]
489
    result = client.QueryInstances(names=[instance_name], fields=I_FIELDS,
490
                                   use_locking=self.useLocking())
491

    
492
    return baserlib.MapFields(I_FIELDS, result[0])
493

    
494
  def DELETE(self):
495
    """Delete an instance.
496

497
    """
498
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
499
                                  ignore_failures=False,
500
                                  dry_run=bool(self.dryRun()))
501
    return baserlib.SubmitJob([op])
502

    
503

    
504
class R_2_instances_name_info(baserlib.R_Generic):
505
  """/2/instances/[instance_name]/info resource.
506

507
  """
508
  def GET(self):
509
    """Request detailed instance information.
510

511
    """
512
    instance_name = self.items[0]
513
    static = bool(self._checkIntVariable("static", default=0))
514

    
515
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
516
                                     static=static)
517
    return baserlib.SubmitJob([op])
518

    
519

    
520
class R_2_instances_name_reboot(baserlib.R_Generic):
521
  """/2/instances/[instance_name]/reboot resource.
522

523
  Implements an instance reboot.
524

525
  """
526
  def POST(self):
527
    """Reboot an instance.
528

529
    The URI takes type=[hard|soft|full] and
530
    ignore_secondaries=[False|True] parameters.
531

532
    """
533
    instance_name = self.items[0]
534
    reboot_type = self.queryargs.get('type',
535
                                     [constants.INSTANCE_REBOOT_HARD])[0]
536
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
537
    op = opcodes.OpRebootInstance(instance_name=instance_name,
538
                                  reboot_type=reboot_type,
539
                                  ignore_secondaries=ignore_secondaries,
540
                                  dry_run=bool(self.dryRun()))
541

    
542
    return baserlib.SubmitJob([op])
543

    
544

    
545
class R_2_instances_name_startup(baserlib.R_Generic):
546
  """/2/instances/[instance_name]/startup resource.
547

548
  Implements an instance startup.
549

550
  """
551
  def PUT(self):
552
    """Startup an instance.
553

554
    The URI takes force=[False|True] parameter to start the instance
555
    if even if secondary disks are failing.
556

557
    """
558
    instance_name = self.items[0]
559
    force_startup = bool(self._checkIntVariable('force'))
560
    op = opcodes.OpStartupInstance(instance_name=instance_name,
561
                                   force=force_startup,
562
                                   dry_run=bool(self.dryRun()))
563

    
564
    return baserlib.SubmitJob([op])
565

    
566

    
567
class R_2_instances_name_shutdown(baserlib.R_Generic):
568
  """/2/instances/[instance_name]/shutdown resource.
569

570
  Implements an instance shutdown.
571

572
  """
573
  def PUT(self):
574
    """Shutdown an instance.
575

576
    """
577
    instance_name = self.items[0]
578
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
579
                                    dry_run=bool(self.dryRun()))
580

    
581
    return baserlib.SubmitJob([op])
582

    
583

    
584
class R_2_instances_name_reinstall(baserlib.R_Generic):
585
  """/2/instances/[instance_name]/reinstall resource.
586

587
  Implements an instance reinstall.
588

589
  """
590
  def POST(self):
591
    """Reinstall an instance.
592

593
    The URI takes os=name and nostartup=[0|1] optional
594
    parameters. By default, the instance will be started
595
    automatically.
596

597
    """
598
    instance_name = self.items[0]
599
    ostype = self._checkStringVariable('os')
600
    nostartup = self._checkIntVariable('nostartup')
601
    ops = [
602
      opcodes.OpShutdownInstance(instance_name=instance_name),
603
      opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
604
      ]
605
    if not nostartup:
606
      ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
607
                                           force=False))
608
    return baserlib.SubmitJob(ops)
609

    
610

    
611
class R_2_instances_name_replace_disks(baserlib.R_Generic):
612
  """/2/instances/[instance_name]/replace-disks resource.
613

614
  """
615
  def POST(self):
616
    """Replaces disks on an instance.
617

618
    """
619
    instance_name = self.items[0]
620
    remote_node = self._checkStringVariable("remote_node", default=None)
621
    mode = self._checkStringVariable("mode", default=None)
622
    raw_disks = self._checkStringVariable("disks", default=None)
623
    iallocator = self._checkStringVariable("iallocator", default=None)
624

    
625
    if raw_disks:
626
      try:
627
        disks = [int(part) for part in raw_disks.split(",")]
628
      except ValueError, err:
629
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
630
    else:
631
      disks = []
632

    
633
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
634
                                remote_node=remote_node,
635
                                mode=mode,
636
                                disks=disks,
637
                                iallocator=iallocator)
638

    
639
    return baserlib.SubmitJob([op])
640

    
641

    
642
class _R_Tags(baserlib.R_Generic):
643
  """ Quasiclass for tagging resources
644

645
  Manages tags. When inheriting this class you must define the
646
  TAG_LEVEL for it.
647

648
  """
649
  TAG_LEVEL = None
650

    
651
  def __init__(self, items, queryargs, req):
652
    """A tag resource constructor.
653

654
    We have to override the default to sort out cluster naming case.
655

656
    """
657
    baserlib.R_Generic.__init__(self, items, queryargs, req)
658

    
659
    if self.TAG_LEVEL != constants.TAG_CLUSTER:
660
      self.name = items[0]
661
    else:
662
      self.name = ""
663

    
664
  def GET(self):
665
    """Returns a list of tags.
666

667
    Example: ["tag1", "tag2", "tag3"]
668

669
    """
670
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
671

    
672
  def PUT(self):
673
    """Add a set of tags.
674

675
    The request as a list of strings should be PUT to this URI. And
676
    you'll have back a job id.
677

678
    """
679
    if 'tag' not in self.queryargs:
680
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
681
                                " the 'tag' parameter")
682
    return baserlib._Tags_PUT(self.TAG_LEVEL,
683
                              self.queryargs['tag'], name=self.name,
684
                              dry_run=bool(self.dryRun()))
685

    
686
  def DELETE(self):
687
    """Delete a tag.
688

689
    In order to delete a set of tags, the DELETE
690
    request should be addressed to URI like:
691
    /tags?tag=[tag]&tag=[tag]
692

693
    """
694
    if 'tag' not in self.queryargs:
695
      # no we not gonna delete all tags
696
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
697
                                " tag(s) using the 'tag' parameter")
698
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
699
                                 self.queryargs['tag'],
700
                                 name=self.name,
701
                                 dry_run=bool(self.dryRun()))
702

    
703

    
704
class R_2_instances_name_tags(_R_Tags):
705
  """ /2/instances/[instance_name]/tags resource.
706

707
  Manages per-instance tags.
708

709
  """
710
  TAG_LEVEL = constants.TAG_INSTANCE
711

    
712

    
713
class R_2_nodes_name_tags(_R_Tags):
714
  """ /2/nodes/[node_name]/tags resource.
715

716
  Manages per-node tags.
717

718
  """
719
  TAG_LEVEL = constants.TAG_NODE
720

    
721

    
722
class R_2_tags(_R_Tags):
723
  """ /2/instances/tags resource.
724

725
  Manages cluster tags.
726

727
  """
728
  TAG_LEVEL = constants.TAG_CLUSTER