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", "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", "valid", "variants"],
152 names=[])
153 job_id = baserlib.SubmitJob([op], cl)
154
155 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
156 diagnose_data = result[0]
157
158 if not isinstance(diagnose_data, list):
159 raise http.HttpBadGateway(message="Can't get OS list")
160
161 os_names = []
162 for (name, valid, variants) in diagnose_data:
163 if valid:
164 os_names.extend(cli.CalculateOSNames(name, variants))
165
166 return os_names
167
170 """/2/redistribute-config resource.
171
172 """
173 @staticmethod
179
182 """/2/jobs resource.
183
184 """
185 @staticmethod
187 """Returns a dictionary of jobs.
188
189 @return: a dictionary with jobs id and uri.
190
191 """
192 fields = ["id"]
193 cl = baserlib.GetClient()
194
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"))
198
201 """/2/jobs/[job_id] resource.
202
203 """
205 """Returns a job status.
206
207 @return: a dictionary with job parameters.
208 The result includes:
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
212 opcodes in the job
213 - opstatus: OpCodes status as a list
214 - opresult: OpCodes results as a list of lists
215
216 """
217 fields = ["id", "ops", "status", "summary",
218 "opstatus", "opresult", "oplog",
219 "received_ts", "start_ts", "end_ts",
220 ]
221 job_id = self.items[0]
222 result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
223 if result is None:
224 raise http.HttpNotFound()
225 return baserlib.MapFields(fields, result)
226
228 """Cancel not-yet-started job.
229
230 """
231 job_id = self.items[0]
232 result = baserlib.GetClient().CancelJob(job_id)
233 return result
234
237 """/2/jobs/[job_id]/wait resource.
238
239 """
240
241
242 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
243
245 """Waits for job changes.
246
247 """
248 job_id = self.items[0]
249
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)
253
254 if not isinstance(fields, list):
255 raise http.HttpBadRequest("The 'fields' parameter should be a list")
256
257 if not (prev_job_info is None or isinstance(prev_job_info, list)):
258 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
259 " be a list")
260
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"
264 " be a number")
265
266 client = baserlib.GetClient()
267 result = client.WaitForJobChangeOnce(job_id, fields,
268 prev_job_info, prev_log_serial,
269 timeout=_WFJC_TIMEOUT)
270 if not result:
271 raise http.HttpNotFound()
272
273 if result == constants.JOB_NOTCHANGED:
274
275 return None
276
277 (job_info, log_entries) = result
278
279 return {
280 "job_info": job_info,
281 "log_entries": log_entries,
282 }
283
286 """/2/nodes resource.
287
288 """
303
306 """/2/nodes/[node_name] resources.
307
308 """
321
324 """ /2/nodes/[node_name]/role resource.
325
326 """
339
341 """Sets the node role.
342
343 @return: a job id
344
345 """
346 if not isinstance(self.req.request_body, basestring):
347 raise http.HttpBadRequest("Invalid body contents, not a string")
348
349 node_name = self.items[0]
350 role = self.req.request_body
351
352 if role == _NR_REGULAR:
353 candidate = False
354 offline = False
355 drained = False
356
357 elif role == _NR_MASTER_CANDIATE:
358 candidate = True
359 offline = drained = None
360
361 elif role == _NR_DRAINED:
362 drained = True
363 candidate = offline = None
364
365 elif role == _NR_OFFLINE:
366 offline = True
367 candidate = drained = None
368
369 else:
370 raise http.HttpBadRequest("Can't set '%s' role" % role)
371
372 op = opcodes.OpSetNodeParams(node_name=node_name,
373 master_candidate=candidate,
374 offline=offline,
375 drained=drained,
376 force=bool(self.useForce()))
377
378 return baserlib.SubmitJob([op])
379
382 """/2/nodes/[node_name]/evacuate resource.
383
384 """
386 """Evacuate all secondary instances off a node.
387
388 """
389 node_name = self.items[0]
390 remote_node = self._checkStringVariable("remote_node", default=None)
391 iallocator = self._checkStringVariable("iallocator", default=None)
392
393 op = opcodes.OpEvacuateNode(node_name=node_name,
394 remote_node=remote_node,
395 iallocator=iallocator)
396
397 return baserlib.SubmitJob([op])
398
401 """/2/nodes/[node_name]/migrate resource.
402
403 """
405 """Migrate all primary instances from a node.
406
407 """
408 node_name = self.items[0]
409 live = bool(self._checkIntVariable("live", default=1))
410
411 op = opcodes.OpMigrateNode(node_name=node_name, live=live)
412
413 return baserlib.SubmitJob([op])
414
440
443 """/2/nodes/[node_name]/storage/modify ressource.
444
445 """
470
473 """/2/nodes/[node_name]/storage/repair ressource.
474
475 """
493
496 """Parses an instance creation request version 1.
497
498 @rtype: L{opcodes.OpCreateInstance}
499 @return: Instance creation opcode
500
501 """
502
503 disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
504
505 disks = []
506 for idx, i in enumerate(disks_input):
507 baserlib.CheckType(i, dict, "Disk %d specification" % idx)
508
509
510 try:
511 size = i[constants.IDISK_SIZE]
512 except KeyError:
513 raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
514 " size" % idx)
515
516 disk = {
517 constants.IDISK_SIZE: size,
518 }
519
520
521 try:
522 disk_access = i[constants.IDISK_MODE]
523 except KeyError:
524 pass
525 else:
526 disk[constants.IDISK_MODE] = disk_access
527
528 disks.append(disk)
529
530 assert len(disks_input) == len(disks)
531
532
533 nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
534
535 nics = []
536 for idx, i in enumerate(nics_input):
537 baserlib.CheckType(i, dict, "NIC %d specification" % idx)
538
539 nic = {}
540
541 for field in constants.INIC_PARAMS:
542 try:
543 value = i[field]
544 except KeyError:
545 continue
546
547 nic[field] = value
548
549 nics.append(nic)
550
551 assert len(nics_input) == len(nics)
552
553
554 hvparams = baserlib.CheckParameter(data, "hvparams", default={})
555 utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
556
557 beparams = baserlib.CheckParameter(data, "beparams", default={})
558 utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
559
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",
565 default=False),
566 pnode=baserlib.CheckParameter(data, "pnode", default=None),
567 snode=baserlib.CheckParameter(data, "snode", default=None),
568 disk_template=baserlib.CheckParameter(data, "disk_template"),
569 disks=disks,
570 nics=nics,
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),
574 wait_for_sync=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",
578 default=None),
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),
583 hvparams=hvparams,
584 beparams=beparams,
585 dry_run=dry_run,
586 )
587
590 """/2/instances resource.
591
592 """
608
610 """Parses an instance creation request version 0.
611
612 Request data version 0 is deprecated and should not be used anymore.
613
614 @rtype: L{opcodes.OpCreateInstance}
615 @return: Instance creation opcode
616
617 """
618
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
624
625
626 disk_data = fn('disks')
627 if not isinstance(disk_data, list):
628 raise http.HttpBadRequest("The 'disks' parameter should be a list")
629 disks = []
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})
635
636
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")
646
647
648 return opcodes.OpCreateInstance(
649 mode=constants.INSTANCE_CREATE,
650 instance_name=fn('name'),
651 disks=disks,
652 disk_template=fn('disk_template'),
653 os_type=fn('os'),
654 pnode=fn('pnode', None),
655 snode=fn('snode', None),
656 iallocator=fn('iallocator', None),
657 nics=nics,
658 start=fn('start', True),
659 ip_check=fn('ip_check', True),
660 name_check=fn('name_check', True),
661 wait_for_sync=True,
662 hypervisor=fn('hypervisor', None),
663 hvparams=hvparams,
664 beparams=beparams,
665 file_storage_dir=fn('file_storage_dir', None),
666 file_driver=fn('file_driver', constants.FD_LOOP),
667 dry_run=bool(self.dryRun()),
668 )
669
671 """Create an instance.
672
673 @return: a job id
674
675 """
676 if not isinstance(self.req.request_body, dict):
677 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
678
679
680 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
681
682 if data_version == 0:
683 op = self._ParseVersion0CreateRequest()
684 elif data_version == 1:
685 op = _ParseInstanceCreateRequestVersion1(self.req.request_body,
686 self.dryRun())
687 else:
688 raise http.HttpBadRequest("Unsupported request data version %s" %
689 data_version)
690
691 return baserlib.SubmitJob([op])
692
695 """/2/instances/[instance_name] resources.
696
697 """
711
720
723 """/2/instances/[instance_name]/info resource.
724
725 """
736
739 """/2/instances/[instance_name]/reboot resource.
740
741 Implements an instance reboot.
742
743 """
745 """Reboot an instance.
746
747 The URI takes type=[hard|soft|full] and
748 ignore_secondaries=[False|True] parameters.
749
750 """
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()))
759
760 return baserlib.SubmitJob([op])
761
764 """/2/instances/[instance_name]/startup resource.
765
766 Implements an instance startup.
767
768 """
770 """Startup an instance.
771
772 The URI takes force=[False|True] parameter to start the instance
773 if even if secondary disks are failing.
774
775 """
776 instance_name = self.items[0]
777 force_startup = bool(self._checkIntVariable('force'))
778 op = opcodes.OpStartupInstance(instance_name=instance_name,
779 force=force_startup,
780 dry_run=bool(self.dryRun()))
781
782 return baserlib.SubmitJob([op])
783
786 """/2/instances/[instance_name]/shutdown resource.
787
788 Implements an instance shutdown.
789
790 """
800
803 """/2/instances/[instance_name]/reinstall resource.
804
805 Implements an instance reinstall.
806
807 """
809 """Reinstall an instance.
810
811 The URI takes os=name and nostartup=[0|1] optional
812 parameters. By default, the instance will be started
813 automatically.
814
815 """
816 instance_name = self.items[0]
817 ostype = self._checkStringVariable('os')
818 nostartup = self._checkIntVariable('nostartup')
819 ops = [
820 opcodes.OpShutdownInstance(instance_name=instance_name),
821 opcodes.OpReinstallInstance(instance_name=instance_name, os_type=ostype),
822 ]
823 if not nostartup:
824 ops.append(opcodes.OpStartupInstance(instance_name=instance_name,
825 force=False))
826 return baserlib.SubmitJob(ops)
827
830 """/2/instances/[instance_name]/replace-disks resource.
831
832 """
834 """Replaces disks on an instance.
835
836 """
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)
842
843 if raw_disks:
844 try:
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))
848 else:
849 disks = []
850
851 op = opcodes.OpReplaceDisks(instance_name=instance_name,
852 remote_node=remote_node,
853 mode=mode,
854 disks=disks,
855 iallocator=iallocator)
856
857 return baserlib.SubmitJob([op])
858
861 """/2/instances/[instance_name]/activate-disks resource.
862
863 """
865 """Activate disks for an instance.
866
867 The URI might contain ignore_size to ignore current recorded size.
868
869 """
870 instance_name = self.items[0]
871 ignore_size = bool(self._checkIntVariable('ignore_size'))
872
873 op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
874 ignore_size=ignore_size)
875
876 return baserlib.SubmitJob([op])
877
880 """/2/instances/[instance_name]/deactivate-disks resource.
881
882 """
892
957
966
975
984