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