OpenStack Networking (Neutron)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

322 lines
12 KiB

  1. # Copyright 2012 New Dream Network, LLC (DreamHost)
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. import hashlib
  15. import hmac
  16. from neutron_lib.agent import topics
  17. from neutron_lib import constants
  18. from neutron_lib import context
  19. from neutron_lib import rpc as n_rpc
  20. from oslo_config import cfg
  21. from oslo_log import log as logging
  22. import oslo_messaging
  23. from oslo_service import loopingcall
  24. from oslo_utils import encodeutils
  25. import requests
  26. import six
  27. from six.moves import urllib
  28. import webob
  29. from neutron._i18n import _
  30. from neutron.agent.linux import utils as agent_utils
  31. from neutron.agent import rpc as agent_rpc
  32. from neutron.common import cache_utils as cache
  33. from neutron.common import ipv6_utils
  34. from neutron.conf.agent.metadata import config
  35. LOG = logging.getLogger(__name__)
  36. MODE_MAP = {
  37. config.USER_MODE: 0o644,
  38. config.GROUP_MODE: 0o664,
  39. config.ALL_MODE: 0o666,
  40. }
  41. class MetadataPluginAPI(object):
  42. """Agent-side RPC for metadata agent-to-plugin interaction.
  43. This class implements the client side of an rpc interface used by the
  44. metadata service to make calls back into the Neutron plugin. The server
  45. side is defined in
  46. neutron.api.rpc.handlers.metadata_rpc.MetadataRpcCallback. For more
  47. information about changing rpc interfaces, see
  48. doc/source/contributor/internals/rpc_api.rst.
  49. API version history:
  50. 1.0 - Initial version.
  51. """
  52. def __init__(self, topic):
  53. target = oslo_messaging.Target(
  54. topic=topic,
  55. namespace=constants.RPC_NAMESPACE_METADATA,
  56. version='1.0')
  57. self.client = n_rpc.get_client(target)
  58. def get_ports(self, context, filters):
  59. cctxt = self.client.prepare()
  60. return cctxt.call(context, 'get_ports', filters=filters)
  61. class MetadataProxyHandler(object):
  62. def __init__(self, conf):
  63. self.conf = conf
  64. self._cache = cache.get_cache(self.conf)
  65. self.plugin_rpc = MetadataPluginAPI(topics.PLUGIN)
  66. self.context = context.get_admin_context_without_session()
  67. @webob.dec.wsgify(RequestClass=webob.Request)
  68. def __call__(self, req):
  69. try:
  70. LOG.debug("Request: %s", req)
  71. instance_id, tenant_id = self._get_instance_and_tenant_id(req)
  72. if instance_id:
  73. return self._proxy_request(instance_id, tenant_id, req)
  74. else:
  75. return webob.exc.HTTPNotFound()
  76. except Exception:
  77. LOG.exception("Unexpected error.")
  78. msg = _('An unknown error has occurred. '
  79. 'Please try your request again.')
  80. explanation = six.text_type(msg)
  81. return webob.exc.HTTPInternalServerError(explanation=explanation)
  82. def _get_ports_from_server(self, router_id=None, ip_address=None,
  83. networks=None):
  84. """Get ports from server."""
  85. filters = self._get_port_filters(router_id, ip_address, networks)
  86. return self.plugin_rpc.get_ports(self.context, filters)
  87. def _get_port_filters(self, router_id=None, ip_address=None,
  88. networks=None):
  89. filters = {}
  90. if router_id:
  91. filters['device_id'] = [router_id]
  92. filters['device_owner'] = constants.ROUTER_INTERFACE_OWNERS
  93. if ip_address:
  94. filters['fixed_ips'] = {'ip_address': [ip_address]}
  95. if networks:
  96. filters['network_id'] = networks
  97. return filters
  98. @cache.cache_method_results
  99. def _get_router_networks(self, router_id):
  100. """Find all networks connected to given router."""
  101. internal_ports = self._get_ports_from_server(router_id=router_id)
  102. return tuple(p['network_id'] for p in internal_ports)
  103. @cache.cache_method_results
  104. def _get_ports_for_remote_address(self, remote_address, networks):
  105. """Get list of ports that has given ip address and are part of
  106. given networks.
  107. :param networks: list of networks in which the ip address will be
  108. searched for
  109. """
  110. return self._get_ports_from_server(networks=networks,
  111. ip_address=remote_address)
  112. def _get_ports(self, remote_address, network_id=None, router_id=None):
  113. """Search for all ports that contain passed ip address and belongs to
  114. given network.
  115. If no network is passed ports are searched on all networks connected to
  116. given router. Either one of network_id or router_id must be passed.
  117. """
  118. if network_id:
  119. networks = (network_id,)
  120. elif router_id:
  121. networks = self._get_router_networks(router_id)
  122. else:
  123. raise TypeError(_("Either one of parameter network_id or router_id"
  124. " must be passed to _get_ports method."))
  125. return self._get_ports_for_remote_address(remote_address, networks)
  126. def _get_instance_and_tenant_id(self, req):
  127. remote_address = req.headers.get('X-Forwarded-For')
  128. network_id = req.headers.get('X-Neutron-Network-ID')
  129. router_id = req.headers.get('X-Neutron-Router-ID')
  130. ports = self._get_ports(remote_address, network_id, router_id)
  131. LOG.debug("Gotten ports for remote_address %(remote_address)s, "
  132. "network_id %(network_id)s, router_id %(router_id)s are: "
  133. "%(ports)s",
  134. {"remote_address": remote_address,
  135. "network_id": network_id,
  136. "router_id": router_id,
  137. "ports": ports})
  138. if len(ports) == 1:
  139. return ports[0]['device_id'], ports[0]['tenant_id']
  140. return None, None
  141. def _proxy_request(self, instance_id, tenant_id, req):
  142. headers = {
  143. 'X-Forwarded-For': req.headers.get('X-Forwarded-For'),
  144. 'X-Instance-ID': instance_id,
  145. 'X-Tenant-ID': tenant_id,
  146. 'X-Instance-ID-Signature': self._sign_instance_id(instance_id)
  147. }
  148. nova_host_port = ipv6_utils.valid_ipv6_url(
  149. self.conf.nova_metadata_host,
  150. self.conf.nova_metadata_port)
  151. url = urllib.parse.urlunsplit((
  152. self.conf.nova_metadata_protocol,
  153. nova_host_port,
  154. req.path_info,
  155. req.query_string,
  156. ''))
  157. disable_ssl_certificate_validation = self.conf.nova_metadata_insecure
  158. if self.conf.auth_ca_cert and not disable_ssl_certificate_validation:
  159. verify_cert = self.conf.auth_ca_cert
  160. else:
  161. verify_cert = not disable_ssl_certificate_validation
  162. client_cert = None
  163. if self.conf.nova_client_cert and self.conf.nova_client_priv_key:
  164. client_cert = (self.conf.nova_client_cert,
  165. self.conf.nova_client_priv_key)
  166. resp = requests.request(method=req.method, url=url,
  167. headers=headers,
  168. data=req.body,
  169. cert=client_cert,
  170. verify=verify_cert)
  171. if resp.status_code == 200:
  172. req.response.content_type = resp.headers['content-type']
  173. req.response.body = resp.content
  174. LOG.debug(str(resp))
  175. return req.response
  176. elif resp.status_code == 403:
  177. LOG.warning(
  178. 'The remote metadata server responded with Forbidden. This '
  179. 'response usually occurs when shared secrets do not match.'
  180. )
  181. return webob.exc.HTTPForbidden()
  182. elif resp.status_code == 400:
  183. return webob.exc.HTTPBadRequest()
  184. elif resp.status_code == 404:
  185. return webob.exc.HTTPNotFound()
  186. elif resp.status_code == 409:
  187. return webob.exc.HTTPConflict()
  188. elif resp.status_code == 500:
  189. msg = _(
  190. 'Remote metadata server experienced an internal server error.'
  191. )
  192. LOG.warning(msg)
  193. explanation = six.text_type(msg)
  194. return webob.exc.HTTPInternalServerError(explanation=explanation)
  195. else:
  196. raise Exception(_('Unexpected response code: %s') %
  197. resp.status_code)
  198. def _sign_instance_id(self, instance_id):
  199. secret = self.conf.metadata_proxy_shared_secret
  200. secret = encodeutils.to_utf8(secret)
  201. instance_id = encodeutils.to_utf8(instance_id)
  202. return hmac.new(secret, instance_id, hashlib.sha256).hexdigest()
  203. class UnixDomainMetadataProxy(object):
  204. def __init__(self, conf):
  205. self.conf = conf
  206. agent_utils.ensure_directory_exists_without_file(
  207. cfg.CONF.metadata_proxy_socket)
  208. def _init_state_reporting(self):
  209. self.context = context.get_admin_context_without_session()
  210. self.failed_state_report = False
  211. self.state_rpc = agent_rpc.PluginReportStateAPI(topics.REPORTS)
  212. self.agent_state = {
  213. 'binary': 'neutron-metadata-agent',
  214. 'host': cfg.CONF.host,
  215. 'topic': 'N/A',
  216. 'configurations': {
  217. 'metadata_proxy_socket': cfg.CONF.metadata_proxy_socket,
  218. 'nova_metadata_host': cfg.CONF.nova_metadata_host,
  219. 'nova_metadata_port': cfg.CONF.nova_metadata_port,
  220. 'log_agent_heartbeats': cfg.CONF.AGENT.log_agent_heartbeats,
  221. },
  222. 'start_flag': True,
  223. 'agent_type': constants.AGENT_TYPE_METADATA}
  224. report_interval = cfg.CONF.AGENT.report_interval
  225. if report_interval:
  226. self.heartbeat = loopingcall.FixedIntervalLoopingCall(
  227. self._report_state)
  228. self.heartbeat.start(interval=report_interval)
  229. def _report_state(self):
  230. try:
  231. self.state_rpc.report_state(
  232. self.context,
  233. self.agent_state,
  234. use_call=self.agent_state.get('start_flag'))
  235. except AttributeError:
  236. # This means the server does not support report_state
  237. LOG.warning('Neutron server does not support state report.'
  238. ' State report for this agent will be disabled.')
  239. self.heartbeat.stop()
  240. return
  241. except Exception:
  242. self.failed_state_report = True
  243. LOG.exception("Failed reporting state!")
  244. return
  245. if self.failed_state_report:
  246. self.failed_state_report = False
  247. LOG.info('Successfully reported state after a previous failure.')
  248. self.agent_state.pop('start_flag', None)
  249. def _get_socket_mode(self):
  250. mode = self.conf.metadata_proxy_socket_mode
  251. if mode == config.DEDUCE_MODE:
  252. user = self.conf.metadata_proxy_user
  253. if (not user or user == '0' or user == 'root' or
  254. agent_utils.is_effective_user(user)):
  255. # user is agent effective user or root => USER_MODE
  256. mode = config.USER_MODE
  257. else:
  258. group = self.conf.metadata_proxy_group
  259. if not group or agent_utils.is_effective_group(group):
  260. # group is agent effective group => GROUP_MODE
  261. mode = config.GROUP_MODE
  262. else:
  263. # otherwise => ALL_MODE
  264. mode = config.ALL_MODE
  265. return MODE_MAP[mode]
  266. def run(self):
  267. server = agent_utils.UnixDomainWSGIServer('neutron-metadata-agent')
  268. server.start(MetadataProxyHandler(self.conf),
  269. self.conf.metadata_proxy_socket,
  270. workers=self.conf.metadata_workers,
  271. backlog=self.conf.metadata_backlog,
  272. mode=self._get_socket_mode())
  273. self._init_state_reporting()
  274. server.wait()