]>
Commit | Line | Data |
---|---|---|
e6263c26 DC |
1 | //! Generic JobState handling |
2 | //! | |
3 | //! A 'Job' can have 3 states | |
4 | //! - Created, when a schedule was created but never executed | |
5 | //! - Started, when a job is running right now | |
6 | //! - Finished, when a job was running in the past | |
7 | //! | |
8 | //! and is identified by 2 values: jobtype and jobname (e.g. 'syncjob' and 'myfirstsyncjob') | |
9 | //! | |
10 | //! This module Provides 2 helper structs to handle those coniditons | |
11 | //! 'Job' which handles locking and writing to a file | |
12 | //! 'JobState' which is the actual state | |
13 | //! | |
14 | //! an example usage would be | |
15 | //! ```no_run | |
16 | //! # use anyhow::{bail, Error}; | |
17 | //! # use proxmox_backup::server::TaskState; | |
18 | //! # use proxmox_backup::config::jobstate::*; | |
77bd2a46 | 19 | //! # fn some_code() -> TaskState { TaskState::OK { endtime: 0 } } |
e6263c26 DC |
20 | //! # fn code() -> Result<(), Error> { |
21 | //! // locks the correct file under /var/lib | |
22 | //! // or fails if someone else holds the lock | |
23 | //! let mut job = match Job::new("jobtype", "jobname") { | |
24 | //! Ok(job) => job, | |
25 | //! Err(err) => bail!("could not lock jobstate"), | |
26 | //! }; | |
27 | //! | |
93bb51fe | 28 | //! // job holds the lock, we can start it |
e6263c26 DC |
29 | //! job.start("someupid")?; |
30 | //! // do something | |
31 | //! let task_state = some_code(); | |
32 | //! job.finish(task_state)?; | |
33 | //! | |
34 | //! // release the lock | |
35 | //! drop(job); | |
36 | //! # Ok(()) | |
37 | //! # } | |
38 | //! | |
39 | //! ``` | |
40 | use std::fs::File; | |
41 | use std::path::{Path, PathBuf}; | |
42 | use std::time::Duration; | |
43 | ||
4ea831bf WB |
44 | use anyhow::{bail, format_err, Error}; |
45 | use proxmox::tools::fs::{ | |
46 | create_path, file_read_optional_string, open_file_locked, replace_file, CreateOptions, | |
47 | }; | |
48 | use serde::{Deserialize, Serialize}; | |
e6263c26 | 49 | |
4ea831bf | 50 | use crate::server::{upid_read_status, worker_is_active_local, TaskState, UPID}; |
e6263c26 | 51 | |
4ea831bf WB |
52 | #[serde(rename_all = "kebab-case")] |
53 | #[derive(Serialize, Deserialize)] | |
e6263c26 DC |
54 | /// Represents the State of a specific Job |
55 | pub enum JobState { | |
56 | /// A job was created at 'time', but never started/finished | |
57 | Created { time: i64 }, | |
58 | /// The Job was last started in 'upid', | |
59 | Started { upid: String }, | |
77bd2a46 | 60 | /// The Job was last started in 'upid', which finished with 'state' |
4ea831bf | 61 | Finished { upid: String, state: TaskState }, |
e6263c26 DC |
62 | } |
63 | ||
64 | /// Represents a Job and holds the correct lock | |
65 | pub struct Job { | |
66 | jobtype: String, | |
67 | jobname: String, | |
68 | /// The State of the job | |
69 | pub state: JobState, | |
70 | _lock: File, | |
71 | } | |
72 | ||
73 | const JOB_STATE_BASEDIR: &str = "/var/lib/proxmox-backup/jobstates"; | |
74 | ||
75 | /// Create jobstate stat dir with correct permission | |
76 | pub fn create_jobstate_dir() -> Result<(), Error> { | |
77 | let backup_user = crate::backup::backup_user()?; | |
78 | let opts = CreateOptions::new() | |
79 | .owner(backup_user.uid) | |
80 | .group(backup_user.gid); | |
81 | ||
82 | create_path(JOB_STATE_BASEDIR, None, Some(opts)) | |
83 | .map_err(|err: Error| format_err!("unable to create rrdb stat dir - {}", err))?; | |
84 | ||
85 | Ok(()) | |
86 | } | |
87 | ||
88 | fn get_path(jobtype: &str, jobname: &str) -> PathBuf { | |
89 | let mut path = PathBuf::from(JOB_STATE_BASEDIR); | |
90 | path.push(format!("{}-{}.json", jobtype, jobname)); | |
91 | path | |
92 | } | |
93 | ||
94 | fn get_lock<P>(path: P) -> Result<File, Error> | |
95 | where | |
4ea831bf | 96 | P: AsRef<Path>, |
e6263c26 DC |
97 | { |
98 | let mut path = path.as_ref().to_path_buf(); | |
99 | path.set_extension("lck"); | |
b56c111e | 100 | let lock = open_file_locked(&path, Duration::new(10, 0), true)?; |
93bb51fe DC |
101 | let backup_user = crate::backup::backup_user()?; |
102 | nix::unistd::chown(&path, Some(backup_user.uid), Some(backup_user.gid))?; | |
103 | Ok(lock) | |
e6263c26 DC |
104 | } |
105 | ||
106 | /// Removes the statefile of a job, this is useful if we delete a job | |
107 | pub fn remove_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> { | |
93bb51fe | 108 | let mut path = get_path(jobtype, jobname); |
e6263c26 | 109 | let _lock = get_lock(&path)?; |
4ea831bf WB |
110 | std::fs::remove_file(&path).map_err(|err| { |
111 | format_err!( | |
112 | "cannot remove statefile for {} - {}: {}", | |
113 | jobtype, | |
114 | jobname, | |
115 | err | |
116 | ) | |
117 | })?; | |
93bb51fe DC |
118 | path.set_extension("lck"); |
119 | // ignore errors | |
4ea831bf WB |
120 | let _ = std::fs::remove_file(&path).map_err(|err| { |
121 | format_err!( | |
122 | "cannot remove lockfile for {} - {}: {}", | |
123 | jobtype, | |
124 | jobname, | |
125 | err | |
126 | ) | |
127 | }); | |
93bb51fe DC |
128 | Ok(()) |
129 | } | |
130 | ||
131 | /// Creates the statefile with the state 'Created' | |
132 | /// overwrites if it exists already | |
133 | pub fn create_state_file(jobtype: &str, jobname: &str) -> Result<(), Error> { | |
134 | let mut job = Job::new(jobtype, jobname)?; | |
135 | job.write_state() | |
e6263c26 DC |
136 | } |
137 | ||
138 | /// Returns the last run time of a job by reading the statefile | |
139 | /// Note that this is not locked | |
140 | pub fn last_run_time(jobtype: &str, jobname: &str) -> Result<i64, Error> { | |
141 | match JobState::load(jobtype, jobname)? { | |
142 | JobState::Created { time } => Ok(time), | |
143 | JobState::Started { upid } | JobState::Finished { upid, .. } => { | |
4ea831bf WB |
144 | let upid: UPID = upid |
145 | .parse() | |
146 | .map_err(|err| format_err!("could not parse upid from state: {}", err))?; | |
e6263c26 DC |
147 | Ok(upid.starttime) |
148 | } | |
149 | } | |
150 | } | |
151 | ||
152 | impl JobState { | |
153 | /// Loads and deserializes the jobstate from type and name. | |
154 | /// When the loaded state indicates a started UPID, | |
155 | /// we go and check if it has already stopped, and | |
156 | /// returning the correct state. | |
157 | /// | |
158 | /// This does not update the state in the file. | |
159 | pub fn load(jobtype: &str, jobname: &str) -> Result<Self, Error> { | |
160 | if let Some(state) = file_read_optional_string(get_path(jobtype, jobname))? { | |
161 | match serde_json::from_str(&state)? { | |
162 | JobState::Started { upid } => { | |
4ea831bf WB |
163 | let parsed: UPID = upid |
164 | .parse() | |
e6263c26 DC |
165 | .map_err(|err| format_err!("error parsing upid: {}", err))?; |
166 | ||
167 | if !worker_is_active_local(&parsed) { | |
77bd2a46 | 168 | let state = upid_read_status(&parsed) |
e6263c26 DC |
169 | .map_err(|err| format_err!("error reading upid log status: {}", err))?; |
170 | ||
4ea831bf | 171 | Ok(JobState::Finished { upid, state }) |
e6263c26 DC |
172 | } else { |
173 | Ok(JobState::Started { upid }) | |
174 | } | |
175 | } | |
176 | other => Ok(other), | |
177 | } | |
178 | } else { | |
179 | Ok(JobState::Created { | |
6a7be83e | 180 | time: proxmox::tools::time::epoch_i64() - 30, |
e6263c26 DC |
181 | }) |
182 | } | |
183 | } | |
184 | } | |
185 | ||
186 | impl Job { | |
187 | /// Creates a new instance of a job with the correct lock held | |
188 | /// (will be hold until the job is dropped again). | |
189 | /// | |
190 | /// This does not load the state from the file, to do that, | |
191 | /// 'load' must be called | |
192 | pub fn new(jobtype: &str, jobname: &str) -> Result<Self, Error> { | |
193 | let path = get_path(jobtype, jobname); | |
194 | ||
195 | let _lock = get_lock(&path)?; | |
196 | ||
4ea831bf | 197 | Ok(Self { |
e6263c26 DC |
198 | jobtype: jobtype.to_string(), |
199 | jobname: jobname.to_string(), | |
200 | state: JobState::Created { | |
6a7be83e | 201 | time: proxmox::tools::time::epoch_i64(), |
e6263c26 DC |
202 | }, |
203 | _lock, | |
204 | }) | |
205 | } | |
206 | ||
e6263c26 DC |
207 | /// Start the job and update the statefile accordingly |
208 | /// Fails if the job was already started | |
209 | pub fn start(&mut self, upid: &str) -> Result<(), Error> { | |
210 | match self.state { | |
211 | JobState::Started { .. } => { | |
212 | bail!("cannot start job that is started!"); | |
213 | } | |
214 | _ => {} | |
215 | } | |
216 | ||
4ea831bf | 217 | self.state = JobState::Started { |
e6263c26 DC |
218 | upid: upid.to_string(), |
219 | }; | |
220 | ||
221 | self.write_state() | |
222 | } | |
223 | ||
224 | /// Finish the job and update the statefile accordingly with the given taskstate | |
225 | /// Fails if the job was not yet started | |
226 | pub fn finish(&mut self, state: TaskState) -> Result<(), Error> { | |
227 | let upid = match &self.state { | |
228 | JobState::Created { .. } => bail!("cannot finish when not started"), | |
229 | JobState::Started { upid } => upid, | |
230 | JobState::Finished { upid, .. } => upid, | |
4ea831bf WB |
231 | } |
232 | .to_string(); | |
e6263c26 | 233 | |
4ea831bf | 234 | self.state = JobState::Finished { upid, state }; |
e6263c26 DC |
235 | |
236 | self.write_state() | |
237 | } | |
238 | ||
713b66b6 DC |
239 | pub fn jobtype(&self) -> &str { |
240 | &self.jobtype | |
241 | } | |
242 | ||
243 | pub fn jobname(&self) -> &str { | |
244 | &self.jobname | |
245 | } | |
246 | ||
e6263c26 DC |
247 | fn write_state(&mut self) -> Result<(), Error> { |
248 | let serialized = serde_json::to_string(&self.state)?; | |
249 | let path = get_path(&self.jobtype, &self.jobname); | |
250 | ||
251 | let backup_user = crate::backup::backup_user()?; | |
252 | let mode = nix::sys::stat::Mode::from_bits_truncate(0o0644); | |
253 | // set the correct owner/group/permissions while saving file | |
254 | // owner(rw) = backup, group(r)= backup | |
255 | let options = CreateOptions::new() | |
256 | .perm(mode) | |
257 | .owner(backup_user.uid) | |
258 | .group(backup_user.gid); | |
259 | ||
4ea831bf | 260 | replace_file(path, serialized.as_bytes(), options) |
e6263c26 DC |
261 | } |
262 | } |