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):
405 """/2/nodes resource.
406
407 """
408
423
426 """/2/nodes/[node_name] resource.
427
428 """
429 GET_ALIASES = {
430 "sip": "secondary_ip",
431 }
432
445
448 """/2/nodes/[node_name]/powercycle resource.
449
450 """
451 POST_OPCODE = opcodes.OpNodePowercycle
452
454 """Tries to powercycle a node.
455
456 """
457 return (self.request_body, {
458 "node_name": self.items[0],
459 "force": self.useForce(),
460 })
461
464 """/2/nodes/[node_name]/role resource.
465
466 """
467 PUT_OPCODE = opcodes.OpNodeSetParams
468
470 """Returns the current node role.
471
472 @return: Node role
473
474 """
475 node_name = self.items[0]
476 client = self.GetClient(query=True)
477 result = client.QueryNodes(names=[node_name], fields=["role"],
478 use_locking=self.useLocking())
479
480 return _NR_MAP[result[0][0]]
481
520
523 """/2/nodes/[node_name]/evacuate resource.
524
525 """
526 POST_OPCODE = opcodes.OpNodeEvacuate
527
529 """Evacuate all instances off a node.
530
531 """
532 return (self.request_body, {
533 "node_name": self.items[0],
534 "dry_run": self.dryRun(),
535 })
536
539 """/2/nodes/[node_name]/migrate resource.
540
541 """
542 POST_OPCODE = opcodes.OpNodeMigrate
543
545 """Migrate all primary instances from a node.
546
547 """
548 if self.queryargs:
549
550 if "live" in self.queryargs and "mode" in self.queryargs:
551 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
552 " be passed")
553
554 if "live" in self.queryargs:
555 if self._checkIntVariable("live", default=1):
556 mode = constants.HT_MIGRATION_LIVE
557 else:
558 mode = constants.HT_MIGRATION_NONLIVE
559 else:
560 mode = self._checkStringVariable("mode", default=None)
561
562 data = {
563 "mode": mode,
564 }
565 else:
566 data = self.request_body
567
568 return (data, {
569 "node_name": self.items[0],
570 })
571
574 """/2/nodes/[node_name]/modify resource.
575
576 """
577 POST_OPCODE = opcodes.OpNodeSetParams
578
580 """Changes parameters of a node.
581
582 """
583 assert len(self.items) == 1
584
585 return (self.request_body, {
586 "node_name": self.items[0],
587 })
588
614
645
668
671 """/2/networks resource.
672
673 """
674 POST_OPCODE = opcodes.OpNetworkAdd
675 POST_RENAME = {
676 "name": "network_name",
677 }
678
680 """Create a network.
681
682 """
683 assert not self.items
684 return (self.request_body, {
685 "dry_run": self.dryRun(),
686 })
687
702
733
750
767
783
786 """/2/groups resource.
787
788 """
789 POST_OPCODE = opcodes.OpGroupAdd
790 POST_RENAME = {
791 "name": "group_name",
792 }
793
795 """Create a node group.
796
797
798 """
799 assert not self.items
800 return (self.request_body, {
801 "dry_run": self.dryRun(),
802 })
803
818
821 """/2/groups/[group_name] resource.
822
823 """
824 DELETE_OPCODE = opcodes.OpGroupRemove
825
838
848
851 """/2/groups/[group_name]/modify resource.
852
853 """
854 PUT_OPCODE = opcodes.OpGroupSetParams
855 PUT_RENAME = {
856 "custom_ndparams": "ndparams",
857 "custom_ipolicy": "ipolicy",
858 "custom_diskparams": "diskparams",
859 }
860
869
872 """/2/groups/[group_name]/rename resource.
873
874 """
875 PUT_OPCODE = opcodes.OpGroupRename
876
886
904
907 """Convert in place the usb_devices string to the proper format.
908
909 In Ganeti 2.8.4 the separator for the usb_devices hvparam was changed from
910 comma to space because commas cannot be accepted on the command line
911 (they already act as the separator between different hvparams). RAPI
912 should be able to accept commas for backwards compatibility, but we want
913 it to also accept the new space separator. Therefore, we convert
914 spaces into commas here and keep the old parsing logic elsewhere.
915
916 """
917 try:
918 hvparams = data["hvparams"]
919 usb_devices = hvparams[constants.HV_USB_DEVICES]
920 hvparams[constants.HV_USB_DEVICES] = usb_devices.replace(" ", ",")
921 data["hvparams"] = hvparams
922 except KeyError:
923
924 pass
925
980
983 """/2/instances-multi-alloc resource.
984
985 """
986 POST_OPCODE = opcodes.OpInstanceMultiAlloc
987
989 """Try to allocate multiple instances.
990
991 @return: A dict with submitted jobs, allocatable instances and failed
992 allocations
993
994 """
995 if "instances" not in self.request_body:
996 raise http.HttpBadRequest("Request is missing required 'instances' field"
997 " in body")
998
999
1000
1001 OPCODE_RENAME = {
1002 "os": "os_type",
1003 "name": "instance_name",
1004 }
1005
1006 body = objects.FillDict(self.request_body, {
1007 "instances": [
1008 baserlib.FillOpcode(opcodes.OpInstanceCreate, inst, {},
1009 rename=OPCODE_RENAME)
1010 for inst in self.request_body["instances"]
1011 ],
1012 })
1013
1014 return (body, {
1015 "dry_run": self.dryRun(),
1016 })
1017
1020 """/2/instances/[instance_name] resource.
1021
1022 """
1023 DELETE_OPCODE = opcodes.OpInstanceRemove
1024
1038
1049
1066
1069 """/2/instances/[instance_name]/reboot resource.
1070
1071 Implements an instance reboot.
1072
1073 """
1074 POST_OPCODE = opcodes.OpInstanceReboot
1075
1077 """Reboot an instance.
1078
1079 The URI takes type=[hard|soft|full] and
1080 ignore_secondaries=[False|True] parameters.
1081
1082 """
1083 return (self.request_body, {
1084 "instance_name": self.items[0],
1085 "reboot_type":
1086 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1087 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1088 "dry_run": self.dryRun(),
1089 })
1090
1093 """/2/instances/[instance_name]/startup resource.
1094
1095 Implements an instance startup.
1096
1097 """
1098 PUT_OPCODE = opcodes.OpInstanceStartup
1099
1113
1116 """/2/instances/[instance_name]/shutdown resource.
1117
1118 Implements an instance shutdown.
1119
1120 """
1121 PUT_OPCODE = opcodes.OpInstanceShutdown
1122
1132
1135 """Parses a request for reinstalling an instance.
1136
1137 """
1138 if not isinstance(data, dict):
1139 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1140
1141 ostype = baserlib.CheckParameter(data, "os", default=None)
1142 start = baserlib.CheckParameter(data, "start", exptype=bool,
1143 default=True)
1144 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1145
1146 ops = [
1147 opcodes.OpInstanceShutdown(instance_name=name),
1148 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1149 osparams=osparams),
1150 ]
1151
1152 if start:
1153 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1154
1155 return ops
1156
1159 """/2/instances/[instance_name]/reinstall resource.
1160
1161 Implements an instance reinstall.
1162
1163 """
1164 POST_OPCODE = opcodes.OpInstanceReinstall
1165
1167 """Reinstall an instance.
1168
1169 The URI takes os=name and nostartup=[0|1] optional
1170 parameters. By default, the instance will be started
1171 automatically.
1172
1173 """
1174 if self.request_body:
1175 if self.queryargs:
1176 raise http.HttpBadRequest("Can't combine query and body parameters")
1177
1178 body = self.request_body
1179 elif self.queryargs:
1180
1181 body = {
1182 "os": self._checkStringVariable("os"),
1183 "start": not self._checkIntVariable("nostartup"),
1184 }
1185 else:
1186 body = {}
1187
1188 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1189
1190 return self.SubmitJob(ops)
1191
1194 """/2/instances/[instance_name]/replace-disks resource.
1195
1196 """
1197 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1198
1200 """Replaces disks on an instance.
1201
1202 """
1203 static = {
1204 "instance_name": self.items[0],
1205 }
1206
1207 if self.request_body:
1208 data = self.request_body
1209 elif self.queryargs:
1210
1211 data = {
1212 "remote_node": self._checkStringVariable("remote_node", default=None),
1213 "mode": self._checkStringVariable("mode", default=None),
1214 "disks": self._checkStringVariable("disks", default=None),
1215 "iallocator": self._checkStringVariable("iallocator", default=None),
1216 }
1217 else:
1218 data = {}
1219
1220
1221 try:
1222 raw_disks = data.pop("disks")
1223 except KeyError:
1224 pass
1225 else:
1226 if raw_disks:
1227 if ht.TListOf(ht.TInt)(raw_disks):
1228 data["disks"] = raw_disks
1229 else:
1230
1231 try:
1232 data["disks"] = [int(part) for part in raw_disks.split(",")]
1233 except (TypeError, ValueError), err:
1234 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1235
1236 return (data, static)
1237
1255
1270
1273 """/2/instances/[instance_name]/recreate-disks resource.
1274
1275 """
1276 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1277
1279 """Recreate disks for an instance.
1280
1281 """
1282 return ({}, {
1283 "instance_name": self.items[0],
1284 })
1285
1288 """/2/instances/[instance_name]/prepare-export resource.
1289
1290 """
1291 PUT_OPCODE = opcodes.OpBackupPrepare
1292
1301
1304 """/2/instances/[instance_name]/export resource.
1305
1306 """
1307 PUT_OPCODE = opcodes.OpBackupExport
1308 PUT_RENAME = {
1309 "destination": "target_node",
1310 }
1311
1319
1334
1349
1352 """/2/instances/[instance_name]/rename resource.
1353
1354 """
1355 PUT_OPCODE = opcodes.OpInstanceRename
1356
1364
1367 """/2/instances/[instance_name]/modify resource.
1368
1369 """
1370 PUT_OPCODE = opcodes.OpInstanceSetParams
1371 PUT_RENAME = {
1372 "custom_beparams": "beparams",
1373 "custom_hvparams": "hvparams",
1374 }
1375
1386
1389 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1390
1391 """
1392 POST_OPCODE = opcodes.OpInstanceGrowDisk
1393
1395 """Increases the size of an instance disk.
1396
1397 """
1398 return (self.request_body, {
1399 "instance_name": self.items[0],
1400 "disk": int(self.items[1]),
1401 })
1402
1405 """/2/instances/[instance_name]/console resource.
1406
1407 """
1408 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1409 GET_OPCODE = opcodes.OpInstanceConsole
1410
1412 """Request information for connecting to instance's console.
1413
1414 @return: Serialized instance console description, see
1415 L{objects.InstanceConsole}
1416
1417 """
1418 instance_name = self.items[0]
1419 client = self.GetClient(query=True)
1420
1421 ((console, oper_state), ) = \
1422 client.QueryInstances([instance_name], ["console", "oper_state"], False)
1423
1424 if not oper_state:
1425 raise http.HttpServiceUnavailable("Instance console unavailable")
1426
1427 assert isinstance(console, dict)
1428 return console
1429
1432 """Tries to extract C{fields} query parameter.
1433
1434 @type args: dictionary
1435 @rtype: list of string
1436 @raise http.HttpBadRequest: When parameter can't be found
1437
1438 """
1439 try:
1440 fields = args["fields"]
1441 except KeyError:
1442 raise http.HttpBadRequest("Missing 'fields' query argument")
1443
1444 return _SplitQueryFields(fields[0])
1445
1448 """Splits fields as given for a query request.
1449
1450 @type fields: string
1451 @rtype: list of string
1452
1453 """
1454 return [i.strip() for i in fields.split(",")]
1455
1456
1457 -class R_2_query(baserlib.ResourceBase):
1458 """/2/query/[resource] resource.
1459
1460 """
1461
1462 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1463 PUT_ACCESS = GET_ACCESS
1464 GET_OPCODE = opcodes.OpQuery
1465 PUT_OPCODE = opcodes.OpQuery
1466
1467 - def _Query(self, fields, qfilter):
1470
1472 """Returns resource information.
1473
1474 @return: Query result, see L{objects.QueryResponse}
1475
1476 """
1477 return self._Query(_GetQueryFields(self.queryargs), None)
1478
1480 """Submits job querying for resources.
1481
1482 @return: Query result, see L{objects.QueryResponse}
1483
1484 """
1485 body = self.request_body
1486
1487 baserlib.CheckType(body, dict, "Body contents")
1488
1489 try:
1490 fields = body["fields"]
1491 except KeyError:
1492 fields = _GetQueryFields(self.queryargs)
1493
1494 qfilter = body.get("qfilter", None)
1495
1496 if qfilter is None:
1497 qfilter = body.get("filter", None)
1498
1499 return self._Query(fields, qfilter)
1500
1503 """/2/query/[resource]/fields resource.
1504
1505 """
1506 GET_OPCODE = opcodes.OpQueryFields
1507
1509 """Retrieves list of available fields for a resource.
1510
1511 @return: List of serialized L{objects.QueryFieldDefinition}
1512
1513 """
1514 try:
1515 raw_fields = self.queryargs["fields"]
1516 except KeyError:
1517 fields = None
1518 else:
1519 fields = _SplitQueryFields(raw_fields[0])
1520
1521 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1522
1602
1611
1620
1629
1638
1647