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
12 changes: 12 additions & 0 deletions config/sys.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[
{kernel, [
{logger, [
{handler, default, logger_std_h, #{
formatter =>
{logger_formatter, #{
template => [time, " ", level, ": ", msg, "\n"]
}}
}}
]}
]}
].
23 changes: 7 additions & 16 deletions ebb.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
# 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.
# ebb minimal configuration.
# unknown keys are rejected

[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
[[dhcp.subnet]]
cidr = "172.16.0.0/12"
interface = "pxe0"

# How long an unaccepted OFFER is held before its IP is reclaimed.
offer_timeout_seconds = 300
[[dhcp.subnet.pool]]
range = "172.16.0.0/12"
48 changes: 48 additions & 0 deletions ebb.toml.sample-full
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# ebb sample configuration.
#
# A section's presence enables it. Unknown keys refuse boot.
# Keys marked optional may be omitted; defaults are shown.
# All other keys are required.

[dhcp]

[[dhcp.subnet]]
cidr = "192.168.1.0/24"
interface = "eno1"

# Optional. Netboot fields placed in the BOOTP header of replies.
next_server = "192.168.1.5"
boot_file = "ipxe.efi"

# Optional. DHCP protocol options; a key's presence means "send it".
option.routers = "192.168.1.1"
option.domain_name_servers = "192.168.1.2"

[[dhcp.subnet.pool]]
range = "192.168.1.50-192.168.1.100"
lease_seconds = 3600 # optional, default 3600
offer_timeout_seconds = 300 # optional, default 300
backend.type = "mem" # optional, default "mem"

# backend.type selects the storage backend and which backend.* keys apply:
# "mem" no parameters
# "file" requires backend.path
[[dhcp.subnet.pool]]
range = "192.168.1.120-192.168.1.200"
backend.type = "file"
backend.path = "/var/lib/ebb/leases-eno1"

[[dhcp.subnet]]
cidr = "10.20.0.0/24"
interface = "eno2"

[[dhcp.subnet.pool]]
range = "10.20.0.50-10.20.0.200"

# proxyDHCP: supply PXE boot information on a network whose addressing is
# owned by an existing DHCP server. Proxy interfaces must not overlap with
# subnet interfaces.
[[dhcp.proxy]]
interface = "eno3"
next_server = "10.30.0.5"
boot_file = "ipxe.efi"
10 changes: 10 additions & 0 deletions ebb.toml.sample-minimal
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# ebb minimal configuration: serve one pool on one interface.

[dhcp]

[[dhcp.subnet]]
cidr = "192.168.1.0/24"
interface = "eno1"

[[dhcp.subnet.pool]]
range = "192.168.1.50-192.168.1.100"
6 changes: 6 additions & 0 deletions include/dhcp.hrl
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
-define(DHCP_PORT, 67).
-define(DHCP_MAGIC_COOKIE, 16#63825363).
% TODO
%-define(MAX_LEASE_SECONDS, 86400).
%-define(MIN_LEASE_SECONDS, 300).

%% Configuration defaults for omitted optional keys.
-define(DEFAULT_LEASE_SECONDS, 3600).
-define(DEFAULT_OFFER_TIMEOUT_SECONDS, 300).
-define(DEFAULT_POOL_BACKEND, "mem").

-record(dhcp_message, {
% Header
op :: op(),
Expand Down
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
]}.

{shell, [
%% {config, "config/sys.config"},
{config, "config/sys.config"},
{apps, [ebb]}
]}.

Expand Down
90 changes: 21 additions & 69 deletions src/ebb_config.erl
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,7 @@ configuration file is required to boot ebb. The file is resolved in this order:
""".

-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").

Expand All @@ -26,100 +23,53 @@ unparseable, or any required key is missing.
""".
-spec load() -> ok | {error, io_lib:chars()}.
load() ->
case resolve_path() of
{path, Path} ->
load_file(Path);
maybe
{path, File} ?= resolve_path(),
logger:notice("Reading configuration from ~p", [File]),
{ok, Config} ?= read_toml(File),
logger:notice("Parsed configuration as valid TOML"),
ok ?= ebb_config_validator:check_structure(Config),
logger:notice("Validated configuration structure"),
persistent_term:put(?MODULE, atomize(Config))
else
{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).
lists:foldl(fun maps:get/2, persistent_term:get(?MODULE), 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)).
is_map_key(Feature, persistent_term:get(?MODULE)).

%%--------------------------------------------------------------------
%% 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.

read_toml(File) ->
case tomerl:read_file(File) of
{ok, Raw} ->
{ok, Raw};
{error, Reason} ->
{error, io_lib:format("cannot read ~s: ~p", [File, Reason])}
end.

search_default_paths() ->
case lists:search(fun filelib:is_regular/1, [?LOCAL_PATH, ?SYSTEM_PATH]) of
{value, Path} ->
Expand Down Expand Up @@ -156,5 +106,7 @@ atomize(Map) when is_map(Map) ->
);
atomize(List) when is_list(List) ->
[atomize(V) || V <- List];
atomize(Value) when is_binary(Value) ->
binary_to_list(Value);
atomize(Value) ->
Value.
Loading
Loading