Add tags in network objects
[ganeti-github.git] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """Remote API resource implementations.
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 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.
34
35 Quoting from RFC2616, section 9.6::
36
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
47 redirect the request.
48
49 So when adding new methods, if they are operating on the URI entity itself,
50 PUT should be prefered over POST.
51
52 """
53
54 # pylint: disable=C0103
55
56 # C0103: Invalid name, since the R_* names are not conforming
57
58 from ganeti import opcodes
59 from ganeti import objects
60 from ganeti import http
61 from ganeti import constants
62 from ganeti import cli
63 from ganeti import rapi
64 from ganeti import ht
65 from ganeti import compat
66 from ganeti import ssconf
67 from ganeti.rapi import baserlib
68
69
70 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
71 I_FIELDS = ["name", "admin_state", "os",
72 "pnode", "snodes",
73 "disk_template",
74 "nic.ips", "nic.macs", "nic.modes",
75 "nic.links", "nic.networks", "nic.bridges",
76 "network_port",
77 "disk.sizes", "disk_usage",
78 "beparams", "hvparams",
79 "oper_state", "oper_ram", "oper_vcpus", "status",
80 "custom_hvparams", "custom_beparams", "custom_nicparams",
81 ] + _COMMON_FIELDS
82
83 N_FIELDS = ["name", "offline", "master_candidate", "drained",
84 "dtotal", "dfree",
85 "mtotal", "mnode", "mfree",
86 "pinst_cnt", "sinst_cnt",
87 "ctotal", "cnodes", "csockets",
88 "pip", "sip", "role",
89 "pinst_list", "sinst_list",
90 "master_capable", "vm_capable",
91 "ndparams",
92 "group.uuid",
93 ] + _COMMON_FIELDS
94
95 NET_FIELDS = ["name", "network", "gateway",
96 "network6", "gateway6",
97 "mac_prefix", "network_type",
98 "free_count", "reserved_count",
99 "map", "group_list", "inst_list",
100 "external_reservations", "tags",
101 ]
102
103 G_FIELDS = [
104 "alloc_policy",
105 "name",
106 "node_cnt",
107 "node_list",
108 "ipolicy",
109 "custom_ipolicy",
110 "diskparams",
111 "custom_diskparams",
112 "ndparams",
113 "custom_ndparams",
114 ] + _COMMON_FIELDS
115
116 J_FIELDS_BULK = [
117 "id", "ops", "status", "summary",
118 "opstatus",
119 "received_ts", "start_ts", "end_ts",
120 ]
121
122 J_FIELDS = J_FIELDS_BULK + [
123 "oplog",
124 "opresult",
125 ]
126
127 _NR_DRAINED = "drained"
128 _NR_MASTER_CANDIDATE = "master-candidate"
129 _NR_MASTER = "master"
130 _NR_OFFLINE = "offline"
131 _NR_REGULAR = "regular"
132
133 _NR_MAP = {
134 constants.NR_MASTER: _NR_MASTER,
135 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
136 constants.NR_DRAINED: _NR_DRAINED,
137 constants.NR_OFFLINE: _NR_OFFLINE,
138 constants.NR_REGULAR: _NR_REGULAR,
139 }
140
141 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
142
143 # Request data version field
144 _REQ_DATA_VERSION = "__version__"
145
146 # Feature string for instance creation request data version 1
147 _INST_CREATE_REQV1 = "instance-create-reqv1"
148
149 # Feature string for instance reinstall request version 1
150 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
151
152 # Feature string for node migration version 1
153 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
154
155 # Feature string for node evacuation with LU-generated jobs
156 _NODE_EVAC_RES1 = "node-evac-res1"
157
158 ALL_FEATURES = frozenset([
159 _INST_CREATE_REQV1,
160 _INST_REINSTALL_REQV1,
161 _NODE_MIGRATE_REQV1,
162 _NODE_EVAC_RES1,
163 ])
164
165 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
166 _WFJC_TIMEOUT = 10
167
168
169 # FIXME: For compatibility we update the beparams/memory field. Needs to be
170 # removed in Ganeti 2.7
171 def _UpdateBeparams(inst):
172 """Updates the beparams dict of inst to support the memory field.
173
174 @param inst: Inst dict
175 @return: Updated inst dict
176
177 """
178 beparams = inst["beparams"]
179 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
180
181 return inst
182
183
184 class R_root(baserlib.ResourceBase):
185 """/ resource.
186
187 """
188 @staticmethod
189 def GET():
190 """Supported for legacy reasons.
191
192 """
193 return None
194
195
196 class R_2(R_root):
197 """/2 resource.
198
199 """
200
201
202 class R_version(baserlib.ResourceBase):
203 """/version resource.
204
205 This resource should be used to determine the remote API version and
206 to adapt clients accordingly.
207
208 """
209 @staticmethod
210 def GET():
211 """Returns the remote API version.
212
213 """
214 return constants.RAPI_VERSION
215
216
217 class R_2_info(baserlib.OpcodeResource):
218 """/2/info resource.
219
220 """
221 GET_OPCODE = opcodes.OpClusterQuery
222
223 def GET(self):
224 """Returns cluster information.
225
226 """
227 client = self.GetClient(query=True)
228 return client.QueryClusterInfo()
229
230
231 class R_2_features(baserlib.ResourceBase):
232 """/2/features resource.
233
234 """
235 @staticmethod
236 def GET():
237 """Returns list of optional RAPI features implemented.
238
239 """
240 return list(ALL_FEATURES)
241
242
243 class R_2_os(baserlib.OpcodeResource):
244 """/2/os resource.
245
246 """
247 GET_OPCODE = opcodes.OpOsDiagnose
248
249 def GET(self):
250 """Return a list of all OSes.
251
252 Can return error 500 in case of a problem.
253
254 Example: ["debian-etch"]
255
256 """
257 cl = self.GetClient()
258 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
259 job_id = self.SubmitJob([op], cl=cl)
260 # we use custom feedback function, instead of print we log the status
261 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
262 diagnose_data = result[0]
263
264 if not isinstance(diagnose_data, list):
265 raise http.HttpBadGateway(message="Can't get OS list")
266
267 os_names = []
268 for (name, variants) in diagnose_data:
269 os_names.extend(cli.CalculateOSNames(name, variants))
270
271 return os_names
272
273
274 class R_2_redist_config(baserlib.OpcodeResource):
275 """/2/redistribute-config resource.
276
277 """
278 PUT_OPCODE = opcodes.OpClusterRedistConf
279
280
281 class R_2_cluster_modify(baserlib.OpcodeResource):
282 """/2/modify resource.
283
284 """
285 PUT_OPCODE = opcodes.OpClusterSetParams
286
287
288 class R_2_jobs(baserlib.ResourceBase):
289 """/2/jobs resource.
290
291 """
292 def GET(self):
293 """Returns a dictionary of jobs.
294
295 @return: a dictionary with jobs id and uri.
296
297 """
298 client = self.GetClient()
299
300 if self.useBulk():
301 bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
302 return baserlib.MapBulkFields(bulkdata, J_FIELDS_BULK)
303 else:
304 jobdata = map(compat.fst, client.QueryJobs(None, ["id"]))
305 return baserlib.BuildUriList(jobdata, "/2/jobs/%s",
306 uri_fields=("id", "uri"))
307
308
309 class R_2_jobs_id(baserlib.ResourceBase):
310 """/2/jobs/[job_id] resource.
311
312 """
313 def GET(self):
314 """Returns a job status.
315
316 @return: a dictionary with job parameters.
317 The result includes:
318 - id: job ID as a number
319 - status: current job status as a string
320 - ops: involved OpCodes as a list of dictionaries for each
321 opcodes in the job
322 - opstatus: OpCodes status as a list
323 - opresult: OpCodes results as a list of lists
324
325 """
326 job_id = self.items[0]
327 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
328 if result is None:
329 raise http.HttpNotFound()
330 return baserlib.MapFields(J_FIELDS, result)
331
332 def DELETE(self):
333 """Cancel not-yet-started job.
334
335 """
336 job_id = self.items[0]
337 result = self.GetClient().CancelJob(job_id)
338 return result
339
340
341 class R_2_jobs_id_wait(baserlib.ResourceBase):
342 """/2/jobs/[job_id]/wait resource.
343
344 """
345 # WaitForJobChange provides access to sensitive information and blocks
346 # machine resources (it's a blocking RAPI call), hence restricting access.
347 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
348
349 def GET(self):
350 """Waits for job changes.
351
352 """
353 job_id = self.items[0]
354
355 fields = self.getBodyParameter("fields")
356 prev_job_info = self.getBodyParameter("previous_job_info", None)
357 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
358
359 if not isinstance(fields, list):
360 raise http.HttpBadRequest("The 'fields' parameter should be a list")
361
362 if not (prev_job_info is None or isinstance(prev_job_info, list)):
363 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
364 " be a list")
365
366 if not (prev_log_serial is None or
367 isinstance(prev_log_serial, (int, long))):
368 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
369 " be a number")
370
371 client = self.GetClient()
372 result = client.WaitForJobChangeOnce(job_id, fields,
373 prev_job_info, prev_log_serial,
374 timeout=_WFJC_TIMEOUT)
375 if not result:
376 raise http.HttpNotFound()
377
378 if result == constants.JOB_NOTCHANGED:
379 # No changes
380 return None
381
382 (job_info, log_entries) = result
383
384 return {
385 "job_info": job_info,
386 "log_entries": log_entries,
387 }
388
389
390 class R_2_nodes(baserlib.OpcodeResource):
391 """/2/nodes resource.
392
393 """
394 GET_OPCODE = opcodes.OpNodeQuery
395
396 def GET(self):
397 """Returns a list of all nodes.
398
399 """
400 client = self.GetClient(query=True)
401
402 if self.useBulk():
403 bulkdata = client.QueryNodes([], N_FIELDS, False)
404 return baserlib.MapBulkFields(bulkdata, N_FIELDS)
405 else:
406 nodesdata = client.QueryNodes([], ["name"], False)
407 nodeslist = [row[0] for row in nodesdata]
408 return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
409 uri_fields=("id", "uri"))
410
411
412 class R_2_nodes_name(baserlib.OpcodeResource):
413 """/2/nodes/[node_name] resource.
414
415 """
416 GET_OPCODE = opcodes.OpNodeQuery
417
418 def GET(self):
419 """Send information about a node.
420
421 """
422 node_name = self.items[0]
423 client = self.GetClient(query=True)
424
425 result = baserlib.HandleItemQueryErrors(client.QueryNodes,
426 names=[node_name], fields=N_FIELDS,
427 use_locking=self.useLocking())
428
429 return baserlib.MapFields(N_FIELDS, result[0])
430
431
432 class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
433 """/2/nodes/[node_name]/powercycle resource.
434
435 """
436 POST_OPCODE = opcodes.OpNodePowercycle
437
438 def GetPostOpInput(self):
439 """Tries to powercycle a node.
440
441 """
442 return (self.request_body, {
443 "node_name": self.items[0],
444 "force": self.useForce(),
445 })
446
447
448 class R_2_nodes_name_role(baserlib.OpcodeResource):
449 """/2/nodes/[node_name]/role resource.
450
451 """
452 PUT_OPCODE = opcodes.OpNodeSetParams
453
454 def GET(self):
455 """Returns the current node role.
456
457 @return: Node role
458
459 """
460 node_name = self.items[0]
461 client = self.GetClient(query=True)
462 result = client.QueryNodes(names=[node_name], fields=["role"],
463 use_locking=self.useLocking())
464
465 return _NR_MAP[result[0][0]]
466
467 def GetPutOpInput(self):
468 """Sets the node role.
469
470 """
471 baserlib.CheckType(self.request_body, basestring, "Body contents")
472
473 role = self.request_body
474
475 if role == _NR_REGULAR:
476 candidate = False
477 offline = False
478 drained = False
479
480 elif role == _NR_MASTER_CANDIDATE:
481 candidate = True
482 offline = drained = None
483
484 elif role == _NR_DRAINED:
485 drained = True
486 candidate = offline = None
487
488 elif role == _NR_OFFLINE:
489 offline = True
490 candidate = drained = None
491
492 else:
493 raise http.HttpBadRequest("Can't set '%s' role" % role)
494
495 assert len(self.items) == 1
496
497 return ({}, {
498 "node_name": self.items[0],
499 "master_candidate": candidate,
500 "offline": offline,
501 "drained": drained,
502 "force": self.useForce(),
503 "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
504 })
505
506
507 class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
508 """/2/nodes/[node_name]/evacuate resource.
509
510 """
511 POST_OPCODE = opcodes.OpNodeEvacuate
512
513 def GetPostOpInput(self):
514 """Evacuate all instances off a node.
515
516 """
517 return (self.request_body, {
518 "node_name": self.items[0],
519 "dry_run": self.dryRun(),
520 })
521
522
523 class R_2_nodes_name_migrate(baserlib.OpcodeResource):
524 """/2/nodes/[node_name]/migrate resource.
525
526 """
527 POST_OPCODE = opcodes.OpNodeMigrate
528
529 def GetPostOpInput(self):
530 """Migrate all primary instances from a node.
531
532 """
533 if self.queryargs:
534 # Support old-style requests
535 if "live" in self.queryargs and "mode" in self.queryargs:
536 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
537 " be passed")
538
539 if "live" in self.queryargs:
540 if self._checkIntVariable("live", default=1):
541 mode = constants.HT_MIGRATION_LIVE
542 else:
543 mode = constants.HT_MIGRATION_NONLIVE
544 else:
545 mode = self._checkStringVariable("mode", default=None)
546
547 data = {
548 "mode": mode,
549 }
550 else:
551 data = self.request_body
552
553 return (data, {
554 "node_name": self.items[0],
555 })
556
557
558 class R_2_nodes_name_modify(baserlib.OpcodeResource):
559 """/2/nodes/[node_name]/modify resource.
560
561 """
562 POST_OPCODE = opcodes.OpNodeSetParams
563
564 def GetPostOpInput(self):
565 """Changes parameters of a node.
566
567 """
568 assert len(self.items) == 1
569
570 return (self.request_body, {
571 "node_name": self.items[0],
572 })
573
574
575 class R_2_nodes_name_storage(baserlib.OpcodeResource):
576 """/2/nodes/[node_name]/storage resource.
577
578 """
579 # LUNodeQueryStorage acquires locks, hence restricting access to GET
580 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
581 GET_OPCODE = opcodes.OpNodeQueryStorage
582
583 def GetGetOpInput(self):
584 """List storage available on a node.
585
586 """
587 storage_type = self._checkStringVariable("storage_type", None)
588 output_fields = self._checkStringVariable("output_fields", None)
589
590 if not output_fields:
591 raise http.HttpBadRequest("Missing the required 'output_fields'"
592 " parameter")
593
594 return ({}, {
595 "nodes": [self.items[0]],
596 "storage_type": storage_type,
597 "output_fields": output_fields.split(","),
598 })
599
600
601 class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
602 """/2/nodes/[node_name]/storage/modify resource.
603
604 """
605 PUT_OPCODE = opcodes.OpNodeModifyStorage
606
607 def GetPutOpInput(self):
608 """Modifies a storage volume on a node.
609
610 """
611 storage_type = self._checkStringVariable("storage_type", None)
612 name = self._checkStringVariable("name", None)
613
614 if not name:
615 raise http.HttpBadRequest("Missing the required 'name'"
616 " parameter")
617
618 changes = {}
619
620 if "allocatable" in self.queryargs:
621 changes[constants.SF_ALLOCATABLE] = \
622 bool(self._checkIntVariable("allocatable", default=1))
623
624 return ({}, {
625 "node_name": self.items[0],
626 "storage_type": storage_type,
627 "name": name,
628 "changes": changes,
629 })
630
631
632 class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
633 """/2/nodes/[node_name]/storage/repair resource.
634
635 """
636 PUT_OPCODE = opcodes.OpRepairNodeStorage
637
638 def GetPutOpInput(self):
639 """Repairs a storage volume on a node.
640
641 """
642 storage_type = self._checkStringVariable("storage_type", None)
643 name = self._checkStringVariable("name", None)
644 if not name:
645 raise http.HttpBadRequest("Missing the required 'name'"
646 " parameter")
647
648 return ({}, {
649 "node_name": self.items[0],
650 "storage_type": storage_type,
651 "name": name,
652 })
653
654
655 class R_2_networks(baserlib.OpcodeResource):
656 """/2/networks resource.
657
658 """
659 GET_OPCODE = opcodes.OpNetworkQuery
660 POST_OPCODE = opcodes.OpNetworkAdd
661 POST_RENAME = {
662 "name": "network_name",
663 }
664
665 def GetPostOpInput(self):
666 """Create a network.
667
668 """
669 assert not self.items
670 return (self.request_body, {
671 "dry_run": self.dryRun(),
672 })
673
674 def GET(self):
675 """Returns a list of all networks.
676
677 """
678 client = self.GetClient()
679
680 if self.useBulk():
681 bulkdata = client.QueryNetworks([], NET_FIELDS, False)
682 return baserlib.MapBulkFields(bulkdata, NET_FIELDS)
683 else:
684 data = client.QueryNetworks([], ["name"], False)
685 networknames = [row[0] for row in data]
686 return baserlib.BuildUriList(networknames, "/2/networks/%s",
687 uri_fields=("name", "uri"))
688
689
690 class R_2_networks_name(baserlib.OpcodeResource):
691 """/2/network/[network_name] resource.
692
693 """
694 DELETE_OPCODE = opcodes.OpNetworkRemove
695
696 def GET(self):
697 """Send information about a network.
698
699 """
700 network_name = self.items[0]
701 client = self.GetClient()
702
703 result = baserlib.HandleItemQueryErrors(client.QueryNetworks,
704 names=[network_name],
705 fields=NET_FIELDS,
706 use_locking=self.useLocking())
707
708 return baserlib.MapFields(NET_FIELDS, result[0])
709
710 def GetDeleteOpInput(self):
711 """Delete a network.
712
713 """
714 assert len(self.items) == 1
715 return (self.request_body, {
716 "network_name": self.items[0],
717 "dry_run": self.dryRun(),
718 })
719
720 class R_2_networks_name_connect(baserlib.OpcodeResource):
721 """/2/network/[network_name]/connect.
722
723 """
724 PUT_OPCODE = opcodes.OpNetworkConnect
725
726 def GetPutOpInput(self):
727 """Changes some parameters of node group.
728
729 """
730 assert self.items
731 return (self.request_body, {
732 "network_name": self.items[0],
733 })
734
735 class R_2_networks_name_disconnect(baserlib.OpcodeResource):
736 """/2/network/[network_name]/disconnect.
737
738 """
739 PUT_OPCODE = opcodes.OpNetworkDisconnect
740
741 def GetPutOpInput(self):
742 """Changes some parameters of node group.
743
744 """
745 assert self.items
746 return (self.request_body, {
747 "network_name": self.items[0],
748 })
749
750 class R_2_groups(baserlib.OpcodeResource):
751 """/2/groups resource.
752
753 """
754 GET_OPCODE = opcodes.OpGroupQuery
755 POST_OPCODE = opcodes.OpGroupAdd
756 POST_RENAME = {
757 "name": "group_name",
758 }
759
760 def GetPostOpInput(self):
761 """Create a node group.
762
763
764 """
765 assert not self.items
766 return (self.request_body, {
767 "dry_run": self.dryRun(),
768 })
769
770 def GET(self):
771 """Returns a list of all node groups.
772
773 """
774 client = self.GetClient(query=True)
775
776 if self.useBulk():
777 bulkdata = client.QueryGroups([], G_FIELDS, False)
778 return baserlib.MapBulkFields(bulkdata, G_FIELDS)
779 else:
780 data = client.QueryGroups([], ["name"], False)
781 groupnames = [row[0] for row in data]
782 return baserlib.BuildUriList(groupnames, "/2/groups/%s",
783 uri_fields=("name", "uri"))
784
785
786 class R_2_groups_name(baserlib.OpcodeResource):
787 """/2/groups/[group_name] resource.
788
789 """
790 DELETE_OPCODE = opcodes.OpGroupRemove
791
792 def GET(self):
793 """Send information about a node group.
794
795 """
796 group_name = self.items[0]
797 client = self.GetClient(query=True)
798
799 result = baserlib.HandleItemQueryErrors(client.QueryGroups,
800 names=[group_name], fields=G_FIELDS,
801 use_locking=self.useLocking())
802
803 return baserlib.MapFields(G_FIELDS, result[0])
804
805 def GetDeleteOpInput(self):
806 """Delete a node group.
807
808 """
809 assert len(self.items) == 1
810 return ({}, {
811 "group_name": self.items[0],
812 "dry_run": self.dryRun(),
813 })
814
815
816 class R_2_groups_name_modify(baserlib.OpcodeResource):
817 """/2/groups/[group_name]/modify resource.
818
819 """
820 PUT_OPCODE = opcodes.OpGroupSetParams
821
822 def GetPutOpInput(self):
823 """Changes some parameters of node group.
824
825 """
826 assert self.items
827 return (self.request_body, {
828 "group_name": self.items[0],
829 })
830
831
832 class R_2_groups_name_rename(baserlib.OpcodeResource):
833 """/2/groups/[group_name]/rename resource.
834
835 """
836 PUT_OPCODE = opcodes.OpGroupRename
837
838 def GetPutOpInput(self):
839 """Changes the name of a node group.
840
841 """
842 assert len(self.items) == 1
843 return (self.request_body, {
844 "group_name": self.items[0],
845 "dry_run": self.dryRun(),
846 })
847
848
849 class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
850 """/2/groups/[group_name]/assign-nodes resource.
851
852 """
853 PUT_OPCODE = opcodes.OpGroupAssignNodes
854
855 def GetPutOpInput(self):
856 """Assigns nodes to a group.
857
858 """
859 assert len(self.items) == 1
860 return (self.request_body, {
861 "group_name": self.items[0],
862 "dry_run": self.dryRun(),
863 "force": self.useForce(),
864 })
865
866
867 class R_2_instances(baserlib.OpcodeResource):
868 """/2/instances resource.
869
870 """
871 GET_OPCODE = opcodes.OpInstanceQuery
872 POST_OPCODE = opcodes.OpInstanceCreate
873 POST_RENAME = {
874 "os": "os_type",
875 "name": "instance_name",
876 }
877
878 def GET(self):
879 """Returns a list of all available instances.
880
881 """
882 client = self.GetClient()
883
884 use_locking = self.useLocking()
885 if self.useBulk():
886 bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
887 return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
888 else:
889 instancesdata = client.QueryInstances([], ["name"], use_locking)
890 instanceslist = [row[0] for row in instancesdata]
891 return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
892 uri_fields=("id", "uri"))
893
894 def GetPostOpInput(self):
895 """Create an instance.
896
897 @return: a job id
898
899 """
900 baserlib.CheckType(self.request_body, dict, "Body contents")
901
902 # Default to request data version 0
903 data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
904
905 if data_version == 0:
906 raise http.HttpBadRequest("Instance creation request version 0 is no"
907 " longer supported")
908 elif data_version != 1:
909 raise http.HttpBadRequest("Unsupported request data version %s" %
910 data_version)
911
912 data = self.request_body.copy()
913 # Remove "__version__"
914 data.pop(_REQ_DATA_VERSION, None)
915
916 return (data, {
917 "dry_run": self.dryRun(),
918 })
919
920
921 class R_2_instances_multi_alloc(baserlib.OpcodeResource):
922 """/2/instances-multi-alloc resource.
923
924 """
925 POST_OPCODE = opcodes.OpInstanceMultiAlloc
926
927 def GetPostOpInput(self):
928 """Try to allocate multiple instances.
929
930 @return: A dict with submitted jobs, allocatable instances and failed
931 allocations
932
933 """
934 if "instances" not in self.request_body:
935 raise http.HttpBadRequest("Request is missing required 'instances' field"
936 " in body")
937
938 op_id = {
939 "OP_ID": self.POST_OPCODE.OP_ID, # pylint: disable=E1101
940 }
941 body = objects.FillDict(self.request_body, {
942 "instances": [objects.FillDict(inst, op_id)
943 for inst in self.request_body["instances"]],
944 })
945
946 return (body, {
947 "dry_run": self.dryRun(),
948 })
949
950
951 class R_2_instances_name(baserlib.OpcodeResource):
952 """/2/instances/[instance_name] resource.
953
954 """
955 GET_OPCODE = opcodes.OpInstanceQuery
956 DELETE_OPCODE = opcodes.OpInstanceRemove
957
958 def GET(self):
959 """Send information about an instance.
960
961 """
962 client = self.GetClient()
963 instance_name = self.items[0]
964
965 result = baserlib.HandleItemQueryErrors(client.QueryInstances,
966 names=[instance_name],
967 fields=I_FIELDS,
968 use_locking=self.useLocking())
969
970 return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
971
972 def GetDeleteOpInput(self):
973 """Delete an instance.
974
975 """
976 assert len(self.items) == 1
977 return ({}, {
978 "instance_name": self.items[0],
979 "ignore_failures": False,
980 "dry_run": self.dryRun(),
981 })
982
983
984 class R_2_instances_name_info(baserlib.OpcodeResource):
985 """/2/instances/[instance_name]/info resource.
986
987 """
988 GET_OPCODE = opcodes.OpInstanceQueryData
989
990 def GetGetOpInput(self):
991 """Request detailed instance information.
992
993 """
994 assert len(self.items) == 1
995 return ({}, {
996 "instances": [self.items[0]],
997 "static": bool(self._checkIntVariable("static", default=0)),
998 })
999
1000
1001 class R_2_instances_name_reboot(baserlib.OpcodeResource):
1002 """/2/instances/[instance_name]/reboot resource.
1003
1004 Implements an instance reboot.
1005
1006 """
1007 POST_OPCODE = opcodes.OpInstanceReboot
1008
1009 def GetPostOpInput(self):
1010 """Reboot an instance.
1011
1012 The URI takes type=[hard|soft|full] and
1013 ignore_secondaries=[False|True] parameters.
1014
1015 """
1016 return ({}, {
1017 "instance_name": self.items[0],
1018 "reboot_type":
1019 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1020 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1021 "dry_run": self.dryRun(),
1022 })
1023
1024
1025 class R_2_instances_name_startup(baserlib.OpcodeResource):
1026 """/2/instances/[instance_name]/startup resource.
1027
1028 Implements an instance startup.
1029
1030 """
1031 PUT_OPCODE = opcodes.OpInstanceStartup
1032
1033 def GetPutOpInput(self):
1034 """Startup an instance.
1035
1036 The URI takes force=[False|True] parameter to start the instance
1037 if even if secondary disks are failing.
1038
1039 """
1040 return ({}, {
1041 "instance_name": self.items[0],
1042 "force": self.useForce(),
1043 "dry_run": self.dryRun(),
1044 "no_remember": bool(self._checkIntVariable("no_remember")),
1045 })
1046
1047
1048 class R_2_instances_name_shutdown(baserlib.OpcodeResource):
1049 """/2/instances/[instance_name]/shutdown resource.
1050
1051 Implements an instance shutdown.
1052
1053 """
1054 PUT_OPCODE = opcodes.OpInstanceShutdown
1055
1056 def GetPutOpInput(self):
1057 """Shutdown an instance.
1058
1059 """
1060 return (self.request_body, {
1061 "instance_name": self.items[0],
1062 "no_remember": bool(self._checkIntVariable("no_remember")),
1063 "dry_run": self.dryRun(),
1064 })
1065
1066
1067 def _ParseInstanceReinstallRequest(name, data):
1068 """Parses a request for reinstalling an instance.
1069
1070 """
1071 if not isinstance(data, dict):
1072 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1073
1074 ostype = baserlib.CheckParameter(data, "os", default=None)
1075 start = baserlib.CheckParameter(data, "start", exptype=bool,
1076 default=True)
1077 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1078
1079 ops = [
1080 opcodes.OpInstanceShutdown(instance_name=name),
1081 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1082 osparams=osparams),
1083 ]
1084
1085 if start:
1086 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1087
1088 return ops
1089
1090
1091 class R_2_instances_name_reinstall(baserlib.OpcodeResource):
1092 """/2/instances/[instance_name]/reinstall resource.
1093
1094 Implements an instance reinstall.
1095
1096 """
1097 POST_OPCODE = opcodes.OpInstanceReinstall
1098
1099 def POST(self):
1100 """Reinstall an instance.
1101
1102 The URI takes os=name and nostartup=[0|1] optional
1103 parameters. By default, the instance will be started
1104 automatically.
1105
1106 """
1107 if self.request_body:
1108 if self.queryargs:
1109 raise http.HttpBadRequest("Can't combine query and body parameters")
1110
1111 body = self.request_body
1112 elif self.queryargs:
1113 # Legacy interface, do not modify/extend
1114 body = {
1115 "os": self._checkStringVariable("os"),
1116 "start": not self._checkIntVariable("nostartup"),
1117 }
1118 else:
1119 body = {}
1120
1121 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1122
1123 return self.SubmitJob(ops)
1124
1125
1126 class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
1127 """/2/instances/[instance_name]/replace-disks resource.
1128
1129 """
1130 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1131
1132 def GetPostOpInput(self):
1133 """Replaces disks on an instance.
1134
1135 """
1136 static = {
1137 "instance_name": self.items[0],
1138 }
1139
1140 if self.request_body:
1141 data = self.request_body
1142 elif self.queryargs:
1143 # Legacy interface, do not modify/extend
1144 data = {
1145 "remote_node": self._checkStringVariable("remote_node", default=None),
1146 "mode": self._checkStringVariable("mode", default=None),
1147 "disks": self._checkStringVariable("disks", default=None),
1148 "iallocator": self._checkStringVariable("iallocator", default=None),
1149 }
1150 else:
1151 data = {}
1152
1153 # Parse disks
1154 try:
1155 raw_disks = data.pop("disks")
1156 except KeyError:
1157 pass
1158 else:
1159 if raw_disks:
1160 if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
1161 data["disks"] = raw_disks
1162 else:
1163 # Backwards compatibility for strings of the format "1, 2, 3"
1164 try:
1165 data["disks"] = [int(part) for part in raw_disks.split(",")]
1166 except (TypeError, ValueError), err:
1167 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1168
1169 return (data, static)
1170
1171
1172 class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
1173 """/2/instances/[instance_name]/activate-disks resource.
1174
1175 """
1176 PUT_OPCODE = opcodes.OpInstanceActivateDisks
1177
1178 def GetPutOpInput(self):
1179 """Activate disks for an instance.
1180
1181 The URI might contain ignore_size to ignore current recorded size.
1182
1183 """
1184 return ({}, {
1185 "instance_name": self.items[0],
1186 "ignore_size": bool(self._checkIntVariable("ignore_size")),
1187 })
1188
1189
1190 class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
1191 """/2/instances/[instance_name]/deactivate-disks resource.
1192
1193 """
1194 PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
1195
1196 def GetPutOpInput(self):
1197 """Deactivate disks for an instance.
1198
1199 """
1200 return ({}, {
1201 "instance_name": self.items[0],
1202 })
1203
1204
1205 class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
1206 """/2/instances/[instance_name]/recreate-disks resource.
1207
1208 """
1209 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1210
1211 def GetPostOpInput(self):
1212 """Recreate disks for an instance.
1213
1214 """
1215 return ({}, {
1216 "instance_name": self.items[0],
1217 })
1218
1219
1220 class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
1221 """/2/instances/[instance_name]/prepare-export resource.
1222
1223 """
1224 PUT_OPCODE = opcodes.OpBackupPrepare
1225
1226 def GetPutOpInput(self):
1227 """Prepares an export for an instance.
1228
1229 """
1230 return ({}, {
1231 "instance_name": self.items[0],
1232 "mode": self._checkStringVariable("mode"),
1233 })
1234
1235
1236 class R_2_instances_name_export(baserlib.OpcodeResource):
1237 """/2/instances/[instance_name]/export resource.
1238
1239 """
1240 PUT_OPCODE = opcodes.OpBackupExport
1241 PUT_RENAME = {
1242 "destination": "target_node",
1243 }
1244
1245 def GetPutOpInput(self):
1246 """Exports an instance.
1247
1248 """
1249 return (self.request_body, {
1250 "instance_name": self.items[0],
1251 })
1252
1253
1254 class R_2_instances_name_migrate(baserlib.OpcodeResource):
1255 """/2/instances/[instance_name]/migrate resource.
1256
1257 """
1258 PUT_OPCODE = opcodes.OpInstanceMigrate
1259
1260 def GetPutOpInput(self):
1261 """Migrates an instance.
1262
1263 """
1264 return (self.request_body, {
1265 "instance_name": self.items[0],
1266 })
1267
1268
1269 class R_2_instances_name_failover(baserlib.OpcodeResource):
1270 """/2/instances/[instance_name]/failover resource.
1271
1272 """
1273 PUT_OPCODE = opcodes.OpInstanceFailover
1274
1275 def GetPutOpInput(self):
1276 """Does a failover of an instance.
1277
1278 """
1279 return (self.request_body, {
1280 "instance_name": self.items[0],
1281 })
1282
1283
1284 class R_2_instances_name_rename(baserlib.OpcodeResource):
1285 """/2/instances/[instance_name]/rename resource.
1286
1287 """
1288 PUT_OPCODE = opcodes.OpInstanceRename
1289
1290 def GetPutOpInput(self):
1291 """Changes the name of an instance.
1292
1293 """
1294 return (self.request_body, {
1295 "instance_name": self.items[0],
1296 })
1297
1298
1299 class R_2_instances_name_modify(baserlib.OpcodeResource):
1300 """/2/instances/[instance_name]/modify resource.
1301
1302 """
1303 PUT_OPCODE = opcodes.OpInstanceSetParams
1304
1305 def GetPutOpInput(self):
1306 """Changes parameters of an instance.
1307
1308 """
1309 return (self.request_body, {
1310 "instance_name": self.items[0],
1311 })
1312
1313
1314 class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
1315 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1316
1317 """
1318 POST_OPCODE = opcodes.OpInstanceGrowDisk
1319
1320 def GetPostOpInput(self):
1321 """Increases the size of an instance disk.
1322
1323 """
1324 return (self.request_body, {
1325 "instance_name": self.items[0],
1326 "disk": int(self.items[1]),
1327 })
1328
1329
1330 class R_2_instances_name_console(baserlib.ResourceBase):
1331 """/2/instances/[instance_name]/console resource.
1332
1333 """
1334 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1335 GET_OPCODE = opcodes.OpInstanceConsole
1336
1337 def GET(self):
1338 """Request information for connecting to instance's console.
1339
1340 @return: Serialized instance console description, see
1341 L{objects.InstanceConsole}
1342
1343 """
1344 client = self.GetClient()
1345
1346 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1347
1348 if console is None:
1349 raise http.HttpServiceUnavailable("Instance console unavailable")
1350
1351 assert isinstance(console, dict)
1352 return console
1353
1354
1355 def _GetQueryFields(args):
1356 """Tries to extract C{fields} query parameter.
1357
1358 @type args: dictionary
1359 @rtype: list of string
1360 @raise http.HttpBadRequest: When parameter can't be found
1361
1362 """
1363 try:
1364 fields = args["fields"]
1365 except KeyError:
1366 raise http.HttpBadRequest("Missing 'fields' query argument")
1367
1368 return _SplitQueryFields(fields[0])
1369
1370
1371 def _SplitQueryFields(fields):
1372 """Splits fields as given for a query request.
1373
1374 @type fields: string
1375 @rtype: list of string
1376
1377 """
1378 return [i.strip() for i in fields.split(",")]
1379
1380
1381 class R_2_query(baserlib.ResourceBase):
1382 """/2/query/[resource] resource.
1383
1384 """
1385 # Results might contain sensitive information
1386 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1387 PUT_ACCESS = GET_ACCESS
1388 GET_OPCODE = opcodes.OpQuery
1389 PUT_OPCODE = opcodes.OpQuery
1390
1391 def _Query(self, fields, qfilter):
1392 return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
1393
1394 def GET(self):
1395 """Returns resource information.
1396
1397 @return: Query result, see L{objects.QueryResponse}
1398
1399 """
1400 return self._Query(_GetQueryFields(self.queryargs), None)
1401
1402 def PUT(self):
1403 """Submits job querying for resources.
1404
1405 @return: Query result, see L{objects.QueryResponse}
1406
1407 """
1408 body = self.request_body
1409
1410 baserlib.CheckType(body, dict, "Body contents")
1411
1412 try:
1413 fields = body["fields"]
1414 except KeyError:
1415 fields = _GetQueryFields(self.queryargs)
1416
1417 qfilter = body.get("qfilter", None)
1418 # TODO: remove this after 2.7
1419 if qfilter is None:
1420 qfilter = body.get("filter", None)
1421
1422 return self._Query(fields, qfilter)
1423
1424
1425 class R_2_query_fields(baserlib.ResourceBase):
1426 """/2/query/[resource]/fields resource.
1427
1428 """
1429 GET_OPCODE = opcodes.OpQueryFields
1430
1431 def GET(self):
1432 """Retrieves list of available fields for a resource.
1433
1434 @return: List of serialized L{objects.QueryFieldDefinition}
1435
1436 """
1437 try:
1438 raw_fields = self.queryargs["fields"]
1439 except KeyError:
1440 fields = None
1441 else:
1442 fields = _SplitQueryFields(raw_fields[0])
1443
1444 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1445
1446
1447 class _R_Tags(baserlib.OpcodeResource):
1448 """Quasiclass for tagging resources.
1449
1450 Manages tags. When inheriting this class you must define the
1451 TAG_LEVEL for it.
1452
1453 """
1454 TAG_LEVEL = None
1455 GET_OPCODE = opcodes.OpTagsGet
1456 PUT_OPCODE = opcodes.OpTagsSet
1457 DELETE_OPCODE = opcodes.OpTagsDel
1458
1459 def __init__(self, items, queryargs, req, **kwargs):
1460 """A tag resource constructor.
1461
1462 We have to override the default to sort out cluster naming case.
1463
1464 """
1465 baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
1466
1467 if self.TAG_LEVEL == constants.TAG_CLUSTER:
1468 self.name = None
1469 else:
1470 self.name = items[0]
1471
1472 def GET(self):
1473 """Returns a list of tags.
1474
1475 Example: ["tag1", "tag2", "tag3"]
1476
1477 """
1478 kind = self.TAG_LEVEL
1479
1480 if kind in (constants.TAG_INSTANCE,
1481 constants.TAG_NODEGROUP,
1482 constants.TAG_NODE):
1483 if not self.name:
1484 raise http.HttpBadRequest("Missing name on tag request")
1485
1486 cl = self.GetClient(query=True)
1487 tags = list(cl.QueryTags(kind, self.name))
1488
1489 elif kind == constants.TAG_CLUSTER:
1490 assert not self.name
1491 # TODO: Use query API?
1492 ssc = ssconf.SimpleStore()
1493 tags = ssc.GetClusterTags()
1494
1495 return list(tags)
1496
1497 def GetPutOpInput(self):
1498 """Add a set of tags.
1499
1500 The request as a list of strings should be PUT to this URI. And
1501 you'll have back a job id.
1502
1503 """
1504 return ({}, {
1505 "kind": self.TAG_LEVEL,
1506 "name": self.name,
1507 "tags": self.queryargs.get("tag", []),
1508 "dry_run": self.dryRun(),
1509 })
1510
1511 def GetDeleteOpInput(self):
1512 """Delete a tag.
1513
1514 In order to delete a set of tags, the DELETE
1515 request should be addressed to URI like:
1516 /tags?tag=[tag]&tag=[tag]
1517
1518 """
1519 # Re-use code
1520 return self.GetPutOpInput()
1521
1522
1523 class R_2_instances_name_tags(_R_Tags):
1524 """ /2/instances/[instance_name]/tags resource.
1525
1526 Manages per-instance tags.
1527
1528 """
1529 TAG_LEVEL = constants.TAG_INSTANCE
1530
1531
1532 class R_2_nodes_name_tags(_R_Tags):
1533 """ /2/nodes/[node_name]/tags resource.
1534
1535 Manages per-node tags.
1536
1537 """
1538 TAG_LEVEL = constants.TAG_NODE
1539
1540
1541 class R_2_groups_name_tags(_R_Tags):
1542 """ /2/groups/[group_name]/tags resource.
1543
1544 Manages per-nodegroup tags.
1545
1546 """
1547 TAG_LEVEL = constants.TAG_NODEGROUP
1548
1549
1550 class R_2_tags(_R_Tags):
1551 """ /2/tags resource.
1552
1553 Manages cluster tags.
1554
1555 """
1556 TAG_LEVEL = constants.TAG_CLUSTER