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-msg=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_CANDIATE = "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_CANDIATE,
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.
164 class R_version(baserlib.ResourceBase):
165 """/version resource.
167 This resource should be used to determine the remote API version and
168 to adapt clients accordingly.
173 """Returns the remote API version.
176 return constants.RAPI_VERSION
179 class R_2_info(baserlib.ResourceBase):
184 """Returns cluster information.
187 client = self.GetClient()
188 return client.QueryClusterInfo()
191 class R_2_features(baserlib.ResourceBase):
192 """/2/features resource.
197 """Returns list of optional RAPI features implemented.
200 return list(ALL_FEATURES)
203 class R_2_os(baserlib.ResourceBase):
208 """Return a list of all OSes.
210 Can return error 500 in case of a problem.
212 Example: ["debian-etch"]
215 cl = self.GetClient()
216 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
217 job_id = self.SubmitJob([op], cl=cl)
218 # we use custom feedback function, instead of print we log the status
219 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
220 diagnose_data = result[0]
222 if not isinstance(diagnose_data, list):
223 raise http.HttpBadGateway(message="Can't get OS list")
226 for (name, variants) in diagnose_data:
227 os_names.extend(cli.CalculateOSNames(name, variants))
232 class R_2_redist_config(baserlib.OpcodeResource):
233 """/2/redistribute-config resource.
236 PUT_OPCODE = opcodes.OpClusterRedistConf
239 class R_2_cluster_modify(baserlib.OpcodeResource):
240 """/2/modify resource.
243 PUT_OPCODE = opcodes.OpClusterSetParams
246 class R_2_jobs(baserlib.ResourceBase):
251 """Returns a dictionary of jobs.
253 @return: a dictionary with jobs id and uri.
256 client = self.GetClient()
259 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
260 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
262 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
263 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
264 uri_fields=("id", "uri"))
267 class R_2_jobs_id(baserlib.ResourceBase):
268 """/2/jobs/[job_id] resource.
272 """Returns a job status.
274 @return: a dictionary with job parameters.
276 - id: job ID as a number
277 - status: current job status as a string
278 - ops: involved OpCodes as a list of dictionaries for each
280 - opstatus: OpCodes status as a list
281 - opresult: OpCodes results as a list of lists
284 job_id = self.items[0]
285 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
287 raise http.HttpNotFound()
288 return baserlib.MapFields(J_FIELDS, result)
291 """Cancel not-yet-started job.
294 job_id = self.items[0]
295 result = self.GetClient().CancelJob(job_id)
299 class R_2_jobs_id_wait(baserlib.ResourceBase):
300 """/2/jobs/[job_id]/wait resource.
303 # WaitForJobChange provides access to sensitive information and blocks
304 # machine resources (it's a blocking RAPI call), hence restricting access.
305 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
308 """Waits for job changes.
311 job_id = self.items[0]
313 fields = self.getBodyParameter("fields")
314 prev_job_info = self.getBodyParameter("previous_job_info", None)
315 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
317 if not isinstance(fields, list):
318 raise http.HttpBadRequest("The 'fields' parameter should be a list")
320 if not (prev_job_info is None or isinstance(prev_job_info, list)):
321 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
324 if not (prev_log_serial is None or
325 isinstance(prev_log_serial, (int, long))):
326 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
329 client = self.GetClient()
330 result = client.WaitForJobChangeOnce(job_id, fields,
331 prev_job_info, prev_log_serial,
332 timeout=_WFJC_TIMEOUT)
334 raise http.HttpNotFound()
336 if result == constants.JOB_NOTCHANGED:
340 (job_info, log_entries) = result
343 "job_info": job_info,
344 "log_entries": log_entries,
348 class R_2_nodes(baserlib.ResourceBase):
349 """/2/nodes resource.
353 """Returns a list of all nodes.
356 client = self.GetClient()
359 bulkdata = client.QueryNodes([], N_FIELDS, False)
360 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
362 nodesdata = client.QueryNodes([], ["name"], False)
363 nodeslist = [row[0] for row in nodesdata]
364 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
365 uri_fields=("id", "uri"))
368 class R_2_nodes_name(baserlib.ResourceBase):
369 """/2/nodes/[node_name] resource.
373 """Send information about a node.
376 node_name = self.items[0]
377 client = self.GetClient()
379 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
380 names=[node_name], fields=N_FIELDS,
381 use_locking=self.useLocking())
383 return baserlib.MapFields(N_FIELDS, result[0])
386 class R_2_nodes_name_role(baserlib.ResourceBase):
387 """ /2/nodes/[node_name]/role resource.
391 """Returns the current node role.
396 node_name = self.items[0]
397 client = self.GetClient()
398 result = client.QueryNodes(names=[node_name], fields=["role"],
399 use_locking=self.useLocking())
401 return _NR_MAP[result[0][0]]
404 """Sets the node role.
409 if not isinstance(self.request_body, basestring):
410 raise http.HttpBadRequest("Invalid body contents, not a string")
412 node_name = self.items[0]
413 role = self.request_body
415 if role == _NR_REGULAR:
420 elif role == _NR_MASTER_CANDIATE:
422 offline = drained = None
424 elif role == _NR_DRAINED:
426 candidate = offline = None
428 elif role == _NR_OFFLINE:
430 candidate = drained = None
433 raise http.HttpBadRequest("Can't set '%s' role" % role)
435 op = opcodes.OpNodeSetParams(node_name=node_name,
436 master_candidate=candidate,
439 force=bool(self.useForce()))
441 return self.SubmitJob([op])
444 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
445 """/2/nodes/[node_name]/evacuate resource.
448 POST_OPCODE = opcodes.OpNodeEvacuate
450 def GetPostOpInput(self):
451 """Evacuate all instances off a node.
454 return (self.request_body, {
455 "node_name": self.items[0],
456 "dry_run": self.dryRun(),
460 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
461 """/2/nodes/[node_name]/migrate resource.
464 POST_OPCODE = opcodes.OpNodeMigrate
466 def GetPostOpInput(self):
467 """Migrate all primary instances from a node.
471 # Support old-style requests
472 if "live" in self.queryargs and "mode" in self.queryargs:
473 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
476 if "live" in self.queryargs:
477 if self._checkIntVariable("live", default=1):
478 mode = constants.HT_MIGRATION_LIVE
480 mode = constants.HT_MIGRATION_NONLIVE
482 mode = self._checkStringVariable("mode", default=None)
488 data = self.request_body
491 "node_name": self.items[0],
495 class R_2_nodes_name_storage(baserlib.ResourceBase):
496 """/2/nodes/[node_name]/storage resource.
499 # LUNodeQueryStorage acquires locks, hence restricting access to GET
500 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
503 node_name = self.items[0]
505 storage_type = self._checkStringVariable("storage_type", None)
507 raise http.HttpBadRequest("Missing the required 'storage_type'"
510 output_fields = self._checkStringVariable("output_fields", None)
511 if not output_fields:
512 raise http.HttpBadRequest("Missing the required 'output_fields'"
515 op = opcodes.OpNodeQueryStorage(nodes=[node_name],
516 storage_type=storage_type,
517 output_fields=output_fields.split(","))
518 return self.SubmitJob([op])
521 class R_2_nodes_name_storage_modify(baserlib.ResourceBase):
522 """/2/nodes/[node_name]/storage/modify resource.
526 node_name = self.items[0]
528 storage_type = self._checkStringVariable("storage_type", None)
530 raise http.HttpBadRequest("Missing the required 'storage_type'"
533 name = self._checkStringVariable("name", None)
535 raise http.HttpBadRequest("Missing the required 'name'"
540 if "allocatable" in self.queryargs:
541 changes[constants.SF_ALLOCATABLE] = \
542 bool(self._checkIntVariable("allocatable", default=1))
544 op = opcodes.OpNodeModifyStorage(node_name=node_name,
545 storage_type=storage_type,
548 return self.SubmitJob([op])
551 class R_2_nodes_name_storage_repair(baserlib.ResourceBase):
552 """/2/nodes/[node_name]/storage/repair resource.
556 node_name = self.items[0]
558 storage_type = self._checkStringVariable("storage_type", None)
560 raise http.HttpBadRequest("Missing the required 'storage_type'"
563 name = self._checkStringVariable("name", None)
565 raise http.HttpBadRequest("Missing the required 'name'"
568 op = opcodes.OpRepairNodeStorage(node_name=node_name,
569 storage_type=storage_type,
571 return self.SubmitJob([op])
574 class R_2_groups(baserlib.OpcodeResource):
575 """/2/groups resource.
578 POST_OPCODE = opcodes.OpGroupAdd
580 "name": "group_name",
583 def GetPostOpInput(self):
584 """Create a node group.
587 assert not self.items
588 return (self.request_body, {
589 "dry_run": self.dryRun(),
593 """Returns a list of all node groups.
596 client = self.GetClient()
599 bulkdata = client.QueryGroups([], G_FIELDS, False)
600 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
602 data = client.QueryGroups([], ["name"], False)
603 groupnames = [row[0] for row in data]
604 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
605 uri_fields=("name", "uri"))
608 class R_2_groups_name(baserlib.ResourceBase):
609 """/2/groups/[group_name] resource.
613 """Send information about a node group.
616 group_name = self.items[0]
617 client = self.GetClient()
619 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
620 names=[group_name], fields=G_FIELDS,
621 use_locking=self.useLocking())
623 return baserlib.MapFields(G_FIELDS, result[0])
626 """Delete a node group.
629 op = opcodes.OpGroupRemove(group_name=self.items[0],
630 dry_run=bool(self.dryRun()))
632 return self.SubmitJob([op])
635 class R_2_groups_name_modify(baserlib.OpcodeResource):
636 """/2/groups/[group_name]/modify resource.
639 PUT_OPCODE = opcodes.OpGroupSetParams
641 def GetPutOpInput(self):
642 """Changes some parameters of node group.
646 return (self.request_body, {
647 "group_name": self.items[0],
651 class R_2_groups_name_rename(baserlib.OpcodeResource):
652 """/2/groups/[group_name]/rename resource.
655 PUT_OPCODE = opcodes.OpGroupRename
657 def GetPutOpInput(self):
658 """Changes the name of a node group.
661 assert len(self.items) == 1
662 return (self.request_body, {
663 "group_name": self.items[0],
664 "dry_run": self.dryRun(),
668 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
669 """/2/groups/[group_name]/assign-nodes resource.
672 PUT_OPCODE = opcodes.OpGroupAssignNodes
674 def GetPutOpInput(self):
675 """Assigns nodes to a group.
678 assert len(self.items) == 1
679 return (self.request_body, {
680 "group_name": self.items[0],
681 "dry_run": self.dryRun(),
682 "force": self.useForce(),
686 def _ParseInstanceCreateRequestVersion1(data, dry_run):
687 """Parses an instance creation request version 1.
689 @rtype: L{opcodes.OpInstanceCreate}
690 @return: Instance creation opcode
699 "name": "instance_name",
702 return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
706 class R_2_instances(baserlib.ResourceBase):
707 """/2/instances resource.
711 """Returns a list of all available instances.
714 client = self.GetClient()
716 use_locking = self.useLocking()
718 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
719 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
721 instancesdata = client.QueryInstances([], ["name"], use_locking)
722 instanceslist = [row[0] for row in instancesdata]
723 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
724 uri_fields=("id", "uri"))
727 """Create an instance.
732 if not isinstance(self.request_body, dict):
733 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
735 # Default to request data version 0
736 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
738 if data_version == 0:
739 raise http.HttpBadRequest("Instance creation request version 0 is no"
741 elif data_version == 1:
742 data = self.request_body.copy()
743 # Remove "__version__"
744 data.pop(_REQ_DATA_VERSION, None)
745 op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
747 raise http.HttpBadRequest("Unsupported request data version %s" %
750 return self.SubmitJob([op])
753 class R_2_instances_name(baserlib.OpcodeResource):
754 """/2/instances/[instance_name] resource.
757 DELETE_OPCODE = opcodes.OpInstanceRemove
760 """Send information about an instance.
763 client = self.GetClient()
764 instance_name = self.items[0]
766 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
767 names=[instance_name],
769 use_locking=self.useLocking())
771 return baserlib.MapFields(I_FIELDS, result[0])
773 def GetDeleteOpInput(self):
774 """Delete an instance.
777 assert len(self.items) == 1
779 "instance_name": self.items[0],
780 "ignore_failures": False,
781 "dry_run": self.dryRun(),
785 class R_2_instances_name_info(baserlib.OpcodeResource):
786 """/2/instances/[instance_name]/info resource.
789 GET_OPCODE = opcodes.OpInstanceQueryData
791 def GetGetOpInput(self):
792 """Request detailed instance information.
795 assert len(self.items) == 1
797 "instances": [self.items[0]],
798 "static": bool(self._checkIntVariable("static", default=0)),
802 class R_2_instances_name_reboot(baserlib.ResourceBase):
803 """/2/instances/[instance_name]/reboot resource.
805 Implements an instance reboot.
809 """Reboot an instance.
811 The URI takes type=[hard|soft|full] and
812 ignore_secondaries=[False|True] parameters.
815 instance_name = self.items[0]
816 reboot_type = self.queryargs.get("type",
817 [constants.INSTANCE_REBOOT_HARD])[0]
818 ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
819 op = opcodes.OpInstanceReboot(instance_name=instance_name,
820 reboot_type=reboot_type,
821 ignore_secondaries=ignore_secondaries,
822 dry_run=bool(self.dryRun()))
824 return self.SubmitJob([op])
827 class R_2_instances_name_startup(baserlib.ResourceBase):
828 """/2/instances/[instance_name]/startup resource.
830 Implements an instance startup.
834 """Startup an instance.
836 The URI takes force=[False|True] parameter to start the instance
837 if even if secondary disks are failing.
840 instance_name = self.items[0]
841 force_startup = bool(self._checkIntVariable("force"))
842 no_remember = bool(self._checkIntVariable("no_remember"))
843 op = opcodes.OpInstanceStartup(instance_name=instance_name,
845 dry_run=bool(self.dryRun()),
846 no_remember=no_remember)
848 return self.SubmitJob([op])
851 def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
852 """Parses a request for an instance shutdown.
854 @rtype: L{opcodes.OpInstanceShutdown}
855 @return: Instance shutdown opcode
858 return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
859 "instance_name": name,
861 "no_remember": no_remember,
865 class R_2_instances_name_shutdown(baserlib.ResourceBase):
866 """/2/instances/[instance_name]/shutdown resource.
868 Implements an instance shutdown.
872 """Shutdown an instance.
877 baserlib.CheckType(self.request_body, dict, "Body contents")
879 no_remember = bool(self._checkIntVariable("no_remember"))
880 op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
881 bool(self.dryRun()), no_remember)
883 return self.SubmitJob([op])
886 def _ParseInstanceReinstallRequest(name, data):
887 """Parses a request for reinstalling an instance.
890 if not isinstance(data, dict):
891 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
893 ostype = baserlib.CheckParameter(data, "os", default=None)
894 start = baserlib.CheckParameter(data, "start", exptype=bool,
896 osparams = baserlib.CheckParameter(data, "osparams", default=None)
899 opcodes.OpInstanceShutdown(instance_name=name),
900 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
905 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
910 class R_2_instances_name_reinstall(baserlib.ResourceBase):
911 """/2/instances/[instance_name]/reinstall resource.
913 Implements an instance reinstall.
917 """Reinstall an instance.
919 The URI takes os=name and nostartup=[0|1] optional
920 parameters. By default, the instance will be started
924 if self.request_body:
926 raise http.HttpBadRequest("Can't combine query and body parameters")
928 body = self.request_body
930 # Legacy interface, do not modify/extend
932 "os": self._checkStringVariable("os"),
933 "start": not self._checkIntVariable("nostartup"),
938 ops = _ParseInstanceReinstallRequest(self.items[0], body)
940 return self.SubmitJob(ops)
943 def _ParseInstanceReplaceDisksRequest(name, data):
944 """Parses a request for an instance export.
946 @rtype: L{opcodes.OpInstanceReplaceDisks}
947 @return: Instance export opcode
951 "instance_name": name,
956 raw_disks = data["disks"]
960 if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
961 # Backwards compatibility for strings of the format "1, 2, 3"
963 data["disks"] = [int(part) for part in raw_disks.split(",")]
964 except (TypeError, ValueError), err:
965 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
967 return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
970 class R_2_instances_name_replace_disks(baserlib.ResourceBase):
971 """/2/instances/[instance_name]/replace-disks resource.
975 """Replaces disks on an instance.
978 op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
980 return self.SubmitJob([op])
983 class R_2_instances_name_activate_disks(baserlib.ResourceBase):
984 """/2/instances/[instance_name]/activate-disks resource.
988 """Activate disks for an instance.
990 The URI might contain ignore_size to ignore current recorded size.
993 instance_name = self.items[0]
994 ignore_size = bool(self._checkIntVariable("ignore_size"))
996 op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
997 ignore_size=ignore_size)
999 return self.SubmitJob([op])
1002 class R_2_instances_name_deactivate_disks(baserlib.ResourceBase):
1003 """/2/instances/[instance_name]/deactivate-disks resource.
1007 """Deactivate disks for an instance.
1010 instance_name = self.items[0]
1012 op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1014 return self.SubmitJob([op])
1017 class R_2_instances_name_prepare_export(baserlib.ResourceBase):
1018 """/2/instances/[instance_name]/prepare-export resource.
1022 """Prepares an export for an instance.
1027 instance_name = self.items[0]
1028 mode = self._checkStringVariable("mode")
1030 op = opcodes.OpBackupPrepare(instance_name=instance_name,
1033 return self.SubmitJob([op])
1036 def _ParseExportInstanceRequest(name, data):
1037 """Parses a request for an instance export.
1039 @rtype: L{opcodes.OpBackupExport}
1040 @return: Instance export opcode
1043 # Rename "destination" to "target_node"
1045 data["target_node"] = data.pop("destination")
1049 return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1050 "instance_name": name,
1054 class R_2_instances_name_export(baserlib.ResourceBase):
1055 """/2/instances/[instance_name]/export resource.
1059 """Exports an instance.
1064 if not isinstance(self.request_body, dict):
1065 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1067 op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1069 return self.SubmitJob([op])
1072 def _ParseMigrateInstanceRequest(name, data):
1073 """Parses a request for an instance migration.
1075 @rtype: L{opcodes.OpInstanceMigrate}
1076 @return: Instance migration opcode
1079 return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1080 "instance_name": name,
1084 class R_2_instances_name_migrate(baserlib.ResourceBase):
1085 """/2/instances/[instance_name]/migrate resource.
1089 """Migrates an instance.
1094 baserlib.CheckType(self.request_body, dict, "Body contents")
1096 op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1098 return self.SubmitJob([op])
1101 class R_2_instances_name_failover(baserlib.ResourceBase):
1102 """/2/instances/[instance_name]/failover resource.
1106 """Does a failover of an instance.
1111 baserlib.CheckType(self.request_body, dict, "Body contents")
1113 op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
1114 "instance_name": self.items[0],
1117 return self.SubmitJob([op])
1120 def _ParseRenameInstanceRequest(name, data):
1121 """Parses a request for renaming an instance.
1123 @rtype: L{opcodes.OpInstanceRename}
1124 @return: Instance rename opcode
1127 return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1128 "instance_name": name,
1132 class R_2_instances_name_rename(baserlib.ResourceBase):
1133 """/2/instances/[instance_name]/rename resource.
1137 """Changes the name of an instance.
1142 baserlib.CheckType(self.request_body, dict, "Body contents")
1144 op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1146 return self.SubmitJob([op])
1149 def _ParseModifyInstanceRequest(name, data):
1150 """Parses a request for modifying an instance.
1152 @rtype: L{opcodes.OpInstanceSetParams}
1153 @return: Instance modify opcode
1156 return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1157 "instance_name": name,
1161 class R_2_instances_name_modify(baserlib.ResourceBase):
1162 """/2/instances/[instance_name]/modify resource.
1166 """Changes some parameters of an instance.
1171 baserlib.CheckType(self.request_body, dict, "Body contents")
1173 op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1175 return self.SubmitJob([op])
1178 class R_2_instances_name_disk_grow(baserlib.ResourceBase):
1179 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1183 """Increases the size of an instance disk.
1188 op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1189 "instance_name": self.items[0],
1190 "disk": int(self.items[1]),
1193 return self.SubmitJob([op])
1196 class R_2_instances_name_console(baserlib.ResourceBase):
1197 """/2/instances/[instance_name]/console resource.
1200 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1203 """Request information for connecting to instance's console.
1205 @return: Serialized instance console description, see
1206 L{objects.InstanceConsole}
1209 client = self.GetClient()
1211 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1214 raise http.HttpServiceUnavailable("Instance console unavailable")
1216 assert isinstance(console, dict)
1220 def _GetQueryFields(args):
1225 fields = args["fields"]
1227 raise http.HttpBadRequest("Missing 'fields' query argument")
1229 return _SplitQueryFields(fields[0])
1232 def _SplitQueryFields(fields):
1236 return [i.strip() for i in fields.split(",")]
1239 class R_2_query(baserlib.ResourceBase):
1240 """/2/query/[resource] resource.
1243 # Results might contain sensitive information
1244 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1246 def _Query(self, fields, filter_):
1247 return self.GetClient().Query(self.items[0], fields, filter_).ToDict()
1250 """Returns resource information.
1252 @return: Query result, see L{objects.QueryResponse}
1255 return self._Query(_GetQueryFields(self.queryargs), None)
1258 """Submits job querying for resources.
1260 @return: Query result, see L{objects.QueryResponse}
1263 body = self.request_body
1265 baserlib.CheckType(body, dict, "Body contents")
1268 fields = body["fields"]
1270 fields = _GetQueryFields(self.queryargs)
1272 return self._Query(fields, self.request_body.get("filter", None))
1275 class R_2_query_fields(baserlib.ResourceBase):
1276 """/2/query/[resource]/fields resource.
1280 """Retrieves list of available fields for a resource.
1282 @return: List of serialized L{objects.QueryFieldDefinition}
1286 raw_fields = self.queryargs["fields"]
1290 fields = _SplitQueryFields(raw_fields[0])
1292 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1295 class _R_Tags(baserlib.ResourceBase):
1296 """ Quasiclass for tagging resources
1298 Manages tags. When inheriting this class you must define the
1304 def __init__(self, items, queryargs, req):
1305 """A tag resource constructor.
1307 We have to override the default to sort out cluster naming case.
1310 baserlib.ResourceBase.__init__(self, items, queryargs, req)
1312 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1315 self.name = items[0]
1318 """Returns a list of tags.
1320 Example: ["tag1", "tag2", "tag3"]
1323 kind = self.TAG_LEVEL
1325 if kind in (constants.TAG_INSTANCE,
1326 constants.TAG_NODEGROUP,
1327 constants.TAG_NODE):
1329 raise http.HttpBadRequest("Missing name on tag request")
1331 cl = self.GetClient()
1332 if kind == constants.TAG_INSTANCE:
1333 fn = cl.QueryInstances
1334 elif kind == constants.TAG_NODEGROUP:
1338 result = fn(names=[self.name], fields=["tags"], use_locking=False)
1339 if not result or not result[0]:
1340 raise http.HttpBadGateway("Invalid response from tag query")
1343 elif kind == constants.TAG_CLUSTER:
1344 assert not self.name
1345 # TODO: Use query API?
1346 ssc = ssconf.SimpleStore()
1347 tags = ssc.GetClusterTags()
1352 """Add a set of tags.
1354 The request as a list of strings should be PUT to this URI. And
1355 you'll have back a job id.
1358 # pylint: disable-msg=W0212
1359 if "tag" not in self.queryargs:
1360 raise http.HttpBadRequest("Please specify tag(s) to add using the"
1361 " the 'tag' parameter")
1362 op = opcodes.OpTagsSet(kind=self.TAG_LEVEL, name=self.name,
1363 tags=self.queryargs["tag"], dry_run=self.dryRun())
1364 return self.SubmitJob([op])
1369 In order to delete a set of tags, the DELETE
1370 request should be addressed to URI like:
1371 /tags?tag=[tag]&tag=[tag]
1374 # pylint: disable-msg=W0212
1375 if "tag" not in self.queryargs:
1376 # no we not gonna delete all tags
1377 raise http.HttpBadRequest("Cannot delete all tags - please specify"
1378 " tag(s) using the 'tag' parameter")
1379 op = opcodes.OpTagsDel(kind=self.TAG_LEVEL, name=self.name,
1380 tags=self.queryargs["tag"], dry_run=self.dryRun())
1381 return self.SubmitJob([op])
1384 class R_2_instances_name_tags(_R_Tags):
1385 """ /2/instances/[instance_name]/tags resource.
1387 Manages per-instance tags.
1390 TAG_LEVEL = constants.TAG_INSTANCE
1393 class R_2_nodes_name_tags(_R_Tags):
1394 """ /2/nodes/[node_name]/tags resource.
1396 Manages per-node tags.
1399 TAG_LEVEL = constants.TAG_NODE
1402 class R_2_groups_name_tags(_R_Tags):
1403 """ /2/groups/[group_name]/tags resource.
1405 Manages per-nodegroup tags.
1408 TAG_LEVEL = constants.TAG_NODEGROUP
1411 class R_2_tags(_R_Tags):
1412 """ /2/tags resource.
1414 Manages cluster tags.
1417 TAG_LEVEL = constants.TAG_CLUSTER