Merge "New application framework"

This commit is contained in:
Zuul 2018-05-06 13:25:03 +00:00 committed by Gerrit Code Review
commit 1d901373e3
10 changed files with 691 additions and 0 deletions

View File

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

View File

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

View File

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

View 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

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

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

View 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

View File

@ -0,0 +1,2 @@
vertices: {}
edges: {}

View File

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

View File

@ -22,6 +22,9 @@ classifier =
[files]
packages =
dragonflow
data_files =
etc/neutron/ =
etc/dragonflow_datapath_layout.yaml
[build_sphinx]
source-dir = doc/source