Skip to content

Commit 9ad1f59

Browse files
committed
Add resource controls and lifecycle tests
1 parent d409d23 commit 9ad1f59

6 files changed

Lines changed: 186 additions & 1 deletion

File tree

lib/systemd/transient_unit.ex

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ defmodule Systemd.TransientUnit do
3131
@spec uint64(String.t(), non_neg_integer()) :: Property.t()
3232
def uint64(name, value), do: property(name, "t", value)
3333

34+
@doc """
35+
Creates a memory limit property such as `MemoryMax`.
36+
"""
37+
@spec memory_max(non_neg_integer()) :: Property.t()
38+
def memory_max(bytes), do: uint64("MemoryMax", bytes)
39+
40+
@doc """
41+
Creates a task-count limit property such as `TasksMax`.
42+
"""
43+
@spec tasks_max(non_neg_integer()) :: Property.t()
44+
def tasks_max(tasks), do: uint64("TasksMax", tasks)
45+
46+
@doc """
47+
Creates a CPU quota property in microseconds per second.
48+
"""
49+
@spec cpu_quota_per_sec_usec(non_neg_integer()) :: Property.t()
50+
def cpu_quota_per_sec_usec(usec), do: uint64("CPUQuotaPerSecUSec", usec)
51+
3452
@doc """
3553
Creates an `ExecStart` property.
3654
"""

lib/systemd/unit_file/validator.ex

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ defmodule Systemd.UnitFile.Validator do
2929
),
3030
"Service" =>
3131
MapSet.new(
32-
~w(Type ExecStart ExecStartPre ExecStartPost ExecCondition ExecReload ExecStop ExecStopPost Restart RestartSec User Group WorkingDirectory RootDirectory Environment EnvironmentFile PassEnvironment UnsetEnvironment TimeoutStartSec TimeoutStopSec TimeoutAbortSec KillSignal KillMode RemainAfterExit GuessMainPID PIDFile RuntimeDirectory RuntimeDirectoryPreserve StateDirectory CacheDirectory LogsDirectory ConfigurationDirectory StandardOutput StandardError SyslogIdentifier LimitNOFILE LimitNPROC LimitMEMLOCK LimitCORE LimitCPU LimitAS LimitFSIZE NoNewPrivileges PrivateTmp PrivateDevices ProtectSystem ProtectHome AmbientCapabilities CapabilityBoundingSet ReadWritePaths ReadOnlyPaths InaccessiblePaths OOMPolicy)
32+
~w(Type ExecStart ExecStartPre ExecStartPost ExecCondition ExecReload ExecStop ExecStopPost Restart RestartSec User Group WorkingDirectory RootDirectory Environment EnvironmentFile PassEnvironment UnsetEnvironment TimeoutStartSec TimeoutStopSec TimeoutAbortSec KillSignal KillMode RemainAfterExit GuessMainPID PIDFile RuntimeDirectory RuntimeDirectoryPreserve StateDirectory CacheDirectory LogsDirectory ConfigurationDirectory StandardOutput StandardError SyslogIdentifier Slice Delegate CPUAccounting CPUWeight CPUQuota MemoryAccounting MemoryMin MemoryLow MemoryHigh MemoryMax MemorySwapMax TasksAccounting TasksMax IOAccounting IOWeight IPAccounting LimitNOFILE LimitNPROC LimitMEMLOCK LimitCORE LimitCPU LimitAS LimitFSIZE NoNewPrivileges PrivateTmp PrivateDevices ProtectSystem ProtectHome AmbientCapabilities CapabilityBoundingSet ReadWritePaths ReadOnlyPaths InaccessiblePaths OOMPolicy)
3333
),
3434
"Install" => MapSet.new(~w(WantedBy RequiredBy Also Alias DefaultInstance)),
3535
"Socket" =>
@@ -223,13 +223,39 @@ defmodule Systemd.UnitFile.Validator do
223223
when directive in [
224224
"RemainAfterExit",
225225
"GuessMainPID",
226+
"Delegate",
227+
"CPUAccounting",
228+
"MemoryAccounting",
229+
"TasksAccounting",
230+
"IOAccounting",
231+
"IPAccounting",
226232
"NoNewPrivileges",
227233
"PrivateTmp",
228234
"PrivateDevices"
229235
] do
230236
boolean("Service", directive, value, span)
231237
end
232238

239+
defp value_errors("Service", directive, value, span)
240+
when directive in [
241+
"MemoryMin",
242+
"MemoryLow",
243+
"MemoryHigh",
244+
"MemoryMax",
245+
"MemorySwapMax",
246+
"TasksMax"
247+
] do
248+
resource_limit("Service", directive, value, span)
249+
end
250+
251+
defp value_errors("Service", directive, value, span)
252+
when directive in ["CPUWeight", "IOWeight"] do
253+
positive_integer("Service", directive, value, span)
254+
end
255+
256+
defp value_errors("Service", "CPUQuota", value, span),
257+
do: percentage("Service", "CPUQuota", value, span)
258+
233259
defp value_errors("Service", "KillMode", value, span) do
234260
one_of("Service", "KillMode", value, ~w(control-group mixed process none), span)
235261
end
@@ -367,6 +393,40 @@ defmodule Systemd.UnitFile.Validator do
367393
end
368394
end
369395

396+
defp resource_limit(_section, _directive, "infinity", _span), do: []
397+
398+
defp resource_limit(section, directive, value, span) do
399+
if ValueParser.resource_quantity?(value) do
400+
[]
401+
else
402+
[
403+
value_error(
404+
section,
405+
directive,
406+
value,
407+
"expected bytes, K/M/G/T/P/E suffix, or infinity",
408+
span
409+
)
410+
]
411+
end
412+
end
413+
414+
defp positive_integer(section, directive, value, span) do
415+
if ValueParser.positive_integer?(value) do
416+
[]
417+
else
418+
[value_error(section, directive, value, "expected a positive integer", span)]
419+
end
420+
end
421+
422+
defp percentage(section, directive, value, span) do
423+
if ValueParser.percentage?(value) do
424+
[]
425+
else
426+
[value_error(section, directive, value, "expected a percentage such as 50%", span)]
427+
end
428+
end
429+
370430
defp octal_mode(section, directive, value, span) do
371431
if ValueParser.octal_mode?(value) do
372432
[]

lib/systemd/unit_file/value_parser.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,19 @@ defmodule Systemd.UnitFile.ValueParser do
3131
])
3232
|> eos()
3333

34+
resource_unit = choice(Enum.map(~w(K M G T P E), &string/1))
35+
resource_quantity = digits |> optional(resource_unit) |> eos()
36+
37+
positive_integer =
38+
ascii_string([?1..?9], min: 1) |> concat(repeat(ascii_string([?0..?9], 1))) |> eos()
39+
40+
percentage = decimal |> ignore(string("%")) |> eos()
3441
octal_mode = ascii_string([?0..?7], min: 3, max: 4) |> eos()
3542

3643
defparsecp(:parse_duration_value, duration)
44+
defparsecp(:parse_resource_quantity_value, resource_quantity)
45+
defparsecp(:parse_positive_integer_value, positive_integer)
46+
defparsecp(:parse_percentage_value, percentage)
3747
defparsecp(:parse_octal_mode_value, octal_mode)
3848

3949
@doc false
@@ -42,6 +52,24 @@ defmodule Systemd.UnitFile.ValueParser do
4252
parse_duration_value(value) |> parse_ok?()
4353
end
4454

55+
@doc false
56+
@spec resource_quantity?(String.t()) :: boolean()
57+
def resource_quantity?(value) when is_binary(value) do
58+
parse_resource_quantity_value(value) |> parse_ok?()
59+
end
60+
61+
@doc false
62+
@spec positive_integer?(String.t()) :: boolean()
63+
def positive_integer?(value) when is_binary(value) do
64+
parse_positive_integer_value(value) |> parse_ok?()
65+
end
66+
67+
@doc false
68+
@spec percentage?(String.t()) :: boolean()
69+
def percentage?(value) when is_binary(value) do
70+
parse_percentage_value(value) |> parse_ok?()
71+
end
72+
4573
@doc false
4674
@spec octal_mode?(String.t()) :: boolean()
4775
def octal_mode?(value) when is_binary(value) do

test/systemd/manager_integration_test.exs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Systemd.ManagerIntegrationTest do
66
Job,
77
JobStatus,
88
Manager,
9+
Properties,
910
TransientUnit,
1011
Unit,
1112
UnitFileOperation,
@@ -96,6 +97,40 @@ defmodule Systemd.ManagerIntegrationTest do
9697
end
9798
end
9899

100+
test "starts a transient unit with resource controls" do
101+
assert {:ok, conn} = Manager.connect()
102+
103+
name = "systemd-elixir-resource-test-#{System.unique_integer([:positive])}.service"
104+
105+
properties = [
106+
TransientUnit.string("Description", "systemd Elixir resource integration test"),
107+
TransientUnit.string("Type", "oneshot"),
108+
TransientUnit.boolean("RemainAfterExit", true),
109+
TransientUnit.memory_max(67_108_864),
110+
TransientUnit.tasks_max(64),
111+
TransientUnit.exec_start("/bin/true", ["/bin/true"])
112+
]
113+
114+
case Manager.start_transient_unit(conn, name, properties) do
115+
{:ok, %Job{} = job} ->
116+
assert :ok = Job.await(conn, job, timeout: 5_000)
117+
assert {:ok, unit} = Manager.get_unit(conn, name)
118+
119+
assert {:ok, 67_108_864} =
120+
Properties.get(
121+
conn,
122+
unit.object_path,
123+
"org.freedesktop.systemd1.Service",
124+
"MemoryMax"
125+
)
126+
127+
assert :ok = Manager.stop_unit(conn, name) |> await_or_ok(conn)
128+
129+
{:error, %Error{} = error} ->
130+
assert Error.permission?(error)
131+
end
132+
end
133+
99134
test "starts and awaits a harmless transient unit" do
100135
assert {:ok, conn} = Manager.connect()
101136

@@ -117,6 +152,9 @@ defmodule Systemd.ManagerIntegrationTest do
117152
end
118153
end
119154

155+
defp await_or_ok({:ok, %Job{} = job}, conn), do: Job.await(conn, job, timeout: 5_000)
156+
defp await_or_ok({:error, %Error{} = error}, _conn), do: {:error, error}
157+
120158
defp assert_operation_or_permission({:ok, %UnitFileOperation{changes: changes}})
121159
when is_list(changes), do: :ok
122160

test/systemd/transient_unit_test.exs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ defmodule Systemd.TransientUnitTest do
1414
]
1515
end
1616

17+
test "builds resource-control properties" do
18+
assert Property.to_dbus(TransientUnit.memory_max(67_108_864)) == [
19+
"MemoryMax",
20+
{"t", 67_108_864}
21+
]
22+
23+
assert Property.to_dbus(TransientUnit.tasks_max(64)) == ["TasksMax", {"t", 64}]
24+
25+
assert Property.to_dbus(TransientUnit.cpu_quota_per_sec_usec(500_000)) == [
26+
"CPUQuotaPerSecUSec",
27+
{"t", 500_000}
28+
]
29+
end
30+
1731
test "builds typed auxiliary units" do
1832
aux = AuxUnit.new("helper.service", [TransientUnit.string("Description", "helper")])
1933

test/systemd/unit_file_test.exs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,33 @@ defmodule Systemd.UnitFileTest do
159159
"[Service]\nPIDFile=/run/app.pid\nSyslogIdentifier=app\nLimitNOFILE=1048576\nLimitMEMLOCK=infinity\nOOMPolicy=stop\n"
160160
end
161161

162+
test "validates common cgroup and resource-control directives" do
163+
assert :ok =
164+
UnitFile.parse!(
165+
"[Service]\nExecStart=/bin/true\nCPUAccounting=yes\nCPUWeight=100\nCPUQuota=50%\nMemoryAccounting=true\nMemoryMax=256M\nMemorySwapMax=infinity\nTasksMax=64\nIOAccounting=on\nIOWeight=200\nDelegate=no\n"
166+
)
167+
|> UnitFile.validate(:service)
168+
169+
assert {:error, errors} =
170+
UnitFile.parse!(
171+
"[Service]\nExecStart=/bin/true\nCPUAccounting=maybe\nCPUWeight=heavy\nCPUQuota=half\nMemoryMax=lots\nTasksMax=many\n"
172+
)
173+
|> UnitFile.validate(:service)
174+
175+
for directive <- ["CPUAccounting", "CPUWeight", "CPUQuota", "MemoryMax", "TasksMax"] do
176+
assert Enum.any?(
177+
errors,
178+
&match?(
179+
%Systemd.UnitFile.ValidationError{
180+
reason: :invalid_directive_value,
181+
directive: ^directive
182+
},
183+
&1
184+
)
185+
)
186+
end
187+
end
188+
162189
test "validates unit file sections and directives" do
163190
assert :ok =
164191
UnitFile.parse!("[Service]\nExecStart=/bin/true\nLimitNOFILE=1048576\n")

0 commit comments

Comments
 (0)