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 version 2 baserlib.library.
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 To be in context of this module for instance creation POST on
32 /2/instances is legitim while PUT would be not, due to it does create a
33 new entity and not just replace /2/instances with it.
35 So when adding new methods, if they are operating on the URI entity itself,
36 PUT should be prefered over POST.
40 # pylint: disable-msg=C0103
42 # C0103: Invalid name, since the R_* names are not conforming
44 from ganeti import opcodes
45 from ganeti import http
46 from ganeti import constants
47 from ganeti import cli
48 from ganeti import rapi
50 from ganeti.rapi import baserlib
53 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
54 I_FIELDS = ["name", "admin_state", "os",
57 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
59 "disk.sizes", "disk_usage",
60 "beparams", "hvparams",
61 "oper_state", "oper_ram", "oper_vcpus", "status",
62 "custom_hvparams", "custom_beparams", "custom_nicparams",
65 N_FIELDS = ["name", "offline", "master_candidate", "drained",
67 "mtotal", "mnode", "mfree",
68 "pinst_cnt", "sinst_cnt",
69 "ctotal", "cnodes", "csockets",
71 "pinst_list", "sinst_list",
72 "master_capable", "vm_capable",
76 G_FIELDS = ["name", "uuid",
78 "node_cnt", "node_list",
79 "ctime", "mtime", "serial_no",
80 ] # "tags" is missing to be able to use _COMMON_FIELDS here.
82 _NR_DRAINED = "drained"
83 _NR_MASTER_CANDIATE = "master-candidate"
85 _NR_OFFLINE = "offline"
86 _NR_REGULAR = "regular"
89 constants.NR_MASTER: _NR_MASTER,
90 constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
91 constants.NR_DRAINED: _NR_DRAINED,
92 constants.NR_OFFLINE: _NR_OFFLINE,
93 constants.NR_REGULAR: _NR_REGULAR,
96 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
98 # Request data version field
99 _REQ_DATA_VERSION = "__version__"
101 # Feature string for instance creation request data version 1
102 _INST_CREATE_REQV1 = "instance-create-reqv1"
104 # Feature string for instance reinstall request version 1
105 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
107 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
111 class R_version(baserlib.R_Generic):
112 """/version resource.
114 This resource should be used to determine the remote API version and
115 to adapt clients accordingly.
120 """Returns the remote API version.
123 return constants.RAPI_VERSION
126 class R_2_info(baserlib.R_Generic):
132 """Returns cluster information.
135 client = baserlib.GetClient()
136 return client.QueryClusterInfo()
139 class R_2_features(baserlib.R_Generic):
140 """/2/features resource.
145 """Returns list of optional RAPI features implemented.
148 return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1]
151 class R_2_os(baserlib.R_Generic):
157 """Return a list of all OSes.
159 Can return error 500 in case of a problem.
161 Example: ["debian-etch"]
164 cl = baserlib.GetClient()
165 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
166 job_id = baserlib.SubmitJob([op], cl)
167 # we use custom feedback function, instead of print we log the status
168 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
169 diagnose_data = result[0]
171 if not isinstance(diagnose_data, list):
172 raise http.HttpBadGateway(message="Can't get OS list")
175 for (name, variants) in diagnose_data:
176 os_names.extend(cli.CalculateOSNames(name, variants))
181 class R_2_redist_config(baserlib.R_Generic):
182 """/2/redistribute-config resource.
187 """Redistribute configuration to all nodes.
190 return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
193 class R_2_cluster_modify(baserlib.R_Generic):
194 """/2/modify resource.
198 """Modifies cluster parameters.
203 op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
206 return baserlib.SubmitJob([op])
209 class R_2_jobs(baserlib.R_Generic):
215 """Returns a dictionary of jobs.
217 @return: a dictionary with jobs id and uri.
221 cl = baserlib.GetClient()
222 # Convert the list of lists to the list of ids
223 result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
224 return baserlib.BuildUriList(result, "/2/jobs/%s",
225 uri_fields=("id", "uri"))
228 class R_2_jobs_id(baserlib.R_Generic):
229 """/2/jobs/[job_id] resource.
233 """Returns a job status.
235 @return: a dictionary with job parameters.
237 - id: job ID as a number
238 - status: current job status as a string
239 - ops: involved OpCodes as a list of dictionaries for each
241 - opstatus: OpCodes status as a list
242 - opresult: OpCodes results as a list of lists
245 fields = ["id", "ops", "status", "summary",
246 "opstatus", "opresult", "oplog",
247 "received_ts", "start_ts", "end_ts",
249 job_id = self.items[0]
250 result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
252 raise http.HttpNotFound()
253 return baserlib.MapFields(fields, result)
256 """Cancel not-yet-started job.
259 job_id = self.items[0]
260 result = baserlib.GetClient().CancelJob(job_id)
264 class R_2_jobs_id_wait(baserlib.R_Generic):
265 """/2/jobs/[job_id]/wait resource.
268 # WaitForJobChange provides access to sensitive information and blocks
269 # machine resources (it's a blocking RAPI call), hence restricting access.
270 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
273 """Waits for job changes.
276 job_id = self.items[0]
278 fields = self.getBodyParameter("fields")
279 prev_job_info = self.getBodyParameter("previous_job_info", None)
280 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
282 if not isinstance(fields, list):
283 raise http.HttpBadRequest("The 'fields' parameter should be a list")
285 if not (prev_job_info is None or isinstance(prev_job_info, list)):
286 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
289 if not (prev_log_serial is None or
290 isinstance(prev_log_serial, (int, long))):
291 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
294 client = baserlib.GetClient()
295 result = client.WaitForJobChangeOnce(job_id, fields,
296 prev_job_info, prev_log_serial,
297 timeout=_WFJC_TIMEOUT)
299 raise http.HttpNotFound()
301 if result == constants.JOB_NOTCHANGED:
305 (job_info, log_entries) = result
308 "job_info": job_info,
309 "log_entries": log_entries,
313 class R_2_nodes(baserlib.R_Generic):
314 """/2/nodes resource.
318 """Returns a list of all nodes.
321 client = baserlib.GetClient()
324 bulkdata = client.QueryNodes([], N_FIELDS, False)
325 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
327 nodesdata = client.QueryNodes([], ["name"], False)
328 nodeslist = [row[0] for row in nodesdata]
329 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
330 uri_fields=("id", "uri"))
333 class R_2_nodes_name(baserlib.R_Generic):
334 """/2/nodes/[node_name] resource.
338 """Send information about a node.
341 node_name = self.items[0]
342 client = baserlib.GetClient()
344 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
345 names=[node_name], fields=N_FIELDS,
346 use_locking=self.useLocking())
348 return baserlib.MapFields(N_FIELDS, result[0])
351 class R_2_nodes_name_role(baserlib.R_Generic):
352 """ /2/nodes/[node_name]/role resource.
356 """Returns the current node role.
361 node_name = self.items[0]
362 client = baserlib.GetClient()
363 result = client.QueryNodes(names=[node_name], fields=["role"],
364 use_locking=self.useLocking())
366 return _NR_MAP[result[0][0]]
369 """Sets the node role.
374 if not isinstance(self.request_body, basestring):
375 raise http.HttpBadRequest("Invalid body contents, not a string")
377 node_name = self.items[0]
378 role = self.request_body
380 if role == _NR_REGULAR:
385 elif role == _NR_MASTER_CANDIATE:
387 offline = drained = None
389 elif role == _NR_DRAINED:
391 candidate = offline = None
393 elif role == _NR_OFFLINE:
395 candidate = drained = None
398 raise http.HttpBadRequest("Can't set '%s' role" % role)
400 op = opcodes.OpNodeSetParams(node_name=node_name,
401 master_candidate=candidate,
404 force=bool(self.useForce()))
406 return baserlib.SubmitJob([op])
409 class R_2_nodes_name_evacuate(baserlib.R_Generic):
410 """/2/nodes/[node_name]/evacuate resource.
414 """Evacuate all secondary instances off a node.
417 node_name = self.items[0]
418 remote_node = self._checkStringVariable("remote_node", default=None)
419 iallocator = self._checkStringVariable("iallocator", default=None)
420 early_r = bool(self._checkIntVariable("early_release", default=0))
421 dry_run = bool(self.dryRun())
423 cl = baserlib.GetClient()
425 op = opcodes.OpNodeEvacStrategy(nodes=[node_name],
426 iallocator=iallocator,
427 remote_node=remote_node)
429 job_id = baserlib.SubmitJob([op], cl)
430 # we use custom feedback function, instead of print we log the status
431 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
434 for iname, node in result[0]:
438 op = opcodes.OpInstanceReplaceDisks(instance_name=iname,
439 remote_node=node, disks=[],
440 mode=constants.REPLACE_DISK_CHG,
441 early_release=early_r)
442 jid = baserlib.SubmitJob([op])
443 jobs.append((jid, iname, node))
448 class R_2_nodes_name_migrate(baserlib.R_Generic):
449 """/2/nodes/[node_name]/migrate resource.
453 """Migrate all primary instances from a node.
456 node_name = self.items[0]
458 if "live" in self.queryargs and "mode" in self.queryargs:
459 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
461 elif "live" in self.queryargs:
462 if self._checkIntVariable("live", default=1):
463 mode = constants.HT_MIGRATION_LIVE
465 mode = constants.HT_MIGRATION_NONLIVE
467 mode = self._checkStringVariable("mode", default=None)
469 op = opcodes.OpNodeMigrate(node_name=node_name, mode=mode)
471 return baserlib.SubmitJob([op])
474 class R_2_nodes_name_storage(baserlib.R_Generic):
475 """/2/nodes/[node_name]/storage resource.
478 # LUNodeQueryStorage acquires locks, hence restricting access to GET
479 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
482 node_name = self.items[0]
484 storage_type = self._checkStringVariable("storage_type", None)
486 raise http.HttpBadRequest("Missing the required 'storage_type'"
489 output_fields = self._checkStringVariable("output_fields", None)
490 if not output_fields:
491 raise http.HttpBadRequest("Missing the required 'output_fields'"
494 op = opcodes.OpNodeQueryStorage(nodes=[node_name],
495 storage_type=storage_type,
496 output_fields=output_fields.split(","))
497 return baserlib.SubmitJob([op])
500 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
501 """/2/nodes/[node_name]/storage/modify resource.
505 node_name = self.items[0]
507 storage_type = self._checkStringVariable("storage_type", None)
509 raise http.HttpBadRequest("Missing the required 'storage_type'"
512 name = self._checkStringVariable("name", None)
514 raise http.HttpBadRequest("Missing the required 'name'"
519 if "allocatable" in self.queryargs:
520 changes[constants.SF_ALLOCATABLE] = \
521 bool(self._checkIntVariable("allocatable", default=1))
523 op = opcodes.OpNodeModifyStorage(node_name=node_name,
524 storage_type=storage_type,
527 return baserlib.SubmitJob([op])
530 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
531 """/2/nodes/[node_name]/storage/repair resource.
535 node_name = self.items[0]
537 storage_type = self._checkStringVariable("storage_type", None)
539 raise http.HttpBadRequest("Missing the required 'storage_type'"
542 name = self._checkStringVariable("name", None)
544 raise http.HttpBadRequest("Missing the required 'name'"
547 op = opcodes.OpRepairNodeStorage(node_name=node_name,
548 storage_type=storage_type,
550 return baserlib.SubmitJob([op])
553 def _ParseCreateGroupRequest(data, dry_run):
554 """Parses a request for creating a node group.
556 @rtype: L{opcodes.OpGroupAdd}
557 @return: Group creation opcode
565 "name": "group_name",
568 return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
572 class R_2_groups(baserlib.R_Generic):
573 """/2/groups resource.
577 """Returns a list of all node groups.
580 client = baserlib.GetClient()
583 bulkdata = client.QueryGroups([], G_FIELDS, False)
584 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
586 data = client.QueryGroups([], ["name"], False)
587 groupnames = [row[0] for row in data]
588 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
589 uri_fields=("name", "uri"))
592 """Create a node group.
597 baserlib.CheckType(self.request_body, dict, "Body contents")
598 op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
599 return baserlib.SubmitJob([op])
602 class R_2_groups_name(baserlib.R_Generic):
603 """/2/groups/[group_name] resource.
607 """Send information about a node group.
610 group_name = self.items[0]
611 client = baserlib.GetClient()
613 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
614 names=[group_name], fields=G_FIELDS,
615 use_locking=self.useLocking())
617 return baserlib.MapFields(G_FIELDS, result[0])
620 """Delete a node group.
623 op = opcodes.OpGroupRemove(group_name=self.items[0],
624 dry_run=bool(self.dryRun()))
626 return baserlib.SubmitJob([op])
629 def _ParseModifyGroupRequest(name, data):
630 """Parses a request for modifying a node group.
632 @rtype: L{opcodes.OpGroupSetParams}
633 @return: Group modify opcode
636 return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
642 class R_2_groups_name_modify(baserlib.R_Generic):
643 """/2/groups/[group_name]/modify resource.
647 """Changes some parameters of node group.
652 baserlib.CheckType(self.request_body, dict, "Body contents")
654 op = _ParseModifyGroupRequest(self.items[0], self.request_body)
656 return baserlib.SubmitJob([op])
659 def _ParseRenameGroupRequest(name, data, dry_run):
660 """Parses a request for renaming a node group.
663 @param name: name of the node group to rename
665 @param data: the body received by the rename request
667 @param dry_run: whether to perform a dry run
669 @rtype: L{opcodes.OpGroupRename}
670 @return: Node group rename opcode
673 return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
679 class R_2_groups_name_rename(baserlib.R_Generic):
680 """/2/groups/[group_name]/rename resource.
684 """Changes the name of a node group.
689 baserlib.CheckType(self.request_body, dict, "Body contents")
690 op = _ParseRenameGroupRequest(self.items[0], self.request_body,
692 return baserlib.SubmitJob([op])
695 class R_2_groups_name_assign_nodes(baserlib.R_Generic):
696 """/2/groups/[group_name]/assign-nodes resource.
700 """Assigns nodes to a group.
705 op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
706 "group_name": self.items[0],
707 "dry_run": self.dryRun(),
708 "force": self.useForce(),
711 return baserlib.SubmitJob([op])
714 def _ParseInstanceCreateRequestVersion1(data, dry_run):
715 """Parses an instance creation request version 1.
717 @rtype: L{opcodes.OpInstanceCreate}
718 @return: Instance creation opcode
727 "name": "instance_name",
730 return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
734 class R_2_instances(baserlib.R_Generic):
735 """/2/instances resource.
739 """Returns a list of all available instances.
742 client = baserlib.GetClient()
744 use_locking = self.useLocking()
746 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
747 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
749 instancesdata = client.QueryInstances([], ["name"], use_locking)
750 instanceslist = [row[0] for row in instancesdata]
751 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
752 uri_fields=("id", "uri"))
754 def _ParseVersion0CreateRequest(self):
755 """Parses an instance creation request version 0.
757 Request data version 0 is deprecated and should not be used anymore.
759 @rtype: L{opcodes.OpInstanceCreate}
760 @return: Instance creation opcode
763 # Do not modify anymore, request data version 0 is deprecated
764 beparams = baserlib.MakeParamsDict(self.request_body,
765 constants.BES_PARAMETERS)
766 hvparams = baserlib.MakeParamsDict(self.request_body,
767 constants.HVS_PARAMETERS)
768 fn = self.getBodyParameter
771 disk_data = fn('disks')
772 if not isinstance(disk_data, list):
773 raise http.HttpBadRequest("The 'disks' parameter should be a list")
775 for idx, d in enumerate(disk_data):
776 if not isinstance(d, int):
777 raise http.HttpBadRequest("Disk %d specification wrong: should"
778 " be an integer" % idx)
779 disks.append({"size": d})
781 # nic processing (one nic only)
782 nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
783 if fn("ip", None) is not None:
784 nics[0]["ip"] = fn("ip")
785 if fn("mode", None) is not None:
786 nics[0]["mode"] = fn("mode")
787 if fn("link", None) is not None:
788 nics[0]["link"] = fn("link")
790 # Do not modify anymore, request data version 0 is deprecated
791 return opcodes.OpInstanceCreate(
792 mode=constants.INSTANCE_CREATE,
793 instance_name=fn('name'),
795 disk_template=fn('disk_template'),
797 pnode=fn('pnode', None),
798 snode=fn('snode', None),
799 iallocator=fn('iallocator', None),
801 start=fn('start', True),
802 ip_check=fn('ip_check', True),
803 name_check=fn('name_check', True),
805 hypervisor=fn('hypervisor', None),
808 file_storage_dir=fn('file_storage_dir', None),
809 file_driver=fn('file_driver', constants.FD_LOOP),
810 dry_run=bool(self.dryRun()),
814 """Create an instance.
819 if not isinstance(self.request_body, dict):
820 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
822 # Default to request data version 0
823 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
825 if data_version == 0:
826 op = self._ParseVersion0CreateRequest()
827 elif data_version == 1:
828 data = self.request_body.copy()
829 # Remove "__version__"
830 data.pop(_REQ_DATA_VERSION, None)
831 op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
833 raise http.HttpBadRequest("Unsupported request data version %s" %
836 return baserlib.SubmitJob([op])
839 class R_2_instances_name(baserlib.R_Generic):
840 """/2/instances/[instance_name] resource.
844 """Send information about an instance.
847 client = baserlib.GetClient()
848 instance_name = self.items[0]
850 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
851 names=[instance_name],
853 use_locking=self.useLocking())
855 return baserlib.MapFields(I_FIELDS, result[0])
858 """Delete an instance.
861 op = opcodes.OpInstanceRemove(instance_name=self.items[0],
862 ignore_failures=False,
863 dry_run=bool(self.dryRun()))
864 return baserlib.SubmitJob([op])
867 class R_2_instances_name_info(baserlib.R_Generic):
868 """/2/instances/[instance_name]/info resource.
872 """Request detailed instance information.
875 instance_name = self.items[0]
876 static = bool(self._checkIntVariable("static", default=0))
878 op = opcodes.OpInstanceQueryData(instances=[instance_name],
880 return baserlib.SubmitJob([op])
883 class R_2_instances_name_reboot(baserlib.R_Generic):
884 """/2/instances/[instance_name]/reboot resource.
886 Implements an instance reboot.
890 """Reboot an instance.
892 The URI takes type=[hard|soft|full] and
893 ignore_secondaries=[False|True] parameters.
896 instance_name = self.items[0]
897 reboot_type = self.queryargs.get('type',
898 [constants.INSTANCE_REBOOT_HARD])[0]
899 ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
900 op = opcodes.OpInstanceReboot(instance_name=instance_name,
901 reboot_type=reboot_type,
902 ignore_secondaries=ignore_secondaries,
903 dry_run=bool(self.dryRun()))
905 return baserlib.SubmitJob([op])
908 class R_2_instances_name_startup(baserlib.R_Generic):
909 """/2/instances/[instance_name]/startup resource.
911 Implements an instance startup.
915 """Startup an instance.
917 The URI takes force=[False|True] parameter to start the instance
918 if even if secondary disks are failing.
921 instance_name = self.items[0]
922 force_startup = bool(self._checkIntVariable('force'))
923 op = opcodes.OpInstanceStartup(instance_name=instance_name,
925 dry_run=bool(self.dryRun()))
927 return baserlib.SubmitJob([op])
930 class R_2_instances_name_shutdown(baserlib.R_Generic):
931 """/2/instances/[instance_name]/shutdown resource.
933 Implements an instance shutdown.
937 """Shutdown an instance.
940 instance_name = self.items[0]
941 op = opcodes.OpInstanceShutdown(instance_name=instance_name,
942 dry_run=bool(self.dryRun()))
944 return baserlib.SubmitJob([op])
947 def _ParseInstanceReinstallRequest(name, data):
948 """Parses a request for reinstalling an instance.
951 if not isinstance(data, dict):
952 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
954 ostype = baserlib.CheckParameter(data, "os", default=None)
955 start = baserlib.CheckParameter(data, "start", exptype=bool,
957 osparams = baserlib.CheckParameter(data, "osparams", default=None)
960 opcodes.OpInstanceShutdown(instance_name=name),
961 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
966 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
971 class R_2_instances_name_reinstall(baserlib.R_Generic):
972 """/2/instances/[instance_name]/reinstall resource.
974 Implements an instance reinstall.
978 """Reinstall an instance.
980 The URI takes os=name and nostartup=[0|1] optional
981 parameters. By default, the instance will be started
985 if self.request_body:
987 raise http.HttpBadRequest("Can't combine query and body parameters")
989 body = self.request_body
991 # Legacy interface, do not modify/extend
993 "os": self._checkStringVariable("os"),
994 "start": not self._checkIntVariable("nostartup"),
999 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1001 return baserlib.SubmitJob(ops)
1004 def _ParseInstanceReplaceDisksRequest(name, data):
1005 """Parses a request for an instance export.
1007 @rtype: L{opcodes.OpInstanceReplaceDisks}
1008 @return: Instance export opcode
1012 "instance_name": name,
1017 raw_disks = data["disks"]
1021 if not ht.TListOf(ht.TInt)(raw_disks): # pylint: disable-msg=E1102
1022 # Backwards compatibility for strings of the format "1, 2, 3"
1024 data["disks"] = [int(part) for part in raw_disks.split(",")]
1025 except (TypeError, ValueError), err:
1026 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1028 return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1031 class R_2_instances_name_replace_disks(baserlib.R_Generic):
1032 """/2/instances/[instance_name]/replace-disks resource.
1036 """Replaces disks on an instance.
1039 op = _ParseInstanceReplaceDisksRequest(self.items[0], self.request_body)
1041 return baserlib.SubmitJob([op])
1044 class R_2_instances_name_activate_disks(baserlib.R_Generic):
1045 """/2/instances/[instance_name]/activate-disks resource.
1049 """Activate disks for an instance.
1051 The URI might contain ignore_size to ignore current recorded size.
1054 instance_name = self.items[0]
1055 ignore_size = bool(self._checkIntVariable('ignore_size'))
1057 op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1058 ignore_size=ignore_size)
1060 return baserlib.SubmitJob([op])
1063 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1064 """/2/instances/[instance_name]/deactivate-disks resource.
1068 """Deactivate disks for an instance.
1071 instance_name = self.items[0]
1073 op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
1075 return baserlib.SubmitJob([op])
1078 class R_2_instances_name_prepare_export(baserlib.R_Generic):
1079 """/2/instances/[instance_name]/prepare-export resource.
1083 """Prepares an export for an instance.
1088 instance_name = self.items[0]
1089 mode = self._checkStringVariable("mode")
1091 op = opcodes.OpBackupPrepare(instance_name=instance_name,
1094 return baserlib.SubmitJob([op])
1097 def _ParseExportInstanceRequest(name, data):
1098 """Parses a request for an instance export.
1100 @rtype: L{opcodes.OpBackupExport}
1101 @return: Instance export opcode
1104 # Rename "destination" to "target_node"
1106 data["target_node"] = data.pop("destination")
1110 return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1111 "instance_name": name,
1115 class R_2_instances_name_export(baserlib.R_Generic):
1116 """/2/instances/[instance_name]/export resource.
1120 """Exports an instance.
1125 if not isinstance(self.request_body, dict):
1126 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1128 op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1130 return baserlib.SubmitJob([op])
1133 def _ParseMigrateInstanceRequest(name, data):
1134 """Parses a request for an instance migration.
1136 @rtype: L{opcodes.OpInstanceMigrate}
1137 @return: Instance migration opcode
1140 return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1141 "instance_name": name,
1145 class R_2_instances_name_migrate(baserlib.R_Generic):
1146 """/2/instances/[instance_name]/migrate resource.
1150 """Migrates an instance.
1155 baserlib.CheckType(self.request_body, dict, "Body contents")
1157 op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1159 return baserlib.SubmitJob([op])
1162 def _ParseRenameInstanceRequest(name, data):
1163 """Parses a request for renaming an instance.
1165 @rtype: L{opcodes.OpInstanceRename}
1166 @return: Instance rename opcode
1169 return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1170 "instance_name": name,
1174 class R_2_instances_name_rename(baserlib.R_Generic):
1175 """/2/instances/[instance_name]/rename resource.
1179 """Changes the name of an instance.
1184 baserlib.CheckType(self.request_body, dict, "Body contents")
1186 op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1188 return baserlib.SubmitJob([op])
1191 def _ParseModifyInstanceRequest(name, data):
1192 """Parses a request for modifying an instance.
1194 @rtype: L{opcodes.OpInstanceSetParams}
1195 @return: Instance modify opcode
1198 return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1199 "instance_name": name,
1203 class R_2_instances_name_modify(baserlib.R_Generic):
1204 """/2/instances/[instance_name]/modify resource.
1208 """Changes some parameters of an instance.
1213 baserlib.CheckType(self.request_body, dict, "Body contents")
1215 op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1217 return baserlib.SubmitJob([op])
1220 class R_2_instances_name_disk_grow(baserlib.R_Generic):
1221 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1225 """Increases the size of an instance disk.
1230 op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1231 "instance_name": self.items[0],
1232 "disk": int(self.items[1]),
1235 return baserlib.SubmitJob([op])
1238 class R_2_instances_name_console(baserlib.R_Generic):
1239 """/2/instances/[instance_name]/console resource.
1242 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1245 """Request information for connecting to instance's console.
1247 @return: Serialized instance console description, see
1248 L{objects.InstanceConsole}
1251 client = baserlib.GetClient()
1253 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1256 raise http.HttpServiceUnavailable("Instance console unavailable")
1258 assert isinstance(console, dict)
1262 def _GetQueryFields(args):
1267 fields = args["fields"]
1269 raise http.HttpBadRequest("Missing 'fields' query argument")
1271 return _SplitQueryFields(fields[0])
1274 def _SplitQueryFields(fields):
1278 return [i.strip() for i in fields.split(",")]
1281 class R_2_query(baserlib.R_Generic):
1282 """/2/query/[resource] resource.
1285 # Results might contain sensitive information
1286 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1288 def _Query(self, fields, filter_):
1289 return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
1292 """Returns resource information.
1294 @return: Query result, see L{objects.QueryResponse}
1297 return self._Query(_GetQueryFields(self.queryargs), None)
1300 """Submits job querying for resources.
1302 @return: Query result, see L{objects.QueryResponse}
1305 body = self.request_body
1307 baserlib.CheckType(body, dict, "Body contents")
1310 fields = body["fields"]
1312 fields = _GetQueryFields(self.queryargs)
1314 return self._Query(fields, self.request_body.get("filter", None))
1317 class R_2_query_fields(baserlib.R_Generic):
1318 """/2/query/[resource]/fields resource.
1322 """Retrieves list of available fields for a resource.
1324 @return: List of serialized L{objects.QueryFieldDefinition}
1328 raw_fields = self.queryargs["fields"]
1332 fields = _SplitQueryFields(raw_fields[0])
1334 return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1337 class _R_Tags(baserlib.R_Generic):
1338 """ Quasiclass for tagging resources
1340 Manages tags. When inheriting this class you must define the
1346 def __init__(self, items, queryargs, req):
1347 """A tag resource constructor.
1349 We have to override the default to sort out cluster naming case.
1352 baserlib.R_Generic.__init__(self, items, queryargs, req)
1354 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1357 self.name = items[0]
1360 """Returns a list of tags.
1362 Example: ["tag1", "tag2", "tag3"]
1365 # pylint: disable-msg=W0212
1366 return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1369 """Add a set of tags.
1371 The request as a list of strings should be PUT to this URI. And
1372 you'll have back a job id.
1375 # pylint: disable-msg=W0212
1376 if 'tag' not in self.queryargs:
1377 raise http.HttpBadRequest("Please specify tag(s) to add using the"
1378 " the 'tag' parameter")
1379 return baserlib._Tags_PUT(self.TAG_LEVEL,
1380 self.queryargs['tag'], name=self.name,
1381 dry_run=bool(self.dryRun()))
1386 In order to delete a set of tags, the DELETE
1387 request should be addressed to URI like:
1388 /tags?tag=[tag]&tag=[tag]
1391 # pylint: disable-msg=W0212
1392 if 'tag' not in self.queryargs:
1393 # no we not gonna delete all tags
1394 raise http.HttpBadRequest("Cannot delete all tags - please specify"
1395 " tag(s) using the 'tag' parameter")
1396 return baserlib._Tags_DELETE(self.TAG_LEVEL,
1397 self.queryargs['tag'],
1399 dry_run=bool(self.dryRun()))
1402 class R_2_instances_name_tags(_R_Tags):
1403 """ /2/instances/[instance_name]/tags resource.
1405 Manages per-instance tags.
1408 TAG_LEVEL = constants.TAG_INSTANCE
1411 class R_2_nodes_name_tags(_R_Tags):
1412 """ /2/nodes/[node_name]/tags resource.
1414 Manages per-node tags.
1417 TAG_LEVEL = constants.TAG_NODE
1420 class R_2_tags(_R_Tags):
1421 """ /2/tags resource.
1423 Manages cluster tags.
1426 TAG_LEVEL = constants.TAG_CLUSTER