Adds ability to run appliance with user-specified drivers/images
This allow users to specify the driver and image associated with a network function. This is dependent on the correct API extension being loaded into neutron from the astara-neutron repo. Depends-on: I6b6f98e8ae89c704f45b05f87f17ebed5a70fc1d Partially-implements: blueprint astara-sfc Co-authored by: Mark McClain <mark@mcclain.xyz> Change-Id: I1c349e56fd23d1aa95c7f8c0da68d25246b0ceb2
This commit is contained in:
parent
0cfd325867
commit
2aabf4b185
|
@ -527,6 +527,12 @@ class AstaraExtClientWrapper(client.Client):
|
|||
}
|
||||
)
|
||||
|
||||
def list_byonfs(self, retrieve_all=True, **_params):
|
||||
return self.list('byonfs', '/byonf', retrieve_all, **_params).get(
|
||||
'byonfs',
|
||||
[]
|
||||
)
|
||||
|
||||
|
||||
class L3PluginApi(object):
|
||||
|
||||
|
@ -870,6 +876,7 @@ class Neutron(object):
|
|||
|
||||
if ports:
|
||||
port = Port.from_dict(ports[0])
|
||||
|
||||
device_name = driver.get_device_name(port)
|
||||
driver.unplug(device_name)
|
||||
|
||||
|
@ -887,3 +894,14 @@ class Neutron(object):
|
|||
|
||||
def clear_device_id(self, port):
|
||||
self.api_client.update_port(port.id, {'port': {'device_id': ''}})
|
||||
|
||||
def tenant_has_byo_for_function(self, tenant_id, function_type):
|
||||
retval = self.api_client.list_byonfs(
|
||||
function_type=function_type,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
if retval:
|
||||
LOG.debug(
|
||||
'Found BYONF for tenant %s with function %s',
|
||||
tenant_id, function_type)
|
||||
return retval[0]
|
||||
|
|
|
@ -62,6 +62,25 @@ def get(requested_driver):
|
|||
)
|
||||
|
||||
|
||||
def load_from_byonf(worker_context, byonf_result, resource_id):
|
||||
""""Returns a loaded driver based on astara-neutron BYONF response
|
||||
|
||||
:param worker_context: Worker context with clients
|
||||
:param byonf_result: dict response from neutron API describing
|
||||
user-provided NF info (specifically image_uuid and
|
||||
driver)
|
||||
:param resource_id: The UUID of the logical resource derived from the
|
||||
notification message
|
||||
|
||||
Responsible for also setting correct driver attributes based on BYONF
|
||||
specs.
|
||||
"""
|
||||
driver_obj = get(byonf_result['driver'])(worker_context, resource_id)
|
||||
if byonf_result.get('image_uuid'):
|
||||
driver_obj.image_uuid = byonf_result['image_uuid']
|
||||
return driver_obj
|
||||
|
||||
|
||||
def enabled_drivers():
|
||||
for driver in cfg.CONF.enabled_drivers:
|
||||
try:
|
||||
|
|
|
@ -22,6 +22,7 @@ import collections
|
|||
import threading
|
||||
import datetime
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_utils import timeutils
|
||||
|
||||
|
@ -32,6 +33,13 @@ from astara import drivers
|
|||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
tenant_opts = [
|
||||
cfg.BoolOpt('enable_byonf', default=False,
|
||||
help='Whether to enable bring-your-own-network-function '
|
||||
'support via operator supplied drivers and images.'),
|
||||
]
|
||||
cfg.CONF.register_opts(tenant_opts)
|
||||
|
||||
|
||||
class InvalidIncomingMessage(Exception):
|
||||
pass
|
||||
|
@ -196,9 +204,8 @@ class TenantResourceManager(object):
|
|||
'a driver.'))
|
||||
return []
|
||||
|
||||
resource_obj = \
|
||||
drivers.get(message.resource.driver)(worker_context,
|
||||
message.resource.id)
|
||||
resource_obj = self._load_resource_from_message(
|
||||
worker_context, message)
|
||||
|
||||
if not resource_obj:
|
||||
# this means the driver didn't load for some reason..
|
||||
|
@ -238,3 +245,24 @@ class TenantResourceManager(object):
|
|||
return self.state_machines[resource_id]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
def _load_resource_from_message(self, worker_context, message):
|
||||
if cfg.CONF.enable_byonf:
|
||||
byonf_res = worker_context.neutron.tenant_has_byo_for_function(
|
||||
tenant_id=self.tenant_id.replace('-', ''),
|
||||
function_type=message.resource.driver)
|
||||
|
||||
if byonf_res:
|
||||
try:
|
||||
return drivers.load_from_byonf(
|
||||
worker_context,
|
||||
byonf_res,
|
||||
message.resource.id)
|
||||
except drivers.InvalidDriverException:
|
||||
LOG.exception(_LE(
|
||||
'Could not load BYONF driver, falling back to '
|
||||
'configured image'))
|
||||
pass
|
||||
|
||||
return drivers.get(message.resource.driver)(
|
||||
worker_context, message.resource.id)
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
|
||||
from astara.test.unit import base
|
||||
|
||||
from astara import drivers
|
||||
|
@ -41,3 +43,21 @@ class DriverFactoryTest(base.RugTestBase):
|
|||
self.config(enabled_drivers=all_driver_cfg)
|
||||
enabled_drivers = [d for d in drivers.enabled_drivers()]
|
||||
self.assertEqual(set(all_driver_obj), set(enabled_drivers))
|
||||
|
||||
@mock.patch('astara.drivers.get')
|
||||
def test_load_from_byonf(self, fake_get):
|
||||
fake_driver_obj = mock.Mock(
|
||||
name='fake_driver_obj',
|
||||
image_uuid='configured_image_uuid')
|
||||
fake_driver = mock.Mock(
|
||||
return_value=fake_driver_obj)
|
||||
fake_get.return_value = fake_driver
|
||||
byonf = {
|
||||
'driver': 'custom_driver',
|
||||
'image_uuid': 'custom_image_uuid',
|
||||
}
|
||||
ctx = mock.Mock()
|
||||
res = drivers.load_from_byonf(ctx, byonf, 'fake_resource_id')
|
||||
self.assertEqual(res, fake_driver_obj)
|
||||
self.assertEqual(res.image_uuid, 'custom_image_uuid')
|
||||
fake_driver.assert_called_with(ctx, 'fake_resource_id')
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
import uuid
|
||||
|
||||
import mock
|
||||
import unittest2 as unittest
|
||||
|
||||
from six.moves import range
|
||||
from astara import event
|
||||
|
@ -25,15 +24,20 @@ from astara import tenant
|
|||
from astara.drivers import router
|
||||
from astara import state
|
||||
from astara.drivers import states
|
||||
from astara.test.unit import fakes
|
||||
from astara.test.unit import base, fakes
|
||||
|
||||
|
||||
class TestTenantResourceManager(unittest.TestCase):
|
||||
class TestTenantResourceManager(base.RugTestBase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestTenantResourceManager, self).setUp()
|
||||
|
||||
self.fake_driver = fakes.fake_driver()
|
||||
self.load_resource_p = mock.patch(
|
||||
'astara.tenant.TenantResourceManager._load_resource_from_message')
|
||||
self.fake_load_resource = self.load_resource_p.start()
|
||||
self.fake_load_resource.return_value = self.fake_driver
|
||||
|
||||
self.tenant_id = 'cfb48b9c-66f6-11e5-a7be-525400cfc326'
|
||||
self.instance_mgr = \
|
||||
mock.patch('astara.instance_manager.InstanceManager').start()
|
||||
|
@ -60,6 +64,8 @@ class TestTenantResourceManager(unittest.TestCase):
|
|||
crud=event.CREATE,
|
||||
body={'key': 'value'},
|
||||
)
|
||||
self.fake_load_resource.return_value = fakes.fake_driver(
|
||||
resource_id='5678')
|
||||
sm = self.trm.get_state_machines(msg, self.ctx)[0]
|
||||
self.assertEqual(sm.resource_id, '5678')
|
||||
self.assertIn('5678', self.trm.state_machines)
|
||||
|
@ -302,3 +308,80 @@ class TestTenantResourceManager(unittest.TestCase):
|
|||
self.assertNotIn('fake-resource-id', self.trm.state_machines)
|
||||
self.assertFalse(
|
||||
self.trm.state_machines.has_been_deleted('fake-resource-id'))
|
||||
|
||||
@mock.patch('astara.drivers.load_from_byonf')
|
||||
@mock.patch('astara.drivers.get')
|
||||
def test__load_driver_from_message_no_byonf(self, fake_get, fake_byonf):
|
||||
self.load_resource_p.stop()
|
||||
self.config(enable_byonf=False)
|
||||
r = event.Resource(
|
||||
tenant_id='1234',
|
||||
id='5678',
|
||||
driver=router.Router.RESOURCE_NAME,
|
||||
)
|
||||
msg = event.Event(
|
||||
resource=r,
|
||||
crud=event.CREATE,
|
||||
body={'key': 'value'},
|
||||
)
|
||||
fake_driver = mock.Mock()
|
||||
fake_driver.return_value = 'fake_driver'
|
||||
fake_get.return_value = fake_driver
|
||||
|
||||
self.assertEqual(
|
||||
self.trm._load_resource_from_message(self.ctx, msg),
|
||||
'fake_driver')
|
||||
fake_get.assert_called_with(msg.resource.driver)
|
||||
fake_driver.assert_called_with(self.ctx, msg.resource.id)
|
||||
self.assertFalse(fake_byonf.called)
|
||||
|
||||
@mock.patch('astara.drivers.load_from_byonf')
|
||||
@mock.patch('astara.drivers.get')
|
||||
def test__load_driver_from_message_with_byonf(self, fake_get, fake_byonf):
|
||||
self.load_resource_p.stop()
|
||||
self.config(enable_byonf=True)
|
||||
r = event.Resource(
|
||||
tenant_id='1234',
|
||||
id='5678',
|
||||
driver=router.Router.RESOURCE_NAME,
|
||||
)
|
||||
msg = event.Event(
|
||||
resource=r,
|
||||
crud=event.CREATE,
|
||||
body={'key': 'value'},
|
||||
)
|
||||
fake_driver = mock.Mock()
|
||||
fake_byonf.return_value = fake_driver
|
||||
|
||||
self.ctx.neutron.tenant_has_byo_for_function.return_value = 'byonf_res'
|
||||
self.assertEqual(
|
||||
self.trm._load_resource_from_message(self.ctx, msg), fake_driver)
|
||||
fake_byonf.assert_called_with(
|
||||
self.ctx, 'byonf_res', msg.resource.id)
|
||||
self.assertFalse(fake_get.called)
|
||||
|
||||
@mock.patch('astara.drivers.load_from_byonf')
|
||||
@mock.patch('astara.drivers.get')
|
||||
def test__load_driver_from_message_empty_byonf(self, fake_get, fake_byonf):
|
||||
self.load_resource_p.stop()
|
||||
self.config(enable_byonf=True)
|
||||
r = event.Resource(
|
||||
tenant_id='1234',
|
||||
id='5678',
|
||||
driver=router.Router.RESOURCE_NAME,
|
||||
)
|
||||
msg = event.Event(
|
||||
resource=r,
|
||||
crud=event.CREATE,
|
||||
body={'key': 'value'},
|
||||
)
|
||||
|
||||
fake_driver = mock.Mock()
|
||||
fake_driver.return_value = 'fake_fallback_driver'
|
||||
fake_get.return_value = fake_driver
|
||||
|
||||
self.ctx.neutron.tenant_has_byo_for_function.return_value = None
|
||||
self.assertEqual(
|
||||
self.trm._load_resource_from_message(self.ctx, msg),
|
||||
'fake_fallback_driver')
|
||||
fake_get.assert_called_with(msg.resource.driver)
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
features:
|
||||
- Operators may now associate custom drivers and image IDs to
|
||||
tenants, via the Neutron API, to override global configuration, providing
|
||||
support for dynamic user-provided network functions. To enable this feature,
|
||||
set ``enable_byonf=True`` in ``orchestrator.ini`` and be sure the version
|
||||
of ``astara-neutron`` loaded into Neutron supports the BYONF API.
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# Copyright (c) 2016 Akanda, Inc. 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.
|
||||
|
||||
import pprint
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
from prettytable import PrettyTable
|
||||
|
||||
from keystoneclient.auth.identity import v3 as ksv3
|
||||
from keystoneclient import session as kssession
|
||||
from neutronclient.v2_0 import client
|
||||
from neutronclient.common import exceptions as neutron_exc
|
||||
|
||||
class ByoExtClientWrapper(client.Client):
|
||||
byonf_path = '/byonf'
|
||||
byonfs_path = '/byonfs'
|
||||
|
||||
|
||||
@client.APIParamsCall
|
||||
def create_byonf(self, byonf):
|
||||
return self.post(
|
||||
self.byonf_path,
|
||||
body={'byonf': byonf}
|
||||
)
|
||||
|
||||
def update_byonf(self, byonf):
|
||||
if not byonf.get('id'):
|
||||
print 'ERROR: must specify id of byonf assocation to update'
|
||||
sys.exit(1)
|
||||
|
||||
path = self.byonf_path + '/' + byonf.pop('id')
|
||||
return self.put(
|
||||
path,
|
||||
body={'byonf': byonf}
|
||||
)
|
||||
|
||||
@client.APIParamsCall
|
||||
def list_byonfs(self, retrieve_all=True, **_params):
|
||||
return self.list('byonfs', self.byonf_path, retrieve_all, **_params)
|
||||
|
||||
@client.APIParamsCall
|
||||
def delete_byonf(self, byonf_id):
|
||||
return self.delete('%s/%s' % (self.byonf_path, byonf_id))
|
||||
|
||||
ks_args = {
|
||||
'auth_url': os.getenv('OS_AUTH_URL', 'http://127.0.0.1:5000/v3'),
|
||||
'username': os.getenv('OS_USERNAME', 'demo'),
|
||||
'password': os.getenv('OS_PASSWORD', 'secrete'),
|
||||
'project_name': os.getenv('OS_PROJECT_NAME', 'demo'),
|
||||
'user_domain_id': 'default',
|
||||
'project_domain_id': 'default',
|
||||
}
|
||||
|
||||
auth = ksv3.Password(**ks_args)
|
||||
ks_session = kssession.Session(auth=auth)
|
||||
api_client = ByoExtClientWrapper(
|
||||
session=ks_session,
|
||||
)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="Script to manage user network functions")
|
||||
parser.add_argument('action', default='list')
|
||||
parser.add_argument('--function', default='')
|
||||
parser.add_argument('--image_id')
|
||||
parser.add_argument('--driver')
|
||||
parser.add_argument('--id')
|
||||
parser.add_argument('--tenant_id')
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
def print_table(byonfs):
|
||||
if not isinstance(byonfs, list):
|
||||
byonfs = [byonfs]
|
||||
|
||||
columns = ['id', 'tenant_id', 'function_type', 'driver', 'image_id']
|
||||
table = PrettyTable(columns)
|
||||
for byonf in byonfs:
|
||||
table.add_row([byonf.get(k) for k in columns])
|
||||
print table
|
||||
|
||||
if args.action in ['create', 'update']:
|
||||
req_args = {
|
||||
'image_id': args.image_id,
|
||||
'function_type': args.function,
|
||||
'driver': args.driver,
|
||||
}
|
||||
if args.tenant_id:
|
||||
req_args['tenant_id'] = args.tenant_id
|
||||
if args.id:
|
||||
req_args['id'] = args.id
|
||||
|
||||
f = getattr(api_client, '%s_byonf' % args.action)
|
||||
result = f(req_args)
|
||||
print_table(result['byonf'])
|
||||
|
||||
elif args.action == 'delete':
|
||||
api_client.delete_byonf(args.id)
|
||||
print 'deleted byonf assocation with id %s' % args.id
|
||||
else:
|
||||
arg2 = {}
|
||||
if args.function:
|
||||
arg2['function_type'] = args.function
|
||||
print_table(api_client.list_byonfs(**arg2)['byonfs'])
|
Loading…
Reference in New Issue