1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
55
56
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", "nic.uuids", "nic.names",
75 "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
76 "network_port",
77 "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
78 "disk.names",
79 "beparams", "hvparams",
80 "oper_state", "oper_ram", "oper_vcpus", "status",
81 "custom_hvparams", "custom_beparams", "custom_nicparams",
82 ] + _COMMON_FIELDS
83
84 N_FIELDS = ["name", "offline", "master_candidate", "drained",
85 "dtotal", "dfree", "sptotal", "spfree",
86 "mtotal", "mnode", "mfree",
87 "pinst_cnt", "sinst_cnt",
88 "ctotal", "cnos", "cnodes", "csockets",
89 "pip", "sip", "role",
90 "pinst_list", "sinst_list",
91 "master_capable", "vm_capable",
92 "ndparams",
93 "group.uuid",
94 ] + _COMMON_FIELDS
95
96 NET_FIELDS = ["name", "network", "gateway",
97 "network6", "gateway6",
98 "mac_prefix",
99 "free_count", "reserved_count",
100 "map", "group_list", "inst_list",
101 "external_reservations",
102 ] + _COMMON_FIELDS
103
104 G_FIELDS = [
105 "alloc_policy",
106 "name",
107 "node_cnt",
108 "node_list",
109 "ipolicy",
110 "custom_ipolicy",
111 "diskparams",
112 "custom_diskparams",
113 "ndparams",
114 "custom_ndparams",
115 ] + _COMMON_FIELDS
116
117 J_FIELDS_BULK = [
118 "id", "ops", "status", "summary",
119 "opstatus",
120 "received_ts", "start_ts", "end_ts",
121 ]
122
123 J_FIELDS = J_FIELDS_BULK + [
124 "oplog",
125 "opresult",
126 ]
127
128 _NR_DRAINED = "drained"
129 _NR_MASTER_CANDIDATE = "master-candidate"
130 _NR_MASTER = "master"
131 _NR_OFFLINE = "offline"
132 _NR_REGULAR = "regular"
133
134 _NR_MAP = {
135 constants.NR_MASTER: _NR_MASTER,
136 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
137 constants.NR_DRAINED: _NR_DRAINED,
138 constants.NR_OFFLINE: _NR_OFFLINE,
139 constants.NR_REGULAR: _NR_REGULAR,
140 }
141
142 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
143
144
145 _REQ_DATA_VERSION = "__version__"
146
147
148 _INST_CREATE_REQV1 = "instance-create-reqv1"
149
150
151 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
152
153
154 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
155
156
157 _NODE_EVAC_RES1 = "node-evac-res1"
158
159 ALL_FEATURES = compat.UniqueFrozenset([
160 _INST_CREATE_REQV1,
161 _INST_REINSTALL_REQV1,
162 _NODE_MIGRATE_REQV1,
163 _NODE_EVAC_RES1,
164 ])
165
166
167 _WFJC_TIMEOUT = 10
173 """Updates the beparams dict of inst to support the memory field.
174
175 @param inst: Inst dict
176 @return: Updated inst dict
177
178 """
179 beparams = inst["beparams"]
180 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
181
182 return inst
183
184
185 -class R_root(baserlib.ResourceBase):
186 """/ resource.
187
188 """
189 @staticmethod
191 """Supported for legacy reasons.
192
193 """
194 return None
195
196
197 -class R_2(R_root):
198 """/2 resource.
199
200 """
201
204 """/version resource.
205
206 This resource should be used to determine the remote API version and
207 to adapt clients accordingly.
208
209 """
210 @staticmethod
216
217
218 -class R_2_info(baserlib.OpcodeResource):
230
233 """/2/features resource.
234
235 """
236 @staticmethod
238 """Returns list of optional RAPI features implemented.
239
240 """
241 return list(ALL_FEATURES)
242
243
244 -class R_2_os(baserlib.OpcodeResource):
245 """/2/os resource.
246
247 """
248 GET_OPCODE = opcodes.OpOsDiagnose
249
251 """Return a list of all OSes.
252
253 Can return error 500 in case of a problem.
254
255 Example: ["debian-etch"]
256
257 """
258 cl = self.GetClient()
259 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
260 job_id = self.SubmitJob([op], cl=cl)
261
262 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
263 diagnose_data = result[0]
264
265 if not isinstance(diagnose_data, list):
266 raise http.HttpBadGateway(message="Can't get OS list")
267
268 os_names = []
269 for (name, variants) in diagnose_data:
270 os_names.extend(cli.CalculateOSNames(name, variants))
271
272 return os_names
273
280
287
288
289 -class R_2_jobs(baserlib.ResourceBase):
290 """/2/jobs resource.
291
292 """
308
311 """/2/jobs/[job_id] resource.
312
313 """
315 """Returns a job status.
316
317 @return: a dictionary with job parameters.
318 The result includes:
319 - id: job ID as a number
320 - status: current job status as a string
321 - ops: involved OpCodes as a list of dictionaries for each
322 opcodes in the job
323 - opstatus: OpCodes status as a list
324 - opresult: OpCodes results as a list of lists
325
326 """
327 job_id = self.items[0]
328 result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
329 if result is None:
330 raise http.HttpNotFound()
331 return baserlib.MapFields(J_FIELDS, result)
332
334 """Cancel not-yet-started job.
335
336 """
337 job_id = self.items[0]
338 result = self.GetClient().CancelJob(job_id)
339 return result
340
343 """/2/jobs/[job_id]/wait resource.
344
345 """
346
347
348 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
349
351 """Waits for job changes.
352
353 """
354 job_id = self.items[0]
355
356 fields = self.getBodyParameter("fields")
357 prev_job_info = self.getBodyParameter("previous_job_info", None)
358 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
359
360 if not isinstance(fields, list):
361 raise http.HttpBadRequest("The 'fields' parameter should be a list")
362
363 if not (prev_job_info is None or isinstance(prev_job_info, list)):
364 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
365 " be a list")
366
367 if not (prev_log_serial is None or
368 isinstance(prev_log_serial, (int, long))):
369 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
370 " be a number")
371
372 client = self.GetClient()
373 result = client.WaitForJobChangeOnce(job_id, fields,
374 prev_job_info, prev_log_serial,
375 timeout=_WFJC_TIMEOUT)
376 if not result:
377 raise http.HttpNotFound()
378
379 if result == constants.JOB_NOTCHANGED:
380
381 return None
382
383 (job_info, log_entries) = result
384
385 return {
386 "job_info": job_info,
387 "log_entries": log_entries,
388 }
389
390
391 -class R_2_nodes(baserlib.OpcodeResource):
411
414 """/2/nodes/[node_name] resource.
415
416 """
417 GET_OPCODE = opcodes.OpNodeQuery
418
431
434 """/2/nodes/[node_name]/powercycle resource.
435
436 """
437 POST_OPCODE = opcodes.OpNodePowercycle
438
440 """Tries to powercycle a node.
441
442 """
443 return (self.request_body, {
444 "node_name": self.items[0],
445 "force": self.useForce(),
446 })
447
450 """/2/nodes/[node_name]/role resource.
451
452 """
453 PUT_OPCODE = opcodes.OpNodeSetParams
454
456 """Returns the current node role.
457
458 @return: Node role
459
460 """
461 node_name = self.items[0]
462 client = self.GetClient(query=True)
463 result = client.QueryNodes(names=[node_name], fields=["role"],
464 use_locking=self.useLocking())
465
466 return _NR_MAP[result[0][0]]
467
506
509 """/2/nodes/[node_name]/evacuate resource.
510
511 """
512 POST_OPCODE = opcodes.OpNodeEvacuate
513
515 """Evacuate all instances off a node.
516
517 """
518 return (self.request_body, {
519 "node_name": self.items[0],
520 "dry_run": self.dryRun(),
521 })
522
525 """/2/nodes/[node_name]/migrate resource.
526
527 """
528 POST_OPCODE = opcodes.OpNodeMigrate
529
531 """Migrate all primary instances from a node.
532
533 """
534 if self.queryargs:
535
536 if "live" in self.queryargs and "mode" in self.queryargs:
537 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
538 " be passed")
539
540 if "live" in self.queryargs:
541 if self._checkIntVariable("live", default=1):
542 mode = constants.HT_MIGRATION_LIVE
543 else:
544 mode = constants.HT_MIGRATION_NONLIVE
545 else:
546 mode = self._checkStringVariable("mode", default=None)
547
548 data = {
549 "mode": mode,
550 }
551 else:
552 data = self.request_body
553
554 return (data, {
555 "node_name": self.items[0],
556 })
557
560 """/2/nodes/[node_name]/modify resource.
561
562 """
563 POST_OPCODE = opcodes.OpNodeSetParams
564
566 """Changes parameters of a node.
567
568 """
569 assert len(self.items) == 1
570
571 return (self.request_body, {
572 "node_name": self.items[0],
573 })
574
600
631
654
689
720
737
754
770
806
809 """/2/groups/[group_name] resource.
810
811 """
812 DELETE_OPCODE = opcodes.OpGroupRemove
813
826
836
852
855 """/2/groups/[group_name]/rename resource.
856
857 """
858 PUT_OPCODE = opcodes.OpGroupRename
859
869
887
890 """Convert in place the usb_devices string to the proper format.
891
892 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
893 comma to space because commas cannot be accepted on the command line
894 (they already act as the separator between different hvparams). RAPI
895 should be able to accept commas for backwards compatibility, but we want
896 it to also accept the new space separator. Therefore, we convert
897 spaces into commas here and keep the old parsing logic elsewhere.
898
899 """
900 try:
901 hvparams = data["hvparams"]
902 usb_devices = hvparams[constants.HV_USB_DEVICES]
903 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
904 data["hvparams"] = hvparams
905 except KeyError:
906
907 pass
908
964
967 """/2/instances-multi-alloc resource.
968
969 """
970 POST_OPCODE = opcodes.OpInstanceMultiAlloc
971
973 """Try to allocate multiple instances.
974
975 @return: A dict with submitted jobs, allocatable instances and failed
976 allocations
977
978 """
979 if "instances" not in self.request_body:
980 raise http.HttpBadRequest("Request is missing required 'instances' field"
981 " in body")
982
983
984
985 OPCODE_RENAME = {
986 "os": "os_type",
987 "name": "instance_name",
988 }
989
990 body = objects.FillDict(self.request_body, {
991 "instances": [
992 baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
993 rename=OPCODE_RENAME)
994 for inst in self.request_body["instances"]
995 ],
996 })
997
998 return (body, {
999 "dry_run": self.dryRun(),
1000 })
1001
1034
1051
1054 """/2/instances/[instance_name]/reboot resource.
1055
1056 Implements an instance reboot.
1057
1058 """
1059 POST_OPCODE = opcodes.OpInstanceReboot
1060
1062 """Reboot an instance.
1063
1064 The URI takes type=[hard|soft|full] and
1065 ignore_secondaries=[False|True] parameters.
1066
1067 """
1068 return ({}, {
1069 "instance_name": self.items[0],
1070 "reboot_type":
1071 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1072 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1073 "dry_run": self.dryRun(),
1074 })
1075
1078 """/2/instances/[instance_name]/startup resource.
1079
1080 Implements an instance startup.
1081
1082 """
1083 PUT_OPCODE = opcodes.OpInstanceStartup
1084
1098
1101 """/2/instances/[instance_name]/shutdown resource.
1102
1103 Implements an instance shutdown.
1104
1105 """
1106 PUT_OPCODE = opcodes.OpInstanceShutdown
1107
1117
1120 """Parses a request for reinstalling an instance.
1121
1122 """
1123 if not isinstance(data, dict):
1124 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1125
1126 ostype = baserlib.CheckParameter(data, "os", default=None)
1127 start = baserlib.CheckParameter(data, "start", exptype=bool,
1128 default=True)
1129 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1130
1131 ops = [
1132 opcodes.OpInstanceShutdown(instance_name=name),
1133 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1134 osparams=osparams),
1135 ]
1136
1137 if start:
1138 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1139
1140 return ops
1141
1144 """/2/instances/[instance_name]/reinstall resource.
1145
1146 Implements an instance reinstall.
1147
1148 """
1149 POST_OPCODE = opcodes.OpInstanceReinstall
1150
1152 """Reinstall an instance.
1153
1154 The URI takes os=name and nostartup=[0|1] optional
1155 parameters. By default, the instance will be started
1156 automatically.
1157
1158 """
1159 if self.request_body:
1160 if self.queryargs:
1161 raise http.HttpBadRequest("Can't combine query and body parameters")
1162
1163 body = self.request_body
1164 elif self.queryargs:
1165
1166 body = {
1167 "os": self._checkStringVariable("os"),
1168 "start": not self._checkIntVariable("nostartup"),
1169 }
1170 else:
1171 body = {}
1172
1173 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1174
1175 return self.SubmitJob(ops)
1176
1179 """/2/instances/[instance_name]/replace-disks resource.
1180
1181 """
1182 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1183
1185 """Replaces disks on an instance.
1186
1187 """
1188 static = {
1189 "instance_name": self.items[0],
1190 }
1191
1192 if self.request_body:
1193 data = self.request_body
1194 elif self.queryargs:
1195
1196 data = {
1197 "remote_node": self._checkStringVariable("remote_node", default=None),
1198 "mode": self._checkStringVariable("mode", default=None),
1199 "disks": self._checkStringVariable("disks", default=None),
1200 "iallocator": self._checkStringVariable("iallocator", default=None),
1201 }
1202 else:
1203 data = {}
1204
1205
1206 try:
1207 raw_disks = data.pop("disks")
1208 except KeyError:
1209 pass
1210 else:
1211 if raw_disks:
1212 if ht.TListOf(ht.TInt)(raw_disks):
1213 data["disks"] = raw_disks
1214 else:
1215
1216 try:
1217 data["disks"] = [int(part) for part in raw_disks.split(",")]
1218 except (TypeError, ValueError), err:
1219 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1220
1221 return (data, static)
1222
1240
1255
1258 """/2/instances/[instance_name]/recreate-disks resource.
1259
1260 """
1261 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1262
1264 """Recreate disks for an instance.
1265
1266 """
1267 return ({}, {
1268 "instance_name": self.items[0],
1269 })
1270
1273 """/2/instances/[instance_name]/prepare-export resource.
1274
1275 """
1276 PUT_OPCODE = opcodes.OpBackupPrepare
1277
1286
1289 """/2/instances/[instance_name]/export resource.
1290
1291 """
1292 PUT_OPCODE = opcodes.OpBackupExport
1293 PUT_RENAME = {
1294 "destination": "target_node",
1295 }
1296
1304
1319
1334
1337 """/2/instances/[instance_name]/rename resource.
1338
1339 """
1340 PUT_OPCODE = opcodes.OpInstanceRename
1341
1349
1367
1370 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1371
1372 """
1373 POST_OPCODE = opcodes.OpInstanceGrowDisk
1374
1376 """Increases the size of an instance disk.
1377
1378 """
1379 return (self.request_body, {
1380 "instance_name": self.items[0],
1381 "disk": int(self.items[1]),
1382 })
1383
1408
1411 """Tries to extract C{fields} query parameter.
1412
1413 @type args: dictionary
1414 @rtype: list of string
1415 @raise http.HttpBadRequest: When parameter can't be found
1416
1417 """
1418 try:
1419 fields = args["fields"]
1420 except KeyError:
1421 raise http.HttpBadRequest("Missing 'fields' query argument")
1422
1423 return _SplitQueryFields(fields[0])
1424
1427 """Splits fields as given for a query request.
1428
1429 @type fields: string
1430 @rtype: list of string
1431
1432 """
1433 return [i.strip() for i in fields.split(",")]
1434
1435
1436 -class R_2_query(baserlib.ResourceBase):
1437 """/2/query/[resource] resource.
1438
1439 """
1440
1441 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1442 PUT_ACCESS = GET_ACCESS
1443 GET_OPCODE = opcodes.OpQuery
1444 PUT_OPCODE = opcodes.OpQuery
1445
1446 - def _Query(self, fields, qfilter):
1448
1450 """Returns resource information.
1451
1452 @return: Query result, see L{objects.QueryResponse}
1453
1454 """
1455 return self._Query(_GetQueryFields(self.queryargs), None)
1456
1458 """Submits job querying for resources.
1459
1460 @return: Query result, see L{objects.QueryResponse}
1461
1462 """
1463 body = self.request_body
1464
1465 baserlib.CheckType(body, dict, "Body contents")
1466
1467 try:
1468 fields = body["fields"]
1469 except KeyError:
1470 fields = _GetQueryFields(self.queryargs)
1471
1472 qfilter = body.get("qfilter", None)
1473
1474 if qfilter is None:
1475 qfilter = body.get("filter", None)
1476
1477 return self._Query(fields, qfilter)
1478
1481 """/2/query/[resource]/fields resource.
1482
1483 """
1484 GET_OPCODE = opcodes.OpQueryFields
1485
1487 """Retrieves list of available fields for a resource.
1488
1489 @return: List of serialized L{objects.QueryFieldDefinition}
1490
1491 """
1492 try:
1493 raw_fields = self.queryargs["fields"]
1494 except KeyError:
1495 fields = None
1496 else:
1497 fields = _SplitQueryFields(raw_fields[0])
1498
1499 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1500
1580
1589
1598
1607
1616
1625