Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 7260cfbe

History | View | Annotate | Download (20.3 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
# pylint: disable-msg=C0103
27

    
28
# C0103: Invalid name, since the R_* names are not conforming
29

    
30
from ganeti import opcodes
31
from ganeti import http
32
from ganeti import constants
33
from ganeti import cli
34
from ganeti import rapi
35
from ganeti.rapi import baserlib
36

    
37

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

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

    
58
_NR_DRAINED = "drained"
59
_NR_MASTER_CANDIATE = "master-candidate"
60
_NR_MASTER = "master"
61
_NR_OFFLINE = "offline"
62
_NR_REGULAR = "regular"
63

    
64
_NR_MAP = {
65
  "M": _NR_MASTER,
66
  "C": _NR_MASTER_CANDIATE,
67
  "D": _NR_DRAINED,
68
  "O": _NR_OFFLINE,
69
  "R": _NR_REGULAR,
70
  }
71

    
72

    
73
class R_version(baserlib.R_Generic):
74
  """/version resource.
75

76
  This resource should be used to determine the remote API version and
77
  to adapt clients accordingly.
78

79
  """
80
  def GET(self):
81
    """Returns the remote API version.
82

83
    """
84
    return constants.RAPI_VERSION
85

    
86

    
87
class R_2_info(baserlib.R_Generic):
88
  """Cluster info.
89

90
  """
91
  def GET(self):
92
    """Returns cluster information.
93

94
    """
95
    client = baserlib.GetClient()
96
    return client.QueryClusterInfo()
97

    
98

    
99
class R_2_os(baserlib.R_Generic):
100
  """/2/os resource.
101

102
  """
103
  def GET(self):
104
    """Return a list of all OSes.
105

106
    Can return error 500 in case of a problem.
107

108
    Example: ["debian-etch"]
109

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

    
119
    if not isinstance(diagnose_data, list):
120
      raise http.HttpBadGateway(message="Can't get OS list")
121

    
122
    os_names = []
123
    for (name, valid, variants) in diagnose_data:
124
      if valid:
125
        os_names.extend(cli.CalculateOSNames(name, variants))
126

    
127
    return os_names
128

    
129

    
130
class R_2_redist_config(baserlib.R_Generic):
131
  """/2/redistribute-config resource.
132

133
  """
134
  def PUT(self):
135
    """Redistribute configuration to all nodes.
136

137
    """
138
    return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
139

    
140

    
141
class R_2_jobs(baserlib.R_Generic):
142
  """/2/jobs resource.
143

144
  """
145
  def GET(self):
146
    """Returns a dictionary of jobs.
147

148
    @return: a dictionary with jobs id and uri.
149

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

    
158

    
159
class R_2_jobs_id(baserlib.R_Generic):
160
  """/2/jobs/[job_id] resource.
161

162
  """
163
  def GET(self):
164
    """Returns a job status.
165

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

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

    
186
  def DELETE(self):
187
    """Cancel not-yet-started job.
188

189
    """
190
    job_id = self.items[0]
191
    result = baserlib.GetClient().CancelJob(job_id)
192
    return result
193

    
194

    
195
class R_2_nodes(baserlib.R_Generic):
196
  """/2/nodes resource.
197

198
  """
199
  def GET(self):
200
    """Returns a list of all nodes.
201

202
    """
203
    client = baserlib.GetClient()
204

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

    
214

    
215
class R_2_nodes_name(baserlib.R_Generic):
216
  """/2/nodes/[node_name] resources.
217

218
  """
219
  def GET(self):
220
    """Send information about a node.
221

222
    """
223
    node_name = self.items[0]
224
    client = baserlib.GetClient()
225
    result = client.QueryNodes(names=[node_name], fields=N_FIELDS,
226
                               use_locking=self.useLocking())
227

    
228
    return baserlib.MapFields(N_FIELDS, result[0])
229

    
230

    
231
class R_2_nodes_name_role(baserlib.R_Generic):
232
  """ /2/nodes/[node_name]/role resource.
233

234
  """
235
  def GET(self):
236
    """Returns the current node role.
237

238
    @return: Node role
239

240
    """
241
    node_name = self.items[0]
242
    client = baserlib.GetClient()
243
    result = client.QueryNodes(names=[node_name], fields=["role"],
244
                               use_locking=self.useLocking())
245

    
246
    return _NR_MAP[result[0][0]]
247

    
248
  def PUT(self):
249
    """Sets the node role.
250

251
    @return: a job id
252

253
    """
254
    if not isinstance(self.req.request_body, basestring):
255
      raise http.HttpBadRequest("Invalid body contents, not a string")
256

    
257
    node_name = self.items[0]
258
    role = self.req.request_body
259

    
260
    if role == _NR_REGULAR:
261
      candidate = False
262
      offline = False
263
      drained = False
264

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

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

    
273
    elif role == _NR_OFFLINE:
274
      offline = True
275
      candidate = drained = None
276

    
277
    else:
278
      raise http.HttpBadRequest("Can't set '%s' role" % role)
279

    
280
    op = opcodes.OpSetNodeParams(node_name=node_name,
281
                                 master_candidate=candidate,
282
                                 offline=offline,
283
                                 drained=drained,
284
                                 force=bool(self.useForce()))
285

    
286
    return baserlib.SubmitJob([op])
287

    
288

    
289
class R_2_nodes_name_evacuate(baserlib.R_Generic):
290
  """/2/nodes/[node_name]/evacuate resource.
291

292
  """
293
  def POST(self):
294
    """Evacuate all secondary instances off a node.
295

296
    """
297
    node_name = self.items[0]
298
    remote_node = self._checkStringVariable("remote_node", default=None)
299
    iallocator = self._checkStringVariable("iallocator", default=None)
300

    
301
    op = opcodes.OpEvacuateNode(node_name=node_name,
302
                                remote_node=remote_node,
303
                                iallocator=iallocator)
304

    
305
    return baserlib.SubmitJob([op])
306

    
307

    
308
class R_2_nodes_name_migrate(baserlib.R_Generic):
309
  """/2/nodes/[node_name]/migrate resource.
310

311
  """
312
  def POST(self):
313
    """Migrate all primary instances from a node.
314

315
    """
316
    node_name = self.items[0]
317
    live = bool(self._checkIntVariable("live", default=1))
318

    
319
    op = opcodes.OpMigrateNode(node_name=node_name, live=live)
320

    
321
    return baserlib.SubmitJob([op])
322

    
323

    
324
class R_2_nodes_name_storage(baserlib.R_Generic):
325
  """/2/nodes/[node_name]/storage ressource.
326

327
  """
328
  # LUQueryNodeStorage acquires locks, hence restricting access to GET
329
  GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
330

    
331
  def GET(self):
332
    node_name = self.items[0]
333

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

    
339
    output_fields = self._checkStringVariable("output_fields", None)
340
    if not output_fields:
341
      raise http.HttpBadRequest("Missing the required 'output_fields'"
342
                                " parameter")
343

    
344
    op = opcodes.OpQueryNodeStorage(nodes=[node_name],
345
                                    storage_type=storage_type,
346
                                    output_fields=output_fields.split(","))
347
    return baserlib.SubmitJob([op])
348

    
349

    
350
class R_2_nodes_name_storage_modify(baserlib.R_Generic):
351
  """/2/nodes/[node_name]/storage/modify ressource.
352

353
  """
354
  def PUT(self):
355
    node_name = self.items[0]
356

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

    
362
    name = self._checkStringVariable("name", None)
363
    if not name:
364
      raise http.HttpBadRequest("Missing the required 'name'"
365
                                " parameter")
366

    
367
    changes = {}
368

    
369
    if "allocatable" in self.queryargs:
370
      changes[constants.SF_ALLOCATABLE] = \
371
        bool(self._checkIntVariable("allocatable", default=1))
372

    
373
    op = opcodes.OpModifyNodeStorage(node_name=node_name,
374
                                     storage_type=storage_type,
375
                                     name=name,
376
                                     changes=changes)
377
    return baserlib.SubmitJob([op])
378

    
379

    
380
class R_2_nodes_name_storage_repair(baserlib.R_Generic):
381
  """/2/nodes/[node_name]/storage/repair ressource.
382

383
  """
384
  def PUT(self):
385
    node_name = self.items[0]
386

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

    
392
    name = self._checkStringVariable("name", None)
393
    if not name:
394
      raise http.HttpBadRequest("Missing the required 'name'"
395
                                " parameter")
396

    
397
    op = opcodes.OpRepairNodeStorage(node_name=node_name,
398
                                     storage_type=storage_type,
399
                                     name=name)
400
    return baserlib.SubmitJob([op])
401

    
402

    
403
class R_2_instances(baserlib.R_Generic):
404
  """/2/instances resource.
405

406
  """
407
  def GET(self):
408
    """Returns a list of all available instances.
409

410
    """
411
    client = baserlib.GetClient()
412

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

    
423
  def POST(self):
424
    """Create an instance.
425

426
    @return: a job id
427

428
    """
429
    if not isinstance(self.req.request_body, dict):
430
      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
431

    
432
    beparams = baserlib.MakeParamsDict(self.req.request_body,
433
                                       constants.BES_PARAMETERS)
434
    hvparams = baserlib.MakeParamsDict(self.req.request_body,
435
                                       constants.HVS_PARAMETERS)
436
    fn = self.getBodyParameter
437

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

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

    
481
    return baserlib.SubmitJob([op])
482

    
483

    
484
class R_2_instances_name(baserlib.R_Generic):
485
  """/2/instances/[instance_name] resources.
486

487
  """
488
  def GET(self):
489
    """Send information about an instance.
490

491
    """
492
    client = baserlib.GetClient()
493
    instance_name = self.items[0]
494
    result = client.QueryInstances(names=[instance_name], fields=I_FIELDS,
495
                                   use_locking=self.useLocking())
496

    
497
    return baserlib.MapFields(I_FIELDS, result[0])
498

    
499
  def DELETE(self):
500
    """Delete an instance.
501

502
    """
503
    op = opcodes.OpRemoveInstance(instance_name=self.items[0],
504
                                  ignore_failures=False,
505
                                  dry_run=bool(self.dryRun()))
506
    return baserlib.SubmitJob([op])
507

    
508

    
509
class R_2_instances_name_info(baserlib.R_Generic):
510
  """/2/instances/[instance_name]/info resource.
511

512
  """
513
  def GET(self):
514
    """Request detailed instance information.
515

516
    """
517
    instance_name = self.items[0]
518
    static = bool(self._checkIntVariable("static", default=0))
519

    
520
    op = opcodes.OpQueryInstanceData(instances=[instance_name],
521
                                     static=static)
522
    return baserlib.SubmitJob([op])
523

    
524

    
525
class R_2_instances_name_reboot(baserlib.R_Generic):
526
  """/2/instances/[instance_name]/reboot resource.
527

528
  Implements an instance reboot.
529

530
  """
531
  def POST(self):
532
    """Reboot an instance.
533

534
    The URI takes type=[hard|soft|full] and
535
    ignore_secondaries=[False|True] parameters.
536

537
    """
538
    instance_name = self.items[0]
539
    reboot_type = self.queryargs.get('type',
540
                                     [constants.INSTANCE_REBOOT_HARD])[0]
541
    ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
542
    op = opcodes.OpRebootInstance(instance_name=instance_name,
543
                                  reboot_type=reboot_type,
544
                                  ignore_secondaries=ignore_secondaries,
545
                                  dry_run=bool(self.dryRun()))
546

    
547
    return baserlib.SubmitJob([op])
548

    
549

    
550
class R_2_instances_name_startup(baserlib.R_Generic):
551
  """/2/instances/[instance_name]/startup resource.
552

553
  Implements an instance startup.
554

555
  """
556
  def PUT(self):
557
    """Startup an instance.
558

559
    The URI takes force=[False|True] parameter to start the instance
560
    if even if secondary disks are failing.
561

562
    """
563
    instance_name = self.items[0]
564
    force_startup = bool(self._checkIntVariable('force'))
565
    op = opcodes.OpStartupInstance(instance_name=instance_name,
566
                                   force=force_startup,
567
                                   dry_run=bool(self.dryRun()))
568

    
569
    return baserlib.SubmitJob([op])
570

    
571

    
572
class R_2_instances_name_shutdown(baserlib.R_Generic):
573
  """/2/instances/[instance_name]/shutdown resource.
574

575
  Implements an instance shutdown.
576

577
  """
578
  def PUT(self):
579
    """Shutdown an instance.
580

581
    """
582
    instance_name = self.items[0]
583
    op = opcodes.OpShutdownInstance(instance_name=instance_name,
584
                                    dry_run=bool(self.dryRun()))
585

    
586
    return baserlib.SubmitJob([op])
587

    
588

    
589
class R_2_instances_name_reinstall(baserlib.R_Generic):
590
  """/2/instances/[instance_name]/reinstall resource.
591

592
  Implements an instance reinstall.
593

594
  """
595
  def POST(self):
596
    """Reinstall an instance.
597

598
    The URI takes os=name and nostartup=[0|1] optional
599
    parameters. By default, the instance will be started
600
    automatically.
601

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

    
615

    
616
class R_2_instances_name_replace_disks(baserlib.R_Generic):
617
  """/2/instances/[instance_name]/replace-disks resource.
618

619
  """
620
  def POST(self):
621
    """Replaces disks on an instance.
622

623
    """
624
    instance_name = self.items[0]
625
    remote_node = self._checkStringVariable("remote_node", default=None)
626
    mode = self._checkStringVariable("mode", default=None)
627
    raw_disks = self._checkStringVariable("disks", default=None)
628
    iallocator = self._checkStringVariable("iallocator", default=None)
629

    
630
    if raw_disks:
631
      try:
632
        disks = [int(part) for part in raw_disks.split(",")]
633
      except ValueError, err:
634
        raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
635
    else:
636
      disks = []
637

    
638
    op = opcodes.OpReplaceDisks(instance_name=instance_name,
639
                                remote_node=remote_node,
640
                                mode=mode,
641
                                disks=disks,
642
                                iallocator=iallocator)
643

    
644
    return baserlib.SubmitJob([op])
645

    
646

    
647
class _R_Tags(baserlib.R_Generic):
648
  """ Quasiclass for tagging resources
649

650
  Manages tags. When inheriting this class you must define the
651
  TAG_LEVEL for it.
652

653
  """
654
  TAG_LEVEL = None
655

    
656
  def __init__(self, items, queryargs, req):
657
    """A tag resource constructor.
658

659
    We have to override the default to sort out cluster naming case.
660

661
    """
662
    baserlib.R_Generic.__init__(self, items, queryargs, req)
663

    
664
    if self.TAG_LEVEL != constants.TAG_CLUSTER:
665
      self.name = items[0]
666
    else:
667
      self.name = ""
668

    
669
  def GET(self):
670
    """Returns a list of tags.
671

672
    Example: ["tag1", "tag2", "tag3"]
673

674
    """
675
    # pylint: disable-msg=W0212
676
    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
677

    
678
  def PUT(self):
679
    """Add a set of tags.
680

681
    The request as a list of strings should be PUT to this URI. And
682
    you'll have back a job id.
683

684
    """
685
    # pylint: disable-msg=W0212
686
    if 'tag' not in self.queryargs:
687
      raise http.HttpBadRequest("Please specify tag(s) to add using the"
688
                                " the 'tag' parameter")
689
    return baserlib._Tags_PUT(self.TAG_LEVEL,
690
                              self.queryargs['tag'], name=self.name,
691
                              dry_run=bool(self.dryRun()))
692

    
693
  def DELETE(self):
694
    """Delete a tag.
695

696
    In order to delete a set of tags, the DELETE
697
    request should be addressed to URI like:
698
    /tags?tag=[tag]&tag=[tag]
699

700
    """
701
    # pylint: disable-msg=W0212
702
    if 'tag' not in self.queryargs:
703
      # no we not gonna delete all tags
704
      raise http.HttpBadRequest("Cannot delete all tags - please specify"
705
                                " tag(s) using the 'tag' parameter")
706
    return baserlib._Tags_DELETE(self.TAG_LEVEL,
707
                                 self.queryargs['tag'],
708
                                 name=self.name,
709
                                 dry_run=bool(self.dryRun()))
710

    
711

    
712
class R_2_instances_name_tags(_R_Tags):
713
  """ /2/instances/[instance_name]/tags resource.
714

715
  Manages per-instance tags.
716

717
  """
718
  TAG_LEVEL = constants.TAG_INSTANCE
719

    
720

    
721
class R_2_nodes_name_tags(_R_Tags):
722
  """ /2/nodes/[node_name]/tags resource.
723

724
  Manages per-node tags.
725

726
  """
727
  TAG_LEVEL = constants.TAG_NODE
728

    
729

    
730
class R_2_tags(_R_Tags):
731
  """ /2/instances/tags resource.
732

733
  Manages cluster tags.
734

735
  """
736
  TAG_LEVEL = constants.TAG_CLUSTER