Merge "Add support for SR-IOV member ports"
This commit is contained in:
@@ -1215,6 +1215,14 @@ member-ids:
|
||||
in: body
|
||||
required: true
|
||||
type: array
|
||||
member_vnic_type:
|
||||
description: |
|
||||
The member vNIC type used for the member port. One of ``normal`` or
|
||||
``direct``.
|
||||
in: body
|
||||
required: true
|
||||
type: string
|
||||
min_version: 2.29
|
||||
members-status-object-list:
|
||||
description: |
|
||||
A list of members status objects.
|
||||
@@ -1479,6 +1487,14 @@ request_errors:
|
||||
in: body
|
||||
required: true
|
||||
type: integer
|
||||
request_sriov:
|
||||
description: |
|
||||
Request that an SR-IOV VF be used for the member network port. Defaults to
|
||||
``false``.
|
||||
in: body
|
||||
required: false
|
||||
type: boolean
|
||||
min_version: 2.29
|
||||
session_persistence:
|
||||
description: |
|
||||
A JSON object specifying the session persistence for the pool or ``null``
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"protocol_port": 80,
|
||||
"id": "957a1ace-1bd2-449b-8455-820b6e4b63f3",
|
||||
"operating_status": "NO_MONITOR",
|
||||
"tags": ["test_tag"]
|
||||
"tags": ["test_tag"],
|
||||
"vnic_type": "normal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"protocol_port": 80,
|
||||
"id": "957a1ace-1bd2-449b-8455-820b6e4b63f3",
|
||||
"operating_status": "NO_MONITOR",
|
||||
"tags": ["test_tag"]
|
||||
"tags": ["test_tag"],
|
||||
"vnic_type": "normal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"protocol_port": 80,
|
||||
"id": "957a1ace-1bd2-449b-8455-820b6e4b63f3",
|
||||
"operating_status": "NO_MONITOR",
|
||||
"tags": ["updated_tag"]
|
||||
"tags": ["updated_tag"],
|
||||
"vnic_type": "normal"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
"protocol_port": 80,
|
||||
"id": "957a1ace-1bd2-449b-8455-820b6e4b63f3",
|
||||
"operating_status": "NO_MONITOR",
|
||||
"tags": ["test_tag"]
|
||||
"tags": ["test_tag"],
|
||||
"vnic_type": "normal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ Response Parameters
|
||||
- tags: tags
|
||||
- updated_at: updated_at
|
||||
- weight: weight
|
||||
- vnic_type: member_vnic_type
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
@@ -156,6 +157,7 @@ Request
|
||||
- pool_id: path-pool-id
|
||||
- project_id: project_id-optional-deprecated
|
||||
- protocol_port: protocol_port
|
||||
- request_sriov: request_sriov
|
||||
- subnet_id: subnet_id-optional
|
||||
- tags: tags-optional
|
||||
- weight: weight-optional
|
||||
@@ -193,6 +195,7 @@ Response Parameters
|
||||
- tags: tags
|
||||
- updated_at: updated_at
|
||||
- weight: weight
|
||||
- vnic_type: member_vnic_type
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
@@ -260,6 +263,7 @@ Response Parameters
|
||||
- tags: tags
|
||||
- updated_at: updated_at
|
||||
- weight: weight
|
||||
- vnic_type: member_vnic_type
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
@@ -347,6 +351,7 @@ Response Parameters
|
||||
- tags: tags
|
||||
- updated_at: updated_at
|
||||
- weight: weight
|
||||
- vnic_type: member_vnic_type
|
||||
|
||||
Response Example
|
||||
----------------
|
||||
@@ -414,6 +419,7 @@ Request
|
||||
- pool_id: path-pool-id
|
||||
- project_id: project_id-optional-deprecated
|
||||
- protocol_port: protocol_port
|
||||
- request_sriov: request_sriov
|
||||
- subnet_id: subnet_id-optional
|
||||
- tags: tags-optional
|
||||
- weight: weight-optional
|
||||
|
||||
@@ -83,9 +83,20 @@ Octavia flavor that will use the compute flavor.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ openstack loadbalancer flavorprofile create --name amphora-sriov-profile --provider amphora --flavor-data '{"compute_flavor": "amphora-sriov-flavor", "sriov_vip": true}'
|
||||
$ openstack loadbalancer flavorprofile create --name amphora-sriov-profile --provider amphora --flavor-data '{"compute_flavor": "amphora-sriov-flavor", "sriov_vip": true, "allow_member_sriov": true}'
|
||||
$ openstack loadbalancer flavor create --name SRIOV-public-members --flavorprofile amphora-sriov-profile --description "A load balancer that uses SR-IOV for the 'public' network and 'members' network." --enable
|
||||
|
||||
When the `allow_member_sriov` Octavia flavor setting is true, users can request
|
||||
Octavia to attach the member ports using SR-IOV VFs. If Octavia is not able to
|
||||
successfully attach the member port as an SR-IOV VF, the member will be marked
|
||||
as `provisioning_status` of `ERROR` as we could not acquire a networking port
|
||||
for the requested member network. If the member network is already attached
|
||||
using a non-SR-IOV port, the member will also be marked with
|
||||
`provisioning_status` of `ERROR`.
|
||||
|
||||
.. note::
|
||||
By default, both `sriov_vip` and `allow_member_sriov` are false.
|
||||
|
||||
Building the Amphora Image
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -984,6 +984,12 @@ contain the following:
|
||||
| | | service existing connections. A valid |
|
||||
| | | value is from 0 to 256. Default is 1. |
|
||||
+-----------------------+--------+------------------------------------------+
|
||||
| vnic_type | string | The member vNIC type used for the member |
|
||||
| | | port. One of normal or direct. |
|
||||
+-----------------------+--------+------------------------------------------+
|
||||
|
||||
..note:: The vnic_type of normal and direct are the same as those defined by
|
||||
neutron ports.
|
||||
|
||||
Delete
|
||||
^^^^^^
|
||||
|
||||
@@ -406,7 +406,10 @@ class HaproxyAmphoraLoadBalancerDriver(
|
||||
fixed_ips.append(ip)
|
||||
port_info = {'mac_address': port.mac_address,
|
||||
'fixed_ips': fixed_ips,
|
||||
'mtu': port.network.mtu}
|
||||
'mtu': port.network.mtu,
|
||||
'is_sriov': False}
|
||||
if port.vnic_type == consts.VNIC_TYPE_DIRECT:
|
||||
port_info['is_sriov'] = True
|
||||
if port.id == amphora.vrrp_port_id:
|
||||
# We have to special-case sharing the vrrp port and pass through
|
||||
# enough extra information to populate the whole VIP port
|
||||
|
||||
@@ -53,5 +53,10 @@ SUPPORTED_FLAVOR_SCHEMA = {
|
||||
"description": "When true, the VIP port will be created using an "
|
||||
"SR-IOV VF port."
|
||||
},
|
||||
consts.ALLOW_MEMBER_SRIOV: {
|
||||
"type": "boolean",
|
||||
"description": "When true, users can request a member port be "
|
||||
"SR-IOV enabled at member creation time."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +462,12 @@ def db_members_to_provider_members(db_members):
|
||||
|
||||
def db_member_to_provider_member(db_member):
|
||||
new_member_dict = member_dict_to_provider_dict(db_member.to_dict())
|
||||
if constants.REQUEST_SRIOV in new_member_dict:
|
||||
request_sriov = new_member_dict.pop(constants.REQUEST_SRIOV)
|
||||
if request_sriov:
|
||||
new_member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_DIRECT
|
||||
else:
|
||||
new_member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_NORMAL
|
||||
return driver_dm.Member.from_dict(new_member_dict)
|
||||
|
||||
|
||||
|
||||
@@ -148,10 +148,13 @@ class RootController:
|
||||
# HTTP Strict Transport Security (HSTS)
|
||||
self._add_a_version(versions, 'v2.27', 'v2', 'SUPPORTED',
|
||||
'2023-05-05T00:00:00Z', host_url)
|
||||
# Add port vnic_type for SR-IOV
|
||||
# Add VIP port vnic_type for SR-IOV
|
||||
self._add_a_version(versions, 'v2.28', 'v2', 'SUPPORTED',
|
||||
'2023-11-08T00:00:00Z', host_url)
|
||||
# Add VIP SGs
|
||||
self._add_a_version(versions, 'v2.29', 'v2', 'CURRENT',
|
||||
self._add_a_version(versions, 'v2.29', 'v2', 'SUPPORTED',
|
||||
'2024-10-15T00:00:00Z', host_url)
|
||||
# Add member port SR-IOV support
|
||||
self._add_a_version(versions, 'v2.30', 'v2', 'CURRENT',
|
||||
'2025-02-26T00:00:00Z', host_url)
|
||||
return {'versions': versions}
|
||||
|
||||
@@ -19,6 +19,7 @@ from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import strutils
|
||||
from pecan import request as pecan_request
|
||||
from sqlalchemy.orm import exc as sa_exception
|
||||
from wsme import types as wtypes
|
||||
from wsmeext import pecan as wsme_pecan
|
||||
|
||||
@@ -146,10 +147,20 @@ class MemberController(base.BaseController):
|
||||
member = member_.member
|
||||
context = pecan_request.context.get('octavia_context')
|
||||
|
||||
flavor_dict = {}
|
||||
with context.session.begin():
|
||||
pool = self.repositories.pool.get(context.session, id=self.pool_id)
|
||||
member.project_id, provider = self._get_lb_project_id_provider(
|
||||
context.session, pool.load_balancer_id)
|
||||
if pool.load_balancer.flavor_id:
|
||||
try:
|
||||
flavor_dict = (
|
||||
self.repositories.flavor.get_flavor_metadata_dict(
|
||||
context.session, pool.load_balancer.flavor_id))
|
||||
except sa_exception.NoResultFound:
|
||||
LOG.error("load balancer has a flavor ID: %s that was not "
|
||||
"found in the database. Assuming no flavor.",
|
||||
pool.load_balancer.flavor_id)
|
||||
|
||||
self._auth_validate_action(context, member.project_id,
|
||||
constants.RBAC_POST)
|
||||
@@ -173,8 +184,23 @@ class MemberController(base.BaseController):
|
||||
raise exceptions.QuotaException(
|
||||
resource=data_models.Member._name())
|
||||
|
||||
member_dict = db_prepare.create_member(member.to_dict(
|
||||
render_unsets=True), self.pool_id, bool(pool.health_monitor))
|
||||
db_member_dict = member.to_dict(render_unsets=True)
|
||||
|
||||
# Validate and store port SR-IOV vnic_type
|
||||
request_sriov = db_member_dict.pop('request_sriov')
|
||||
if (request_sriov and not
|
||||
flavor_dict.get(constants.ALLOW_MEMBER_SRIOV, False)):
|
||||
raise exceptions.MemberSRIOVDisabled
|
||||
if request_sriov:
|
||||
db_member_dict[constants.VNIC_TYPE] = (
|
||||
constants.VNIC_TYPE_DIRECT)
|
||||
else:
|
||||
db_member_dict[constants.VNIC_TYPE] = (
|
||||
constants.VNIC_TYPE_NORMAL)
|
||||
|
||||
member_dict = db_prepare.create_member(db_member_dict,
|
||||
self.pool_id,
|
||||
bool(pool.health_monitor))
|
||||
|
||||
self._test_lb_and_listener_and_pool_statuses(context.session)
|
||||
|
||||
@@ -204,6 +230,28 @@ class MemberController(base.BaseController):
|
||||
|
||||
def _graph_create(self, lock_session, member_dict):
|
||||
pool = self.repositories.pool.get(lock_session, id=self.pool_id)
|
||||
|
||||
# Validate and store port SR-IOV vnic_type
|
||||
request_sriov = member_dict.pop('request_sriov')
|
||||
flavor_dict = {}
|
||||
if pool.load_balancer.flavor_id:
|
||||
try:
|
||||
flavor_dict = (
|
||||
self.repositories.flavor.get_flavor_metadata_dict(
|
||||
lock_session, pool.load_balancer.flavor_id))
|
||||
except sa_exception.NoResultFound:
|
||||
LOG.error("load balancer has a flavor ID: %s that was not "
|
||||
"found in the database. Assuming no flavor.",
|
||||
pool.load_balancer.flavor_id)
|
||||
if (request_sriov and not
|
||||
flavor_dict.get(constants.ALLOW_MEMBER_SRIOV, False)):
|
||||
raise exceptions.MemberSRIOVDisabled
|
||||
|
||||
if request_sriov:
|
||||
member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_DIRECT
|
||||
else:
|
||||
member_dict[constants.VNIC_TYPE] = constants.VNIC_TYPE_NORMAL
|
||||
|
||||
member_dict = db_prepare.create_member(
|
||||
member_dict, self.pool_id, bool(pool.health_monitor))
|
||||
db_member = self._validate_create_member(lock_session, member_dict)
|
||||
@@ -440,6 +488,11 @@ class MembersController(MemberController):
|
||||
m.project_id = db_pool.project_id
|
||||
db_member_dict = m.to_dict(render_unsets=False)
|
||||
db_member_dict.pop('id')
|
||||
# We don't allow updating the vnic_type
|
||||
# TODO(johnsom) Give the user an error once we change the
|
||||
# wsme type for batch member update to not use
|
||||
# the MemberPOST type
|
||||
db_member_dict.pop(constants.REQUEST_SRIOV)
|
||||
self.repositories.member.update(
|
||||
context.session, m.id, **db_member_dict)
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class MemberResponse(BaseMemberType):
|
||||
monitor_address = wtypes.wsattr(types.IPAddressType())
|
||||
monitor_port = wtypes.wsattr(wtypes.IntegerType())
|
||||
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType()))
|
||||
vnic_type = wtypes.wsattr(wtypes.StringType())
|
||||
|
||||
@classmethod
|
||||
def from_data_model(cls, data_model, children=False):
|
||||
@@ -85,6 +86,7 @@ class MemberPOST(BaseMemberType):
|
||||
default=None)
|
||||
monitor_address = wtypes.wsattr(types.IPAddressType(), default=None)
|
||||
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
|
||||
request_sriov = wtypes.wsattr(bool, default=False)
|
||||
|
||||
|
||||
class MemberRootPOST(types.BaseType):
|
||||
@@ -129,6 +131,7 @@ class MemberSingleCreate(BaseMemberType):
|
||||
minimum=constants.MIN_PORT_NUMBER, maximum=constants.MAX_PORT_NUMBER))
|
||||
monitor_address = wtypes.wsattr(types.IPAddressType())
|
||||
tags = wtypes.wsattr(wtypes.ArrayType(wtypes.StringType(max_length=255)))
|
||||
request_sriov = wtypes.wsattr(bool, default=False)
|
||||
|
||||
|
||||
class MemberStatusResponse(BaseMemberType):
|
||||
|
||||
@@ -423,6 +423,7 @@ REQ_CONN_TIMEOUT = 'req_conn_timeout'
|
||||
REQ_READ_TIMEOUT = 'req_read_timeout'
|
||||
REQUEST_ERRORS = 'request_errors'
|
||||
REQUEST_ID = 'request_id'
|
||||
REQUEST_SRIOV = 'request_sriov'
|
||||
ROLE = 'role'
|
||||
SECURITY_GROUP_IDS = 'security_group_ids'
|
||||
SECURITY_GROUPS = 'security_groups'
|
||||
@@ -588,6 +589,7 @@ ATTACH_PORT = 'attach-port'
|
||||
CALCULATE_AMPHORA_DELTA = 'calculate-amphora-delta'
|
||||
CREATE_VIP_BASE_PORT = 'create-vip-base-port'
|
||||
DELETE_AMPHORA = 'delete-amphora'
|
||||
DELETE_AMPHORA_MEMBER_PORTS = 'delete-amphora-member-ports'
|
||||
DELETE_PORT = 'delete-port'
|
||||
DISABLE_AMP_HEALTH_MONITORING = 'disable-amphora-health-monitoring'
|
||||
GET_AMPHORA_FIREWALL_RULES = 'get-amphora-firewall-rules'
|
||||
@@ -929,6 +931,7 @@ AMPHORA_SUPPORTED_ALPN_PROTOCOLS = [lib_consts.ALPN_PROTOCOL_HTTP_2,
|
||||
lib_consts.ALPN_PROTOCOL_HTTP_1_0]
|
||||
|
||||
SRIOV_VIP = 'sriov_vip'
|
||||
ALLOW_MEMBER_SRIOV = 'allow_member_sriov'
|
||||
|
||||
# Amphora interface fields
|
||||
IF_TYPE = 'if_type'
|
||||
|
||||
@@ -378,7 +378,7 @@ class Member(BaseDataModel):
|
||||
subnet_id=None, operating_status=None, pool=None,
|
||||
created_at=None, updated_at=None, provisioning_status=None,
|
||||
name=None, monitor_address=None, monitor_port=None,
|
||||
tags=None):
|
||||
tags=None, vnic_type=None):
|
||||
self.id = id
|
||||
self.project_id = project_id
|
||||
self.pool_id = pool_id
|
||||
@@ -397,6 +397,7 @@ class Member(BaseDataModel):
|
||||
self.monitor_address = monitor_address
|
||||
self.monitor_port = monitor_port
|
||||
self.tags = tags
|
||||
self.vnic_type = vnic_type
|
||||
|
||||
def delete(self):
|
||||
for mem in self.pool.members:
|
||||
@@ -904,3 +905,14 @@ class VipSecurityGroup(BaseDataModel):
|
||||
def __init__(self, load_balancer_id: str = None, sg_id: str = None):
|
||||
self.load_balancer_id = load_balancer_id
|
||||
self.sg_id = sg_id
|
||||
|
||||
|
||||
class AmphoraMemberPort(BaseDataModel):
|
||||
|
||||
def __init__(self, port_id=None, amphora_id=None, network_id=None,
|
||||
created_at=None, updated_at=None):
|
||||
self.port_id = port_id
|
||||
self.amphora_id = amphora_id
|
||||
self.network_id = network_id
|
||||
self.created_at = created_at
|
||||
self.updated_at = updated_at
|
||||
|
||||
@@ -435,3 +435,8 @@ class AmphoraNetworkConfigException(OctaviaException):
|
||||
class ListenerNoChildren(APIException):
|
||||
msg = _('Protocol %(protocol)s listeners cannot have child objects.')
|
||||
code = 400
|
||||
|
||||
|
||||
class MemberSRIOVDisabled(APIException):
|
||||
msg = _('The load balancer flavor does not allow SR-IOV member ports.')
|
||||
code = 400
|
||||
|
||||
@@ -125,13 +125,13 @@ class NoopManager:
|
||||
compute_id=compute_id))
|
||||
connection.commit()
|
||||
|
||||
# TODO(johnsom) Add vnic_type here
|
||||
return network_models.Interface(
|
||||
id=uuidutils.generate_uuid(),
|
||||
compute_id=compute_id,
|
||||
network_id=network_id,
|
||||
fixed_ips=[],
|
||||
port_id=uuidutils.generate_uuid(),
|
||||
vnic_type=constants.VNIC_TYPE_NORMAL
|
||||
)
|
||||
|
||||
def detach_port(self, compute_id, port_id):
|
||||
|
||||
@@ -207,6 +207,9 @@ class AmphoraFlows:
|
||||
name=constants.DELETE_AMPHORA + '-' + amphora_id,
|
||||
inject={constants.AMPHORA: amphora,
|
||||
constants.PASSIVE_FAILURE: True}))
|
||||
delete_amphora_flow.add(network_tasks.DeleteAmphoraMemberPorts(
|
||||
name=constants.DELETE_AMPHORA_MEMBER_PORTS + '-' + amphora_id,
|
||||
inject={constants.AMPHORA_ID: amphora[constants.ID]}))
|
||||
delete_amphora_flow.add(database_tasks.DisableAmphoraHealthMonitoring(
|
||||
name=constants.DISABLE_AMP_HEALTH_MONITORING + '-' + amphora_id,
|
||||
inject={constants.AMPHORA: amphora}))
|
||||
@@ -219,11 +222,6 @@ class AmphoraFlows:
|
||||
str(amphora[constants.VRRP_PORT_ID])),
|
||||
inject={constants.PORT_ID: amphora[constants.VRRP_PORT_ID],
|
||||
constants.PASSIVE_FAILURE: True}))
|
||||
# TODO(johnsom) What about cleaning up any member ports?
|
||||
# maybe we should get the list of attached ports prior to delete
|
||||
# and call delete on them here. Fix this as part of
|
||||
# https://storyboard.openstack.org/#!/story/2007077
|
||||
|
||||
return delete_amphora_flow
|
||||
|
||||
def get_vrrp_subflow(self, prefix, timeout_dict=None,
|
||||
|
||||
@@ -176,7 +176,8 @@ class ListenerFlows:
|
||||
|
||||
fw_rules_subflow.add(
|
||||
amphora_driver_tasks.AmphoraeGetConnectivityStatus(
|
||||
name=constants.AMPHORAE_GET_CONNECTIVITY_STATUS,
|
||||
name=(sf_name + '-' +
|
||||
constants.AMPHORAE_GET_CONNECTIVITY_STATUS),
|
||||
requires=constants.AMPHORAE,
|
||||
inject={constants.TIMEOUT_DICT: timeout_dict,
|
||||
constants.NEW_AMPHORA_ID: constants.NIL_UUID},
|
||||
|
||||
@@ -31,6 +31,7 @@ from octavia.controller.worker.v2.tasks import database_tasks
|
||||
from octavia.controller.worker.v2.tasks import lifecycle_tasks
|
||||
from octavia.controller.worker.v2.tasks import network_tasks
|
||||
from octavia.controller.worker.v2.tasks import notification_tasks
|
||||
from octavia.db import api as db_apis
|
||||
from octavia.db import repositories as repo
|
||||
|
||||
CONF = cfg.CONF
|
||||
@@ -41,6 +42,8 @@ class LoadBalancerFlows:
|
||||
|
||||
def __init__(self):
|
||||
self.amp_flows = amphora_flows.AmphoraFlows()
|
||||
self.amphora_repo = repo.AmphoraRepository()
|
||||
self.amphora_member_port_repo = repo.AmphoraMemberPortRepository()
|
||||
self.listener_flows = listener_flows.ListenerFlows()
|
||||
self.pool_flows = pool_flows.PoolFlows()
|
||||
self.member_flows = member_flows.MemberFlows()
|
||||
@@ -336,6 +339,9 @@ class LoadBalancerFlows:
|
||||
pools_delete = self._get_delete_pools_flow(pools)
|
||||
delete_LB_flow.add(pools_delete)
|
||||
delete_LB_flow.add(listeners_delete)
|
||||
member_ports_delete = self.get_delete_member_ports_subflow(
|
||||
lb[constants.LOADBALANCER_ID])
|
||||
delete_LB_flow.add(member_ports_delete)
|
||||
delete_LB_flow.add(network_tasks.UnplugVIP(
|
||||
requires=constants.LOADBALANCER))
|
||||
delete_LB_flow.add(network_tasks.DeallocateVIP(
|
||||
@@ -749,3 +755,30 @@ class LoadBalancerFlows:
|
||||
requires=constants.LOADBALANCER))
|
||||
|
||||
return failover_LB_flow
|
||||
|
||||
def get_delete_member_ports_subflow(self, load_balancer_id):
|
||||
"""A subflow that will delete all of the member ports on an LB
|
||||
|
||||
:param load_balancer_id: A load balancer ID
|
||||
:returns: A Taskflow flow
|
||||
"""
|
||||
port_delete_flow = unordered_flow.Flow('delete_member_ports')
|
||||
|
||||
session = db_apis.get_session()
|
||||
with session.begin():
|
||||
amps = self.amphora_repo.get_amphorae_ids_on_lb(session,
|
||||
load_balancer_id)
|
||||
for amp in amps:
|
||||
with session.begin():
|
||||
ports = self.amphora_member_port_repo.get_port_ids(session,
|
||||
amp)
|
||||
for port in ports:
|
||||
port_delete_flow.add(
|
||||
network_tasks.DeletePort(
|
||||
name='delete_member_port' + '-' + port,
|
||||
inject={constants.PORT_ID: port}))
|
||||
port_delete_flow.add(
|
||||
database_tasks.DeleteAmpMemberPortInDB(
|
||||
name='delete_member_port_in_db' + '-' + port,
|
||||
inject={constants.PORT_ID: port}))
|
||||
return port_delete_flow
|
||||
|
||||
@@ -55,6 +55,7 @@ class BaseDatabaseTask(task.Task):
|
||||
self.l7policy_repo = repo.L7PolicyRepository()
|
||||
self.l7rule_repo = repo.L7RuleRepository()
|
||||
self.task_utils = task_utilities.TaskUtils()
|
||||
self.amphora_member_port_repo = repo.AmphoraMemberPortRepository()
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _delete_from_amp_health(self, session, amphora_id):
|
||||
@@ -420,6 +421,21 @@ class DeleteL7RuleInDB(BaseDatabaseTask):
|
||||
'except': str(e)})
|
||||
|
||||
|
||||
class DeleteAmpMemberPortInDB(BaseDatabaseTask):
|
||||
"""Delete an amphora member port record in the DB."""
|
||||
|
||||
def execute(self, port_id):
|
||||
"""Delete the amphora member port in DB
|
||||
|
||||
:param port_id: The port_id to be deleted
|
||||
:returns: None
|
||||
"""
|
||||
|
||||
LOG.debug("Delete in DB for amphora member port %s", port_id)
|
||||
with db_apis.session().begin() as session:
|
||||
self.amphora_member_port_repo.delete(session, port_id=port_id)
|
||||
|
||||
|
||||
class ReloadAmphora(BaseDatabaseTask):
|
||||
"""Get an amphora object from the database."""
|
||||
|
||||
|
||||
@@ -17,16 +17,19 @@ import time
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import excutils
|
||||
from sqlalchemy.orm import exc as sa_exception
|
||||
from taskflow import task
|
||||
from taskflow.types import failure
|
||||
import tenacity
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import data_models
|
||||
from octavia.common import exceptions
|
||||
from octavia.common import utils
|
||||
from octavia.controller.worker import task_utils
|
||||
from octavia.db import api as db_apis
|
||||
from octavia.db import repositories as repo
|
||||
from octavia.i18n import _
|
||||
from octavia.network import base
|
||||
from octavia.network import data_models as n_data_models
|
||||
|
||||
@@ -43,6 +46,7 @@ class BaseNetworkTask(task.Task):
|
||||
self.task_utils = task_utils.TaskUtils()
|
||||
self.loadbalancer_repo = repo.LoadBalancerRepository()
|
||||
self.amphora_repo = repo.AmphoraRepository()
|
||||
self.amphora_member_port_repo = repo.AmphoraMemberPortRepository()
|
||||
|
||||
@property
|
||||
def network_driver(self):
|
||||
@@ -78,6 +82,7 @@ class CalculateAmphoraDelta(BaseNetworkTask):
|
||||
loadbalancer[constants.VIP_NETWORK_ID]
|
||||
}
|
||||
|
||||
net_vnic_type_map = {}
|
||||
for pool in db_lb.pools:
|
||||
for member in pool.members:
|
||||
if (member.subnet_id and
|
||||
@@ -85,6 +90,8 @@ class CalculateAmphoraDelta(BaseNetworkTask):
|
||||
constants.PENDING_DELETE):
|
||||
member_network = self.network_driver.get_subnet(
|
||||
member.subnet_id).network_id
|
||||
net_vnic_type_map[member_network] = getattr(
|
||||
member, 'vnic_type', constants.VNIC_TYPE_NORMAL)
|
||||
desired_subnet_to_net_map[member.subnet_id] = (
|
||||
member_network)
|
||||
|
||||
@@ -117,7 +124,8 @@ class CalculateAmphoraDelta(BaseNetworkTask):
|
||||
n_data_models.FixedIP(
|
||||
subnet_id=subnet_id)
|
||||
for subnet_id, net_id in desired_subnet_to_net_map.items()
|
||||
if net_id == add_net_id])
|
||||
if net_id == add_net_id],
|
||||
vnic_type=net_vnic_type_map[add_net_id])
|
||||
for add_net_id in add_ids]
|
||||
|
||||
# Calculate member Subnet deltas
|
||||
@@ -215,45 +223,6 @@ class GetPlumbedNetworks(BaseNetworkTask):
|
||||
amphora[constants.COMPUTE_ID])
|
||||
|
||||
|
||||
class PlugNetworks(BaseNetworkTask):
|
||||
"""Task to plug the networks.
|
||||
|
||||
This uses the delta to add all missing networks/nics
|
||||
"""
|
||||
|
||||
def execute(self, amphora, delta):
|
||||
"""Update the amphora networks for the delta."""
|
||||
|
||||
LOG.debug("Plug or unplug networks for amphora id: %s",
|
||||
amphora[constants.ID])
|
||||
|
||||
if not delta:
|
||||
LOG.debug("No network deltas for amphora id: %s",
|
||||
amphora[constants.ID])
|
||||
return
|
||||
|
||||
# add nics
|
||||
for nic in delta[constants.ADD_NICS]:
|
||||
self.network_driver.plug_network(amphora[constants.COMPUTE_ID],
|
||||
nic[constants.NETWORK_ID])
|
||||
|
||||
def revert(self, amphora, delta, *args, **kwargs):
|
||||
"""Handle a failed network plug by removing all nics added."""
|
||||
|
||||
LOG.warning("Unable to plug networks for amp id %s",
|
||||
amphora[constants.ID])
|
||||
if not delta:
|
||||
return
|
||||
|
||||
for nic in delta[constants.ADD_NICS]:
|
||||
try:
|
||||
self.network_driver.unplug_network(
|
||||
amphora[constants.COMPUTE_ID],
|
||||
nic[constants.NETWORK_ID])
|
||||
except base.NetworkNotFound:
|
||||
pass
|
||||
|
||||
|
||||
class UnPlugNetworks(BaseNetworkTask):
|
||||
"""Task to unplug the networks
|
||||
|
||||
@@ -316,6 +285,14 @@ class HandleNetworkDelta(BaseNetworkTask):
|
||||
fixed_ip.subnet = self.network_driver.get_subnet(
|
||||
fixed_ip.subnet_id)
|
||||
|
||||
def _cleanup_port(self, port_id, compute_id):
|
||||
try:
|
||||
self.network_driver.delete_port(port_id)
|
||||
except Exception:
|
||||
LOG.error(f'Unable to delete port {port_id} after failing to plug '
|
||||
f'the port into compute {compute_id}. This port '
|
||||
f'may now be abandoned in neutron.')
|
||||
|
||||
def execute(self, amphora, delta):
|
||||
"""Handle network plugging based off deltas."""
|
||||
session = db_apis.get_session()
|
||||
@@ -324,20 +301,43 @@ class HandleNetworkDelta(BaseNetworkTask):
|
||||
id=amphora.get(constants.ID))
|
||||
updated_ports = {}
|
||||
for nic in delta[constants.ADD_NICS]:
|
||||
network_id = nic[constants.NETWORK_ID]
|
||||
subnet_id = nic[constants.FIXED_IPS][0][constants.SUBNET_ID]
|
||||
interface = self.network_driver.plug_network(
|
||||
db_amp.compute_id, nic[constants.NETWORK_ID])
|
||||
port = self.network_driver.get_port(interface.port_id)
|
||||
# nova may plugged undesired subnets (it plugs one of the subnets
|
||||
# of the network), we can safely unplug the subnets we don't need,
|
||||
# the desired subnet will be added in the 'ADD_SUBNETS' loop.
|
||||
extra_subnets = [
|
||||
fixed_ip.subnet_id
|
||||
for fixed_ip in port.fixed_ips
|
||||
if fixed_ip.subnet_id != subnet_id]
|
||||
for subnet_id in extra_subnets:
|
||||
port = self.network_driver.unplug_fixed_ip(
|
||||
port_id=interface.port_id, subnet_id=subnet_id)
|
||||
|
||||
try:
|
||||
port = self.network_driver.create_port(
|
||||
network_id,
|
||||
name=f'octavia-lb-member-{amphora.get(constants.ID)}',
|
||||
vnic_type=nic[constants.VNIC_TYPE])
|
||||
except exceptions.NotFound as e:
|
||||
if 'Network' in str(e):
|
||||
raise base.NetworkNotFound(str(e))
|
||||
raise base.CreatePortException(str(e))
|
||||
except Exception as e:
|
||||
message = _(f'Error creating a port on network {network_id}. ')
|
||||
LOG.exception(message)
|
||||
raise base.CreatePortException(message) from e
|
||||
|
||||
try:
|
||||
self.network_driver.plug_port(db_amp, port)
|
||||
except exceptions.NotFound as e:
|
||||
self._cleanup_port(port.id, db_amp.compute_id)
|
||||
if 'Instance' in str(e):
|
||||
raise base.AmphoraNotFound(str(e))
|
||||
raise base.PlugNetworkException(str(e))
|
||||
except Exception as e:
|
||||
self._cleanup_port(port.id, db_amp.compute_id)
|
||||
message = _('Error plugging amphora (compute_id: '
|
||||
'{compute_id}) into network {network_id}.').format(
|
||||
compute_id=db_amp.compute_id, network_id=network_id)
|
||||
LOG.exception(message)
|
||||
raise base.PlugNetworkException(message) from e
|
||||
with session.begin():
|
||||
self.amphora_member_port_repo.create(
|
||||
session, port_id=port.id,
|
||||
amphora_id=amphora.get(constants.ID),
|
||||
network_id=network_id)
|
||||
|
||||
self._fill_port_info(port)
|
||||
updated_ports[port.network_id] = port.to_dict(recurse=True)
|
||||
|
||||
@@ -395,6 +395,14 @@ class HandleNetworkDelta(BaseNetworkTask):
|
||||
self.network_driver.delete_port(port_id)
|
||||
except Exception:
|
||||
LOG.exception("Unable to delete the port")
|
||||
try:
|
||||
with session.begin():
|
||||
self.amphora_member_port_repo.delete(session,
|
||||
port_id=port_id)
|
||||
except sa_exception.NoResultFound:
|
||||
# Passively fail here for upgrade compatibility
|
||||
LOG.warning("No Amphora member port records found for "
|
||||
"port_id: %s", port_id)
|
||||
|
||||
updated_ports.pop(network_id, None)
|
||||
return {amphora[constants.ID]: list(updated_ports.values())}
|
||||
@@ -969,6 +977,22 @@ class DeletePort(BaseNetworkTask):
|
||||
raise
|
||||
|
||||
|
||||
class DeleteAmphoraMemberPorts(BaseNetworkTask):
|
||||
"""Task to delete all of the member ports on an Amphora."""
|
||||
|
||||
def execute(self, amphora_id, passive_failure=False):
|
||||
delete_port = DeletePort()
|
||||
session = db_apis.get_session()
|
||||
|
||||
with session.begin():
|
||||
ports = self.amphora_member_port_repo.get_port_ids(
|
||||
session, amphora_id)
|
||||
for port in ports:
|
||||
delete_port.execute(port, passive_failure)
|
||||
with session.begin():
|
||||
self.amphora_member_port_repo.delete(session, port_id=port)
|
||||
|
||||
|
||||
class CreateVIPBasePort(BaseNetworkTask):
|
||||
"""Task to create the VIP base port for an amphora."""
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ class OctaviaBase(models.ModelBase):
|
||||
obj.load_balancer_id + obj.subnet_id)
|
||||
if obj.__class__.__name__ in ['VipSecurityGroup']:
|
||||
return obj.__class__.__name__ + obj.load_balancer_id + obj.sg_id
|
||||
if obj.__class__.__name__ in ['AmphoraMemberPort']:
|
||||
return obj.__class__.__name__ + obj.port_id
|
||||
raise NotImplementedError
|
||||
|
||||
def to_data_model(
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# 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.
|
||||
|
||||
"""Add member vnic_type
|
||||
|
||||
Revision ID: 8db7a6443785
|
||||
Revises: 3097e55493ae
|
||||
Create Date: 2024-03-29 20:34:37.263847
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
from octavia.common import constants
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '8db7a6443785'
|
||||
down_revision = '3097e55493ae'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column(
|
||||
u'member',
|
||||
sa.Column(u'vnic_type', sa.String(64), nullable=False,
|
||||
server_default=constants.VNIC_TYPE_NORMAL)
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
# 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.
|
||||
|
||||
"""add_member_port_table
|
||||
|
||||
Revision ID: fabf4983846b
|
||||
Revises: 8db7a6443785
|
||||
Create Date: 2024-08-30 23:12:01.713217
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fabf4983846b'
|
||||
down_revision = '8db7a6443785'
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.create_table(
|
||||
'amphora_member_port',
|
||||
sa.Column('port_id', sa.String(36), primary_key=True),
|
||||
sa.Column('amphora_id', sa.String(36), nullable=False, index=True),
|
||||
sa.Column('network_id', sa.String(36)),
|
||||
sa.Column('created_at', sa.DateTime()),
|
||||
sa.Column('updated_at', sa.DateTime())
|
||||
)
|
||||
op.create_foreign_key(
|
||||
'fk_member_port_amphora_id', 'amphora_member_port',
|
||||
'amphora', ['amphora_id'], ['id']
|
||||
)
|
||||
+19
-1
@@ -229,6 +229,7 @@ class Member(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
|
||||
nullable=False)
|
||||
enabled = sa.Column(sa.Boolean(), nullable=False)
|
||||
pool = orm.relationship("Pool", back_populates="members")
|
||||
vnic_type = sa.Column(sa.String(64), nullable=True)
|
||||
|
||||
_tags = orm.relationship(
|
||||
'Tags',
|
||||
@@ -246,7 +247,7 @@ class Member(base_models.BASE, base_models.IdMixin, base_models.ProjectMixin,
|
||||
f"ip_address={self.ip_address!r}, "
|
||||
f"protocol_port={self.protocol_port!r}, "
|
||||
f"operating_status={self.operating_status!r}, "
|
||||
f"weight={self.weight!r})")
|
||||
f"weight={self.weight!r}, vnic_type={self.vnic_type!r})")
|
||||
|
||||
|
||||
class HealthMonitor(base_models.BASE, base_models.IdMixin,
|
||||
@@ -1000,3 +1001,20 @@ class VipSecurityGroup(base_models.BASE):
|
||||
sa.ForeignKey("vip.load_balancer_id", name="fk_vip_sg_vip_lb_id"),
|
||||
nullable=False)
|
||||
sg_id = sa.Column(sa.String(64), nullable=False)
|
||||
|
||||
|
||||
class AmphoraMemberPort(base_models.BASE, models.TimestampMixin):
|
||||
|
||||
__data_model__ = data_models.AmphoraMemberPort
|
||||
|
||||
__tablename__ = "amphora_member_port"
|
||||
|
||||
port_id = sa.Column(
|
||||
sa.String(36),
|
||||
primary_key=True)
|
||||
amphora_id = sa.Column(
|
||||
sa.String(36),
|
||||
sa.ForeignKey("amphora.id", name="fk_member_port_amphora_id"),
|
||||
nullable=False)
|
||||
network_id = sa.Column(
|
||||
sa.String(36))
|
||||
|
||||
@@ -256,6 +256,7 @@ class Repositories:
|
||||
self.flavor_profile = FlavorProfileRepository()
|
||||
self.availability_zone = AvailabilityZoneRepository()
|
||||
self.availability_zone_profile = AvailabilityZoneProfileRepository()
|
||||
self.amphora_member_port = AmphoraMemberPortRepository()
|
||||
|
||||
def create_load_balancer_and_vip(self, session, lb_dict, vip_dict,
|
||||
additional_vip_dicts=None):
|
||||
@@ -1454,6 +1455,20 @@ class AmphoraRepository(BaseRepository):
|
||||
amp.status = consts.PENDING_DELETE
|
||||
lock_session.flush()
|
||||
|
||||
def get_amphorae_ids_on_lb(self, session, lb_id):
|
||||
"""Returns a list of amphora IDs associated with the load balancer
|
||||
|
||||
:param session: A Sql Alchemy database session.
|
||||
:param lb_id: A load balancer ID.
|
||||
:returns: A list of amphora IDs
|
||||
"""
|
||||
return session.scalars(
|
||||
select(
|
||||
self.model_class.id
|
||||
).where(
|
||||
self.model_class.load_balancer_id == lb_id
|
||||
)).all()
|
||||
|
||||
|
||||
class AmphoraBuildReqRepository(BaseRepository):
|
||||
model_class = models.AmphoraBuildRequest
|
||||
@@ -2133,3 +2148,15 @@ class AvailabilityZoneRepository(_GetALLExceptDELETEDIdMixin, BaseRepository):
|
||||
class AvailabilityZoneProfileRepository(_GetALLExceptDELETEDIdMixin,
|
||||
BaseRepository):
|
||||
model_class = models.AvailabilityZoneProfile
|
||||
|
||||
|
||||
class AmphoraMemberPortRepository(BaseRepository):
|
||||
model_class = models.AmphoraMemberPort
|
||||
|
||||
def get_port_ids(self, session, amphora_id):
|
||||
return session.scalars(
|
||||
select(
|
||||
self.model_class.port_id
|
||||
).where(
|
||||
self.model_class.amphora_id == amphora_id
|
||||
)).all()
|
||||
|
||||
@@ -163,16 +163,6 @@ class AbstractNetworkDriver(metaclass=abc.ABCMeta):
|
||||
:raises: UnplugVIPException, PluggedVIPNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def plug_network(self, compute_id, network_id):
|
||||
"""Connects an existing amphora to an existing network.
|
||||
|
||||
:param compute_id: id of an amphora in the compute service
|
||||
:param network_id: id of a network
|
||||
:return: octavia.network.data_models.Interface instance
|
||||
:raises: PlugNetworkException, AmphoraNotFound, NetworkNotFound
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def unplug_network(self, compute_id, network_id):
|
||||
"""Disconnects an existing amphora from an existing network.
|
||||
|
||||
@@ -19,12 +19,13 @@ from octavia.common import data_models
|
||||
class Interface(data_models.BaseDataModel):
|
||||
|
||||
def __init__(self, id=None, compute_id=None, network_id=None,
|
||||
fixed_ips=None, port_id=None):
|
||||
fixed_ips=None, port_id=None, vnic_type=None):
|
||||
self.id = id
|
||||
self.compute_id = compute_id
|
||||
self.network_id = network_id
|
||||
self.port_id = port_id
|
||||
self.fixed_ips = fixed_ips
|
||||
self.vnic_type = vnic_type
|
||||
|
||||
|
||||
class Delta(data_models.BaseDataModel):
|
||||
|
||||
@@ -655,26 +655,6 @@ class AllowedAddressPairsDriver(neutron_base.BaseNeutronDriver):
|
||||
load_balancer.amphorae):
|
||||
self.unplug_aap_port(vip, amphora, subnet)
|
||||
|
||||
def plug_network(self, compute_id, network_id):
|
||||
try:
|
||||
interface = self.compute.attach_network_or_port(
|
||||
compute_id=compute_id, network_id=network_id)
|
||||
except exceptions.NotFound as e:
|
||||
if 'Instance' in str(e):
|
||||
raise base.AmphoraNotFound(str(e))
|
||||
if 'Network' in str(e):
|
||||
raise base.NetworkNotFound(str(e))
|
||||
raise base.PlugNetworkException(str(e))
|
||||
except Exception as e:
|
||||
message = _('Error plugging amphora (compute_id: {compute_id}) '
|
||||
'into network {network_id}.').format(
|
||||
compute_id=compute_id,
|
||||
network_id=network_id)
|
||||
LOG.exception(message)
|
||||
raise base.PlugNetworkException(message) from e
|
||||
|
||||
return self._nova_interface_to_octavia_interface(compute_id, interface)
|
||||
|
||||
def unplug_network(self, compute_id, network_id):
|
||||
interfaces = self.get_plugged_networks(compute_id)
|
||||
if not interfaces:
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from openstack.network.v2.network_ip_availability import NetworkIPAvailability
|
||||
|
||||
from octavia.network import data_models as network_models
|
||||
@@ -52,7 +51,8 @@ def convert_port_to_model(port):
|
||||
admin_state_up=port.is_admin_state_up,
|
||||
fixed_ips=fixed_ips,
|
||||
qos_policy_id=port.qos_policy_id,
|
||||
security_group_ids=port.security_group_ids
|
||||
security_group_ids=port.security_group_ids,
|
||||
vnic_type=port.binding_vnic_type
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -198,37 +198,15 @@ class NoopManager:
|
||||
vip.ip_address)] = (vip, amphora, subnet,
|
||||
'unplug_aap_port')
|
||||
|
||||
def plug_network(self, compute_id, network_id):
|
||||
LOG.debug("Network %s no-op, plug_network compute_id %s, network_id "
|
||||
"%s", self.__class__.__name__, compute_id,
|
||||
network_id)
|
||||
self.networkconfigconfig[(compute_id, network_id)] = (
|
||||
compute_id, network_id, 'plug_network')
|
||||
interface = network_models.Interface(
|
||||
id=uuidutils.generate_uuid(),
|
||||
compute_id=compute_id,
|
||||
network_id=network_id,
|
||||
fixed_ips=[],
|
||||
port_id=uuidutils.generate_uuid()
|
||||
)
|
||||
_NOOP_MANAGER_VARS['ports'][interface.port_id] = (
|
||||
network_models.Port(
|
||||
id=interface.port_id,
|
||||
network_id=network_id))
|
||||
_NOOP_MANAGER_VARS['interfaces'][(network_id, compute_id)] = (
|
||||
interface)
|
||||
return interface
|
||||
|
||||
def unplug_network(self, compute_id, network_id):
|
||||
LOG.debug("Network %s no-op, unplug_network compute_id %s, "
|
||||
"network_id %s",
|
||||
self.__class__.__name__, compute_id, network_id)
|
||||
self.networkconfigconfig[(compute_id, network_id)] = (
|
||||
compute_id, network_id, 'unplug_network')
|
||||
_NOOP_MANAGER_VARS['interfaces'].pop((network_id, compute_id), None)
|
||||
|
||||
def get_plugged_networks(self, compute_id):
|
||||
LOG.debug("Network %s no-op, get_plugged_networks amphora_id %s",
|
||||
LOG.debug("Network %s no-op, get_plugged_networks compute_id %s",
|
||||
self.__class__.__name__, compute_id)
|
||||
self.networkconfigconfig[compute_id] = (
|
||||
compute_id, 'get_plugged_networks')
|
||||
@@ -253,11 +231,11 @@ class NoopManager:
|
||||
subnet_id=fixed_ip.subnet_id,
|
||||
ip_address=fixed_ip.ip_address))
|
||||
# Add the interface object to the list
|
||||
# TODO(johnsom) Fix this to have the vnic_type
|
||||
interfaces.append(network_models.Interface(
|
||||
compute_id=interface.compute_id,
|
||||
network_id=interface.network_id,
|
||||
port_id=interface.port_id, fixed_ips=fixed_ips))
|
||||
port_id=interface.port_id, fixed_ips=fixed_ips,
|
||||
vnic_type=interface.vnic_type))
|
||||
return interfaces
|
||||
|
||||
def update_vip(self, loadbalancer, for_delete=False):
|
||||
@@ -589,9 +567,6 @@ class NoopNetworkDriver(driver_base.AbstractNetworkDriver):
|
||||
def unplug_vip(self, loadbalancer, vip):
|
||||
self.driver.unplug_vip(loadbalancer, vip)
|
||||
|
||||
def plug_network(self, compute_id, network_id):
|
||||
return self.driver.plug_network(compute_id, network_id)
|
||||
|
||||
def unplug_network(self, compute_id, network_id):
|
||||
self.driver.unplug_network(compute_id, network_id)
|
||||
|
||||
|
||||
@@ -139,7 +139,8 @@ MOCK_NEUTRON_PORT = Port(**{'network_id': MOCK_NETWORK_ID,
|
||||
'mac_address': MOCK_MAC_ADDR,
|
||||
'fixed_ips': [{'ip_address': MOCK_IP_ADDRESS,
|
||||
'subnet_id': MOCK_SUBNET_ID}],
|
||||
'security_groups': [MOCK_SECURITY_GROUP_ID]})
|
||||
'security_groups': [MOCK_SECURITY_GROUP_ID],
|
||||
'binding_vnic_type': constants.VNIC_TYPE_NORMAL})
|
||||
MOCK_NEUTRON_QOS_POLICY_ID = 'mock-qos-id'
|
||||
MOCK_QOS_POLICY_ID1 = 'qos1-id'
|
||||
MOCK_QOS_POLICY_ID2 = 'qos2-id'
|
||||
|
||||
@@ -148,7 +148,8 @@ class SampleDriverDataModels:
|
||||
constants.CREATED_AT: self.created_at,
|
||||
constants.UPDATED_AT: self.updated_at,
|
||||
lib_consts.MONITOR_ADDRESS: '192.0.2.26',
|
||||
lib_consts.MONITOR_PORT: 81}
|
||||
lib_consts.MONITOR_PORT: 81,
|
||||
lib_consts.VNIC_TYPE: lib_consts.VNIC_TYPE_NORMAL}
|
||||
|
||||
self.test_member1_dict.update(self._common_test_dict)
|
||||
|
||||
@@ -157,6 +158,8 @@ class SampleDriverDataModels:
|
||||
self.test_member2_dict[constants.IP_ADDRESS] = '192.0.2.17'
|
||||
self.test_member2_dict[lib_consts.MONITOR_ADDRESS] = '192.0.2.27'
|
||||
self.test_member2_dict[lib_consts.NAME] = 'member2'
|
||||
self.test_member2_dict[
|
||||
lib_consts.VNIC_TYPE] = lib_consts.VNIC_TYPE_DIRECT
|
||||
|
||||
self.test_member3_dict = copy.deepcopy(self.test_member1_dict)
|
||||
self.test_member3_dict[lib_consts.ID] = self.member3_id
|
||||
@@ -185,24 +188,28 @@ class SampleDriverDataModels:
|
||||
self.db_pool1_members = [self.db_member1, self.db_member2]
|
||||
self.db_pool2_members = [self.db_member3, self.db_member4]
|
||||
|
||||
self.provider_member1_dict = {lib_consts.ADDRESS: '192.0.2.16',
|
||||
lib_consts.ADMIN_STATE_UP: True,
|
||||
lib_consts.MEMBER_ID: self.member1_id,
|
||||
lib_consts.MONITOR_ADDRESS: '192.0.2.26',
|
||||
lib_consts.MONITOR_PORT: 81,
|
||||
lib_consts.NAME: 'member1',
|
||||
lib_consts.POOL_ID: self.pool1_id,
|
||||
lib_consts.PROJECT_ID: self.project_id,
|
||||
lib_consts.PROTOCOL_PORT: 80,
|
||||
lib_consts.SUBNET_ID: self.subnet_id,
|
||||
lib_consts.WEIGHT: 0,
|
||||
lib_consts.BACKUP: False}
|
||||
self.provider_member1_dict = {
|
||||
lib_consts.ADDRESS: '192.0.2.16',
|
||||
lib_consts.ADMIN_STATE_UP: True,
|
||||
lib_consts.MEMBER_ID: self.member1_id,
|
||||
lib_consts.MONITOR_ADDRESS: '192.0.2.26',
|
||||
lib_consts.MONITOR_PORT: 81,
|
||||
lib_consts.NAME: 'member1',
|
||||
lib_consts.POOL_ID: self.pool1_id,
|
||||
lib_consts.PROJECT_ID: self.project_id,
|
||||
lib_consts.PROTOCOL_PORT: 80,
|
||||
lib_consts.SUBNET_ID: self.subnet_id,
|
||||
lib_consts.WEIGHT: 0,
|
||||
lib_consts.BACKUP: False,
|
||||
lib_consts.VNIC_TYPE: lib_consts.VNIC_TYPE_NORMAL}
|
||||
|
||||
self.provider_member2_dict = copy.deepcopy(self.provider_member1_dict)
|
||||
self.provider_member2_dict[lib_consts.MEMBER_ID] = self.member2_id
|
||||
self.provider_member2_dict[lib_consts.ADDRESS] = '192.0.2.17'
|
||||
self.provider_member2_dict[lib_consts.MONITOR_ADDRESS] = '192.0.2.27'
|
||||
self.provider_member2_dict[lib_consts.NAME] = 'member2'
|
||||
self.provider_member2_dict[
|
||||
lib_consts.VNIC_TYPE] = lib_consts.VNIC_TYPE_DIRECT
|
||||
|
||||
self.provider_member3_dict = copy.deepcopy(self.provider_member1_dict)
|
||||
self.provider_member3_dict[lib_consts.MEMBER_ID] = self.member3_id
|
||||
|
||||
@@ -503,7 +503,8 @@ class DriverAgentTest(base.OctaviaDBTestBase):
|
||||
lib_consts.MONITOR_PORT,
|
||||
lib_consts.SUBNET_ID,
|
||||
lib_consts.WEIGHT,
|
||||
lib_consts.BACKUP):
|
||||
lib_consts.BACKUP,
|
||||
lib_consts.VNIC_TYPE):
|
||||
self.assertEqual(provider_member_dict.get(key),
|
||||
result_member_dict.get(key))
|
||||
|
||||
|
||||
@@ -2837,7 +2837,7 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
|
||||
def _get_lb_bodies(self, create_listeners, expected_listeners,
|
||||
create_pools=None, additional_vips=None,
|
||||
vip_sg_ids=None):
|
||||
vip_sg_ids=None, flavor_id=None, sriov=False):
|
||||
create_lb = {
|
||||
'name': 'lb1',
|
||||
'project_id': self._project_id,
|
||||
@@ -2871,6 +2871,11 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
'vip_vnic_type': constants.VNIC_TYPE_NORMAL,
|
||||
'vip_sg_ids': vip_sg_ids or [],
|
||||
}
|
||||
if flavor_id:
|
||||
create_lb['flavor_id'] = flavor_id
|
||||
expected_lb['flavor_id'] = flavor_id
|
||||
if sriov:
|
||||
expected_lb['vip_vnic_type'] = constants.VNIC_TYPE_DIRECT
|
||||
expected_lb.update(create_lb)
|
||||
expected_lb['listeners'] = expected_listeners
|
||||
expected_lb['pools'] = create_pools or []
|
||||
@@ -3043,7 +3048,7 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
expected_pool['healthmonitor'] = expected_hm
|
||||
return create_pool, expected_pool
|
||||
|
||||
def _get_member_bodies(self, protocol_port=80):
|
||||
def _get_member_bodies(self, protocol_port=80, sriov=False):
|
||||
create_member = {
|
||||
'address': '10.0.0.1',
|
||||
'protocol_port': protocol_port
|
||||
@@ -3056,6 +3061,9 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
'project_id': self._project_id,
|
||||
'tags': []
|
||||
}
|
||||
if sriov:
|
||||
create_member[constants.REQUEST_SRIOV] = True
|
||||
expected_member[constants.VNIC_TYPE] = constants.VNIC_TYPE_DIRECT
|
||||
expected_member.update(create_member)
|
||||
return create_member, expected_member
|
||||
|
||||
@@ -3396,6 +3404,47 @@ class TestLoadBalancerGraph(base.BaseAPITest):
|
||||
api_lb = response.json.get(self.root_tag)
|
||||
self._assert_graphs_equal(expected_lb, api_lb)
|
||||
|
||||
def test_with_one_listener_one_member_sriov(self):
|
||||
flavor_profile = self.create_flavor_profile(
|
||||
'sriov-graph-create', 'noop_driver',
|
||||
f'{{"{constants.ALLOW_MEMBER_SRIOV}": true, '
|
||||
f'"{constants.SRIOV_VIP}": true}}')
|
||||
|
||||
flavor = self.create_flavor('sriov-graph-create', '',
|
||||
flavor_profile['id'], True)
|
||||
|
||||
create_member, expected_member = self._get_member_bodies(sriov=True)
|
||||
create_pool, expected_pool = self._get_pool_bodies(
|
||||
create_members=[create_member],
|
||||
expected_members=[expected_member])
|
||||
create_listener, expected_listener = self._get_listener_bodies(
|
||||
create_default_pool_name=create_pool['name'])
|
||||
create_lb, expected_lb = self._get_lb_bodies(
|
||||
create_listeners=[create_listener],
|
||||
expected_listeners=[expected_listener],
|
||||
create_pools=[create_pool], flavor_id=flavor['id'], sriov=True)
|
||||
body = self._build_body(create_lb)
|
||||
response = self.post(self.LBS_PATH, body)
|
||||
api_lb = response.json.get(self.root_tag)
|
||||
self._assert_graphs_equal(expected_lb, api_lb)
|
||||
|
||||
def test_with_one_listener_one_member_sriov_disabled(self):
|
||||
create_member, expected_member = self._get_member_bodies(sriov=True)
|
||||
create_pool, expected_pool = self._get_pool_bodies(
|
||||
create_members=[create_member],
|
||||
expected_members=[expected_member])
|
||||
create_listener, expected_listener = self._get_listener_bodies(
|
||||
create_default_pool_name=create_pool['name'])
|
||||
create_lb, expected_lb = self._get_lb_bodies(
|
||||
create_listeners=[create_listener],
|
||||
expected_listeners=[expected_listener],
|
||||
create_pools=[create_pool])
|
||||
body = self._build_body(create_lb)
|
||||
response = self.post(self.LBS_PATH, body, status=400)
|
||||
error_text = response.json.get('faultstring')
|
||||
self.assertIn('flavor does not allow SR-IOV member ports.',
|
||||
error_text)
|
||||
|
||||
def test_with_one_listener_one_hm(self):
|
||||
create_hm, expected_hm = self._get_hm_bodies()
|
||||
create_pool, expected_pool = self._get_pool_bodies(
|
||||
|
||||
@@ -18,6 +18,7 @@ from octavia_lib.api.drivers import data_models as driver_dm
|
||||
from oslo_config import cfg
|
||||
from oslo_config import fixture as oslo_fixture
|
||||
from oslo_utils import uuidutils
|
||||
from sqlalchemy.orm import exc as sa_exception
|
||||
|
||||
from octavia.api.drivers import utils as driver_utils
|
||||
from octavia.common import constants
|
||||
@@ -623,6 +624,55 @@ class TestMember(base.BaseAPITest):
|
||||
self.assertIn('Provider \'bad_driver\' reports error: broken',
|
||||
response.get('faultstring'))
|
||||
|
||||
def test_create_with_sriov(self):
|
||||
flavor_profile = self.create_flavor_profile(
|
||||
'sriov-member-create', 'noop_driver',
|
||||
f'{{"{constants.ALLOW_MEMBER_SRIOV}": true}}')
|
||||
|
||||
flavor = self.create_flavor('sriov-member-create', '',
|
||||
flavor_profile['id'], True)
|
||||
|
||||
vip_subnet_id = uuidutils.generate_uuid()
|
||||
lb = self.create_load_balancer(vip_subnet_id, flavor_id=flavor['id'])
|
||||
lb_id = lb.get('loadbalancer').get('id')
|
||||
self.set_lb_status(lb_id)
|
||||
|
||||
listener = self.create_listener(
|
||||
constants.PROTOCOL_HTTP, 80,
|
||||
lb_id=lb_id)
|
||||
listener_id = listener.get('listener').get('id')
|
||||
self.set_lb_status(lb_id)
|
||||
|
||||
pool_with_listener = self.create_pool(
|
||||
lb_id, constants.PROTOCOL_HTTP,
|
||||
constants.LB_ALGORITHM_ROUND_ROBIN, listener_id=listener_id)
|
||||
pool_with_listener_id = pool_with_listener.get('pool').get('id')
|
||||
self.set_lb_status(lb_id)
|
||||
|
||||
api_member = self.create_member(
|
||||
pool_with_listener_id, '192.0.2.1',
|
||||
80, request_sriov=True).get(self.root_tag)
|
||||
self.assertEqual(constants.VNIC_TYPE_DIRECT,
|
||||
api_member[constants.VNIC_TYPE])
|
||||
|
||||
def test_create_with_sriov_disabled(self):
|
||||
# Test with no flavor enabling SR-IOV members
|
||||
response = self.create_member(
|
||||
self.pool_id, '192.0.2.1',
|
||||
80, request_sriov=True, status=400)
|
||||
self.assertIn('flavor does not allow SR-IOV member ports',
|
||||
response.get('faultstring'))
|
||||
|
||||
@mock.patch('octavia.db.repositories.FlavorRepository.'
|
||||
'get_flavor_metadata_dict')
|
||||
def test_create_with_sriov_missing_flavor(self, mock_get_flavor):
|
||||
mock_get_flavor.side_effect = sa_exception.NoResultFound()
|
||||
response = self.create_member(
|
||||
self.pool_id, '192.0.2.1',
|
||||
80, request_sriov=True, status=400)
|
||||
self.assertIn('flavor does not allow SR-IOV member ports',
|
||||
response.get('faultstring'))
|
||||
|
||||
@mock.patch('octavia.api.drivers.driver_factory.get_driver')
|
||||
@mock.patch('octavia.api.drivers.utils.call_provider')
|
||||
def test_full_batch_members(self, mock_provider, mock_get_driver):
|
||||
|
||||
@@ -289,7 +289,8 @@ class MemberModelTest(base.OctaviaDBTestBase, ModelTestMixin):
|
||||
f"project_id={member.project_id!r}, "
|
||||
f"provisioning_status='ACTIVE', "
|
||||
f"ip_address='10.0.0.1', protocol_port=80, "
|
||||
f"operating_status='ONLINE', weight=None)",
|
||||
f"operating_status='ONLINE', weight=None, "
|
||||
f"vnic_type=None)",
|
||||
str(member))
|
||||
|
||||
self.assertIsNotNone(member.created_at)
|
||||
|
||||
@@ -135,7 +135,7 @@ class AllRepositoriesTest(base.OctaviaDBTestBase):
|
||||
'amp_build_slots', 'amp_build_req', 'quotas',
|
||||
'flavor', 'flavor_profile', 'listener_cidr',
|
||||
'availability_zone', 'availability_zone_profile',
|
||||
'additional_vip')
|
||||
'additional_vip', 'amphora_member_port')
|
||||
for repo_attr in repo_attr_names:
|
||||
single_repo = getattr(self.repos, repo_attr, None)
|
||||
message = (f"Class Repositories should have {repo_attr} instance "
|
||||
|
||||
@@ -780,7 +780,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
self.driver.clients[API_VERSION].plug_network.assert_called_once_with(
|
||||
self.amp, dict(mac_address=FAKE_MAC_ADDRESS,
|
||||
fixed_ips=[],
|
||||
mtu=FAKE_MTU))
|
||||
mtu=FAKE_MTU,
|
||||
is_sriov=False))
|
||||
|
||||
self.driver.clients[API_VERSION].plug_network.reset_mock()
|
||||
|
||||
@@ -792,7 +793,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
subnet_cidr='198.51.100.0/24',
|
||||
host_routes=[],
|
||||
gateway=FAKE_GATEWAY)],
|
||||
mtu=FAKE_MTU))
|
||||
mtu=FAKE_MTU, is_sriov=False))
|
||||
|
||||
self.driver.clients[API_VERSION].plug_network.reset_mock()
|
||||
|
||||
@@ -819,7 +820,7 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
additional_vips=[],
|
||||
mtu=FAKE_MTU,
|
||||
is_sriov=False
|
||||
)))
|
||||
), is_sriov=False))
|
||||
|
||||
def test_post_network_plug_with_host_routes(self):
|
||||
SUBNET_ID = 'SUBNET_ID'
|
||||
@@ -859,7 +860,8 @@ class TestHaproxyAmphoraLoadBalancerDriverTest(base.TestCase):
|
||||
self.driver.clients[API_VERSION].plug_network.assert_called_once_with(
|
||||
self.amp, dict(mac_address=FAKE_MAC_ADDRESS,
|
||||
fixed_ips=expected_fixed_ips,
|
||||
mtu=FAKE_MTU))
|
||||
mtu=FAKE_MTU,
|
||||
is_sriov=False))
|
||||
|
||||
def test_get_haproxy_versions(self):
|
||||
ref_haproxy_versions = ['1', '6']
|
||||
|
||||
@@ -18,6 +18,7 @@ from oslo_config import cfg
|
||||
from oslo_config import fixture as oslo_fixture
|
||||
from oslo_utils import uuidutils
|
||||
from taskflow.patterns import linear_flow as flow
|
||||
from taskflow.patterns import unordered_flow
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.common import exceptions
|
||||
@@ -544,3 +545,27 @@ class TestLoadBalancerFlows(base.TestCase):
|
||||
|
||||
self._test_get_failover_LB_flow_no_amps_act_stdby([amphora_dict,
|
||||
amphora2_dict])
|
||||
|
||||
@mock.patch('octavia.db.repositories.AmphoraMemberPortRepository.'
|
||||
'get_port_ids')
|
||||
@mock.patch('octavia.db.repositories.AmphoraRepository.'
|
||||
'get_amphorae_ids_on_lb')
|
||||
@mock.patch('octavia.db.api.get_session', return_value=mock.MagicMock())
|
||||
def test_get_delete_member_ports_subflow(self,
|
||||
mock_session,
|
||||
mock_amps_on_lb,
|
||||
mock_get_port_ids,
|
||||
mock_get_net_driver):
|
||||
lb_id = uuidutils.generate_uuid()
|
||||
amps = ['fake_amp1', 'fake_amp2']
|
||||
port1 = uuidutils.generate_uuid()
|
||||
port2 = uuidutils.generate_uuid()
|
||||
ports = [port1, port2]
|
||||
|
||||
mock_amps_on_lb.return_value = amps
|
||||
mock_get_port_ids.return_value = ports
|
||||
|
||||
delete_flow = self.LBFlow.get_delete_member_ports_subflow(lb_id)
|
||||
|
||||
self.assertIsInstance(delete_flow, unordered_flow.Flow)
|
||||
self.assertEqual(8, len(delete_flow))
|
||||
|
||||
@@ -334,6 +334,7 @@ class TestNetworkTasks(base.TestCase):
|
||||
mock_driver.reset_mock()
|
||||
member_mock = mock.MagicMock()
|
||||
member_mock.subnet_id = member_private_subnet.id
|
||||
member_mock.vnic_type = constants.VNIC_TYPE_NORMAL
|
||||
member2_mock = mock.MagicMock()
|
||||
member2_mock.subnet_id = member_private_subnet2.id
|
||||
pool_mock.members = [member_mock]
|
||||
@@ -351,7 +352,8 @@ class TestNetworkTasks(base.TestCase):
|
||||
network_id=3,
|
||||
fixed_ips=[
|
||||
data_models.FixedIP(
|
||||
subnet_id=member_private_subnet.id)])],
|
||||
subnet_id=member_private_subnet.id)],
|
||||
vnic_type=constants.VNIC_TYPE_NORMAL)],
|
||||
delete_nics=[],
|
||||
add_subnets=[{
|
||||
'subnet_id': member_private_subnet.id,
|
||||
@@ -431,6 +433,7 @@ class TestNetworkTasks(base.TestCase):
|
||||
mock_driver.reset_mock()
|
||||
member_mock = mock.MagicMock()
|
||||
member_mock.subnet_id = member_private_subnet.id
|
||||
member_mock.vnic_type = constants.VNIC_TYPE_NORMAL
|
||||
pool_mock.members = [member_mock]
|
||||
az = {
|
||||
constants.COMPUTE_ZONE: 'foo'
|
||||
@@ -451,7 +454,8 @@ class TestNetworkTasks(base.TestCase):
|
||||
add_nics=[data_models.Interface(
|
||||
network_id=member_private_net.id,
|
||||
fixed_ips=[data_models.FixedIP(
|
||||
subnet_id=member_private_subnet.id)])],
|
||||
subnet_id=member_private_subnet.id)],
|
||||
vnic_type=constants.VNIC_TYPE_NORMAL)],
|
||||
delete_nics=[data_models.Interface(network_id='bad_net')],
|
||||
add_subnets=[{
|
||||
'subnet_id': member_private_subnet.id,
|
||||
@@ -611,63 +615,6 @@ class TestNetworkTasks(base.TestCase):
|
||||
mock_driver.get_plugged_networks.assert_called_once_with(
|
||||
COMPUTE_ID)
|
||||
|
||||
def test_plug_networks(self, mock_get_net_driver):
|
||||
mock_driver = mock.MagicMock()
|
||||
mock_get_net_driver.return_value = mock_driver
|
||||
|
||||
def _interface(network_id):
|
||||
return [data_models.Interface(network_id=network_id)]
|
||||
|
||||
net = network_tasks.PlugNetworks()
|
||||
|
||||
net.execute(self.amphora_mock, None)
|
||||
self.assertFalse(mock_driver.plug_network.called)
|
||||
|
||||
delta = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
add_nics=[],
|
||||
delete_nics=[]).to_dict(recurse=True)
|
||||
net.execute(self.amphora_mock, delta)
|
||||
self.assertFalse(mock_driver.plug_network.called)
|
||||
|
||||
delta = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
add_nics=_interface(1),
|
||||
delete_nics=[]).to_dict(recurse=True)
|
||||
net.execute(self.amphora_mock, delta)
|
||||
mock_driver.plug_network.assert_called_once_with(COMPUTE_ID, 1)
|
||||
|
||||
# revert
|
||||
net.revert(self.amphora_mock, None)
|
||||
self.assertFalse(mock_driver.unplug_network.called)
|
||||
|
||||
delta = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
add_nics=[],
|
||||
delete_nics=[]).to_dict(recurse=True)
|
||||
net.revert(self.amphora_mock, delta)
|
||||
self.assertFalse(mock_driver.unplug_network.called)
|
||||
|
||||
delta = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
add_nics=_interface(1),
|
||||
delete_nics=[]).to_dict(recurse=True)
|
||||
net.revert(self.amphora_mock, delta)
|
||||
mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1)
|
||||
|
||||
mock_driver.reset_mock()
|
||||
mock_driver.unplug_network.side_effect = net_base.NetworkNotFound
|
||||
net.revert(self.amphora_mock, delta) # No exception
|
||||
mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1)
|
||||
|
||||
mock_driver.reset_mock()
|
||||
mock_driver.unplug_network.side_effect = TestException('test')
|
||||
self.assertRaises(TestException,
|
||||
net.revert,
|
||||
self.amphora_mock,
|
||||
delta)
|
||||
mock_driver.unplug_network.assert_called_once_with(COMPUTE_ID, 1)
|
||||
|
||||
def test_unplug_networks(self, mock_get_net_driver):
|
||||
mock_driver = mock.MagicMock()
|
||||
mock_get_net_driver.return_value = mock_driver
|
||||
@@ -755,6 +702,7 @@ class TestNetworkTasks(base.TestCase):
|
||||
nic1.fixed_ips = [data_models.FixedIP(
|
||||
subnet_id=uuidutils.generate_uuid())]
|
||||
nic1.network_id = uuidutils.generate_uuid()
|
||||
nic1.nic_type = constants.VNIC_TYPE_NORMAL
|
||||
nic2 = data_models.Interface()
|
||||
nic2.fixed_ips = [data_models.FixedIP(
|
||||
subnet_id=uuidutils.generate_uuid())]
|
||||
@@ -779,8 +727,18 @@ class TestNetworkTasks(base.TestCase):
|
||||
delete_subnets=[]
|
||||
).to_dict(recurse=True)
|
||||
|
||||
mock_net_driver.plug_network.return_value = interface1
|
||||
mock_net_driver.get_port.return_value = port1
|
||||
mock_net_driver.plug_port.side_effect = [
|
||||
interface1,
|
||||
exceptions.NotFound(resource='Instance', id='1'),
|
||||
exceptions.NotFound(resource='Boom', id='1'),
|
||||
Exception("boom")]
|
||||
mock_net_driver.create_port.side_effect = [
|
||||
port1,
|
||||
port1,
|
||||
exceptions.NotFound(resource='Network', id='1'),
|
||||
port1,
|
||||
port1]
|
||||
|
||||
fixed_port1 = mock.MagicMock()
|
||||
fixed_port1.network_id = port1.network_id
|
||||
fixed_port1.fixed_ips = [fixed_ip]
|
||||
@@ -795,20 +753,34 @@ class TestNetworkTasks(base.TestCase):
|
||||
result = handle_net_delta_obj.execute(self.amphora_mock,
|
||||
delta)
|
||||
|
||||
mock_net_driver.plug_network.assert_called_once_with(
|
||||
self.db_amphora_mock.compute_id, nic1.network_id)
|
||||
mock_net_driver.unplug_fixed_ip.assert_called_once_with(
|
||||
port_id=interface1.port_id, subnet_id=fixed_ip2.subnet_id)
|
||||
mock_net_driver.get_port.assert_called_once_with(interface1.port_id)
|
||||
mock_net_driver.plug_port.assert_called_once_with(
|
||||
self.db_amphora_mock, port1)
|
||||
mock_net_driver.get_network.assert_called_once_with(port1.network_id)
|
||||
mock_net_driver.get_subnet.assert_called_once_with(fixed_ip.subnet_id)
|
||||
mock_net_driver.get_subnet.assert_has_calls(
|
||||
[mock.call(fixed_ip.subnet_id), mock.call(fixed_ip2.subnet_id)])
|
||||
|
||||
self.assertEqual({self.db_amphora_mock.id: [fixed_port1.to_dict()]},
|
||||
self.assertEqual({self.db_amphora_mock.id: [port1.to_dict()]},
|
||||
result)
|
||||
|
||||
mock_net_driver.unplug_network.assert_called_with(
|
||||
self.db_amphora_mock.compute_id, nic2.network_id)
|
||||
|
||||
self.assertRaises(net_base.AmphoraNotFound,
|
||||
handle_net_delta_obj.execute, self.amphora_mock,
|
||||
delta)
|
||||
|
||||
self.assertRaises(net_base.NetworkNotFound,
|
||||
handle_net_delta_obj.execute, self.amphora_mock,
|
||||
delta)
|
||||
|
||||
self.assertRaises(net_base.PlugNetworkException,
|
||||
handle_net_delta_obj.execute, self.amphora_mock,
|
||||
delta)
|
||||
|
||||
self.assertRaises(net_base.PlugNetworkException,
|
||||
handle_net_delta_obj.execute, self.amphora_mock,
|
||||
delta)
|
||||
|
||||
# Revert
|
||||
delta2 = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
@@ -857,12 +829,13 @@ class TestNetworkTasks(base.TestCase):
|
||||
port_id=port_id,
|
||||
fixed_ips=[
|
||||
data_models.FixedIP(
|
||||
subnet_id=subnet_id)])
|
||||
subnet_id=subnet_id)],
|
||||
vnic_type=constants.VNIC_TYPE_NORMAL)
|
||||
|
||||
net = network_tasks.HandleNetworkDeltas()
|
||||
|
||||
net.execute({}, self.load_balancer_mock)
|
||||
self.assertFalse(mock_driver.plug_network.called)
|
||||
self.assertFalse(mock_driver.create_port.called)
|
||||
|
||||
delta = data_models.Delta(amphora_id=self.db_amphora_mock.id,
|
||||
compute_id=self.db_amphora_mock.compute_id,
|
||||
@@ -871,7 +844,7 @@ class TestNetworkTasks(base.TestCase):
|
||||
add_subnets=[],
|
||||
delete_subnets=[]).to_dict(recurse=True)
|
||||
net.execute({self.db_amphora_mock.id: delta}, self.load_balancer_mock)
|
||||
self.assertFalse(mock_driver.plug_network.called)
|
||||
self.assertFalse(mock_driver.create_port.called)
|
||||
|
||||
# Adding a subnet on a new network
|
||||
port = data_models.Port(
|
||||
@@ -879,7 +852,7 @@ class TestNetworkTasks(base.TestCase):
|
||||
network_id=network1,
|
||||
fixed_ips=[
|
||||
data_models.FixedIP(subnet_id=subnet1)])
|
||||
mock_driver.get_port.return_value = port
|
||||
mock_driver.create_port.return_value = port
|
||||
mock_driver.plug_fixed_ip.return_value = port
|
||||
mock_driver.get_network.return_value = data_models.Network(
|
||||
id=network1)
|
||||
@@ -900,8 +873,8 @@ class TestNetworkTasks(base.TestCase):
|
||||
delete_subnets=[]).to_dict(recurse=True)
|
||||
updated_ports = net.execute({self.db_amphora_mock.id: delta},
|
||||
self.load_balancer_mock)
|
||||
mock_driver.plug_network.assert_called_once_with(
|
||||
self.db_amphora_mock.compute_id, network1)
|
||||
mock_driver.plug_port.assert_called_once_with(
|
||||
self.db_amphora_mock, port)
|
||||
mock_driver.unplug_network.assert_not_called()
|
||||
|
||||
self.assertEqual(1, len(updated_ports))
|
||||
@@ -949,7 +922,6 @@ class TestNetworkTasks(base.TestCase):
|
||||
delete_subnets=[]).to_dict(recurse=True)
|
||||
updated_ports = net.execute({self.db_amphora_mock.id: delta},
|
||||
self.load_balancer_mock)
|
||||
mock_driver.plug_network.assert_not_called()
|
||||
mock_driver.unplug_network.assert_not_called()
|
||||
mock_driver.get_port.assert_not_called()
|
||||
mock_driver.plug_fixed_ip.assert_called_once_with(port_id=port1,
|
||||
@@ -989,7 +961,6 @@ class TestNetworkTasks(base.TestCase):
|
||||
updated_ports = net.execute({self.db_amphora_mock.id: delta},
|
||||
self.load_balancer_mock)
|
||||
mock_driver.delete_port.assert_not_called()
|
||||
mock_driver.plug_network.assert_not_called()
|
||||
mock_driver.plug_fixed_ip.assert_not_called()
|
||||
mock_driver.unplug_fixed_ip.assert_called_once_with(
|
||||
port_id=port1, subnet_id=subnet1)
|
||||
@@ -1014,7 +985,6 @@ class TestNetworkTasks(base.TestCase):
|
||||
net.execute({self.db_amphora_mock.id: delta},
|
||||
self.load_balancer_mock)
|
||||
mock_driver.delete_port.assert_not_called()
|
||||
mock_driver.plug_network.assert_not_called()
|
||||
mock_driver.plug_fixed_ip.assert_not_called()
|
||||
mock_driver.unplug_fixed_ip.assert_not_called()
|
||||
|
||||
@@ -1043,7 +1013,6 @@ class TestNetworkTasks(base.TestCase):
|
||||
updated_ports = net.execute({self.db_amphora_mock.id: delta},
|
||||
self.load_balancer_mock)
|
||||
mock_driver.delete_port.assert_called_once_with(port1)
|
||||
mock_driver.plug_network.assert_not_called()
|
||||
mock_driver.plug_fixed_ip.assert_not_called()
|
||||
self.assertEqual(1, len(updated_ports))
|
||||
self.assertEqual(0, len(updated_ports[self.db_amphora_mock.id]))
|
||||
|
||||
@@ -996,47 +996,6 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
mock_unplug_network.assert_called_once_with(
|
||||
lb.amphorae[0].compute_id, subnet.network_id)
|
||||
|
||||
def test_plug_network_when_compute_instance_cant_be_found(self):
|
||||
net_id = t_constants.MOCK_NOVA_INTERFACE.net_id
|
||||
network_attach = self.driver.compute.attach_network_or_port
|
||||
network_attach.side_effect = exceptions.NotFound(
|
||||
resource='Instance not found', id=1)
|
||||
self.assertRaises(network_base.AmphoraNotFound,
|
||||
self.driver.plug_network,
|
||||
t_constants.MOCK_COMPUTE_ID, net_id)
|
||||
|
||||
def test_plug_network_when_network_cant_be_found(self):
|
||||
net_id = t_constants.MOCK_NOVA_INTERFACE.net_id
|
||||
network_attach = self.driver.compute.attach_network_or_port
|
||||
network_attach.side_effect = nova_exceptions.NotFound(
|
||||
404, message='Network not found')
|
||||
self.assertRaises(network_base.NetworkException,
|
||||
self.driver.plug_network,
|
||||
t_constants.MOCK_COMPUTE_ID, net_id)
|
||||
|
||||
def test_plug_network_when_interface_attach_fails(self):
|
||||
net_id = t_constants.MOCK_NOVA_INTERFACE.net_id
|
||||
network_attach = self.driver.compute.attach_network_or_port
|
||||
network_attach.side_effect = TypeError
|
||||
self.assertRaises(network_base.PlugNetworkException,
|
||||
self.driver.plug_network,
|
||||
t_constants.MOCK_COMPUTE_ID, net_id)
|
||||
|
||||
def test_plug_network(self):
|
||||
net_id = t_constants.MOCK_NOVA_INTERFACE.net_id
|
||||
network_attach = self.driver.compute.attach_network_or_port
|
||||
network_attach.return_value = t_constants.MOCK_NOVA_INTERFACE
|
||||
oct_interface = self.driver.plug_network(
|
||||
t_constants.MOCK_COMPUTE_ID, net_id)
|
||||
exp_ips = [fixed_ip.get('ip_address')
|
||||
for fixed_ip in t_constants.MOCK_NOVA_INTERFACE.fixed_ips]
|
||||
actual_ips = [fixed_ip.ip_address
|
||||
for fixed_ip in oct_interface.fixed_ips]
|
||||
self.assertEqual(exp_ips, actual_ips)
|
||||
self.assertEqual(t_constants.MOCK_COMPUTE_ID,
|
||||
oct_interface.compute_id)
|
||||
self.assertEqual(net_id, oct_interface.network_id)
|
||||
|
||||
def test_unplug_network_when_compute_port_cant_be_found(self):
|
||||
net_id = t_constants.MOCK_NOVA_INTERFACE.net_id
|
||||
list_ports = self.driver.network_proxy.ports
|
||||
@@ -1614,7 +1573,8 @@ class TestAllowedAddressPairsDriver(base.TestCase):
|
||||
'fixed_ips': [{'ip_address': IP_ADDRESS1,
|
||||
'subnet_id': SUBNET1_ID}],
|
||||
'security_groups': [],
|
||||
'qos_policy_id': QOS_POLICY_ID})
|
||||
'qos_policy_id': QOS_POLICY_ID,
|
||||
'binding_vnic_type': constants.VNIC_TYPE_NORMAL})
|
||||
|
||||
reference_port_dict = {'admin_state_up': ADMIN_STATE_UP,
|
||||
'device_id': t_constants.MOCK_DEVICE_ID,
|
||||
|
||||
@@ -15,6 +15,7 @@ from unittest import mock
|
||||
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from octavia.common import constants
|
||||
from octavia.db import models
|
||||
from octavia.network import data_models as network_models
|
||||
from octavia.network.drivers.noop_driver import driver
|
||||
@@ -49,12 +50,12 @@ class TestNoopNetworkDriver(base.TestCase):
|
||||
self.network_id = self.FAKE_UUID_3
|
||||
self.network_name = 'net1'
|
||||
self.device_id = self.FAKE_UUID_4
|
||||
self.ip_address = "10.0.0.2"
|
||||
self.ip_address = "192.0.2.2"
|
||||
self.load_balancer = models.LoadBalancer()
|
||||
self.load_balancer.id = self.FAKE_UUID_2
|
||||
|
||||
self.vip = models.Vip()
|
||||
self.vip.ip_address = "10.0.0.1"
|
||||
self.vip.ip_address = "192.0.2.1"
|
||||
self.vip.subnet_id = uuidutils.generate_uuid()
|
||||
self.vip.port_id = uuidutils.generate_uuid()
|
||||
self.amphora_id = self.FAKE_UUID_1
|
||||
@@ -70,15 +71,15 @@ class TestNoopNetworkDriver(base.TestCase):
|
||||
self.amphora1.compute_id = self.compute_id
|
||||
self.amphora1.vrrp_port_id = uuidutils.generate_uuid()
|
||||
self.amphora1.ha_port_id = uuidutils.generate_uuid()
|
||||
self.amphora1.vrrp_ip = '10.0.1.10'
|
||||
self.amphora1.ha_ip = '10.0.1.11'
|
||||
self.amphora1.vrrp_ip = '192.0.2.10'
|
||||
self.amphora1.ha_ip = '192.0.2.11'
|
||||
self.amphora2 = models.Amphora()
|
||||
self.amphora2.id = uuidutils.generate_uuid()
|
||||
self.amphora2.compute_id = self.compute2_id
|
||||
self.amphora2.vrrp_port_id = uuidutils.generate_uuid()
|
||||
self.amphora2.ha_port_id = uuidutils.generate_uuid()
|
||||
self.amphora2.vrrp_ip = '10.0.2.10'
|
||||
self.amphora2.ha_ip = '10.0.2.11'
|
||||
self.amphora2.vrrp_ip = '192.0.2.20'
|
||||
self.amphora2.ha_ip = '192.0.2.21'
|
||||
self.load_balancer.amphorae = [self.amphora1, self.amphora2]
|
||||
self.load_balancer.vip = self.vip
|
||||
self.subnet = mock.MagicMock()
|
||||
@@ -119,12 +120,6 @@ class TestNoopNetworkDriver(base.TestCase):
|
||||
self.driver.driver.networkconfigconfig[(
|
||||
self.load_balancer.id, self.vip.ip_address)])
|
||||
|
||||
def test_plug_network(self):
|
||||
self.driver.plug_network(self.compute_id, self.network_id)
|
||||
self.assertEqual((self.compute_id, self.network_id, 'plug_network'),
|
||||
self.driver.driver.networkconfigconfig[(
|
||||
self.compute_id, self.network_id)])
|
||||
|
||||
def test_unplug_network(self):
|
||||
self.driver.unplug_network(self.compute_id, self.network_id)
|
||||
self.assertEqual((self.compute_id, self.network_id, 'unplug_network'),
|
||||
@@ -132,53 +127,40 @@ class TestNoopNetworkDriver(base.TestCase):
|
||||
self.compute_id, self.network_id)])
|
||||
|
||||
def test_get_plugged_networks(self):
|
||||
self.driver.get_plugged_networks(self.compute_id)
|
||||
self.assertEqual((self.compute_id, 'get_plugged_networks'),
|
||||
self.driver.driver.networkconfigconfig[(
|
||||
self.compute_id)])
|
||||
interface_db_mock = mock.MagicMock()
|
||||
interface_db_mock.port_id = self.port_id
|
||||
interface_db_mock.network_id = self.network_id
|
||||
interface_db_mock.compute_id = self.compute_id
|
||||
interface_db_mock.vnic_type = constants.VNIC_TYPE_NORMAL
|
||||
|
||||
def test_plug_unplug_and_get_plugged_networks(self):
|
||||
amphora = mock.MagicMock()
|
||||
amphora.compute_id = uuidutils.generate_uuid()
|
||||
|
||||
network = self.driver.plug_network(amphora.compute_id,
|
||||
self.network_id)
|
||||
self.assertEqual(
|
||||
network,
|
||||
network_models.Interface(
|
||||
id=mock.ANY,
|
||||
compute_id=amphora.compute_id,
|
||||
network_id=self.network_id,
|
||||
fixed_ips=[],
|
||||
port_id=mock.ANY
|
||||
))
|
||||
fixed_ips_db_mock = mock.MagicMock()
|
||||
fixed_ips_db_mock.port_id = self.port_id
|
||||
fixed_ips_db_mock.subnet_id = self.subnet_id
|
||||
fixed_ips_db_mock.ip_address = self.ip_address
|
||||
|
||||
# mock out the sqlite db calls
|
||||
connect_mock = mock.MagicMock()
|
||||
connection_mock = mock.MagicMock()
|
||||
self.mock_engine.connect.return_value = connect_mock
|
||||
connect_mock.__enter__.return_value = connection_mock
|
||||
|
||||
mock_interface = mock.MagicMock()
|
||||
mock_interface.compute_id = amphora.compute_id
|
||||
mock_interface.network_id = self.network_id
|
||||
mock_interface.port_id = '1'
|
||||
connection_mock.execute.side_effect = [[mock_interface], [], []]
|
||||
connection_mock.execute.side_effect = [[interface_db_mock],
|
||||
[fixed_ips_db_mock]]
|
||||
|
||||
networks = self.driver.get_plugged_networks(amphora.compute_id)
|
||||
result = self.driver.get_plugged_networks(self.compute_id)
|
||||
|
||||
self.assertEqual(
|
||||
networks,
|
||||
[network_models.Interface(
|
||||
id=mock.ANY,
|
||||
compute_id=amphora.compute_id,
|
||||
network_id=self.network_id,
|
||||
fixed_ips=[],
|
||||
port_id=mock.ANY
|
||||
)])
|
||||
self.driver.unplug_network(amphora.compute_id,
|
||||
self.network_id)
|
||||
networks = self.driver.get_plugged_networks(amphora.compute_id)
|
||||
self.assertEqual([], networks)
|
||||
self.assertEqual((self.compute_id, 'get_plugged_networks'),
|
||||
self.driver.driver.networkconfigconfig[(
|
||||
self.compute_id)])
|
||||
|
||||
expected_fixed_ips = [network_models.FixedIP(
|
||||
subnet_id=self.subnet_id, ip_address=self.ip_address)]
|
||||
expected_interfaces = [network_models.Interface(
|
||||
compute_id=self.compute_id, network_id=self.network_id,
|
||||
port_id=self.port_id, fixed_ips=expected_fixed_ips,
|
||||
vnic_type=constants.VNIC_TYPE_NORMAL)]
|
||||
|
||||
self.assertEqual(expected_interfaces, result)
|
||||
|
||||
def test_update_vip(self):
|
||||
self.driver.update_vip(self.load_balancer)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Octavia Amphora based load balancers now support using SR-IOV virtual
|
||||
functions (VF) on the member ports.
|
||||
upgrade:
|
||||
- |
|
||||
You must update the amphora image to support the SR-IOV member port
|
||||
feature.
|
||||
+1
-1
@@ -45,7 +45,7 @@ castellan>=0.16.0 # Apache-2.0
|
||||
tenacity>=5.0.4 # Apache-2.0
|
||||
distro>=1.2.0 # Apache-2.0
|
||||
jsonschema>=3.2.0 # MIT
|
||||
octavia-lib>=3.3.0 # Apache-2.0
|
||||
octavia-lib>=3.6.0 # Apache-2.0
|
||||
setproctitle>=1.1.10 # BSD
|
||||
python-dateutil>=2.7.0 # BSD
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
import graphviz
|
||||
from taskflow import engines
|
||||
@@ -94,11 +95,19 @@ def generate(flow_list, output_directory):
|
||||
{constants.POOL_ID:
|
||||
'08ada7a2-3eff-42c6-bdd8-b6f2ecd73358'}]
|
||||
lb = dmh.generate_load_balancer()
|
||||
if 'v2' in current_tuple[0]:
|
||||
lb = utils.lb_dict_to_provider_dict(lb.to_dict())
|
||||
delete_flow = get_flow_method(lb, listeners, pools)
|
||||
else:
|
||||
delete_flow, store = get_flow_method(lb)
|
||||
with patch('octavia.db.repositories.AmphoraRepository.'
|
||||
'get_amphorae_ids_on_lb',
|
||||
return_value=[
|
||||
'a9aa2b0b-0442-471e-b400-e04847e3ef1f']):
|
||||
with patch('octavia.db.repositories.'
|
||||
'AmphoraMemberPortRepository.get_port_ids',
|
||||
return_value=[
|
||||
'6e03e9ad-726a-46ee-90e0-1cad753ba1b0']):
|
||||
if 'v2' in current_tuple[0]:
|
||||
lb = utils.lb_dict_to_provider_dict(lb.to_dict())
|
||||
delete_flow = get_flow_method(lb, listeners, pools)
|
||||
else:
|
||||
delete_flow, store = get_flow_method(lb)
|
||||
current_engine = engines.load(delete_flow)
|
||||
elif (current_tuple[1] == 'LoadBalancerFlows' and
|
||||
current_tuple[2] == 'get_failover_LB_flow'):
|
||||
|
||||
Reference in New Issue
Block a user