Merge "Adds ability to run appliance with user-specified drivers/images"
This commit is contained in:
commit
23e3607089
|
@ -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]
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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