kuryr-kubernetes/kuryr_kubernetes/controller/drivers/nested_vlan_vif.py
Michał Dulko 9cd15b6d37 Revert "Nit: Change from dict to object notation"
This reverts commit feec91cec1. Turns out
this wasn't okay, sub_ports property on Trunk objects is a list of
dictionaries.

This also fixes unit tests to account for that.

Change-Id: I17f217a6f2bfc833019ba407c248564e74b663d2
2023-03-28 15:54:11 +02:00

312 lines
13 KiB
Python

# All Rights Reserved.
#
# 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.
from time import sleep
from kuryr.lib import constants as kl_const
from kuryr.lib import exceptions as kl_exc
from kuryr.lib import segmentation_type_drivers as seg_driver
from openstack import exceptions as os_exc
from oslo_config import cfg
from oslo_log import log as logging
from kuryr_kubernetes import clients
from kuryr_kubernetes import config
from kuryr_kubernetes import constants
from kuryr_kubernetes.controller.drivers import nested_vif
from kuryr_kubernetes.controller.drivers import utils
from kuryr_kubernetes import exceptions as k_exc
from kuryr_kubernetes import os_vif_util as ovu
LOG = logging.getLogger(__name__)
DEFAULT_MAX_RETRY_COUNT = 3
DEFAULT_RETRY_INTERVAL = 1
ACTIVE_TIMEOUT = 90
CONF = cfg.CONF
class NestedVlanPodVIFDriver(nested_vif.NestedPodVIFDriver):
"""Manages ports for nested-containers using VLANs to provide VIFs."""
def request_vif(self, pod, project_id, subnets, security_groups):
os_net = clients.get_network_client()
parent_port = self._get_parent_port(pod)
trunk_id = self._get_trunk_id(parent_port)
rq = self._get_port_request(pod, project_id, subnets, security_groups)
port = os_net.create_port(**rq)
self._check_port_binding([port])
if not self._tag_on_creation:
utils.tag_neutron_resources([port])
vlan_id = self._add_subport(trunk_id, port.id)
return ovu.neutron_to_osvif_vif_nested_vlan(port, subnets, vlan_id)
def request_vifs(self, pod, project_id, subnets, security_groups,
num_ports, semaphore, trunk_ip=None):
"""This method creates subports and returns a list with their vifs.
It creates up to num_ports subports and attaches them to the trunk
port.
If not enough vlan ids are available for all the subports to create,
it creates as much as available vlan ids.
Note the os_net add_trunk_subports is an atomic operation that will
either attach all or none of the subports. Therefore, if there is a
vlan id collision, all the created ports will be deleted and the
exception is raised.
"""
os_net = clients.get_network_client()
if trunk_ip:
parent_port = self._get_parent_port_by_host_ip(trunk_ip)
else:
parent_port = self._get_parent_port(pod)
trunk_id = self._get_trunk_id(parent_port)
port_rq, subports_info = self._create_subports_info(
pod, project_id, subnets, security_groups,
trunk_id, num_ports, unbound=True)
if not subports_info:
LOG.error("There are no vlan ids available to create subports")
return []
bulk_port_rq = [port_rq] * len(subports_info)
# restrict amount of create Ports in bulk that might be running
# in parallel.
with semaphore:
try:
ports = list(os_net.create_ports(bulk_port_rq))
except os_exc.SDKException:
for subport_info in subports_info:
self._release_vlan_id(subport_info['segmentation_id'])
LOG.exception("Error creating bulk ports: %s", bulk_port_rq)
raise
self._check_port_binding(ports)
if not self._tag_on_creation:
utils.tag_neutron_resources(ports)
for index, port in enumerate(ports):
subports_info[index]['port_id'] = port['id']
try:
try:
os_net.add_trunk_subports(trunk_id, subports_info)
except os_exc.ConflictException:
LOG.error("vlan ids already in use on trunk")
utils.delete_ports(ports)
for subport_info in subports_info:
self._release_vlan_id(subport_info['segmentation_id'])
return []
except os_exc.SDKException:
LOG.exception("Error happened during subport addition to trunk")
utils.delete_ports(ports)
for subport_info in subports_info:
self._release_vlan_id(subport_info['segmentation_id'])
return []
vifs = []
for index, port in enumerate(ports):
vlan_id = subports_info[index]['segmentation_id']
vif = ovu.neutron_to_osvif_vif_nested_vlan(port, subnets, vlan_id)
vifs.append(vif)
return vifs
def activate_vif(self, vif, pod=None, retry_info=None):
try:
super().activate_vif(vif)
except k_exc.PortNotReady:
if retry_info and retry_info.get('elapsed', 0) > ACTIVE_TIMEOUT:
parent_port = self._get_parent_port(pod)
trunk_id = self._get_trunk_id(parent_port)
# NOTE(dulek): We don't need a lock to prevent VLAN ID from
# being taken over because the SegmentationDriver
# will keep it reserved in memory unless we
# release it. And we won't.
LOG.warning('Subport %s is in DOWN status for more than %d '
'seconds. This is a Neutron issue. Attempting to '
'reattach the subport to trunk %s using VLAN ID %s'
' to fix it.', vif.id, retry_info['elapsed'],
trunk_id, vif.vlan_id)
try:
self._remove_subport(trunk_id, vif.id)
except os_exc.NotFoundException:
# NOTE(dulek): This may happen when _add_subport() failed
# or Kuryr crashed between the calls. Let's
# try to fix it hoping that VLAN ID is still
# free.
LOG.warning('Subport %s was not attached to the trunk. '
'Trying to attach it anyway.', vif.id)
self._add_subport(trunk_id, vif.id,
requested_vlan_id=vif.vlan_id)
LOG.warning("Reattached subport %s, its state will be "
"rechecked when event will be retried.", vif.id)
raise
def release_vif(self, pod, vif, project_id=None):
os_net = clients.get_network_client()
parent_port = self._get_parent_port(pod)
trunk_id = self._get_trunk_id(parent_port)
try:
self._remove_subport(trunk_id, vif.id)
except os_exc.NotFoundException:
pass
self._release_vlan_id(vif.vlan_id)
os_net.delete_port(vif.id)
def _get_port_request(self, pod, project_id, subnets, security_groups,
unbound=False):
port_req_body = {'project_id': project_id,
'network_id': utils.get_network_id(subnets),
'fixed_ips': ovu.osvif_to_neutron_fixed_ips(subnets),
'device_owner': kl_const.DEVICE_OWNER,
'admin_state_up': True}
# only set name if port_debug is enabled
if config.CONF.kubernetes.port_debug:
if unbound:
port_req_body['name'] = constants.KURYR_PORT_NAME
else:
port_req_body['name'] = utils.get_port_name(pod)
if security_groups:
port_req_body['security_groups'] = security_groups
if self._tag_on_creation:
tags = CONF.neutron_defaults.resource_tags
if tags:
port_req_body['tags'] = tags
return port_req_body
def _create_subports_info(self, pod, project_id, subnets,
security_groups, trunk_id, num_ports,
unbound=False):
subports_info = []
in_use_vlan_ids = self._get_in_use_vlan_ids_set(trunk_id)
port_rq = self._get_port_request(pod, project_id, subnets,
security_groups, unbound)
for _ in range(num_ports):
try:
vlan_id = seg_driver.allocate_segmentation_id(in_use_vlan_ids)
except kl_exc.SegmentationIdAllocationFailure:
LOG.warning("There is not enough vlan ids available to "
"create a batch of %d subports.", num_ports)
break
in_use_vlan_ids.add(vlan_id)
subports_info.append({'segmentation_id': vlan_id,
'port_id': '',
'segmentation_type': 'vlan'})
return port_rq, subports_info
def _get_trunk_id(self, port):
try:
return port['trunk_details']['trunk_id']
except (KeyError, TypeError):
LOG.error("Neutron port is missing trunk details. "
"Please ensure that k8s node port is associated "
"with a Neutron vlan trunk")
raise k_exc.K8sNodeTrunkPortFailure
def _add_subport(self, trunk_id, subport, requested_vlan_id=None):
"""Adds subport port to Neutron trunk
This method gets vlanid allocated from kuryr segmentation driver.
In active/active HA type deployment, possibility of vlanid conflict
is there. In such a case, vlanid will be requested again and subport
addition is re-tried. This is tried DEFAULT_MAX_RETRY_COUNT times in
case of vlanid conflict.
"""
# TODO(vikasc): Better approach for retrying in case of
# vlan-id conflict.
os_net = clients.get_network_client()
retry_count = 1
while True:
if requested_vlan_id:
vlan_id = requested_vlan_id
else:
try:
vlan_id = self._get_vlan_id(trunk_id)
except os_exc.SDKException:
LOG.error("Getting VLAN ID for subport on "
"trunk %s failed!!", trunk_id)
raise
subport = [{'segmentation_id': vlan_id,
'port_id': subport,
'segmentation_type': 'vlan'}]
try:
os_net.add_trunk_subports(trunk_id, subport)
except os_exc.ConflictException:
if (retry_count < DEFAULT_MAX_RETRY_COUNT and
not requested_vlan_id):
LOG.error("VLAN ID already in use on trunk %s. "
"Retrying.", trunk_id)
retry_count += 1
sleep(DEFAULT_RETRY_INTERVAL)
continue
else:
LOG.error("Failed to add subport %s to trunk %s due to "
"VLAN ID %d conflict.", subport, trunk_id,
vlan_id)
raise
except os_exc.SDKException:
self._release_vlan_id(vlan_id)
LOG.exception("Error happened during subport "
"addition to trunk %s", trunk_id)
raise
return vlan_id
def _remove_subports(self, trunk_id, subports_id):
os_net = clients.get_network_client()
subports_body = []
for subport_id in set(subports_id):
subports_body.append({'port_id': subport_id})
try:
os_net.delete_trunk_subports(trunk_id, subports_body)
except os_exc.NotFoundException:
if len(subports_id) > 1:
LOG.debug('Not Found on subport deletion, most likely some '
'subports in the list got detached already.')
raise # We don't know if all ports are detached, so raise.
# If single requested port is detached already, we're cool, ignore.
except os_exc.SDKException:
LOG.exception("Error happened during subport removal from "
"trunk %s", trunk_id)
raise
def _remove_subport(self, trunk_id, subport_id):
self._remove_subports(trunk_id, [subport_id])
def _get_vlan_id(self, trunk_id):
vlan_ids = self._get_in_use_vlan_ids_set(trunk_id)
return seg_driver.allocate_segmentation_id(vlan_ids)
def _release_vlan_id(self, id):
return seg_driver.release_segmentation_id(id)
def _get_in_use_vlan_ids_set(self, trunk_id):
vlan_ids = set()
os_net = clients.get_network_client()
trunk = os_net.get_trunk(trunk_id)
for port in trunk.sub_ports:
vlan_ids.add(port['segmentation_id'])
return vlan_ids