Add simple LearningSwitch Example
This commit is contained in:
parent
ab21f6e240
commit
f2768496c8
11 changed files with 334 additions and 0 deletions
20
examples/learning_switch/.gitignore
vendored
Normal file
20
examples/learning_switch/.gitignore
vendored
Normal 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/learning_switch/README.md
Normal file
21
examples/learning_switch/README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# LearningSwitch
|
||||||
|
|
||||||
|
**TODO: Add description**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||||
|
by adding `learning_switch` to your list of dependencies in `mix.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
def deps do
|
||||||
|
[
|
||||||
|
{:learning_switch, "~> 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/learning_switch](https://hexdocs.pm/learning_switch).
|
||||||
|
|
||||||
17
examples/learning_switch/config/config.exs
Normal file
17
examples/learning_switch/config/config.exs
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# This file is responsible for configuring your application
|
||||||
|
# and its dependencies with the aid of the Mix.Config module.
|
||||||
|
use Mix.Config
|
||||||
|
|
||||||
|
config :tres,
|
||||||
|
protocol: :tcp,
|
||||||
|
port: 6653,
|
||||||
|
max_connections: 10,
|
||||||
|
num_acceptors: 10,
|
||||||
|
callback_module: LearningSwitch.Ofctl,
|
||||||
|
callback_args: []
|
||||||
|
|
||||||
|
config :logger,
|
||||||
|
level: :debug,
|
||||||
|
format: "$date $time [$level] $metadata$message\n",
|
||||||
|
metadata: [:application],
|
||||||
|
handle_otp_reports: true
|
||||||
18
examples/learning_switch/lib/learning_switch.ex
Normal file
18
examples/learning_switch/lib/learning_switch.ex
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule LearningSwitch do
|
||||||
|
@moduledoc """
|
||||||
|
Documentation for LearningSwitch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Hello world.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> LearningSwitch.hello
|
||||||
|
:world
|
||||||
|
|
||||||
|
"""
|
||||||
|
def hello do
|
||||||
|
:world
|
||||||
|
end
|
||||||
|
end
|
||||||
20
examples/learning_switch/lib/learning_switch/application.ex
Normal file
20
examples/learning_switch/lib/learning_switch/application.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
defmodule LearningSwitch.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: LearningSwitch.Worker.start_link(arg)
|
||||||
|
# {LearningSwitch.Worker, arg},
|
||||||
|
]
|
||||||
|
|
||||||
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
# for other strategies and supported options
|
||||||
|
opts = [strategy: :one_for_one, name: LearningSwitch.Supervisor]
|
||||||
|
Supervisor.start_link(children, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
66
examples/learning_switch/lib/learning_switch/fdb.ex
Normal file
66
examples/learning_switch/lib/learning_switch/fdb.ex
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
defmodule LearningSwitch.FDB do
|
||||||
|
use Agent
|
||||||
|
|
||||||
|
defmodule Entry do
|
||||||
|
defstruct [
|
||||||
|
mac: nil,
|
||||||
|
port_no: nil,
|
||||||
|
age_max: 0,
|
||||||
|
last_update: 0
|
||||||
|
]
|
||||||
|
|
||||||
|
def new(mac, port_no, age_max \\ 180) do
|
||||||
|
%Entry{
|
||||||
|
mac: mac,
|
||||||
|
port_no: port_no,
|
||||||
|
age_max: age_max,
|
||||||
|
last_update: :os.timestamp
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(self, port_no) do
|
||||||
|
%{self|port_no: port_no, last_update: :os.timestamp}
|
||||||
|
end
|
||||||
|
|
||||||
|
def aged_out?(self) do
|
||||||
|
:timer.now_diff(:os.timestamp, self.last_update) > (1000 * self.age_max)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link do
|
||||||
|
Agent.start_link(&Map.new/0)
|
||||||
|
end
|
||||||
|
|
||||||
|
def lookup(self, mac) do
|
||||||
|
entry = Agent.get(self, &Map.get(&1, mac))
|
||||||
|
entry && entry.port_no
|
||||||
|
end
|
||||||
|
|
||||||
|
def learn(self, mac, port_no) do
|
||||||
|
entry = Agent.get(self, &Map.get(&1, mac))
|
||||||
|
entry = if entry do
|
||||||
|
Entry.update(entry, port_no)
|
||||||
|
else
|
||||||
|
Entry.new(mac, port_no)
|
||||||
|
end
|
||||||
|
Agent.update(self, &Map.put(&1, mac, entry))
|
||||||
|
end
|
||||||
|
|
||||||
|
def age(self) do
|
||||||
|
mac_addrs = Agent.get(self, &find_aged_entries(&1))
|
||||||
|
for mac <- mac_addrs do
|
||||||
|
Agent.update(self, &Map.delete(&1, mac))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# private function
|
||||||
|
|
||||||
|
defp find_aged_entries(map) do
|
||||||
|
Enum.flat_map(map, fn({mac, entry}) ->
|
||||||
|
case Entry.aged_out?(entry) do
|
||||||
|
true -> [mac]
|
||||||
|
false -> []
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
138
examples/learning_switch/lib/learning_switch/ofctl.ex
Normal file
138
examples/learning_switch/lib/learning_switch/ofctl.ex
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
defmodule LearningSwitch.Ofctl do
|
||||||
|
use GenServer
|
||||||
|
use Tres.Controller
|
||||||
|
|
||||||
|
import Logger
|
||||||
|
|
||||||
|
alias LearningSwitch.FDB
|
||||||
|
|
||||||
|
@ingress_filtering_table_id 0
|
||||||
|
@forwarding_table_id 1
|
||||||
|
|
||||||
|
@aging_time 180
|
||||||
|
|
||||||
|
@mcast {"010000000000", "110000000000"}
|
||||||
|
@bcast "ffffffffffff"
|
||||||
|
@ipv6_mcast {"333300000000", "ffff00000000"}
|
||||||
|
|
||||||
|
defmodule State do
|
||||||
|
defstruct [
|
||||||
|
datapath_id: nil,
|
||||||
|
conn_ref: nil,
|
||||||
|
fdb_pid: nil
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_link(datapath_id, args) do
|
||||||
|
GenServer.start_link(__MODULE__, [datapath_id, args])
|
||||||
|
end
|
||||||
|
|
||||||
|
def init([datapath_id, _args]) do
|
||||||
|
:ok = debug("Switch Ready: datapath_id: #{inspect(datapath_id)}")
|
||||||
|
conn_ref = SwitchRegistry.monitor(datapath_id)
|
||||||
|
{:ok, pid} = FDB.start_link
|
||||||
|
init_datapath(datapath_id)
|
||||||
|
state = %State{
|
||||||
|
datapath_id: datapath_id,
|
||||||
|
conn_ref: conn_ref,
|
||||||
|
fdb_pid: pid
|
||||||
|
}
|
||||||
|
{:ok, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info(%PacketIn{} = packet_in, state) do
|
||||||
|
<<_dhost::6-bytes, shost::6-bytes, _rest::bytes>> = packet_in.data
|
||||||
|
eth_src = Openflow.Utils.to_hex_string(shost)
|
||||||
|
FDB.learn(state.fdb_pid, eth_src, packet_in.in_port)
|
||||||
|
add_forwarding_flow_and_packet_out(packet_in, state)
|
||||||
|
:ok = debug("PacketIn: eth_src: #{eth_src} datapath_id: #{inspect(state.datapath_id)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
def handle_info({:'DOWN', ref, :process, _pid, _reason}, %State{conn_ref: ref} = state) do
|
||||||
|
:ok = debug("Switch Disconnected: datapath_id: #{inspect(state.datapath_id)}")
|
||||||
|
{:stop, :normal, state}
|
||||||
|
end
|
||||||
|
def handle_info(info, state) do
|
||||||
|
:ok = warn("Unhandled message #{inspect(info)}: #{inspect(state.datapath_id)}")
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
# private functions
|
||||||
|
|
||||||
|
defp init_datapath(datapath_id) do
|
||||||
|
init_flow_tables(datapath_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp init_flow_tables(datapath_id) do
|
||||||
|
for flow_options <- [
|
||||||
|
add_default_broadcast_flow_entry(),
|
||||||
|
add_default_flooding_flow_entry(),
|
||||||
|
add_multicast_mac_drop_flow_entry(),
|
||||||
|
add_ipv6_multicast_mac_drop_flow_entry(),
|
||||||
|
add_default_forwarding_flow_entry()] do
|
||||||
|
send_flow_mod_add(datapath_id, flow_options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_forwarding_flow_and_packet_out(packet_in, state) do
|
||||||
|
<<dhost::6-bytes, _shost::6-bytes, _rest::bytes>> = packet_in.data
|
||||||
|
eth_dst = Openflow.Utils.to_hex_string(dhost)
|
||||||
|
port_no = FDB.lookup(state.fdb_pid, eth_dst)
|
||||||
|
add_forwarding_flow_entry(packet_in, port_no)
|
||||||
|
packet_out(packet_in, port_no || :flood)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp packet_out(%PacketIn{datapath_id: datapath_id, data: data}, port_no) do
|
||||||
|
send_packet_out(
|
||||||
|
datapath_id,
|
||||||
|
data: data,
|
||||||
|
actions: [Output.new(port_no)]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_forwarding_flow_entry(_packet_in, nil), do: :noop
|
||||||
|
defp add_forwarding_flow_entry(%PacketIn{datapath_id: datapath_id, data: data} = packet_in, port_no) do
|
||||||
|
<<dhost::6-bytes, shost::6-bytes, _rest::bytes>> = data
|
||||||
|
send_flow_mod_add(
|
||||||
|
datapath_id,
|
||||||
|
idle_timeout: @aging_time,
|
||||||
|
priority: 2,
|
||||||
|
match: Match.new(
|
||||||
|
in_port: packet_in.in_port,
|
||||||
|
eth_dst: Openflow.Utils.to_hex_string(dhost),
|
||||||
|
eth_src: Openflow.Utils.to_hex_string(shost)),
|
||||||
|
instructions: [ApplyActions.new(Output.new(port_no))]
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_default_broadcast_flow_entry do
|
||||||
|
[table_id: @forwarding_table_id,
|
||||||
|
priority: 3,
|
||||||
|
match: Match.new(eth_dst: @bcast),
|
||||||
|
instructions: [ApplyActions.new(Output.new(:flood))]]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_default_flooding_flow_entry do
|
||||||
|
[table_id: @forwarding_table_id,
|
||||||
|
priority: 1,
|
||||||
|
instructions: [ApplyActions.new(Output.new(:controller))]]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_multicast_mac_drop_flow_entry do
|
||||||
|
[table_id: @ingress_filtering_table_id,
|
||||||
|
priority: 2,
|
||||||
|
match: Match.new(eth_dst: @mcast)]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_ipv6_multicast_mac_drop_flow_entry do
|
||||||
|
[table_id: @ingress_filtering_table_id,
|
||||||
|
priority: 2,
|
||||||
|
match: Match.new(eth_dst: @ipv6_mcast)]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp add_default_forwarding_flow_entry do
|
||||||
|
[table_id: @ingress_filtering_table_id,
|
||||||
|
priority: 1,
|
||||||
|
instructions: [GotoTable.new(@forwarding_table_id)]]
|
||||||
|
end
|
||||||
|
end
|
||||||
23
examples/learning_switch/mix.exs
Normal file
23
examples/learning_switch/mix.exs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
defmodule LearningSwitch.Mixfile do
|
||||||
|
use Mix.Project
|
||||||
|
|
||||||
|
def project do
|
||||||
|
[
|
||||||
|
app: :learning_switch,
|
||||||
|
version: "0.1.0",
|
||||||
|
elixir: "~> 1.5",
|
||||||
|
start_permanent: Mix.env == :prod,
|
||||||
|
deps: deps()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Run "mix help compile.app" to learn about applications.
|
||||||
|
def application do
|
||||||
|
[extra_applications: [:logger, :tres],
|
||||||
|
mod: {LearningSwitch.Application, []}]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp deps do
|
||||||
|
[{:tres, path: "../../../tres"}]
|
||||||
|
end
|
||||||
|
end
|
||||||
2
examples/learning_switch/mix.lock
Normal file
2
examples/learning_switch/mix.lock
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
%{"binpp": {:git, "https://github.com/jtendo/binpp.git", "64bd68d215d1a6cd35871e7c134d7fe2e46214ea", [branch: "master"]},
|
||||||
|
"ranch": {:hex, :ranch, "1.4.0", "10272f95da79340fa7e8774ba7930b901713d272905d0012b06ca6d994f8826b", [], [], "hexpm"}}
|
||||||
8
examples/learning_switch/test/learning_switch_test.exs
Normal file
8
examples/learning_switch/test/learning_switch_test.exs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
defmodule LearningSwitchTest do
|
||||||
|
use ExUnit.Case
|
||||||
|
doctest LearningSwitch
|
||||||
|
|
||||||
|
test "greets the world" do
|
||||||
|
assert LearningSwitch.hello() == :world
|
||||||
|
end
|
||||||
|
end
|
||||||
1
examples/learning_switch/test/test_helper.exs
Normal file
1
examples/learning_switch/test/test_helper.exs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue