PageRenderTime 78ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/nova/api/openstack/compute/plugins/v3/coverage.py

https://github.com/bcwaldon/nova
Python | 324 lines | 249 code | 35 blank | 40 comment | 47 complexity | 31eb788237417367bb4be61c6b357e2f MD5 | raw file
  1. # vim: tabstop=4 shiftwidth=4 softtabstop=4
  2. # Copyright 2012 IBM Corp.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. # See: http://wiki.openstack.org/Nova/CoverageExtension for more information
  16. # and usage explanation for this API extension
  17. import imp
  18. import os
  19. import re
  20. import socket
  21. import sys
  22. import telnetlib
  23. import tempfile
  24. from oslo.config import cfg
  25. from webob import exc
  26. from nova.api.openstack import extensions
  27. from nova.api.openstack import wsgi
  28. from nova import baserpc
  29. from nova import db
  30. from nova.openstack.common.gettextutils import _
  31. from nova.openstack.common import log as logging
  32. from nova.openstack.common.rpc import common as rpc_common
  33. LOG = logging.getLogger(__name__)
  34. ALIAS = "os-coverage"
  35. authorize = extensions.extension_authorizer('compute', 'v3:' + ALIAS)
  36. CONF = cfg.CONF
  37. def _import_coverage():
  38. """This function ensures loading coverage module from python-coverage."""
  39. try:
  40. path = sys.path[:]
  41. if os.getcwd() in path:
  42. # To avoid importing this module itself, it need to remove current
  43. # directory from search path.
  44. path.remove(os.getcwd())
  45. coverage_mod = imp.find_module('coverage', path)
  46. return imp.load_module('coverage', *coverage_mod)
  47. except ImportError as e:
  48. LOG.warning(_("Can't load coverage module: %s"), e)
  49. return None
  50. coverage = _import_coverage()
  51. class CoverageController(wsgi.Controller):
  52. """The Coverage report API controller for the OpenStack API."""
  53. def __init__(self):
  54. self.data_path = None
  55. self.services = []
  56. self.combine = False
  57. self._cover_inst = None
  58. self.host = CONF.host
  59. super(CoverageController, self).__init__()
  60. @property
  61. def coverInst(self):
  62. if not self._cover_inst:
  63. if self.data_path is None:
  64. self.data_path = tempfile.mkdtemp(prefix='nova-coverage_')
  65. data_out = os.path.join(self.data_path, '.nova-coverage.api')
  66. self._cover_inst = coverage.coverage(data_file=data_out)
  67. return self._cover_inst
  68. def _find_services(self, req):
  69. """Returns a list of services."""
  70. context = req.environ['nova.context']
  71. services = db.service_get_all(context)
  72. hosts = []
  73. for serv in services:
  74. hosts.append({"service": serv["topic"], "host": serv["host"]})
  75. return hosts
  76. def _find_ports(self, req, hosts):
  77. """Return a list of backdoor ports for all services in the list."""
  78. context = req.environ['nova.context']
  79. ports = []
  80. #TODO(mtreinish): Figure out how to bind the backdoor socket to 0.0.0.0
  81. # Currently this will only work if the host is resolved as loopback on
  82. # the same host as api-server
  83. for host in hosts:
  84. base = baserpc.BaseAPI(host['service'])
  85. _host = host
  86. try:
  87. _host['port'] = base.get_backdoor_port(context, host['host'])
  88. except rpc_common.UnsupportedRpcVersion:
  89. _host['port'] = None
  90. #NOTE(mtreinish): if the port is None then it wasn't set in
  91. # the configuration file for this service. However, that
  92. # doesn't necessarily mean that we don't have backdoor ports
  93. # for all the services. So, skip the telnet connection for
  94. # this service.
  95. if _host['port']:
  96. ports.append(_host)
  97. else:
  98. LOG.warning(_("Can't connect to service: %s, no port"
  99. "specified\n"), host['service'])
  100. return ports
  101. def _start_coverage_telnet(self, tn, service):
  102. data_file = os.path.join(self.data_path,
  103. '.nova-coverage.%s' % str(service))
  104. tn.write('import sys\n')
  105. tn.write('from coverage import coverage\n')
  106. tn.write("coverInst = coverage(data_file='%s') "
  107. "if 'coverInst' not in locals() "
  108. "else coverInst\n" % data_file)
  109. tn.write('coverInst.skipModules = sys.modules.keys()\n')
  110. tn.write("coverInst.start()\n")
  111. tn.write("print 'finished'\n")
  112. tn.expect([re.compile('finished')])
  113. def _check_coverage_module_loaded(self):
  114. if not self.coverInst:
  115. msg = _("Python coverage module is not installed.")
  116. raise exc.HTTPServiceUnavailable(explanation=msg)
  117. @extensions.expected_errors(503)
  118. @wsgi.action('start')
  119. @wsgi.response(204)
  120. def _start_coverage(self, req, body):
  121. '''Begin recording coverage information.'''
  122. authorize(req.environ['nova.context'])
  123. self._check_coverage_module_loaded()
  124. LOG.debug(_("Coverage begin"))
  125. body = body['start']
  126. self.combine = False
  127. if 'combine' in body.keys():
  128. self.combine = bool(body['combine'])
  129. self.coverInst.skipModules = sys.modules.keys()
  130. self.coverInst.start()
  131. hosts = self._find_services(req)
  132. ports = self._find_ports(req, hosts)
  133. self.services = []
  134. for service in ports:
  135. try:
  136. service['telnet'] = telnetlib.Telnet(service['host'],
  137. service['port'])
  138. # NOTE(mtreinish): Fallback to try connecting to lo if
  139. # ECONNREFUSED is raised. If using the hostname that is returned
  140. # for the service from the service_get_all() DB query raises
  141. # ECONNREFUSED it most likely means that the hostname in the DB
  142. # doesn't resolve to 127.0.0.1. Currently backdoors only open on
  143. # loopback so this is for covering the common single host use case
  144. except socket.error as e:
  145. exc_info = sys.exc_info()
  146. if 'ECONNREFUSED' in e and service['host'] == self.host:
  147. service['telnet'] = telnetlib.Telnet('127.0.0.1',
  148. service['port'])
  149. else:
  150. raise exc_info[0], exc_info[1], exc_info[2]
  151. self.services.append(service)
  152. self._start_coverage_telnet(service['telnet'], service['service'])
  153. def _stop_coverage_telnet(self, tn):
  154. tn.write("coverInst.stop()\n")
  155. tn.write("coverInst.save()\n")
  156. tn.write("print 'finished'\n")
  157. tn.expect([re.compile('finished')])
  158. def _check_coverage(self):
  159. try:
  160. self.coverInst.stop()
  161. self.coverInst.save()
  162. except AssertionError:
  163. return True
  164. return False
  165. @extensions.expected_errors((409, 503))
  166. @wsgi.action('stop')
  167. def _stop_coverage(self, req, body):
  168. authorize(req.environ['nova.context'])
  169. self._check_coverage_module_loaded()
  170. for service in self.services:
  171. self._stop_coverage_telnet(service['telnet'])
  172. if self._check_coverage():
  173. msg = _("Coverage not running")
  174. raise exc.HTTPConflict(explanation=msg)
  175. return {'path': self.data_path}
  176. def _report_coverage_telnet(self, tn, path, xml=False):
  177. if xml:
  178. execute = str("coverInst.xml_report(outfile='%s')\n" % path)
  179. tn.write(execute)
  180. tn.write("print 'finished'\n")
  181. tn.expect([re.compile('finished')])
  182. else:
  183. execute = str("output = open('%s', 'w')\n" % path)
  184. tn.write(execute)
  185. tn.write("coverInst.report(file=output)\n")
  186. tn.write("output.close()\n")
  187. tn.write("print 'finished'\n")
  188. tn.expect([re.compile('finished')])
  189. tn.close()
  190. @extensions.expected_errors((400, 409, 503))
  191. @wsgi.action('report')
  192. def _report_coverage(self, req, body):
  193. authorize(req.environ['nova.context'])
  194. self._check_coverage_module_loaded()
  195. self._stop_coverage(req, {'stop': {}})
  196. xml = False
  197. html = False
  198. path = None
  199. body = body['report']
  200. if 'file' in body.keys():
  201. path = body['file']
  202. if path != os.path.basename(path):
  203. msg = _("Invalid path")
  204. raise exc.HTTPBadRequest(explanation=msg)
  205. path = os.path.join(self.data_path, path)
  206. else:
  207. msg = _("No path given for report file")
  208. raise exc.HTTPBadRequest(explanation=msg)
  209. if 'xml' in body.keys():
  210. xml = body['xml']
  211. elif 'html' in body.keys():
  212. if not self.combine:
  213. msg = _("You can't use html reports without combining")
  214. raise exc.HTTPBadRequest(explanation=msg)
  215. html = body['html']
  216. if self.combine:
  217. data_out = os.path.join(self.data_path, '.nova-coverage')
  218. import coverage
  219. coverInst = coverage.coverage(data_file=data_out)
  220. coverInst.combine()
  221. if xml:
  222. coverInst.xml_report(outfile=path)
  223. elif html:
  224. if os.path.isdir(path):
  225. msg = _("Directory conflict: %s already exists")
  226. raise exc.HTTPBadRequest(explanation=msg)
  227. coverInst.html_report(directory=path)
  228. else:
  229. output = open(path, 'w')
  230. coverInst.report(file=output)
  231. output.close()
  232. for service in self.services:
  233. service['telnet'].close()
  234. else:
  235. if xml:
  236. apipath = path + '.api'
  237. self.coverInst.xml_report(outfile=apipath)
  238. for service in self.services:
  239. self._report_coverage_telnet(service['telnet'],
  240. path + '.%s'
  241. % service['service'],
  242. xml=True)
  243. else:
  244. output = open(path + '.api', 'w')
  245. self.coverInst.report(file=output)
  246. for service in self.services:
  247. self._report_coverage_telnet(service['telnet'],
  248. path + '.%s' % service['service'])
  249. output.close()
  250. return {'path': path}
  251. def _reset_coverage_telnet(self, tn):
  252. tn.write("coverInst.erase()\n")
  253. tn.write("print 'finished'\n")
  254. tn.expect([re.compile('finished')])
  255. @extensions.expected_errors(503)
  256. @wsgi.action('reset')
  257. @wsgi.response(204)
  258. def _reset_coverage(self, req, body):
  259. authorize(req.environ['nova.context'])
  260. self._check_coverage_module_loaded()
  261. # Reopen telnet connections if they are closed.
  262. for service in self.services:
  263. if not service['telnet'].get_socket():
  264. service['telnet'].open(service['host'], service['port'])
  265. # Stop coverage if it is started.
  266. try:
  267. self._stop_coverage(req, {'stop': {}})
  268. except exc.HTTPConflict:
  269. pass
  270. for service in self.services:
  271. self._reset_coverage_telnet(service['telnet'])
  272. service['telnet'].close()
  273. self.coverInst.erase()
  274. class Coverage(extensions.V3APIExtensionBase):
  275. """Enable Nova Coverage."""
  276. name = "Coverage"
  277. alias = ALIAS
  278. namespace = ("http://docs.openstack.org/compute/ext/"
  279. "coverage/api/v3")
  280. version = 1
  281. def get_resources(self):
  282. resources = []
  283. res = extensions.ResourceExtension(ALIAS,
  284. controller=CoverageController(),
  285. collection_actions={"action": "POST"})
  286. resources.append(res)
  287. return resources
  288. def get_controller_extensions(self):
  289. return []