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.

470 lines
18KB

  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 collections
  15. import re
  16. from neutron.agent.linux import external_process
  17. from neutron.agent.linux import ip_lib
  18. from neutron.common import utils
  19. from neutron_lib import constants as n_const
  20. from oslo_concurrency import lockutils
  21. from oslo_log import log
  22. from oslo_utils import uuidutils
  23. from ovsdbapp.backend.ovs_idl import event as row_event
  24. from ovsdbapp.backend.ovs_idl import vlog
  25. import six
  26. from networking_ovn.agent.metadata import driver as metadata_driver
  27. from networking_ovn.agent.metadata import ovsdb
  28. from networking_ovn.agent.metadata import server as metadata_server
  29. from networking_ovn.common import config
  30. from networking_ovn.common import constants as ovn_const
  31. LOG = log.getLogger(__name__)
  32. _SYNC_STATE_LOCK = lockutils.ReaderWriterLock()
  33. CHASSIS_METADATA_LOCK = 'chassis_metadata_lock'
  34. NS_PREFIX = 'ovnmeta-'
  35. METADATA_DEFAULT_PREFIX = 16
  36. METADATA_DEFAULT_IP = '169.254.169.254'
  37. METADATA_DEFAULT_CIDR = '%s/%d' % (METADATA_DEFAULT_IP,
  38. METADATA_DEFAULT_PREFIX)
  39. METADATA_PORT = 80
  40. MAC_PATTERN = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
  41. MetadataPortInfo = collections.namedtuple('MetadataPortInfo', ['mac',
  42. 'ip_addresses'])
  43. def _sync_lock(f):
  44. """Decorator to block all operations for a global sync call."""
  45. @six.wraps(f)
  46. def wrapped(*args, **kwargs):
  47. with _SYNC_STATE_LOCK.write_lock():
  48. return f(*args, **kwargs)
  49. return wrapped
  50. class ConfigException(Exception):
  51. """Misconfiguration of the agent
  52. This exception is raised when agent detects its wrong configuration.
  53. Typically agent should resync when this is raised.
  54. """
  55. class PortBindingChassisEvent(row_event.RowEvent):
  56. def __init__(self, metadata_agent):
  57. self.agent = metadata_agent
  58. table = 'Port_Binding'
  59. events = (self.ROW_UPDATE)
  60. super(PortBindingChassisEvent, self).__init__(
  61. events, table, None)
  62. self.event_name = 'PortBindingChassisEvent'
  63. def run(self, event, row, old):
  64. # Check if the port has been bound/unbound to our chassis and update
  65. # the metadata namespace accordingly.
  66. # Type must be empty to make sure it's a VIF.
  67. resync = False
  68. if row.type != "":
  69. return
  70. new_chassis = getattr(row, 'chassis', [])
  71. old_chassis = getattr(old, 'chassis', [])
  72. with _SYNC_STATE_LOCK.read_lock():
  73. try:
  74. if new_chassis and new_chassis[0].name == self.agent.chassis:
  75. LOG.info("Port %s in datapath %s bound to our chassis",
  76. row.logical_port, str(row.datapath.uuid))
  77. self.agent.update_datapath(str(row.datapath.uuid))
  78. elif old_chassis and old_chassis[0].name == self.agent.chassis:
  79. LOG.info("Port %s in datapath %s unbound from our chassis",
  80. row.logical_port, str(row.datapath.uuid))
  81. self.agent.update_datapath(str(row.datapath.uuid))
  82. except ConfigException:
  83. # We're now in the reader lock mode, we need to exit the
  84. # context and then use writer lock
  85. resync = True
  86. if resync:
  87. self.agent.resync()
  88. class ChassisCreateEvent(row_event.RowEvent):
  89. """Row create event - Chassis name == our_chassis.
  90. On connection, we get a dump of all chassis so if we catch a creation
  91. of our own chassis it has to be a reconnection. In this case, we need
  92. to do a full sync to make sure that we capture all changes while the
  93. connection to OVSDB was down.
  94. """
  95. def __init__(self, metadata_agent):
  96. self.agent = metadata_agent
  97. self.first_time = True
  98. table = 'Chassis'
  99. events = (self.ROW_CREATE)
  100. super(ChassisCreateEvent, self).__init__(
  101. events, table, (('name', '=', self.agent.chassis),))
  102. self.event_name = 'ChassisCreateEvent'
  103. def run(self, event, row, old):
  104. if self.first_time:
  105. self.first_time = False
  106. else:
  107. # NOTE(lucasagomes): Re-register the ovn metadata agent
  108. # with the local chassis in case its entry was re-created
  109. # (happens when restarting the ovn-controller)
  110. self.agent.register_metadata_agent()
  111. LOG.info("Connection to OVSDB established, doing a full sync")
  112. self.agent.sync()
  113. class SbGlobalUpdateEvent(row_event.RowEvent):
  114. """Row update event on SB_Global table."""
  115. def __init__(self, metadata_agent):
  116. self.agent = metadata_agent
  117. table = 'SB_Global'
  118. events = (self.ROW_UPDATE)
  119. super(SbGlobalUpdateEvent, self).__init__(events, table, None)
  120. self.event_name = 'SbGlobalUpdateEvent'
  121. def run(self, event, row, old):
  122. self.agent.sb_idl.update_metadata_health_status(
  123. self.agent.chassis, row.nb_cfg).execute()
  124. class MetadataAgent(object):
  125. def __init__(self, conf):
  126. self.conf = conf
  127. vlog.use_python_logger(max_level=config.get_ovn_ovsdb_log_level())
  128. self._process_monitor = external_process.ProcessMonitor(
  129. config=self.conf,
  130. resource_type='metadata')
  131. def _load_config(self):
  132. self.chassis = self._get_own_chassis_name()
  133. self.ovn_bridge = self._get_ovn_bridge()
  134. LOG.debug("Loaded chassis %s and ovn bridge %s.",
  135. self.chassis, self.ovn_bridge)
  136. @_sync_lock
  137. def resync(self):
  138. """Resync the agent.
  139. Reload the configuration and sync the agent again.
  140. """
  141. self._load_config()
  142. self.sync()
  143. def start(self):
  144. # Launch the server that will act as a proxy between the VM's and Nova.
  145. proxy = metadata_server.UnixDomainMetadataProxy(self.conf)
  146. proxy.run()
  147. # Open the connection to OVS database
  148. self.ovs_idl = ovsdb.MetadataAgentOvsIdl().start()
  149. self._load_config()
  150. # Open the connection to OVN SB database.
  151. self.sb_idl = ovsdb.MetadataAgentOvnSbIdl(
  152. [PortBindingChassisEvent(self), ChassisCreateEvent(self),
  153. SbGlobalUpdateEvent(self)]).start()
  154. # Do the initial sync.
  155. self.sync()
  156. # Register the agent with its corresponding Chassis
  157. self.register_metadata_agent()
  158. proxy.wait()
  159. def register_metadata_agent(self):
  160. # NOTE(lucasagomes): db_add() will not overwrite the UUID if
  161. # it's already set.
  162. ext_ids = {
  163. ovn_const.OVN_AGENT_METADATA_ID_KEY: uuidutils.generate_uuid()}
  164. self.sb_idl.db_add('Chassis', self.chassis, 'external_ids',
  165. ext_ids).execute(check_error=True)
  166. def _get_own_chassis_name(self):
  167. """Return the external_ids:system-id value of the Open_vSwitch table.
  168. As long as ovn-controller is running on this node, the key is
  169. guaranteed to exist and will include the chassis name.
  170. """
  171. ext_ids = self.ovs_idl.db_get(
  172. 'Open_vSwitch', '.', 'external_ids').execute()
  173. return ext_ids['system-id']
  174. def _get_ovn_bridge(self):
  175. """Return the external_ids:ovn-bridge value of the Open_vSwitch table.
  176. This is the OVS bridge used to plug the metadata ports to.
  177. If the key doesn't exist, this method will return 'br-int' as default.
  178. """
  179. ext_ids = self.ovs_idl.db_get(
  180. 'Open_vSwitch', '.', 'external_ids').execute()
  181. try:
  182. ovn_bridge = ext_ids['ovn-bridge']
  183. except KeyError:
  184. LOG.warning("Can't read ovn-bridge external-id from OVSDB. Using "
  185. "br-int instead.")
  186. return 'br-int'
  187. return ovn_bridge
  188. @_sync_lock
  189. def sync(self):
  190. """Agent sync.
  191. This function will make sure that all networks with ports in our
  192. chassis are serving metadata. Also, it will tear down those namespaces
  193. which were serving metadata but are no longer needed.
  194. """
  195. metadata_namespaces = self.ensure_all_networks_provisioned()
  196. system_namespaces = tuple(
  197. ns.decode('utf-8') if isinstance(ns, bytes) else ns
  198. for ns in ip_lib.list_network_namespaces())
  199. unused_namespaces = [ns for ns in system_namespaces if
  200. ns.startswith(NS_PREFIX) and
  201. ns not in metadata_namespaces]
  202. for ns in unused_namespaces:
  203. self.teardown_datapath(self._get_datapath_name(ns))
  204. @staticmethod
  205. def _get_veth_name(datapath):
  206. return ['{}{}{}'.format(n_const.TAP_DEVICE_PREFIX,
  207. datapath[:10], i) for i in [0, 1]]
  208. @staticmethod
  209. def _get_datapath_name(namespace):
  210. return namespace[len(NS_PREFIX):]
  211. @staticmethod
  212. def _get_namespace_name(datapath):
  213. return NS_PREFIX + datapath
  214. def teardown_datapath(self, datapath):
  215. """Unprovision this datapath to stop serving metadata.
  216. This function will shutdown metadata proxy if it's running and delete
  217. the VETH pair, the OVS port and the namespace.
  218. """
  219. self.update_chassis_metadata_networks(datapath, remove=True)
  220. namespace = self._get_namespace_name(datapath)
  221. ip = ip_lib.IPWrapper(namespace)
  222. # If the namespace doesn't exist, return
  223. if not ip.netns.exists(namespace):
  224. return
  225. LOG.info("Cleaning up %s namespace which is not needed anymore",
  226. namespace)
  227. metadata_driver.MetadataDriver.destroy_monitored_metadata_proxy(
  228. self._process_monitor, datapath, self.conf, namespace)
  229. veth_name = self._get_veth_name(datapath)
  230. self.ovs_idl.del_port(veth_name[0]).execute()
  231. if ip_lib.device_exists(veth_name[0]):
  232. ip_lib.IPWrapper().del_veth(veth_name[0])
  233. ip.garbage_collect_namespace()
  234. def update_datapath(self, datapath):
  235. """Update the metadata service for this datapath.
  236. This function will:
  237. * Provision the namespace if it wasn't already in place.
  238. * Update the namespace if it was already serving metadata (for example,
  239. after binding/unbinding the first/last port of a subnet in our
  240. chassis).
  241. * Tear down the namespace if there are no more ports in our chassis
  242. for this datapath.
  243. """
  244. ports = self.sb_idl.get_ports_on_chassis(self.chassis)
  245. datapath_ports = [p for p in ports if p.type == '' and
  246. str(p.datapath.uuid) == datapath]
  247. if datapath_ports:
  248. self.provision_datapath(datapath)
  249. else:
  250. self.teardown_datapath(datapath)
  251. def provision_datapath(self, datapath):
  252. """Provision the datapath so that it can serve metadata.
  253. This function will create the namespace and VETH pair if needed
  254. and assign the IP addresses to the interface corresponding to the
  255. metadata port of the network. It will also remove existing IP
  256. addresses that are no longer needed.
  257. :return: The metadata namespace name of this datapath
  258. """
  259. LOG.debug("Provisioning datapath %s", datapath)
  260. port = self.sb_idl.get_metadata_port_network(datapath)
  261. # If there's no metadata port or it doesn't have a MAC or IP
  262. # addresses, then tear the namespace down if needed. This might happen
  263. # when there are no subnets yet created so metadata port doesn't have
  264. # an IP address.
  265. if not (port and port.mac and
  266. port.external_ids.get(ovn_const.OVN_CIDRS_EXT_ID_KEY, None)):
  267. LOG.debug("There is no metadata port for datapath %s or it has no "
  268. "MAC or IP addresses configured, tearing the namespace "
  269. "down if needed", datapath)
  270. self.teardown_datapath(datapath)
  271. return
  272. # First entry of the mac field must be the MAC address.
  273. match = MAC_PATTERN.match(port.mac[0].split(' ')[0])
  274. # If it is not, we can't provision the namespace. Tear it down if
  275. # needed and log the error.
  276. if not match:
  277. LOG.error("Metadata port for datapath %s doesn't have a MAC "
  278. "address, tearing the namespace down if needed",
  279. datapath)
  280. self.teardown_datapath(datapath)
  281. return
  282. mac = match.group()
  283. ip_addresses = set(
  284. port.external_ids[ovn_const.OVN_CIDRS_EXT_ID_KEY].split(' '))
  285. ip_addresses.add(METADATA_DEFAULT_CIDR)
  286. metadata_port = MetadataPortInfo(mac, ip_addresses)
  287. # Create the VETH pair if it's not created. Also the add_veth function
  288. # will create the namespace for us.
  289. namespace = self._get_namespace_name(datapath)
  290. veth_name = self._get_veth_name(datapath)
  291. ip1 = ip_lib.IPDevice(veth_name[0])
  292. if ip_lib.device_exists(veth_name[1], namespace):
  293. ip2 = ip_lib.IPDevice(veth_name[1], namespace)
  294. else:
  295. LOG.debug("Creating VETH %s in %s namespace", veth_name[1],
  296. namespace)
  297. # Might happen that the end in the root namespace exists even
  298. # though the other end doesn't. Make sure we delete it first if
  299. # that's the case.
  300. if ip1.exists():
  301. ip1.link.delete()
  302. ip1, ip2 = ip_lib.IPWrapper().add_veth(
  303. veth_name[0], veth_name[1], namespace)
  304. # Make sure both ends of the VETH are up
  305. ip1.link.set_up()
  306. ip2.link.set_up()
  307. # Configure the MAC address.
  308. ip2.link.set_address(metadata_port.mac)
  309. dev_info = ip2.addr.list()
  310. # Configure the IP addresses on the VETH pair and remove those
  311. # that we no longer need.
  312. current_cidrs = {dev['cidr'] for dev in dev_info}
  313. for ipaddr in current_cidrs - metadata_port.ip_addresses:
  314. ip2.addr.delete(ipaddr)
  315. for ipaddr in metadata_port.ip_addresses - current_cidrs:
  316. # NOTE(dalvarez): metadata only works on IPv4. We're doing this
  317. # extra check here because it could be that the metadata port has
  318. # an IPv6 address if there's an IPv6 subnet with SLAAC in its
  319. # network. Neutron IPAM will autoallocate an IPv6 address for every
  320. # port in the network.
  321. if utils.get_ip_version(ipaddr) == 4:
  322. ip2.addr.add(ipaddr)
  323. # Check that this port is not attached to any other OVS bridge. This
  324. # can happen when the OVN bridge changes (for example, during a
  325. # migration from ML2/OVS).
  326. ovs_bridges = set(self.ovs_idl.list_br().execute())
  327. try:
  328. ovs_bridges.remove(self.ovn_bridge)
  329. except KeyError:
  330. LOG.warning("Configured OVN bridge %s cannot be found in "
  331. "the system. Resyncing the agent.", self.ovn_bridge)
  332. raise ConfigException()
  333. if ovs_bridges:
  334. with self.ovs_idl.transaction() as txn:
  335. for br in ovs_bridges:
  336. txn.add(self.ovs_idl.del_port(veth_name[0], bridge=br,
  337. if_exists=True))
  338. # Configure the OVS port and add external_ids:iface-id so that it
  339. # can be tracked by OVN.
  340. self.ovs_idl.add_port(self.ovn_bridge,
  341. veth_name[0]).execute()
  342. self.ovs_idl.db_set(
  343. 'Interface', veth_name[0],
  344. ('external_ids', {'iface-id': port.logical_port})).execute()
  345. # Spawn metadata proxy if it's not already running.
  346. metadata_driver.MetadataDriver.spawn_monitored_metadata_proxy(
  347. self._process_monitor, namespace, METADATA_PORT,
  348. self.conf, network_id=datapath)
  349. self.update_chassis_metadata_networks(datapath)
  350. return namespace
  351. def ensure_all_networks_provisioned(self):
  352. """Ensure that all datapaths are provisioned.
  353. This function will make sure that all datapaths with ports bound to
  354. our chassis have its namespace, VETH pair and OVS port created and
  355. metadata proxy is up and running.
  356. :return: A list with the namespaces that are currently serving
  357. metadata
  358. """
  359. # Retrieve all ports in our Chassis with type == ''
  360. ports = self.sb_idl.get_ports_on_chassis(self.chassis)
  361. datapaths = {str(p.datapath.uuid) for p in ports if p.type == ''}
  362. namespaces = []
  363. # Make sure that all those datapaths are serving metadata
  364. for datapath in datapaths:
  365. netns = self.provision_datapath(datapath)
  366. if netns:
  367. namespaces.append(netns)
  368. return namespaces
  369. # NOTE(lucasagomes): Even tho the metadata agent is a multi-process
  370. # application, there's only one Southbound database IDL instance in
  371. # the agent which handles the OVSDB events therefore we do not need
  372. # the external=True parameter in the @synchronized decorator.
  373. @lockutils.synchronized(CHASSIS_METADATA_LOCK)
  374. def update_chassis_metadata_networks(self, datapath, remove=False):
  375. """Update metadata networks hosted in this chassis.
  376. Add or remove a datapath from the list of current datapaths that
  377. we're currently serving metadata.
  378. """
  379. current_dps = self.sb_idl.get_chassis_metadata_networks(self.chassis)
  380. updated = False
  381. if remove:
  382. if datapath in current_dps:
  383. current_dps.remove(datapath)
  384. updated = True
  385. else:
  386. if datapath not in current_dps:
  387. current_dps.append(datapath)
  388. updated = True
  389. if updated:
  390. with self.sb_idl.create_transaction(check_error=True) as txn:
  391. txn.add(self.sb_idl.set_chassis_metadata_networks(
  392. self.chassis, current_dps))