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 http
60 from ganeti import constants
61 from ganeti import cli
62 from ganeti import rapi
63 from ganeti import ht
64 from ganeti import compat
65 from ganeti import ssconf
66 from ganeti.rapi import baserlib
67
68
69 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
70 I_FIELDS = ["name", "admin_state", "os",
71 "pnode", "snodes",
72 "disk_template",
73 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
74 "network_port",
75 "disk.sizes", "disk_usage",
76 "beparams", "hvparams",
77 "oper_state", "oper_ram", "oper_vcpus", "status",
78 "custom_hvparams", "custom_beparams", "custom_nicparams",
79 ] + _COMMON_FIELDS
80
81 N_FIELDS = ["name", "offline", "master_candidate", "drained",
82 "dtotal", "dfree",
83 "mtotal", "mnode", "mfree",
84 "pinst_cnt", "sinst_cnt",
85 "ctotal", "cnodes", "csockets",
86 "pip", "sip", "role",
87 "pinst_list", "sinst_list",
88 "master_capable", "vm_capable",
89 "ndparams",
90 "group.uuid",
91 ] + _COMMON_FIELDS
92
93 G_FIELDS = [
94 "alloc_policy",
95 "name",
96 "node_cnt",
97 "node_list",
98 "ipolicy",
99 "custom_ipolicy",
100 "diskparams",
101 "custom_diskparams",
102 "ndparams",
103 "custom_ndparams",
104 ] + _COMMON_FIELDS
105
106 J_FIELDS_BULK = [
107 "id", "ops", "status", "summary",
108 "opstatus",
109 "received_ts", "start_ts", "end_ts",
110 ]
111
112 J_FIELDS = J_FIELDS_BULK + [
113 "oplog",
114 "opresult",
115 ]
116
117 _NR_DRAINED = "drained"
118 _NR_MASTER_CANDIDATE = "master-candidate"
119 _NR_MASTER = "master"
120 _NR_OFFLINE = "offline"
121 _NR_REGULAR = "regular"
122
123 _NR_MAP = {
124 constants.NR_MASTER: _NR_MASTER,
125 constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
126 constants.NR_DRAINED: _NR_DRAINED,
127 constants.NR_OFFLINE: _NR_OFFLINE,
128 constants.NR_REGULAR: _NR_REGULAR,
129 }
130
131 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
132
133
134 _REQ_DATA_VERSION = "__version__"
135
136
137 _INST_CREATE_REQV1 = "instance-create-reqv1"
138
139
140 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
141
142
143 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
144
145
146 _NODE_EVAC_RES1 = "node-evac-res1"
147
148 ALL_FEATURES = frozenset([
149 _INST_CREATE_REQV1,
150 _INST_REINSTALL_REQV1,
151 _NODE_MIGRATE_REQV1,
152 _NODE_EVAC_RES1,
153 ])
154
155
156 _WFJC_TIMEOUT = 10
162 """Updates the beparams dict of inst to support the memory field.
163
164 @param inst: Inst dict
165 @return: Updated inst dict
166
167 """
168 beparams = inst["beparams"]
169 beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
170
171 return inst
172
173
174 -class R_root(baserlib.ResourceBase):
175 """/ resource.
176
177 """
178 @staticmethod
180 """Supported for legacy reasons.
181
182 """
183 return None
184
185
186 -class R_2(R_root):
187 """/2 resource.
188
189 """
190
193 """/version resource.
194
195 This resource should be used to determine the remote API version and
196 to adapt clients accordingly.
197
198 """
199 @staticmethod
205
206
207 -class R_2_info(baserlib.OpcodeResource):
219
222 """/2/features resource.
223
224 """
225 @staticmethod
227 """Returns list of optional RAPI features implemented.
228
229 """
230 return list(ALL_FEATURES)
231
232
233 -class R_2_os(baserlib.OpcodeResource):
234 """/2/os resource.
235
236 """
237 GET_OPCODE = opcodes.OpOsDiagnose
238
240 """Return a list of all OSes.
241
242 Can return error 500 in case of a problem.
243
244 Example: ["debian-etch"]
245
246 """
247 cl = self.GetClient()
248 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
249 job_id = self.SubmitJob([op], cl=cl)
250
251 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
252 diagnose_data = result[0]
253
254 if not isinstance(diagnose_data, list):
255 raise http.HttpBadGateway(message="Can't get OS list")
256
257 os_names = []
258 for (name, variants) in diagnose_data:
259 os_names.extend(cli.CalculateOSNames(name, variants))
260
261 return os_names
262
269
276
277
278 -class R_2_jobs(baserlib.ResourceBase):
279 """/2/jobs resource.
280
281 """
297
300 """/2/jobs/[job_id] resource.
301
302 """
304 """Returns a job status.
305
306 @return: a dictionary with job parameters.
307 The result includes:
308 - id: job ID as a number
309 - status: current job status as a string
310 - ops: involved OpCodes as a list of dictionaries for each
311 opcodes in the job
312 - opstatus: OpCodes status as a list
313 - opresult: OpCodes results as a list of lists
314
315 """
316 job_id = self.items[0]
317 result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
318 if result is None:
319 raise http.HttpNotFound()
320 return baserlib.MapFields(J_FIELDS, result)
321
323 """Cancel not-yet-started job.
324
325 """
326 job_id = self.items[0]
327 result = self.GetClient().CancelJob(job_id)
328 return result
329
332 """/2/jobs/[job_id]/wait resource.
333
334 """
335
336
337 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
338
340 """Waits for job changes.
341
342 """
343 job_id = self.items[0]
344
345 fields = self.getBodyParameter("fields")
346 prev_job_info = self.getBodyParameter("previous_job_info", None)
347 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
348
349 if not isinstance(fields, list):
350 raise http.HttpBadRequest("The 'fields' parameter should be a list")
351
352 if not (prev_job_info is None or isinstance(prev_job_info, list)):
353 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
354 " be a list")
355
356 if not (prev_log_serial is None or
357 isinstance(prev_log_serial, (int, long))):
358 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
359 " be a number")
360
361 client = self.GetClient()
362 result = client.WaitForJobChangeOnce(job_id, fields,
363 prev_job_info, prev_log_serial,
364 timeout=_WFJC_TIMEOUT)
365 if not result:
366 raise http.HttpNotFound()
367
368 if result == constants.JOB_NOTCHANGED:
369
370 return None
371
372 (job_info, log_entries) = result
373
374 return {
375 "job_info": job_info,
376 "log_entries": log_entries,
377 }
378
379
380 -class R_2_nodes(baserlib.OpcodeResource):
400
403 """/2/nodes/[node_name] resource.
404
405 """
406 GET_OPCODE = opcodes.OpNodeQuery
407
420
423 """/2/nodes/[node_name]/powercycle resource.
424
425 """
426 POST_OPCODE = opcodes.OpNodePowercycle
427
429 """Tries to powercycle a node.
430
431 """
432 return (self.request_body, {
433 "node_name": self.items[0],
434 "force": self.useForce(),
435 })
436
439 """/2/nodes/[node_name]/role resource.
440
441 """
442 PUT_OPCODE = opcodes.OpNodeSetParams
443
445 """Returns the current node role.
446
447 @return: Node role
448
449 """
450 node_name = self.items[0]
451 client = self.GetClient()
452 result = client.QueryNodes(names=[node_name], fields=["role"],
453 use_locking=self.useLocking())
454
455 return _NR_MAP[result[0][0]]
456
495
498 """/2/nodes/[node_name]/evacuate resource.
499
500 """
501 POST_OPCODE = opcodes.OpNodeEvacuate
502
504 """Evacuate all instances off a node.
505
506 """
507 return (self.request_body, {
508 "node_name": self.items[0],
509 "dry_run": self.dryRun(),
510 })
511
514 """/2/nodes/[node_name]/migrate resource.
515
516 """
517 POST_OPCODE = opcodes.OpNodeMigrate
518
520 """Migrate all primary instances from a node.
521
522 """
523 if self.queryargs:
524
525 if "live" in self.queryargs and "mode" in self.queryargs:
526 raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
527 " be passed")
528
529 if "live" in self.queryargs:
530 if self._checkIntVariable("live", default=1):
531 mode = constants.HT_MIGRATION_LIVE
532 else:
533 mode = constants.HT_MIGRATION_NONLIVE
534 else:
535 mode = self._checkStringVariable("mode", default=None)
536
537 data = {
538 "mode": mode,
539 }
540 else:
541 data = self.request_body
542
543 return (data, {
544 "node_name": self.items[0],
545 })
546
549 """/2/nodes/[node_name]/modify resource.
550
551 """
552 POST_OPCODE = opcodes.OpNodeSetParams
553
555 """Changes parameters of a node.
556
557 """
558 assert len(self.items) == 1
559
560 return (self.request_body, {
561 "node_name": self.items[0],
562 })
563
589
620
643
678
681 """/2/groups/[group_name] resource.
682
683 """
684 DELETE_OPCODE = opcodes.OpGroupRemove
685
698
708
724
727 """/2/groups/[group_name]/rename resource.
728
729 """
730 PUT_OPCODE = opcodes.OpGroupRename
731
741
759
813
846
863
866 """/2/instances/[instance_name]/reboot resource.
867
868 Implements an instance reboot.
869
870 """
871 POST_OPCODE = opcodes.OpInstanceReboot
872
874 """Reboot an instance.
875
876 The URI takes type=[hard|soft|full] and
877 ignore_secondaries=[False|True] parameters.
878
879 """
880 return ({}, {
881 "instance_name": self.items[0],
882 "reboot_type":
883 self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
884 "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
885 "dry_run": self.dryRun(),
886 })
887
890 """/2/instances/[instance_name]/startup resource.
891
892 Implements an instance startup.
893
894 """
895 PUT_OPCODE = opcodes.OpInstanceStartup
896
910
913 """/2/instances/[instance_name]/shutdown resource.
914
915 Implements an instance shutdown.
916
917 """
918 PUT_OPCODE = opcodes.OpInstanceShutdown
919
929
932 """Parses a request for reinstalling an instance.
933
934 """
935 if not isinstance(data, dict):
936 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
937
938 ostype = baserlib.CheckParameter(data, "os", default=None)
939 start = baserlib.CheckParameter(data, "start", exptype=bool,
940 default=True)
941 osparams = baserlib.CheckParameter(data, "osparams", default=None)
942
943 ops = [
944 opcodes.OpInstanceShutdown(instance_name=name),
945 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
946 osparams=osparams),
947 ]
948
949 if start:
950 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
951
952 return ops
953
956 """/2/instances/[instance_name]/reinstall resource.
957
958 Implements an instance reinstall.
959
960 """
961 POST_OPCODE = opcodes.OpInstanceReinstall
962
964 """Reinstall an instance.
965
966 The URI takes os=name and nostartup=[0|1] optional
967 parameters. By default, the instance will be started
968 automatically.
969
970 """
971 if self.request_body:
972 if self.queryargs:
973 raise http.HttpBadRequest("Can't combine query and body parameters")
974
975 body = self.request_body
976 elif self.queryargs:
977
978 body = {
979 "os": self._checkStringVariable("os"),
980 "start": not self._checkIntVariable("nostartup"),
981 }
982 else:
983 body = {}
984
985 ops = _ParseInstanceReinstallRequest(self.items[0], body)
986
987 return self.SubmitJob(ops)
988
991 """/2/instances/[instance_name]/replace-disks resource.
992
993 """
994 POST_OPCODE = opcodes.OpInstanceReplaceDisks
995
997 """Replaces disks on an instance.
998
999 """
1000 static = {
1001 "instance_name": self.items[0],
1002 }
1003
1004 if self.request_body:
1005 data = self.request_body
1006 elif self.queryargs:
1007
1008 data = {
1009 "remote_node": self._checkStringVariable("remote_node", default=None),
1010 "mode": self._checkStringVariable("mode", default=None),
1011 "disks": self._checkStringVariable("disks", default=None),
1012 "iallocator": self._checkStringVariable("iallocator", default=None),
1013 }
1014 else:
1015 data = {}
1016
1017
1018 try:
1019 raw_disks = data.pop("disks")
1020 except KeyError:
1021 pass
1022 else:
1023 if raw_disks:
1024 if ht.TListOf(ht.TInt)(raw_disks):
1025 data["disks"] = raw_disks
1026 else:
1027
1028 try:
1029 data["disks"] = [int(part) for part in raw_disks.split(",")]
1030 except (TypeError, ValueError), err:
1031 raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
1032
1033 return (data, static)
1034
1052
1067
1070 """/2/instances/[instance_name]/recreate-disks resource.
1071
1072 """
1073 POST_OPCODE = opcodes.OpInstanceRecreateDisks
1074
1076 """Recreate disks for an instance.
1077
1078 """
1079 return ({}, {
1080 "instance_name": self.items[0],
1081 })
1082
1085 """/2/instances/[instance_name]/prepare-export resource.
1086
1087 """
1088 PUT_OPCODE = opcodes.OpBackupPrepare
1089
1098
1101 """/2/instances/[instance_name]/export resource.
1102
1103 """
1104 PUT_OPCODE = opcodes.OpBackupExport
1105 PUT_RENAME = {
1106 "destination": "target_node",
1107 }
1108
1116
1131
1146
1149 """/2/instances/[instance_name]/rename resource.
1150
1151 """
1152 PUT_OPCODE = opcodes.OpInstanceRename
1153
1161
1176
1179 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1180
1181 """
1182 POST_OPCODE = opcodes.OpInstanceGrowDisk
1183
1185 """Increases the size of an instance disk.
1186
1187 """
1188 return (self.request_body, {
1189 "instance_name": self.items[0],
1190 "disk": int(self.items[1]),
1191 })
1192
1195 """/2/instances/[instance_name]/console resource.
1196
1197 """
1198 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1199 GET_OPCODE = opcodes.OpInstanceConsole
1200
1202 """Request information for connecting to instance's console.
1203
1204 @return: Serialized instance console description, see
1205 L{objects.InstanceConsole}
1206
1207 """
1208 client = self.GetClient()
1209
1210 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1211
1212 if console is None:
1213 raise http.HttpServiceUnavailable("Instance console unavailable")
1214
1215 assert isinstance(console, dict)
1216 return console
1217
1220 """
1221
1222 """
1223 try:
1224 fields = args["fields"]
1225 except KeyError:
1226 raise http.HttpBadRequest("Missing 'fields' query argument")
1227
1228 return _SplitQueryFields(fields[0])
1229
1232 """
1233
1234 """
1235 return [i.strip() for i in fields.split(",")]
1236
1237
1238 -class R_2_query(baserlib.ResourceBase):
1239 """/2/query/[resource] resource.
1240
1241 """
1242
1243 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1244 GET_OPCODE = opcodes.OpQuery
1245 PUT_OPCODE = opcodes.OpQuery
1246
1247 - def _Query(self, fields, qfilter):
1249
1251 """Returns resource information.
1252
1253 @return: Query result, see L{objects.QueryResponse}
1254
1255 """
1256 return self._Query(_GetQueryFields(self.queryargs), None)
1257
1259 """Submits job querying for resources.
1260
1261 @return: Query result, see L{objects.QueryResponse}
1262
1263 """
1264 body = self.request_body
1265
1266 baserlib.CheckType(body, dict, "Body contents")
1267
1268 try:
1269 fields = body["fields"]
1270 except KeyError:
1271 fields = _GetQueryFields(self.queryargs)
1272
1273 qfilter = body.get("qfilter", None)
1274
1275 if qfilter is None:
1276 qfilter = body.get("filter", None)
1277
1278 return self._Query(fields, qfilter)
1279
1282 """/2/query/[resource]/fields resource.
1283
1284 """
1285 GET_OPCODE = opcodes.OpQueryFields
1286
1288 """Retrieves list of available fields for a resource.
1289
1290 @return: List of serialized L{objects.QueryFieldDefinition}
1291
1292 """
1293 try:
1294 raw_fields = self.queryargs["fields"]
1295 except KeyError:
1296 fields = None
1297 else:
1298 fields = _SplitQueryFields(raw_fields[0])
1299
1300 return self.GetClient().QueryFields(self.items[0], fields).ToDict()
1301
1386
1395
1404
1413
1422