Add an example controller for hardware switch test

This commit is contained in:
Eishun Kondoh 2017-12-01 11:11:11 +09:00
parent cfc2152b7d
commit f2297c551a
16 changed files with 501 additions and 50 deletions

20
examples/heckle/.gitignore vendored Normal file
View file

@ -0,0 +1,20 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez

21
examples/heckle/README.md Normal file
View file

@ -0,0 +1,21 @@
# Heckle
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `heckle` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:heckle, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/heckle](https://hexdocs.pm/heckle).

View file

@ -0,0 +1,30 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config
config :heckle,
vlan_tagging: true,
vlan_id: 123,
vlan_trunk: "veth0",
access_port1: "veth4",
access_port2: "veth3",
receiver_mac: "00000000000f",
receiver_ip: {8,8,8,8},
sender_mac: "001122334455",
inside_local: {192,168,5,10},
outside_local: {192,168,255,1},
flow_pattern: :bum
config :tres,
protocol: :tcp,
port: 6653,
max_connections: 10,
num_acceptors: 10,
callback_module: Heckle.Controller,
callback_args: ["0000d2851d52d749"]
config :logger,
level: :debug,
format: "$date $time [$level] $metadata$message\n",
metadata: [:application],
handle_otp_reports: true

View file

@ -0,0 +1,18 @@
defmodule Heckle do
@moduledoc """
Documentation for Heckle.
"""
@doc """
Hello world.
## Examples
iex> Heckle.hello
:world
"""
def hello do
:world
end
end

View file

@ -0,0 +1,20 @@
defmodule Heckle.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
def start(_type, _args) do
# List all child processes to be supervised
children = [
# Starts a worker by calling: Heckle.Worker.start_link(arg)
# {Heckle.Worker, arg},
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Heckle.Supervisor]
Supervisor.start_link(children, opts)
end
end

View file

@ -0,0 +1,133 @@
defmodule Heckle.Controller do
use GenServer
use Bitwise
use Tres.Controller
import Logger
alias Heckle.PipelineProfiles
alias Heckle.FlowPatterns
defmodule State do
defstruct [
dpid: nil,
conn_ref: nil,
access_port1_name: nil,
access_port2_name: nil,
trunk_port_name: nil,
access_port1: nil,
access_port2: nil,
trunk_port: nil,
vlan_tagging: nil,
vlan_id: nil,
receiver_mac: nil,
receiver_ip: nil,
sender_mac: nil,
inside_local: nil,
outside_local: nil,
flow_pattern: nil
]
end
def start_link(dpid, args) do
GenServer.start(__MODULE__, [dpid, args])
end
def init([{dpid, _aux_id}, [dpid]]) do
:ok = info("Switch Ready: dpid: #{inspect(dpid)} on #{inspect(self())}")
state = init_state(dpid)
{:ok, state}
end
def init([{dpid, _aux_id}, [_dpid]]) do
:ok = info("Switch Ready: dpid: #{inspect(dpid)} but not acceptable")
:ignore
end
def handle_cast(:send_flows, state) do
state
|> FlowPatterns.flows
|> Enum.each(&send_flow_mod_add(state.dpid, &1))
{:noreply, state}
end
def handle_info(%Desc.Reply{mfr_desc: "Aruba"} = desc, %State{dpid: dpid} = state) do
info("Switch Desc: mfr = #{desc.mfr_desc} hw = #{desc.hw_desc} sw = #{desc.sw_desc}")
:ok = PipelineProfiles.of_aruba()
|> TableFeatures.Request.new
|> send_message(dpid)
{:noreply, state}
end
def handle_info(%Desc.Reply{} = desc, state) do
:ok = info("Switch Desc: mfr = #{desc.mfr_desc} hw = #{desc.hw_desc} sw = #{desc.sw_desc}")
:ok = GenServer.cast(self(), :send_flows)
{:noreply, state}
end
def handle_info(%TableFeatures.Reply{xid: xid}, state) do
:ok = info("Pipeline modification is success (xid: #{xid})")
:ok = GenServer.cast(self(), :send_flows)
{:noreply, state}
end
def handle_info(%PortDesc.Reply{ports: ports}, state) do
info("Received Port Desc")
access_port1 = Enum.find(ports, fn(port) -> port.name == state.access_port1_name end)
access_port2 = Enum.find(ports, fn(port) -> port.name == state.access_port2_name end)
trunk_port = Enum.find(ports, fn(port) -> port.name == state.trunk_port_name end)
:ok = desc_stats_request(state.dpid)
{:noreply, %{state|access_port1: access_port1, access_port2: access_port2, trunk_port: trunk_port}}
end
def handle_info(%ErrorMsg{code: code, type: type, data: data, xid: xid}, state) do
:ok = warn("Request Failed(xid: #{xid}):"<>
" code: #{code}"<>
" type: #{type}"<>
" data: #{inspect(data)}"<>
" dpid: #{inspect(state.dpid)}")
{:stop, :request_failed, state}
end
def handle_info({:'DOWN', ref, :process, _pid, _reason}, %State{conn_ref: ref} = state) do
:ok = debug("Switch Disconnected: dpid: #{inspect(state.dpid)}")
{:stop, :normal, state}
end
def handle_info(_info, state) do
{:noreply, state}
end
# private functions
defp init_state(dpid) do
:ok = init_datapath(dpid)
conn_ref = SwitchRegistry.monitor(dpid)
config = Application.get_all_env(:heckle)
%State{
dpid: dpid,
conn_ref: conn_ref,
access_port1_name: config[:access_port1],
access_port2_name: config[:access_port2],
trunk_port_name: config[:vlan_trunk],
vlan_tagging: config[:vlan_tagging] || true,
vlan_id: 0x1000 ||| (config[:vlan_id] || 0),
receiver_mac: config[:receiver_mac],
receiver_ip: config[:receiver_ip],
sender_mac: config[:sender_mac],
inside_local: config[:inside_local],
outside_local: config[:outside_local],
flow_pattern: config[:flow_pattern] || :nat
}
end
defp init_datapath(dpid) do
:ok = send_flow_mod_delete(dpid)
:ok = port_desc_stats_request(dpid)
:ok = set_config(dpid)
end
defp desc_stats_request(dpid) do
:ok = send_message(Desc.Request.new, dpid)
end
defp port_desc_stats_request(dpid) do
:ok = send_message(PortDesc.Request.new, dpid)
end
defp set_config(dpid) do
:ok = send_message(SetConfig.new(miss_send_len: :no_buffer), dpid)
end
end

View file

@ -0,0 +1,113 @@
defmodule Heckle.FlowPatterns do
use Bitwise
use Tres.Controller
@mcast {"010000000000", "010000000000"}
def flows(%Heckle.Controller.State{flow_pattern: :nat} = state) do
[[table_id: 1,
priority: 30,
cookie: 0x3000000000000001,
match: Match.new(
eth_dst: state.receiver_mac,
eth_src: state.sender_mac,
eth_type: 0x0800,
ipv4_src: state.inside_local,
ipv4_dst: state.receiver_ip
),
instructions: [
ApplyActions.new([
SetField.new({:eth_dst, state.receiver_mac}),
SetField.new({:ipv4_src, state.outside_local}),
Output.new(state.trunk_port.number)
])]
],
[table_id: 1,
priority: 30,
cookie: 0x3000000000000001,
match: Match.new(
eth_dst: state.sender_mac,
eth_src: state.receiver_mac,
eth_type: 0x0800,
ipv4_src: state.receiver_ip,
ipv4_dst: state.outside_local
),
instructions: [
ApplyActions.new([
SetField.new({:eth_src, state.receiver_mac}),
SetField.new({:ipv4_dst, state.inside_local}),
Output.new(state.access_port1.number)
])]
],
] ++ classifier(state)
end
def flows(%Heckle.Controller.State{flow_pattern: :bum} = state) do
[[table_id: 1,
priority: 30,
cookie: 0x3000000000000001,
match: Match.new(
vlan_vid: state.vlan_id,
eth_dst: @mcast
),
instructions: [
ApplyActions.new([
Openflow.Action.Output.new(state.trunk_port.number),
Openflow.Action.PopVlan.new,
Openflow.Action.Output.new(state.access_port1.number),
Openflow.Action.Output.new(state.access_port2.number)
])]
]] ++ classifier(state)
end
# private functions
defp classifier(state) do
[
[table_id: 0,
priority: 50,
cookie: 0x1000000000000001,
match: Match.new(
in_port: state.trunk_port.number,
vlan_vid: state.vlan_id
),
instructions: [
ApplyActions.new([
PushVlan.new,
SetField.new({:vlan_vid, state.vlan_id})
]),
GotoTable.new(1)
]
],
[table_id: 0,
priority: 20,
cookie: 0x1000000000000001,
match: Match.new(
in_port: state.access_port1.number,
eth_src: state.sender_mac
),
instructions: [
ApplyActions.new([
PushVlan.new,
SetField.new({:vlan_vid, state.vlan_id})
]),
GotoTable.new(1)
]
],
[table_id: 0,
priority: 20,
cookie: 0x1000000000000001,
match: Match.new(
in_port: state.access_port2.number,
eth_src: state.sender_mac
),
instructions: [
ApplyActions.new([
PushVlan.new,
SetField.new({:vlan_vid, state.vlan_id})
]),
GotoTable.new(1)
]
],
]
end
end

View file

@ -0,0 +1,31 @@
defmodule Heckle.PipelineProfiles do
use Tres.Controller
def of_aruba do
[
TableFeatures.Body.new(
table_id: 0,
name: "classifier",
max_entries: 10,
config: [:table_miss_mask], # deprecated mask.
match: [:in_port, :masked_eth_src],
wildcards: [:in_port, :masked_eth_src],
instructions: [GotoTable],
write_actions: [Output],
next_tables: [1],
),
TableFeatures.Body.new(
table_id: 1,
name: "NAT",
max_entries: 10,
config: [:table_miss_mask],
match: [:eth_dst, :eth_src, :eth_type, :ipv4_src, :ipv4_dst],
wildcards: [:eth_dst, :eth_src, :eth_type, :ipv4_src, :ipv4_dst],
instructions: [ApplyActions],
write_actions: [SetField, PopVlan, PushVlan, Output],
apply_setfield: [:eth_dst, :vlan_vid, :ipv4_src, :ipv4_dst],
next_tables: [],
)
]
end
end

22
examples/heckle/mix.exs Normal file
View file

@ -0,0 +1,22 @@
defmodule Heckle.Mixfile do
use Mix.Project
@tres_path "../../../tres"
def project do
[app: :heckle,
version: "0.1.0",
elixir: "~> 1.5",
start_permanent: Mix.env == :prod,
deps: deps()]
end
def application do
[extra_applications: [:logger, :tres],
mod: {Heckle.Application, []}]
end
defp deps do
[{:tres, path: @tres_path}]
end
end

5
examples/heckle/mix.lock Normal file
View file

@ -0,0 +1,5 @@
%{"binpp": {:git, "https://github.com/jtendo/binpp.git", "64bd68d215d1a6cd35871e7c134d7fe2e46214ea", [branch: "master"]},
"eovsdb": {:git, "https://github.com/shun159/eovsdb.git", "1ff1572708d72fd25631c681f2102407903252a3", [branch: "master"]},
"jsone": {:git, "https://github.com/sile/jsone.git", "eecc9666c7165e1870b78a7a762549ae8d1c391b", [tag: "1.2.1"]},
"ranch": {:hex, :ranch, "1.4.0", "10272f95da79340fa7e8774ba7930b901713d272905d0012b06ca6d994f8826b", [], [], "hexpm"},
"uuid": {:git, "https://github.com/avtobiff/erlang-uuid.git", "585c2474afb4a597ae8c8bf6d21e5a9c73f18e0b", [tag: "v0.5.0"]}}

View file

@ -0,0 +1,8 @@
defmodule HeckleTest do
use ExUnit.Case
doctest Heckle
test "greets the world" do
assert Heckle.hello() == :world
end
end

View file

@ -0,0 +1 @@
ExUnit.start()