From 8c229cc7e24f1c7075b38ff0e21aab7c75299c4d Mon Sep 17 00:00:00 2001 From: Oleksiy Mishchenko <oleksiy@google.com> Date: Fri, 11 Jul 2008 09:47:51 +0000 Subject: [PATCH] Initial copy of RAPI filebase to the trunk Reviewed-by: iustinp --- Makefile.am | 13 +- daemons/ganeti-rapi | 95 ++++++ lib/rapi/RESTHTTPServer.py | 234 +++++++++++++++ lib/rapi/__init__.py | 22 ++ lib/rapi/httperror.py | 49 ++++ lib/rapi/resources.py | 576 +++++++++++++++++++++++++++++++++++++ 6 files changed, 988 insertions(+), 1 deletion(-) create mode 100755 daemons/ganeti-rapi create mode 100644 lib/rapi/RESTHTTPServer.py create mode 100644 lib/rapi/__init__.py create mode 100644 lib/rapi/httperror.py create mode 100644 lib/rapi/resources.py diff --git a/Makefile.am b/Makefile.am index e96a50b3a..d06fc2885 100644 --- a/Makefile.am +++ b/Makefile.am @@ -14,6 +14,7 @@ DOCBOOK_WRAPPER = $(top_srcdir)/autotools/docbook-wrapper REPLACE_VARS_SED = autotools/replace_vars.sed hypervisordir = $(pkgpythondir)/hypervisor +rapidir = $(pkgpythondir)/rapi toolsdir = $(pkglibdir)/tools docdir = $(datadir)/doc/$(PACKAGE) @@ -25,6 +26,7 @@ DIRS = \ doc/examples \ lib \ lib/hypervisor \ + lib/rapi \ man \ qa \ qa/hooks \ @@ -43,6 +45,7 @@ CLEANFILES = \ doc/examples/ganeti.cron \ lib/*.py[co] \ lib/hypervisor/*.py[co] \ + lib/rapi/*.py[co] \ man/*.[78] \ man/*.in \ qa/*.py[co] \ @@ -84,6 +87,13 @@ hypervisor_PYTHON = \ lib/hypervisor/hv_fake.py \ lib/hypervisor/hv_xen.py +rapi_PYTHON = \ + lib/rapi/__init__.py \ + lib/rapi/RESTHTTPServer.py \ + lib/rapi/httperror.py \ + lib/rapi/resources.py + + docsgml = \ doc/hooks.sgml \ doc/install.sgml \ @@ -99,6 +109,7 @@ dist_sbin_SCRIPTS = \ daemons/ganeti-watcher \ daemons/ganeti-master \ daemons/ganeti-masterd \ + daemons/ganeti-rapi \ scripts/gnt-backup \ scripts/gnt-cluster \ scripts/gnt-debug \ @@ -254,7 +265,7 @@ $(REPLACE_VARS_SED): Makefile stamp-directories #.PHONY: srclinks srclinks: stamp-directories set -e; \ - for i in man/footer.sgml $(pkgpython_PYTHON) $(hypervisor_PYTHON); do \ + for i in man/footer.sgml $(pkgpython_PYTHON) $(hypervisor_PYTHON) $(rapi_PYTHON); do \ if test ! -f $$i -a -f $(abs_top_srcdir)/$$i; then \ $(LN_S) $(abs_top_srcdir)/$$i $$i; \ fi; \ diff --git a/daemons/ganeti-rapi b/daemons/ganeti-rapi new file mode 100755 index 000000000..fa1d41f3e --- /dev/null +++ b/daemons/ganeti-rapi @@ -0,0 +1,95 @@ +#!/usr/bin/python +# + +# Copyright (C) 2006, 2007 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +""" Ganeti Remote API master script. +""" + +import glob +import optparse +import sys +import os + +# we need to import rpc early in order to get our custom reactor, +# instead of the default twisted one; without this, things breaks in a +# not-nice-to-debug way +from ganeti import rpc +from ganeti import constants +from ganeti import utils +from ganeti.rapi import RESTHTTPServer + + +def ParseOptions(): + """Parse the command line options. + + Returns: + (options, args) as from OptionParser.parse_args() + + """ + parser = optparse.OptionParser(description="Ganeti Remote API", + usage="%prog [-d] [-p port]", + version="%%prog (ganeti) %s" % + constants.RAPI_VERSION) + parser.add_option("-d", "--debug", dest="debug", + help="Enable some debug messages", + default=False, action="store_true") + parser.add_option("-p", "--port", dest="port", + help="Port to run API (%s default)." % + constants.RAPI_PORT, + default=constants.RAPI_PORT, type="int") + parser.add_option("-S", "--https", dest="ssl", + help="Secure HTTP protocol with SSL", + default=False, action="store_true") + parser.add_option("-K", "--ssl-key", dest="ssl_key", + help="SSL key", + default=None, type="string") + parser.add_option("-C", "--ssl-cert", dest="ssl_cert", + help="SSL certificate", + default=None, type="string") + parser.add_option("-f", "--foreground", dest="fork", + help="Don't detach from the current terminal", + default=True, action="store_false") + + options, args = parser.parse_args() + + if len(args) != 0: + print >> sys.stderr, "Usage: %s [-d] [-p port]" % sys.argv[0] + sys.exit(1) + + if options.ssl and not (options.ssl_cert and options.ssl_key): + print >> sys.stderr, ("For secure mode please provide " + "--ssl-key and --ssl-cert arguments") + sys.exit(1) + + return options, args + + +def main(): + """Main function. + + """ + options, args = ParseOptions() + if options.fork: + utils.Daemonize(logfile=constants.LOG_RAPISERVER) + RESTHTTPServer.start(options) + sys.exit(0) + + +if __name__ == '__main__': + main() diff --git a/lib/rapi/RESTHTTPServer.py b/lib/rapi/RESTHTTPServer.py new file mode 100644 index 000000000..c480ff877 --- /dev/null +++ b/lib/rapi/RESTHTTPServer.py @@ -0,0 +1,234 @@ +# +# + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + +"""RESTfull HTTPS Server module. + +""" + +import socket +import BaseHTTPServer +import OpenSSL +import time + +from ganeti import constants +from ganeti import errors +from ganeti import logger +from ganeti import rpc +from ganeti import serializer +from ganeti.rapi import resources +from ganeti.rapi import httperror + + +class HttpLogfile: + """Utility class to write HTTP server log files. + + The written format is the "Common Log Format" as defined by Apache: + http://httpd.apache.org/docs/2.2/mod/mod_log_config.html#examples + + """ + MONTHNAME = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def __init__(self, path): + self._fd = open(path, 'a', 1) + + def __del__(self): + try: + self.Close() + except: + # Swallow exceptions + pass + + def Close(self): + if self._fd is not None: + self._fd.close() + self._fd = None + + def LogRequest(self, request, format, *args): + if self._fd is None: + raise errors.ProgrammerError("Logfile already closed") + + request_time = self._FormatCurrentTime() + + self._fd.write("%s %s %s [%s] %s\n" % ( + # Remote host address + request.address_string(), + + # RFC1413 identity (identd) + "-", + + # Remote user + "-", + + # Request time + request_time, + + # Message + format % args, + )) + + def _FormatCurrentTime(self): + """Formats current time in Common Log Format. + + """ + return self._FormatLogTime(time.time()) + + def _FormatLogTime(self, seconds): + """Formats time for Common Log Format. + + All timestamps are logged in the UTC timezone. + + Args: + - seconds: Time in seconds since the epoch + + """ + (_, month, _, _, _, _, _, _, _) = tm = time.gmtime(seconds) + format = "%d/" + self.MONTHNAME[month] + "/%Y:%H:%M:%S +0000" + return time.strftime(format, tm) + + +class RESTHTTPServer(BaseHTTPServer.HTTPServer): + """Class to provide an HTTP/HTTPS server. + + """ + allow_reuse_address = True + + def __init__(self, server_address, HandlerClass, options): + """REST Server Constructor. + + Args: + server_address: a touple containing: + ip: a string with IP address, localhost if empty string + port: port number, integer + HandlerClass: HTTPRequestHandler object + options: Command-line options + + """ + logger.SetupLogging(debug=options.debug, program='ganeti-rapi') + + self.httplog = HttpLogfile(constants.LOG_RAPIACCESS) + + BaseHTTPServer.HTTPServer.__init__(self, server_address, HandlerClass) + if options.ssl: + # Set up SSL + context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) + context.use_privatekey_file(options.ssl_key) + context.use_certificate_file(options.ssl_cert) + self.socket = OpenSSL.SSL.Connection(context, + socket.socket(self.address_family, + self.socket_type)) + else: + self.socket = socket.socket(self.address_family, self.socket_type) + + self.server_bind() + self.server_activate() + + +class JsonResponse: + CONTENT_TYPE = "application/json" + + def Encode(self, data): + return serializer.DumpJson(data) + + +class RESTRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """REST Request Handler Class. + + """ + def setup(self): + """Setup secure read and write file objects. + + """ + self.connection = self.request + self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) + self.wfile = socket._fileobject(self.request, "wb", self.wbufsize) + self._resmap = resources.Mapper() + + def handle_one_request(self): + """Handle a single REST request. + + """ + self.raw_requestline = None + try: + self.raw_requestline = self.rfile.readline() + except OpenSSL.SSL.Error, ex: + logger.Error("Error in SSL: %s" % str(ex)) + if not self.raw_requestline: + self.close_connection = 1 + return + if not self.parse_request(): # An error code has been sent, just exit + return + + try: + (HandlerClass, items, args) = self._resmap.getController(self.path) + handler = HandlerClass(self, items, args) + + command = self.command.upper() + try: + fn = getattr(handler, command) + except AttributeError, err: + raise httperror.HTTPBadRequest() + + try: + result = fn() + + except errors.OpPrereqError, err: + # TODO: "Not found" is not always the correct error. Ganeti's core must + # differentiate between different error types. + raise httperror.HTTPNotFound(message=str(err)) + + encoder = JsonResponse() + encoded_result = encoder.Encode(result) + + self.send_response(200) + self.send_header("Content-Type", encoder.CONTENT_TYPE) + self.end_headers() + self.wfile.write(encoded_result) + + except httperror.HTTPException, err: + self.send_error(err.code, message=err.message) + + except Exception, err: + self.send_error(httperror.HTTPInternalError.code, message=str(err)) + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + """ + self.server.httplog.LogRequest(self, format, *args) + + +def start(options): + # Disable signal handlers, otherwise we can't exit the daemon in a clean way + # by sending a signal. + rpc.install_twisted_signal_handlers = False + + httpd = RESTHTTPServer(("", options.port), RESTRequestHandler, options) + try: + httpd.serve_forever() + finally: + httpd.server_close() diff --git a/lib/rapi/__init__.py b/lib/rapi/__init__.py new file mode 100644 index 000000000..15360b583 --- /dev/null +++ b/lib/rapi/__init__.py @@ -0,0 +1,22 @@ +# +# + +# Copyright (C) 2007, 2008 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +# empty file for package definition diff --git a/lib/rapi/httperror.py b/lib/rapi/httperror.py new file mode 100644 index 000000000..cbb8adb9a --- /dev/null +++ b/lib/rapi/httperror.py @@ -0,0 +1,49 @@ +# +# + +# Copyright (C) 2006, 2007, 2008 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""HTTP errors. + +""" + + +class HTTPException(Exception): + code = None + message = None + + def __init__(self, message=None): + if message is not None: + self.message = message + + +class HTTPBadRequest(HTTPException): + code = 400 + + +class HTTPNotFound(HTTPException): + code = 404 + + +class HTTPInternalError(HTTPException): + code = 500 + + +class HTTPServiceUnavailable(HTTPException): + code = 503 diff --git a/lib/rapi/resources.py b/lib/rapi/resources.py new file mode 100644 index 000000000..3d5ce246b --- /dev/null +++ b/lib/rapi/resources.py @@ -0,0 +1,576 @@ +# +# + +# Copyright (C) 2006, 2007, 2008 Google Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Remote API resources. + +""" + +import cgi +import re + +import ganeti.opcodes +import ganeti.errors +import ganeti.cli + +from ganeti import constants +from ganeti import utils +from ganeti.rapi import httperror + + +# Initialized at the end of this file. +_CONNECTOR = {} + + +def BuildUriList(names, uri_format): + """Builds a URI list as used by index resources. + + Args: + - names: List of names as strings + - uri_format: Format to be applied for URI + + """ + def _MapName(name): + return { "name": name, "uri": uri_format % name, } + + # Make sure the result is sorted, makes it nicer to look at and simplifies + # unittests. + names.sort() + + return map(_MapName, names) + + +def ExtractField(sequence, index): + """Creates a list containing one column out of a list of lists. + + Args: + - sequence: Sequence of lists + - index: Index of field + + """ + return map(lambda item: item[index], sequence) + + +def MapFields(names, data): + """Maps two lists into one dictionary. + + Args: + - names: Field names (list of strings) + - data: Field data (list) + + Example: + >>> MapFields(["a", "b"], ["foo", 123]) + {'a': 'foo', 'b': 123} + + """ + if len(names) != len(data): + raise AttributeError("Names and data must have the same length") + return dict([(names[i], data[i]) for i in range(len(names))]) + + +def RequireLock(name='cmd'): + """Function decorator to automatically acquire locks. + + PEP-318 style function decorator. + + """ + def wrapper(fn): + def new_f(*args, **kwargs): + try: + utils.Lock(name, max_retries=15) + try: + # Call real function + return fn(*args, **kwargs) + finally: + utils.Unlock(name) + utils.LockCleanup() + except ganeti.errors.LockError, err: + raise httperror.HTTPServiceUnavailable(message=str(err)) + + # Override function metadata + new_f.func_name = fn.func_name + new_f.func_doc = fn.func_doc + + return new_f + + return wrapper + + +def _Tags_GET(kind, name=None): + """Helper function to retrieve tags. + + """ + if name is None: + # Do not cause "missing parameter" error, which happens if a parameter + # is None. + name = "" + op = ganeti.opcodes.OpGetTags(kind=kind, name=name) + tags = ganeti.cli.SubmitOpCode(op) + return list(tags) + + +class Mapper: + """Map resource to method. + + """ + def __init__(self, connector=_CONNECTOR): + """Resource mapper constructor. + + Args: + con: a dictionary, mapping method name with URL path regexp + + """ + self._connector = connector + + def getController(self, uri): + """Find method for a given URI. + + Args: + uri: string with URI + + Returns: + None if no method is found or a tuple containing the following fields: + methd: name of method mapped to URI + items: a list of variable intems in the path + args: a dictionary with additional parameters from URL + + """ + if '?' in uri: + (path, query) = uri.split('?', 1) + args = cgi.parse_qs(query) + else: + path = uri + query = None + args = {} + + result = None + + for key, handler in self._connector.iteritems(): + # Regex objects + if hasattr(key, "match"): + m = key.match(path) + if m: + result = (handler, list(m.groups()), args) + break + + # String objects + elif key == path: + result = (handler, [], args) + break + + if result is not None: + return result + else: + raise httperror.HTTPNotFound() + + +class R_Generic(object): + """Generic class for resources. + + """ + def __init__(self, request, items, queryargs): + """Generic resource constructor. + + Args: + request: HTTPRequestHandler object + items: a list with variables encoded in the URL + queryargs: a dictionary with additional options from URL + + """ + self.request = request + self.items = items + self.queryargs = queryargs + + +class R_root(R_Generic): + """/ resource. + + """ + DOC_URI = "/" + + def GET(self): + """Show the list of mapped resources. + + Returns: + A dictionary with 'name' and 'uri' keys for each of them. + + """ + root_pattern = re.compile('^R_([a-zA-Z0-9]+)$') + + rootlist = [] + for handler in _CONNECTOR.values(): + m = root_pattern.match(handler.__name__) + if m: + name = m.group(1) + if name != 'root': + rootlist.append(name) + + return BuildUriList(rootlist, "/%s") + + +class R_version(R_Generic): + """/version resource. + + This resource should be used to determine the remote API version and to adapt + clients accordingly. + + """ + DOC_URI = "/version" + + def GET(self): + """Returns the remote API version. + + """ + return constants.RAPI_VERSION + + +class R_tags(R_Generic): + """/tags resource. + + Manages cluster tags. + + """ + DOC_URI = "/tags" + + def GET(self): + """Returns a list of all cluster tags. + + Example: ["tag1", "tag2", "tag3"] + + """ + return _Tags_GET(constants.TAG_CLUSTER) + + +class R_info(R_Generic): + """Cluster info. + + """ + DOC_URI = "/info" + + def GET(self): + """Returns cluster information. + + Example: { + "config_version": 3, + "name": "cluster1.example.com", + "software_version": "1.2.4", + "os_api_version": 5, + "export_version": 0, + "master": "node1.example.com", + "architecture": [ + "64bit", + "x86_64" + ], + "hypervisor_type": "xen-3.0", + "protocol_version": 12 + } + + """ + op = ganeti.opcodes.OpQueryClusterInfo() + return ganeti.cli.SubmitOpCode(op) + + +class R_nodes(R_Generic): + """/nodes resource. + + """ + DOC_URI = "/nodes" + + @RequireLock() + def _GetDetails(self, nodeslist): + """Returns detailed instance data for bulk output. + + Args: + instance: A list of nodes names. + + Returns: + A list of nodes properties + + """ + fields = ["name","dtotal", "dfree", + "mtotal", "mnode", "mfree", + "pinst_cnt", "sinst_cnt", "tags"] + + op = ganeti.opcodes.OpQueryNodes(output_fields=fields, + names=nodeslist) + result = ganeti.cli.SubmitOpCode(op) + + nodes_details = [] + for node in result: + mapped = MapFields(fields, node) + nodes_details.append(mapped) + return nodes_details + + def GET(self): + """Returns a list of all nodes. + + Returns: + A dictionary with 'name' and 'uri' keys for each of them. + + Example: [ + { + "name": "node1.example.com", + "uri": "\/instances\/node1.example.com" + }, + { + "name": "node2.example.com", + "uri": "\/instances\/node2.example.com" + }] + + If the optional 'bulk' argument is provided and set to 'true' + value (i.e '?bulk=1'), the output contains detailed + information about nodes as a list. Note: Lock required. + + Example: [ + { + "pinst_cnt": 1, + "mfree": 31280, + "mtotal": 32763, + "name": "www.example.com", + "tags": [], + "mnode": 512, + "dtotal": 5246208, + "sinst_cnt": 2, + "dfree": 5171712 + }, + ... + ] + + """ + op = ganeti.opcodes.OpQueryNodes(output_fields=["name"], names=[]) + nodeslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0) + + if 'bulk' in self.queryargs: + return self._GetDetails(nodeslist) + + return BuildUriList(nodeslist, "/nodes/%s") + + +class R_nodes_name(R_Generic): + """/nodes/[node_name] resources. + + """ + DOC_URI = "/nodes/[node_name]" + + @RequireLock() + def GET(self): + """Send information about a node. + + """ + node_name = self.items[0] + fields = ["name","dtotal", "dfree", + "mtotal", "mnode", "mfree", + "pinst_cnt", "sinst_cnt", "tags"] + + op = ganeti.opcodes.OpQueryNodes(output_fields=fields, + names=[node_name]) + result = ganeti.cli.SubmitOpCode(op) + + return MapFields(fields, result[0]) + + +class R_nodes_name_tags(R_Generic): + """/nodes/[node_name]/tags resource. + + Manages per-node tags. + + """ + DOC_URI = "/nodes/[node_name]/tags" + + def GET(self): + """Returns a list of node tags. + + Example: ["tag1", "tag2", "tag3"] + + """ + return _Tags_GET(constants.TAG_NODE, name=self.items[0]) + + +class R_instances(R_Generic): + """/instances resource. + + """ + DOC_URI = "/instances" + + @RequireLock() + def _GetDetails(self, instanceslist): + """Returns detailed instance data for bulk output. + + Args: + instance: A list of instances names. + + Returns: + A list with instances properties. + + """ + fields = ["name", "os", "pnode", "snodes", + "admin_state", "admin_ram", + "disk_template", "ip", "mac", "bridge", + "sda_size", "sdb_size", "vcpus", + "oper_state", "status", "tags"] + + op = ganeti.opcodes.OpQueryInstances(output_fields=fields, + names=instanceslist) + result = ganeti.cli.SubmitOpCode(op) + + instances_details = [] + for instance in result: + mapped = MapFields(fields, instance) + instances_details.append(mapped) + return instances_details + + def GET(self): + """Returns a list of all available instances. + + Returns: + A dictionary with 'name' and 'uri' keys for each of them. + + Example: [ + { + "name": "web.example.com", + "uri": "\/instances\/web.example.com" + }, + { + "name": "mail.example.com", + "uri": "\/instances\/mail.example.com" + }] + + If the optional 'bulk' argument is provided and set to 'true' + value (i.e '?bulk=1'), the output contains detailed + information about instances as a list. Note: Lock required. + + Example: [ + { + "status": "running", + "bridge": "xen-br0", + "name": "web.example.com", + "tags": ["tag1", "tag2"], + "admin_ram": 512, + "sda_size": 20480, + "pnode": "node1.example.com", + "mac": "01:23:45:67:89:01", + "sdb_size": 4096, + "snodes": ["node2.example.com"], + "disk_template": "drbd", + "ip": null, + "admin_state": true, + "os": "debian-etch", + "vcpus": 2, + "oper_state": true + }, + ... + ] + + """ + op = ganeti.opcodes.OpQueryInstances(output_fields=["name"], names=[]) + instanceslist = ExtractField(ganeti.cli.SubmitOpCode(op), 0) + + if 'bulk' in self.queryargs: + return self._GetDetails(instanceslist) + + else: + return BuildUriList(instanceslist, "/instances/%s") + + +class R_instances_name(R_Generic): + """/instances/[instance_name] resources. + + """ + DOC_URI = "/instances/[instance_name]" + + @RequireLock() + def GET(self): + """Send information about an instance. + + """ + instance_name = self.items[0] + fields = ["name", "os", "pnode", "snodes", + "admin_state", "admin_ram", + "disk_template", "ip", "mac", "bridge", + "sda_size", "sdb_size", "vcpus", + "oper_state", "status", "tags"] + + op = ganeti.opcodes.OpQueryInstances(output_fields=fields, + names=[instance_name]) + result = ganeti.cli.SubmitOpCode(op) + + return MapFields(fields, result[0]) + + +class R_instances_name_tags(R_Generic): + """/instances/[instance_name]/tags resource. + + Manages per-instance tags. + + """ + DOC_URI = "/instances/[instance_name]/tags" + + def GET(self): + """Returns a list of instance tags. + + Example: ["tag1", "tag2", "tag3"] + + """ + return _Tags_GET(constants.TAG_INSTANCE, name=self.items[0]) + + +class R_os(R_Generic): + """/os resource. + + """ + DOC_URI = "/os" + + @RequireLock() + def GET(self): + """Return a list of all OSes. + + Can return error 500 in case of a problem. + + Example: ["debian-etch"] + + """ + op = ganeti.opcodes.OpDiagnoseOS(output_fields=["name", "valid"], + names=[]) + diagnose_data = ganeti.cli.SubmitOpCode(op) + + if not isinstance(diagnose_data, list): + raise httperror.HTTPInternalError(message="Can't get OS list") + + return [row[0] for row in diagnose_data if row[1]] + + +_CONNECTOR.update({ + "/": R_root, + + "/version": R_version, + + "/tags": R_tags, + "/info": R_info, + + "/nodes": R_nodes, + re.compile(r'^/nodes/([\w\._-]+)$'): R_nodes_name, + re.compile(r'^/nodes/([\w\._-]+)/tags$'): R_nodes_name_tags, + + "/instances": R_instances, + re.compile(r'^/instances/([\w\._-]+)$'): R_instances_name, + re.compile(r'^/instances/([\w\._-]+)/tags$'): R_instances_name_tags, + + "/os": R_os, + }) -- GitLab