diff --git a/README.md b/README.md index 7c90bad..d323ecf 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ $ iex -S mix - learning-switch: Simple Layer2 switch - leader-example: Simple election based multiple controller using Ulf Wiger's Locks Leader - patch\_panel: inteligent patch\_panel example +- simple\_router: An OpenFlow controller that emulates layer 3 switch (router). License ------- diff --git a/bin/enum_gen b/bin/enum_gen index 4fe9e79..6c0d6d5 100755 Binary files a/bin/enum_gen and b/bin/enum_gen differ diff --git a/examples/simple_router/.formatter.exs b/examples/simple_router/.formatter.exs new file mode 100644 index 0000000..d2cda26 --- /dev/null +++ b/examples/simple_router/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/examples/simple_router/.gitignore b/examples/simple_router/.gitignore new file mode 100644 index 0000000..3075870 --- /dev/null +++ b/examples/simple_router/.gitignore @@ -0,0 +1,24 @@ +# 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 + +# Ignore package tarball (built via "mix hex.build"). +simple_router-*.tar + diff --git a/examples/simple_router/README.md b/examples/simple_router/README.md new file mode 100644 index 0000000..878110f --- /dev/null +++ b/examples/simple_router/README.md @@ -0,0 +1,47 @@ +# SimpleRouter + +#### An OpenFlow controller that emulates layer 3 switch (router). + +## Configuration + +```elixir +config :simple_router, + interfaces: %{ + "veth1" => %{mac_address: "02:00:00:01:00:01", ip_address: "192.168.1.1/24"}, + "veth3" => %{mac_address: "02:00:00:01:00:02", ip_address: "192.168.2.1/24"}, + }, + routes: %{ + "0.0.0.0/0" => "192.168.1.2" + } +``` + +## Flow Tables + +``` +> $ sudo ovs-ofctl dump-flows br0 -OOpenFlow13 --color=auto + cookie=0x0, duration=10.217s, table=0, n_packets=3, n_bytes=126, priority=1,arp actions=goto_table:1 + cookie=0x0, duration=10.217s, table=0, n_packets=19, n_bytes=1862, priority=1,ip actions=goto_table:2 + cookie=0x0, duration=10.208s, table=1, n_packets=1, n_bytes=42, priority=0,arp,in_port=veth1,arp_tpa=192.168.1.1,arp_op=1 actions=learn(table=4,idle_timeout=300,priority=1,NXM_NX_REG0[]=NXM_OF_ARP_SPA[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]),push:NXM_OF_ARP_TPA[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],push:NXM_OF_ETH_SRC[],pop:NXM_OF_ETH_DST[],pop:NXM_OF_ARP_TPA[],pop:NXM_NX_ARP_THA[],pop:NXM_OF_ARP_SPA[],set_field:2->arp_op,set_field:02:00:00:01:00:01->arp_sha,set_field:02:00:00:01:00:01->eth_src,IN_PORT + cookie=0x0, duration=10.207s, table=1, n_packets=1, n_bytes=42, priority=0,arp,in_port=veth1,arp_tpa=192.168.1.1,arp_op=2 actions=learn(table=4,idle_timeout=300,priority=1,NXM_NX_REG0[]=NXM_OF_ARP_SPA[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]) + cookie=0x0, duration=10.206s, table=1, n_packets=1, n_bytes=42, priority=0,arp,in_port=veth3,arp_tpa=192.168.2.1,arp_op=1 actions=learn(table=4,idle_timeout=300,priority=1,NXM_NX_REG0[]=NXM_OF_ARP_SPA[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]),push:NXM_OF_ARP_TPA[],push:NXM_NX_ARP_SHA[],push:NXM_OF_ARP_SPA[],push:NXM_OF_ETH_SRC[],pop:NXM_OF_ETH_DST[],pop:NXM_OF_ARP_TPA[],pop:NXM_NX_ARP_THA[],pop:NXM_OF_ARP_SPA[],set_field:2->arp_op,set_field:02:00:00:01:00:02->arp_sha,set_field:02:00:00:01:00:02->eth_src,IN_PORT + cookie=0x0, duration=10.206s, table=1, n_packets=0, n_bytes=0, priority=0,arp,in_port=veth3,arp_tpa=192.168.2.1,arp_op=2 actions=learn(table=4,idle_timeout=300,priority=1,NXM_NX_REG0[]=NXM_OF_ARP_SPA[],load:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]) + cookie=0x0, duration=10.205s, table=2, n_packets=0, n_bytes=0, priority=124,ip,nw_dst=192.168.1.0/24 actions=move:NXM_OF_IP_DST[]->NXM_NX_REG0[],goto_table:3 + cookie=0x0, duration=10.205s, table=2, n_packets=9, n_bytes=882, priority=124,ip,nw_dst=192.168.2.0/24 actions=move:NXM_OF_IP_DST[]->NXM_NX_REG0[],goto_table:3 + cookie=0x0, duration=10.205s, table=2, n_packets=10, n_bytes=980, priority=0,ip actions=set_field:0xc0a80102->reg0,goto_table:3 + cookie=0x0, duration=10.212s, table=3, n_packets=10, n_bytes=980, priority=24,reg0=0xc0a80100/0xffffff00 actions=set_field:0x1->reg1,resubmit(,4),resubmit(,5) + cookie=0x0, duration=10.212s, table=3, n_packets=9, n_bytes=882, priority=24,reg0=0xc0a80200/0xffffff00 actions=set_field:0x2->reg1,resubmit(,4),resubmit(,5) + cookie=0x0, duration=9.891s, table=4, n_packets=9, n_bytes=882, idle_timeout=300, priority=1,reg0=0xc0a80202 actions=load:0x9a9114dd25d8->NXM_OF_ETH_DST[] + cookie=0x0, duration=9.855s, table=4, n_packets=9, n_bytes=882, idle_timeout=300, priority=1,reg0=0xc0a80102 actions=load:0x7a69d574651c->NXM_OF_ETH_DST[] + cookie=0x0, duration=10.207s, table=4, n_packets=1, n_bytes=98, priority=0,reg1=0x1 actions=controller(userdata=41.52.50.5f.6d.69.73.73.69.6e.67.ff.ff.ff.ff.ff.ff.02.00.00.01.00.01.08.06.00.01.08.00.06.04.00.01.02.00.00.01.00.01.c0.a8.01.01.00.00.00.00.00.00,pause) + cookie=0x0, duration=10.206s, table=4, n_packets=0, n_bytes=0, priority=0,reg1=0x2 actions=controller(userdata=41.52.50.5f.6d.69.73.73.69.6e.67.ff.ff.ff.ff.ff.ff.02.00.00.01.00.02.08.06.00.01.08.00.06.04.00.01.02.00.00.01.00.02.c0.a8.02.01.00.00.00.00.00.00,pause) + cookie=0x0, duration=10.206s, table=5, n_packets=9, n_bytes=882, priority=1,reg1=0x1 actions=group:1 + cookie=0x0, duration=10.205s, table=5, n_packets=9, n_bytes=882, priority=1,reg1=0x2 actions=group:2 + ``` + +## Group Tables + ``` +> $ sudo ovs-ofctl dump-groups br0 -OOpenFlow13 --color=auto +OFPST_GROUP_DESC reply (OF1.3) (xid=0x2): + group_id=1,type=indirect,bucket=actions=set_field:02:00:00:01:00:01->eth_src,output:veth1 + group_id=2,type=indirect,bucket=actions=set_field:02:00:00:01:00:02->eth_src,output:veth3 +``` diff --git a/examples/simple_router/config/config.exs b/examples/simple_router/config/config.exs new file mode 100644 index 0000000..899635d --- /dev/null +++ b/examples/simple_router/config/config.exs @@ -0,0 +1,20 @@ +# 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: SimpleRouter.Openflow.Controller, + callback_args: [] + +config :simple_router, + interfaces: %{ + "veth1" => %{mac_address: "02:00:00:01:00:01", ip_address: "192.168.1.1/24"}, + "veth3" => %{mac_address: "02:00:00:01:00:02", ip_address: "192.168.2.1/24"}, + }, + routes: %{ + "0.0.0.0/0" => "192.168.1.2" + } diff --git a/examples/simple_router/lib/simple_router.ex b/examples/simple_router/lib/simple_router.ex new file mode 100644 index 0000000..7eccb34 --- /dev/null +++ b/examples/simple_router/lib/simple_router.ex @@ -0,0 +1,18 @@ +defmodule SimpleRouter do + @moduledoc """ + Documentation for SimpleRouter. + """ + + @doc """ + Hello world. + + ## Examples + + iex> SimpleRouter.hello() + :world + + """ + def hello do + :world + end +end diff --git a/examples/simple_router/lib/simple_router/application.ex b/examples/simple_router/lib/simple_router/application.ex new file mode 100644 index 0000000..6843961 --- /dev/null +++ b/examples/simple_router/lib/simple_router/application.ex @@ -0,0 +1,9 @@ +defmodule SimpleRouter.Application do + @moduledoc false + + use Application + + def start(_type, _args) do + SimpleRouter.Supervisor.start_link() + end +end diff --git a/examples/simple_router/lib/simple_router/config.ex b/examples/simple_router/lib/simple_router/config.ex new file mode 100644 index 0000000..cba4f9c --- /dev/null +++ b/examples/simple_router/lib/simple_router/config.ex @@ -0,0 +1,55 @@ +defmodule SimpleRouter.Config do + @moduledoc false + + alias Tres.IPv4Address + + @spec interfaces() :: %{String.t() => map()} + def interfaces do + :interfaces + |> get_env(%{}) + |> Enum.reduce(%{}, &interfaces_to_erl/2) + end + + @spec routes() :: [tuple()] + def routes do + :routes + |> get_env(%{}) + |> Enum.reduce([], &routes_to_erl/2) + end + + # private functions + + defp interfaces_to_erl({ifname, %{mac_address: mac, ip_address: ip}}, acc) do + {ipaddr, mask} = IPv4Address.parse(ip) + + entry = + %{ + mac_address: String.replace(mac, ~r/:/, ""), + ip_address: ipaddr, + subnet_mask: mask, + network_address: IPv4Address.to_network({ipaddr, mask}), + prefix_length: get_cidr(ip) + } + + Map.merge(acc, %{ifname => entry}) + end + + defp routes_to_erl({dst, nexthop}, acc) do + {dstaddr, mask} = IPv4Address.parse(dst) + {nexthop, _netmask} = IPv4Address.parse(nexthop) + entry = %{dst: dstaddr, mask: mask, prefix_length: get_cidr(dst), nexthop: nexthop} + [entry | acc] + end + + defp get_cidr(ip) do + case String.split(ip, ~r/\//) do + [^ip] -> + 32 + [_addr, cidr] -> + String.to_integer(cidr) + end + end + + defp get_env(key, default), + do: Application.get_env(:simple_router, key, default) +end diff --git a/examples/simple_router/lib/simple_router/openflow/controller.ex b/examples/simple_router/lib/simple_router/openflow/controller.ex new file mode 100644 index 0000000..95b2146 --- /dev/null +++ b/examples/simple_router/lib/simple_router/openflow/controller.ex @@ -0,0 +1,137 @@ +defmodule SimpleRouter.Openflow.Controller do + @moduledoc """ + Emulates Router + """ + + use GenServer + use Tres.Controller + + import Logger + + alias SimpleRouter.Config + alias SimpleRouter.Openflow.FlowTables + alias SimpleRouter.Openflow.GroupTables + + defmodule State do + @moduledoc false + + defstruct [ + datapath_id: nil, + ports: %{} + ] + end + + def start_link([{datapath_id, _aux_id}, _start_args]) do + GenServer.start_link(__MODULE__, [datapath_id]) + end + + def init([datapath_id]) do + :ok = debug("Switch Connected: datapath_id = #{datapath_id}") + {:ok, %State{datapath_id: datapath_id}, {:continue, :init_datapath}} + end + + def handle_continue(:init_datapath, state) do + :ok = init_datapath(state.datapath_id) + {:noreply, state, {:continue, :init_interface}} + end + + def handle_continue(:init_interface, state) do + port_desc = PortDesc.Request.new() + :ok = send_message(port_desc, state.datapath_id) + {:noreply, state} + end + + def handle_info(%PortDesc.Reply{datapath_id: dpid, ports: ports}, state) do + :ok = debug("port_reply from dpid: #{dpid}") + ports = handle_ports(ports) + routes = Config.routes() + :ok = FlowTables.classifier(dpid) + :ok = init_egress_groups(ports, dpid) + :ok = init_interface_lookup_flows(ports, dpid) + :ok = init_arp_handler_flows(ports, dpid) + :ok = init_egress_flows(ports, dpid) + :ok = init_connected_routes(ports, dpid) + :ok = init_static_routes(routes, dpid) + {:noreply, %{state | ports: ports}} + end + + def handle_info(%NxPacketIn2{userdata: <<"ARP_missing", arp_req::bytes>>} = pin, state) do + :ok = debug("ARP Entry missing: #{inspect(pin)}") + nexthop_address = pin.metadata[:reg0] + + send_packet_out( + packet_in: pin, + metadata: Keyword.replace!(pin.metadata, :in_port, :controller), + data: <> + ) + + {:noreply, state} + end + + def handle_info(info, state) do + :ok = warn("Unhandled info received: #{inspect(info)}") + {:noreply, state} + end + + # private functions + + @spec handle_ports([%Openflow.Port{}]) :: [map()] + defp handle_ports(ports) do + ifaces = Config.interfaces() + handle_ports([], ports, ifaces) + end + + defp handle_ports(acc, [], _ifaces), do: acc + + defp handle_ports(acc0, [port|rest], ifaces) do + case Map.get(ifaces, port.name) do + iface0 when is_map(iface0) -> + iface = Map.merge(iface0, %{number: port.number}) + handle_ports([iface|acc0], rest, ifaces) + _ -> + handle_ports(acc0, rest, ifaces) + end + end + + @spec init_egress_groups([map()], String.t()) :: :ok + defp init_egress_groups(ifaces, datapath_id), + do: Enum.each(ifaces, &GroupTables.egress(&1, datapath_id)) + + @spec init_interface_lookup_flows([map()], String.t()) :: :ok + defp init_interface_lookup_flows(ifaces, datapath_id), + do: Enum.each(ifaces, &FlowTables.lookup_iface(&1, datapath_id)) + + @spec init_arp_handler_flows([map()], String.t()) :: :ok + defp init_arp_handler_flows(ifaces, datapath_id), + do: Enum.each(ifaces, &FlowTables.arp_handlers(&1, datapath_id)) + + @spec init_egress_flows([map()], String.t()) :: :ok + defp init_egress_flows(ifaces, datapath_id), + do: Enum.each(ifaces, &FlowTables.egress(&1, datapath_id)) + + @spec init_connected_routes([map()], String.t()) :: :ok + defp init_connected_routes(ifaces, datapath_id), + do: Enum.each(ifaces, &FlowTables.connected_route(&1, datapath_id)) + + @spec init_static_routes([map()], String.t()) :: :ok + defp init_static_routes(routes, datapath_id), + do: Enum.each(routes, &FlowTables.static_route(&1, datapath_id)) + + @spec init_datapath(String.t()) :: :ok + defp init_datapath(datapath_id) do + :ok = set_config(datapath_id) + :ok = set_packet_in_format(datapath_id) + end + + @spec set_config(String.t()) :: :ok + defp set_config(datapath_id) do + set_config = SetConfig.new(miss_send_len: :no_buffer) + :ok = send_message(set_config, datapath_id) + end + + @spec set_packet_in_format(String.t()) :: :ok + defp set_packet_in_format(datapath_id) do + pin_format = NxSetPacketInFormat.new(:nxt_packet_in2) + :ok = send_message(pin_format, datapath_id) + end +end diff --git a/examples/simple_router/lib/simple_router/openflow/flow_tables.ex b/examples/simple_router/lib/simple_router/openflow/flow_tables.ex new file mode 100644 index 0000000..ec216b5 --- /dev/null +++ b/examples/simple_router/lib/simple_router/openflow/flow_tables.ex @@ -0,0 +1,222 @@ +defmodule SimpleRouter.Openflow.FlowTables do + @moduledoc """ + Flow Table Definitions + """ + + use Tres.Controller + + alias Tres.IPv4Address + alias Tres.MacAddress + + @classifier_table_id 0 + @arp_handler_table_id 1 + @routing_table_id 2 + @interface_lookup_table_id 3 + @arp_lookup_table_id 4 + @egress_table_id 5 + + def classifier(datapath_id) do + # ARP + send_flow_mod_add( + datapath_id, + table_id: @classifier_table_id, + priority: 1, + match: Match.new(eth_type: 0x0806), + instructions: [GotoTable.new(@arp_handler_table_id)] + ) + + # IPv4 + send_flow_mod_add( + datapath_id, + table_id: @classifier_table_id, + priority: 1, + match: Match.new(eth_type: 0x0800), + instructions: [GotoTable.new(@routing_table_id)] + ) + end + + def connected_route(iface, datapath_id) do + send_flow_mod_add( + datapath_id, + table_id: @routing_table_id, + priority: 100 + iface.prefix_length, + match: Match.new( + eth_type: 0x0800, + ipv4_dst: {iface.network_address, iface.subnet_mask} + ), + instructions: [ + ApplyActions.new(NxRegMove.new(src_field: :ipv4_dst, dst_field: :reg0)), + GotoTable.new(@interface_lookup_table_id) + ] + ) + end + + def static_route(route, datapath_id) do + send_flow_mod_add( + datapath_id, + table_id: @routing_table_id, + priority: route.prefix_length, + match: Match.new( + eth_type: 0x0800, + ipv4_dst: {route.dst, route.mask} + ), + instructions: [ + ApplyActions.new(SetField.new({:reg0, IPv4Address.to_int(route.nexthop)})), + GotoTable.new(@interface_lookup_table_id) + ] + ) + end + + def arp_handlers(iface, datapath_id) do + :ok = arp_request_handler(iface, datapath_id) + :ok = arp_reply_handler(iface, datapath_id) + :ok = arp_entry_is_missing(iface, datapath_id) + end + + def arp_entry_is_missing(iface, datapath_id) do + send_flow_mod_add( + datapath_id, + priority: 0, + table_id: @arp_lookup_table_id, + match: Match.new(reg1: iface.number), + instructions: [ + ApplyActions.new( + NxController2.new(userdata: arp_packet(iface), pause: true) + ) + ] + ) + end + + def lookup_iface(iface, datapath_id) do + match = + Match.new( + reg0: { + IPv4Address.to_int(iface.network_address), + IPv4Address.to_int(iface.subnet_mask) + } + ) + + send_flow_mod_add( + datapath_id, + table_id: @interface_lookup_table_id, + priority: iface.prefix_length, + match: match, + instructions: [ + ApplyActions.new([ + SetField.new({:reg1, iface.number}), + NxResubmitTable.new(@arp_lookup_table_id), + NxResubmitTable.new(@egress_table_id) + ]) + ] + ) + end + + def egress(iface, datapath_id) do + send_flow_mod_add( + datapath_id, + table_id: @egress_table_id, + priority: 1, + match: Match.new(reg1: iface.number), + instructions: ApplyActions.new(Group.new(iface.number)) + ) + end + + # private functions + + defp arp_request_handler(iface, datapath_id) do + send_flow_mod_add( + datapath_id, + table_id: @arp_handler_table_id, + priority: 0, + match: Match.new( + in_port: iface.number, + eth_type: 0x0806, + # Request + arp_op: 0x1, + arp_tpa: iface.ip_address + ), + instructions: ApplyActions.new(arp_respond_and_learn_actions(iface.mac_address)) + ) + end + + defp arp_reply_handler(iface, datapath_id) do + send_flow_mod_add( + datapath_id, + table_id: @arp_handler_table_id, + priority: 0, + match: Match.new( + in_port: iface.number, + eth_type: 0x0806, + # Reply + arp_op: 0x2, + arp_tpa: iface.ip_address + ), + instructions: ApplyActions.new(learn_arp_reply_actions()) + ) + end + + defp arp_respond_and_learn_actions(mac) do + [ + NxLearn.new( + priority: 1, + table_id: @arp_lookup_table_id, + idle_timeout: 300, + flow_specs: [ + NxFlowSpecMatch.new(src: :arp_spa, dst: :reg0), + NxFlowSpecLoad.new(src: :eth_src, dst: :eth_dst) + ] + ), + NxStackPush.new(field: :arp_tpa), + NxStackPush.new(field: :arp_sha), + NxStackPush.new(field: :arp_spa), + NxStackPush.new(field: :eth_src), + NxStackPop.new(field: :eth_dst), + NxStackPop.new(field: :arp_tpa), + NxStackPop.new(field: :arp_tha), + NxStackPop.new(field: :arp_spa), + SetField.new({:arp_op, 0x2}), + SetField.new({:arp_sha, mac}), + SetField.new({:eth_src, mac}), + Output.new(:in_port) + ] + end + + defp learn_arp_reply_actions do + [ + NxLearn.new( + priority: 1, + table_id: @arp_lookup_table_id, + idle_timeout: 300, + flow_specs: [ + NxFlowSpecMatch.new(src: :arp_spa, dst: :reg0), + NxFlowSpecLoad.new(src: :eth_src, dst: :eth_dst) + ] + ) + ] + end + + @arphrd_ether 1 + @eth_p_ip 0x0800 + + defp arp_packet(iface) do + ether_header = << + 0xffffffffffff::48, # destination ethernet address + (MacAddress.to_i(iface.mac_address))::48, # source ethernet address + 0x0806::16 # ethernet type + >> + + # Target Protocol Address will append in NX_PACKET_IN2 + arp_header = << + @arphrd_ether::16, # hardware type + @eth_p_ip::16, # protocol type + 6::8, # hardware address length + 4::8, # protocol address length + 1::16, # ARPOP_REQUEST + (MacAddress.to_i(iface.mac_address))::48, # Source Hardware Address + (IPv4Address.to_int(iface.ip_address))::32, # Source Protocol Address + 0x000000000000::48 # Target Hardware Address + >> + + <<"ARP_missing", ether_header::bytes, arp_header::bytes>> + end +end diff --git a/examples/simple_router/lib/simple_router/openflow/group_tables.ex b/examples/simple_router/lib/simple_router/openflow/group_tables.ex new file mode 100644 index 0000000..9cad5b1 --- /dev/null +++ b/examples/simple_router/lib/simple_router/openflow/group_tables.ex @@ -0,0 +1,24 @@ +defmodule SimpleRouter.Openflow.GroupTables do + @moduledoc """ + Group Table Definitions + """ + + use Tres.Controller + + def egress(iface, datapath_id) do + bucket = + Openflow.Bucket.new( + actions: [ + SetField.new({:eth_src, iface.mac_address}), + Output.new(iface.number) + ] + ) + + send_group_mod_add( + datapath_id, + type: :indirect, + group_id: iface.number, + buckets: [bucket] + ) + end +end diff --git a/examples/simple_router/lib/simple_router/supervisor.ex b/examples/simple_router/lib/simple_router/supervisor.ex new file mode 100644 index 0000000..5f3b67a --- /dev/null +++ b/examples/simple_router/lib/simple_router/supervisor.ex @@ -0,0 +1,17 @@ +defmodule SimpleRouter.Supervisor do + @moduledoc """ + Top Level supervisor + """ + + use Supervisor + + @children [] + + def start_link do + Supervisor.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(_args) do + Supervisor.init(@children, strategy: :one_for_one) + end +end diff --git a/examples/simple_router/mix.exs b/examples/simple_router/mix.exs new file mode 100644 index 0000000..4bf5407 --- /dev/null +++ b/examples/simple_router/mix.exs @@ -0,0 +1,27 @@ +defmodule SimpleRouter.MixProject do + use Mix.Project + + def project do + [ + app: :simple_router, + version: "0.1.0", + elixir: "~> 1.7", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger, :tres, :pkt], + mod: {SimpleRouter.Application, []} + ] + end + + defp deps do + [ + {:tres, path: "../../../tres"}, + {:pkt, github: "msantos/pkt"} + ] + end +end diff --git a/examples/simple_router/mix.lock b/examples/simple_router/mix.lock new file mode 100644 index 0000000..fcb0cff --- /dev/null +++ b/examples/simple_router/mix.lock @@ -0,0 +1,7 @@ +%{ + "eovsdb": {:git, "https://github.com/shun159/eovsdb.git", "1ff1572708d72fd25631c681f2102407903252a3", [branch: "master"]}, + "jsone": {:git, "https://github.com/sile/jsone.git", "b23d312a5ed051ea7ad0989a9f2cb1a9c3f9a502", [tag: "1.4.6"]}, + "pkt": {:git, "https://github.com/msantos/pkt.git", "ff0e9a7d28cdae941bce935602cd252cad1ea296", []}, + "ranch": {:hex, :ranch, "1.6.2", "6db93c78f411ee033dbb18ba8234c5574883acb9a75af0fb90a9b82ea46afa00", [:rebar3], [], "hexpm"}, + "uuid": {:git, "https://github.com/avtobiff/erlang-uuid.git", "585c2474afb4a597ae8c8bf6d21e5a9c73f18e0b", [tag: "v0.5.0"]}, +} diff --git a/examples/simple_router/test/simple_router_test.exs b/examples/simple_router/test/simple_router_test.exs new file mode 100644 index 0000000..a1513a2 --- /dev/null +++ b/examples/simple_router/test/simple_router_test.exs @@ -0,0 +1,8 @@ +defmodule SimpleRouterTest do + use ExUnit.Case + doctest SimpleRouter + + test "greets the world" do + assert SimpleRouter.hello() == :world + end +end diff --git a/examples/simple_router/test/test_helper.exs b/examples/simple_router/test/test_helper.exs new file mode 100644 index 0000000..869559e --- /dev/null +++ b/examples/simple_router/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start()