4 # Copyright (C) 2006, 2007, 2008 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 utils
49 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", "status",
64 N_FIELDS = ["name", "offline", "master_candidate", "drained",
66 "mtotal", "mnode", "mfree",
67 "pinst_cnt", "sinst_cnt",
68 "ctotal", "cnodes", "csockets",
70 "pinst_list", "sinst_list",
73 _NR_DRAINED = "drained"
74 _NR_MASTER_CANDIATE = "master-candidate"
76 _NR_OFFLINE = "offline"
77 _NR_REGULAR = "regular"
81 "C": _NR_MASTER_CANDIATE,
87 # Request data version field
88 _REQ_DATA_VERSION = "__version__"
90 # Feature string for instance creation request data version 1
91 _INST_CREATE_REQV1 = "instance-create-reqv1"
93 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
97 class R_version(baserlib.R_Generic):
100 This resource should be used to determine the remote API version and
101 to adapt clients accordingly.
106 """Returns the remote API version.
109 return constants.RAPI_VERSION
112 class R_2_info(baserlib.R_Generic):
118 """Returns cluster information.
121 client = baserlib.GetClient()
122 return client.QueryClusterInfo()
125 class R_2_features(baserlib.R_Generic):
126 """/2/features resource.
131 """Returns list of optional RAPI features implemented.
134 return [_INST_CREATE_REQV1]
137 class R_2_os(baserlib.R_Generic):
143 """Return a list of all OSes.
145 Can return error 500 in case of a problem.
147 Example: ["debian-etch"]
150 cl = baserlib.GetClient()
151 op = opcodes.OpDiagnoseOS(output_fields=["name", "valid", "variants"],
153 job_id = baserlib.SubmitJob([op], cl)
154 # we use custom feedback function, instead of print we log the status
155 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
156 diagnose_data = result[0]
158 if not isinstance(diagnose_data, list):
159 raise http.HttpBadGateway(message="Can't get OS list")
162 for (name, valid, variants) in diagnose_data:
164 os_names.extend(cli.CalculateOSNames(name, variants))
169 class R_2_redist_config(baserlib.R_Generic):
170 """/2/redistribute-config resource.
175 """Redistribute configuration to all nodes.
178 return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
181 class R_2_jobs(baserlib.R_Generic):
187 """Returns a dictionary of jobs.
189 @return: a dictionary with jobs id and uri.
193 cl = baserlib.GetClient()
194 # Convert the list of lists to the list of ids
195 result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
196 return baserlib.BuildUriList(result, "/2/jobs/%s",
197 uri_fields=("id", "uri"))
200 class R_2_jobs_id(baserlib.R_Generic):
201 """/2/jobs/[job_id] resource.
205 """Returns a job status.
207 @return: a dictionary with job parameters.
209 - id: job ID as a number
210 - status: current job status as a string
211 - ops: involved OpCodes as a list of dictionaries for each
213 - opstatus: OpCodes status as a list
214 - opresult: OpCodes results as a list of lists
217 fields = ["id", "ops", "status", "summary",
218 "opstatus", "opresult", "oplog",
219 "received_ts", "start_ts", "end_ts",
221 job_id = self.items[0]
222 result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
224 raise http.HttpNotFound()
225 return baserlib.MapFields(fields, result)
228 """Cancel not-yet-started job.
231 job_id = self.items[0]
232 result = baserlib.GetClient().CancelJob(job_id)
236 class R_2_jobs_id_wait(baserlib.R_Generic):
237 """/2/jobs/[job_id]/wait resource.
240 # WaitForJobChange provides access to sensitive information and blocks
241 # machine resources (it's a blocking RAPI call), hence restricting access.
242 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
245 """Waits for job changes.
248 job_id = self.items[0]
250 fields = self.getBodyParameter("fields")
251 prev_job_info = self.getBodyParameter("previous_job_info", None)
252 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
254 if not isinstance(fields, list):
255 raise http.HttpBadRequest("The 'fields' parameter should be a list")
257 if not (prev_job_info is None or isinstance(prev_job_info, list)):
258 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
261 if not (prev_log_serial is None or
262 isinstance(prev_log_serial, (int, long))):
263 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
266 client = baserlib.GetClient()
267 result = client.WaitForJobChangeOnce(job_id, fields,
268 prev_job_info, prev_log_serial,
269 timeout=_WFJC_TIMEOUT)
271 raise http.HttpNotFound()
273 if result == constants.JOB_NOTCHANGED:
277 (job_info, log_entries) = result
280 "job_info": job_info,
281 "log_entries": log_entries,
285 class R_2_nodes(baserlib.R_Generic):
286 """/2/nodes resource.
290 """Returns a list of all nodes.
293 client = baserlib.GetClient()
296 bulkdata = client.QueryNodes([], N_FIELDS, False)
297 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
299 nodesdata = client.QueryNodes([], ["name"], False)
300 nodeslist = [row[0] for row in nodesdata]
301 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
302 uri_fields=("id", "uri"))
305 class R_2_nodes_name(baserlib.R_Generic):
306 """/2/nodes/[node_name] resources.
310 """Send information about a node.
313 node_name = self.items[0]
314 client = baserlib.GetClient()
316 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
317 names=[node_name], fields=N_FIELDS,
318 use_locking=self.useLocking())
320 return baserlib.MapFields(N_FIELDS, result[0])
323 class R_2_nodes_name_role(baserlib.R_Generic):
324 """ /2/nodes/[node_name]/role resource.
328 """Returns the current node role.
333 node_name = self.items[0]
334 client = baserlib.GetClient()
335 result = client.QueryNodes(names=[node_name], fields=["role"],
336 use_locking=self.useLocking())
338 return _NR_MAP[result[0][0]]
341 """Sets the node role.
346 if not isinstance(self.req.request_body, basestring):
347 raise http.HttpBadRequest("Invalid body contents, not a string")
349 node_name = self.items[0]
350 role = self.req.request_body
352 if role == _NR_REGULAR:
357 elif role == _NR_MASTER_CANDIATE:
359 offline = drained = None
361 elif role == _NR_DRAINED:
363 candidate = offline = None
365 elif role == _NR_OFFLINE:
367 candidate = drained = None
370 raise http.HttpBadRequest("Can't set '%s' role" % role)
372 op = opcodes.OpSetNodeParams(node_name=node_name,
373 master_candidate=candidate,
376 force=bool(self.useForce()))
378 return baserlib.SubmitJob([op])
381 class R_2_nodes_name_evacuate(baserlib.R_Generic):
382 """/2/nodes/[node_name]/evacuate resource.
386 """Evacuate all secondary instances off a node.
389 node_name = self.items[0]
390 remote_node = self._checkStringVariable("remote_node", default=None)
391 iallocator = self._checkStringVariable("iallocator", default=None)
393 op = opcodes.OpEvacuateNode(node_name=node_name,
394 remote_node=remote_node,
395 iallocator=iallocator)
397 return baserlib.SubmitJob([op])
400 class R_2_nodes_name_migrate(baserlib.R_Generic):
401 """/2/nodes/[node_name]/migrate resource.
405 """Migrate all primary instances from a node.
408 node_name = self.items[0]
409 live = bool(self._checkIntVariable("live", default=1))
411 op = opcodes.OpMigrateNode(node_name=node_name, live=live)
413 return baserlib.SubmitJob([op])
416 class R_2_nodes_name_storage(baserlib.R_Generic):
417 """/2/nodes/[node_name]/storage ressource.
420 # LUQueryNodeStorage acquires locks, hence restricting access to GET
421 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
424 node_name = self.items[0]
426 storage_type = self._checkStringVariable("storage_type", None)
428 raise http.HttpBadRequest("Missing the required 'storage_type'"
431 output_fields = self._checkStringVariable("output_fields", None)
432 if not output_fields:
433 raise http.HttpBadRequest("Missing the required 'output_fields'"
436 op = opcodes.OpQueryNodeStorage(nodes=[node_name],
437 storage_type=storage_type,
438 output_fields=output_fields.split(","))
439 return baserlib.SubmitJob([op])
442 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
443 """/2/nodes/[node_name]/storage/modify ressource.
447 node_name = self.items[0]
449 storage_type = self._checkStringVariable("storage_type", None)
451 raise http.HttpBadRequest("Missing the required 'storage_type'"
454 name = self._checkStringVariable("name", None)
456 raise http.HttpBadRequest("Missing the required 'name'"
461 if "allocatable" in self.queryargs:
462 changes[constants.SF_ALLOCATABLE] = \
463 bool(self._checkIntVariable("allocatable", default=1))
465 op = opcodes.OpModifyNodeStorage(node_name=node_name,
466 storage_type=storage_type,
469 return baserlib.SubmitJob([op])
472 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
473 """/2/nodes/[node_name]/storage/repair ressource.
477 node_name = self.items[0]
479 storage_type = self._checkStringVariable("storage_type", None)
481 raise http.HttpBadRequest("Missing the required 'storage_type'"
484 name = self._checkStringVariable("name", None)
486 raise http.HttpBadRequest("Missing the required 'name'"
489 op = opcodes.OpRepairNodeStorage(node_name=node_name,
490 storage_type=storage_type,
492 return baserlib.SubmitJob([op])
495 def _ParseInstanceCreateRequestVersion1(data, dry_run):
496 """Parses an instance creation request version 1.
498 @rtype: L{opcodes.OpCreateInstance}
499 @return: Instance creation opcode
503 disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
506 for idx, i in enumerate(disks_input):
507 baserlib.CheckType(i, dict, "Disk %d specification" % idx)
513 raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
520 # Optional disk access mode
522 disk_access = i["mode"]
526 disk["mode"] = disk_access
530 assert len(disks_input) == len(disks)
533 nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
536 for idx, i in enumerate(nics_input):
537 baserlib.CheckType(i, dict, "NIC %d specification" % idx)
541 for field in ["mode", "ip", "link", "bridge"]:
551 assert len(nics_input) == len(nics)
554 hvparams = baserlib.CheckParameter(data, "hvparams", default={})
555 utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
557 beparams = baserlib.CheckParameter(data, "beparams", default={})
558 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
560 return opcodes.OpCreateInstance(
561 mode=baserlib.CheckParameter(data, "mode"),
562 instance_name=baserlib.CheckParameter(data, "name"),
563 os_type=baserlib.CheckParameter(data, "os", default=None),
564 force_variant=baserlib.CheckParameter(data, "force_variant",
566 pnode=baserlib.CheckParameter(data, "pnode", default=None),
567 snode=baserlib.CheckParameter(data, "snode", default=None),
568 disk_template=baserlib.CheckParameter(data, "disk_template"),
571 src_node=baserlib.CheckParameter(data, "src_node", default=None),
572 src_path=baserlib.CheckParameter(data, "src_path", default=None),
573 start=baserlib.CheckParameter(data, "start", default=True),
575 ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
576 name_check=baserlib.CheckParameter(data, "name_check", default=True),
577 file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
579 file_driver=baserlib.CheckParameter(data, "file_driver",
580 default=constants.FD_LOOP),
581 iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
582 hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
589 class R_2_instances(baserlib.R_Generic):
590 """/2/instances resource.
594 """Returns a list of all available instances.
597 client = baserlib.GetClient()
599 use_locking = self.useLocking()
601 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
602 return baserlib.MapBulkFields(bulkdata, I_FIELDS)
604 instancesdata = client.QueryInstances([], ["name"], use_locking)
605 instanceslist = [row[0] for row in instancesdata]
606 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
607 uri_fields=("id", "uri"))
609 def _ParseVersion0CreateRequest(self):
610 """Parses an instance creation request version 0.
612 Request data version 0 is deprecated and should not be used anymore.
614 @rtype: L{opcodes.OpCreateInstance}
615 @return: Instance creation opcode
618 # Do not modify anymore, request data version 0 is deprecated
619 beparams = baserlib.MakeParamsDict(self.req.request_body,
620 constants.BES_PARAMETERS)
621 hvparams = baserlib.MakeParamsDict(self.req.request_body,
622 constants.HVS_PARAMETERS)
623 fn = self.getBodyParameter
626 disk_data = fn('disks')
627 if not isinstance(disk_data, list):
628 raise http.HttpBadRequest("The 'disks' parameter should be a list")
630 for idx, d in enumerate(disk_data):
631 if not isinstance(d, int):
632 raise http.HttpBadRequest("Disk %d specification wrong: should"
633 " be an integer" % idx)
634 disks.append({"size": d})
636 # nic processing (one nic only)
637 nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
638 if fn("ip", None) is not None:
639 nics[0]["ip"] = fn("ip")
640 if fn("mode", None) is not None:
641 nics[0]["mode"] = fn("mode")
642 if fn("link", None) is not None:
643 nics[0]["link"] = fn("link")
644 if fn("bridge", None) is not None:
645 nics[0]["bridge"] = fn("bridge")
647 # Do not modify anymore, request data version 0 is deprecated
648 return opcodes.OpCreateInstance(
649 mode=constants.INSTANCE_CREATE,
650 instance_name=fn('name'),
652 disk_template=fn('disk_template'),
654 pnode=fn('pnode', None),
655 snode=fn('snode', None),
656 iallocator=fn('iallocator', None),
658 start=fn('start', True),
659 ip_check=fn('ip_check', True),
660 name_check=fn('name_check', True),
662 hypervisor=fn('hypervisor', None),
665 file_storage_dir=fn('file_storage_dir', None),
666 file_driver=fn('file_driver', constants.FD_LOOP),
667 dry_run=bool(self.dryRun()),
671 """Create an instance.
676 if not isinstance(self.req.request_body, dict):
677 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
679 # Default to request data version 0
680 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
682 if data_version == 0:
683 op = self._ParseVersion0CreateRequest()
684 elif data_version == 1:
685 op = _ParseInstanceCreateRequestVersion1(self.req.request_body,
688 raise http.HttpBadRequest("Unsupported request data version %s" %
691 return baserlib.SubmitJob([op])
694 class R_2_instances_name(baserlib.R_Generic):
695 """/2/instances/[instance_name] resources.
699 """Send information about an instance.
702 client = baserlib.GetClient()
703 instance_name = self.items[0]
705 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
706 names=[instance_name],
708 use_locking=self.useLocking())
710 return baserlib.MapFields(I_FIELDS, result[0])
713 """Delete an instance.
716 op = opcodes.OpRemoveInstance(instance_name=self.items[0],
717 ignore_failures=False,
718 dry_run=bool(self.dryRun()))
719 return baserlib.SubmitJob([op])
722 class R_2_instances_name_info(baserlib.R_Generic):
723 """/2/instances/[instance_name]/info resource.
727 """Request detailed instance information.
730 instance_name = self.items[0]
731 static = bool(self._checkIntVariable("static", default=0))
733 op = opcodes.OpQueryInstanceData(instances=[instance_name],
735 return baserlib.SubmitJob([op])
738 class R_2_instances_name_reboot(baserlib.R_Generic):
739 """/2/instances/[instance_name]/reboot resource.
741 Implements an instance reboot.
745 """Reboot an instance.
747 The URI takes type=[hard|soft|full] and
748 ignore_secondaries=[False|True] parameters.
751 instance_name = self.items[0]
752 reboot_type = self.queryargs.get('type',
753 [constants.INSTANCE_REBOOT_HARD])[0]
754 ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
755 op = opcodes.OpRebootInstance(instance_name=instance_name,
756 reboot_type=reboot_type,
757 ignore_secondaries=ignore_secondaries,
758 dry_run=bool(self.dryRun()))
760 return baserlib.SubmitJob([op])
763 class R_2_instances_name_startup(baserlib.R_Generic):
764 """/2/instances/[instance_name]/startup resource.
766 Implements an instance startup.
770 """Startup an instance.
772 The URI takes force=[False|True] parameter to start the instance
773 if even if secondary disks are failing.
776 instance_name = self.items[0]
777 force_startup = bool(self._checkIntVariable('force'))
778 op = opcodes.OpStartupInstance(instance_name=instance_name,
780 dry_run=bool(self.dryRun()))
782 return baserlib.SubmitJob([op])
785 class R_2_instances_name_shutdown(baserlib.R_Generic):
786 """/2/instances/[instance_name]/shutdown resource.
788 Implements an instance shutdown.
792 """Shutdown an instance.
795 instance_name = self.items[0]
796 op = opcodes.OpShutdownInstance(instance_name=instance_name,
797 dry_run=bool(self.dryRun()))
799 return baserlib.SubmitJob([op])
802 class R_2_instances_name_reinstall(baserlib.R_Generic):
803 """/2/instances/[instance_name]/reinstall resource.
805 Implements an instance reinstall.
809 """Reinstall an instance.
811 The URI takes os=name and nostartup=[0|1] optional
812 parameters. By default, the instance will be started
816 instance_name = self.items[0]
817 ostype = self._checkStringVariable('os')
818 nostartup = self._checkIntVariable('nostartup')
820 opcodes.OpShutdownInstance(instance_name=instance_name),
821 opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
824 ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
826 return baserlib.SubmitJob(ops)
829 class R_2_instances_name_replace_disks(baserlib.R_Generic):
830 """/2/instances/[instance_name]/replace-disks resource.
834 """Replaces disks on an instance.
837 instance_name = self.items[0]
838 remote_node = self._checkStringVariable("remote_node", default=None)
839 mode = self._checkStringVariable("mode", default=None)
840 raw_disks = self._checkStringVariable("disks", default=None)
841 iallocator = self._checkStringVariable("iallocator", default=None)
845 disks = [int(part) for part in raw_disks.split(",")]
846 except ValueError, err:
847 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
851 op = opcodes.OpReplaceDisks(instance_name=instance_name,
852 remote_node=remote_node,
855 iallocator=iallocator)
857 return baserlib.SubmitJob([op])
860 class R_2_instances_name_activate_disks(baserlib.R_Generic):
861 """/2/instances/[instance_name]/activate-disks resource.
865 """Activate disks for an instance.
867 The URI might contain ignore_size to ignore current recorded size.
870 instance_name = self.items[0]
871 ignore_size = bool(self._checkIntVariable('ignore_size'))
873 op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
874 ignore_size=ignore_size)
876 return baserlib.SubmitJob([op])
879 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
880 """/2/instances/[instance_name]/deactivate-disks resource.
884 """Deactivate disks for an instance.
887 instance_name = self.items[0]
889 op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
891 return baserlib.SubmitJob([op])
894 class _R_Tags(baserlib.R_Generic):
895 """ Quasiclass for tagging resources
897 Manages tags. When inheriting this class you must define the
903 def __init__(self, items, queryargs, req):
904 """A tag resource constructor.
906 We have to override the default to sort out cluster naming case.
909 baserlib.R_Generic.__init__(self, items, queryargs, req)
911 if self.TAG_LEVEL != constants.TAG_CLUSTER:
917 """Returns a list of tags.
919 Example: ["tag1", "tag2", "tag3"]
922 # pylint: disable-msg=W0212
923 return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
926 """Add a set of tags.
928 The request as a list of strings should be PUT to this URI. And
929 you'll have back a job id.
932 # pylint: disable-msg=W0212
933 if 'tag' not in self.queryargs:
934 raise http.HttpBadRequest("Please specify tag(s) to add using the"
935 " the 'tag' parameter")
936 return baserlib._Tags_PUT(self.TAG_LEVEL,
937 self.queryargs['tag'], name=self.name,
938 dry_run=bool(self.dryRun()))
943 In order to delete a set of tags, the DELETE
944 request should be addressed to URI like:
945 /tags?tag=[tag]&tag=[tag]
948 # pylint: disable-msg=W0212
949 if 'tag' not in self.queryargs:
950 # no we not gonna delete all tags
951 raise http.HttpBadRequest("Cannot delete all tags - please specify"
952 " tag(s) using the 'tag' parameter")
953 return baserlib._Tags_DELETE(self.TAG_LEVEL,
954 self.queryargs['tag'],
956 dry_run=bool(self.dryRun()))
959 class R_2_instances_name_tags(_R_Tags):
960 """ /2/instances/[instance_name]/tags resource.
962 Manages per-instance tags.
965 TAG_LEVEL = constants.TAG_INSTANCE
968 class R_2_nodes_name_tags(_R_Tags):
969 """ /2/nodes/[node_name]/tags resource.
971 Manages per-node tags.
974 TAG_LEVEL = constants.TAG_NODE
977 class R_2_tags(_R_Tags):
978 """ /2/instances/tags resource.
980 Manages cluster tags.
983 TAG_LEVEL = constants.TAG_CLUSTER