Merge "New application framework"
This commit is contained in:
commit
1d901373e3
|
@ -28,6 +28,7 @@ if is_service_enabled df-metadata ; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
DRAGONFLOW_CONF=/etc/neutron/dragonflow.ini
|
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_PATH=/etc/neutron
|
||||||
Q_PLUGIN_EXTRA_CONF_FILES=(dragonflow.ini)
|
Q_PLUGIN_EXTRA_CONF_FILES=(dragonflow.ini)
|
||||||
|
|
||||||
|
|
|
@ -249,6 +249,7 @@ function configure_df_plugin {
|
||||||
popd
|
popd
|
||||||
mkdir -p $Q_PLUGIN_EXTRA_CONF_PATH
|
mkdir -p $Q_PLUGIN_EXTRA_CONF_PATH
|
||||||
cp $DRAGONFLOW_DIR/etc/dragonflow.ini.sample $DRAGONFLOW_CONF
|
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-svc ; then
|
||||||
if is_service_enabled q-qos ; then
|
if is_service_enabled q-qos ; then
|
||||||
|
|
|
@ -177,6 +177,14 @@ df_opts = [
|
||||||
default=False,
|
default=False,
|
||||||
help=_("Automatically detect port-behind-port scenarios, "
|
help=_("Automatically detect port-behind-port scenarios, "
|
||||||
"e.g., amphora, or macvlan")),
|
"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')),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
||||||
|
vertices: {}
|
||||||
|
edges: {}
|
|
@ -28,6 +28,7 @@ WebOb>=1.7.1 # MIT
|
||||||
jsonmodels>=2.1.3 # BSD License (3 clause)
|
jsonmodels>=2.1.3 # BSD License (3 clause)
|
||||||
skydive-client>=0.4.4 # Apache-2.0
|
skydive-client>=0.4.4 # Apache-2.0
|
||||||
cotyledon>=1.3.0 # 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
|
# These repos are installed from git in OpenStack CI if the job
|
||||||
# configures them as required-projects:
|
# configures them as required-projects:
|
||||||
|
|
Loading…
Reference in New Issue