···3344 alias SowerAgent.Config
55 alias SowerAgent.Storage
66+ alias SowerClient.Activator
67 alias SowerClient.Orchestration.Deployment
78 alias SowerClient.Orchestration.DeploymentProfile
89 alias SowerClient.Orchestration.SeedDeployment
9101011 def run(%Deployment{} = deployment) do
1111- deploy_result = upgrade(deployment)
1212+ result =
1313+ deployment
1414+ |> upgrade()
1515+ |> deployment_result()
1616+1717+ maybe_reboot(deployment, result)
1818+ result
1919+ end
12202121+ def deployment_result(deploy_result) do
1322 Enum.all?(deploy_result, fn r ->
1423 case r do
1524 {:ok, {:ok, _}} -> true
···8493 end)
8594 |> async_stream(fn
8695 {:ok, {:ok, %SeedDeployment{seed: seed} = seed_deploy}} ->
9696+ profile = get_deploy_profile(seed_deploy.subscription_sid)
9797+8798 Logger.info(
8899 msg: "Activating seed",
89100 name: seed.name,
90101 seed_sid: seed.sid,
91102 seed_type: seed.seed_type,
92103 artifact: seed.artifact,
9393- deployment_sid: deployment.sid
104104+ deployment_sid: deployment.sid,
105105+ activation_args: get_in(profile.activation_args)
94106 )
951079696- result = SowerAgent.Seed.activate(seed, get_deploy_profile(seed_deploy.subscription_sid))
108108+ result = SowerAgent.Seed.activate(seed, profile)
9710998110 case result do
99111 {:ok, output} ->
···173185 Storage.read().subscriptions |> Enum.find(&(&1.sid == sid))
174186 end
175187188188+ def maybe_reboot(%Deployment{} = _deployment, result) when result != :success, do: :ok
189189+190190+ def maybe_reboot(%Deployment{} = deployment, :success) do
191191+ case reboot_reason(deployment.seed_deployments) do
192192+ nil ->
193193+ :ok
194194+195195+ reason ->
196196+ if Application.get_env(:sower_agent, :enable_activation, true) do
197197+ Logger.info(
198198+ msg: "Reboot required by deployment policy",
199199+ deployment_sid: deployment.sid,
200200+ reason: reason
201201+ )
202202+203203+ case Activator.reboot(reason: reason) do
204204+ {:ok, output} ->
205205+ Logger.info(
206206+ msg: "Reboot request completed",
207207+ deployment_sid: deployment.sid,
208208+ reason: reason,
209209+ output: output
210210+ )
211211+212212+ {:error, code, output} ->
213213+ Logger.error(
214214+ msg: "Reboot request failed",
215215+ deployment_sid: deployment.sid,
216216+ reason: reason,
217217+ code: code,
218218+ output: output
219219+ )
220220+221221+ {:error, reboot_error} ->
222222+ Logger.error(
223223+ msg: "Reboot request failed",
224224+ deployment_sid: deployment.sid,
225225+ reason: reason,
226226+ error: inspect(reboot_error)
227227+ )
228228+ end
229229+ else
230230+ Logger.debug(
231231+ msg: "Reboot run in noop",
232232+ deployment_sid: deployment.sid,
233233+ reason: reason
234234+ )
235235+ end
236236+ end
237237+ end
238238+239239+ def reboot_reason(
240240+ seed_deployments,
241241+ get_profile \\ &get_deploy_profile/1,
242242+ read_link \\ &:file.read_link_all/1
243243+ ) do
244244+ profiles =
245245+ seed_deployments
246246+ |> Enum.filter(fn %SeedDeployment{seed: seed} ->
247247+ seed.seed_type == "nixos"
248248+ end)
249249+ |> Enum.map(fn %SeedDeployment{subscription_sid: subscription_sid} ->
250250+ get_profile.(subscription_sid) || %DeploymentProfile{}
251251+ end)
252252+253253+ cond do
254254+ profiles == [] ->
255255+ nil
256256+257257+ Enum.any?(profiles, fn profile ->
258258+ profile.reboot_policy == "always"
259259+ end) ->
260260+ "policy_always"
261261+262262+ Enum.any?(profiles, fn profile ->
263263+ profile.reboot_policy == "when-required" and
264264+ SowerAgent.Seed.activation_mode(profile) == "boot"
265265+ end) ->
266266+ "boot_mode"
267267+268268+ Enum.any?(profiles, fn profile ->
269269+ profile.reboot_policy == "when-required" and
270270+ SowerAgent.Seed.activation_mode(profile) == "switch"
271271+ end) ->
272272+ detect_boot_critical_change_reason(read_link)
273273+274274+ true ->
275275+ nil
276276+ end
277277+ end
278278+176279 defp maybe_write_log(_deployment, _seed, []), do: :ok
177280178281 # TODO: when you write to disk, you should ensure it gets deleted
···194297195298 defp strip_ansi(text) do
196299 Regex.replace(~r/\x1b\[[0-9;]*[a-zA-Z]/, text, "")
300300+ end
301301+302302+ defp detect_boot_critical_change_reason(read_link) do
303303+ with {:ok, profile_store_path} <- resolved_symlink("/nix/var/nix/profiles/system", read_link),
304304+ {:ok, current_store_path} <- resolved_symlink("/run/current-system", read_link),
305305+ {:ok, booted_store_path} <- resolved_symlink("/run/booted-system", read_link) do
306306+ cond do
307307+ current_store_path != profile_store_path ->
308308+ "system_changed"
309309+310310+ "#{current_store_path}/initrd" != "#{booted_store_path}/initrd" ->
311311+ "initrd_changed"
312312+313313+ "#{current_store_path}/kernel" != "#{booted_store_path}/kernel" ->
314314+ "kernel_changed"
315315+316316+ "#{current_store_path}/kernel-modules" != "#{booted_store_path}/kernel-modules" ->
317317+ "modules_changed"
318318+319319+ true ->
320320+ nil
321321+ end
322322+ else
323323+ {:error, reason} ->
324324+ Logger.warning(
325325+ msg: "Could not evaluate reboot requirement from system profile links",
326326+ reason: inspect(reason)
327327+ )
328328+329329+ nil
330330+ end
331331+ end
332332+333333+ defp resolved_symlink(path, read_link) do
334334+ case read_link.(path) do
335335+ {:ok, resolved} when is_binary(resolved) ->
336336+ {:ok, resolved}
337337+338338+ {:ok, resolved} when is_list(resolved) ->
339339+ {:ok, List.to_string(resolved)}
340340+341341+ {:error, reason} ->
342342+ {:error, {path, reason}}
343343+344344+ other ->
345345+ {:error, {path, other}}
346346+ end
197347 end
198348end
+17-3
apps/sower_agent/lib/sower_agent/seed.ex
···11defmodule SowerAgent.Seed do
22 alias SowerClient.{Activator, Seed}
33+ alias SowerClient.Orchestration.DeploymentProfile
3445 require Logger
5667 @default_socket_path "/run/sower-activator/activator.sock"
7888- def activate(%Seed{seed_type: "home-manager"} = seed) do
99+ def activate(seed, profile \\ %DeploymentProfile{})
1010+1111+ def activate(%Seed{seed_type: "home-manager"} = seed, _profile) do
912 run_activation("home-manager", seed.artifact)
1013 end
11141212- def activate(%Seed{seed_type: "nixos"} = seed) do
1313- run_activation("nixos", seed.artifact, mode: "switch")
1515+ def activate(%Seed{seed_type: "nixos"} = seed, %DeploymentProfile{} = profile) do
1616+ run_activation("nixos", seed.artifact, mode: activation_mode(profile))
1717+ end
1818+1919+ # TODO pass these args through to the activator once we validate the store paths it receives
2020+ def activation_mode(%DeploymentProfile{} = profile) do
2121+ case profile.activation_args do
2222+ [mode | _] when is_binary(mode) and mode != "" ->
2323+ mode
2424+2525+ _ ->
2626+ "switch"
2727+ end
1428 end
15291630 defp run_activation(type, path, opts \\ []) do
···11package main
2233-// ActivateRequest is sent by clients to request activation.
44-type ActivateRequest struct {
55- ID string `json:"id"` // Request correlation ID
66- Type string `json:"type"` // "nixos" or "home-manager"
77- Path string `json:"path"` // Nix store path
88- Mode string `json:"mode"` // "switch", "boot", etc. (NixOS only)
33+// Request is sent by clients to request activation or reboot.
44+type Request struct {
55+ ID string `json:"id"` // Request correlation ID
66+ Type string `json:"type"` // "nixos", "home-manager", or "reboot"
77+ Path string `json:"path,omitempty"` // Nix store path for activation
88+ Mode string `json:"mode,omitempty"` // "switch", "boot", etc. (NixOS only)
99+ Reason string `json:"reason,omitempty"` // Reboot reason
910}
10111112// ResponseType indicates the type of response message.