Package ganeti :: Package rapi :: Module rlib2
[hide private]
[frames] | no frames]

Source Code for Module ganeti.rapi.rlib2

   1  # 
   2  # 
   3   
   4  # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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 resource implementations. 
  23   
  24  PUT or POST? 
  25  ============ 
  26   
  27  According to RFC2616 the main difference between PUT and POST is that 
  28  POST can create new resources but PUT can only create the resource the 
  29  URI was pointing to on the PUT request. 
  30   
  31  In the context of this module POST on ``/2/instances`` to change an existing 
  32  entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a 
  33  new instance) with a name specified in the request. 
  34   
  35  Quoting from RFC2616, section 9.6:: 
  36   
  37    The fundamental difference between the POST and PUT requests is reflected in 
  38    the different meaning of the Request-URI. The URI in a POST request 
  39    identifies the resource that will handle the enclosed entity. That resource 
  40    might be a data-accepting process, a gateway to some other protocol, or a 
  41    separate entity that accepts annotations. In contrast, the URI in a PUT 
  42    request identifies the entity enclosed with the request -- the user agent 
  43    knows what URI is intended and the server MUST NOT attempt to apply the 
  44    request to some other resource. If the server desires that the request be 
  45    applied to a different URI, it MUST send a 301 (Moved Permanently) response; 
  46    the user agent MAY then make its own decision regarding whether or not to 
  47    redirect the request. 
  48   
  49  So when adding new methods, if they are operating on the URI entity itself, 
  50  PUT should be prefered over POST. 
  51   
  52  """ 
  53   
  54  # pylint: disable=C0103 
  55   
  56  # C0103: Invalid name, since the R_* names are not conforming 
  57   
  58  from ganeti import opcodes 
  59  from ganeti import http 
  60  from ganeti import constants 
  61  from ganeti import cli 
  62  from ganeti import rapi 
  63  from ganeti import ht 
  64  from ganeti import compat 
  65  from ganeti import ssconf 
  66  from ganeti.rapi import baserlib 
  67   
  68   
  69  _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"] 
  70  I_FIELDS = ["name", "admin_state", "os", 
  71              "pnode", "snodes", 
  72              "disk_template", 
  73              "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges", 
  74              "network_port", 
  75              "disk.sizes", "disk_usage", 
  76              "beparams", "hvparams", 
  77              "oper_state", "oper_ram", "oper_vcpus", "status", 
  78              "custom_hvparams", "custom_beparams", "custom_nicparams", 
  79              ] + _COMMON_FIELDS 
  80   
  81  N_FIELDS = ["name", "offline", "master_candidate", "drained", 
  82              "dtotal", "dfree", 
  83              "mtotal", "mnode", "mfree", 
  84              "pinst_cnt", "sinst_cnt", 
  85              "ctotal", "cnodes", "csockets", 
  86              "pip", "sip", "role", 
  87              "pinst_list", "sinst_list", 
  88              "master_capable", "vm_capable", 
  89              "ndparams", 
  90              "group.uuid", 
  91              ] + _COMMON_FIELDS 
  92   
  93  G_FIELDS = [ 
  94    "alloc_policy", 
  95    "name", 
  96    "node_cnt", 
  97    "node_list", 
  98    "ipolicy", 
  99    "custom_ipolicy", 
 100    "diskparams", 
 101    "custom_diskparams", 
 102    "ndparams", 
 103    "custom_ndparams", 
 104    ] + _COMMON_FIELDS 
 105   
 106  J_FIELDS_BULK = [ 
 107    "id", "ops", "status", "summary", 
 108    "opstatus", 
 109    "received_ts", "start_ts", "end_ts", 
 110    ] 
 111   
 112  J_FIELDS = J_FIELDS_BULK + [ 
 113    "oplog", 
 114    "opresult", 
 115    ] 
 116   
 117  _NR_DRAINED = "drained" 
 118  _NR_MASTER_CANDIDATE = "master-candidate" 
 119  _NR_MASTER = "master" 
 120  _NR_OFFLINE = "offline" 
 121  _NR_REGULAR = "regular" 
 122   
 123  _NR_MAP = { 
 124    constants.NR_MASTER: _NR_MASTER, 
 125    constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE, 
 126    constants.NR_DRAINED: _NR_DRAINED, 
 127    constants.NR_OFFLINE: _NR_OFFLINE, 
 128    constants.NR_REGULAR: _NR_REGULAR, 
 129    } 
 130   
 131  assert frozenset(_NR_MAP.keys()) == constants.NR_ALL 
 132   
 133  # Request data version field 
 134  _REQ_DATA_VERSION = "__version__" 
 135   
 136  # Feature string for instance creation request data version 1 
 137  _INST_CREATE_REQV1 = "instance-create-reqv1" 
 138   
 139  # Feature string for instance reinstall request version 1 
 140  _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1" 
 141   
 142  # Feature string for node migration version 1 
 143  _NODE_MIGRATE_REQV1 = "node-migrate-reqv1" 
 144   
 145  # Feature string for node evacuation with LU-generated jobs 
 146  _NODE_EVAC_RES1 = "node-evac-res1" 
 147   
 148  ALL_FEATURES = frozenset([ 
 149    _INST_CREATE_REQV1, 
 150    _INST_REINSTALL_REQV1, 
 151    _NODE_MIGRATE_REQV1, 
 152    _NODE_EVAC_RES1, 
 153    ]) 
 154   
 155  # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change. 
 156  _WFJC_TIMEOUT = 10 
157 158 159 # FIXME: For compatibility we update the beparams/memory field. Needs to be 160 # removed in Ganeti 2.7 161 -def _UpdateBeparams(inst):
162 """Updates the beparams dict of inst to support the memory field. 163 164 @param inst: Inst dict 165 @return: Updated inst dict 166 167 """ 168 beparams = inst["beparams"] 169 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM] 170 171 return inst
172
173 174 -class R_root(baserlib.ResourceBase):
175 """/ resource. 176 177 """ 178 @staticmethod
179 - def GET():
180 """Supported for legacy reasons. 181 182 """ 183 return None
184
185 186 -class R_2(R_root):
187 """/2 resource. 188 189 """
190
191 192 -class R_version(baserlib.ResourceBase):
193 """/version resource. 194 195 This resource should be used to determine the remote API version and 196 to adapt clients accordingly. 197 198 """ 199 @staticmethod
200 - def GET():
201 """Returns the remote API version. 202 203 """ 204 return constants.RAPI_VERSION
205
206 207 -class R_2_info(baserlib.OpcodeResource):
208 """/2/info resource. 209 210 """ 211 GET_OPCODE = opcodes.OpClusterQuery 212
213 - def GET(self):
214 """Returns cluster information. 215 216 """ 217 client = self.GetClient() 218 return client.QueryClusterInfo()
219
220 221 -class R_2_features(baserlib.ResourceBase):
222 """/2/features resource. 223 224 """ 225 @staticmethod
226 - def GET():
227 """Returns list of optional RAPI features implemented. 228 229 """ 230 return list(ALL_FEATURES)
231
232 233 -class R_2_os(baserlib.OpcodeResource):
234 """/2/os resource. 235 236 """ 237 GET_OPCODE = opcodes.OpOsDiagnose 238
239 - def GET(self):
240 """Return a list of all OSes. 241 242 Can return error 500 in case of a problem. 243 244 Example: ["debian-etch"] 245 246 """ 247 cl = self.GetClient() 248 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[]) 249 job_id = self.SubmitJob([op], cl=cl) 250 # we use custom feedback function, instead of print we log the status 251 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn) 252 diagnose_data = result[0] 253 254 if not isinstance(diagnose_data, list): 255 raise http.HttpBadGateway(message="Can't get OS list") 256 257 os_names = [] 258 for (name, variants) in diagnose_data: 259 os_names.extend(cli.CalculateOSNames(name, variants)) 260 261 return os_names
262
263 264 -class R_2_redist_config(baserlib.OpcodeResource):
265 """/2/redistribute-config resource. 266 267 """ 268 PUT_OPCODE = opcodes.OpClusterRedistConf
269
270 271 -class R_2_cluster_modify(baserlib.OpcodeResource):
272 """/2/modify resource. 273 274 """ 275 PUT_OPCODE = opcodes.OpClusterSetParams
276
277 278 -class R_2_jobs(baserlib.ResourceBase):
279 """/2/jobs resource. 280 281 """
282 - def GET(self):
283 """Returns a dictionary of jobs. 284 285 @return: a dictionary with jobs id and uri. 286 287 """ 288 client = self.GetClient() 289 290 if self.useBulk(): 291 bulkdata = client.QueryJobs(None, J_FIELDS_BULK) 292 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK) 293 else: 294 jobdata = map(compat.fst, client.QueryJobs(None, ["id"])) 295 return baserlib.BuildUriList(jobdata, "/2/jobs/%s", 296 uri_fields=("id", "uri"))
297
298 299 -class R_2_jobs_id(baserlib.ResourceBase):
300 """/2/jobs/[job_id] resource. 301 302 """
303 - def GET(self):
304 """Returns a job status. 305 306 @return: a dictionary with job parameters. 307 The result includes: 308 - id: job ID as a number 309 - status: current job status as a string 310 - ops: involved OpCodes as a list of dictionaries for each 311 opcodes in the job 312 - opstatus: OpCodes status as a list 313 - opresult: OpCodes results as a list of lists 314 315 """ 316 job_id = self.items[0] 317 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0] 318 if result is None: 319 raise http.HttpNotFound() 320 return baserlib.MapFields(J_FIELDS, result)
321
322 - def DELETE(self):
323 """Cancel not-yet-started job. 324 325 """ 326 job_id = self.items[0] 327 result = self.GetClient().CancelJob(job_id) 328 return result
329
330 331 -class R_2_jobs_id_wait(baserlib.ResourceBase):
332 """/2/jobs/[job_id]/wait resource. 333 334 """ 335 # WaitForJobChange provides access to sensitive information and blocks 336 # machine resources (it's a blocking RAPI call), hence restricting access. 337 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] 338
339 - def GET(self):
340 """Waits for job changes. 341 342 """ 343 job_id = self.items[0] 344 345 fields = self.getBodyParameter("fields") 346 prev_job_info = self.getBodyParameter("previous_job_info", None) 347 prev_log_serial = self.getBodyParameter("previous_log_serial", None) 348 349 if not isinstance(fields, list): 350 raise http.HttpBadRequest("The 'fields' parameter should be a list") 351 352 if not (prev_job_info is None or isinstance(prev_job_info, list)): 353 raise http.HttpBadRequest("The 'previous_job_info' parameter should" 354 " be a list") 355 356 if not (prev_log_serial is None or 357 isinstance(prev_log_serial, (int, long))): 358 raise http.HttpBadRequest("The 'previous_log_serial' parameter should" 359 " be a number") 360 361 client = self.GetClient() 362 result = client.WaitForJobChangeOnce(job_id, fields, 363 prev_job_info, prev_log_serial, 364 timeout=_WFJC_TIMEOUT) 365 if not result: 366 raise http.HttpNotFound() 367 368 if result == constants.JOB_NOTCHANGED: 369 # No changes 370 return None 371 372 (job_info, log_entries) = result 373 374 return { 375 "job_info": job_info, 376 "log_entries": log_entries, 377 }
378
379 380 -class R_2_nodes(baserlib.OpcodeResource):
381 """/2/nodes resource. 382 383 """ 384 GET_OPCODE = opcodes.OpNodeQuery 385
386 - def GET(self):
387 """Returns a list of all nodes. 388 389 """ 390 client = self.GetClient() 391 392 if self.useBulk(): 393 bulkdata = client.QueryNodes([], N_FIELDS, False) 394 return baserlib.MapBulkFields(bulkdata, N_FIELDS) 395 else: 396 nodesdata = client.QueryNodes([], ["name"], False) 397 nodeslist = [row[0] for row in nodesdata] 398 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s", 399 uri_fields=("id", "uri"))
400
401 402 -class R_2_nodes_name(baserlib.OpcodeResource):
403 """/2/nodes/[node_name] resource. 404 405 """ 406 GET_OPCODE = opcodes.OpNodeQuery 407
408 - def GET(self):
409 """Send information about a node. 410 411 """ 412 node_name = self.items[0] 413 client = self.GetClient() 414 415 result = baserlib.HandleItemQueryErrors(client.QueryNodes, 416 names=[node_name], fields=N_FIELDS, 417 use_locking=self.useLocking()) 418 419 return baserlib.MapFields(N_FIELDS, result[0])
420
421 422 -class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
423 """/2/nodes/[node_name]/powercycle resource. 424 425 """ 426 POST_OPCODE = opcodes.OpNodePowercycle 427
428 - def GetPostOpInput(self):
429 """Tries to powercycle a node. 430 431 """ 432 return (self.request_body, { 433 "node_name": self.items[0], 434 "force": self.useForce(), 435 })
436
437 438 -class R_2_nodes_name_role(baserlib.OpcodeResource):
439 """/2/nodes/[node_name]/role resource. 440 441 """ 442 PUT_OPCODE = opcodes.OpNodeSetParams 443
444 - def GET(self):
445 """Returns the current node role. 446 447 @return: Node role 448 449 """ 450 node_name = self.items[0] 451 client = self.GetClient() 452 result = client.QueryNodes(names=[node_name], fields=["role"], 453 use_locking=self.useLocking()) 454 455 return _NR_MAP[result[0][0]]
456
457 - def GetPutOpInput(self):
458 """Sets the node role. 459 460 """ 461 baserlib.CheckType(self.request_body, basestring, "Body contents") 462 463 role = self.request_body 464 465 if role == _NR_REGULAR: 466 candidate = False 467 offline = False 468 drained = False 469 470 elif role == _NR_MASTER_CANDIDATE: 471 candidate = True 472 offline = drained = None 473 474 elif role == _NR_DRAINED: 475 drained = True 476 candidate = offline = None 477 478 elif role == _NR_OFFLINE: 479 offline = True 480 candidate = drained = None 481 482 else: 483 raise http.HttpBadRequest("Can't set '%s' role" % role) 484 485 assert len(self.items) == 1 486 487 return ({}, { 488 "node_name": self.items[0], 489 "master_candidate": candidate, 490 "offline": offline, 491 "drained": drained, 492 "force": self.useForce(), 493 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)), 494 })
495
496 497 -class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
498 """/2/nodes/[node_name]/evacuate resource. 499 500 """ 501 POST_OPCODE = opcodes.OpNodeEvacuate 502
503 - def GetPostOpInput(self):
504 """Evacuate all instances off a node. 505 506 """ 507 return (self.request_body, { 508 "node_name": self.items[0], 509 "dry_run": self.dryRun(), 510 })
511
512 513 -class R_2_nodes_name_migrate(baserlib.OpcodeResource):
514 """/2/nodes/[node_name]/migrate resource. 515 516 """ 517 POST_OPCODE = opcodes.OpNodeMigrate 518
519 - def GetPostOpInput(self):
520 """Migrate all primary instances from a node. 521 522 """ 523 if self.queryargs: 524 # Support old-style requests 525 if "live" in self.queryargs and "mode" in self.queryargs: 526 raise http.HttpBadRequest("Only one of 'live' and 'mode' should" 527 " be passed") 528 529 if "live" in self.queryargs: 530 if self._checkIntVariable("live", default=1): 531 mode = constants.HT_MIGRATION_LIVE 532 else: 533 mode = constants.HT_MIGRATION_NONLIVE 534 else: 535 mode = self._checkStringVariable("mode", default=None) 536 537 data = { 538 "mode": mode, 539 } 540 else: 541 data = self.request_body 542 543 return (data, { 544 "node_name": self.items[0], 545 })
546
547 548 -class R_2_nodes_name_modify(baserlib.OpcodeResource):
549 """/2/nodes/[node_name]/modify resource. 550 551 """ 552 POST_OPCODE = opcodes.OpNodeSetParams 553
554 - def GetPostOpInput(self):
555 """Changes parameters of a node. 556 557 """ 558 assert len(self.items) == 1 559 560 return (self.request_body, { 561 "node_name": self.items[0], 562 })
563
564 565 -class R_2_nodes_name_storage(baserlib.OpcodeResource):
566 """/2/nodes/[node_name]/storage resource. 567 568 """ 569 # LUNodeQueryStorage acquires locks, hence restricting access to GET 570 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] 571 GET_OPCODE = opcodes.OpNodeQueryStorage 572
573 - def GetGetOpInput(self):
574 """List storage available on a node. 575 576 """ 577 storage_type = self._checkStringVariable("storage_type", None) 578 output_fields = self._checkStringVariable("output_fields", None) 579 580 if not output_fields: 581 raise http.HttpBadRequest("Missing the required 'output_fields'" 582 " parameter") 583 584 return ({}, { 585 "nodes": [self.items[0]], 586 "storage_type": storage_type, 587 "output_fields": output_fields.split(","), 588 })
589
590 591 -class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
592 """/2/nodes/[node_name]/storage/modify resource. 593 594 """ 595 PUT_OPCODE = opcodes.OpNodeModifyStorage 596
597 - def GetPutOpInput(self):
598 """Modifies a storage volume on a node. 599 600 """ 601 storage_type = self._checkStringVariable("storage_type", None) 602 name = self._checkStringVariable("name", None) 603 604 if not name: 605 raise http.HttpBadRequest("Missing the required 'name'" 606 " parameter") 607 608 changes = {} 609 610 if "allocatable" in self.queryargs: 611 changes[constants.SF_ALLOCATABLE] = \ 612 bool(self._checkIntVariable("allocatable", default=1)) 613 614 return ({}, { 615 "node_name": self.items[0], 616 "storage_type": storage_type, 617 "name": name, 618 "changes": changes, 619 })
620
621 622 -class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
623 """/2/nodes/[node_name]/storage/repair resource. 624 625 """ 626 PUT_OPCODE = opcodes.OpRepairNodeStorage 627
628 - def GetPutOpInput(self):
629 """Repairs a storage volume on a node. 630 631 """ 632 storage_type = self._checkStringVariable("storage_type", None) 633 name = self._checkStringVariable("name", None) 634 if not name: 635 raise http.HttpBadRequest("Missing the required 'name'" 636 " parameter") 637 638 return ({}, { 639 "node_name": self.items[0], 640 "storage_type": storage_type, 641 "name": name, 642 })
643
644 645 -class R_2_groups(baserlib.OpcodeResource):
646 """/2/groups resource. 647 648 """ 649 GET_OPCODE = opcodes.OpGroupQuery 650 POST_OPCODE = opcodes.OpGroupAdd 651 POST_RENAME = { 652 "name": "group_name", 653 } 654
655 - def GetPostOpInput(self):
656 """Create a node group. 657 658 """ 659 assert not self.items 660 return (self.request_body, { 661 "dry_run": self.dryRun(), 662 })
663
664 - def GET(self):
665 """Returns a list of all node groups. 666 667 """ 668 client = self.GetClient() 669 670 if self.useBulk(): 671 bulkdata = client.QueryGroups([], G_FIELDS, False) 672 return baserlib.MapBulkFields(bulkdata, G_FIELDS) 673 else: 674 data = client.QueryGroups([], ["name"], False) 675 groupnames = [row[0] for row in data] 676 return baserlib.BuildUriList(groupnames, "/2/groups/%s", 677 uri_fields=("name", "uri"))
678
679 680 -class R_2_groups_name(baserlib.OpcodeResource):
681 """/2/groups/[group_name] resource. 682 683 """ 684 DELETE_OPCODE = opcodes.OpGroupRemove 685
686 - def GET(self):
687 """Send information about a node group. 688 689 """ 690 group_name = self.items[0] 691 client = self.GetClient() 692 693 result = baserlib.HandleItemQueryErrors(client.QueryGroups, 694 names=[group_name], fields=G_FIELDS, 695 use_locking=self.useLocking()) 696 697 return baserlib.MapFields(G_FIELDS, result[0])
698
699 - def GetDeleteOpInput(self):
700 """Delete a node group. 701 702 """ 703 assert len(self.items) == 1 704 return ({}, { 705 "group_name": self.items[0], 706 "dry_run": self.dryRun(), 707 })
708
709 710 -class R_2_groups_name_modify(baserlib.OpcodeResource):
711 """/2/groups/[group_name]/modify resource. 712 713 """ 714 PUT_OPCODE = opcodes.OpGroupSetParams 715
716 - def GetPutOpInput(self):
717 """Changes some parameters of node group. 718 719 """ 720 assert self.items 721 return (self.request_body, { 722 "group_name": self.items[0], 723 })
724
725 726 -class R_2_groups_name_rename(baserlib.OpcodeResource):
727 """/2/groups/[group_name]/rename resource. 728 729 """ 730 PUT_OPCODE = opcodes.OpGroupRename 731
732 - def GetPutOpInput(self):
733 """Changes the name of a node group. 734 735 """ 736 assert len(self.items) == 1 737 return (self.request_body, { 738 "group_name": self.items[0], 739 "dry_run": self.dryRun(), 740 })
741
742 743 -class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
744 """/2/groups/[group_name]/assign-nodes resource. 745 746 """ 747 PUT_OPCODE = opcodes.OpGroupAssignNodes 748
749 - def GetPutOpInput(self):
750 """Assigns nodes to a group. 751 752 """ 753 assert len(self.items) == 1 754 return (self.request_body, { 755 "group_name": self.items[0], 756 "dry_run": self.dryRun(), 757 "force": self.useForce(), 758 })
759
760 761 -class R_2_instances(baserlib.OpcodeResource):
762 """/2/instances resource. 763 764 """ 765 GET_OPCODE = opcodes.OpInstanceQuery 766 POST_OPCODE = opcodes.OpInstanceCreate 767 POST_RENAME = { 768 "os": "os_type", 769 "name": "instance_name", 770 } 771
772 - def GET(self):
773 """Returns a list of all available instances. 774 775 """ 776 client = self.GetClient() 777 778 use_locking = self.useLocking() 779 if self.useBulk(): 780 bulkdata = client.QueryInstances([], I_FIELDS, use_locking) 781 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS)) 782 else: 783 instancesdata = client.QueryInstances([], ["name"], use_locking) 784 instanceslist = [row[0] for row in instancesdata] 785 return baserlib.BuildUriList(instanceslist, "/2/instances/%s", 786 uri_fields=("id", "uri"))
787
788 - def GetPostOpInput(self):
789 """Create an instance. 790 791 @return: a job id 792 793 """ 794 baserlib.CheckType(self.request_body, dict, "Body contents") 795 796 # Default to request data version 0 797 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0) 798 799 if data_version == 0: 800 raise http.HttpBadRequest("Instance creation request version 0 is no" 801 " longer supported") 802 elif data_version != 1: 803 raise http.HttpBadRequest("Unsupported request data version %s" % 804 data_version) 805 806 data = self.request_body.copy() 807 # Remove "__version__" 808 data.pop(_REQ_DATA_VERSION, None) 809 810 return (data, { 811 "dry_run": self.dryRun(), 812 })
813
814 815 -class R_2_instances_name(baserlib.OpcodeResource):
816 """/2/instances/[instance_name] resource. 817 818 """ 819 GET_OPCODE = opcodes.OpInstanceQuery 820 DELETE_OPCODE = opcodes.OpInstanceRemove 821
822 - def GET(self):
823 """Send information about an instance. 824 825 """ 826 client = self.GetClient() 827 instance_name = self.items[0] 828 829 result = baserlib.HandleItemQueryErrors(client.QueryInstances, 830 names=[instance_name], 831 fields=I_FIELDS, 832 use_locking=self.useLocking()) 833 834 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
835
836 - def GetDeleteOpInput(self):
837 """Delete an instance. 838 839 """ 840 assert len(self.items) == 1 841 return ({}, { 842 "instance_name": self.items[0], 843 "ignore_failures": False, 844 "dry_run": self.dryRun(), 845 })
846
847 848 -class R_2_instances_name_info(baserlib.OpcodeResource):
849 """/2/instances/[instance_name]/info resource. 850 851 """ 852 GET_OPCODE = opcodes.OpInstanceQueryData 853
854 - def GetGetOpInput(self):
855 """Request detailed instance information. 856 857 """ 858 assert len(self.items) == 1 859 return ({}, { 860 "instances": [self.items[0]], 861 "static": bool(self._checkIntVariable("static", default=0)), 862 })
863
864 865 -class R_2_instances_name_reboot(baserlib.OpcodeResource):
866 """/2/instances/[instance_name]/reboot resource. 867 868 Implements an instance reboot. 869 870 """ 871 POST_OPCODE = opcodes.OpInstanceReboot 872
873 - def GetPostOpInput(self):
874 """Reboot an instance. 875 876 The URI takes type=[hard|soft|full] and 877 ignore_secondaries=[False|True] parameters. 878 879 """ 880 return ({}, { 881 "instance_name": self.items[0], 882 "reboot_type": 883 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0], 884 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")), 885 "dry_run": self.dryRun(), 886 })
887
888 889 -class R_2_instances_name_startup(baserlib.OpcodeResource):
890 """/2/instances/[instance_name]/startup resource. 891 892 Implements an instance startup. 893 894 """ 895 PUT_OPCODE = opcodes.OpInstanceStartup 896
897 - def GetPutOpInput(self):
898 """Startup an instance. 899 900 The URI takes force=[False|True] parameter to start the instance 901 if even if secondary disks are failing. 902 903 """ 904 return ({}, { 905 "instance_name": self.items[0], 906 "force": self.useForce(), 907 "dry_run": self.dryRun(), 908 "no_remember": bool(self._checkIntVariable("no_remember")), 909 })
910
911 912 -class R_2_instances_name_shutdown(baserlib.OpcodeResource):
913 """/2/instances/[instance_name]/shutdown resource. 914 915 Implements an instance shutdown. 916 917 """ 918 PUT_OPCODE = opcodes.OpInstanceShutdown 919
920 - def GetPutOpInput(self):
921 """Shutdown an instance. 922 923 """ 924 return (self.request_body, { 925 "instance_name": self.items[0], 926 "no_remember": bool(self._checkIntVariable("no_remember")), 927 "dry_run": self.dryRun(), 928 })
929
930 931 -def _ParseInstanceReinstallRequest(name, data):
932 """Parses a request for reinstalling an instance. 933 934 """ 935 if not isinstance(data, dict): 936 raise http.HttpBadRequest("Invalid body contents, not a dictionary") 937 938 ostype = baserlib.CheckParameter(data, "os", default=None) 939 start = baserlib.CheckParameter(data, "start", exptype=bool, 940 default=True) 941 osparams = baserlib.CheckParameter(data, "osparams", default=None) 942 943 ops = [ 944 opcodes.OpInstanceShutdown(instance_name=name), 945 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype, 946 osparams=osparams), 947 ] 948 949 if start: 950 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False)) 951 952 return ops
953
954 955 -class R_2_instances_name_reinstall(baserlib.OpcodeResource):
956 """/2/instances/[instance_name]/reinstall resource. 957 958 Implements an instance reinstall. 959 960 """ 961 POST_OPCODE = opcodes.OpInstanceReinstall 962
963 - def POST(self):
964 """Reinstall an instance. 965 966 The URI takes os=name and nostartup=[0|1] optional 967 parameters. By default, the instance will be started 968 automatically. 969 970 """ 971 if self.request_body: 972 if self.queryargs: 973 raise http.HttpBadRequest("Can't combine query and body parameters") 974 975 body = self.request_body 976 elif self.queryargs: 977 # Legacy interface, do not modify/extend 978 body = { 979 "os": self._checkStringVariable("os"), 980 "start": not self._checkIntVariable("nostartup"), 981 } 982 else: 983 body = {} 984 985 ops = _ParseInstanceReinstallRequest(self.items[0], body) 986 987 return self.SubmitJob(ops)
988
989 990 -class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
991 """/2/instances/[instance_name]/replace-disks resource. 992 993 """ 994 POST_OPCODE = opcodes.OpInstanceReplaceDisks 995
996 - def GetPostOpInput(self):
997 """Replaces disks on an instance. 998 999 """ 1000 static = { 1001 "instance_name": self.items[0], 1002 } 1003 1004 if self.request_body: 1005 data = self.request_body 1006 elif self.queryargs: 1007 # Legacy interface, do not modify/extend 1008 data = { 1009 "remote_node": self._checkStringVariable("remote_node", default=None), 1010 "mode": self._checkStringVariable("mode", default=None), 1011 "disks": self._checkStringVariable("disks", default=None), 1012 "iallocator": self._checkStringVariable("iallocator", default=None), 1013 } 1014 else: 1015 data = {} 1016 1017 # Parse disks 1018 try: 1019 raw_disks = data.pop("disks") 1020 except KeyError: 1021 pass 1022 else: 1023 if raw_disks: 1024 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102 1025 data["disks"] = raw_disks 1026 else: 1027 # Backwards compatibility for strings of the format "1, 2, 3" 1028 try: 1029 data["disks"] = [int(part) for part in raw_disks.split(",")] 1030 except (TypeError, ValueError), err: 1031 raise http.HttpBadRequest("Invalid disk index passed: %s" % err) 1032 1033 return (data, static)
1034
1035 1036 -class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1037 """/2/instances/[instance_name]/activate-disks resource. 1038 1039 """ 1040 PUT_OPCODE = opcodes.OpInstanceActivateDisks 1041
1042 - def GetPutOpInput(self):
1043 """Activate disks for an instance. 1044 1045 The URI might contain ignore_size to ignore current recorded size. 1046 1047 """ 1048 return ({}, { 1049 "instance_name": self.items[0], 1050 "ignore_size": bool(self._checkIntVariable("ignore_size")), 1051 })
1052
1053 1054 -class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1055 """/2/instances/[instance_name]/deactivate-disks resource. 1056 1057 """ 1058 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks 1059
1060 - def GetPutOpInput(self):
1061 """Deactivate disks for an instance. 1062 1063 """ 1064 return ({}, { 1065 "instance_name": self.items[0], 1066 })
1067
1068 1069 -class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1070 """/2/instances/[instance_name]/recreate-disks resource. 1071 1072 """ 1073 POST_OPCODE = opcodes.OpInstanceRecreateDisks 1074
1075 - def GetPostOpInput(self):
1076 """Recreate disks for an instance. 1077 1078 """ 1079 return ({}, { 1080 "instance_name": self.items[0], 1081 })
1082
1083 1084 -class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1085 """/2/instances/[instance_name]/prepare-export resource. 1086 1087 """ 1088 PUT_OPCODE = opcodes.OpBackupPrepare 1089
1090 - def GetPutOpInput(self):
1091 """Prepares an export for an instance. 1092 1093 """ 1094 return ({}, { 1095 "instance_name": self.items[0], 1096 "mode": self._checkStringVariable("mode"), 1097 })
1098
1099 1100 -class R_2_instances_name_export(baserlib.OpcodeResource):
1101 """/2/instances/[instance_name]/export resource. 1102 1103 """ 1104 PUT_OPCODE = opcodes.OpBackupExport 1105 PUT_RENAME = { 1106 "destination": "target_node", 1107 } 1108
1109 - def GetPutOpInput(self):
1110 """Exports an instance. 1111 1112 """ 1113 return (self.request_body, { 1114 "instance_name": self.items[0], 1115 })
1116
1117 1118 -class R_2_instances_name_migrate(baserlib.OpcodeResource):
1119 """/2/instances/[instance_name]/migrate resource. 1120 1121 """ 1122 PUT_OPCODE = opcodes.OpInstanceMigrate 1123
1124 - def GetPutOpInput(self):
1125 """Migrates an instance. 1126 1127 """ 1128 return (self.request_body, { 1129 "instance_name": self.items[0], 1130 })
1131
1132 1133 -class R_2_instances_name_failover(baserlib.OpcodeResource):
1134 """/2/instances/[instance_name]/failover resource. 1135 1136 """ 1137 PUT_OPCODE = opcodes.OpInstanceFailover 1138
1139 - def GetPutOpInput(self):
1140 """Does a failover of an instance. 1141 1142 """ 1143 return (self.request_body, { 1144 "instance_name": self.items[0], 1145 })
1146
1147 1148 -class R_2_instances_name_rename(baserlib.OpcodeResource):
1149 """/2/instances/[instance_name]/rename resource. 1150 1151 """ 1152 PUT_OPCODE = opcodes.OpInstanceRename 1153
1154 - def GetPutOpInput(self):
1155 """Changes the name of an instance. 1156 1157 """ 1158 return (self.request_body, { 1159 "instance_name": self.items[0], 1160 })
1161
1162 1163 -class R_2_instances_name_modify(baserlib.OpcodeResource):
1164 """/2/instances/[instance_name]/modify resource. 1165 1166 """ 1167 PUT_OPCODE = opcodes.OpInstanceSetParams 1168
1169 - def GetPutOpInput(self):
1170 """Changes parameters of an instance. 1171 1172 """ 1173 return (self.request_body, { 1174 "instance_name": self.items[0], 1175 })
1176
1177 1178 -class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1179 """/2/instances/[instance_name]/disk/[disk_index]/grow resource. 1180 1181 """ 1182 POST_OPCODE = opcodes.OpInstanceGrowDisk 1183
1184 - def GetPostOpInput(self):
1185 """Increases the size of an instance disk. 1186 1187 """ 1188 return (self.request_body, { 1189 "instance_name": self.items[0], 1190 "disk": int(self.items[1]), 1191 })
1192
1193 1194 -class R_2_instances_name_console(baserlib.ResourceBase):
1195 """/2/instances/[instance_name]/console resource. 1196 1197 """ 1198 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] 1199 GET_OPCODE = opcodes.OpInstanceConsole 1200
1201 - def GET(self):
1202 """Request information for connecting to instance's console. 1203 1204 @return: Serialized instance console description, see 1205 L{objects.InstanceConsole} 1206 1207 """ 1208 client = self.GetClient() 1209 1210 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False) 1211 1212 if console is None: 1213 raise http.HttpServiceUnavailable("Instance console unavailable") 1214 1215 assert isinstance(console, dict) 1216 return console
1217
1218 1219 -def _GetQueryFields(args):
1220 """ 1221 1222 """ 1223 try: 1224 fields = args["fields"] 1225 except KeyError: 1226 raise http.HttpBadRequest("Missing 'fields' query argument") 1227 1228 return _SplitQueryFields(fields[0])
1229
1230 1231 -def _SplitQueryFields(fields):
1232 """ 1233 1234 """ 1235 return [i.strip() for i in fields.split(",")]
1236
1237 1238 -class R_2_query(baserlib.ResourceBase):
1239 """/2/query/[resource] resource. 1240 1241 """ 1242 # Results might contain sensitive information 1243 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE] 1244 GET_OPCODE = opcodes.OpQuery 1245 PUT_OPCODE = opcodes.OpQuery 1246
1247 - def _Query(self, fields, qfilter):
1248 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1249
1250 - def GET(self):
1251 """Returns resource information. 1252 1253 @return: Query result, see L{objects.QueryResponse} 1254 1255 """ 1256 return self._Query(_GetQueryFields(self.queryargs), None)
1257
1258 - def PUT(self):
1259 """Submits job querying for resources. 1260 1261 @return: Query result, see L{objects.QueryResponse} 1262 1263 """ 1264 body = self.request_body 1265 1266 baserlib.CheckType(body, dict, "Body contents") 1267 1268 try: 1269 fields = body["fields"] 1270 except KeyError: 1271 fields = _GetQueryFields(self.queryargs) 1272 1273 qfilter = body.get("qfilter", None) 1274 # TODO: remove this after 2.7 1275 if qfilter is None: 1276 qfilter = body.get("filter", None) 1277 1278 return self._Query(fields, qfilter)
1279
1280 1281 -class R_2_query_fields(baserlib.ResourceBase):
1282 """/2/query/[resource]/fields resource. 1283 1284 """ 1285 GET_OPCODE = opcodes.OpQueryFields 1286
1287 - def GET(self):
1288 """Retrieves list of available fields for a resource. 1289 1290 @return: List of serialized L{objects.QueryFieldDefinition} 1291 1292 """ 1293 try: 1294 raw_fields = self.queryargs["fields"] 1295 except KeyError: 1296 fields = None 1297 else: 1298 fields = _SplitQueryFields(raw_fields[0]) 1299 1300 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1301
1302 1303 -class _R_Tags(baserlib.OpcodeResource):
1304 """Quasiclass for tagging resources. 1305 1306 Manages tags. When inheriting this class you must define the 1307 TAG_LEVEL for it. 1308 1309 """ 1310 TAG_LEVEL = None 1311 GET_OPCODE = opcodes.OpTagsGet 1312 PUT_OPCODE = opcodes.OpTagsSet 1313 DELETE_OPCODE = opcodes.OpTagsDel 1314
1315 - def __init__(self, items, queryargs, req, **kwargs):
1316 """A tag resource constructor. 1317 1318 We have to override the default to sort out cluster naming case. 1319 1320 """ 1321 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs) 1322 1323 if self.TAG_LEVEL == constants.TAG_CLUSTER: 1324 self.name = None 1325 else: 1326 self.name = items[0]
1327
1328 - def GET(self):
1329 """Returns a list of tags. 1330 1331 Example: ["tag1", "tag2", "tag3"] 1332 1333 """ 1334 kind = self.TAG_LEVEL 1335 1336 if kind in (constants.TAG_INSTANCE, 1337 constants.TAG_NODEGROUP, 1338 constants.TAG_NODE): 1339 if not self.name: 1340 raise http.HttpBadRequest("Missing name on tag request") 1341 1342 cl = self.GetClient() 1343 if kind == constants.TAG_INSTANCE: 1344 fn = cl.QueryInstances 1345 elif kind == constants.TAG_NODEGROUP: 1346 fn = cl.QueryGroups 1347 else: 1348 fn = cl.QueryNodes 1349 result = fn(names=[self.name], fields=["tags"], use_locking=False) 1350 if not result or not result[0]: 1351 raise http.HttpBadGateway("Invalid response from tag query") 1352 tags = result[0][0] 1353 1354 elif kind == constants.TAG_CLUSTER: 1355 assert not self.name 1356 # TODO: Use query API? 1357 ssc = ssconf.SimpleStore() 1358 tags = ssc.GetClusterTags() 1359 1360 return list(tags)
1361
1362 - def GetPutOpInput(self):
1363 """Add a set of tags. 1364 1365 The request as a list of strings should be PUT to this URI. And 1366 you'll have back a job id. 1367 1368 """ 1369 return ({}, { 1370 "kind": self.TAG_LEVEL, 1371 "name": self.name, 1372 "tags": self.queryargs.get("tag", []), 1373 "dry_run": self.dryRun(), 1374 })
1375
1376 - def GetDeleteOpInput(self):
1377 """Delete a tag. 1378 1379 In order to delete a set of tags, the DELETE 1380 request should be addressed to URI like: 1381 /tags?tag=[tag]&tag=[tag] 1382 1383 """ 1384 # Re-use code 1385 return self.GetPutOpInput()
1386
1387 1388 -class R_2_instances_name_tags(_R_Tags):
1389 """ /2/instances/[instance_name]/tags resource. 1390 1391 Manages per-instance tags. 1392 1393 """ 1394 TAG_LEVEL = constants.TAG_INSTANCE
1395
1396 1397 -class R_2_nodes_name_tags(_R_Tags):
1398 """ /2/nodes/[node_name]/tags resource. 1399 1400 Manages per-node tags. 1401 1402 """ 1403 TAG_LEVEL = constants.TAG_NODE
1404
1405 1406 -class R_2_groups_name_tags(_R_Tags):
1407 """ /2/groups/[group_name]/tags resource. 1408 1409 Manages per-nodegroup tags. 1410 1411 """ 1412 TAG_LEVEL = constants.TAG_NODEGROUP
1413
1414 1415 -class R_2_tags(_R_Tags):
1416 """ /2/tags resource. 1417 1418 Manages cluster tags. 1419 1420 """ 1421 TAG_LEVEL = constants.TAG_CLUSTER
1422