Merge "Adds ability to run appliance with user-specified drivers/images"

This commit is contained in:
Jenkins 2016-03-18 20:48:30 +00:00 committed by Gerrit Code Review
commit 23e3607089
7 changed files with 300 additions and 6 deletions

View File

@ -750,6 +750,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): class L3PluginApi(object):
@ -1142,6 +1148,7 @@ class Neutron(object):
if ports: if ports:
port = Port.from_dict(ports[0]) port = Port.from_dict(ports[0])
device_name = driver.get_device_name(port) device_name = driver.get_device_name(port)
driver.unplug(device_name) driver.unplug(device_name)
@ -1159,3 +1166,14 @@ class Neutron(object):
def clear_device_id(self, port): def clear_device_id(self, port):
self.api_client.update_port(port.id, {'port': {'device_id': ''}}) 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(): def enabled_drivers():
for driver in cfg.CONF.enabled_drivers: for driver in cfg.CONF.enabled_drivers:
try: try:

View File

@ -22,6 +22,7 @@ import collections
import threading import threading
import datetime import datetime
from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import timeutils from oslo_utils import timeutils
@ -32,6 +33,13 @@ from astara import drivers
LOG = logging.getLogger(__name__) 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): class InvalidIncomingMessage(Exception):
pass pass
@ -196,9 +204,8 @@ class TenantResourceManager(object):
'a driver.')) 'a driver.'))
return [] return []
resource_obj = \ resource_obj = self._load_resource_from_message(
drivers.get(message.resource.driver)(worker_context, worker_context, message)
message.resource.id)
if not resource_obj: if not resource_obj:
# this means the driver didn't load for some reason.. # this means the driver didn't load for some reason..
@ -238,3 +245,24 @@ class TenantResourceManager(object):
return self.state_machines[resource_id] return self.state_machines[resource_id]
except KeyError: except KeyError:
return None 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 # License for the specific language governing permissions and limitations
# under the License. # under the License.
import mock
from astara.test.unit import base from astara.test.unit import base
from astara import drivers from astara import drivers
@ -41,3 +43,21 @@ class DriverFactoryTest(base.RugTestBase):
self.config(enabled_drivers=all_driver_cfg) self.config(enabled_drivers=all_driver_cfg)
enabled_drivers = [d for d in drivers.enabled_drivers()] enabled_drivers = [d for d in drivers.enabled_drivers()]
self.assertEqual(set(all_driver_obj), set(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 uuid
import mock import mock
import unittest2 as unittest
from six.moves import range from six.moves import range
from astara import event from astara import event
@ -25,15 +24,20 @@ from astara import tenant
from astara.drivers import router from astara.drivers import router
from astara import state from astara import state
from astara.drivers import states 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): def setUp(self):
super(TestTenantResourceManager, self).setUp() super(TestTenantResourceManager, self).setUp()
self.fake_driver = fakes.fake_driver() 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.tenant_id = 'cfb48b9c-66f6-11e5-a7be-525400cfc326'
self.instance_mgr = \ self.instance_mgr = \
mock.patch('astara.instance_manager.InstanceManager').start() mock.patch('astara.instance_manager.InstanceManager').start()
@ -60,6 +64,8 @@ class TestTenantResourceManager(unittest.TestCase):
crud=event.CREATE, crud=event.CREATE,
body={'key': 'value'}, 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] sm = self.trm.get_state_machines(msg, self.ctx)[0]
self.assertEqual(sm.resource_id, '5678') self.assertEqual(sm.resource_id, '5678')
self.assertIn('5678', self.trm.state_machines) 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.assertNotIn('fake-resource-id', self.trm.state_machines)
self.assertFalse( self.assertFalse(
self.trm.state_machines.has_been_deleted('fake-resource-id')) 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'])