4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
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.
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.
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
22 """Remote API resource implementations.
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.
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.
35 Quoting from RFC2616, section 9.6::
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
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
54 # pylint: disable=C0103
56 # C0103: Invalid name, since the R_* names are not conforming
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
64 from ganeti import compat
65 from ganeti import ssconf
66 from ganeti.rapi import baserlib
69 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70 I_FIELDS = ["name", "admin_state", "os",
73 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
75 "disk.sizes", "disk_usage",
76 "beparams", "hvparams",
77 "oper_state", "oper_ram", "oper_vcpus", "status",
78 "custom_hvparams", "custom_beparams", "custom_nicparams",
81 N_FIELDS = ["name", "offline", "master_candidate", "drained",
83 "mtotal", "mnode", "mfree",
84 "pinst_cnt", "sinst_cnt",
85 "ctotal", "cnodes", "csockets",
87 "pinst_list", "sinst_list",
88 "master_capable", "vm_capable",
100 "id", "ops", "status", "summary",
102 "received_ts", "start_ts", "end_ts",
105 J_FIELDS = J_FIELDS_BULK + [
110 _NR_DRAINED = "drained"
111 _NR_MASTER_CANDIDATE = "master-candidate"
112 _NR_MASTER = "master"
113 _NR_OFFLINE = "offline"
114 _NR_REGULAR = "regular"
117 constants.NR_MASTER: _NR_MASTER,
118 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
119 constants.NR_DRAINED: _NR_DRAINED,
120 constants.NR_OFFLINE: _NR_OFFLINE,
121 constants.NR_REGULAR: _NR_REGULAR,
124 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
126 # Request data version field
127 _REQ_DATA_VERSION = "__version__"
129 # Feature string for instance creation request data version 1
130 _INST_CREATE_REQV1 = "instance-create-reqv1"
132 # Feature string for instance reinstall request version 1
133 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
135 # Feature string for node migration version 1
136 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
138 # Feature string for node evacuation with LU-generated jobs
139 _NODE_EVAC_RES1 = "node-evac-res1"
141 ALL_FEATURES = frozenset([
143 _INST_REINSTALL_REQV1,
148 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
152 class R_root(baserlib.ResourceBase):
158 """Supported for legacy reasons.
170 class R_version(baserlib.ResourceBase):
171 """/version resource.
173 This resource should be used to determine the remote API version and
174 to adapt clients accordingly.
179 """Returns the remote API version.
182 return constants.RAPI_VERSION
185 class R_2_info(baserlib.OpcodeResource):
189 GET_OPCODE = opcodes.OpClusterQuery
192 """Returns cluster information.
195 client = self.GetClient()
196 return client.QueryClusterInfo()
199 class R_2_features(baserlib.ResourceBase):
200 """/2/features resource.
205 """Returns list of optional RAPI features implemented.
208 return list(ALL_FEATURES)
211 class R_2_os(baserlib.OpcodeResource):
215 GET_OPCODE = opcodes.OpOsDiagnose
218 """Return a list of all OSes.
220 Can return error 500 in case of a problem.
222 Example: ["debian-etch"]
225 cl = self.GetClient()
226 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
227 job_id = self.SubmitJob([op], cl=cl)
228 # we use custom feedback function, instead of print we log the status
229 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
230 diagnose_data = result[0]
232 if not isinstance(diagnose_data, list):
233 raise http.HttpBadGateway(message="Can't get OS list")
236 for (name, variants) in diagnose_data:
237 os_names.extend(cli.CalculateOSNames(name, variants))
242 class R_2_redist_config(baserlib.OpcodeResource):
243 """/2/redistribute-config resource.
246 PUT_OPCODE = opcodes.OpClusterRedistConf
249 class R_2_cluster_modify(baserlib.OpcodeResource):
250 """/2/modify resource.
253 PUT_OPCODE = opcodes.OpClusterSetParams
256 class R_2_jobs(baserlib.ResourceBase):
261 """Returns a dictionary of jobs.
263 @return: a dictionary with jobs id and uri.
266 client = self.GetClient()
269 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
270 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
272 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
273 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
274 uri_fields=("id", "uri"))
277 class R_2_jobs_id(baserlib.ResourceBase):
278 """/2/jobs/[job_id] resource.
282 """Returns a job status.
284 @return: a dictionary with job parameters.
286 - id: job ID as a number
287 - status: current job status as a string
288 - ops: involved OpCodes as a list of dictionaries for each
290 - opstatus: OpCodes status as a list
291 - opresult: OpCodes results as a list of lists
294 job_id = self.items[0]
295 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
297 raise http.HttpNotFound()
298 return baserlib.MapFields(J_FIELDS, result)
301 """Cancel not-yet-started job.
304 job_id = self.items[0]
305 result = self.GetClient().CancelJob(job_id)
309 class R_2_jobs_id_wait(baserlib.ResourceBase):
310 """/2/jobs/[job_id]/wait resource.
313 # WaitForJobChange provides access to sensitive information and blocks
314 # machine resources (it's a blocking RAPI call), hence restricting access.
315 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
318 """Waits for job changes.
321 job_id = self.items[0]
323 fields = self.getBodyParameter("fields")
324 prev_job_info = self.getBodyParameter("previous_job_info", None)
325 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
327 if not isinstance(fields, list):
328 raise http.HttpBadRequest("The 'fields' parameter should be a list")
330 if not (prev_job_info is None or isinstance(prev_job_info, list)):
331 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
334 if not (prev_log_serial is None or
335 isinstance(prev_log_serial, (int, long))):
336 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
339 client = self.GetClient()
340 result = client.WaitForJobChangeOnce(job_id, fields,
341 prev_job_info, prev_log_serial,
342 timeout=_WFJC_TIMEOUT)
344 raise http.HttpNotFound()
346 if result == constants.JOB_NOTCHANGED:
350 (job_info, log_entries) = result
353 "job_info": job_info,
354 "log_entries": log_entries,
358 class R_2_nodes(baserlib.OpcodeResource):
359 """/2/nodes resource.
362 GET_OPCODE = opcodes.OpNodeQuery
365 """Returns a list of all nodes.
368 client = self.GetClient()
371 bulkdata = client.QueryNodes([], N_FIELDS, False)
372 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
374 nodesdata = client.QueryNodes([], ["name"], False)
375 nodeslist = [row[0] for row in nodesdata]
376 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
377 uri_fields=("id", "uri"))
380 class R_2_nodes_name(baserlib.OpcodeResource):
381 """/2/nodes/[node_name] resource.
384 GET_OPCODE = opcodes.OpNodeQuery
387 """Send information about a node.
390 node_name = self.items[0]
391 client = self.GetClient()
393 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
394 names=[node_name], fields=N_FIELDS,
395 use_locking=self.useLocking())
397 return baserlib.MapFields(N_FIELDS, result[0])
400 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
401 """/2/nodes/[node_name]/powercycle resource.
404 POST_OPCODE = opcodes.OpNodePowercycle
406 def GetPostOpInput(self):
407 """Tries to powercycle a node.
410 return (self.request_body, {
411 "node_name": self.items[0],
412 "force": self.useForce(),
416 class R_2_nodes_name_role(baserlib.OpcodeResource):
417 """/2/nodes/[node_name]/role resource.
420 PUT_OPCODE = opcodes.OpNodeSetParams
423 """Returns the current node role.
428 node_name = self.items[0]
429 client = self.GetClient()
430 result = client.QueryNodes(names=[node_name], fields=["role"],
431 use_locking=self.useLocking())
433 return _NR_MAP[result[0][0]]
435 def GetPutOpInput(self):
436 """Sets the node role.
439 baserlib.CheckType(self.request_body, basestring, "Body contents")
441 role = self.request_body
443 if role == _NR_REGULAR:
448 elif role == _NR_MASTER_CANDIDATE:
450 offline = drained = None
452 elif role == _NR_DRAINED:
454 candidate = offline = None
456 elif role == _NR_OFFLINE:
458 candidate = drained = None
461 raise http.HttpBadRequest("Can't set '%s' role" % role)
463 assert len(self.items) == 1
466 "node_name": self.items[0],
467 "master_candidate": candidate,
470 "force": self.useForce(),
471 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
475 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
476 """/2/nodes/[node_name]/evacuate resource.
479 POST_OPCODE = opcodes.OpNodeEvacuate
481 def GetPostOpInput(self):
482 """Evacuate all instances off a node.
485 return (self.request_body, {
486 "node_name": self.items[0],
487 "dry_run": self.dryRun(),
491 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
492 """/2/nodes/[node_name]/migrate resource.
495 POST_OPCODE = opcodes.OpNodeMigrate
497 def GetPostOpInput(self):
498 """Migrate all primary instances from a node.
502 # Support old-style requests
503 if "live" in self.queryargs and "mode" in self.queryargs:
504 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
507 if "live" in self.queryargs:
508 if self._checkIntVariable("live", default=1):
509 mode = constants.HT_MIGRATION_LIVE
511 mode = constants.HT_MIGRATION_NONLIVE
513 mode = self._checkStringVariable("mode", default=None)
519 data = self.request_body
522 "node_name": self.items[0],
526 class R_2_nodes_name_modify(baserlib.OpcodeResource):
527 """/2/nodes/[node_name]/modify resource.
530 POST_OPCODE = opcodes.OpNodeSetParams
532 def GetPostOpInput(self):
533 """Changes parameters of a node.
536 assert len(self.items) == 1
538 return (self.request_body, {
539 "node_name": self.items[0],
543 class R_2_nodes_name_storage(baserlib.OpcodeResource):
544 """/2/nodes/[node_name]/storage resource.
547 # LUNodeQueryStorage acquires locks, hence restricting access to GET
548 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
549 GET_OPCODE = opcodes.OpNodeQueryStorage
551 def GetGetOpInput(self):
552 """List storage available on a node.
555 storage_type = self._checkStringVariable("storage_type", None)
556 output_fields = self._checkStringVariable("output_fields", None)
558 if not output_fields:
559 raise http.HttpBadRequest("Missing the required 'output_fields'"
563 "nodes": [self.items[0]],
564 "storage_type": storage_type,
565 "output_fields": output_fields.split(","),
569 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
570 """/2/nodes/[node_name]/storage/modify resource.
573 PUT_OPCODE = opcodes.OpNodeModifyStorage
575 def GetPutOpInput(self):
576 """Modifies a storage volume on a node.
579 storage_type = self._checkStringVariable("storage_type", None)
580 name = self._checkStringVariable("name", None)
583 raise http.HttpBadRequest("Missing the required 'name'"
588 if "allocatable" in self.queryargs:
589 changes[constants.SF_ALLOCATABLE] = \
590 bool(self._checkIntVariable("allocatable", default=1))
593 "node_name": self.items[0],
594 "storage_type": storage_type,
600 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
601 """/2/nodes/[node_name]/storage/repair resource.
604 PUT_OPCODE = opcodes.OpRepairNodeStorage
606 def GetPutOpInput(self):
607 """Repairs a storage volume on a node.
610 storage_type = self._checkStringVariable("storage_type", None)
611 name = self._checkStringVariable("name", None)
613 raise http.HttpBadRequest("Missing the required 'name'"
617 "node_name": self.items[0],
618 "storage_type": storage_type,
623 class R_2_groups(baserlib.OpcodeResource):
624 """/2/groups resource.
627 GET_OPCODE = opcodes.OpGroupQuery
628 POST_OPCODE = opcodes.OpGroupAdd
630 "name": "group_name",
633 def GetPostOpInput(self):
634 """Create a node group.
637 assert not self.items
638 return (self.request_body, {
639 "dry_run": self.dryRun(),
643 """Returns a list of all node groups.
646 client = self.GetClient()
649 bulkdata = client.QueryGroups([], G_FIELDS, False)
650 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
652 data = client.QueryGroups([], ["name"], False)
653 groupnames = [row[0] for row in data]
654 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
655 uri_fields=("name", "uri"))
658 class R_2_groups_name(baserlib.OpcodeResource):
659 """/2/groups/[group_name] resource.
662 DELETE_OPCODE = opcodes.OpGroupRemove
665 """Send information about a node group.
668 group_name = self.items[0]
669 client = self.GetClient()
671 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
672 names=[group_name], fields=G_FIELDS,
673 use_locking=self.useLocking())
675 return baserlib.MapFields(G_FIELDS, result[0])
677 def GetDeleteOpInput(self):
678 """Delete a node group.
681 assert len(self.items) == 1
683 "group_name": self.items[0],
684 "dry_run": self.dryRun(),
688 class R_2_groups_name_modify(baserlib.OpcodeResource):
689 """/2/groups/[group_name]/modify resource.
692 PUT_OPCODE = opcodes.OpGroupSetParams
694 def GetPutOpInput(self):
695 """Changes some parameters of node group.
699 return (self.request_body, {
700 "group_name": self.items[0],
704 class R_2_groups_name_rename(baserlib.OpcodeResource):
705 """/2/groups/[group_name]/rename resource.
708 PUT_OPCODE = opcodes.OpGroupRename
710 def GetPutOpInput(self):
711 """Changes the name of a node group.
714 assert len(self.items) == 1
715 return (self.request_body, {
716 "group_name": self.items[0],
717 "dry_run": self.dryRun(),
721 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
722 """/2/groups/[group_name]/assign-nodes resource.
725 PUT_OPCODE = opcodes.OpGroupAssignNodes
727 def GetPutOpInput(self):
728 """Assigns nodes to a group.
731 assert len(self.items) == 1
732 return (self.request_body, {
733 "group_name": self.items[0],
734 "dry_run": self.dryRun(),
735 "force": self.useForce(),
739 class R_2_instances(baserlib.OpcodeResource):
740 """/2/instances resource.
743 GET_OPCODE = opcodes.OpInstanceQuery
744 POST_OPCODE = opcodes.OpInstanceCreate
747 "name": "instance_name",
751 """Returns a list of all available instances.
754 client = self.GetClient()
756 use_locking = self.useLocking()
758 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
759 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
761 instancesdata = client.QueryInstances([], ["name"], use_locking)
762 instanceslist = [row[0] for row in instancesdata]
763 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
764 uri_fields=("id", "uri"))
766 def GetPostOpInput(self):
767 """Create an instance.
772 baserlib.CheckType(self.request_body, dict, "Body contents")
774 # Default to request data version 0
775 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
777 if data_version == 0:
778 raise http.HttpBadRequest("Instance creation request version 0 is no"
780 elif data_version != 1:
781 raise http.HttpBadRequest("Unsupported request data version %s" %
784 data = self.request_body.copy()
785 # Remove "__version__"
786 data.pop(_REQ_DATA_VERSION, None)
789 "dry_run": self.dryRun(),
793 class R_2_instances_name(baserlib.OpcodeResource):
794 """/2/instances/[instance_name] resource.
797 GET_OPCODE = opcodes.OpInstanceQuery
798 DELETE_OPCODE = opcodes.OpInstanceRemove
801 """Send information about an instance.
804 client = self.GetClient()
805 instance_name = self.items[0]
807 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
808 names=[instance_name],
810 use_locking=self.useLocking())
812 return baserlib.MapFields(I_FIELDS, result[0])
814 def GetDeleteOpInput(self):
815 """Delete an instance.
818 assert len(self.items) == 1
820 "instance_name": self.items[0],
821 "ignore_failures": False,
822 "dry_run": self.dryRun(),
826 class R_2_instances_name_info(baserlib.OpcodeResource):
827 """/2/instances/[instance_name]/info resource.
830 GET_OPCODE = opcodes.OpInstanceQueryData
832 def GetGetOpInput(self):
833 """Request detailed instance information.
836 assert len(self.items) == 1
838 "instances": [self.items[0]],
839 "static": bool(self._checkIntVariable("static", default=0)),
843 class R_2_instances_name_reboot(baserlib.OpcodeResource):
844 """/2/instances/[instance_name]/reboot resource.
846 Implements an instance reboot.
849 POST_OPCODE = opcodes.OpInstanceReboot
851 def GetPostOpInput(self):
852 """Reboot an instance.
854 The URI takes type=[hard|soft|full] and
855 ignore_secondaries=[False|True] parameters.
859 "instance_name": self.items[0],
861 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
862 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
863 "dry_run": self.dryRun(),
867 class R_2_instances_name_startup(baserlib.OpcodeResource):
868 """/2/instances/[instance_name]/startup resource.
870 Implements an instance startup.
873 PUT_OPCODE = opcodes.OpInstanceStartup
875 def GetPutOpInput(self):
876 """Startup an instance.
878 The URI takes force=[False|True] parameter to start the instance
879 if even if secondary disks are failing.
883 "instance_name": self.items[0],
884 "force": self.useForce(),
885 "dry_run": self.dryRun(),
886 "no_remember": bool(self._checkIntVariable("no_remember")),
890 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
891 """/2/instances/[instance_name]/shutdown resource.
893 Implements an instance shutdown.
896 PUT_OPCODE = opcodes.OpInstanceShutdown
898 def GetPutOpInput(self):
899 """Shutdown an instance.
902 return (self.request_body, {
903 "instance_name": self.items[0],
904 "no_remember": bool(self._checkIntVariable("no_remember")),
905 "dry_run": self.dryRun(),
909 def _ParseInstanceReinstallRequest(name, data):
910 """Parses a request for reinstalling an instance.
913 if not isinstance(data, dict):
914 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
916 ostype = baserlib.CheckParameter(data, "os", default=None)
917 start = baserlib.CheckParameter(data, "start", exptype=bool,
919 osparams = baserlib.CheckParameter(data, "osparams", default=None)
922 opcodes.OpInstanceShutdown(instance_name=name),
923 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
928 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
933 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
934 """/2/instances/[instance_name]/reinstall resource.
936 Implements an instance reinstall.
939 POST_OPCODE = opcodes.OpInstanceReinstall
942 """Reinstall an instance.
944 The URI takes os=name and nostartup=[0|1] optional
945 parameters. By default, the instance will be started
949 if self.request_body:
951 raise http.HttpBadRequest("Can't combine query and body parameters")
953 body = self.request_body
955 # Legacy interface, do not modify/extend
957 "os": self._checkStringVariable("os"),
958 "start": not self._checkIntVariable("nostartup"),
963 ops = _ParseInstanceReinstallRequest(self.items[0], body)
965 return self.SubmitJob(ops)
968 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
969 """/2/instances/[instance_name]/replace-disks resource.
972 POST_OPCODE = opcodes.OpInstanceReplaceDisks
974 def GetPostOpInput(self):
975 """Replaces disks on an instance.
978 data = self.request_body.copy()
980 "instance_name": self.items[0],
983 if self.request_body:
984 data = self.request_body
986 # Legacy interface, do not modify/extend
988 "remote_node": self._checkStringVariable("remote_node", default=None),
989 "mode": self._checkStringVariable("mode", default=None),
990 "disks": self._checkStringVariable("disks", default=None),
991 "iallocator": self._checkStringVariable("iallocator", default=None),
998 raw_disks = data.pop("disks")
1003 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1004 data["disks"] = raw_disks
1006 # Backwards compatibility for strings of the format "1, 2, 3"
1008 data["disks"] = [int(part) for part in raw_disks.split(",")]
1009 except (TypeError, ValueError), err:
1010 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1012 return (data, static)
1015 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1016 """/2/instances/[instance_name]/activate-disks resource.
1019 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1021 def GetPutOpInput(self):
1022 """Activate disks for an instance.
1024 The URI might contain ignore_size to ignore current recorded size.
1028 "instance_name": self.items[0],
1029 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1033 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1034 """/2/instances/[instance_name]/deactivate-disks resource.
1037 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1039 def GetPutOpInput(self):
1040 """Deactivate disks for an instance.
1044 "instance_name": self.items[0],
1048 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1049 """/2/instances/[instance_name]/recreate-disks resource.
1052 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1054 def GetPostOpInput(self):
1055 """Recreate disks for an instance.
1059 "instance_name": self.items[0],
1063 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1064 """/2/instances/[instance_name]/prepare-export resource.
1067 PUT_OPCODE = opcodes.OpBackupPrepare
1069 def GetPutOpInput(self):
1070 """Prepares an export for an instance.
1074 "instance_name": self.items[0],
1075 "mode": self._checkStringVariable("mode"),
1079 class R_2_instances_name_export(baserlib.OpcodeResource):
1080 """/2/instances/[instance_name]/export resource.
1083 PUT_OPCODE = opcodes.OpBackupExport
1085 "destination": "target_node",
1088 def GetPutOpInput(self):
1089 """Exports an instance.
1092 return (self.request_body, {
1093 "instance_name": self.items[0],
1097 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1098 """/2/instances/[instance_name]/migrate resource.
1101 PUT_OPCODE = opcodes.OpInstanceMigrate
1103 def GetPutOpInput(self):
1104 """Migrates an instance.
1107 return (self.request_body, {
1108 "instance_name": self.items[0],
1112 class R_2_instances_name_failover(baserlib.OpcodeResource):
1113 """/2/instances/[instance_name]/failover resource.
1116 PUT_OPCODE = opcodes.OpInstanceFailover
1118 def GetPutOpInput(self):
1119 """Does a failover of an instance.
1122 return (self.request_body, {
1123 "instance_name": self.items[0],
1127 class R_2_instances_name_rename(baserlib.OpcodeResource):
1128 """/2/instances/[instance_name]/rename resource.
1131 PUT_OPCODE = opcodes.OpInstanceRename
1133 def GetPutOpInput(self):
1134 """Changes the name of an instance.
1137 return (self.request_body, {
1138 "instance_name": self.items[0],
1142 class R_2_instances_name_modify(baserlib.OpcodeResource):
1143 """/2/instances/[instance_name]/modify resource.
1146 PUT_OPCODE = opcodes.OpInstanceSetParams
1148 def GetPutOpInput(self):
1149 """Changes parameters of an instance.
1152 return (self.request_body, {
1153 "instance_name": self.items[0],
1157 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1158 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1161 POST_OPCODE = opcodes.OpInstanceGrowDisk
1163 def GetPostOpInput(self):
1164 """Increases the size of an instance disk.
1167 return (self.request_body, {
1168 "instance_name": self.items[0],
1169 "disk": int(self.items[1]),
1173 class R_2_instances_name_console(baserlib.ResourceBase):
1174 """/2/instances/[instance_name]/console resource.
1177 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1178 GET_OPCODE = opcodes.OpInstanceConsole
1181 """Request information for connecting to instance's console.
1183 @return: Serialized instance console description, see
1184 L{objects.InstanceConsole}
1187 client = self.GetClient()
1189 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1192 raise http.HttpServiceUnavailable("Instance console unavailable")
1194 assert isinstance(console, dict)
1198 def _GetQueryFields(args):
1203 fields = args["fields"]
1205 raise http.HttpBadRequest("Missing 'fields' query argument")
1207 return _SplitQueryFields(fields[0])
1210 def _SplitQueryFields(fields):
1214 return [i.strip() for i in fields.split(",")]
1217 class R_2_query(baserlib.ResourceBase):
1218 """/2/query/[resource] resource.
1221 # Results might contain sensitive information
1222 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1223 GET_OPCODE = opcodes.OpQuery
1224 PUT_OPCODE = opcodes.OpQuery
1226 def _Query(self, fields, qfilter):
1227 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1230 """Returns resource information.
1232 @return: Query result, see L{objects.QueryResponse}
1235 return self._Query(_GetQueryFields(self.queryargs), None)
1238 """Submits job querying for resources.
1240 @return: Query result, see L{objects.QueryResponse}
1243 body = self.request_body
1245 baserlib.CheckType(body, dict, "Body contents")
1248 fields = body["fields"]
1250 fields = _GetQueryFields(self.queryargs)
1252 qfilter = body.get("qfilter", None)
1253 # TODO: remove this after 2.7
1255 qfilter = body.get("filter", None)
1257 return self._Query(fields, qfilter)
1260 class R_2_query_fields(baserlib.ResourceBase):
1261 """/2/query/[resource]/fields resource.
1264 GET_OPCODE = opcodes.OpQueryFields
1267 """Retrieves list of available fields for a resource.
1269 @return: List of serialized L{objects.QueryFieldDefinition}
1273 raw_fields = self.queryargs["fields"]
1277 fields = _SplitQueryFields(raw_fields[0])
1279 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1282 class _R_Tags(baserlib.OpcodeResource):
1283 """ Quasiclass for tagging resources
1285 Manages tags. When inheriting this class you must define the
1290 GET_OPCODE = opcodes.OpTagsGet
1291 PUT_OPCODE = opcodes.OpTagsSet
1292 DELETE_OPCODE = opcodes.OpTagsDel
1294 def __init__(self, items, queryargs, req, **kwargs):
1295 """A tag resource constructor.
1297 We have to override the default to sort out cluster naming case.
1300 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1302 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1305 self.name = items[0]
1308 """Returns a list of tags.
1310 Example: ["tag1", "tag2", "tag3"]
1313 kind = self.TAG_LEVEL
1315 if kind in (constants.TAG_INSTANCE,
1316 constants.TAG_NODEGROUP,
1317 constants.TAG_NODE):
1319 raise http.HttpBadRequest("Missing name on tag request")
1321 cl = self.GetClient()
1322 if kind == constants.TAG_INSTANCE:
1323 fn = cl.QueryInstances
1324 elif kind == constants.TAG_NODEGROUP:
1328 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1329 if not result or not result[0]:
1330 raise http.HttpBadGateway("Invalid response from tag query")
1333 elif kind == constants.TAG_CLUSTER:
1334 assert not self.name
1335 # TODO: Use query API?
1336 ssc = ssconf.SimpleStore()
1337 tags = ssc.GetClusterTags()
1341 def GetPutOpInput(self):
1342 """Add a set of tags.
1344 The request as a list of strings should be PUT to this URI. And
1345 you'll have back a job id.
1349 "kind": self.TAG_LEVEL,
1351 "tags": self.queryargs.get("tag", []),
1352 "dry_run": self.dryRun(),
1355 def GetDeleteOpInput(self):
1358 In order to delete a set of tags, the DELETE
1359 request should be addressed to URI like:
1360 /tags?tag=[tag]&tag=[tag]
1364 return self.GetPutOpInput()
1367 class R_2_instances_name_tags(_R_Tags):
1368 """ /2/instances/[instance_name]/tags resource.
1370 Manages per-instance tags.
1373 TAG_LEVEL = constants.TAG_INSTANCE
1376 class R_2_nodes_name_tags(_R_Tags):
1377 """ /2/nodes/[node_name]/tags resource.
1379 Manages per-node tags.
1382 TAG_LEVEL = constants.TAG_NODE
1385 class R_2_groups_name_tags(_R_Tags):
1386 """ /2/groups/[group_name]/tags resource.
1388 Manages per-nodegroup tags.
1391 TAG_LEVEL = constants.TAG_NODEGROUP
1394 class R_2_tags(_R_Tags):
1395 """ /2/tags resource.
1397 Manages cluster tags.
1400 TAG_LEVEL = constants.TAG_CLUSTER