1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 """Remote API resource implementations.
32
33 PUT or POST?
34 ============
35
36 According to RFC2616 the main difference between PUT and POST is that
37 POST can create new resources but PUT can only create the resource the
38 URI was pointing to on the PUT request.
39
40 In the context of this module POST on ``/2/instances`` to change an existing
41 entity is legitimate, while PUT would not be. PUT creates a new entity (e.g. a
42 new instance) with a name specified in the request.
43
44 Quoting from RFC2616, section 9.6::
45
46 The fundamental difference between the POST and PUT requests is reflected in
47 the different meaning of the Request-URI. The URI in a POST request
48 identifies the resource that will handle the enclosed entity. That resource
49 might be a data-accepting process, a gateway to some other protocol, or a
50 separate entity that accepts annotations. In contrast, the URI in a PUT
51 request identifies the entity enclosed with the request -- the user agent
52 knows what URI is intended and the server MUST NOT attempt to apply the
53 request to some other resource. If the server desires that the request be
54 applied to a different URI, it MUST send a 301 (Moved Permanently) response;
55 the user agent MAY then make its own decision regarding whether or not to
56 redirect the request.
57
58 So when adding new methods, if they are operating on the URI entity itself,
59 PUT should be prefered over POST.
60
61 """
62
63
64
65
66
67 from ganeti import opcodes
68 from ganeti import objects
69 from ganeti import http
70 from ganeti import constants
71 from ganeti import cli
72 from ganeti import rapi
73 from ganeti import ht
74 from ganeti import compat
75 from ganeti.rapi import baserlib
76
77
78 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
79 I_FIELDS = ["name", "admin_state", "os",
80 "pnode", "snodes",
81 "disk_template",
82 "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
83 "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
84 "network_port",
85 "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
86 "disk.names",
87 "beparams", "hvparams",
88 "oper_state", "oper_ram", "oper_vcpus", "status",
89 "custom_hvparams", "custom_beparams", "custom_nicparams",
90 ] + _COMMON_FIELDS
91
92 N_FIELDS = ["name", "offline", "master_candidate", "drained",
93 "dtotal", "dfree", "sptotal", "spfree",
94 "mtotal", "mnode", "mfree",
95 "pinst_cnt", "sinst_cnt",
96 "ctotal", "cnos", "cnodes", "csockets",
97 "pip", "sip", "role",
98 "pinst_list", "sinst_list",
99 "master_capable", "vm_capable",
100 "ndparams",
101 "group.uuid",
102 ] + _COMMON_FIELDS
103
104 NET_FIELDS = ["name", "network", "gateway",
105 "network6", "gateway6",
106 "mac_prefix",
107 "free_count", "reserved_count",
108 "map", "group_list", "inst_list",
109 "external_reservations",
110 ] + _COMMON_FIELDS
111
112 G_FIELDS = [
113 "alloc_policy",
114 "name",
115 "node_cnt",
116 "node_list",
117 "ipolicy",
118 "custom_ipolicy",
119 "diskparams",
120 "custom_diskparams",
121 "ndparams",
122 "custom_ndparams",
123 ] + _COMMON_FIELDS
124
125 J_FIELDS_BULK = [
126 "id", "ops", "status", "summary",
127 "opstatus",
128 "received_ts", "start_ts", "end_ts",
129 ]
130
131 J_FIELDS = J_FIELDS_BULK + [
132 "oplog",
133 "opresult",
134 ]
135
136 _NR_DRAINED = "drained"
137 _NR_MASTER_CANDIDATE = "master-candidate"
138 _NR_MASTER = "master"
139 _NR_OFFLINE = "offline"
140 _NR_REGULAR = "regular"
141
142 _NR_MAP = {
143 constants.NR_MASTER: _NR_MASTER,
144 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
145 constants.NR_DRAINED: _NR_DRAINED,
146 constants.NR_OFFLINE: _NR_OFFLINE,
147 constants.NR_REGULAR: _NR_REGULAR,
148 }
149
150 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
151
152
153 _REQ_DATA_VERSION = "__version__"
154
155
156 _INST_CREATE_REQV1 = "instance-create-reqv1"
157
158
159 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
160
161
162 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
163
164
165 _NODE_EVAC_RES1 = "node-evac-res1"
166
167 ALL_FEATURES = compat.UniqueFrozenset([
168 _INST_CREATE_REQV1,
169 _INST_REINSTALL_REQV1,
170 _NODE_MIGRATE_REQV1,
171 _NODE_EVAC_RES1,
172 ])
173
174
175 _WFJC_TIMEOUT = 10
181 """Updates the beparams dict of inst to support the memory field.
182
183 @param inst: Inst dict
184 @return: Updated inst dict
185
186 """
187 beparams = inst["beparams"]
188 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
189
190 return inst
191
192
193 -class R_root(baserlib.ResourceBase):
194 """/ resource.
195
196 """
197 @staticmethod
199 """Supported for legacy reasons.
200
201 """
202 return None
203
204
205 -class R_2(R_root):
206 """/2 resource.
207
208 """
209
212 """/version resource.
213
214 This resource should be used to determine the remote API version and
215 to adapt clients accordingly.
216
217 """
218 @staticmethod
224
225
226 -class R_2_info(baserlib.OpcodeResource):
227 """/2/info resource.
228
229 """
230 GET_OPCODE = opcodes.OpClusterQuery
231 GET_ALIASES = {
232 "volume_group_name": "vg_name",
233 "drbd_usermode_helper": "drbd_helper",
234 }
235
242
245 """/2/features resource.
246
247 """
248 @staticmethod
250 """Returns list of optional RAPI features implemented.
251
252 """
253 return list(ALL_FEATURES)
254
255
256 -class R_2_os(baserlib.OpcodeResource):
257 """/2/os resource.
258
259 """
260 GET_OPCODE = opcodes.OpOsDiagnose
261
263 """Return a list of all OSes.
264
265 Can return error 500 in case of a problem.
266
267 Example: ["debian-etch"]
268
269 """
270 cl = self.GetClient()
271 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
272 job_id = self.SubmitJob([op], cl=cl)
273
274 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
275 diagnose_data = result[0]
276
277 if not isinstance(diagnose_data, list):
278 raise http.HttpBadGateway(message="Can't get OS list")
279
280 os_names = []
281 for (name, variants) in diagnose_data:
282 os_names.extend(cli.CalculateOSNames(name, variants))
283
284 return os_names
285
292
302
303
304 -class R_2_jobs(baserlib.ResourceBase):
305 """/2/jobs resource.
306
307 """
323
326 """/2/jobs/[job_id] resource.
327
328 """
330 """Returns a job status.
331
332 @return: a dictionary with job parameters.
333 The result includes:
334 - id: job ID as a number
335 - status: current job status as a string
336 - ops: involved OpCodes as a list of dictionaries for each
337 opcodes in the job
338 - opstatus: OpCodes status as a list
339 - opresult: OpCodes results as a list of lists
340
341 """
342 job_id = self.items[0]
343 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
344 if result is None:
345 raise http.HttpNotFound()
346 return baserlib.MapFields(J_FIELDS, result)
347
349 """Cancel not-yet-started job.
350
351 """
352 job_id = self.items[0]
353 result = self.GetClient().CancelJob(job_id)
354 return result
355
358 """/2/jobs/[job_id]/wait resource.
359
360 """
361
362
363 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
364
366 """Waits for job changes.
367
368 """
369 job_id = self.items[0]
370
371 fields = self.getBodyParameter("fields")
372 prev_job_info = self.getBodyParameter("previous_job_info", None)
373 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
374
375 if not isinstance(fields, list):
376 raise http.HttpBadRequest("The 'fields' parameter should be a list")
377
378 if not (prev_job_info is None or isinstance(prev_job_info, list)):
379 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
380 " be a list")
381
382 if not (prev_log_serial is None or
383 isinstance(prev_log_serial, (int, long))):
384 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
385 " be a number")
386
387 client = self.GetClient()
388 result = client.WaitForJobChangeOnce(job_id, fields,
389 prev_job_info, prev_log_serial,
390 timeout=_WFJC_TIMEOUT)
391 if not result:
392 raise http.HttpNotFound()
393
394 if result == constants.JOB_NOTCHANGED:
395
396 return None
397
398 (job_info, log_entries) = result
399
400 return {
401 "job_info": job_info,
402 "log_entries": log_entries,
403 }
404
405
406 -class R_2_nodes(baserlib.OpcodeResource):
407 """/2/nodes resource.
408
409 """
410
425
428 """/2/nodes/[node_name] resource.
429
430 """
431 GET_ALIASES = {
432 "sip": "secondary_ip",
433 }
434
447
450 """/2/nodes/[node_name]/powercycle resource.
451
452 """
453 POST_OPCODE = opcodes.OpNodePowercycle
454
456 """Tries to powercycle a node.
457
458 """
459 return (self.request_body, {
460 "node_name": self.items[0],
461 "force": self.useForce(),
462 })
463
466 """/2/nodes/[node_name]/role resource.
467
468 """
469 PUT_OPCODE = opcodes.OpNodeSetParams
470
472 """Returns the current node role.
473
474 @return: Node role
475
476 """
477 node_name = self.items[0]
478 client = self.GetClient()
479 result = client.QueryNodes(names=[node_name], fields=["role"],
480 use_locking=self.useLocking())
481
482 return _NR_MAP[result[0][0]]
483
522
525 """/2/nodes/[node_name]/evacuate resource.
526
527 """
528 POST_OPCODE = opcodes.OpNodeEvacuate
529
531 """Evacuate all instances off a node.
532
533 """
534 return (self.request_body, {
535 "node_name": self.items[0],
536 "dry_run": self.dryRun(),
537 })
538
541 """/2/nodes/[node_name]/migrate resource.
542
543 """
544 POST_OPCODE = opcodes.OpNodeMigrate
545
547 """Migrate all primary instances from a node.
548
549 """
550 if self.queryargs:
551
552 if "live" in self.queryargs and "mode" in self.queryargs:
553 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
554 " be passed")
555
556 if "live" in self.queryargs:
557 if self._checkIntVariable("live", default=1):
558 mode = constants.HT_MIGRATION_LIVE
559 else:
560 mode = constants.HT_MIGRATION_NONLIVE
561 else:
562 mode = self._checkStringVariable("mode", default=None)
563
564 data = {
565 "mode": mode,
566 }
567 else:
568 data = self.request_body
569
570 return (data, {
571 "node_name": self.items[0],
572 })
573
576 """/2/nodes/[node_name]/modify resource.
577
578 """
579 POST_OPCODE = opcodes.OpNodeSetParams
580
582 """Changes parameters of a node.
583
584 """
585 assert len(self.items) == 1
586
587 return (self.request_body, {
588 "node_name": self.items[0],
589 })
590
616
647
670
673 """/2/networks resource.
674
675 """
676 POST_OPCODE = opcodes.OpNetworkAdd
677 POST_RENAME = {
678 "name": "network_name",
679 }
680
682 """Create a network.
683
684 """
685 assert not self.items
686 return (self.request_body, {
687 "dry_run": self.dryRun(),
688 })
689
704
735
752
769
785
788 """/2/groups resource.
789
790 """
791 POST_OPCODE = opcodes.OpGroupAdd
792 POST_RENAME = {
793 "name": "group_name",
794 }
795
797 """Create a node group.
798
799
800 """
801 assert not self.items
802 return (self.request_body, {
803 "dry_run": self.dryRun(),
804 })
805
820
823 """/2/groups/[group_name] resource.
824
825 """
826 DELETE_OPCODE = opcodes.OpGroupRemove
827
840
850
853 """/2/groups/[group_name]/modify resource.
854
855 """
856 PUT_OPCODE = opcodes.OpGroupSetParams
857 PUT_RENAME = {
858 "custom_ndparams": "ndparams",
859 "custom_ipolicy": "ipolicy",
860 "custom_diskparams": "diskparams",
861 }
862
871
874 """/2/groups/[group_name]/rename resource.
875
876 """
877 PUT_OPCODE = opcodes.OpGroupRename
878
888
906
909 """Convert in place the usb_devices string to the proper format.
910
911 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
912 comma to space because commas cannot be accepted on the command line
913 (they already act as the separator between different hvparams). RAPI
914 should be able to accept commas for backwards compatibility, but we want
915 it to also accept the new space separator. Therefore, we convert
916 spaces into commas here and keep the old parsing logic elsewhere.
917
918 """
919 try:
920 hvparams = data["hvparams"]
921 usb_devices = hvparams[constants.HV_USB_DEVICES]
922 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
923 data["hvparams"] = hvparams
924 except KeyError:
925
926 pass
927
982
985 """/2/instances-multi-alloc resource.
986
987 """
988 POST_OPCODE = opcodes.OpInstanceMultiAlloc
989
991 """Try to allocate multiple instances.
992
993 @return: A dict with submitted jobs, allocatable instances and failed
994 allocations
995
996 """
997 if "instances" not in self.request_body:
998 raise http.HttpBadRequest("Request is missing required 'instances' field"
999 " in body")
1000
1001
1002
1003 OPCODE_RENAME = {
1004 "os": "os_type",
1005 "name": "instance_name",
1006 }
1007
1008 body = objects.FillDict(self.request_body, {
1009 "instances": [
1010 baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
1011 rename=OPCODE_RENAME)
1012 for inst in self.request_body["instances"]
1013 ],
1014 })
1015
1016 return (body, {
1017 "dry_run": self.dryRun(),
1018 })
1019
1022 """/2/instances/[instance_name] resource.
1023
1024 """
1025 DELETE_OPCODE = opcodes.OpInstanceRemove
1026
1040
1051
1068
1071 """/2/instances/[instance_name]/reboot resource.
1072
1073 Implements an instance reboot.
1074
1075 """
1076 POST_OPCODE = opcodes.OpInstanceReboot
1077
1079 """Reboot an instance.
1080
1081 The URI takes type=[hard|soft|full] and
1082 ignore_secondaries=[False|True] parameters.
1083
1084 """
1085 return (self.request_body, {
1086 "instance_name": self.items[0],
1087 "reboot_type":
1088 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1089 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1090 "dry_run": self.dryRun(),
1091 })
1092
1095 """/2/instances/[instance_name]/startup resource.
1096
1097 Implements an instance startup.
1098
1099 """
1100 PUT_OPCODE = opcodes.OpInstanceStartup
1101
1115
1118 """/2/instances/[instance_name]/shutdown resource.
1119
1120 Implements an instance shutdown.
1121
1122 """
1123 PUT_OPCODE = opcodes.OpInstanceShutdown
1124
1134
1137 """Parses a request for reinstalling an instance.
1138
1139 """
1140 if not isinstance(data, dict):
1141 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1142
1143 ostype = baserlib.CheckParameter(data, "os", default=None)
1144 start = baserlib.CheckParameter(data, "start", exptype=bool,
1145 default=True)
1146 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1147
1148 ops = [
1149 opcodes.OpInstanceShutdown(instance_name=name),
1150 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1151 osparams=osparams),
1152 ]
1153
1154 if start:
1155 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1156
1157 return ops
1158
1161 """/2/instances/[instance_name]/reinstall resource.
1162
1163 Implements an instance reinstall.
1164
1165 """
1166 POST_OPCODE = opcodes.OpInstanceReinstall
1167
1169 """Reinstall an instance.
1170
1171 The URI takes os=name and nostartup=[0|1] optional
1172 parameters. By default, the instance will be started
1173 automatically.
1174
1175 """
1176 if self.request_body:
1177 if self.queryargs:
1178 raise http.HttpBadRequest("Can't combine query and body parameters")
1179
1180 body = self.request_body
1181 elif self.queryargs:
1182
1183 body = {
1184 "os": self._checkStringVariable("os"),
1185 "start": not self._checkIntVariable("nostartup"),
1186 }
1187 else:
1188 body = {}
1189
1190 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1191
1192 return self.SubmitJob(ops)
1193
1196 """/2/instances/[instance_name]/replace-disks resource.
1197
1198 """
1199 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1200
1202 """Replaces disks on an instance.
1203
1204 """
1205 static = {
1206 "instance_name": self.items[0],
1207 }
1208
1209 if self.request_body:
1210 data = self.request_body
1211 elif self.queryargs:
1212
1213 data = {
1214 "remote_node": self._checkStringVariable("remote_node", default=None),
1215 "mode": self._checkStringVariable("mode", default=None),
1216 "disks": self._checkStringVariable("disks", default=None),
1217 "iallocator": self._checkStringVariable("iallocator", default=None),
1218 }
1219 else:
1220 data = {}
1221
1222
1223 try:
1224 raw_disks = data.pop("disks")
1225 except KeyError:
1226 pass
1227 else:
1228 if raw_disks:
1229 if ht.TListOf(ht.TInt)(raw_disks):
1230 data["disks"] = raw_disks
1231 else:
1232
1233 try:
1234 data["disks"] = [int(part) for part in raw_disks.split(",")]
1235 except (TypeError, ValueError), err:
1236 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1237
1238 return (data, static)
1239
1257
1272
1275 """/2/instances/[instance_name]/recreate-disks resource.
1276
1277 """
1278 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1279
1281 """Recreate disks for an instance.
1282
1283 """
1284 return (self.request_body, {
1285 "instance_name": self.items[0],
1286 })
1287
1290 """/2/instances/[instance_name]/prepare-export resource.
1291
1292 """
1293 PUT_OPCODE = opcodes.OpBackupPrepare
1294
1303
1306 """/2/instances/[instance_name]/export resource.
1307
1308 """
1309 PUT_OPCODE = opcodes.OpBackupExport
1310 PUT_RENAME = {
1311 "destination": "target_node",
1312 }
1313
1321
1336
1351
1354 """/2/instances/[instance_name]/rename resource.
1355
1356 """
1357 PUT_OPCODE = opcodes.OpInstanceRename
1358
1366
1369 """/2/instances/[instance_name]/modify resource.
1370
1371 """
1372 PUT_OPCODE = opcodes.OpInstanceSetParams
1373 PUT_RENAME = {
1374 "custom_beparams": "beparams",
1375 "custom_hvparams": "hvparams",
1376 }
1377
1388
1391 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1392
1393 """
1394 POST_OPCODE = opcodes.OpInstanceGrowDisk
1395
1397 """Increases the size of an instance disk.
1398
1399 """
1400 return (self.request_body, {
1401 "instance_name": self.items[0],
1402 "disk": int(self.items[1]),
1403 })
1404
1407 """/2/instances/[instance_name]/console resource.
1408
1409 """
1410 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1411 GET_OPCODE = opcodes.OpInstanceConsole
1412
1414 """Request information for connecting to instance's console.
1415
1416 @return: Serialized instance console description, see
1417 L{objects.InstanceConsole}
1418
1419 """
1420 instance_name = self.items[0]
1421 client = self.GetClient()
1422
1423 ((console, oper_state), ) = \
1424 client.QueryInstances([instance_name], ["console", "oper_state"], False)
1425
1426 if not oper_state:
1427 raise http.HttpServiceUnavailable("Instance console unavailable")
1428
1429 assert isinstance(console, dict)
1430 return console
1431
1434 """Tries to extract C{fields} query parameter.
1435
1436 @type args: dictionary
1437 @rtype: list of string
1438 @raise http.HttpBadRequest: When parameter can't be found
1439
1440 """
1441 try:
1442 fields = args["fields"]
1443 except KeyError:
1444 raise http.HttpBadRequest("Missing 'fields' query argument")
1445
1446 return _SplitQueryFields(fields[0])
1447
1450 """Splits fields as given for a query request.
1451
1452 @type fields: string
1453 @rtype: list of string
1454
1455 """
1456 return [i.strip() for i in fields.split(",")]
1457
1458
1459 -class R_2_query(baserlib.ResourceBase):
1460 """/2/query/[resource] resource.
1461
1462 """
1463
1464 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1465 PUT_ACCESS = GET_ACCESS
1466 GET_OPCODE = opcodes.OpQuery
1467 PUT_OPCODE = opcodes.OpQuery
1468
1469 - def _Query(self, fields, qfilter):
1472
1474 """Returns resource information.
1475
1476 @return: Query result, see L{objects.QueryResponse}
1477
1478 """
1479 return self._Query(_GetQueryFields(self.queryargs), None)
1480
1482 """Submits job querying for resources.
1483
1484 @return: Query result, see L{objects.QueryResponse}
1485
1486 """
1487 body = self.request_body
1488
1489 baserlib.CheckType(body, dict, "Body contents")
1490
1491 try:
1492 fields = body["fields"]
1493 except KeyError:
1494 fields = _GetQueryFields(self.queryargs)
1495
1496 qfilter = body.get("qfilter", None)
1497
1498 if qfilter is None:
1499 qfilter = body.get("filter", None)
1500
1501 return self._Query(fields, qfilter)
1502
1505 """/2/query/[resource]/fields resource.
1506
1507 """
1508 GET_OPCODE = opcodes.OpQueryFields
1509
1511 """Retrieves list of available fields for a resource.
1512
1513 @return: List of serialized L{objects.QueryFieldDefinition}
1514
1515 """
1516 try:
1517 raw_fields = self.queryargs["fields"]
1518 except KeyError:
1519 fields = None
1520 else:
1521 fields = _SplitQueryFields(raw_fields[0])
1522
1523 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1524
1600
1609
1618
1627
1636
1645