]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/node/services.rs
use proxmox-sortable-macro directly
[proxmox-backup.git] / src / api2 / node / services.rs
1 use std::process::{Command, Stdio};
2
3 use anyhow::{bail, Error};
4 use serde_json::{json, Value};
5
6 use proxmox_router::{list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap};
7 use proxmox_schema::api;
8 use proxmox_sortable_macro::sortable;
9
10 use pbs_api_types::{Authid, NODE_SCHEMA, PRIV_SYS_AUDIT, PRIV_SYS_MODIFY, SERVICE_ID_SCHEMA};
11
12 use proxmox_rest_server::WorkerTask;
13
14 static SERVICE_NAME_LIST: [&str; 7] = [
15 "proxmox-backup",
16 "proxmox-backup-proxy",
17 "sshd",
18 "syslog",
19 "cron",
20 "postfix",
21 "systemd-timesyncd",
22 ];
23
24 pub fn real_service_name(service: &str) -> &str {
25 // since postfix package 3.1.0-3.1 the postfix unit is only here
26 // to manage subinstances, of which the default is called "-".
27 // This is where we look for the daemon status
28
29 if service == "postfix" {
30 "postfix@-"
31 } else {
32 service
33 }
34 }
35
36 fn get_full_service_state(service: &str) -> Result<Value, Error> {
37 let real_service_name = real_service_name(service);
38
39 let mut child = Command::new("systemctl")
40 .args(["show", real_service_name])
41 .stdout(Stdio::piped())
42 .spawn()?;
43
44 use std::io::{BufRead, BufReader};
45
46 let mut result = json!({});
47
48 if let Some(ref mut stdout) = child.stdout {
49 for line in BufReader::new(stdout).lines() {
50 match line {
51 Ok(line) => {
52 let mut iter = line.splitn(2, '=');
53 let key = iter.next();
54 let value = iter.next();
55 if let (Some(key), Some(value)) = (key, value) {
56 result[key] = Value::from(value);
57 }
58 }
59 Err(err) => {
60 log::error!("reading service config failed: {}", err);
61 let _ = child.kill();
62 break;
63 }
64 }
65 }
66 }
67
68 let status = child.wait().unwrap();
69 if !status.success() {
70 bail!("systemctl show failed with {}", status);
71 }
72
73 Ok(result)
74 }
75
76 fn json_service_state(service: &str, status: Value) -> Value {
77 if let Some(desc) = status["Description"].as_str() {
78 let name = status["Name"].as_str().unwrap_or(service);
79
80 let state = if status["Type"] == "oneshot" && status["SubState"] == "dead" {
81 status["Result"].as_str().or(status["SubState"].as_str())
82 } else {
83 status["SubState"].as_str()
84 }
85 .unwrap_or("unknown");
86
87 let unit_state = if status["LoadState"] == "not-found" {
88 "not-found"
89 } else {
90 status["UnitFileState"].as_str().unwrap_or("unknown")
91 };
92
93 return json!({
94 "service": service,
95 "name": name,
96 "desc": desc,
97 "state": state,
98 "unit-state": unit_state,
99 });
100 }
101
102 Value::Null
103 }
104
105 #[api(
106 input: {
107 properties: {
108 node: {
109 schema: NODE_SCHEMA,
110 },
111 },
112 },
113 returns: {
114 description: "Returns a list of systemd services.",
115 type: Array,
116 items: {
117 description: "Service details.",
118 properties: {
119 service: {
120 schema: SERVICE_ID_SCHEMA,
121 },
122 name: {
123 type: String,
124 description: "systemd service name.",
125 },
126 desc: {
127 type: String,
128 description: "systemd service description.",
129 },
130 state: {
131 type: String,
132 description: "systemd service 'SubState'.",
133 },
134 "unit-state": {
135 type: String,
136 description: "systemd service unit state.",
137 },
138 },
139 },
140 },
141 access: {
142 permission: &Permission::Privilege(&["system", "services"], PRIV_SYS_AUDIT, false),
143 },
144 )]
145 /// Service list.
146 fn list_services(_param: Value) -> Result<Value, Error> {
147 let mut list = vec![];
148
149 for service in &SERVICE_NAME_LIST {
150 match get_full_service_state(service) {
151 Ok(status) => {
152 let state = json_service_state(service, status);
153 if state != Value::Null {
154 list.push(state);
155 }
156 }
157 Err(err) => log::error!("{}", err),
158 }
159 }
160
161 Ok(Value::from(list))
162 }
163
164 #[api(
165 input: {
166 properties: {
167 node: {
168 schema: NODE_SCHEMA,
169 },
170 service: {
171 schema: SERVICE_ID_SCHEMA,
172 },
173 },
174 },
175 access: {
176 permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_AUDIT, false),
177 },
178 )]
179 /// Read service properties.
180 fn get_service_state(service: String, _param: Value) -> Result<Value, Error> {
181 let service = service.as_str();
182
183 if !SERVICE_NAME_LIST.contains(&service) {
184 bail!("unknown service name '{}'", service);
185 }
186
187 let status = get_full_service_state(service)?;
188
189 Ok(json_service_state(service, status))
190 }
191
192 fn run_service_command(service: &str, cmd: &str, auth_id: Authid) -> Result<Value, Error> {
193 let workerid = format!("srv{}", &cmd);
194
195 let cmd = match cmd {
196 "start" | "stop" | "restart" => cmd.to_string(),
197 "reload" => "try-reload-or-restart".to_string(), // some services do not implement reload
198 _ => bail!("unknown service command '{}'", cmd),
199 };
200 let service = service.to_string();
201
202 let upid = WorkerTask::new_thread(
203 &workerid,
204 Some(service.clone()),
205 auth_id.to_string(),
206 false,
207 move |_worker| {
208 if service == "proxmox-backup" && cmd == "stop" {
209 bail!(
210 "invalid service cmd '{} {}' cannot stop essential service!",
211 service,
212 cmd
213 );
214 }
215
216 let real_service_name = real_service_name(&service);
217
218 let status = Command::new("systemctl")
219 .args([&cmd, real_service_name])
220 .status()?;
221
222 if !status.success() {
223 bail!("systemctl {} failed with {}", cmd, status);
224 }
225
226 Ok(())
227 },
228 )?;
229
230 Ok(upid.into())
231 }
232
233 #[api(
234 protected: true,
235 input: {
236 properties: {
237 node: {
238 schema: NODE_SCHEMA,
239 },
240 service: {
241 schema: SERVICE_ID_SCHEMA,
242 },
243 },
244 },
245 access: {
246 permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
247 },
248 )]
249 /// Start service.
250 fn start_service(
251 service: String,
252 _param: Value,
253 rpcenv: &mut dyn RpcEnvironment,
254 ) -> Result<Value, Error> {
255 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
256
257 log::info!("starting service {}", service);
258
259 run_service_command(&service, "start", auth_id)
260 }
261
262 #[api(
263 protected: true,
264 input: {
265 properties: {
266 node: {
267 schema: NODE_SCHEMA,
268 },
269 service: {
270 schema: SERVICE_ID_SCHEMA,
271 },
272 },
273 },
274 access: {
275 permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
276 },
277 )]
278 /// Stop service.
279 fn stop_service(
280 service: String,
281 _param: Value,
282 rpcenv: &mut dyn RpcEnvironment,
283 ) -> Result<Value, Error> {
284 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
285
286 log::info!("stopping service {}", service);
287
288 run_service_command(&service, "stop", auth_id)
289 }
290
291 #[api(
292 protected: true,
293 input: {
294 properties: {
295 node: {
296 schema: NODE_SCHEMA,
297 },
298 service: {
299 schema: SERVICE_ID_SCHEMA,
300 },
301 },
302 },
303 access: {
304 permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
305 },
306 )]
307 /// Retart service.
308 fn restart_service(
309 service: String,
310 _param: Value,
311 rpcenv: &mut dyn RpcEnvironment,
312 ) -> Result<Value, Error> {
313 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
314
315 log::info!("re-starting service {}", service);
316
317 if &service == "proxmox-backup-proxy" {
318 // special case, avoid aborting running tasks
319 run_service_command(&service, "reload", auth_id)
320 } else {
321 run_service_command(&service, "restart", auth_id)
322 }
323 }
324
325 #[api(
326 protected: true,
327 input: {
328 properties: {
329 node: {
330 schema: NODE_SCHEMA,
331 },
332 service: {
333 schema: SERVICE_ID_SCHEMA,
334 },
335 },
336 },
337 access: {
338 permission: &Permission::Privilege(&["system", "services", "{service}"], PRIV_SYS_MODIFY, false),
339 },
340 )]
341 /// Reload service.
342 fn reload_service(
343 service: String,
344 _param: Value,
345 rpcenv: &mut dyn RpcEnvironment,
346 ) -> Result<Value, Error> {
347 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
348
349 log::info!("reloading service {}", service);
350
351 run_service_command(&service, "reload", auth_id)
352 }
353
354 #[sortable]
355 const SERVICE_SUBDIRS: SubdirMap = &sorted!([
356 ("reload", &Router::new().post(&API_METHOD_RELOAD_SERVICE)),
357 ("restart", &Router::new().post(&API_METHOD_RESTART_SERVICE)),
358 ("start", &Router::new().post(&API_METHOD_START_SERVICE)),
359 ("state", &Router::new().get(&API_METHOD_GET_SERVICE_STATE)),
360 ("stop", &Router::new().post(&API_METHOD_STOP_SERVICE)),
361 ]);
362
363 const SERVICE_ROUTER: Router = Router::new()
364 .get(&list_subdirs_api_method!(SERVICE_SUBDIRS))
365 .subdirs(SERVICE_SUBDIRS);
366
367 pub const ROUTER: Router = Router::new()
368 .get(&API_METHOD_LIST_SERVICES)
369 .match_all("service", &SERVICE_ROUTER);