defmodule Systemd.UnitFile.Builder do
@moduledoc """
Typed builders for common systemd unit file sections.
"""
alias Systemd.UnitFile
@type directives :: keyword() | %{optional(atom() | String.t()) => term()}
@directive_names %{
cpu_accounting: "CPUAccounting",
cpu_quota: "CPUQuota",
cpu_weight: "CPUWeight",
io_accounting: "IOAccounting",
io_weight: "IOWeight",
ip_accounting: "IPAccounting",
limit_as: "LimitAS",
limit_core: "LimitCORE",
limit_cpu: "LimitCPU",
limit_data: "LimitDATA",
limit_fsize: "LimitFSIZE",
limit_locks: "LimitLOCKS",
limit_memlock: "LimitMEMLOCK",
limit_msgqueue: "LimitMSGQUEUE",
limit_nice: "LimitNICE",
limit_nofile: "LimitNOFILE",
limit_nproc: "LimitNPROC",
limit_rss: "LimitRSS",
limit_rtprio: "LimitRTPRIO",
limit_rttime: "LimitRTTIME",
limit_sigpending: "LimitSIGPENDING",
memory_accounting: "MemoryAccounting",
memory_high: "MemoryHigh",
memory_low: "MemoryLow",
memory_max: "MemoryMax",
memory_min: "MemoryMin",
memory_swap_max: "MemorySwapMax",
no_new_privileges: "NoNewPrivileges",
oom_policy: "OOMPolicy",
pid_file: "PIDFile",
private_devices: "PrivateDevices",
private_network: "PrivateNetwork",
private_tmp: "PrivateTmp",
private_users: "PrivateUsers",
protect_clock: "ProtectClock",
protect_control_groups: "ProtectControlGroups",
protect_home: "ProtectHome",
protect_hostname: "ProtectHostname",
protect_kernel_logs: "ProtectKernelLogs",
protect_kernel_modules: "ProtectKernelModules",
protect_kernel_tunables: "ProtectKernelTunables",
protect_proc: "ProtectProc",
protect_system: "ProtectSystem",
restrict_address_families: "RestrictAddressFamilies",
restrict_namespaces: "RestrictNamespaces",
restrict_realtime: "RestrictRealtime",
restrict_suid_sgid: "RestrictSUIDSGID",
runtime_directory_preserve: "RuntimeDirectoryPreserve",
selinux_context: "SELinuxContext",
system_call_architectures: "SystemCallArchitectures",
system_call_error_number: "SystemCallErrorNumber",
system_call_filter: "SystemCallFilter",
tasks_accounting: "TasksAccounting",
tasks_max: "TasksMax",
smack_process_label: "SmackProcessLabel",
syslog_identifier: "SyslogIdentifier",
tty_path: "TTYPath",
usb_function_descriptors: "USBFunctionDescriptors",
usb_function_strings: "USBFunctionStrings"
}
@doc """
Builds a unit file from common `Unit`, `Service`, and `Install` sections.
"""
@spec service(keyword()) :: UnitFile.t()
def service(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Service", Keyword.get(opts, :service, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a socket unit file from common `Unit`, `Socket`, and `Install` sections.
"""
@spec socket(keyword()) :: UnitFile.t()
def socket(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Socket", Keyword.get(opts, :socket, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a timer unit file from common `Unit`, `Timer`, and `Install` sections.
"""
@spec timer(keyword()) :: UnitFile.t()
def timer(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Timer", Keyword.get(opts, :timer, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a mount unit file from common `Unit`, `Mount`, and `Install` sections.
"""
@spec mount(keyword()) :: UnitFile.t()
def mount(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Mount", Keyword.get(opts, :mount, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a path unit file from common `Unit`, `Path`, and `Install` sections.
"""
@spec path(keyword()) :: UnitFile.t()
def path(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Path", Keyword.get(opts, :path, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a target unit file from common `Unit`, `Target`, and `Install` sections.
"""
@spec target(keyword()) :: UnitFile.t()
def target(opts) when is_list(opts) do
UnitFile.parse!("")
|> maybe_append_section("Unit", Keyword.get(opts, :unit, []))
|> maybe_append_section("Target", Keyword.get(opts, :target, []))
|> maybe_append_section("Install", Keyword.get(opts, :install, []))
end
@doc """
Builds a unit file with one section.
"""
@spec section(String.t(), directives()) :: UnitFile.t()
def section(name, directives) do
UnitFile.parse!("")
|> maybe_append_section(name, directives)
end
defp maybe_append_section(unit_file, _name, directives) when directives in [nil, [], %{}] do
unit_file
end
defp maybe_append_section(unit_file, name, directives) do
Enum.reduce(directives, unit_file, fn {directive, value}, unit_file ->
append_directive(unit_file, name, normalize_name(directive), value)
end)
end
defp append_directive(unit_file, section, name, values) when is_list(values) do
Enum.reduce(values, unit_file, &UnitFile.append(&2, section, name, normalize_value(&1)))
end
defp append_directive(unit_file, section, name, value) do
UnitFile.append(unit_file, section, name, normalize_value(value))
end
defp normalize_name(name) when is_atom(name) do
Map.get_lazy(@directive_names, name, fn ->
name
|> Atom.to_string()
|> Macro.camelize()
end)
end
defp normalize_name(name) when is_binary(name), do: name
defp normalize_value(value) when is_binary(value), do: value
defp normalize_value(value), do: to_string(value)
end