]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/tape/media.rs
tape: MediaListEntry - add ctime
[proxmox-backup.git] / src / api2 / tape / media.rs
1 use std::path::Path;
2
3 use anyhow::{bail, format_err, Error};
4 use serde::{Serialize, Deserialize};
5
6 use proxmox::{
7 api::{api, Router, SubdirMap},
8 list_subdirs_api_method,
9 };
10
11 use crate::{
12 config::{
13 self,
14 },
15 api2::types::{
16 BACKUP_ID_SCHEMA,
17 BACKUP_TYPE_SCHEMA,
18 MEDIA_POOL_NAME_SCHEMA,
19 MEDIA_LABEL_SCHEMA,
20 MediaPoolConfig,
21 MediaListEntry,
22 MediaStatus,
23 MediaContentEntry,
24 },
25 backup::{
26 BackupDir,
27 },
28 tape::{
29 TAPE_STATUS_DIR,
30 Inventory,
31 MediaPool,
32 MediaCatalog,
33 update_online_status,
34 },
35 };
36
37 #[api(
38 input: {
39 properties: {
40 pool: {
41 schema: MEDIA_POOL_NAME_SCHEMA,
42 optional: true,
43 },
44 },
45 },
46 returns: {
47 description: "List of registered backup media.",
48 type: Array,
49 items: {
50 type: MediaListEntry,
51 },
52 },
53 )]
54 /// List pool media
55 pub async fn list_media(pool: Option<String>) -> Result<Vec<MediaListEntry>, Error> {
56
57 let (config, _digest) = config::media_pool::config()?;
58
59 let status_path = Path::new(TAPE_STATUS_DIR);
60
61 let catalogs = tokio::task::spawn_blocking(move || {
62 // update online media status
63 if let Err(err) = update_online_status(status_path) {
64 eprintln!("{}", err);
65 eprintln!("update online media status failed - using old state");
66 }
67 // test what catalog files we have
68 MediaCatalog::media_with_catalogs(status_path)
69 }).await??;
70
71 let mut list = Vec::new();
72
73 for (_section_type, data) in config.sections.values() {
74 let pool_name = match data["name"].as_str() {
75 None => continue,
76 Some(name) => name,
77 };
78 if let Some(ref name) = pool {
79 if name != pool_name {
80 continue;
81 }
82 }
83
84 let config: MediaPoolConfig = config.lookup("pool", pool_name)?;
85
86 let use_offline_media = true; // does not matter here
87 let pool = MediaPool::with_config(status_path, &config, use_offline_media)?;
88
89 let current_time = proxmox::tools::time::epoch_i64();
90
91 for media in pool.list_media() {
92 let expired = pool.media_is_expired(&media, current_time);
93
94 let media_set_uuid = media.media_set_label()
95 .map(|set| set.uuid.to_string());
96
97 let seq_nr = media.media_set_label()
98 .map(|set| set.seq_nr);
99
100 let media_set_name = media.media_set_label()
101 .map(|set| {
102 pool.generate_media_set_name(&set.uuid, config.template.clone())
103 .unwrap_or_else(|_| set.uuid.to_string())
104 });
105
106 let catalog_ok = if media.media_set_label().is_none() {
107 // Media is empty, we need no catalog
108 true
109 } else {
110 catalogs.contains(media.uuid())
111 };
112
113 list.push(MediaListEntry {
114 uuid: media.uuid().to_string(),
115 changer_id: media.changer_id().to_string(),
116 ctime: media.ctime(),
117 pool: Some(pool_name.to_string()),
118 location: media.location().clone(),
119 status: *media.status(),
120 catalog: catalog_ok,
121 expired,
122 media_set_ctime: media.media_set_label().map(|set| set.ctime),
123 media_set_uuid,
124 media_set_name,
125 seq_nr,
126 });
127 }
128 }
129
130 if pool.is_none() {
131
132 let inventory = Inventory::load(status_path)?;
133
134 for media_id in inventory.list_unassigned_media() {
135
136 let (mut status, location) = inventory.status_and_location(&media_id.label.uuid);
137
138 if status == MediaStatus::Unknown {
139 status = MediaStatus::Writable;
140 }
141
142 list.push(MediaListEntry {
143 uuid: media_id.label.uuid.to_string(),
144 ctime: media_id.label.ctime,
145 changer_id: media_id.label.changer_id.to_string(),
146 location,
147 status,
148 catalog: true, // empty, so we do not need a catalog
149 expired: false,
150 media_set_uuid: None,
151 media_set_name: None,
152 media_set_ctime: None,
153 seq_nr: None,
154 pool: None,
155 });
156 }
157 }
158
159 Ok(list)
160 }
161
162 #[api(
163 input: {
164 properties: {
165 "changer-id": {
166 schema: MEDIA_LABEL_SCHEMA,
167 },
168 force: {
169 description: "Force removal (even if media is used in a media set).",
170 type: bool,
171 optional: true,
172 },
173 },
174 },
175 )]
176 /// Destroy media (completely remove from database)
177 pub fn destroy_media(changer_id: String, force: Option<bool>,) -> Result<(), Error> {
178
179 let force = force.unwrap_or(false);
180
181 let status_path = Path::new(TAPE_STATUS_DIR);
182 let mut inventory = Inventory::load(status_path)?;
183
184 let media_id = inventory.find_media_by_changer_id(&changer_id)
185 .ok_or_else(|| format_err!("no such media '{}'", changer_id))?;
186
187 if !force {
188 if let Some(ref set) = media_id.media_set_label {
189 let is_empty = set.uuid.as_ref() == [0u8;16];
190 if !is_empty {
191 bail!("media '{}' contains data (please use 'force' flag to remove.", changer_id);
192 }
193 }
194 }
195
196 let uuid = media_id.label.uuid.clone();
197 drop(media_id);
198
199 inventory.remove_media(&uuid)?;
200
201 Ok(())
202 }
203
204 #[api(
205 properties: {
206 pool: {
207 schema: MEDIA_POOL_NAME_SCHEMA,
208 optional: true,
209 },
210 "changer-id": {
211 schema: MEDIA_LABEL_SCHEMA,
212 optional: true,
213 },
214 "media": {
215 description: "Filter by media UUID.",
216 type: String,
217 optional: true,
218 },
219 "media-set": {
220 description: "Filter by media set UUID.",
221 type: String,
222 optional: true,
223 },
224 "backup-type": {
225 schema: BACKUP_TYPE_SCHEMA,
226 optional: true,
227 },
228 "backup-id": {
229 schema: BACKUP_ID_SCHEMA,
230 optional: true,
231 },
232 },
233 )]
234 #[derive(Serialize,Deserialize)]
235 #[serde(rename_all="kebab-case")]
236 /// Content list filter parameters
237 pub struct MediaContentListFilter {
238 pub pool: Option<String>,
239 pub changer_id: Option<String>,
240 pub media: Option<String>,
241 pub media_set: Option<String>,
242 pub backup_type: Option<String>,
243 pub backup_id: Option<String>,
244 }
245
246 #[api(
247 input: {
248 properties: {
249 "filter": {
250 type: MediaContentListFilter,
251 flatten: true,
252 },
253 },
254 },
255 returns: {
256 description: "Media content list.",
257 type: Array,
258 items: {
259 type: MediaContentEntry,
260 },
261 },
262 )]
263 /// List media content
264 pub fn list_content(
265 filter: MediaContentListFilter,
266 ) -> Result<Vec<MediaContentEntry>, Error> {
267
268 let (config, _digest) = config::media_pool::config()?;
269
270 let status_path = Path::new(TAPE_STATUS_DIR);
271 let inventory = Inventory::load(status_path)?;
272
273 let media_uuid = filter.media.and_then(|s| s.parse().ok());
274 let media_set_uuid = filter.media_set.and_then(|s| s.parse().ok());
275
276 let mut list = Vec::new();
277
278 for media_id in inventory.list_used_media() {
279 let set = media_id.media_set_label.as_ref().unwrap();
280
281 if let Some(ref changer_id) = filter.changer_id {
282 if &media_id.label.changer_id != changer_id { continue; }
283 }
284
285 if let Some(ref pool) = filter.pool {
286 if &set.pool != pool { continue; }
287 }
288
289 if let Some(ref media_uuid) = media_uuid {
290 if &media_id.label.uuid != media_uuid { continue; }
291 }
292
293 if let Some(ref media_set_uuid) = media_set_uuid {
294 if &set.uuid != media_set_uuid { continue; }
295 }
296
297 let config: MediaPoolConfig = config.lookup("pool", &set.pool)?;
298
299 let media_set_name = inventory
300 .generate_media_set_name(&set.uuid, config.template.clone())
301 .unwrap_or_else(|_| set.uuid.to_string());
302
303 let catalog = MediaCatalog::open(status_path, &media_id.label.uuid, false, false)?;
304
305 for snapshot in catalog.snapshot_index().keys() {
306 let backup_dir: BackupDir = snapshot.parse()?;
307
308 if let Some(ref backup_type) = filter.backup_type {
309 if backup_dir.group().backup_type() != backup_type { continue; }
310 }
311 if let Some(ref backup_id) = filter.backup_id {
312 if backup_dir.group().backup_id() != backup_id { continue; }
313 }
314
315 list.push(MediaContentEntry {
316 uuid: media_id.label.uuid.to_string(),
317 changer_id: media_id.label.changer_id.to_string(),
318 pool: set.pool.clone(),
319 media_set_name: media_set_name.clone(),
320 media_set_uuid: set.uuid.to_string(),
321 seq_nr: set.seq_nr,
322 snapshot: snapshot.to_owned(),
323 backup_time: backup_dir.backup_time(),
324 });
325 }
326 }
327
328 Ok(list)
329 }
330
331 const SUBDIRS: SubdirMap = &[
332 (
333 "content",
334 &Router::new()
335 .get(&API_METHOD_LIST_CONTENT)
336 ),
337 (
338 "destroy",
339 &Router::new()
340 .get(&API_METHOD_DESTROY_MEDIA)
341 ),
342 (
343 "list",
344 &Router::new()
345 .get(&API_METHOD_LIST_MEDIA)
346 ),
347 ];
348
349
350 pub const ROUTER: Router = Router::new()
351 .get(&list_subdirs_api_method!(SUBDIRS))
352 .subdirs(SUBDIRS);