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