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 import ssconf
76 from ganeti.rapi import baserlib
77
78
79 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
80 I_FIELDS = ["name", "admin_state", "os",
81 "pnode", "snodes",
82 "disk_template",
83 "nic.ips", "nic.macs", "nic.modes", "nic.uuids", "nic.names",
84 "nic.links", "nic.networks", "nic.networks.names", "nic.bridges",
85 "network_port",
86 "disk.sizes", "disk.spindles", "disk_usage", "disk.uuids",
87 "disk.names",
88 "beparams", "hvparams",
89 "oper_state", "oper_ram", "oper_vcpus", "status",
90 "custom_hvparams", "custom_beparams", "custom_nicparams",
91 ] + _COMMON_FIELDS
92
93 N_FIELDS = ["name", "offline", "master_candidate", "drained",
94 "dtotal", "dfree", "sptotal", "spfree",
95 "mtotal", "mnode", "mfree",
96 "pinst_cnt", "sinst_cnt",
97 "ctotal", "cnos", "cnodes", "csockets",
98 "pip", "sip", "role",
99 "pinst_list", "sinst_list",
100 "master_capable", "vm_capable",
101 "ndparams",
102 "group.uuid",
103 ] + _COMMON_FIELDS
104
105 NET_FIELDS = ["name", "network", "gateway",
106 "network6", "gateway6",
107 "mac_prefix",
108 "free_count", "reserved_count",
109 "map", "group_list", "inst_list",
110 "external_reservations",
111 ] + _COMMON_FIELDS
112
113 G_FIELDS = [
114 "alloc_policy",
115 "name",
116 "node_cnt",
117 "node_list",
118 "ipolicy",
119 "custom_ipolicy",
120 "diskparams",
121 "custom_diskparams",
122 "ndparams",
123 "custom_ndparams",
124 ] + _COMMON_FIELDS
125
126 J_FIELDS_BULK = [
127 "id", "ops", "status", "summary",
128 "opstatus",
129 "received_ts", "start_ts", "end_ts",
130 ]
131
132 J_FIELDS = J_FIELDS_BULK + [
133 "oplog",
134 "opresult",
135 ]
136
137 _NR_DRAINED = "drained"
138 _NR_MASTER_CANDIDATE = "master-candidate"
139 _NR_MASTER = "master"
140 _NR_OFFLINE = "offline"
141 _NR_REGULAR = "regular"
142
143 _NR_MAP = {
144 constants.NR_MASTER: _NR_MASTER,
145 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
146 constants.NR_DRAINED: _NR_DRAINED,
147 constants.NR_OFFLINE: _NR_OFFLINE,
148 constants.NR_REGULAR: _NR_REGULAR,
149 }
150
151 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
152
153
154 _REQ_DATA_VERSION = "__version__"
155
156
157 _INST_CREATE_REQV1 = "instance-create-reqv1"
158
159
160 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
161
162
163 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
164
165
166 _NODE_EVAC_RES1 = "node-evac-res1"
167
168 ALL_FEATURES = compat.UniqueFrozenset([
169 _INST_CREATE_REQV1,
170 _INST_REINSTALL_REQV1,
171 _NODE_MIGRATE_REQV1,
172 _NODE_EVAC_RES1,
173 ])
174
175
176 _WFJC_TIMEOUT = 10
182 """Updates the beparams dict of inst to support the memory field.
183
184 @param inst: Inst dict
185 @return: Updated inst dict
186
187 """
188 beparams = inst["beparams"]
189 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
190
191 return inst
192
193
194 -class R_root(baserlib.ResourceBase):
195 """/ resource.
196
197 """
198 @staticmethod
200 """Supported for legacy reasons.
201
202 """
203 return None
204
205
206 -class R_2(R_root):
207 """/2 resource.
208
209 """
210
213 """/version resource.
214
215 This resource should be used to determine the remote API version and
216 to adapt clients accordingly.
217
218 """
219 @staticmethod
225
226
227 -class R_2_info(baserlib.OpcodeResource):
228 """/2/info resource.
229
230 """
231 GET_OPCODE = opcodes.OpClusterQuery
232 GET_ALIASES = {
233 "volume_group_name": "vg_name",
234 "drbd_usermode_helper": "drbd_helper",
235 }
236
243
246 """/2/features resource.
247
248 """
249 @staticmethod
251 """Returns list of optional RAPI features implemented.
252
253 """
254 return list(ALL_FEATURES)
255
256
257 -class R_2_os(baserlib.OpcodeResource):
258 """/2/os resource.
259
260 """
261 GET_OPCODE = opcodes.OpOsDiagnose
262
264 """Return a list of all OSes.
265
266 Can return error 500 in case of a problem.
267
268 Example: ["debian-etch"]
269
270 """
271 cl = self.GetClient()
272 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
273 job_id = self.SubmitJob([op], cl=cl)
274
275 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
276 diagnose_data = result[0]
277
278 if not isinstance(diagnose_data, list):
279 raise http.HttpBadGateway(message="Can't get OS list")
280
281 os_names = []
282 for (name, variants) in diagnose_data:
283 os_names.extend(cli.CalculateOSNames(name, variants))
284
285 return os_names
286
293
300
301
302 -class R_2_jobs(baserlib.ResourceBase):
303 """/2/jobs resource.
304
305 """
321
324 """/2/jobs/[job_id] resource.
325
326 """
328 """Returns a job status.
329
330 @return: a dictionary with job parameters.
331 The result includes:
332 - id: job ID as a number
333 - status: current job status as a string
334 - ops: involved OpCodes as a list of dictionaries for each
335 opcodes in the job
336 - opstatus: OpCodes status as a list
337 - opresult: OpCodes results as a list of lists
338
339 """
340 job_id = self.items[0]
341 result = self.GetClient(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
342 if result is None:
343 raise http.HttpNotFound()
344 return baserlib.MapFields(J_FIELDS, result)
345
347 """Cancel not-yet-started job.
348
349 """
350 job_id = self.items[0]
351 result = self.GetClient().CancelJob(job_id)
352 return result
353
356 """/2/jobs/[job_id]/wait resource.
357
358 """
359
360
361 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
362
364 """Waits for job changes.
365
366 """
367 job_id = self.items[0]
368
369 fields = self.getBodyParameter("fields")
370 prev_job_info = self.getBodyParameter("previous_job_info", None)
371 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
372
373 if not isinstance(fields, list):
374 raise http.HttpBadRequest("The 'fields' parameter should be a list")
375
376 if not (prev_job_info is None or isinstance(prev_job_info, list)):
377 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
378 " be a list")
379
380 if not (prev_log_serial is None or
381 isinstance(prev_log_serial, (int, long))):
382 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
383 " be a number")
384
385 client = self.GetClient()
386 result = client.WaitForJobChangeOnce(job_id, fields,
387 prev_job_info, prev_log_serial,
388 timeout=_WFJC_TIMEOUT)
389 if not result:
390 raise http.HttpNotFound()
391
392 if result == constants.JOB_NOTCHANGED:
393
394 return None
395
396 (job_info, log_entries) = result
397
398 return {
399 "job_info": job_info,
400 "log_entries": log_entries,
401 }
402
403
404 -class R_2_nodes(baserlib.OpcodeResource):
424
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(query=True)
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
705
736
753
770
786
822
825 """/2/groups/[group_name] resource.
826
827 """
828 DELETE_OPCODE = opcodes.OpGroupRemove
829
842
852
855 """/2/groups/[group_name]/modify resource.
856
857 """
858 PUT_OPCODE = opcodes.OpGroupSetParams
859 PUT_RENAME = {
860 "custom_ndparams": "ndparams",
861 "custom_ipolicy": "ipolicy",
862 "custom_diskparams": "diskparams",
863 }
864
873
876 """/2/groups/[group_name]/rename resource.
877
878 """
879 PUT_OPCODE = opcodes.OpGroupRename
880
890
908
911 """Convert in place the usb_devices string to the proper format.
912
913 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
914 comma to space because commas cannot be accepted on the command line
915 (they already act as the separator between different hvparams). RAPI
916 should be able to accept commas for backwards compatibility, but we want
917 it to also accept the new space separator. Therefore, we convert
918 spaces into commas here and keep the old parsing logic elsewhere.
919
920 """
921 try:
922 hvparams = data["hvparams"]
923 usb_devices = hvparams[constants.HV_USB_DEVICES]
924 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
925 data["hvparams"] = hvparams
926 except KeyError:
927
928 pass
929
985
988 """/2/instances-multi-alloc resource.
989
990 """
991 POST_OPCODE = opcodes.OpInstanceMultiAlloc
992
994 """Try to allocate multiple instances.
995
996 @return: A dict with submitted jobs, allocatable instances and failed
997 allocations
998
999 """
1000 if "instances" not in self.request_body:
1001 raise http.HttpBadRequest("Request is missing required 'instances' field"
1002 " in body")
1003
1004
1005
1006 OPCODE_RENAME = {
1007 "os": "os_type",
1008 "name": "instance_name",
1009 }
1010
1011 body = objects.FillDict(self.request_body, {
1012 "instances": [
1013 baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
1014 rename=OPCODE_RENAME)
1015 for inst in self.request_body["instances"]
1016 ],
1017 })
1018
1019 return (body, {
1020 "dry_run": self.dryRun(),
1021 })
1022
1055
1072
1075 """/2/instances/[instance_name]/reboot resource.
1076
1077 Implements an instance reboot.
1078
1079 """
1080 POST_OPCODE = opcodes.OpInstanceReboot
1081
1083 """Reboot an instance.
1084
1085 The URI takes type=[hard|soft|full] and
1086 ignore_secondaries=[False|True] parameters.
1087
1088 """
1089 return (self.request_body, {
1090 "instance_name": self.items[0],
1091 "reboot_type":
1092 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1093 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1094 "dry_run": self.dryRun(),
1095 })
1096
1099 """/2/instances/[instance_name]/startup resource.
1100
1101 Implements an instance startup.
1102
1103 """
1104 PUT_OPCODE = opcodes.OpInstanceStartup
1105
1119
1122 """/2/instances/[instance_name]/shutdown resource.
1123
1124 Implements an instance shutdown.
1125
1126 """
1127 PUT_OPCODE = opcodes.OpInstanceShutdown
1128
1138
1141 """Parses a request for reinstalling an instance.
1142
1143 """
1144 if not isinstance(data, dict):
1145 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1146
1147 ostype = baserlib.CheckParameter(data, "os", default=None)
1148 start = baserlib.CheckParameter(data, "start", exptype=bool,
1149 default=True)
1150 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1151
1152 ops = [
1153 opcodes.OpInstanceShutdown(instance_name=name),
1154 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1155 osparams=osparams),
1156 ]
1157
1158 if start:
1159 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1160
1161 return ops
1162
1165 """/2/instances/[instance_name]/reinstall resource.
1166
1167 Implements an instance reinstall.
1168
1169 """
1170 POST_OPCODE = opcodes.OpInstanceReinstall
1171
1173 """Reinstall an instance.
1174
1175 The URI takes os=name and nostartup=[0|1] optional
1176 parameters. By default, the instance will be started
1177 automatically.
1178
1179 """
1180 if self.request_body:
1181 if self.queryargs:
1182 raise http.HttpBadRequest("Can't combine query and body parameters")
1183
1184 body = self.request_body
1185 elif self.queryargs:
1186
1187 body = {
1188 "os": self._checkStringVariable("os"),
1189 "start": not self._checkIntVariable("nostartup"),
1190 }
1191 else:
1192 body = {}
1193
1194 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1195
1196 return self.SubmitJob(ops)
1197
1200 """/2/instances/[instance_name]/replace-disks resource.
1201
1202 """
1203 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1204
1206 """Replaces disks on an instance.
1207
1208 """
1209 static = {
1210 "instance_name": self.items[0],
1211 }
1212
1213 if self.request_body:
1214 data = self.request_body
1215 elif self.queryargs:
1216
1217 data = {
1218 "remote_node": self._checkStringVariable("remote_node", default=None),
1219 "mode": self._checkStringVariable("mode", default=None),
1220 "disks": self._checkStringVariable("disks", default=None),
1221 "iallocator": self._checkStringVariable("iallocator", default=None),
1222 }
1223 else:
1224 data = {}
1225
1226
1227 try:
1228 raw_disks = data.pop("disks")
1229 except KeyError:
1230 pass
1231 else:
1232 if raw_disks:
1233 if ht.TListOf(ht.TInt)(raw_disks):
1234 data["disks"] = raw_disks
1235 else:
1236
1237 try:
1238 data["disks"] = [int(part) for part in raw_disks.split(",")]
1239 except (TypeError, ValueError), err:
1240 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1241
1242 return (data, static)
1243
1261
1276
1279 """/2/instances/[instance_name]/recreate-disks resource.
1280
1281 """
1282 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1283
1285 """Recreate disks for an instance.
1286
1287 """
1288 return ({}, {
1289 "instance_name": self.items[0],
1290 })
1291
1294 """/2/instances/[instance_name]/prepare-export resource.
1295
1296 """
1297 PUT_OPCODE = opcodes.OpBackupPrepare
1298
1307
1310 """/2/instances/[instance_name]/export resource.
1311
1312 """
1313 PUT_OPCODE = opcodes.OpBackupExport
1314 PUT_RENAME = {
1315 "destination": "target_node",
1316 }
1317
1325
1340
1355
1358 """/2/instances/[instance_name]/rename resource.
1359
1360 """
1361 PUT_OPCODE = opcodes.OpInstanceRename
1362
1370
1373 """/2/instances/[instance_name]/modify resource.
1374
1375 """
1376 PUT_OPCODE = opcodes.OpInstanceSetParams
1377 PUT_RENAME = {
1378 "custom_beparams": "beparams",
1379 "custom_hvparams": "hvparams",
1380 }
1381
1392
1395 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1396
1397 """
1398 POST_OPCODE = opcodes.OpInstanceGrowDisk
1399
1401 """Increases the size of an instance disk.
1402
1403 """
1404 return (self.request_body, {
1405 "instance_name": self.items[0],
1406 "disk": int(self.items[1]),
1407 })
1408
1433
1436 """Tries to extract C{fields} query parameter.
1437
1438 @type args: dictionary
1439 @rtype: list of string
1440 @raise http.HttpBadRequest: When parameter can't be found
1441
1442 """
1443 try:
1444 fields = args["fields"]
1445 except KeyError:
1446 raise http.HttpBadRequest("Missing 'fields' query argument")
1447
1448 return _SplitQueryFields(fields[0])
1449
1452 """Splits fields as given for a query request.
1453
1454 @type fields: string
1455 @rtype: list of string
1456
1457 """
1458 return [i.strip() for i in fields.split(",")]
1459
1460
1461 -class R_2_query(baserlib.ResourceBase):
1462 """/2/query/[resource] resource.
1463
1464 """
1465
1466 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1467 PUT_ACCESS = GET_ACCESS
1468 GET_OPCODE = opcodes.OpQuery
1469 PUT_OPCODE = opcodes.OpQuery
1470
1471 - def _Query(self, fields, qfilter):
1473
1475 """Returns resource information.
1476
1477 @return: Query result, see L{objects.QueryResponse}
1478
1479 """
1480 return self._Query(_GetQueryFields(self.queryargs), None)
1481
1483 """Submits job querying for resources.
1484
1485 @return: Query result, see L{objects.QueryResponse}
1486
1487 """
1488 body = self.request_body
1489
1490 baserlib.CheckType(body, dict, "Body contents")
1491
1492 try:
1493 fields = body["fields"]
1494 except KeyError:
1495 fields = _GetQueryFields(self.queryargs)
1496
1497 qfilter = body.get("qfilter", None)
1498
1499 if qfilter is None:
1500 qfilter = body.get("filter", None)
1501
1502 return self._Query(fields, qfilter)
1503
1506 """/2/query/[resource]/fields resource.
1507
1508 """
1509 GET_OPCODE = opcodes.OpQueryFields
1510
1512 """Retrieves list of available fields for a resource.
1513
1514 @return: List of serialized L{objects.QueryFieldDefinition}
1515
1516 """
1517 try:
1518 raw_fields = self.queryargs["fields"]
1519 except KeyError:
1520 fields = None
1521 else:
1522 fields = _SplitQueryFields(raw_fields[0])
1523
1524 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1525
1605
1614
1623
1632
1641
1650