Statistics
| Branch: | Tag: | Revision:

root / lib / rapi / rlib2.py @ 460d22be

History | View | Annotate | Download (20.2 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
      name_check=fn('name_check', True),
468
      wait_for_sync=True,
469
      hypervisor=fn('hypervisor', None),
470
      hvparams=hvparams,
471
      beparams=beparams,
472
      file_storage_dir=fn('file_storage_dir', None),
473
      file_driver=fn('file_driver', 'loop'),
474
      dry_run=bool(self.dryRun()),
475
      )
476

    
477
    return baserlib.SubmitJob([op])
478

    
479

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

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

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

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

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

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

    
504

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

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

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

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

    
520

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

524
  Implements an instance reboot.
525

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

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

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

    
543
    return baserlib.SubmitJob([op])
544

    
545

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

549
  Implements an instance startup.
550

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

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

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

    
565
    return baserlib.SubmitJob([op])
566

    
567

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

571
  Implements an instance shutdown.
572

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

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

    
582
    return baserlib.SubmitJob([op])
583

    
584

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

588
  Implements an instance reinstall.
589

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

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

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

    
611

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

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

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

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

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

    
640
    return baserlib.SubmitJob([op])
641

    
642

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

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

649
  """
650
  TAG_LEVEL = None
651

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

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

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

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

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

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

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

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

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

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

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

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

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

    
704

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

708
  Manages per-instance tags.
709

710
  """
711
  TAG_LEVEL = constants.TAG_INSTANCE
712

    
713

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

717
  Manages per-node tags.
718

719
  """
720
  TAG_LEVEL = constants.TAG_NODE
721

    
722

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

726
  Manages cluster tags.
727

728
  """
729
  TAG_LEVEL = constants.TAG_CLUSTER