Neutron integration with OVN
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.

209 lines
7.8 KiB

  1. # Copyright 2017 Red Hat, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain 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,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import hashlib
  15. import hmac
  16. from neutron.agent.linux import utils as agent_utils
  17. from neutron.conf.agent.metadata import config
  18. from neutron_lib.callbacks import events
  19. from neutron_lib.callbacks import registry
  20. from neutron_lib.callbacks import resources
  21. from oslo_config import cfg
  22. from oslo_log import log as logging
  23. from oslo_utils import encodeutils
  24. import requests
  25. import six
  26. import six.moves.urllib.parse as urlparse
  27. import webob
  28. from networking_ovn._i18n import _
  29. from networking_ovn.agent.metadata import ovsdb
  30. from networking_ovn.common import constants as ovn_const
  31. LOG = logging.getLogger(__name__)
  32. MODE_MAP = {
  33. config.USER_MODE: 0o644,
  34. config.GROUP_MODE: 0o664,
  35. config.ALL_MODE: 0o666,
  36. }
  37. class MetadataProxyHandler(object):
  38. def __init__(self, conf):
  39. self.conf = conf
  40. self.subscribe()
  41. def subscribe(self):
  42. registry.subscribe(self.post_fork_initialize,
  43. resources.PROCESS,
  44. events.AFTER_INIT)
  45. def post_fork_initialize(self, resource, event, trigger, payload=None):
  46. # We need to open a connection to OVN SouthBound database for
  47. # each worker so that we can process the metadata requests.
  48. self.sb_idl = ovsdb.MetadataAgentOvnSbIdl().start()
  49. @webob.dec.wsgify(RequestClass=webob.Request)
  50. def __call__(self, req):
  51. try:
  52. LOG.debug("Request: %s", req)
  53. instance_id, project_id = self._get_instance_and_project_id(req)
  54. if instance_id:
  55. return self._proxy_request(instance_id, project_id, req)
  56. else:
  57. return webob.exc.HTTPNotFound()
  58. except Exception:
  59. LOG.exception("Unexpected error.")
  60. msg = _('An unknown error has occurred. '
  61. 'Please try your request again.')
  62. explanation = six.text_type(msg)
  63. return webob.exc.HTTPInternalServerError(explanation=explanation)
  64. def _get_instance_and_project_id(self, req):
  65. remote_address = req.headers.get('X-Forwarded-For')
  66. network_id = req.headers.get('X-OVN-Network-ID')
  67. ports = self.sb_idl.get_network_port_bindings_by_ip(network_id,
  68. remote_address)
  69. num_ports = len(ports)
  70. if num_ports == 1:
  71. external_ids = ports[0].external_ids
  72. return (external_ids[ovn_const.OVN_DEVID_EXT_ID_KEY],
  73. external_ids[ovn_const.OVN_PROJID_EXT_ID_KEY])
  74. elif num_ports == 0:
  75. LOG.error("No port found in network %s with IP address %s",
  76. network_id, remote_address)
  77. elif num_ports > 1:
  78. port_uuids = ', '.join([str(port.uuid) for port in ports])
  79. LOG.error("More than one port found in network %s with IP address "
  80. "%s. Please run the neutron-ovn-db-sync-util script as "
  81. "there seems to be inconsistent data between Neutron "
  82. "and OVN databases. OVN Port uuids: %s", network_id,
  83. remote_address, port_uuids)
  84. return None, None
  85. def _proxy_request(self, instance_id, tenant_id, req):
  86. headers = {
  87. 'X-Forwarded-For': req.headers.get('X-Forwarded-For'),
  88. 'X-Instance-ID': instance_id,
  89. 'X-Tenant-ID': tenant_id,
  90. 'X-Instance-ID-Signature': self._sign_instance_id(instance_id)
  91. }
  92. nova_host_port = '%s:%s' % (self.conf.nova_metadata_host,
  93. self.conf.nova_metadata_port)
  94. url = urlparse.urlunsplit((
  95. self.conf.nova_metadata_protocol,
  96. nova_host_port,
  97. req.path_info,
  98. req.query_string,
  99. ''))
  100. disable_ssl_certificate_validation = self.conf.nova_metadata_insecure
  101. if self.conf.auth_ca_cert and not disable_ssl_certificate_validation:
  102. verify_cert = self.conf.auth_ca_cert
  103. else:
  104. verify_cert = not disable_ssl_certificate_validation
  105. client_cert = None
  106. if self.conf.nova_client_cert and self.conf.nova_client_priv_key:
  107. client_cert = (self.conf.nova_client_cert,
  108. self.conf.nova_client_priv_key)
  109. resp = requests.request(method=req.method, url=url,
  110. headers=headers,
  111. data=req.body,
  112. cert=client_cert,
  113. verify=verify_cert)
  114. if resp.status_code == 200:
  115. req.response.content_type = resp.headers['content-type']
  116. req.response.body = resp.content
  117. LOG.debug(str(resp))
  118. return req.response
  119. elif resp.status_code == 403:
  120. LOG.warning(
  121. 'The remote metadata server responded with Forbidden. This '
  122. 'response usually occurs when shared secrets do not match.'
  123. )
  124. return webob.exc.HTTPForbidden()
  125. elif resp.status_code == 400:
  126. return webob.exc.HTTPBadRequest()
  127. elif resp.status_code == 404:
  128. return webob.exc.HTTPNotFound()
  129. elif resp.status_code == 409:
  130. return webob.exc.HTTPConflict()
  131. elif resp.status_code == 500:
  132. msg = _(
  133. 'Remote metadata server experienced an internal server error.'
  134. )
  135. LOG.warning(msg)
  136. explanation = six.text_type(msg)
  137. return webob.exc.HTTPInternalServerError(explanation=explanation)
  138. else:
  139. raise Exception(_('Unexpected response code: %s') %
  140. resp.status_code)
  141. def _sign_instance_id(self, instance_id):
  142. secret = self.conf.metadata_proxy_shared_secret
  143. secret = encodeutils.to_utf8(secret)
  144. instance_id = encodeutils.to_utf8(instance_id)
  145. return hmac.new(secret, instance_id, hashlib.sha256).hexdigest()
  146. class UnixDomainMetadataProxy(object):
  147. def __init__(self, conf):
  148. self.conf = conf
  149. agent_utils.ensure_directory_exists_without_file(
  150. cfg.CONF.metadata_proxy_socket)
  151. def _get_socket_mode(self):
  152. mode = self.conf.metadata_proxy_socket_mode
  153. if mode == config.DEDUCE_MODE:
  154. user = self.conf.metadata_proxy_user
  155. if (not user or user == '0' or user == 'root' or
  156. agent_utils.is_effective_user(user)):
  157. # user is agent effective user or root => USER_MODE
  158. mode = config.USER_MODE
  159. else:
  160. group = self.conf.metadata_proxy_group
  161. if not group or agent_utils.is_effective_group(group):
  162. # group is agent effective group => GROUP_MODE
  163. mode = config.GROUP_MODE
  164. else:
  165. # otherwise => ALL_MODE
  166. mode = config.ALL_MODE
  167. return MODE_MAP[mode]
  168. def run(self):
  169. self.server = agent_utils.UnixDomainWSGIServer(
  170. 'networking-ovn-metadata-agent')
  171. self.server.start(MetadataProxyHandler(self.conf),
  172. self.conf.metadata_proxy_socket,
  173. workers=self.conf.metadata_workers,
  174. backlog=self.conf.metadata_backlog,
  175. mode=self._get_socket_mode())
  176. def wait(self):
  177. self.server.wait()