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:
Adam Gandelman 2016-03-14 17:14:57 -07:00
parent 0cfd325867
commit 2aabf4b185
7 changed files with 300 additions and 6 deletions

View File

@ -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]

View File

@ -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:

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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.

119
tools/astara-byonf Executable file
View File

@ -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'])