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",
75 "nic.links", "nic.networks", "nic.networks.names", "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",
98 "free_count", "reserved_count",
99 "map", "group_list", "inst_list",
100 "external_reservations",
101 ] + _COMMON_FIELDS
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
144 _REQ_DATA_VERSION = "__version__"
145
146
147 _INST_CREATE_REQV1 = "instance-create-reqv1"
148
149
150 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
151
152
153 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
154
155
156 _NODE_EVAC_RES1 = "node-evac-res1"
157
158 ALL_FEATURES = compat.UniqueFrozenset([
159 _INST_CREATE_REQV1,
160 _INST_REINSTALL_REQV1,
161 _NODE_MIGRATE_REQV1,
162 _NODE_EVAC_RES1,
163 ])
164
165
166 _WFJC_TIMEOUT = 10
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
190 """Supported for legacy reasons.
191
192 """
193 return None
194
195
196 -class R_2(R_root):
197 """/2 resource.
198
199 """
200
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
215
216
217 -class R_2_info(baserlib.OpcodeResource):
229
232 """/2/features resource.
233
234 """
235 @staticmethod
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
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
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
279
286
287
288 -class R_2_jobs(baserlib.ResourceBase):
289 """/2/jobs resource.
290
291 """
307
310 """/2/jobs/[job_id] resource.
311
312 """
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(query=True).QueryJobs([job_id, ], J_FIELDS)[0]
328 if result is None:
329 raise http.HttpNotFound()
330 return baserlib.MapFields(J_FIELDS, result)
331
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
342 """/2/jobs/[job_id]/wait resource.
343
344 """
345
346
347 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
348
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
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):
410
413 """/2/nodes/[node_name] resource.
414
415 """
416 GET_OPCODE = opcodes.OpNodeQuery
417
430
433 """/2/nodes/[node_name]/powercycle resource.
434
435 """
436 POST_OPCODE = opcodes.OpNodePowercycle
437
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
449 """/2/nodes/[node_name]/role resource.
450
451 """
452 PUT_OPCODE = opcodes.OpNodeSetParams
453
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
505
508 """/2/nodes/[node_name]/evacuate resource.
509
510 """
511 POST_OPCODE = opcodes.OpNodeEvacuate
512
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
524 """/2/nodes/[node_name]/migrate resource.
525
526 """
527 POST_OPCODE = opcodes.OpNodeMigrate
528
530 """Migrate all primary instances from a node.
531
532 """
533 if self.queryargs:
534
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
559 """/2/nodes/[node_name]/modify resource.
560
561 """
562 POST_OPCODE = opcodes.OpNodeSetParams
563
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
599
630
653
688
719
736
753
769
805
808 """/2/groups/[group_name] resource.
809
810 """
811 DELETE_OPCODE = opcodes.OpGroupRemove
812
825
835
851
854 """/2/groups/[group_name]/rename resource.
855
856 """
857 PUT_OPCODE = opcodes.OpGroupRename
858
868
886
940
943 """/2/instances-multi-alloc resource.
944
945 """
946 POST_OPCODE = opcodes.OpInstanceMultiAlloc
947
949 """Try to allocate multiple instances.
950
951 @return: A dict with submitted jobs, allocatable instances and failed
952 allocations
953
954 """
955 if "instances" not in self.request_body:
956 raise http.HttpBadRequest("Request is missing required 'instances' field"
957 " in body")
958
959 op_id = {
960 "OP_ID": self.POST_OPCODE.OP_ID,
961 }
962 body = objects.FillDict(self.request_body, {
963 "instances": [objects.FillDict(inst, op_id)
964 for inst in self.request_body["instances"]],
965 })
966
967 return (body, {
968 "dry_run": self.dryRun(),
969 })
970
1003
1020
1023 """/2/instances/[instance_name]/reboot resource.
1024
1025 Implements an instance reboot.
1026
1027 """
1028 POST_OPCODE = opcodes.OpInstanceReboot
1029
1031 """Reboot an instance.
1032
1033 The URI takes type=[hard|soft|full] and
1034 ignore_secondaries=[False|True] parameters.
1035
1036 """
1037 return ({}, {
1038 "instance_name": self.items[0],
1039 "reboot_type":
1040 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
1041 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
1042 "dry_run": self.dryRun(),
1043 })
1044
1047 """/2/instances/[instance_name]/startup resource.
1048
1049 Implements an instance startup.
1050
1051 """
1052 PUT_OPCODE = opcodes.OpInstanceStartup
1053
1067
1070 """/2/instances/[instance_name]/shutdown resource.
1071
1072 Implements an instance shutdown.
1073
1074 """
1075 PUT_OPCODE = opcodes.OpInstanceShutdown
1076
1086
1089 """Parses a request for reinstalling an instance.
1090
1091 """
1092 if not isinstance(data, dict):
1093 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1094
1095 ostype = baserlib.CheckParameter(data, "os", default=None)
1096 start = baserlib.CheckParameter(data, "start", exptype=bool,
1097 default=True)
1098 osparams = baserlib.CheckParameter(data, "osparams", default=None)
1099
1100 ops = [
1101 opcodes.OpInstanceShutdown(instance_name=name),
1102 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
1103 osparams=osparams),
1104 ]
1105
1106 if start:
1107 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
1108
1109 return ops
1110
1113 """/2/instances/[instance_name]/reinstall resource.
1114
1115 Implements an instance reinstall.
1116
1117 """
1118 POST_OPCODE = opcodes.OpInstanceReinstall
1119
1121 """Reinstall an instance.
1122
1123 The URI takes os=name and nostartup=[0|1] optional
1124 parameters. By default, the instance will be started
1125 automatically.
1126
1127 """
1128 if self.request_body:
1129 if self.queryargs:
1130 raise http.HttpBadRequest("Can't combine query and body parameters")
1131
1132 body = self.request_body
1133 elif self.queryargs:
1134
1135 body = {
1136 "os": self._checkStringVariable("os"),
1137 "start": not self._checkIntVariable("nostartup"),
1138 }
1139 else:
1140 body = {}
1141
1142 ops = _ParseInstanceReinstallRequest(self.items[0], body)
1143
1144 return self.SubmitJob(ops)
1145
1148 """/2/instances/[instance_name]/replace-disks resource.
1149
1150 """
1151 POST_OPCODE = opcodes.OpInstanceReplaceDisks
1152
1154 """Replaces disks on an instance.
1155
1156 """
1157 static = {
1158 "instance_name": self.items[0],
1159 }
1160
1161 if self.request_body:
1162 data = self.request_body
1163 elif self.queryargs:
1164
1165 data = {
1166 "remote_node": self._checkStringVariable("remote_node", default=None),
1167 "mode": self._checkStringVariable("mode", default=None),
1168 "disks": self._checkStringVariable("disks", default=None),
1169 "iallocator": self._checkStringVariable("iallocator", default=None),
1170 }
1171 else:
1172 data = {}
1173
1174
1175 try:
1176 raw_disks = data.pop("disks")
1177 except KeyError:
1178 pass
1179 else:
1180 if raw_disks:
1181 if ht.TListOf(ht.TInt)(raw_disks):
1182 data["disks"] = raw_disks
1183 else:
1184
1185 try:
1186 data["disks"] = [int(part) for part in raw_disks.split(",")]
1187 except (TypeError, ValueError), err:
1188 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1189
1190 return (data, static)
1191
1209
1224
1227 """/2/instances/[instance_name]/recreate-disks resource.
1228
1229 """
1230 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1231
1233 """Recreate disks for an instance.
1234
1235 """
1236 return ({}, {
1237 "instance_name": self.items[0],
1238 })
1239
1242 """/2/instances/[instance_name]/prepare-export resource.
1243
1244 """
1245 PUT_OPCODE = opcodes.OpBackupPrepare
1246
1255
1258 """/2/instances/[instance_name]/export resource.
1259
1260 """
1261 PUT_OPCODE = opcodes.OpBackupExport
1262 PUT_RENAME = {
1263 "destination": "target_node",
1264 }
1265
1273
1288
1303
1306 """/2/instances/[instance_name]/rename resource.
1307
1308 """
1309 PUT_OPCODE = opcodes.OpInstanceRename
1310
1318
1333
1336 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1337
1338 """
1339 POST_OPCODE = opcodes.OpInstanceGrowDisk
1340
1342 """Increases the size of an instance disk.
1343
1344 """
1345 return (self.request_body, {
1346 "instance_name": self.items[0],
1347 "disk": int(self.items[1]),
1348 })
1349
1374
1377 """Tries to extract C{fields} query parameter.
1378
1379 @type args: dictionary
1380 @rtype: list of string
1381 @raise http.HttpBadRequest: When parameter can't be found
1382
1383 """
1384 try:
1385 fields = args["fields"]
1386 except KeyError:
1387 raise http.HttpBadRequest("Missing 'fields' query argument")
1388
1389 return _SplitQueryFields(fields[0])
1390
1393 """Splits fields as given for a query request.
1394
1395 @type fields: string
1396 @rtype: list of string
1397
1398 """
1399 return [i.strip() for i in fields.split(",")]
1400
1401
1402 -class R_2_query(baserlib.ResourceBase):
1403 """/2/query/[resource] resource.
1404
1405 """
1406
1407 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]
1408 PUT_ACCESS = GET_ACCESS
1409 GET_OPCODE = opcodes.OpQuery
1410 PUT_OPCODE = opcodes.OpQuery
1411
1412 - def _Query(self, fields, qfilter):
1414
1416 """Returns resource information.
1417
1418 @return: Query result, see L{objects.QueryResponse}
1419
1420 """
1421 return self._Query(_GetQueryFields(self.queryargs), None)
1422
1424 """Submits job querying for resources.
1425
1426 @return: Query result, see L{objects.QueryResponse}
1427
1428 """
1429 body = self.request_body
1430
1431 baserlib.CheckType(body, dict, "Body contents")
1432
1433 try:
1434 fields = body["fields"]
1435 except KeyError:
1436 fields = _GetQueryFields(self.queryargs)
1437
1438 qfilter = body.get("qfilter", None)
1439
1440 if qfilter is None:
1441 qfilter = body.get("filter", None)
1442
1443 return self._Query(fields, qfilter)
1444
1447 """/2/query/[resource]/fields resource.
1448
1449 """
1450 GET_OPCODE = opcodes.OpQueryFields
1451
1453 """Retrieves list of available fields for a resource.
1454
1455 @return: List of serialized L{objects.QueryFieldDefinition}
1456
1457 """
1458 try:
1459 raw_fields = self.queryargs["fields"]
1460 except KeyError:
1461 fields = None
1462 else:
1463 fields = _SplitQueryFields(raw_fields[0])
1464
1465 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1466
1542
1551
1560
1569
1578
1587