Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions ebb.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ebb configuration.
#
# ebb looks for this file at $EBB_CONFIG, ./ebb.toml, then /etc/ebb/ebb.toml,
# and refuses to start without it.
#
# Features are enabled by presence: a feature runs iff its section exists.
# When a section is present, all of its keys are required.

[dhcp]
# If the ERTS user doesn't have privileges to bind to port 67, this will silently fail.
listen_port = 67

# IPv4 CIDR of the managed subnet. Leases are allocated from this range.
range = "172.16.0.0/12"

# How long granted leases last.
lease_seconds = 3600

# How long an unaccepted OFFER is held before its IP is reclaimed.
offer_timeout_seconds = 300
6 changes: 0 additions & 6 deletions include/dhcp.hrl
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
-define(DHCP_MAGIC_COOKIE, 16#63825363).

-define(DEFAULT_LEASE_SECONDS, 3600).
% TODO
%-define(MAX_LEASE_SECONDS, 86400).
%-define(MIN_LEASE_SECONDS, 300).

-define(DEFAULT_OFFER_TIMEOUT_SECONDS, 300).

-define(DEFAULT_CIDR_RANGE, "172.16.0.0/16").

-record(dhcp_message, {
% Header
op :: op(),
Expand Down
17 changes: 17 additions & 0 deletions priv/scripts/osx/pxe-boot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail

# Subnet must match ebb's ?DEFAULT_CIDR_RANGE ("172.16.0.0/16").
NET_START="172.16.0.1"
NET_END="172.16.255.254"
NET_MASK="255.255.0.0"

exec sudo qemu-system-x86_64 \
-machine q35 \
-cpu qemu64 \
-m 512 \
-netdev vmnet-host,id=net0,start-address="${NET_START}",end-address="${NET_END}",subnet-mask="${NET_MASK}" \
-device e1000,netdev=net0,mac=52:54:00:12:34:56 \
-boot n \
-nographic \
-no-reboot
1 change: 1 addition & 0 deletions rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{deps, [
%{khepri, {git, "https://github.com/rabbitmq/khepri", {branch, main}}},
{erl_cidr, "1.2.1"},
{tomerl, "0.5.0"},
{eqwalizer_support,
{git_subdir, "https://github.com/whatsapp/eqwalizer.git", {branch, "main"},
"eqwalizer_support"}}
Expand Down
11 changes: 7 additions & 4 deletions rebar.lock
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{"1.2.0",
[{<<"eqwalizer_support">>,
{git_subdir,"https://github.com/whatsapp/eqwalizer.git",
{ref,"6370858ff6f8a02e3c853cf034cdf3c5a85034dc"},
{ref,"01f151ca206bb019db0999a2c9c8c8aaae79bbab"},
"eqwalizer_support"},
0},
{<<"erl_cidr">>,{pkg,<<"erl_cidr">>,<<"1.2.1">>},0}]}.
{<<"erl_cidr">>,{pkg,<<"erl_cidr">>,<<"1.2.1">>},0},
{<<"tomerl">>,{pkg,<<"tomerl">>,<<"0.5.0">>},0}]}.
[
{pkg_hash,[
{<<"erl_cidr">>, <<"921BB463BCF10EAD937AE491E5BCAC2823E550B339A9893AC6574518553CC067">>}]},
{<<"erl_cidr">>, <<"921BB463BCF10EAD937AE491E5BCAC2823E550B339A9893AC6574518553CC067">>},
{<<"tomerl">>, <<"6E7CEAC151AC573B87B985F3D2112F29933B89BA0693FA741318D899A17AC318">>}]},
{pkg_hash_ext,[
{<<"erl_cidr">>, <<"FCF5D51B1CC1B26D1748AB39F9B614AB445AD17172A9396D48C6B5C15EA9EFCF">>}]}
{<<"erl_cidr">>, <<"FCF5D51B1CC1B26D1748AB39F9B614AB445AD17172A9396D48C6B5C15EA9EFCF">>},
{<<"tomerl">>, <<"2A7FB62F9EBF0E75561B39255638BC2B805B437C86FEC538657E7C3B576979FA">>}]}
].
9 changes: 8 additions & 1 deletion src/ebb.app.src
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
{mod, {ebb_app, []}},
{applications, [
kernel,
stdlib
stdlib,
tomerl
% NOTE: we also depend on the erl_cidr package, whose OTP app is
% confusingly named inet_cidr. Listing either name here breaks
% rebar3 dialyzer (app/package name mismatch). It is a pure
% library (no processes to start), so omitting it is safe in
% dev; a future release definition must include inet_cidr
% explicitly.
]},
{env, []},
{modules, []},
Expand Down
8 changes: 7 additions & 1 deletion src/ebb_app.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@
-export([start/2, stop/1]).

start(_StartType, _StartArgs) ->
ebb_sup:start_link().
case ebb_config:load() of
ok ->
ebb_sup:start_link();
{error, Reason} ->
logger:critical("Refusing to start: ~s", [Reason]),
{error, {config_error, lists:flatten(Reason)}}
end.

stop(_State) ->
ok.
Expand Down
160 changes: 160 additions & 0 deletions src/ebb_config.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
-module(ebb_config).
-moduledoc """
Configuration for ebb, loaded from a TOML file into persistent terms. A
configuration file is required to boot ebb. The file is resolved in this order:

1. The path in the `EBB_CONFIG` environment variable
2. `./ebb.toml`
3. `/etc/ebb/ebb.toml`
""".

-export([load/0, get/1, enabled/1]).
%% Exported for tests
-export([load_file/1]).

-define(PT_KEY, ?MODULE).
-define(LOCAL_PATH, "ebb.toml").
-define(SYSTEM_PATH, "/etc/ebb/ebb.toml").

%%--------------------------------------------------------------------
%% API
%%--------------------------------------------------------------------

-doc """
Returns an error if no file can be found, the file is unreadable or
unparseable, or any required key is missing.
""".
-spec load() -> ok | {error, io_lib:chars()}.
load() ->
case resolve_path() of
{path, Path} ->
load_file(Path);
{error, Reason} ->
{error, Reason}
end.

-doc """
Load configuration from an explicit path. See also `load/0`.
""".
-spec load_file(file:name_all()) -> ok | {error, io_lib:chars()}.
load_file(Path) ->
case tomerl:read_file(Path) of
{ok, Raw} ->
try
store(Path, atomize(Raw))
catch
throw:{unknown_key, Key} ->
{error,
io_lib:format("unknown configuration key \"~s\" in ~s", [
Key, Path
])}
end;
{error, Reason} ->
{error, io_lib:format("cannot read ~s: ~p", [Path, Reason])}
end.

store(Path, Config) ->
case missing_keys(Config) of
[] ->
persistent_term:put(?PT_KEY, Config),
logger:notice("Loaded configuration from ~s, enabled features: ~p", [
Path, [F || F <- maps:keys(features()), is_map_key(F, Config)]
]),
ok;
Missing ->
{error,
io_lib:format("~s is missing required keys: ~s", [
Path, string:join(Missing, ", ")
])}
end.

-doc """
Fetch a configuration value by path, e.g. `get([dhcp, listen_port])`.
Crashes on unknown paths; see `features/0`.
""".
-spec get([atom()]) -> term().
get(Path) when is_list(Path) ->
lists:foldl(fun maps:get/2, persistent_term:get(?PT_KEY), Path).

-doc """
Features are gated by presence: a feature is enabled iff its section
exists in the configuration file.
""".
-spec enabled(atom()) -> boolean().
enabled(Feature) ->
is_map_key(Feature, persistent_term:get(?PT_KEY)).

%%--------------------------------------------------------------------
%% Internal
%%--------------------------------------------------------------------

%% Known feature sections and the keys each requires when present.
%% An absent section simply disables the feature.
features() ->
#{
dhcp => [listen_port, range, lease_seconds, offer_timeout_seconds]
}.

missing_keys(Config) ->
maps:fold(
fun(Feature, RequiredKeys, Acc) ->
case Config of
#{Feature := Section} ->
Acc ++
[
atom_to_list(Feature) ++ "." ++ atom_to_list(Key)
|| Key <- RequiredKeys, not is_map_key(Key, Section)
];
#{} ->
Acc
end
end,
[],
features()
).

resolve_path() ->
case os:getenv("EBB_CONFIG") of
false -> search_default_paths();
"" -> search_default_paths();
Path -> explicit_path(Path)
end.

search_default_paths() ->
case lists:search(fun filelib:is_regular/1, [?LOCAL_PATH, ?SYSTEM_PATH]) of
{value, Path} ->
{path, Path};
false ->
{error,
io_lib:format(
"no configuration file found; provide ~s, ~s,"
" or set EBB_CONFIG",
[?LOCAL_PATH, ?SYSTEM_PATH]
)}
end.

explicit_path(Path) ->
case filelib:is_regular(Path) of
true ->
{path, Path};
false ->
{error,
io_lib:format(
"EBB_CONFIG points at ~s, which does not exist"
" or is not a regular file",
[Path]
)}
end.

%% tomerl produces binary keys; convert them (and table arrays) to the
%% atom-keyed shape used internally.
atomize(Map) when is_map(Map) ->
maps:fold(
fun(K, V, Acc) -> Acc#{binary_to_existing_atom(K) => atomize(V)} end,
#{},
Map
);
atomize(List) when is_list(List) ->
[atomize(V) || V <- List];
atomize(Value) ->
Value.
8 changes: 5 additions & 3 deletions src/ebb_dhcp_pool_mem.erl
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ dump() ->
init([]) ->
{ok, #{
pool => [],
range => ?DEFAULT_CIDR_RANGE
range => ebb_config:get([dhcp, range])
}}.

handle_call({get_offer, Msg}, _From, #{pool := Pool} = State) ->
Expand All @@ -85,7 +85,7 @@ handle_call({create_offer, Msg}, _From, State) ->
% otherwise give them the default
Value;
_ ->
?DEFAULT_LEASE_SECONDS
ebb_config:get([dhcp, lease_seconds])
end,
% See next_ip/2 deficiencies
{ok, ClientIP} = next_ip(Pool, Range),
Expand All @@ -95,7 +95,9 @@ handle_call({create_offer, Msg}, _From, State) ->
subnet_mask = to_mask(Range),
state = offered,
expiration = erlang:send_after(
?DEFAULT_OFFER_TIMEOUT_SECONDS * 1000, self(), {expire, ClientID}
ebb_config:get([dhcp, offer_timeout_seconds]) * 1000,
self(),
{expire, ClientID}
),
duration = LeaseDuration
},
Expand Down
7 changes: 4 additions & 3 deletions src/ebb_dhcpd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

init([]) ->
{ok, Socket} = gen_udp:open(67, [{active, once}, {broadcast, true}, binary]),
Port = ebb_config:get([dhcp, listen_port]),
{ok, Socket} = gen_udp:open(Port, [{active, once}, {broadcast, true}, binary]),
{ok, #{
socket => Socket
}}.
Expand Down Expand Up @@ -254,7 +255,7 @@ server_ip(Socket) ->
end.

guess_server_ip() ->
Cidr = inet_cidr:parse(?DEFAULT_CIDR_RANGE),
Cidr = inet_cidr:parse(ebb_config:get([dhcp, range])),
{Start, _End, _Prefix} = Cidr,
{ok, Addrs} = inet:getifaddrs(),
case
Expand All @@ -275,5 +276,5 @@ send_broadcast(Socket, Packet) ->
gen_udp:send(Socket, Bcast, 68, Packet).

subnet_broadcast() ->
{_Start, End, _Prefix} = inet_cidr:parse(?DEFAULT_CIDR_RANGE),
{_Start, End, _Prefix} = inet_cidr:parse(ebb_config:get([dhcp, range])),
End.
34 changes: 34 additions & 0 deletions src/ebb_provision_sup.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-module(ebb_provision_sup).
-moduledoc """
Supervisor for the provisioning feature, including:
- the DHCP listener and its lease pool.

Started by `ebb_sup` only when the `[dhcp]` section is present in the
configuration.
""".

-behaviour(supervisor).

-export([start_link/0]).
-export([init/1]).

start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).

init([]) ->
SupFlags = #{
strategy => one_for_all,
intensity => 3,
period => 5
},
ChildSpecs = [
#{
id => ebb_dhcp_pool_mem,
start => {ebb_dhcp_pool_mem, start_link, []}
},
#{
id => ebb_dhcpd,
start => {ebb_dhcpd, start_link, []}
}
],
{ok, {SupFlags, ChildSpecs}}.
Loading
Loading