New application framework
Add initial scaffolding to support applications with non-hard-coded table names and registers. Includes: * 'wiring' configuration (vertices and edges) * Decorator for new applications, with States, Entrypoints, Exitpoints, and public and private variables. Includes code for reading and parsing this information, and connecting the 'external' tables, i.e. the exitpoints of one app to the entrypoint if the next app. Change-Id: I81ee47b0c6e741888b3446602cca5e1835c9ae2f Related-Bug: #1738986 Co-Authored-By: Lihi Wishnitzer <lihiwish@gmail.com>
This commit is contained in:
parent
c051a44b88
commit
c3280d5ca5
@ -28,6 +28,7 @@ if is_service_enabled df-metadata ; then
|
||||
fi
|
||||
|
||||
DRAGONFLOW_CONF=/etc/neutron/dragonflow.ini
|
||||
DRAGONFLOW_DATAPATH=/etc/neutron/dragonflow_datapath_layout.yaml
|
||||
Q_PLUGIN_EXTRA_CONF_PATH=/etc/neutron
|
||||
Q_PLUGIN_EXTRA_CONF_FILES=(dragonflow.ini)
|
||||
|
||||
|
@ -249,6 +249,7 @@ function configure_df_plugin {
|
||||
popd
|
||||
mkdir -p $Q_PLUGIN_EXTRA_CONF_PATH
|
||||
cp $DRAGONFLOW_DIR/etc/dragonflow.ini.sample $DRAGONFLOW_CONF
|
||||
cp $DRAGONFLOW_DIR/etc/dragonflow_datapath_layout.yaml $DRAGONFLOW_DATAPATH
|
||||
|
||||
if is_service_enabled q-svc ; then
|
||||
if is_service_enabled q-qos ; then
|
||||
|
@ -177,6 +177,14 @@ df_opts = [
|
||||
default=False,
|
||||
help=_("Automatically detect port-behind-port scenarios, "
|
||||
"e.g., amphora, or macvlan")),
|
||||
cfg.StrOpt('datapath_layout_path',
|
||||
help=_("Path to datapath layout configuration"),
|
||||
default="/etc/neutron/dragonflow_datapath_layout.yaml"),
|
||||
# FIXME (dimak) rename to something simpler once all tables are
|
||||
# auto-allocated.
|
||||
cfg.IntOpt('datapath_autoalloc_table_offset',
|
||||
default=201,
|
||||
help=_('Start offset for new datapath application tables')),
|
||||
]
|
||||
|
||||
|
||||
|
114
dragonflow/controller/app_base.py
Normal file
114
dragonflow/controller/app_base.py
Normal file
@ -0,0 +1,114 @@
|
||||
# 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 collections
|
||||
|
||||
from dragonflow.controller import df_base_app
|
||||
|
||||
|
||||
# Specify the states, entrypoints, exitpoints, public mappings and private
|
||||
# mappings of an application:
|
||||
# States - the number of states the application has (Translates to number of
|
||||
# OpenFlow tables)
|
||||
# Entrypoints - Where do packets come in?
|
||||
# Exitpoints - Where do packets come out?
|
||||
# Public Mappings - Metadata that is passed between applications
|
||||
# Private Mappings - Metadata that is private to this application (e.g. to save
|
||||
# a state accross tables)
|
||||
Specification = collections.namedtuple(
|
||||
'Specification',
|
||||
('states', 'entrypoints', 'exitpoints', 'public_mapping',
|
||||
'private_mapping'),
|
||||
)
|
||||
|
||||
|
||||
def define_specification(states, entrypoints, exitpoints,
|
||||
public_mapping=None, private_mapping=None):
|
||||
if public_mapping is None:
|
||||
public_mapping = {}
|
||||
|
||||
if private_mapping is None:
|
||||
private_mapping = {}
|
||||
|
||||
def decorator(cls):
|
||||
cls._specification = Specification(
|
||||
states=states,
|
||||
entrypoints=entrypoints,
|
||||
exitpoints=exitpoints,
|
||||
public_mapping=public_mapping,
|
||||
private_mapping=private_mapping,
|
||||
)
|
||||
return cls
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
# Entrypoint: An entrypoint for packets - The application accepts packets here,
|
||||
# and they should be routed to the given target (or OpenFlow table).
|
||||
# consumes: Which metadata is consumbed by this entrypoint.
|
||||
Entrypoint = collections.namedtuple(
|
||||
'Entrypoint',
|
||||
('name', 'target', 'consumes'),
|
||||
)
|
||||
|
||||
|
||||
# Exitpoint: An exitpoint for packets - The application sends (resubmits, or
|
||||
# gotos) packets to this table (provided by the framework).
|
||||
# provides: Which metadata is set on the packet
|
||||
Exitpoint = collections.namedtuple(
|
||||
'Exitpoint',
|
||||
('name', 'provides'),
|
||||
)
|
||||
|
||||
|
||||
# The allocation of states (table numbers), entrypoints and exitpoints (tables
|
||||
# for incoming and outgoing packets), and register mapping (where to place
|
||||
# the metadata)
|
||||
DpAlloc = collections.namedtuple(
|
||||
'DpAlloc',
|
||||
('states', 'exitpoints', 'entrypoints', 'full_mapping'),
|
||||
)
|
||||
|
||||
|
||||
class VariableMapping(dict):
|
||||
pass
|
||||
|
||||
|
||||
class AttributeDict(dict):
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
|
||||
class Base(df_base_app.DFlowApp):
|
||||
def __init__(self, dp_alloc, *args, **kwargs):
|
||||
super(Base, self).__init__(*args, **kwargs)
|
||||
self._dp_alloc = dp_alloc
|
||||
|
||||
def initialize(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def states(self):
|
||||
return self._dp_alloc.states
|
||||
|
||||
@property
|
||||
def exitpoints(self):
|
||||
return self._dp_alloc.exitpoints
|
||||
|
||||
@property
|
||||
def entrypoints(self):
|
||||
return self._dp_alloc.entrypoints
|
||||
|
||||
|
||||
register_event = df_base_app.register_event
|
265
dragonflow/controller/datapath.py
Normal file
265
dragonflow/controller/datapath.py
Normal file
@ -0,0 +1,265 @@
|
||||
# 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.
|
||||
from oslo_log import log
|
||||
import stevedore
|
||||
|
||||
from dragonflow._i18n import _
|
||||
from dragonflow import conf as cfg
|
||||
from dragonflow.controller import app_base
|
||||
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
REGS = frozenset((
|
||||
'reg0',
|
||||
'reg1',
|
||||
'reg2',
|
||||
'reg3',
|
||||
'reg4',
|
||||
'reg5',
|
||||
'reg6',
|
||||
'reg7',
|
||||
'metadata',
|
||||
))
|
||||
|
||||
|
||||
def _sequence_generator(offset):
|
||||
while True:
|
||||
yield offset
|
||||
offset += 1
|
||||
|
||||
|
||||
class Datapath(object):
|
||||
"""
|
||||
Given the layout (e.g. from the config file), instantiate all the
|
||||
applications in the datapath (vertices), and connect them (edges).
|
||||
Instantiation includes allocating OpenFlow tables and registers.
|
||||
Connection includes wiring and mapping the registers
|
||||
"""
|
||||
def __init__(self, layout):
|
||||
self._layout = layout
|
||||
self._dp_allocs = {}
|
||||
self._public_variables = set()
|
||||
self.apps = None
|
||||
|
||||
def set_up(self, ryu_base, vswitch_api, nb_api, notifier):
|
||||
"""
|
||||
Instantiate the application classes.
|
||||
Instantiate the applications (Including table and register allocation)
|
||||
Wire the applications (including translating registers)
|
||||
"""
|
||||
self._dp = ryu_base.datapath
|
||||
self._table_generator = _sequence_generator(
|
||||
cfg.CONF.df.datapath_autoalloc_table_offset)
|
||||
self._public_variables.clear()
|
||||
|
||||
app_classes = {}
|
||||
self.apps = {}
|
||||
|
||||
for vertex in self._layout.vertices:
|
||||
if vertex.type in app_classes:
|
||||
continue
|
||||
|
||||
app_class = self._get_app_class(vertex.type)
|
||||
app_classes[vertex.type] = app_class
|
||||
self._public_variables.update(
|
||||
app_class._specification.public_mapping.keys(),
|
||||
)
|
||||
|
||||
for vertex in self._layout.vertices:
|
||||
app_class = app_classes[vertex.type]
|
||||
dp_alloc = self._create_dp_alloc(app_class._specification)
|
||||
self.log_datapath_allocation(vertex.name, dp_alloc)
|
||||
self._dp_allocs[vertex.name] = dp_alloc
|
||||
app = app_class(api=ryu_base,
|
||||
vswitch_api=vswitch_api,
|
||||
nb_api=nb_api,
|
||||
neutron_server_notifier=notifier,
|
||||
dp_alloc=dp_alloc,
|
||||
**(vertex.params or {})
|
||||
)
|
||||
self.apps[vertex.name] = app
|
||||
|
||||
for app in self.apps.values():
|
||||
app.initialize()
|
||||
|
||||
for edge in self._layout.edges:
|
||||
self._install_edge(edge)
|
||||
|
||||
def _get_app_class(self, app_type):
|
||||
"""Get an application class (Python class) by app name"""
|
||||
mgr = stevedore.NamedExtensionManager(
|
||||
'dragonflow.controller.apps',
|
||||
[app_type],
|
||||
invoke_on_load=False,
|
||||
)
|
||||
for ext in mgr:
|
||||
return ext.plugin
|
||||
else:
|
||||
raise RuntimeError(_('Failed to load app {0}').format(app_type))
|
||||
|
||||
def _create_dp_alloc(self, specification):
|
||||
"""
|
||||
Allocate the tables and registers for the given application (given
|
||||
by its specification)
|
||||
"""
|
||||
public_mapping = specification.public_mapping.copy()
|
||||
unmapped_vars = self._public_variables.difference(public_mapping)
|
||||
|
||||
# Convert to set() so the result won't be a frozenset()
|
||||
unmapped_regs = set(REGS).difference(
|
||||
public_mapping.values(),
|
||||
).difference(
|
||||
specification.private_mapping.values(),
|
||||
)
|
||||
|
||||
while unmapped_vars and unmapped_regs:
|
||||
public_mapping[unmapped_vars.pop()] = unmapped_regs.pop()
|
||||
|
||||
if unmapped_vars:
|
||||
raise RuntimeError(
|
||||
_("Can't allocate enough registers for variables"),
|
||||
)
|
||||
|
||||
states_dict = {
|
||||
state: next(self._table_generator)
|
||||
for state in specification.states
|
||||
}
|
||||
states = app_base.AttributeDict(**states_dict)
|
||||
|
||||
exitpoints_dict = {
|
||||
exit.name: next(self._table_generator)
|
||||
for exit in specification.exitpoints
|
||||
}
|
||||
exitpoints = app_base.AttributeDict(**exitpoints_dict)
|
||||
|
||||
entrypoints_dict = {
|
||||
entry.name: states[entry.target]
|
||||
for entry in specification.entrypoints
|
||||
}
|
||||
entrypoints = app_base.AttributeDict(**entrypoints_dict)
|
||||
|
||||
return app_base.DpAlloc(
|
||||
states=states,
|
||||
exitpoints=exitpoints,
|
||||
entrypoints=entrypoints,
|
||||
full_mapping=public_mapping,
|
||||
)
|
||||
|
||||
def _get_connector_config(self, connector):
|
||||
return self._dp_allocs[connector.vertex]
|
||||
|
||||
def _install_edge(self, edge):
|
||||
"""
|
||||
Wire two applications. Infer the translation of metadata fields,
|
||||
and install the actions/instructions to pass a packet from one
|
||||
application's exit point to another's entry point
|
||||
"""
|
||||
exitpoint = edge.exitpoint
|
||||
exit_config = self._get_connector_config(exitpoint)
|
||||
entrypoint = edge.entrypoint
|
||||
entry_config = self._get_connector_config(entrypoint)
|
||||
translations = []
|
||||
|
||||
for var in self._public_variables:
|
||||
exit_reg = exit_config.full_mapping[var]
|
||||
entry_reg = entry_config.full_mapping[var]
|
||||
if exit_reg == entry_reg:
|
||||
continue
|
||||
|
||||
translations.append(
|
||||
(exit_reg, entry_reg),
|
||||
)
|
||||
|
||||
self._install_goto(
|
||||
# Source
|
||||
exit_config.exitpoints[exitpoint.name],
|
||||
# Destination
|
||||
entry_config.entrypoints[entrypoint.name],
|
||||
translations,
|
||||
)
|
||||
|
||||
def _install_goto(self, source, dest, translations):
|
||||
"""
|
||||
Install the actions/instructions to pass a packet from one
|
||||
application's exit point to another's entry point, including
|
||||
translating the metadata fields.
|
||||
"""
|
||||
ofproto = self._dp.ofproto
|
||||
parser = self._dp.ofproto_parser
|
||||
actions = []
|
||||
|
||||
try:
|
||||
from_regs, to_regs = zip(*translations)
|
||||
except ValueError:
|
||||
from_regs, to_regs = ((), ())
|
||||
|
||||
# Push all register values
|
||||
for reg in from_regs:
|
||||
actions.append(
|
||||
parser.NXActionStackPush(field=reg, start=0, end=32),
|
||||
)
|
||||
|
||||
# Pop into target registers in reverse order
|
||||
for reg in reversed(to_regs):
|
||||
actions.append(
|
||||
parser.NXActionStackPop(field=reg, start=0, end=32),
|
||||
)
|
||||
|
||||
if source < dest:
|
||||
instructions = [
|
||||
parser.OFPInstructionActions(
|
||||
ofproto.OFPIT_APPLY_ACTIONS,
|
||||
actions,
|
||||
),
|
||||
parser.OFPInstructionGotoTable(dest),
|
||||
]
|
||||
else:
|
||||
actions.append(parser.NXActionResubmitTable(table_id=dest))
|
||||
|
||||
instructions = [
|
||||
parser.OFPInstructionActions(
|
||||
ofproto.OFPIT_APPLY_ACTIONS,
|
||||
actions,
|
||||
),
|
||||
]
|
||||
|
||||
message = parser.OFPFlowMod(
|
||||
self._dp,
|
||||
table_id=source,
|
||||
command=ofproto.OFPFC_ADD,
|
||||
match=parser.OFPMatch(),
|
||||
instructions=instructions,
|
||||
)
|
||||
self._dp.send_msg(message)
|
||||
|
||||
def log_datapath_allocation(self, name, dp_alloc):
|
||||
"""
|
||||
Log the dp_alloc object (The allocation of tables, registers, etc.) for
|
||||
the given application
|
||||
"""
|
||||
LOG.debug("Application: %s", name)
|
||||
LOG.debug("\tStates:")
|
||||
for state, table_num in dp_alloc.states.items():
|
||||
LOG.debug("\t\t%s: %s", state, table_num)
|
||||
|
||||
LOG.debug("\tEntrypoints:")
|
||||
for entry_name, table_num in dp_alloc.entrypoints.items():
|
||||
LOG.debug("\t\t%s: %s", entry_name, table_num)
|
||||
|
||||
LOG.debug("\tExitpoints:")
|
||||
for exit_name, table_num in dp_alloc.exitpoints.items():
|
||||
LOG.debug("\t\t%s: %s", exit_name, table_num)
|
||||
|
||||
LOG.debug("\tMapping:")
|
||||
for var, reg in dp_alloc.full_mapping.items():
|
||||
LOG.debug("\t\t%s: %s", var, reg)
|
71
dragonflow/controller/datapath_layout.py
Normal file
71
dragonflow/controller/datapath_layout.py
Normal file
@ -0,0 +1,71 @@
|
||||
# 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 collections
|
||||
|
||||
import yaml
|
||||
|
||||
from dragonflow import conf as cfg
|
||||
|
||||
|
||||
Vertex = collections.namedtuple(
|
||||
'Vertex',
|
||||
('name', 'type', 'params'),
|
||||
)
|
||||
|
||||
|
||||
Edge = collections.namedtuple(
|
||||
'Edge',
|
||||
('exitpoint', 'entrypoint'),
|
||||
)
|
||||
|
||||
|
||||
_ConnectorBase = collections.namedtuple(
|
||||
'Connector',
|
||||
('vertex', 'direction', 'name'),
|
||||
)
|
||||
|
||||
|
||||
class Connector(_ConnectorBase):
|
||||
@classmethod
|
||||
def from_string(cls, val):
|
||||
return cls(*val.split('.'))
|
||||
|
||||
|
||||
Layout = collections.namedtuple(
|
||||
'Layout',
|
||||
('vertices', 'edges'),
|
||||
)
|
||||
|
||||
|
||||
def get_datapath_layout(path=None):
|
||||
if path is None:
|
||||
path = cfg.CONF.df.datapath_layout_path
|
||||
|
||||
with open(path) as f:
|
||||
raw_layout = yaml.load(f)
|
||||
|
||||
vertices = tuple(
|
||||
Vertex(
|
||||
name=key,
|
||||
type=value['type'],
|
||||
params=value.get('params'),
|
||||
) for key, value in raw_layout['vertices'].items()
|
||||
)
|
||||
|
||||
edges = tuple(
|
||||
Edge(
|
||||
exitpoint=Connector.from_string(key),
|
||||
entrypoint=Connector.from_string(value),
|
||||
) for key, value in raw_layout['edges'].items()
|
||||
)
|
||||
|
||||
return Layout(vertices, edges)
|
225
dragonflow/tests/unit/test_datapath.py
Normal file
225
dragonflow/tests/unit/test_datapath.py
Normal file
@ -0,0 +1,225 @@
|
||||
# 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 functools
|
||||
|
||||
import mock
|
||||
import testscenarios
|
||||
|
||||
from dragonflow.controller import app_base
|
||||
from dragonflow.controller import datapath
|
||||
from dragonflow.controller import datapath_layout
|
||||
from dragonflow.tests import base as tests_base
|
||||
|
||||
load_tests = testscenarios.load_tests_apply_scenarios
|
||||
|
||||
|
||||
@app_base.define_specification(
|
||||
states=('main',),
|
||||
entrypoints=(
|
||||
app_base.Entrypoint(
|
||||
name='conn1',
|
||||
target='main',
|
||||
consumes={},
|
||||
),
|
||||
app_base.Entrypoint(
|
||||
name='conn2',
|
||||
target='main',
|
||||
consumes={},
|
||||
),
|
||||
),
|
||||
exitpoints=(
|
||||
app_base.Exitpoint(
|
||||
name='conn1',
|
||||
provides={},
|
||||
),
|
||||
app_base.Exitpoint(
|
||||
name='conn2',
|
||||
provides={},
|
||||
),
|
||||
),
|
||||
public_mapping={
|
||||
'var1': 'reg0',
|
||||
'var2': 'reg1',
|
||||
'var3': 'reg2',
|
||||
},
|
||||
private_mapping={
|
||||
'priv1': 'reg3',
|
||||
'priv2': 'reg7',
|
||||
}
|
||||
)
|
||||
class DummyApp(app_base.Base):
|
||||
def __init__(self, *args, **kwargs):
|
||||
# super(DummyApp, self).__init__(*args, **kwargs)
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
|
||||
@app_base.define_specification(
|
||||
states=('main',),
|
||||
entrypoints=(
|
||||
app_base.Entrypoint(
|
||||
name='conn1',
|
||||
target='main',
|
||||
consumes={},
|
||||
),
|
||||
app_base.Entrypoint(
|
||||
name='conn2',
|
||||
target='main',
|
||||
consumes={},
|
||||
),
|
||||
),
|
||||
exitpoints=(
|
||||
app_base.Exitpoint(
|
||||
name='conn1',
|
||||
provides={},
|
||||
),
|
||||
app_base.Exitpoint(
|
||||
name='conn2',
|
||||
provides={},
|
||||
),
|
||||
),
|
||||
public_mapping={
|
||||
'var1': 'reg0',
|
||||
'var3': 'reg1',
|
||||
},
|
||||
private_mapping={
|
||||
'priv1': 'reg2',
|
||||
'priv2': 'reg7',
|
||||
}
|
||||
)
|
||||
class Dummy2App(DummyApp):
|
||||
pass
|
||||
|
||||
|
||||
class TestDatapath(tests_base.BaseTestCase):
|
||||
scenarios = [
|
||||
(
|
||||
'empty-config',
|
||||
{
|
||||
'layout': datapath_layout.Layout(
|
||||
vertices=(),
|
||||
edges=(),
|
||||
),
|
||||
'raises': None,
|
||||
},
|
||||
),
|
||||
(
|
||||
'non-existent-vertex',
|
||||
{
|
||||
'layout': datapath_layout.Layout(
|
||||
vertices=(),
|
||||
edges=(
|
||||
datapath_layout.Edge(
|
||||
exitpoint=datapath_layout.Connector(
|
||||
'app1', 'out', 'conn1',
|
||||
),
|
||||
entrypoint=datapath_layout.Connector(
|
||||
'app2', 'out', 'conn1',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'raises': KeyError,
|
||||
},
|
||||
),
|
||||
(
|
||||
'connected-vertices',
|
||||
{
|
||||
'layout': datapath_layout.Layout(
|
||||
vertices=(
|
||||
datapath_layout.Vertex(
|
||||
name='app1',
|
||||
type='dummy',
|
||||
params={'key1': 'val1'},
|
||||
),
|
||||
datapath_layout.Vertex(
|
||||
name='app2',
|
||||
type='dummy2',
|
||||
params={'key2': 'val2'},
|
||||
),
|
||||
),
|
||||
edges=(
|
||||
datapath_layout.Edge(
|
||||
exitpoint=datapath_layout.Connector(
|
||||
'app1', 'out', 'conn1',
|
||||
),
|
||||
entrypoint=datapath_layout.Connector(
|
||||
'app2', 'in', 'conn1',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'raises': None,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
def get_dummy_class(self, type):
|
||||
if type == 'dummy':
|
||||
return DummyApp
|
||||
else:
|
||||
return Dummy2App
|
||||
|
||||
def setUp(self):
|
||||
super(TestDatapath, self).setUp()
|
||||
self.dp = datapath.Datapath(self.layout)
|
||||
self.dp._get_app_class = mock.Mock(side_effect=self.get_dummy_class)
|
||||
self.dp._install_goto = mock.Mock()
|
||||
|
||||
def test_set_up(self):
|
||||
if self.raises:
|
||||
caller = functools.partial(
|
||||
self.assertRaises,
|
||||
self.raises,
|
||||
)
|
||||
else:
|
||||
def caller(func, *args):
|
||||
func(*args)
|
||||
|
||||
caller(
|
||||
self.dp.set_up,
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
)
|
||||
|
||||
def test_app_initialization(self):
|
||||
if self.raises is not None:
|
||||
raise self.skipTest('Tests only positive flows')
|
||||
|
||||
self.dp.set_up(
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
)
|
||||
self.assertEqual(
|
||||
len(self.layout.vertices),
|
||||
self.dp._get_app_class.call_count,
|
||||
)
|
||||
|
||||
def test_installed_gotos(self):
|
||||
if self.raises is not None:
|
||||
raise self.skipTest('Tests only positive flows')
|
||||
|
||||
self.dp.set_up(
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
mock.Mock(),
|
||||
)
|
||||
self.assertEqual(
|
||||
len(self.layout.edges),
|
||||
self.dp._install_goto.call_count,
|
||||
)
|
||||
# FIXME add check for actual call parameters
|
2
etc/dragonflow_datapath_layout.yaml
Normal file
2
etc/dragonflow_datapath_layout.yaml
Normal file
@ -0,0 +1,2 @@
|
||||
vertices: {}
|
||||
edges: {}
|
@ -28,6 +28,7 @@ WebOb>=1.7.1 # MIT
|
||||
jsonmodels>=2.1.3 # BSD License (3 clause)
|
||||
skydive-client>=0.4.4 # Apache-2.0
|
||||
cotyledon>=1.3.0 # Apache-2.0
|
||||
PyYAML>=3.10 # MIT
|
||||
|
||||
# These repos are installed from git in OpenStack CI if the job
|
||||
# configures them as required-projects:
|
||||
|
Loading…
Reference in New Issue
Block a user