1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """Remote API version 2 baserlib.library.
23
24 PUT or POST?
25 ============
26
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.
30
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.
34
35 So when adding new methods, if they are operating on the URI entity itself,
36 PUT should be prefered over POST.
37
38 """
39
40
41
42
43
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
51
52
53 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
54 I_FIELDS = ["name", "admin_state", "os",
55 "pnode", "snodes",
56 "disk_template",
57 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
58 "network_port",
59 "disk.sizes", "disk_usage",
60 "beparams", "hvparams",
61 "oper_state", "oper_ram", "oper_vcpus", "status",
62 ] + _COMMON_FIELDS
63
64 N_FIELDS = ["name", "offline", "master_candidate", "drained",
65 "dtotal", "dfree",
66 "mtotal", "mnode", "mfree",
67 "pinst_cnt", "sinst_cnt",
68 "ctotal", "cnodes", "csockets",
69 "pip", "sip", "role",
70 "pinst_list", "sinst_list",
71 ] + _COMMON_FIELDS
72
73 _NR_DRAINED = "drained"
74 _NR_MASTER_CANDIATE = "master-candidate"
75 _NR_MASTER = "master"
76 _NR_OFFLINE = "offline"
77 _NR_REGULAR = "regular"
78
79 _NR_MAP = {
80 "M": _NR_MASTER,
81 "C": _NR_MASTER_CANDIATE,
82 "D": _NR_DRAINED,
83 "O": _NR_OFFLINE,
84 "R": _NR_REGULAR,
85 }
86
87
88 _REQ_DATA_VERSION = "__version__"
89
90
91 _INST_CREATE_REQV1 = "instance-create-reqv1"
92
93
94 _WFJC_TIMEOUT = 10
98 """/version resource.
99
100 This resource should be used to determine the remote API version and
101 to adapt clients accordingly.
102
103 """
104 @staticmethod
110
113 """Cluster info.
114
115 """
116 @staticmethod
123
126 """/2/features resource.
127
128 """
129 @staticmethod
131 """Returns list of optional RAPI features implemented.
132
133 """
134 return [_INST_CREATE_REQV1]
135
136
137 -class R_2_os(baserlib.R_Generic):
138 """/2/os resource.
139
140 """
141 @staticmethod
143 """Return a list of all OSes.
144
145 Can return error 500 in case of a problem.
146
147 Example: ["debian-etch"]
148
149 """
150 cl = baserlib.GetClient()
151 op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
152 job_id = baserlib.SubmitJob([op], cl)
153
154 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
155 diagnose_data = result[0]
156
157 if not isinstance(diagnose_data, list):
158 raise http.HttpBadGateway(message="Can't get OS list")
159
160 os_names = []
161 for (name, variants) in diagnose_data:
162 os_names.extend(cli.CalculateOSNames(name, variants))
163
164 return os_names
165
168 """/2/redistribute-config resource.
169
170 """
171 @staticmethod
177
180 """/2/jobs resource.
181
182 """
183 @staticmethod
185 """Returns a dictionary of jobs.
186
187 @return: a dictionary with jobs id and uri.
188
189 """
190 fields = ["id"]
191 cl = baserlib.GetClient()
192
193 result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
194 return baserlib.BuildUriList(result, "/2/jobs/%s",
195 uri_fields=("id", "uri"))
196
199 """/2/jobs/[job_id] resource.
200
201 """
203 """Returns a job status.
204
205 @return: a dictionary with job parameters.
206 The result includes:
207 - id: job ID as a number
208 - status: current job status as a string
209 - ops: involved OpCodes as a list of dictionaries for each
210 opcodes in the job
211 - opstatus: OpCodes status as a list
212 - opresult: OpCodes results as a list of lists
213
214 """
215 fields = ["id", "ops", "status", "summary",
216 "opstatus", "opresult", "oplog",
217 "received_ts", "start_ts", "end_ts",
218 ]
219 job_id = self.items[0]
220 result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
221 if result is None:
222 raise http.HttpNotFound()
223 return baserlib.MapFields(fields, result)
224
226 """Cancel not-yet-started job.
227
228 """
229 job_id = self.items[0]
230 result = baserlib.GetClient().CancelJob(job_id)
231 return result
232
235 """/2/jobs/[job_id]/wait resource.
236
237 """
238
239
240 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
241
243 """Waits for job changes.
244
245 """
246 job_id = self.items[0]
247
248 fields = self.getBodyParameter("fields")
249 prev_job_info = self.getBodyParameter("previous_job_info", None)
250 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
251
252 if not isinstance(fields, list):
253 raise http.HttpBadRequest("The 'fields' parameter should be a list")
254
255 if not (prev_job_info is None or isinstance(prev_job_info, list)):
256 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
257 " be a list")
258
259 if not (prev_log_serial is None or
260 isinstance(prev_log_serial, (int, long))):
261 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
262 " be a number")
263
264 client = baserlib.GetClient()
265 result = client.WaitForJobChangeOnce(job_id, fields,
266 prev_job_info, prev_log_serial,
267 timeout=_WFJC_TIMEOUT)
268 if not result:
269 raise http.HttpNotFound()
270
271 if result == constants.JOB_NOTCHANGED:
272
273 return None
274
275 (job_info, log_entries) = result
276
277 return {
278 "job_info": job_info,
279 "log_entries": log_entries,
280 }
281
284 """/2/nodes resource.
285
286 """
301
304 """/2/nodes/[node_name] resources.
305
306 """
319
322 """ /2/nodes/[node_name]/role resource.
323
324 """
337
339 """Sets the node role.
340
341 @return: a job id
342
343 """
344 if not isinstance(self.request_body, basestring):
345 raise http.HttpBadRequest("Invalid body contents, not a string")
346
347 node_name = self.items[0]
348 role = self.request_body
349
350 if role == _NR_REGULAR:
351 candidate = False
352 offline = False
353 drained = False
354
355 elif role == _NR_MASTER_CANDIATE:
356 candidate = True
357 offline = drained = None
358
359 elif role == _NR_DRAINED:
360 drained = True
361 candidate = offline = None
362
363 elif role == _NR_OFFLINE:
364 offline = True
365 candidate = drained = None
366
367 else:
368 raise http.HttpBadRequest("Can't set '%s' role" % role)
369
370 op = opcodes.OpSetNodeParams(node_name=node_name,
371 master_candidate=candidate,
372 offline=offline,
373 drained=drained,
374 force=bool(self.useForce()))
375
376 return baserlib.SubmitJob([op])
377
380 """/2/nodes/[node_name]/evacuate resource.
381
382 """
384 """Evacuate all secondary instances off a node.
385
386 """
387 node_name = self.items[0]
388 remote_node = self._checkStringVariable("remote_node", default=None)
389 iallocator = self._checkStringVariable("iallocator", default=None)
390 early_r = bool(self._checkIntVariable("early_release", default=0))
391 dry_run = bool(self.dryRun())
392
393 cl = baserlib.GetClient()
394
395 op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
396 iallocator=iallocator,
397 remote_node=remote_node)
398
399 job_id = baserlib.SubmitJob([op], cl)
400
401 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
402
403 jobs = []
404 for iname, node in result:
405 if dry_run:
406 jid = None
407 else:
408 op = opcodes.OpReplaceDisks(instance_name=iname,
409 remote_node=node, disks=[],
410 mode=constants.REPLACE_DISK_CHG,
411 early_release=early_r)
412 jid = baserlib.SubmitJob([op])
413 jobs.append((jid, iname, node))
414
415 return jobs
416
442
468
471 """/2/nodes/[node_name]/storage/modify ressource.
472
473 """
498
501 """/2/nodes/[node_name]/storage/repair ressource.
502
503 """
521
524 """Parses an instance creation request version 1.
525
526 @rtype: L{opcodes.OpCreateInstance}
527 @return: Instance creation opcode
528
529 """
530
531 disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
532
533 disks = []
534 for idx, i in enumerate(disks_input):
535 baserlib.CheckType(i, dict, "Disk %d specification" % idx)
536
537
538 try:
539 size = i[constants.IDISK_SIZE]
540 except KeyError:
541 raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
542 " size" % idx)
543
544 disk = {
545 constants.IDISK_SIZE: size,
546 }
547
548
549 try:
550 disk_access = i[constants.IDISK_MODE]
551 except KeyError:
552 pass
553 else:
554 disk[constants.IDISK_MODE] = disk_access
555
556 disks.append(disk)
557
558 assert len(disks_input) == len(disks)
559
560
561 nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
562
563 nics = []
564 for idx, i in enumerate(nics_input):
565 baserlib.CheckType(i, dict, "NIC %d specification" % idx)
566
567 nic = {}
568
569 for field in constants.INIC_PARAMS:
570 try:
571 value = i[field]
572 except KeyError:
573 continue
574
575 nic[field] = value
576
577 nics.append(nic)
578
579 assert len(nics_input) == len(nics)
580
581
582 hvparams = baserlib.CheckParameter(data, "hvparams", default={})
583 utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
584
585 beparams = baserlib.CheckParameter(data, "beparams", default={})
586 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
587
588 return opcodes.OpCreateInstance(
589 mode=baserlib.CheckParameter(data, "mode"),
590 instance_name=baserlib.CheckParameter(data, "name"),
591 os_type=baserlib.CheckParameter(data, "os"),
592 osparams=baserlib.CheckParameter(data, "osparams", default={}),
593 force_variant=baserlib.CheckParameter(data, "force_variant",
594 default=False),
595 pnode=baserlib.CheckParameter(data, "pnode", default=None),
596 snode=baserlib.CheckParameter(data, "snode", default=None),
597 disk_template=baserlib.CheckParameter(data, "disk_template"),
598 disks=disks,
599 nics=nics,
600 src_node=baserlib.CheckParameter(data, "src_node", default=None),
601 src_path=baserlib.CheckParameter(data, "src_path", default=None),
602 start=baserlib.CheckParameter(data, "start", default=True),
603 wait_for_sync=True,
604 ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
605 name_check=baserlib.CheckParameter(data, "name_check", default=True),
606 file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
607 default=None),
608 file_driver=baserlib.CheckParameter(data, "file_driver",
609 default=constants.FD_LOOP),
610 source_handshake=baserlib.CheckParameter(data, "source_handshake",
611 default=None),
612 source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
613 default=None),
614 source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
615 default=None),
616 iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
617 hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
618 hvparams=hvparams,
619 beparams=beparams,
620 dry_run=dry_run,
621 )
622
625 """/2/instances resource.
626
627 """
643
645 """Parses an instance creation request version 0.
646
647 Request data version 0 is deprecated and should not be used anymore.
648
649 @rtype: L{opcodes.OpCreateInstance}
650 @return: Instance creation opcode
651
652 """
653
654 beparams = baserlib.MakeParamsDict(self.request_body,
655 constants.BES_PARAMETERS)
656 hvparams = baserlib.MakeParamsDict(self.request_body,
657 constants.HVS_PARAMETERS)
658 fn = self.getBodyParameter
659
660
661 disk_data = fn('disks')
662 if not isinstance(disk_data, list):
663 raise http.HttpBadRequest("The 'disks' parameter should be a list")
664 disks = []
665 for idx, d in enumerate(disk_data):
666 if not isinstance(d, int):
667 raise http.HttpBadRequest("Disk %d specification wrong: should"
668 " be an integer" % idx)
669 disks.append({"size": d})
670
671
672 nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
673 if fn("ip", None) is not None:
674 nics[0]["ip"] = fn("ip")
675 if fn("mode", None) is not None:
676 nics[0]["mode"] = fn("mode")
677 if fn("link", None) is not None:
678 nics[0]["link"] = fn("link")
679 if fn("bridge", None) is not None:
680 nics[0]["bridge"] = fn("bridge")
681
682
683 return opcodes.OpCreateInstance(
684 mode=constants.INSTANCE_CREATE,
685 instance_name=fn('name'),
686 disks=disks,
687 disk_template=fn('disk_template'),
688 os_type=fn('os'),
689 pnode=fn('pnode', None),
690 snode=fn('snode', None),
691 iallocator=fn('iallocator', None),
692 nics=nics,
693 start=fn('start', True),
694 ip_check=fn('ip_check', True),
695 name_check=fn('name_check', True),
696 wait_for_sync=True,
697 hypervisor=fn('hypervisor', None),
698 hvparams=hvparams,
699 beparams=beparams,
700 file_storage_dir=fn('file_storage_dir', None),
701 file_driver=fn('file_driver', constants.FD_LOOP),
702 dry_run=bool(self.dryRun()),
703 )
704
706 """Create an instance.
707
708 @return: a job id
709
710 """
711 if not isinstance(self.request_body, dict):
712 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
713
714
715 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
716
717 if data_version == 0:
718 op = self._ParseVersion0CreateRequest()
719 elif data_version == 1:
720 op = _ParseInstanceCreateRequestVersion1(self.request_body,
721 self.dryRun())
722 else:
723 raise http.HttpBadRequest("Unsupported request data version %s" %
724 data_version)
725
726 return baserlib.SubmitJob([op])
727
730 """/2/instances/[instance_name] resources.
731
732 """
746
755
758 """/2/instances/[instance_name]/info resource.
759
760 """
771
774 """/2/instances/[instance_name]/reboot resource.
775
776 Implements an instance reboot.
777
778 """
780 """Reboot an instance.
781
782 The URI takes type=[hard|soft|full] and
783 ignore_secondaries=[False|True] parameters.
784
785 """
786 instance_name = self.items[0]
787 reboot_type = self.queryargs.get('type',
788 [constants.INSTANCE_REBOOT_HARD])[0]
789 ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
790 op = opcodes.OpRebootInstance(instance_name=instance_name,
791 reboot_type=reboot_type,
792 ignore_secondaries=ignore_secondaries,
793 dry_run=bool(self.dryRun()))
794
795 return baserlib.SubmitJob([op])
796
799 """/2/instances/[instance_name]/startup resource.
800
801 Implements an instance startup.
802
803 """
805 """Startup an instance.
806
807 The URI takes force=[False|True] parameter to start the instance
808 if even if secondary disks are failing.
809
810 """
811 instance_name = self.items[0]
812 force_startup = bool(self._checkIntVariable('force'))
813 op = opcodes.OpStartupInstance(instance_name=instance_name,
814 force=force_startup,
815 dry_run=bool(self.dryRun()))
816
817 return baserlib.SubmitJob([op])
818
821 """/2/instances/[instance_name]/shutdown resource.
822
823 Implements an instance shutdown.
824
825 """
835
838 """/2/instances/[instance_name]/reinstall resource.
839
840 Implements an instance reinstall.
841
842 """
844 """Reinstall an instance.
845
846 The URI takes os=name and nostartup=[0|1] optional
847 parameters. By default, the instance will be started
848 automatically.
849
850 """
851 instance_name = self.items[0]
852 ostype = self._checkStringVariable('os')
853 nostartup = self._checkIntVariable('nostartup')
854 ops = [
855 opcodes.OpShutdownInstance(instance_name=instance_name),
856 opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
857 ]
858 if not nostartup:
859 ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
860 force=False))
861 return baserlib.SubmitJob(ops)
862
865 """/2/instances/[instance_name]/replace-disks resource.
866
867 """
869 """Replaces disks on an instance.
870
871 """
872 instance_name = self.items[0]
873 remote_node = self._checkStringVariable("remote_node", default=None)
874 mode = self._checkStringVariable("mode", default=None)
875 raw_disks = self._checkStringVariable("disks", default=None)
876 iallocator = self._checkStringVariable("iallocator", default=None)
877
878 if raw_disks:
879 try:
880 disks = [int(part) for part in raw_disks.split(",")]
881 except ValueError, err:
882 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
883 else:
884 disks = []
885
886 op = opcodes.OpReplaceDisks(instance_name=instance_name,
887 remote_node=remote_node,
888 mode=mode,
889 disks=disks,
890 iallocator=iallocator)
891
892 return baserlib.SubmitJob([op])
893
896 """/2/instances/[instance_name]/activate-disks resource.
897
898 """
900 """Activate disks for an instance.
901
902 The URI might contain ignore_size to ignore current recorded size.
903
904 """
905 instance_name = self.items[0]
906 ignore_size = bool(self._checkIntVariable('ignore_size'))
907
908 op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
909 ignore_size=ignore_size)
910
911 return baserlib.SubmitJob([op])
912
915 """/2/instances/[instance_name]/deactivate-disks resource.
916
917 """
927
930 """/2/instances/[instance_name]/prepare-export resource.
931
932 """
946
949 """Parses a request for an instance export.
950
951 @rtype: L{opcodes.OpExportInstance}
952 @return: Instance export opcode
953
954 """
955 mode = baserlib.CheckParameter(data, "mode",
956 default=constants.EXPORT_MODE_LOCAL)
957 target_node = baserlib.CheckParameter(data, "destination")
958 shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
959 remove_instance = baserlib.CheckParameter(data, "remove_instance",
960 exptype=bool, default=False)
961 x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
962 destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
963 default=None)
964
965 return opcodes.OpExportInstance(instance_name=name,
966 mode=mode,
967 target_node=target_node,
968 shutdown=shutdown,
969 remove_instance=remove_instance,
970 x509_key_name=x509_key_name,
971 destination_x509_ca=destination_x509_ca)
972
975 """/2/instances/[instance_name]/export resource.
976
977 """
990
993 """Parses a request for an instance migration.
994
995 @rtype: L{opcodes.OpMigrateInstance}
996 @return: Instance migration opcode
997
998 """
999 mode = baserlib.CheckParameter(data, "mode", default=None)
1000 cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1001 default=False)
1002
1003 return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1004 cleanup=cleanup)
1005
1008 """/2/instances/[instance_name]/migrate resource.
1009
1010 """
1022
1025 """Parses a request for renaming an instance.
1026
1027 @rtype: L{opcodes.OpRenameInstance}
1028 @return: Instance rename opcode
1029
1030 """
1031 new_name = baserlib.CheckParameter(data, "new_name")
1032 ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1033 name_check = baserlib.CheckParameter(data, "name_check", default=True)
1034
1035 return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1036 name_check=name_check, ip_check=ip_check)
1037
1040 """/2/instances/[instance_name]/rename resource.
1041
1042 """
1054
1057 """Parses a request for modifying an instance.
1058
1059 @rtype: L{opcodes.OpSetInstanceParams}
1060 @return: Instance modify opcode
1061
1062 """
1063 osparams = baserlib.CheckParameter(data, "osparams", default={})
1064 force = baserlib.CheckParameter(data, "force", default=False)
1065 nics = baserlib.CheckParameter(data, "nics", default=[])
1066 disks = baserlib.CheckParameter(data, "disks", default=[])
1067 disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1068 remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1069 os_name = baserlib.CheckParameter(data, "os_name", default=None)
1070 force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1071
1072
1073 hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1074 utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1075 allowed_values=[constants.VALUE_DEFAULT])
1076
1077 beparams = baserlib.CheckParameter(data, "beparams", default={})
1078 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1079 allowed_values=[constants.VALUE_DEFAULT])
1080
1081 return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1082 beparams=beparams, osparams=osparams,
1083 force=force, nics=nics, disks=disks,
1084 disk_template=disk_template,
1085 remote_node=remote_node, os_name=os_name,
1086 force_variant=force_variant)
1087
1090 """/2/instances/[instance_name]/modify resource.
1091
1092 """
1104
1169
1178
1187
1196