Implement Openflow Protocol and Callback system

This commit is contained in:
Eishun Kondoh 2017-11-20 13:38:24 +09:00
parent fc02a678de
commit e52fe31b79
48 changed files with 937 additions and 244 deletions

View file

@ -8,8 +8,9 @@ defmodule Tres.Application do
def start(_type, _args) do
import Supervisor.Spec
{cb_mod, _cb_args} = Tres.Utils.get_callback_module
children = [worker(Registry, [[keys: :unique, name: SwitchRegistry]], id: SwitchRegistry),
supervisor(Tres.MessageHandlerSup, [], id: MessageHandlerSup)]
supervisor(Tres.MessageHandlerSup, [cb_mod], id: MessageHandlerSup)]
opts = [strategy: :one_for_one, name: Tres.Supervisor]
{:ok, _connection_pid} = Tres.Utils.start_openflow_listener
Supervisor.start_link(children, opts)

14
lib/tres/controller.ex Normal file
View file

@ -0,0 +1,14 @@
defmodule Tres.Controller do
def controller_helpers do
quote do
import Tres.SwitchRegistry, only: [send_message: 2]
use Tres.Messages
use Tres.MessageHelper
end
end
defmacro __using__(_) do
controller_helpers()
end
end

108
lib/tres/example_handler.ex Normal file
View file

@ -0,0 +1,108 @@
defmodule Tres.ExampleHandler do
use GenServer
use Tres.Controller
import Logger
defmodule State do
defstruct [
datapath_id: nil,
aux_id: nil,
conn_ref: nil
]
end
def start_link(datapath, args) do
GenServer.start_link(__MODULE__, [datapath, args])
end
def init([{datapath_id, aux_id}, _args]) do
info("[#{__MODULE__}] Switch Ready: "
<> "datapath_id: #{datapath_id} "
<> "aux_id: #{aux_id} "
<> "in #{inspect(self())}")
_ = send_flows_for_test(datapath_id)
_ = send_flow_stats_request(datapath_id)
_ = send_desc_stats_request(datapath_id)
_ = send_port_desc_stats_request(datapath_id)
conn_ref = SwitchRegistry.monitor(datapath_id)
state = %State{datapath_id: datapath_id, aux_id: aux_id, conn_ref: conn_ref}
{:ok, state}
end
def handle_info(%Flow.Reply{datapath_id: datapath_id} = desc, state) do
handle_flow_stats_reply(desc, datapath_id)
{:noreply, state}
end
def handle_info(%PortDesc.Reply{datapath_id: datapath_id} = desc, state) do
handle_port_desc_stats_reply(desc, datapath_id)
{:noreply, state}
end
def handle_info(%Desc.Reply{datapath_id: datapath_id} = desc, state) do
handle_desc_stats_reply(desc, datapath_id)
{:noreply, state}
end
# To prevent process leakage, following section is required.
def handle_info({:'DOWN', ref, :process, _pid, _reason}, %State{conn_ref: ref} = state) do
:ok = warn("[#{__MODULE__}] Switch Disconnected: datapath_id: #{state.datapath_id}")
{:stop, :normal, state}
end
# `Catch all` function is required.
def handle_info(info, state) do
:ok = warn("[#{__MODULE__}] unhandled message #{inspect(info)}: #{state.datapath_id}")
{:noreply, state}
end
# private functions
defp send_flows_for_test(datapath_id) do
for count <- Range.new(1, 1024) do
send_flow_mod_add(datapath_id, match: Match.new(metadata: count))
end
end
defp send_flow_stats_request(datapath_id) do
Flow.Request.new
|> send_message(datapath_id)
end
defp send_desc_stats_request(datapath_id) do
Desc.Request.new
|> send_message(datapath_id)
end
defp send_port_desc_stats_request(datapath_id) do
PortDesc.Request.new
|> send_message(datapath_id)
end
defp handle_flow_stats_reply(desc, datapath_id) do
info("[#{__MODULE__}] Switch #{length(desc.flows)} installed on #{datapath_id}")
end
defp handle_desc_stats_reply(desc, datapath_id) do
info(
"[#{__MODULE__}] Switch Desc: "
<> "mfr = #{desc.mfr_desc} "
<> "hw = #{desc.hw_desc} "
<> "sw = #{desc.sw_desc} "
<> "for #{datapath_id}"
)
end
defp handle_port_desc_stats_reply(port_desc, datapath_id) do
for port <- port_desc.ports do
info(
"[#{__MODULE__}] Switch has port: "
<> "number = #{port.number} "
<> "hw_addr = #{port.hw_addr} "
<> "name = #{port.name} "
<> "config = #{inspect(port.config)} "
<> "current_speed = #{port.current_speed} "
<> "on #{datapath_id}"
)
end
end
end

View file

@ -1,52 +0,0 @@
defmodule Tres.MessageHandler do
use GenServer
defmodule State do
defstruct [
ip_addr: nil,
port: nil,
datapath_id: nil,
conn_pid: nil,
conn_ref: nil,
handler_pid: nil,
handler_ref: nil
]
end
alias Tres.MessageHandler.State
@process_flags [trap_exit: true]
def start_link({ip_addr, port}, conn_pid) do
GenServer.start_link(__MODULE__, [{ip_addr, port}, conn_pid])
end
def init([{ip_addr, port}, conn_pid]) do
init_process()
conn_ref = Process.monitor(conn_pid)
state = %State{
conn_pid: conn_pid,
conn_ref: conn_ref,
ip_addr: ip_addr,
port: port
}
{:ok, state}
end
def handle_info({:'EXIT', _pid, _reason}, state) do
{:stop, :normal, state}
end
def handle_info({:'DOWN', conn_ref, :process, _conn_pid, _reason}, %State{conn_ref: conn_ref} = state) do
{:stop, :normal, state}
end
def terminate(_reason, state) do
{:shutdown, state}
end
## private functions
defp init_process do
for {flag, value} <- @process_flags, do: Process.flag(flag, value)
end
end

View file

@ -1,16 +1,17 @@
defmodule Tres.MessageHandlerSup do
use Supervisor
def start_link do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
def start_link(cb_mod) do
Supervisor.start_link(__MODULE__, [cb_mod], name: __MODULE__)
end
def init(_) do
children = [worker(Tres.MessageHandler, [], restart: :temporary)]
def init([cb_mod]) do
children = [worker(cb_mod, [], restart: :temporary, shutdown: 5000)]
supervise(children, strategy: :simple_one_for_one)
end
def start_child({ip_addr, port}) do
Supervisor.start_child(__MODULE__, [{ip_addr, port}, self()])
def start_child({dpid, aux_id}) do
{_cb_mod, cb_args} = Tres.Utils.get_callback_module
Supervisor.start_child(__MODULE__, [{dpid, aux_id}, cb_args])
end
end

View file

@ -0,0 +1,88 @@
defmodule Tres.MessageHelper do
defmacro __using__(_) do
quote location: :keep do
defp send_flow_mod_add(datapath_id, options) do
flow_mod = %Openflow.FlowMod{
cookie: options[:cookie] || 0,
priority: options[:priority] || 0,
table_id: options[:table_id] || 0,
command: :add,
idle_timeout: options[:idle_timeout] || 0,
hard_timeout: options[:hard_timeout] || 0,
buffer_id: :no_buffer,
out_port: :any,
out_group: :any,
flags: options[:flags] || [],
match: options[:match] || Openflow.Match.new,
instructions: options[:instructions] || [],
}
send_message(flow_mod, datapath_id)
end
defp send_flow_mod_modify(datapath_id, options) do
command = Tres.Utils.flow_command(options, :modify)
flow_mod = %Openflow.FlowMod{
cookie: options[:cookie] || 0,
table_id: options[:table_id] || 0,
command: command,
idle_timeout: options[:idle_timeout] || 0,
hard_timeout: options[:hard_timeout] || 0,
out_port: :any,
out_group: :any,
match: options[:match] || Openflow.Match.new,
instructions: options[:instructions] || [],
}
send_message(flow_mod, datapath_id)
end
defp send_flow_mod_delete(datapath_id, options) do
command = Tres.Utils.flow_command(options, :delete)
flow_mod = %Openflow.FlowMod{
cookie: options[:cookie] || 0,
cookie_mask: options[:cookie_mask] || 0,
table_id: options[:table_id] || :all,
command: command,
out_port: options[:out_port] || :any,
out_group: options[:out_group] || :any,
match: options[:match] || Openflow.Match.new
}
send_message(flow_mod, datapath_id)
end
defp send_packet_out(datapath_id, options) do
packet_out = %Openflow.PacketOut{
buffer_id: options[:buffer_id] || :no_buffer,
in_port: options[:in_port] || :controller,
actions: options[:actions] || [],
data: options[:data] || ""
}
send_message(packet_out, datapath_id)
end
defp send_group_mod_add(datapath_id, options) do
group_mod = Openflow.GroupMod.new(
command: :add,
type: options[:type] || :all,
group_id: options[:group_id] || 0,
buckets: options[:buckets] || []
)
send_message(group_mod, datapath_id)
end
defp send_group_mod_delete(datapath_id, group_id) do
group_mod = Openflow.GroupMod.new(command: :delete, group_id: group_id)
send_message(group_mod, datapath_id)
end
defp send_group_mod_modify(datapath_id, options) do
group_mod = Openflow.GroupMod.new(
command: :modify,
type: options[:type] || :all,
group_id: options[:group_id] || 0,
buckets: options[:buckets] || []
)
send_message(group_mod, datapath_id)
end
end
end
end

128
lib/tres/messages.ex Normal file
View file

@ -0,0 +1,128 @@
defmodule Tres.Messages do
defmacro __using__(_) do
quote do
alias Openflow.ErrorMsg
alias Openflow.Echo
alias Openflow.Features
alias Openflow.GetConfig
alias Openflow.SetConfig
alias Openflow.PacketIn
alias Openflow.FlowRemoved
alias Openflow.PortStatus
alias Openflow.PacketOut
alias Openflow.FlowMod
alias Openflow.GroupMod
alias Openflow.PortMod
alias Openflow.TableMod
alias Openflow.Multipart
alias Openflow.Barrier
alias Openflow.Role
alias Openflow.GetAsync
alias Openflow.SetAsync
alias Openflow.MeterMod
alias Openflow.Match
alias Openflow.Port
alias Openflow.NxSetPacketInFormat
alias Openflow.NxSetControllerId
alias Openflow.NxPacketIn2
alias Openflow.Multipart.Desc
alias Openflow.Multipart.Flow
alias Openflow.Multipart.Aggregate
alias Openflow.Multipart.Table
alias Openflow.Multipart.PortStats
alias Openflow.Multipart.Queue
alias Openflow.Multipart.Group
alias Openflow.Multipart.GroupDesc
alias Openflow.Multipart.GroupFeatures
alias Openflow.Multipart.Meter
alias Openflow.Multipart.MeterConfig
alias Openflow.Multipart.MeterFeatures
alias Openflow.Multipart.TableFeatures
alias Openflow.Multipart.PortDesc
alias Openflow.Instruction.GotoTable
alias Openflow.Instruction.WriteMetadata
alias Openflow.Instruction.WriteActions
alias Openflow.Instruction.ApplyActions
alias Openflow.Instruction.ClearActions
alias Openflow.Instruction.Meter
alias Openflow.Action.Output
alias Openflow.Action.CopyTtlOut
alias Openflow.Action.CopyTtlIn
alias Openflow.Action.SetMplsTtl
alias Openflow.Action.DecMplsTtl
alias Openflow.Action.PushVlan
alias Openflow.Action.PopVlan
alias Openflow.Action.PushMpls
alias Openflow.Action.PopMpls
alias Openflow.Action.SetQueue
alias Openflow.Action.Group
alias Openflow.Action.SetNwTtl
alias Openflow.Action.DecNwTtl
alias Openflow.Action.SetField
alias Openflow.Action.PushPbb
alias Openflow.Action.PopPbb
alias Openflow.Action.Encap
alias Openflow.Action.Decap
alias Openflow.Action.SetSequence
alias Openflow.Action.ValidateSequence
alias Openflow.Action.NxResubmit
alias Openflow.Action.NxSetTunnel
alias Openflow.Action.NxSetQueue
alias Openflow.Action.NxPopQueue
alias Openflow.Action.NxRegMove
alias Openflow.Action.NxRegLoad
alias Openflow.Action.NxNote
alias Openflow.Action.NxSetTunnel64
alias Openflow.Action.NxMultipath
alias Openflow.Action.NxBundle
alias Openflow.Action.NxBundleLoad
alias Openflow.Action.NxResubmitTable
alias Openflow.Action.NxOutputReg
alias Openflow.Action.NxLearn
alias Openflow.Action.NxExit
alias Openflow.Action.NxDecTtl
alias Openflow.Action.NxFinTimeout
alias Openflow.Action.NxController
alias Openflow.Action.NxDecTtlCntIds
alias Openflow.Action.NxWriteMetadata
alias Openflow.Action.NxPushMpls
alias Openflow.Action.NxPopMpls
alias Openflow.Action.NxSetMplsTtl
alias Openflow.Action.NxDecMplsTtl
alias Openflow.Action.NxStackPush
alias Openflow.Action.NxStackPop
alias Openflow.Action.NxSample
alias Openflow.Action.NxSetMplsLabel
alias Openflow.Action.NxSetMplsTc
alias Openflow.Action.NxOutputReg2
alias Openflow.Action.NxRegLoad2
alias Openflow.Action.NxConjunction
alias Openflow.Action.NxConntrack
alias Openflow.Action.NxNat
alias Openflow.Action.NxController2
alias Openflow.Action.NxSample2
alias Openflow.Action.NxOutputTrunc
alias Openflow.Action.NxGroup
alias Openflow.Action.NxSample3
alias Openflow.Action.NxClone
alias Openflow.Action.NxCtClear
alias Openflow.Action.NxResubmitTableCt
alias Openflow.Action.NxLearn2
alias Openflow.Action.NxEncap
alias Openflow.Action.NxDecap
alias Openflow.Action.NxDebugRecirc
alias Openflow.Action.NxFlowSpecMatch
alias Openflow.Action.NxFlowSpecLoad
alias Openflow.Action.NxFlowSpecOutput
alias Tres.SwitchRegistry
end
end
end

View file

@ -3,6 +3,7 @@ defmodule Tres.SecureChannel do
import Logger
alias :tres_xact_kv, as: XACT_KV
alias Tres.SecureChannelState
alias Tres.SwitchRegistry
alias Tres.MessageHandlerSup
@ -32,12 +33,11 @@ defmodule Tres.SecureChannel do
end
def init([ref, socket, transport, _opts]) do
state_data =
ref
|> init_secure_channel(socket, transport)
|> init_handler
info("[#{__MODULE__}] TCP connected to Switch on #{state_data.ip_addr}:#{state_data.port} on #{inspect(self())}")
:gen_statem.enter_loop(__MODULE__, [debug: []], :INIT, state_data, [])
state_data = init_secure_channel(ref, socket, transport)
debug("[#{__MODULE__}] TCP connected to Switch on"
<> " #{state_data.ip_addr}:#{state_data.port}"
<> " on #{inspect(self())}")
:gen_statem.enter_loop(__MODULE__, [debug: [:debug]], :INIT, state_data, [])
end
# TCP handler
@ -73,8 +73,9 @@ defmodule Tres.SecureChannel do
handle_WATING(type, message, state_data)
end
def terminate(reason, state, %SecureChannelState{datapath_id: datapath_id, aux_id: aux_id}) do
def terminate(reason, state, %SecureChannelState{datapath_id: datapath_id, aux_id: aux_id, xact_kv_ref: kv_ref}) do
warn("[#{__MODULE__}] termiate: #{inspect(reason)} state = #{inspect(state)}")
true = XACT_KV.drop(kv_ref)
:ok = SwitchRegistry.unregister({datapath_id, aux_id})
end
@ -83,7 +84,8 @@ defmodule Tres.SecureChannel do
defp init_secure_channel(ref, socket, transport) do
init_process(ref)
:ok = transport.setopts(socket, [active: :once])
SecureChannelState.new(ref: ref, socket: socket, transport: transport)
kv_ref = XACT_KV.create
SecureChannelState.new(ref: ref, socket: socket, transport: transport, xact_kv_ref: kv_ref)
end
defp init_process(ref) do
@ -93,8 +95,8 @@ defmodule Tres.SecureChannel do
end
defp init_handler(state_data) do
%SecureChannelState{ip_addr: ip_addr, port: port} = state_data
{:ok, pid} = MessageHandlerSup.start_child({ip_addr, port})
%SecureChannelState{datapath_id: dpid, aux_id: aux_id} = state_data
{:ok, pid} = MessageHandlerSup.start_child({dpid, aux_id})
ref = Process.monitor(pid)
%{state_data|handler_pid: pid, handler_ref: ref}
end
@ -128,8 +130,7 @@ defmodule Tres.SecureChannel do
close_connection(:features_handshake_timeout, state_data)
end
defp handle_CONNECTING(:internal, {:openflow, %Openflow.Features.Reply{} = features}, state_data) do
# TODO: Send to handler
info("[#{__MODULE__}] Switch connected datapath_id: #{features.datapath_id} auxiliary_id: #{features.aux_id}")
debug("[#{__MODULE__}] Switch connected datapath_id: #{features.datapath_id} auxiliary_id: #{features.aux_id}")
_ = maybe_cancel_timer(state_data.timer_ref)
handle_features_handshake(features, state_data)
end
@ -142,9 +143,10 @@ defmodule Tres.SecureChannel do
end
# CONNECTED state
defp handle_CONNECTED(:enter, :CONNECTING, _state_data) do
defp handle_CONNECTED(:enter, :CONNECTING, state_data) do
new_state_data = init_handler(state_data)
start_periodic_idle_check()
:keep_state_and_data
{:keep_state, new_state_data}
end
defp handle_CONNECTED(:info, :idle_check, state_data) do
start_periodic_idle_check()
@ -162,19 +164,48 @@ defmodule Tres.SecureChannel do
send_echo_reply(echo.xid, echo.data, state_data)
:keep_state_and_data
end
defp handle_CONNECTED(:internal, {:openflow, _message}, _state_data) do
# TODO: Send to handler
defp handle_CONNECTED(:internal, {:openflow, %Openflow.Barrier.Reply{xid: xid}}, state_data) do
for {:xact_entry, _xid, message, _orig} <- XACT_KV.get(state_data.xact_kv_ref, xid) do
unless is_nil(message) do
send(state_data.handler_pid, message)
XACT_KV.delete(state_data.xact_kv_ref, message.xid)
end
end
:keep_state_and_data
end
defp handle_CONNECTED(:internal, {:openflow, message}, state_data) do
%SecureChannelState{datapath_id: dpid, aux_id: aux_id} = state_data
new_message = %{message|datapath_id: dpid, aux_id: aux_id}
state_data.xact_kv_ref
|> XACT_KV.is_exists(message.xid)
|> handle_message(new_message, state_data)
:keep_state_and_data
end
defp handle_CONNECTED(:cast, {:send_message, message}, state_data) do
message
|> send_message(state_data)
xid = SecureChannelState.increment_transaction_id(state_data.xid)
messages = [
%{message|xid: xid},
%{Openflow.Barrier.Request.new|xid: xid}
]
XACT_KV.insert(state_data.xact_kv_ref, xid, message)
send_message(messages, state_data)
:keep_state_and_data
end
defp handle_message(_in_xact = true, message, state_data) do
[{:xact_entry, _xid, prev_message, _orig}|_] = XACT_KV.get(state_data.xact_kv_ref, message.xid)
new_message = Openflow.append_body(prev_message, message)
XACT_KV.update(state_data.xact_kv_ref, message.xid, new_message)
end
defp handle_message(_in_xact = false, message, %SecureChannelState{handler_pid: handler_pid}) do
send(handler_pid, message)
end
# WATING state
defp handle_WATING(:enter, :CONNECTING, state_data) do
warn("[#{__MODULE__}] Possible HANG Detected on datapath_id: #{state_data.datapath_id} !")
%SecureChannelState{handler_pid: handler_pid, datapath_id: dpid, aux_id: aux_id} = state_data
send(handler_pid, {:switch_hang, {dpid, aux_id}})
start_periodic_idle_check()
:keep_state_and_data
end
@ -186,8 +217,9 @@ defmodule Tres.SecureChannel do
defp handle_WATING(:info, :ping_timeout, state_data) do
handle_ping_timeout(state_data, :WAITING)
end
defp handle_WATING(:internal, {:openflow, _message}, state_data) do
# TODO: Send to handler
defp handle_WATING(:internal, {:openflow, message}, state_data) do
%SecureChannelState{handler_pid: handler_pid, datapath_id: dpid, aux_id: aux_id} = state_data
send(handler_pid, %{message|datapath_id: dpid, aux_id: aux_id})
{:next_state, :CONNECTING, state_data}
end
defp handle_WATING(type, message, state_data)
@ -249,12 +281,10 @@ defmodule Tres.SecureChannel do
{:next_state, :CONNECTED, new_state_data}
end
defp monitor_connection(datapath_id, aux_id) when aux_id > 0 do
datapath_id
|> SwitchRegistry.lookup_pid
|> Process.monitor
end
defp monitor_connection(_datapath_id, _aux_id), do: nil
defp monitor_connection(datapath_id, aux_id) when aux_id > 0,
do: SwitchRegistry.monitor(datapath_id)
defp monitor_connection(_datapath_id, _aux_id),
do: nil
defp send_hello(state_data) do
@supported_version
@ -320,7 +350,7 @@ defmodule Tres.SecureChannel do
end
defp send_message(message, %SecureChannelState{socket: socket, transport: transport}) do
debug("[#{__MODULE__}] Sending: #{inspect(message.__struct__)}(xid: #{message.xid})")
#debug("[#{__MODULE__}] Sending: #{inspect(message.__struct__)}(xid: #{message.xid})")
Tres.Utils.send_message(message, socket, transport)
end
@ -371,7 +401,7 @@ defmodule Tres.SecureChannel do
{:stop, :normal, %{state_data|socket: nil}}
end
defp close_connection({:trap_detected, reason}, state_data) do
warn("[#{__MODULE__}] connection terminated: Handler process down by #{reason}")
warn("[#{__MODULE__}] connection terminated: Trapped by #{reason}")
{:stop, :normal, %{state_data|socket: nil}}
end
defp close_connection(:tcp_closed, state_data) do

View file

@ -16,10 +16,12 @@ defmodule Tres.SecureChannelState do
ping_xid: 0,
ping_timer_ref: nil,
ping_fail_count: 0,
last_received: 0
last_received: 0,
xact_kv_ref: nil
)
alias __MODULE__
alias :tres_xact_kv, as: XACT_KV
def new(options) do
ref = Keyword.get(options, :ref)
@ -27,13 +29,15 @@ defmodule Tres.SecureChannelState do
transport = Keyword.get(options, :transport)
{:ok, {ip_addr, port}} = :inet.peername(socket)
{:ok, xid_agent} = Agent.start_link(fn -> 0 end)
kv_ref = XACT_KV.create
%SecureChannelState{
ref: ref,
socket: socket,
transport: transport,
ip_addr: :inet.ntoa(ip_addr),
port: port,
xid: xid_agent
xid: xid_agent,
xact_kv_ref: kv_ref
}
end

View file

@ -24,6 +24,12 @@ defmodule Tres.SwitchRegistry do
send_message(message, {dpid, 0})
end
def monitor(datapath_id) do
datapath_id
|> lookup_pid
|> Process.monitor
end
# private function
defp dispatch(entries, message) do

View file

@ -6,10 +6,16 @@ defmodule Tres.Utils do
@default_num_acceptors 10
@default_openflow_port 6633
def get_callback_module do
cb_mod = get_config(:callback_module, Tres.ExampleHandler)
cb_args = get_config(:callback_args, [])
{cb_mod, cb_args}
end
def start_openflow_listener do
max_connections = Application.get_env(:tres, :max_connections, @default_max_connections)
num_acceptors = Application.get_env(:tres, :num_acceptors, @default_num_acceptors)
port = Application.get_env(:tres, :port, @default_openflow_port)
max_connections = get_config(:max_connections, @default_max_connections)
num_acceptors = get_config(:num_acceptors, @default_num_acceptors)
port = get_config(:port, @default_openflow_port)
options = [max_connections: max_connections, num_acceptors: num_acceptors, port: port]
:ranch.start_listener(Tres, :ranch_tcp, options, @connection_manager, [])
end
@ -23,4 +29,31 @@ defmodule Tres.Utils do
error("[#{__MODULE__}] Unencodable error: #{inspect(message)}")
end
end
def is_multipart?(message) do
message.__struct__
|> Module.split
|> Enum.at(1)
|> String.match?(~r/Multipart/)
end
def flow_command(:delete, options) do
if Keyword.get(options, :strict, false) do
:delete_strict
else
:delete
end
end
def flow_command(:modify, options) do
if Keyword.get(options, :strict, false) do
:modify_strict
else
:modify
end
end
# private functions
defp get_config(item, default) do
Application.get_env(:tres, item, default)
end
end