summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.json8
-rw-r--r--doc/api_samples/all_extensions/extensions-get-resp.xml3
-rw-r--r--etc/nova/policy.json1
-rw-r--r--nova/api/openstack/compute/contrib/coverage_ext.py236
-rw-r--r--nova/tests/api/openstack/compute/contrib/test_coverage_ext.py190
-rw-r--r--nova/tests/fake_policy.py1
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl8
-rw-r--r--nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl3
8 files changed, 450 insertions, 0 deletions
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.json b/doc/api_samples/all_extensions/extensions-get-resp.json
index 399d937a7..e44d1a117 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.json
+++ b/doc/api_samples/all_extensions/extensions-get-resp.json
@@ -129,6 +129,14 @@
"updated": "2011-12-23T00:00:00+00:00"
},
{
+ "alias": "os-coverage",
+ "description": "Enable Nova Coverage",
+ "links": [],
+ "name": "Coverage",
+ "namespace": "http://docs.openstack.org/compute/ext/coverage/api/v2",
+ "updated": "2012-10-15T00:00:00+00:00"
+ },
+ {
"alias": "os-create-server-ext",
"description": "Extended support to the Create Server v1.1 API",
"links": [],
diff --git a/doc/api_samples/all_extensions/extensions-get-resp.xml b/doc/api_samples/all_extensions/extensions-get-resp.xml
index e4d3b8cc3..b8f4c8986 100644
--- a/doc/api_samples/all_extensions/extensions-get-resp.xml
+++ b/doc/api_samples/all_extensions/extensions-get-resp.xml
@@ -60,6 +60,9 @@
<extension alias="os-consoles" updated="2011-12-23T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/os-consoles/api/v2" name="Consoles">
<description>Interactive Console support.</description>
</extension>
+ <extension alias="os-coverage" updated="2012-10-15T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/coverage/api/v2" name="Coverage">
+ <description>Enable Nova Coverage</description>
+ </extension>
<extension alias="os-create-server-ext" updated="2011-07-19T00:00:00+00:00" namespace="http://docs.openstack.org/compute/ext/createserverext/api/v1.1" name="Createserverext">
<description>Extended support to the Create Server v1.1 API</description>
</extension>
diff --git a/etc/nova/policy.json b/etc/nova/policy.json
index 778203e75..aad8296a2 100644
--- a/etc/nova/policy.json
+++ b/etc/nova/policy.json
@@ -33,6 +33,7 @@
"compute_extension:cloudpipe_update": "rule:admin_api",
"compute_extension:console_output": "",
"compute_extension:consoles": "",
+ "compute_extension:coverage_ext": "rule:admin_api",
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
diff --git a/nova/api/openstack/compute/contrib/coverage_ext.py b/nova/api/openstack/compute/contrib/coverage_ext.py
new file mode 100644
index 000000000..954eddf4c
--- /dev/null
+++ b/nova/api/openstack/compute/contrib/coverage_ext.py
@@ -0,0 +1,236 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 IBM
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License
+
+# See: http://wiki.openstack.org/Nova/CoverageExtension for more information
+# and usage explanation for this API extension
+
+import os
+import re
+import sys
+import telnetlib
+import tempfile
+import time
+
+from coverage import coverage
+from webob import exc
+
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import wsgi
+from nova.compute import api as compute_api
+from nova import db
+from nova.network import api as network_api
+from nova.openstack.common import log as logging
+from nova import utils
+
+
+LOG = logging.getLogger(__name__)
+authorize = extensions.extension_authorizer('compute', 'coverage_ext')
+
+
+class CoverageController(object):
+ """The Coverage report API controller for the OpenStack API"""
+ def __init__(self):
+ self.data_path = tempfile.mkdtemp(prefix='nova-coverage_')
+ data_out = os.path.join(self.data_path, '.nova-coverage')
+ self.coverInst = coverage(data_file=data_out)
+ self.compute_api = compute_api.API()
+ self.network_api = network_api.API()
+ self.services = []
+ self.combine = False
+ super(CoverageController, self).__init__()
+
+ def _find_services(self, req):
+ """Returns a list of services"""
+ context = req.environ['nova.context']
+ services = db.service_get_all(context, False)
+ hosts = []
+ for serv in services:
+ hosts.append({"service": serv["topic"], "host": serv["host"]})
+ return hosts
+
+ def _find_ports(self, req, hosts):
+ """Return a list of backdoor ports for all services in the list"""
+ context = req.environ['nova.context']
+
+ apicommands = {
+ "compute": self.compute_api.get_backdoor_port,
+ "network": self.network_api.get_backdoor_port,
+ }
+ ports = []
+ temp = {}
+ #TODO(mtreinish): Figure out how to bind the backdoor socket to 0.0.0.0
+ # Currently this will only work if the host is resolved as loopback on
+ # the same host as api-server
+ for host in hosts:
+ if host['service'] in apicommands:
+ get_port_fn = apicommands[host['service']]
+ _host = host
+ _host['port'] = get_port_fn(context, host['host'])
+ ports.append(_host)
+ else:
+ LOG.debug(_("No backdoor API command for service: %s\n"), host)
+ return ports
+
+ def _start_coverage_telnet(self, tn, service):
+ tn.write('import sys\n')
+ tn.write('from coverage import coverage\n')
+ if self.combine:
+ data_file = os.path.join(self.data_path,
+ '.nova-coverage.%s' % str(service))
+ tn.write("coverInst = coverage(data_file='%s')\n)" % data_file)
+ else:
+ tn.write('coverInst = coverage()\n')
+ tn.write('coverInst.skipModules = sys.modules.keys()\n')
+ tn.write("coverInst.start()\n")
+ tn.write("print 'finished'\n")
+ tn.expect([re.compile('finished')])
+
+ def _start_coverage(self, req, body):
+ '''Begin recording coverage information.'''
+ LOG.debug("Coverage begin")
+ body = body['start']
+ self.combine = False
+ if 'combine' in body.keys():
+ self.combine = bool(body['combine'])
+ self.coverInst.skipModules = sys.modules.keys()
+ self.coverInst.start()
+ hosts = self._find_services(req)
+ ports = self._find_ports(req, hosts)
+ self.services = []
+ for service in ports:
+ service['telnet'] = telnetlib.Telnet(service['host'],
+ service['port'])
+ self.services.append(service)
+ self._start_coverage_telnet(service['telnet'], service['service'])
+
+ def _stop_coverage_telnet(self, tn):
+ tn.write("coverInst.stop()\n")
+ tn.write("coverInst.save()\n")
+ tn.write("print 'finished'\n")
+ tn.expect([re.compile('finished')])
+
+ def _check_coverage(self):
+ try:
+ self.coverInst.stop()
+ self.coverInst.save()
+ except AssertionError:
+ return True
+ return False
+
+ def _stop_coverage(self, req):
+ for service in self.services:
+ self._stop_coverage_telnet(service['telnet'])
+ if self._check_coverage():
+ msg = ("Coverage not running")
+ raise exc.HTTPNotFound(explanation=msg)
+
+ def _report_coverage_telnet(self, tn, path, xml=False):
+ if xml:
+ execute = str("coverInst.xml_report(outfile='%s')\n" % path)
+ tn.write(execute)
+ tn.write("print 'finished'\n")
+ tn.expect([re.compile('finished')])
+ else:
+ execute = str("output = open('%s', 'w')\n" % path)
+ tn.write(execute)
+ tn.write("coverInst.report(file=output)\n")
+ tn.write("output.close()\n")
+ tn.write("print 'finished'\n")
+ tn.expect([re.compile('finished')])
+ tn.close()
+
+ def _report_coverage(self, req, body):
+ self._stop_coverage(req)
+ xml = False
+ path = None
+
+ body = body['report']
+ if 'file' in body.keys():
+ path = body['file']
+ if path != os.path.basename(path):
+ msg = ("Invalid path")
+ raise exc.HTTPBadRequest(explanation=msg)
+ path = os.path.join(self.data_path, path)
+ else:
+ msg = ("No path given for report file")
+ raise exc.HTTPBadRequest(explanation=msg)
+
+ if 'xml' in body.keys():
+ xml = body['xml']
+
+ if self.combine:
+ self.coverInst.combine()
+ if xml:
+ self.coverInst.xml_report(outfile=path)
+ else:
+ output = open(path, 'w')
+ self.coverInst.report(file=output)
+ output.close()
+ for service in self.services:
+ service['telnet'].close()
+ else:
+ if xml:
+ apipath = path + '.api'
+ self.coverInst.xml_report(outfile=apipath)
+ for service in self.services:
+ self._report_coverage_telnet(service['telnet'],
+ path + '.%s'
+ % service['service'],
+ xml=True)
+ else:
+ output = open(path + '.api', 'w')
+ self.coverInst.report(file=output)
+ for service in self.services:
+ self._report_coverage_telnet(service['telnet'],
+ path + '.%s' % service['service'])
+ output.close()
+ return {'path': path}
+
+ def action(self, req, body):
+ _actions = {
+ 'start': self._start_coverage,
+ 'stop': self._stop_coverage,
+ 'report': self._report_coverage,
+ }
+ authorize(req.environ['nova.context'])
+ for action, data in body.iteritems():
+ if action == 'stop':
+ return _actions[action](req)
+ elif action == 'report' or action == 'start':
+ return _actions[action](req, body)
+ else:
+ msg = _("Coverage doesn't have %s action") % action
+ raise exc.HTTPBadRequest(explanation=msg)
+ raise exc.HTTPBadRequest(explanation=_("Invalid request body"))
+
+
+class Coverage_ext(extensions.ExtensionDescriptor):
+ """Enable Nova Coverage"""
+
+ name = "Coverage"
+ alias = "os-coverage"
+ namespace = ("http://docs.openstack.org/compute/ext/"
+ "coverage/api/v2")
+ updated = "2012-10-15T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+ res = extensions.ResourceExtension('os-coverage',
+ controller=CoverageController(),
+ collection_actions={"action": "POST"})
+ resources.append(res)
+ return resources
diff --git a/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py b/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py
new file mode 100644
index 000000000..e7d00e68a
--- /dev/null
+++ b/nova/tests/api/openstack/compute/contrib/test_coverage_ext.py
@@ -0,0 +1,190 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2012 IBM
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License
+
+import os.path
+import telnetlib
+
+from coverage import coverage
+import webob
+
+from nova.api.openstack.compute.contrib import coverage_ext
+from nova import context
+from nova import exception
+from nova.openstack.common import jsonutils
+from nova import test
+from nova.tests.api.openstack import fakes
+
+
+def fake_telnet(self, data):
+ return
+
+
+def fake_check_coverage(self):
+ return False
+
+
+def fake_xml_report(self, outfile):
+ return
+
+
+def fake_report(self, file):
+ return
+
+
+class CoverageExtensionTest(test.TestCase):
+
+ def setUp(self):
+ super(CoverageExtensionTest, self).setUp()
+ self.stubs.Set(telnetlib.Telnet, 'write', fake_telnet)
+ self.stubs.Set(telnetlib.Telnet, 'expect', fake_telnet)
+ self.stubs.Set(coverage, 'report', fake_report)
+ self.stubs.Set(coverage, 'xml_report', fake_xml_report)
+ self.admin_context = context.RequestContext('fakeadmin_0',
+ 'fake',
+ is_admin=True)
+ self.user_context = context.RequestContext('fakeadmin_0',
+ 'fake',
+ is_admin=False)
+
+ def test_not_admin(self):
+ body = {'start': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.user_context))
+ self.assertEqual(res.status_int, 403)
+
+ def test_start_coverage_action(self):
+ body = {'start': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 200)
+
+ def test_stop_coverage_action(self):
+ self.stubs.Set(coverage_ext.CoverageController,
+ '_check_coverage', fake_check_coverage)
+ body = {'stop': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 200)
+
+ def test_report_coverage_action_file(self):
+ self.stubs.Set(coverage_ext.CoverageController,
+ '_check_coverage', fake_check_coverage)
+ self.test_start_coverage_action()
+ body = {
+ 'report': {
+ 'file': 'coverage-unit-test.report',
+ },
+ }
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 200)
+ resp_dict = jsonutils.loads(res.body)
+ self.assertTrue('path' in resp_dict)
+ self.assertTrue('coverage-unit-test.report' in resp_dict['path'])
+
+ def test_report_coverage_action_xml_file(self):
+ self.stubs.Set(coverage_ext.CoverageController,
+ '_check_coverage', fake_check_coverage)
+ body = {
+ 'report': {
+ 'file': 'coverage-xml-unit-test.report',
+ 'xml': 'True',
+ },
+ }
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 200)
+ resp_dict = jsonutils.loads(res.body)
+ self.assertTrue('path' in resp_dict)
+ self.assertTrue('coverage-xml-unit-test.report' in resp_dict['path'])
+
+ def test_report_coverage_action_nofile(self):
+ self.stubs.Set(coverage_ext.CoverageController,
+ '_check_coverage', fake_check_coverage)
+ body = {'report': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 400)
+
+ def test_coverage_bad_body(self):
+ body = {}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 400)
+
+ def test_coverage_report_bad_path(self):
+ self.stubs.Set(coverage_ext.CoverageController,
+ '_check_coverage', fake_check_coverage)
+ body = {
+ 'report': {
+ 'file': '/tmp/coverage-xml-unit-test.report',
+ }
+ }
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 400)
+
+ def test_stop_coverage_action_nostart(self):
+ body = {'stop': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 404)
+
+ def test_report_coverage_action_nostart(self):
+ body = {'stop': {}}
+ req = webob.Request.blank('/v2/fake/os-coverage/action')
+ req.method = "POST"
+ req.body = jsonutils.dumps(body)
+ req.headers["content-type"] = "application/json"
+ res = req.get_response(fakes.wsgi_app(
+ fake_auth_context=self.admin_context))
+ self.assertEqual(res.status_int, 404)
diff --git a/nova/tests/fake_policy.py b/nova/tests/fake_policy.py
index b3ae0fa17..ae0383fe0 100644
--- a/nova/tests/fake_policy.py
+++ b/nova/tests/fake_policy.py
@@ -109,6 +109,7 @@ policy_data = """
"compute_extension:config_drive": "",
"compute_extension:console_output": "",
"compute_extension:consoles": "",
+ "compute_extension:coverage_ext": "is_admin:True",
"compute_extension:createserverext": "",
"compute_extension:deferred_delete": "",
"compute_extension:disk_config": "",
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
index 65cbb4889..09131ffb0 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.json.tpl
@@ -129,6 +129,14 @@
"updated": "%(timestamp)s"
},
{
+ "alias": "os-coverage",
+ "description": "%(text)s",
+ "links": [],
+ "name": "Coverage",
+ "namespace": "http://docs.openstack.org/compute/ext/coverage/api/v2",
+ "updated": "%(timestamp)s"
+ },
+ {
"alias": "os-create-server-ext",
"description": "%(text)s",
"links": [],
diff --git a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
index bdef0266c..8241a1792 100644
--- a/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
+++ b/nova/tests/integrated/api_samples/all_extensions/extensions-get-resp.xml.tpl
@@ -48,6 +48,9 @@
<extension alias="os-consoles" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/os-consoles/api/v2" name="Consoles">
<description>%(text)s</description>
</extension>
+ <extension alias="os-coverage" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/coverage/api/v2" name="Coverage">
+ <description>%(text)s</description>
+ </extension>
<extension alias="os-create-server-ext" updated="%(timestamp)s" namespace="http://docs.openstack.org/compute/ext/createserverext/api/v1.1" name="Createserverext">
<description>%(text)s</description>
</extension>