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.rapi import baserlib
66
67
68 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
69 I_FIELDS = ["name", "admin_state", "os",
70 "pnode", "snodes",
71 "disk_template",
72 "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
73 "network_port",
74 "disk.sizes", "disk_usage",
75 "beparams", "hvparams",
76 "oper_state", "oper_ram", "oper_vcpus", "status",
77 "custom_hvparams", "custom_beparams", "custom_nicparams",
78 ] + _COMMON_FIELDS
79
80 N_FIELDS = ["name", "offline", "master_candidate", "drained",
81 "dtotal", "dfree",
82 "mtotal", "mnode", "mfree",
83 "pinst_cnt", "sinst_cnt",
84 "ctotal", "cnodes", "csockets",
85 "pip", "sip", "role",
86 "pinst_list", "sinst_list",
87 "master_capable", "vm_capable",
88 "group.uuid",
89 ] + _COMMON_FIELDS
90
91 G_FIELDS = [
92 "alloc_policy",
93 "name",
94 "node_cnt",
95 "node_list",
96 ] + _COMMON_FIELDS
97
98 J_FIELDS_BULK = [
99 "id", "ops", "status", "summary",
100 "opstatus",
101 "received_ts", "start_ts", "end_ts",
102 ]
103
104 J_FIELDS = J_FIELDS_BULK + [
105 "oplog",
106 "opresult",
107 ]
108
109 _NR_DRAINED = "drained"
110 _NR_MASTER_CANDIATE = "master-candidate"
111 _NR_MASTER = "master"
112 _NR_OFFLINE = "offline"
113 _NR_REGULAR = "regular"
114
115 _NR_MAP = {
116 constants.NR_MASTER: _NR_MASTER,
117 constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
118 constants.NR_DRAINED: _NR_DRAINED,
119 constants.NR_OFFLINE: _NR_OFFLINE,
120 constants.NR_REGULAR: _NR_REGULAR,
121 }
122
123 assert frozenset(_NR_MAP.keys()) == constants.NR_ALL
124
125
126 _REQ_DATA_VERSION = "__version__"
127
128
129 _INST_CREATE_REQV1 = "instance-create-reqv1"
130
131
132 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
133
134
135 _NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
136
137
138 _NODE_EVAC_RES1 = "node-evac-res1"
139
140 ALL_FEATURES = frozenset([
141 _INST_CREATE_REQV1,
142 _INST_REINSTALL_REQV1,
143 _NODE_MIGRATE_REQV1,
144 _NODE_EVAC_RES1,
145 ])
146
147
148 _WFJC_TIMEOUT = 10
152 """/version resource.
153
154 This resource should be used to determine the remote API version and
155 to adapt clients accordingly.
156
157 """
158 @staticmethod
164
167 """/2/info resource.
168
169 """
170 @staticmethod
177
180 """/2/features resource.
181
182 """
183 @staticmethod
185 """Returns list of optional RAPI features implemented.
186
187 """
188 return list(ALL_FEATURES)
189
190
191 -class R_2_os(baserlib.R_Generic):
192 """/2/os resource.
193
194 """
195 @staticmethod
197 """Return a list of all OSes.
198
199 Can return error 500 in case of a problem.
200
201 Example: ["debian-etch"]
202
203 """
204 cl = baserlib.GetClient()
205 op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
206 job_id = baserlib.SubmitJob([op], cl)
207
208 result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
209 diagnose_data = result[0]
210
211 if not isinstance(diagnose_data, list):
212 raise http.HttpBadGateway(message="Can't get OS list")
213
214 os_names = []
215 for (name, variants) in diagnose_data:
216 os_names.extend(cli.CalculateOSNames(name, variants))
217
218 return os_names
219
222 """/2/redistribute-config resource.
223
224 """
225 @staticmethod
231
234 """/2/modify resource.
235
236 """
247
250 """/2/jobs resource.
251
252 """
268
271 """/2/jobs/[job_id] resource.
272
273 """
275 """Returns a job status.
276
277 @return: a dictionary with job parameters.
278 The result includes:
279 - id: job ID as a number
280 - status: current job status as a string
281 - ops: involved OpCodes as a list of dictionaries for each
282 opcodes in the job
283 - opstatus: OpCodes status as a list
284 - opresult: OpCodes results as a list of lists
285
286 """
287 job_id = self.items[0]
288 result = baserlib.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
289 if result is None:
290 raise http.HttpNotFound()
291 return baserlib.MapFields(J_FIELDS, result)
292
294 """Cancel not-yet-started job.
295
296 """
297 job_id = self.items[0]
298 result = baserlib.GetClient().CancelJob(job_id)
299 return result
300
303 """/2/jobs/[job_id]/wait resource.
304
305 """
306
307
308 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
309
311 """Waits for job changes.
312
313 """
314 job_id = self.items[0]
315
316 fields = self.getBodyParameter("fields")
317 prev_job_info = self.getBodyParameter("previous_job_info", None)
318 prev_log_serial = self.getBodyParameter("previous_log_serial", None)
319
320 if not isinstance(fields, list):
321 raise http.HttpBadRequest("The 'fields' parameter should be a list")
322
323 if not (prev_job_info is None or isinstance(prev_job_info, list)):
324 raise http.HttpBadRequest("The 'previous_job_info' parameter should"
325 " be a list")
326
327 if not (prev_log_serial is None or
328 isinstance(prev_log_serial, (int, long))):
329 raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
330 " be a number")
331
332 client = baserlib.GetClient()
333 result = client.WaitForJobChangeOnce(job_id, fields,
334 prev_job_info, prev_log_serial,
335 timeout=_WFJC_TIMEOUT)
336 if not result:
337 raise http.HttpNotFound()
338
339 if result == constants.JOB_NOTCHANGED:
340
341 return None
342
343 (job_info, log_entries) = result
344
345 return {
346 "job_info": job_info,
347 "log_entries": log_entries,
348 }
349
352 """/2/nodes resource.
353
354 """
369
372 """/2/nodes/[node_name] resource.
373
374 """
387
390 """ /2/nodes/[node_name]/role resource.
391
392 """
405
407 """Sets the node role.
408
409 @return: a job id
410
411 """
412 if not isinstance(self.request_body, basestring):
413 raise http.HttpBadRequest("Invalid body contents, not a string")
414
415 node_name = self.items[0]
416 role = self.request_body
417
418 if role == _NR_REGULAR:
419 candidate = False
420 offline = False
421 drained = False
422
423 elif role == _NR_MASTER_CANDIATE:
424 candidate = True
425 offline = drained = None
426
427 elif role == _NR_DRAINED:
428 drained = True
429 candidate = offline = None
430
431 elif role == _NR_OFFLINE:
432 offline = True
433 candidate = drained = None
434
435 else:
436 raise http.HttpBadRequest("Can't set '%s' role" % role)
437
438 op = opcodes.OpNodeSetParams(node_name=node_name,
439 master_candidate=candidate,
440 offline=offline,
441 drained=drained,
442 force=bool(self.useForce()))
443
444 return baserlib.SubmitJob([op])
445
461
498
524
527 """/2/nodes/[node_name]/storage/modify resource.
528
529 """
554
557 """/2/nodes/[node_name]/storage/repair resource.
558
559 """
577
580 """Parses a request for creating a node group.
581
582 @rtype: L{opcodes.OpGroupAdd}
583 @return: Group creation opcode
584
585 """
586 override = {
587 "dry_run": dry_run,
588 }
589
590 rename = {
591 "name": "group_name",
592 }
593
594 return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
595 rename=rename)
596
626
629 """/2/groups/[group_name] resource.
630
631 """
644
653
656 """Parses a request for modifying a node group.
657
658 @rtype: L{opcodes.OpGroupSetParams}
659 @return: Group modify opcode
660
661 """
662 return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
663 "group_name": name,
664 })
665
668 """/2/groups/[group_name]/modify resource.
669
670 """
682
685 """Parses a request for renaming a node group.
686
687 @type name: string
688 @param name: name of the node group to rename
689 @type data: dict
690 @param data: the body received by the rename request
691 @type dry_run: bool
692 @param dry_run: whether to perform a dry run
693
694 @rtype: L{opcodes.OpGroupRename}
695 @return: Node group rename opcode
696
697 """
698 return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
699 "group_name": name,
700 "dry_run": dry_run,
701 })
702
705 """/2/groups/[group_name]/rename resource.
706
707 """
718
721 """/2/groups/[group_name]/assign-nodes resource.
722
723 """
737
740 """Parses an instance creation request version 1.
741
742 @rtype: L{opcodes.OpInstanceCreate}
743 @return: Instance creation opcode
744
745 """
746 override = {
747 "dry_run": dry_run,
748 }
749
750 rename = {
751 "os": "os_type",
752 "name": "instance_name",
753 }
754
755 return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
756 rename=rename)
757
804
807 """/2/instances/[instance_name] resource.
808
809 """
823
832
835 """/2/instances/[instance_name]/info resource.
836
837 """
848
851 """/2/instances/[instance_name]/reboot resource.
852
853 Implements an instance reboot.
854
855 """
857 """Reboot an instance.
858
859 The URI takes type=[hard|soft|full] and
860 ignore_secondaries=[False|True] parameters.
861
862 """
863 instance_name = self.items[0]
864 reboot_type = self.queryargs.get("type",
865 [constants.INSTANCE_REBOOT_HARD])[0]
866 ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
867 op = opcodes.OpInstanceReboot(instance_name=instance_name,
868 reboot_type=reboot_type,
869 ignore_secondaries=ignore_secondaries,
870 dry_run=bool(self.dryRun()))
871
872 return baserlib.SubmitJob([op])
873
876 """/2/instances/[instance_name]/startup resource.
877
878 Implements an instance startup.
879
880 """
882 """Startup an instance.
883
884 The URI takes force=[False|True] parameter to start the instance
885 if even if secondary disks are failing.
886
887 """
888 instance_name = self.items[0]
889 force_startup = bool(self._checkIntVariable("force"))
890 no_remember = bool(self._checkIntVariable("no_remember"))
891 op = opcodes.OpInstanceStartup(instance_name=instance_name,
892 force=force_startup,
893 dry_run=bool(self.dryRun()),
894 no_remember=no_remember)
895
896 return baserlib.SubmitJob([op])
897
900 """Parses a request for an instance shutdown.
901
902 @rtype: L{opcodes.OpInstanceShutdown}
903 @return: Instance shutdown opcode
904
905 """
906 return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
907 "instance_name": name,
908 "dry_run": dry_run,
909 "no_remember": no_remember,
910 })
911
914 """/2/instances/[instance_name]/shutdown resource.
915
916 Implements an instance shutdown.
917
918 """
930
933 """Parses a request for reinstalling an instance.
934
935 """
936 if not isinstance(data, dict):
937 raise http.HttpBadRequest("Invalid body contents, not a dictionary")
938
939 ostype = baserlib.CheckParameter(data, "os", default=None)
940 start = baserlib.CheckParameter(data, "start", exptype=bool,
941 default=True)
942 osparams = baserlib.CheckParameter(data, "osparams", default=None)
943
944 ops = [
945 opcodes.OpInstanceShutdown(instance_name=name),
946 opcodes.OpInstanceReinstall(instance_name=name, os_type=ostype,
947 osparams=osparams),
948 ]
949
950 if start:
951 ops.append(opcodes.OpInstanceStartup(instance_name=name, force=False))
952
953 return ops
954
957 """/2/instances/[instance_name]/reinstall resource.
958
959 Implements an instance reinstall.
960
961 """
963 """Reinstall an instance.
964
965 The URI takes os=name and nostartup=[0|1] optional
966 parameters. By default, the instance will be started
967 automatically.
968
969 """
970 if self.request_body:
971 if self.queryargs:
972 raise http.HttpBadRequest("Can't combine query and body parameters")
973
974 body = self.request_body
975 elif self.queryargs:
976
977 body = {
978 "os": self._checkStringVariable("os"),
979 "start": not self._checkIntVariable("nostartup"),
980 }
981 else:
982 body = {}
983
984 ops = _ParseInstanceReinstallRequest(self.items[0], body)
985
986 return baserlib.SubmitJob(ops)
987
990 """Parses a request for an instance export.
991
992 @rtype: L{opcodes.OpInstanceReplaceDisks}
993 @return: Instance export opcode
994
995 """
996 override = {
997 "instance_name": name,
998 }
999
1000
1001 try:
1002 raw_disks = data.pop("disks")
1003 except KeyError:
1004 pass
1005 else:
1006 if raw_disks:
1007 if ht.TListOf(ht.TInt)(raw_disks):
1008 data["disks"] = raw_disks
1009 else:
1010
1011 try:
1012 data["disks"] = [int(part) for part in raw_disks.split(",")]
1013 except (TypeError, ValueError), err:
1014 raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1015
1016 return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
1017
1043
1046 """/2/instances/[instance_name]/activate-disks resource.
1047
1048 """
1050 """Activate disks for an instance.
1051
1052 The URI might contain ignore_size to ignore current recorded size.
1053
1054 """
1055 instance_name = self.items[0]
1056 ignore_size = bool(self._checkIntVariable("ignore_size"))
1057
1058 op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
1059 ignore_size=ignore_size)
1060
1061 return baserlib.SubmitJob([op])
1062
1065 """/2/instances/[instance_name]/deactivate-disks resource.
1066
1067 """
1077
1080 """/2/instances/[instance_name]/prepare-export resource.
1081
1082 """
1096
1099 """Parses a request for an instance export.
1100
1101 @rtype: L{opcodes.OpBackupExport}
1102 @return: Instance export opcode
1103
1104 """
1105
1106 try:
1107 data["target_node"] = data.pop("destination")
1108 except KeyError:
1109 pass
1110
1111 return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
1112 "instance_name": name,
1113 })
1114
1117 """/2/instances/[instance_name]/export resource.
1118
1119 """
1132
1135 """Parses a request for an instance migration.
1136
1137 @rtype: L{opcodes.OpInstanceMigrate}
1138 @return: Instance migration opcode
1139
1140 """
1141 return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
1142 "instance_name": name,
1143 })
1144
1147 """/2/instances/[instance_name]/migrate resource.
1148
1149 """
1161
1164 """/2/instances/[instance_name]/failover resource.
1165
1166 """
1180
1183 """Parses a request for renaming an instance.
1184
1185 @rtype: L{opcodes.OpInstanceRename}
1186 @return: Instance rename opcode
1187
1188 """
1189 return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
1190 "instance_name": name,
1191 })
1192
1195 """/2/instances/[instance_name]/rename resource.
1196
1197 """
1209
1212 """Parses a request for modifying an instance.
1213
1214 @rtype: L{opcodes.OpInstanceSetParams}
1215 @return: Instance modify opcode
1216
1217 """
1218 return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
1219 "instance_name": name,
1220 })
1221
1224 """/2/instances/[instance_name]/modify resource.
1225
1226 """
1238
1241 """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1242
1243 """
1245 """Increases the size of an instance disk.
1246
1247 @return: a job id
1248
1249 """
1250 op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
1251 "instance_name": self.items[0],
1252 "disk": int(self.items[1]),
1253 })
1254
1255 return baserlib.SubmitJob([op])
1256
1259 """/2/instances/[instance_name]/console resource.
1260
1261 """
1262 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1263
1265 """Request information for connecting to instance's console.
1266
1267 @return: Serialized instance console description, see
1268 L{objects.InstanceConsole}
1269
1270 """
1271 client = baserlib.GetClient()
1272
1273 ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
1274
1275 if console is None:
1276 raise http.HttpServiceUnavailable("Instance console unavailable")
1277
1278 assert isinstance(console, dict)
1279 return console
1280
1283 """
1284
1285 """
1286 try:
1287 fields = args["fields"]
1288 except KeyError:
1289 raise http.HttpBadRequest("Missing 'fields' query argument")
1290
1291 return _SplitQueryFields(fields[0])
1292
1295 """
1296
1297 """
1298 return [i.strip() for i in fields.split(",")]
1299
1302 """/2/query/[resource] resource.
1303
1304 """
1305
1306 GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
1307
1308 - def _Query(self, fields, filter_):
1310
1312 """Returns resource information.
1313
1314 @return: Query result, see L{objects.QueryResponse}
1315
1316 """
1317 return self._Query(_GetQueryFields(self.queryargs), None)
1318
1320 """Submits job querying for resources.
1321
1322 @return: Query result, see L{objects.QueryResponse}
1323
1324 """
1325 body = self.request_body
1326
1327 baserlib.CheckType(body, dict, "Body contents")
1328
1329 try:
1330 fields = body["fields"]
1331 except KeyError:
1332 fields = _GetQueryFields(self.queryargs)
1333
1334 return self._Query(fields, self.request_body.get("filter", None))
1335
1338 """/2/query/[resource]/fields resource.
1339
1340 """
1342 """Retrieves list of available fields for a resource.
1343
1344 @return: List of serialized L{objects.QueryFieldDefinition}
1345
1346 """
1347 try:
1348 raw_fields = self.queryargs["fields"]
1349 except KeyError:
1350 fields = None
1351 else:
1352 fields = _SplitQueryFields(raw_fields[0])
1353
1354 return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
1355
1420
1429
1438
1447
1456