dragonflow/dragonflow/switch/drivers/ovs/datapath.py

351 lines
12 KiB
Python

# 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 os import path
from oslo_log import log
from oslo_serialization import jsonutils
import stevedore
from dragonflow._i18n import _
from dragonflow import conf as cfg
from dragonflow.controller import app_base
from dragonflow.controller import datapath_layout as dp_layout
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
# FIXME(oanson) remove when done porting
self._dp_allocs[dp_layout.LEGACY_APP] = self._create_legacy_dp_alloc()
def _create_legacy_dp_alloc(self):
# Create all possible exits and entries
table_offset = cfg.CONF.df.datapath_autoalloc_table_offset
return app_base.DpAlloc(
states=(),
entrypoints={str(x): x for x in range(table_offset)},
exitpoints={str(x): x for x in range(table_offset)},
full_mapping={
'source_port_key': 'reg6',
'destination_port_key': 'reg7',
'network_key': 'metadata',
}
)
def set_up(self, os_ken_base, switch_backend, nb_api, neutron_notifier):
"""
Instantiate the application classes.
Instantiate the applications (Including table and register allocation)
Wire the applications (including translating registers)
"""
self._dp = os_ken_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=os_ken_base,
switch_backend=switch_backend,
nb_api=nb_api,
neutron_server_notifier=neutron_notifier,
dp_alloc=dp_alloc,
**(vertex.params or {})
)
self.apps[vertex.name] = app
self.write_datapath_allocation()
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)
def write_datapath_allocation(self):
if not cfg.CONF.df.write_datapath_allocation:
return
dppath = cfg.CONF.df.datapath_allocation_output_path
if (path.isfile(dppath) and
not cfg.CONF.df.overwrite_datapath_allocation_output_path):
LOG.warning("File %s exists, but cannot overwrite", dppath)
return
try:
with open(dppath, 'w') as f:
dp_allocs = self._get_dp_allocs_basic_dictionary()
jsonutils.dump(dp_allocs, f)
except IOError:
LOG.exception("Cannot open file %s", dppath)
def _get_dp_allocs_basic_dictionary(self):
return {key: self._dp_alloc_to_dict(value)
for key, value in self._dp_allocs.items()}
def _dp_alloc_to_dict(self, dpalloc):
return {
'states': dict(dpalloc.states),
'entrypoints': dict(dpalloc.entrypoints),
'exitpoints': dict(dpalloc.exitpoints),
'full_mapping': dict(dpalloc.full_mapping),
}
# LoadedDatapath.apps: dict: vertex name -> dpalloc (of AttributeDict)
LoadedDatapath = collections.namedtuple('LoadedDatapath', ('apps',))
def _parse_dp_alloc(dpalloc):
kwargs = {k: app_base.AttributeDict(dpalloc.get(k, ()))
for k in ('states', 'exitpoints', 'entrypoints', 'full_mapping')}
return app_base.DpAlloc(**kwargs)
def load_datapath_from_dict(dp_allocs_dict):
apps = {app: _parse_dp_alloc(dpalloc)
for app, dpalloc in dp_allocs_dict.items()}
return LoadedDatapath(apps=apps)
def load_datapath_from_file_stream(stream):
dp_allocs_dict = jsonutils.load(stream)
return load_datapath_from_dict(dp_allocs_dict)
def load_datapath_from_file_name(dppath):
with open(dppath, 'r') as f:
return load_datapath_from_file_stream(f)
def load_datapath():
if not cfg.CONF.df.write_datapath_allocation:
# We assume that if the DF-controller is not allowed to write the file,
# then it would not exist. Otherwise, change the config or use one of
# the other methods available above.
return
dppath = cfg.CONF.df.datapath_allocation_output_path
return load_datapath_from_file_name(dppath)