RAPI: fixes related to write mode
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21
22 """Remote API version 2 baserlib.library.
23
24 """
25
26 import ganeti.opcodes
27 from ganeti import http
28 from ganeti import luxi
29 from ganeti import constants
30 from ganeti.rapi import baserlib
31
32
33 I_FIELDS = ["name", "admin_state", "os",
34             "pnode", "snodes",
35             "disk_template",
36             "nic.ips", "nic.macs", "nic.bridges",
37             "disk.sizes", "disk_usage",
38             "beparams", "hvparams",
39             "oper_state", "oper_ram", "status",
40             "tags"]
41
42 N_FIELDS = ["name", "offline", "master_candidate", "drained",
43             "dtotal", "dfree",
44             "mtotal", "mnode", "mfree",
45             "pinst_cnt", "sinst_cnt", "tags",
46             "ctotal", "cnodes", "csockets",
47             ]
48
49
50 class R_version(baserlib.R_Generic):
51   """/version resource.
52
53   This resource should be used to determine the remote API version and
54   to adapt clients accordingly.
55
56   """
57   DOC_URI = "/version"
58
59   def GET(self):
60     """Returns the remote API version.
61
62     """
63     return constants.RAPI_VERSION
64
65
66 class R_2_info(baserlib.R_Generic):
67   """Cluster info.
68
69   """
70   DOC_URI = "/2/info"
71
72   def GET(self):
73     """Returns cluster information.
74
75     Example::
76
77       {
78         "config_version": 3,
79         "name": "cluster1.example.com",
80         "software_version": "1.2.4",
81         "os_api_version": 5,
82         "export_version": 0,
83         "master": "node1.example.com",
84         "architecture": [
85           "64bit",
86           "x86_64"
87         ],
88         "hypervisor_type": "xen-pvm",
89         "protocol_version": 12
90       }
91
92     """
93     client = luxi.Client()
94     return client.QueryClusterInfo()
95
96
97 class R_2_os(baserlib.R_Generic):
98   """/2/os resource.
99
100   """
101   DOC_URI = "/2/os"
102
103   def GET(self):
104     """Return a list of all OSes.
105
106     Can return error 500 in case of a problem.
107
108     Example: ["debian-etch"]
109
110     """
111     op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"],
112                                      names=[])
113     diagnose_data = ganeti.cli.SubmitOpCode(op)
114
115     if not isinstance(diagnose_data, list):
116       raise http.HttpInternalServerError(message="Can't get OS list")
117
118     return [row[0] for row in diagnose_data if row[1]]
119
120
121 class R_2_jobs(baserlib.R_Generic):
122   """/2/jobs resource.
123
124   """
125   DOC_URI = "/2/jobs"
126
127   def GET(self):
128     """Returns a dictionary of jobs.
129
130     @return: a dictionary with jobs id and uri.
131
132     """
133     fields = ["id"]
134     # Convert the list of lists to the list of ids
135     result = [job_id for [job_id] in luxi.Client().QueryJobs(None, fields)]
136     return baserlib.BuildUriList(result, "/2/jobs/%s",
137                                  uri_fields=("id", "uri"))
138
139
140 class R_2_jobs_id(baserlib.R_Generic):
141   """/2/jobs/[job_id] resource.
142
143   """
144   DOC_URI = "/2/jobs/[job_id]"
145
146   def GET(self):
147     """Returns a job status.
148
149     @return: a dictionary with job parameters.
150         The result includes:
151             - id: job ID as a number
152             - status: current job status as a string
153             - ops: involved OpCodes as a list of dictionaries for each
154               opcodes in the job
155             - opstatus: OpCodes status as a list
156             - opresult: OpCodes results as a list of lists
157
158     """
159     fields = ["id", "ops", "status", "summary",
160               "opstatus", "opresult", "oplog",
161               "received_ts", "start_ts", "end_ts",
162               ]
163     job_id = self.items[0]
164     result = luxi.Client().QueryJobs([job_id, ], fields)[0]
165     if result is None:
166       raise http.HttpNotFound()
167     return baserlib.MapFields(fields, result)
168
169   def DELETE(self):
170     """Cancel not-yet-started job.
171
172     """
173     job_id = self.items[0]
174     result = luxi.Client().CancelJob(job_id)
175     return result
176
177
178 class R_2_nodes(baserlib.R_Generic):
179   """/2/nodes resource.
180
181   """
182   DOC_URI = "/2/nodes"
183
184   def GET(self):
185     """Returns a list of all nodes.
186
187     Example::
188
189       [
190         {
191           "id": "node1.example.com",
192           "uri": "\/instances\/node1.example.com"
193         },
194         {
195           "id": "node2.example.com",
196           "uri": "\/instances\/node2.example.com"
197         }
198       ]
199
200     If the optional 'bulk' argument is provided and set to 'true'
201     value (i.e '?bulk=1'), the output contains detailed
202     information about nodes as a list.
203
204     Example::
205
206       [
207         {
208           "pinst_cnt": 1,
209           "mfree": 31280,
210           "mtotal": 32763,
211           "name": "www.example.com",
212           "tags": [],
213           "mnode": 512,
214           "dtotal": 5246208,
215           "sinst_cnt": 2,
216           "dfree": 5171712,
217           "offline": false
218         },
219         ...
220       ]
221
222     @return: a dictionary with 'name' and 'uri' keys for each of them
223
224     """
225     client = luxi.Client()
226
227     if self.useBulk():
228       bulkdata = client.QueryNodes([], N_FIELDS, False)
229       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
230     else:
231       nodesdata = client.QueryNodes([], ["name"], False)
232       nodeslist = [row[0] for row in nodesdata]
233       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
234                                    uri_fields=("id", "uri"))
235
236
237 class R_2_nodes_name(baserlib.R_Generic):
238   """/2/nodes/[node_name] resources.
239
240   """
241   DOC_URI = "/nodes/[node_name]"
242
243   def GET(self):
244     """Send information about a node.
245
246     """
247     node_name = self.items[0]
248     client = luxi.Client()
249     result = client.QueryNodes(names=[node_name], fields=N_FIELDS,
250                                use_locking=self.useLocking())
251
252     return baserlib.MapFields(N_FIELDS, result[0])
253
254
255 class R_2_instances(baserlib.R_Generic):
256   """/2/instances resource.
257
258   """
259   DOC_URI = "/2/instances"
260
261   def GET(self):
262     """Returns a list of all available instances.
263
264
265     Example::
266
267       [
268         {
269           "name": "web.example.com",
270           "uri": "\/instances\/web.example.com"
271         },
272         {
273           "name": "mail.example.com",
274           "uri": "\/instances\/mail.example.com"
275         }
276       ]
277
278     If the optional 'bulk' argument is provided and set to 'true'
279     value (i.e '?bulk=1'), the output contains detailed
280     information about instances as a list.
281
282     Example::
283
284       [
285         {
286            "status": "running",
287            "bridge": "xen-br0",
288            "name": "web.example.com",
289            "tags": ["tag1", "tag2"],
290            "admin_ram": 512,
291            "sda_size": 20480,
292            "pnode": "node1.example.com",
293            "mac": "01:23:45:67:89:01",
294            "sdb_size": 4096,
295            "snodes": ["node2.example.com"],
296            "disk_template": "drbd",
297            "ip": null,
298            "admin_state": true,
299            "os": "debian-etch",
300            "vcpus": 2,
301            "oper_state": true
302         },
303         ...
304       ]
305
306     @returns: a dictionary with 'name' and 'uri' keys for each of them.
307
308     """
309     client = luxi.Client()
310
311     use_locking = self.useLocking()
312     if self.useBulk():
313       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
314       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
315     else:
316       instancesdata = client.QueryInstances([], ["name"], use_locking)
317       instanceslist = [row[0] for row in instancesdata]
318       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
319                                    uri_fields=("id", "uri"))
320
321   def POST(self):
322     """Create an instance.
323
324     @returns: a job id
325
326     """
327     if not isinstance(self.req.request_body, dict):
328       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
329
330     beparams = baserlib.MakeParamsDict(self.req.request_body,
331                                        constants.BES_PARAMETERS)
332     hvparams = baserlib.MakeParamsDict(self.req.request_body,
333                                        constants.HVS_PARAMETERS)
334     fn = self.getBodyParameter
335
336     # disk processing
337     disk_data = fn('disks')
338     if not isinstance(disk_data, list):
339       raise http.HttpBadRequest("The 'disks' parameter should be a list")
340     disks = []
341     for idx, d in enumerate(disk_data):
342       if not isinstance(d, int):
343         raise http.HttpBadRequest("Disk %d specification wrong: should"
344                                   " be an integer")
345       disks.append({"size": d})
346     # nic processing (one nic only)
347     nics = [{"mac": fn("mac", constants.VALUE_AUTO),
348              "ip": fn("ip", None),
349              "bridge": fn("bridge", None)}]
350
351     op = ganeti.opcodes.OpCreateInstance(
352         mode=constants.INSTANCE_CREATE,
353         instance_name=fn('name'),
354         disks=disks,
355         disk_template=fn('disk_template'),
356         os_type=fn('os'),
357         pnode=fn('pnode', None),
358         snode=fn('snode', None),
359         iallocator=fn('iallocator', None),
360         nics=nics,
361         start=fn('start', True),
362         ip_check=fn('ip_check', True),
363         wait_for_sync=True,
364         hypervisor=fn('hypervisor', None),
365         hvparams=hvparams,
366         beparams=beparams,
367         file_storage_dir=fn('file_storage_dir', None),
368         file_driver=fn('file_driver', 'loop'),
369         )
370
371     job_id = ganeti.cli.SendJob([op])
372     return job_id
373
374
375 class R_2_instances_name(baserlib.R_Generic):
376   """/2/instances/[instance_name] resources.
377
378   """
379   DOC_URI = "/2/instances/[instance_name]"
380
381   def GET(self):
382     """Send information about an instance.
383
384     """
385     client = luxi.Client()
386     instance_name = self.items[0]
387     result = client.QueryInstances(names=[instance_name], fields=I_FIELDS,
388                                    use_locking=self.useLocking())
389
390     return baserlib.MapFields(I_FIELDS, result[0])
391
392   def DELETE(self):
393     """Delete an instance.
394
395     """
396     op = ganeti.opcodes.OpRemoveInstance(instance_name=self.items[0],
397                                          ignore_failures=False)
398     job_id = ganeti.cli.SendJob([op])
399     return job_id
400
401
402 class R_2_instances_name_reboot(baserlib.R_Generic):
403   """/2/instances/[instance_name]/reboot resource.
404
405   Implements an instance reboot.
406
407   """
408
409   DOC_URI = "/2/instances/[instance_name]/reboot"
410
411   def POST(self):
412     """Reboot an instance.
413
414     The URI takes type=[hard|soft|full] and
415     ignore_secondaries=[False|True] parameters.
416
417     """
418     instance_name = self.items[0]
419     reboot_type = self.queryargs.get('type',
420                                      [constants.INSTANCE_REBOOT_HARD])[0]
421     ignore_secondaries = bool(self.queryargs.get('ignore_secondaries',
422                                                  [False])[0])
423     op = ganeti.opcodes.OpRebootInstance(
424         instance_name=instance_name,
425         reboot_type=reboot_type,
426         ignore_secondaries=ignore_secondaries)
427
428     job_id = ganeti.cli.SendJob([op])
429
430     return job_id
431
432
433 class R_2_instances_name_startup(baserlib.R_Generic):
434   """/2/instances/[instance_name]/startup resource.
435
436   Implements an instance startup.
437
438   """
439
440   DOC_URI = "/2/instances/[instance_name]/startup"
441
442   def PUT(self):
443     """Startup an instance.
444
445     The URI takes force=[False|True] parameter to start the instance
446     if even if secondary disks are failing.
447
448     """
449     instance_name = self.items[0]
450     force_startup = bool(self.queryargs.get('force', [False])[0])
451     op = ganeti.opcodes.OpStartupInstance(instance_name=instance_name,
452                                           force=force_startup)
453
454     job_id = ganeti.cli.SendJob([op])
455
456     return job_id
457
458
459 class R_2_instances_name_shutdown(baserlib.R_Generic):
460   """/2/instances/[instance_name]/shutdown resource.
461
462   Implements an instance shutdown.
463
464   """
465
466   DOC_URI = "/2/instances/[instance_name]/shutdown"
467
468   def PUT(self):
469     """Shutdown an instance.
470
471     """
472     instance_name = self.items[0]
473     op = ganeti.opcodes.OpShutdownInstance(instance_name=instance_name)
474
475     job_id = ganeti.cli.SendJob([op])
476
477     return job_id
478
479
480 class _R_Tags(baserlib.R_Generic):
481   """ Quasiclass for tagging resources
482
483   Manages tags. Inheriting this class you suppose to define DOC_URI and
484   TAG_LEVEL for it.
485
486   """
487   TAG_LEVEL = None
488
489   def __init__(self, items, queryargs, req):
490     """A tag resource constructor.
491
492     We have to override the default to sort out cluster naming case.
493
494     """
495     baserlib.R_Generic.__init__(self, items, queryargs, req)
496
497     if self.TAG_LEVEL != constants.TAG_CLUSTER:
498       self.name = items[0]
499     else:
500       self.name = ""
501
502   def GET(self):
503     """Returns a list of tags.
504
505     Example: ["tag1", "tag2", "tag3"]
506
507     """
508     return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
509
510   def PUT(self):
511     """Add a set of tags.
512
513     The request as a list of strings should be PUT to this URI. And
514     you'll have back a job id.
515
516     """
517     if 'tag' not in self.queryargs:
518       raise http.HttpBadRequest("Please specify tag(s) to add using the"
519                                 " the 'tag' parameter")
520     return baserlib._Tags_PUT(self.TAG_LEVEL,
521                               self.queryargs['tag'], name=self.name)
522
523   def DELETE(self):
524     """Delete a tag.
525
526     In order to delete a set of tags, the DELETE
527     request should be addressed to URI like:
528     /tags?tag=[tag]&tag=[tag]
529
530     """
531     if 'tag' not in self.queryargs:
532       # no we not gonna delete all tags
533       raise http.HttpBadRequest("Cannot delete all tags - please specify"
534                                 " tag(s) using the 'tag' parameter")
535     return baserlib._Tags_DELETE(self.TAG_LEVEL,
536                                  self.queryargs['tag'],
537                                  name=self.name)
538
539
540 class R_2_instances_name_tags(_R_Tags):
541   """ /2/instances/[instance_name]/tags resource.
542
543   Manages per-instance tags.
544
545   """
546   DOC_URI = "/2/instances/[instance_name]/tags"
547   TAG_LEVEL = constants.TAG_INSTANCE
548
549
550 class R_2_nodes_name_tags(_R_Tags):
551   """ /2/nodes/[node_name]/tags resource.
552
553   Manages per-node tags.
554
555   """
556   DOC_URI = "/2/nodes/[node_name]/tags"
557   TAG_LEVEL = constants.TAG_NODE
558
559
560 class R_2_tags(_R_Tags):
561   """ /2/instances/tags resource.
562
563   Manages cluster tags.
564
565   """
566   DOC_URI = "/2/tags"
567   TAG_LEVEL = constants.TAG_CLUSTER