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.rapi import baserlib
68 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
69 I_FIELDS = ["name", "admin_state", "os",
72 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
74 "disk.sizes", "disk_usage",
75 "beparams", "hvparams",
76 "oper_state", "oper_ram", "oper_vcpus", "status",
77 "custom_hvparams", "custom_beparams", "custom_nicparams",
80 N_FIELDS = ["name", "offline", "master_candidate", "drained",
82 "mtotal", "mnode", "mfree",
83 "pinst_cnt", "sinst_cnt",
84 "ctotal", "cnodes", "csockets",
86 "pinst_list", "sinst_list",
87 "master_capable", "vm_capable",
99 "id", "ops", "status", "summary",
101 "received_ts", "start_ts", "end_ts",
104 J_FIELDS = J_FIELDS_BULK + [
109 _NR_DRAINED = "drained"
110 _NR_MASTER_CANDIATE = "master-candidate"
111 _NR_MASTER = "master"
112 _NR_OFFLINE = "offline"
113 _NR_REGULAR = "regular"
116 constants.NR_MASTER: _NR_MASTER,
117 constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
118 constants.NR_DRAINED: _NR_DRAINED,
119 constants.NR_OFFLINE: _NR_OFFLINE,
120 constants.NR_REGULAR: _NR_REGULAR,
123 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
125 # Request data version field
126 _REQ_DATA_VERSION = "__version__"
128 # Feature string for instance creation request data version 1
129 _INST_CREATE_REQV1 = "instance-create-reqv1"
131 # Feature string for instance reinstall request version 1
132 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
134 # Feature string for node migration version 1
135 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
137 # Feature string for node evacuation with LU-generated jobs
138 _NODE_EVAC_RES1 = "node-evac-res1"
140 ALL_FEATURES = frozenset([
142 _INST_REINSTALL_REQV1,
147 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
151 class R_version(baserlib.R_Generic):
152 """/version resource.
154 This resource should be used to determine the remote API version and
155 to adapt clients accordingly.
160 """Returns the remote API version.
163 return constants.RAPI_VERSION
166 class R_2_info(baserlib.R_Generic):
172 """Returns cluster information.
175 client = baserlib.GetClient()
176 return client.QueryClusterInfo()
179 class R_2_features(baserlib.R_Generic):
180 """/2/features resource.
185 """Returns list of optional RAPI features implemented.
188 return list(ALL_FEATURES)
191 class R_2_os(baserlib.R_Generic):
197 """Return a list of all OSes.
199 Can return error 500 in case of a problem.
201 Example: ["debian-etch"]
204 cl = baserlib.GetClient()
205 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
206 job_id = baserlib.SubmitJob([op], cl)
207 # we use custom feedback function, instead of print we log the status
208 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
209 diagnose_data = result[0]
211 if not isinstance(diagnose_data, list):
212 raise http.HttpBadGateway(message="Can't get OS list")
215 for (name, variants) in diagnose_data:
216 os_names.extend(cli.CalculateOSNames(name, variants))
221 class R_2_redist_config(baserlib.R_Generic):
222 """/2/redistribute-config resource.
227 """Redistribute configuration to all nodes.
230 return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
233 class R_2_cluster_modify(baserlib.R_Generic):
234 """/2/modify resource.
238 """Modifies cluster parameters.
243 op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
246 return baserlib.SubmitJob([op])
249 class R_2_jobs(baserlib.R_Generic):
254 """Returns a dictionary of jobs.
256 @return: a dictionary with jobs id and uri.
259 client = baserlib.GetClient()
262 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
263 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
265 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
266 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
267 uri_fields=("id", "uri"))
270 class R_2_jobs_id(baserlib.R_Generic):
271 """/2/jobs/[job_id] resource.
275 """Returns a job status.
277 @return: a dictionary with job parameters.
279 - id: job ID as a number
280 - status: current job status as a string
281 - ops: involved OpCodes as a list of dictionaries for each
283 - opstatus: OpCodes status as a list
284 - opresult: OpCodes results as a list of lists
287 job_id = self.items[0]
288 result = baserlib.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
290 raise http.HttpNotFound()
291 return baserlib.MapFields(J_FIELDS, result)
294 """Cancel not-yet-started job.
297 job_id = self.items[0]
298 result = baserlib.GetClient().CancelJob(job_id)
302 class R_2_jobs_id_wait(baserlib.R_Generic):
303 """/2/jobs/[job_id]/wait resource.
306 # WaitForJobChange provides access to sensitive information and blocks
307 # machine resources (it's a blocking RAPI call), hence restricting access.
308 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
311 """Waits for job changes.
314 job_id = self.items[0]
316 fields = self.getBodyParameter("fields")
317 prev_job_info = self.getBodyParameter("previous_job_info", None)
318 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
320 if not isinstance(fields, list):
321 raise http.HttpBadRequest("The 'fields' parameter should be a list")
323 if not (prev_job_info is None or isinstance(prev_job_info, list)):
324 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
327 if not (prev_log_serial is None or
328 isinstance(prev_log_serial, (int, long))):
329 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
332 client = baserlib.GetClient()
333 result = client.WaitForJobChangeOnce(job_id, fields,
334 prev_job_info, prev_log_serial,
335 timeout=_WFJC_TIMEOUT)
337 raise http.HttpNotFound()
339 if result == constants.JOB_NOTCHANGED:
343 (job_info, log_entries) = result
346 "job_info": job_info,
347 "log_entries": log_entries,
351 class R_2_nodes(baserlib.R_Generic):
352 """/2/nodes resource.
356 """Returns a list of all nodes.
359 client = baserlib.GetClient()
362 bulkdata = client.QueryNodes([], N_FIELDS, False)
363 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
365 nodesdata = client.QueryNodes([], ["name"], False)
366 nodeslist = [row[0] for row in nodesdata]
367 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
368 uri_fields=("id", "uri"))
371 class R_2_nodes_name(baserlib.R_Generic):
372 """/2/nodes/[node_name] resource.
376 """Send information about a node.
379 node_name = self.items[0]
380 client = baserlib.GetClient()
382 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
383 names=[node_name], fields=N_FIELDS,
384 use_locking=self.useLocking())
386 return baserlib.MapFields(N_FIELDS, result[0])
389 class R_2_nodes_name_role(baserlib.R_Generic):
390 """ /2/nodes/[node_name]/role resource.
394 """Returns the current node role.
399 node_name = self.items[0]
400 client = baserlib.GetClient()
401 result = client.QueryNodes(names=[node_name], fields=["role"],
402 use_locking=self.useLocking())
404 return _NR_MAP[result[0][0]]
407 """Sets the node role.
412 if not isinstance(self.request_body, basestring):
413 raise http.HttpBadRequest("Invalid body contents, not a string")
415 node_name = self.items[0]
416 role = self.request_body
418 auto_promote = bool(self._checkIntVariable("auto-promote"))
420 if role == _NR_REGULAR:
425 elif role == _NR_MASTER_CANDIATE:
427 offline = drained = None
429 elif role == _NR_DRAINED:
431 candidate = offline = None
433 elif role == _NR_OFFLINE:
435 candidate = drained = None
438 raise http.HttpBadRequest("Can't set '%s' role" % role)
440 op = opcodes.OpNodeSetParams(node_name=node_name,
441 master_candidate=candidate,
444 auto_promote=auto_promote,
445 force=bool(self.useForce()))
447 return baserlib.SubmitJob([op])
450 class R_2_nodes_name_evacuate(baserlib.R_Generic):
451 """/2/nodes/[node_name]/evacuate resource.
455 """Evacuate all instances off a node.
458 op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
459 "node_name": self.items[0],
460 "dry_run": self.dryRun(),
463 return baserlib.SubmitJob([op])
466 class R_2_nodes_name_migrate(baserlib.R_Generic):
467 """/2/nodes/[node_name]/migrate resource.
471 """Migrate all primary instances from a node.
474 node_name = self.items[0]
477 # Support old-style requests
478 if "live" in self.queryargs and "mode" in self.queryargs:
479 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
482 if "live" in self.queryargs:
483 if self._checkIntVariable("live", default=1):
484 mode = constants.HT_MIGRATION_LIVE
486 mode = constants.HT_MIGRATION_NONLIVE
488 mode = self._checkStringVariable("mode", default=None)
494 data = self.request_body
496 op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
497 "node_name": node_name,
500 return baserlib.SubmitJob([op])
503 class R_2_nodes_name_modify(baserlib.R_Generic):
504 """/2/nodes/[node_name]/modify resource.
508 """Changes parameters of a node.
513 baserlib.CheckType(self.request_body, dict, "Body contents")
515 op = baserlib.FillOpcode(opcodes.OpNodeSetParams, self.request_body, {
516 "node_name": self.items[0],
519 return baserlib.SubmitJob([op])
522 class R_2_nodes_name_storage(baserlib.R_Generic):
523 """/2/nodes/[node_name]/storage resource.
526 # LUNodeQueryStorage acquires locks, hence restricting access to GET
527 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
530 node_name = self.items[0]
532 storage_type = self._checkStringVariable("storage_type", None)
534 raise http.HttpBadRequest("Missing the required 'storage_type'"
537 output_fields = self._checkStringVariable("output_fields", None)
538 if not output_fields:
539 raise http.HttpBadRequest("Missing the required 'output_fields'"
542 op = opcodes.OpNodeQueryStorage(nodes=[node_name],
543 storage_type=storage_type,
544 output_fields=output_fields.split(","))
545 return baserlib.SubmitJob([op])
548 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
549 """/2/nodes/[node_name]/storage/modify resource.
553 node_name = self.items[0]
555 storage_type = self._checkStringVariable("storage_type", None)
557 raise http.HttpBadRequest("Missing the required 'storage_type'"
560 name = self._checkStringVariable("name", None)
562 raise http.HttpBadRequest("Missing the required 'name'"
567 if "allocatable" in self.queryargs:
568 changes[constants.SF_ALLOCATABLE] = \
569 bool(self._checkIntVariable("allocatable", default=1))
571 op = opcodes.OpNodeModifyStorage(node_name=node_name,
572 storage_type=storage_type,
575 return baserlib.SubmitJob([op])
578 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
579 """/2/nodes/[node_name]/storage/repair resource.
583 node_name = self.items[0]
585 storage_type = self._checkStringVariable("storage_type", None)
587 raise http.HttpBadRequest("Missing the required 'storage_type'"
590 name = self._checkStringVariable("name", None)
592 raise http.HttpBadRequest("Missing the required 'name'"
595 op = opcodes.OpRepairNodeStorage(node_name=node_name,
596 storage_type=storage_type,
598 return baserlib.SubmitJob([op])
601 def _ParseCreateGroupRequest(data, dry_run):
602 """Parses a request for creating a node group.
604 @rtype: L{opcodes.OpGroupAdd}
605 @return: Group creation opcode
613 "name": "group_name",
616 return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
620 class R_2_groups(baserlib.R_Generic):
621 """/2/groups resource.
625 """Returns a list of all node groups.
628 client = baserlib.GetClient()
631 bulkdata = client.QueryGroups([], G_FIELDS, False)
632 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
634 data = client.QueryGroups([], ["name"], False)
635 groupnames = [row[0] for row in data]
636 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
637 uri_fields=("name", "uri"))
640 """Create a node group.
645 baserlib.CheckType(self.request_body, dict, "Body contents")
646 op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
647 return baserlib.SubmitJob([op])
650 class R_2_groups_name(baserlib.R_Generic):
651 """/2/groups/[group_name] resource.
655 """Send information about a node group.
658 group_name = self.items[0]
659 client = baserlib.GetClient()
661 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
662 names=[group_name], fields=G_FIELDS,
663 use_locking=self.useLocking())
665 return baserlib.MapFields(G_FIELDS, result[0])
668 """Delete a node group.
671 op = opcodes.OpGroupRemove(group_name=self.items[0],
672 dry_run=bool(self.dryRun()))
674 return baserlib.SubmitJob([op])
677 def _ParseModifyGroupRequest(name, data):
678 """Parses a request for modifying a node group.
680 @rtype: L{opcodes.OpGroupSetParams}
681 @return: Group modify opcode
684 return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
689 class R_2_groups_name_modify(baserlib.R_Generic):
690 """/2/groups/[group_name]/modify resource.
694 """Changes some parameters of node group.
699 baserlib.CheckType(self.request_body, dict, "Body contents")
701 op = _ParseModifyGroupRequest(self.items[0], self.request_body)
703 return baserlib.SubmitJob([op])
706 def _ParseRenameGroupRequest(name, data, dry_run):
707 """Parses a request for renaming a node group.
710 @param name: name of the node group to rename
712 @param data: the body received by the rename request
714 @param dry_run: whether to perform a dry run
716 @rtype: L{opcodes.OpGroupRename}
717 @return: Node group rename opcode
720 return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
726 class R_2_groups_name_rename(baserlib.R_Generic):
727 """/2/groups/[group_name]/rename resource.
731 """Changes the name of a node group.
736 baserlib.CheckType(self.request_body, dict, "Body contents")
737 op = _ParseRenameGroupRequest(self.items[0], self.request_body,
739 return baserlib.SubmitJob([op])
742 class R_2_groups_name_assign_nodes(baserlib.R_Generic):
743 """/2/groups/[group_name]/assign-nodes resource.
747 """Assigns nodes to a group.
752 op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
753 "group_name": self.items[0],
754 "dry_run": self.dryRun(),
755 "force": self.useForce(),
758 return baserlib.SubmitJob([op])
761 def _ParseInstanceCreateRequestVersion1(data, dry_run):
762 """Parses an instance creation request version 1.
764 @rtype: L{opcodes.OpInstanceCreate}
765 @return: Instance creation opcode
774 "name": "instance_name",
777 return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
781 class R_2_instances(baserlib.R_Generic):
782 """/2/instances resource.
786 """Returns a list of all available instances.
789 client = baserlib.GetClient()
791 use_locking = self.useLocking()
793 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
794 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
796 instancesdata = client.QueryInstances([], ["name"], use_locking)
797 instanceslist = [row[0] for row in instancesdata]
798 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
799 uri_fields=("id", "uri"))
802 """Create an instance.
807 if not isinstance(self.request_body, dict):
808 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
810 # Default to request data version 0
811 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
813 if data_version == 0:
814 raise http.HttpBadRequest("Instance creation request version 0 is no"
816 elif data_version == 1:
817 data = self.request_body.copy()
818 # Remove "__version__"
819 data.pop(_REQ_DATA_VERSION, None)
820 op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
822 raise http.HttpBadRequest("Unsupported request data version %s" %
825 return baserlib.SubmitJob([op])
828 class R_2_instances_name(baserlib.R_Generic):
829 """/2/instances/[instance_name] resource.
833 """Send information about an instance.
836 client = baserlib.GetClient()
837 instance_name = self.items[0]
839 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
840 names=[instance_name],
842 use_locking=self.useLocking())
844 return baserlib.MapFields(I_FIELDS, result[0])
847 """Delete an instance.
850 op = opcodes.OpInstanceRemove(instance_name=self.items[0],
851 ignore_failures=False,
852 dry_run=bool(self.dryRun()))
853 return baserlib.SubmitJob([op])
856 class R_2_instances_name_info(baserlib.R_Generic):
857 """/2/instances/[instance_name]/info resource.
861 """Request detailed instance information.
864 instance_name = self.items[0]
865 static = bool(self._checkIntVariable("static", default=0))
867 op = opcodes.OpInstanceQueryData(instances=[instance_name],
869 return baserlib.SubmitJob([op])
872 class R_2_instances_name_reboot(baserlib.R_Generic):
873 """/2/instances/[instance_name]/reboot resource.
875 Implements an instance reboot.
879 """Reboot an instance.
881 The URI takes type=[hard|soft|full] and
882 ignore_secondaries=[False|True] parameters.
885 instance_name = self.items[0]
886 reboot_type = self.queryargs.get("type",
887 [constants.INSTANCE_REBOOT_HARD])[0]
888 ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
889 op = opcodes.OpInstanceReboot(instance_name=instance_name,
890 reboot_type=reboot_type,
891 ignore_secondaries=ignore_secondaries,
892 dry_run=bool(self.dryRun()))
894 return baserlib.SubmitJob([op])
897 class R_2_instances_name_startup(baserlib.R_Generic):
898 """/2/instances/[instance_name]/startup resource.
900 Implements an instance startup.
904 """Startup an instance.
906 The URI takes force=[False|True] parameter to start the instance
907 if even if secondary disks are failing.
910 instance_name = self.items[0]
911 force_startup = bool(self._checkIntVariable("force"))
912 no_remember = bool(self._checkIntVariable("no_remember"))
913 op = opcodes.OpInstanceStartup(instance_name=instance_name,
915 dry_run=bool(self.dryRun()),
916 no_remember=no_remember)
918 return baserlib.SubmitJob([op])
921 def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
922 """Parses a request for an instance shutdown.
924 @rtype: L{opcodes.OpInstanceShutdown}
925 @return: Instance shutdown opcode
928 return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
929 "instance_name": name,
931 "no_remember": no_remember,
935 class R_2_instances_name_shutdown(baserlib.R_Generic):
936 """/2/instances/[instance_name]/shutdown resource.
938 Implements an instance shutdown.
942 """Shutdown an instance.
947 no_remember = bool(self._checkIntVariable("no_remember"))
948 op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
949 bool(self.dryRun()), no_remember)
951 return baserlib.SubmitJob([op])
954 def _ParseInstanceReinstallRequest(name, data):
955 """Parses a request for reinstalling an instance.
958 if not isinstance(data, dict):
959 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
961 ostype = baserlib.CheckParameter(data, "os", default=None)
962 start = baserlib.CheckParameter(data, "start", exptype=bool,
964 osparams = baserlib.CheckParameter(data, "osparams", default=None)
967 opcodes.OpInstanceShutdown(instance_name=name),
968 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
973 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
978 class R_2_instances_name_reinstall(baserlib.R_Generic):
979 """/2/instances/[instance_name]/reinstall resource.
981 Implements an instance reinstall.
985 """Reinstall an instance.
987 The URI takes os=name and nostartup=[0|1] optional
988 parameters. By default, the instance will be started
992 if self.request_body:
994 raise http.HttpBadRequest("Can't combine query and body parameters")
996 body = self.request_body
998 # Legacy interface, do not modify/extend
1000 "os": self._checkStringVariable("os"),
1001 "start": not self._checkIntVariable("nostartup"),
1006 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1008 return baserlib.SubmitJob(ops)
1011 def _ParseInstanceReplaceDisksRequest(name, data):
1012 """Parses a request for an instance export.
1014 @rtype: L{opcodes.OpInstanceReplaceDisks}
1015 @return: Instance export opcode
1019 "instance_name": name,
1024 raw_disks = data["disks"]
1028 if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1029 # Backwards compatibility for strings of the format "1, 2, 3"
1031 data["disks"] = [int(part) for part in raw_disks.split(",")]
1032 except (TypeError, ValueError), err:
1033 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1035 return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1038 class R_2_instances_name_replace_disks(baserlib.R_Generic):
1039 """/2/instances/[instance_name]/replace-disks resource.
1043 """Replaces disks on an instance.
1046 op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1048 return baserlib.SubmitJob([op])
1051 class R_2_instances_name_activate_disks(baserlib.R_Generic):
1052 """/2/instances/[instance_name]/activate-disks resource.
1056 """Activate disks for an instance.
1058 The URI might contain ignore_size to ignore current recorded size.
1061 instance_name = self.items[0]
1062 ignore_size = bool(self._checkIntVariable("ignore_size"))
1064 op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1065 ignore_size=ignore_size)
1067 return baserlib.SubmitJob([op])
1070 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1071 """/2/instances/[instance_name]/deactivate-disks resource.
1075 """Deactivate disks for an instance.
1078 instance_name = self.items[0]
1080 op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1082 return baserlib.SubmitJob([op])
1085 class R_2_instances_name_prepare_export(baserlib.R_Generic):
1086 """/2/instances/[instance_name]/prepare-export resource.
1090 """Prepares an export for an instance.
1095 instance_name = self.items[0]
1096 mode = self._checkStringVariable("mode")
1098 op = opcodes.OpBackupPrepare(instance_name=instance_name,
1101 return baserlib.SubmitJob([op])
1104 def _ParseExportInstanceRequest(name, data):
1105 """Parses a request for an instance export.
1107 @rtype: L{opcodes.OpBackupExport}
1108 @return: Instance export opcode
1111 # Rename "destination" to "target_node"
1113 data["target_node"] = data.pop("destination")
1117 return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1118 "instance_name": name,
1122 class R_2_instances_name_export(baserlib.R_Generic):
1123 """/2/instances/[instance_name]/export resource.
1127 """Exports an instance.
1132 if not isinstance(self.request_body, dict):
1133 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1135 op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1137 return baserlib.SubmitJob([op])
1140 def _ParseMigrateInstanceRequest(name, data):
1141 """Parses a request for an instance migration.
1143 @rtype: L{opcodes.OpInstanceMigrate}
1144 @return: Instance migration opcode
1147 return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1148 "instance_name": name,
1152 class R_2_instances_name_migrate(baserlib.R_Generic):
1153 """/2/instances/[instance_name]/migrate resource.
1157 """Migrates an instance.
1162 baserlib.CheckType(self.request_body, dict, "Body contents")
1164 op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1166 return baserlib.SubmitJob([op])
1169 class R_2_instances_name_failover(baserlib.R_Generic):
1170 """/2/instances/[instance_name]/failover resource.
1174 """Does a failover of an instance.
1179 baserlib.CheckType(self.request_body, dict, "Body contents")
1181 op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1182 "instance_name": self.items[0],
1185 return baserlib.SubmitJob([op])
1188 def _ParseRenameInstanceRequest(name, data):
1189 """Parses a request for renaming an instance.
1191 @rtype: L{opcodes.OpInstanceRename}
1192 @return: Instance rename opcode
1195 return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1196 "instance_name": name,
1200 class R_2_instances_name_rename(baserlib.R_Generic):
1201 """/2/instances/[instance_name]/rename resource.
1205 """Changes the name of an instance.
1210 baserlib.CheckType(self.request_body, dict, "Body contents")
1212 op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1214 return baserlib.SubmitJob([op])
1217 def _ParseModifyInstanceRequest(name, data):
1218 """Parses a request for modifying an instance.
1220 @rtype: L{opcodes.OpInstanceSetParams}
1221 @return: Instance modify opcode
1224 return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1225 "instance_name": name,
1229 class R_2_instances_name_modify(baserlib.R_Generic):
1230 """/2/instances/[instance_name]/modify resource.
1234 """Changes some parameters of an instance.
1239 baserlib.CheckType(self.request_body, dict, "Body contents")
1241 op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1243 return baserlib.SubmitJob([op])
1246 class R_2_instances_name_disk_grow(baserlib.R_Generic):
1247 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1251 """Increases the size of an instance disk.
1256 op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1257 "instance_name": self.items[0],
1258 "disk": int(self.items[1]),
1261 return baserlib.SubmitJob([op])
1264 class R_2_instances_name_console(baserlib.R_Generic):
1265 """/2/instances/[instance_name]/console resource.
1268 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1271 """Request information for connecting to instance's console.
1273 @return: Serialized instance console description, see
1274 L{objects.InstanceConsole}
1277 client = baserlib.GetClient()
1279 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1282 raise http.HttpServiceUnavailable("Instance console unavailable")
1284 assert isinstance(console, dict)
1288 def _GetQueryFields(args):
1293 fields = args["fields"]
1295 raise http.HttpBadRequest("Missing 'fields' query argument")
1297 return _SplitQueryFields(fields[0])
1300 def _SplitQueryFields(fields):
1304 return [i.strip() for i in fields.split(",")]
1307 class R_2_query(baserlib.R_Generic):
1308 """/2/query/[resource] resource.
1311 # Results might contain sensitive information
1312 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1314 def _Query(self, fields, filter_):
1315 return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1318 """Returns resource information.
1320 @return: Query result, see L{objects.QueryResponse}
1323 return self._Query(_GetQueryFields(self.queryargs), None)
1326 """Submits job querying for resources.
1328 @return: Query result, see L{objects.QueryResponse}
1331 body = self.request_body
1333 baserlib.CheckType(body, dict, "Body contents")
1336 fields = body["fields"]
1338 fields = _GetQueryFields(self.queryargs)
1340 return self._Query(fields, self.request_body.get("filter", None))
1343 class R_2_query_fields(baserlib.R_Generic):
1344 """/2/query/[resource]/fields resource.
1348 """Retrieves list of available fields for a resource.
1350 @return: List of serialized L{objects.QueryFieldDefinition}
1354 raw_fields = self.queryargs["fields"]
1358 fields = _SplitQueryFields(raw_fields[0])
1360 return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1363 class _R_Tags(baserlib.R_Generic):
1364 """ Quasiclass for tagging resources
1366 Manages tags. When inheriting this class you must define the
1372 def __init__(self, items, queryargs, req):
1373 """A tag resource constructor.
1375 We have to override the default to sort out cluster naming case.
1378 baserlib.R_Generic.__init__(self, items, queryargs, req)
1380 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1383 self.name = items[0]
1386 """Returns a list of tags.
1388 Example: ["tag1", "tag2", "tag3"]
1391 # pylint: disable=W0212
1392 return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1395 """Add a set of tags.
1397 The request as a list of strings should be PUT to this URI. And
1398 you'll have back a job id.
1401 # pylint: disable=W0212
1402 if "tag" not in self.queryargs:
1403 raise http.HttpBadRequest("Please specify tag(s) to add using the"
1404 " the 'tag' parameter")
1405 return baserlib._Tags_PUT(self.TAG_LEVEL,
1406 self.queryargs["tag"], name=self.name,
1407 dry_run=bool(self.dryRun()))
1412 In order to delete a set of tags, the DELETE
1413 request should be addressed to URI like:
1414 /tags?tag=[tag]&tag=[tag]
1417 # pylint: disable=W0212
1418 if "tag" not in self.queryargs:
1419 # no we not gonna delete all tags
1420 raise http.HttpBadRequest("Cannot delete all tags - please specify"
1421 " tag(s) using the 'tag' parameter")
1422 return baserlib._Tags_DELETE(self.TAG_LEVEL,
1423 self.queryargs["tag"],
1425 dry_run=bool(self.dryRun()))
1428 class R_2_instances_name_tags(_R_Tags):
1429 """ /2/instances/[instance_name]/tags resource.
1431 Manages per-instance tags.
1434 TAG_LEVEL = constants.TAG_INSTANCE
1437 class R_2_nodes_name_tags(_R_Tags):
1438 """ /2/nodes/[node_name]/tags resource.
1440 Manages per-node tags.
1443 TAG_LEVEL = constants.TAG_NODE
1446 class R_2_groups_name_tags(_R_Tags):
1447 """ /2/groups/[group_name]/tags resource.
1449 Manages per-nodegroup tags.
1452 TAG_LEVEL = constants.TAG_NODEGROUP
1455 class R_2_tags(_R_Tags):
1456 """ /2/tags resource.
1458 Manages cluster tags.
1461 TAG_LEVEL = constants.TAG_CLUSTER