]> git.proxmox.com Git - proxmox-backup.git/blob - src/api2/config/changer.rs
api: tape: don't allow overwriting of ids in changer/drive config
[proxmox-backup.git] / src / api2 / config / changer.rs
1 use ::serde::{Deserialize, Serialize};
2 use anyhow::Error;
3 use hex::FromHex;
4 use serde_json::Value;
5
6 use proxmox_router::{http_bail, Permission, Router, RpcEnvironment};
7 use proxmox_schema::{api, param_bail};
8
9 use pbs_api_types::{
10 Authid, LtoTapeDrive, ScsiTapeChanger, ScsiTapeChangerUpdater, CHANGER_NAME_SCHEMA,
11 PRIV_TAPE_AUDIT, PRIV_TAPE_MODIFY, PROXMOX_CONFIG_DIGEST_SCHEMA, SLOT_ARRAY_SCHEMA,
12 };
13 use pbs_config::CachedUserInfo;
14 use pbs_tape::linux_list_drives::{check_drive_path, linux_tape_changer_list};
15
16 #[api(
17 protected: true,
18 input: {
19 properties: {
20 config: {
21 type: ScsiTapeChanger,
22 flatten: true,
23 },
24 },
25 },
26 access: {
27 permission: &Permission::Privilege(&["tape", "device"], PRIV_TAPE_MODIFY, false),
28 },
29 )]
30 /// Create a new changer device
31 pub fn create_changer(config: ScsiTapeChanger) -> Result<(), Error> {
32 let _lock = pbs_config::drive::lock()?;
33
34 let (mut section_config, _digest) = pbs_config::drive::config()?;
35
36 if section_config.sections.get(&config.name).is_some() {
37 param_bail!("name", "Entry '{}' already exists", config.name);
38 }
39
40 let linux_changers = linux_tape_changer_list();
41
42 check_drive_path(&linux_changers, &config.path)?;
43
44 let existing: Vec<ScsiTapeChanger> = section_config.convert_to_typed_array("changer")?;
45
46 for changer in existing {
47 if changer.path == config.path {
48 param_bail!(
49 "path",
50 "Path '{}' already in use by '{}'",
51 config.path,
52 changer.name
53 );
54 }
55 }
56
57 section_config.set_data(&config.name, "changer", &config)?;
58
59 pbs_config::drive::save_config(&section_config)?;
60
61 Ok(())
62 }
63
64 #[api(
65 input: {
66 properties: {
67 name: {
68 schema: CHANGER_NAME_SCHEMA,
69 },
70 },
71 },
72 returns: {
73 type: ScsiTapeChanger,
74 },
75 access: {
76 permission: &Permission::Privilege(&["tape", "device", "{name}"], PRIV_TAPE_AUDIT, false),
77 },
78 )]
79 /// Get tape changer configuration
80 pub fn get_config(
81 name: String,
82 _param: Value,
83 rpcenv: &mut dyn RpcEnvironment,
84 ) -> Result<ScsiTapeChanger, Error> {
85 let (config, digest) = pbs_config::drive::config()?;
86
87 let data: ScsiTapeChanger = config.lookup("changer", &name)?;
88
89 rpcenv["digest"] = hex::encode(digest).into();
90
91 Ok(data)
92 }
93
94 #[api(
95 input: {
96 properties: {},
97 },
98 returns: {
99 description: "The list of configured changers (with config digest).",
100 type: Array,
101 items: {
102 type: ScsiTapeChanger,
103 },
104 },
105 access: {
106 description: "List configured tape changer filtered by Tape.Audit privileges",
107 permission: &Permission::Anybody,
108 },
109 )]
110 /// List changers
111 pub fn list_changers(
112 _param: Value,
113 rpcenv: &mut dyn RpcEnvironment,
114 ) -> Result<Vec<ScsiTapeChanger>, Error> {
115 let auth_id: Authid = rpcenv.get_auth_id().unwrap().parse()?;
116 let user_info = CachedUserInfo::new()?;
117
118 let (config, digest) = pbs_config::drive::config()?;
119
120 let list: Vec<ScsiTapeChanger> = config.convert_to_typed_array("changer")?;
121
122 let list = list
123 .into_iter()
124 .filter(|changer| {
125 let privs = user_info.lookup_privs(&auth_id, &["tape", "device", &changer.name]);
126 privs & PRIV_TAPE_AUDIT != 0
127 })
128 .collect();
129
130 rpcenv["digest"] = hex::encode(digest).into();
131
132 Ok(list)
133 }
134 #[api()]
135 #[derive(Serialize, Deserialize)]
136 #[serde(rename_all = "kebab-case")]
137 /// Deletable property name
138 pub enum DeletableProperty {
139 /// Delete export-slots.
140 ExportSlots,
141 /// Delete eject-before-unload.
142 EjectBeforeUnload,
143 }
144
145 #[api(
146 protected: true,
147 input: {
148 properties: {
149 name: {
150 schema: CHANGER_NAME_SCHEMA,
151 },
152 update: {
153 type: ScsiTapeChangerUpdater,
154 flatten: true,
155 },
156 delete: {
157 description: "List of properties to delete.",
158 type: Array,
159 optional: true,
160 items: {
161 type: DeletableProperty,
162 },
163 },
164 digest: {
165 schema: PROXMOX_CONFIG_DIGEST_SCHEMA,
166 optional: true,
167 },
168 },
169 },
170 access: {
171 permission: &Permission::Privilege(&["tape", "device", "{name}"], PRIV_TAPE_MODIFY, false),
172 },
173 )]
174 /// Update a tape changer configuration
175 pub fn update_changer(
176 name: String,
177 update: ScsiTapeChangerUpdater,
178 delete: Option<Vec<DeletableProperty>>,
179 digest: Option<String>,
180 _param: Value,
181 ) -> Result<(), Error> {
182 let _lock = pbs_config::drive::lock()?;
183
184 let (mut config, expected_digest) = pbs_config::drive::config()?;
185
186 if let Some(ref digest) = digest {
187 let digest = <[u8; 32]>::from_hex(digest)?;
188 crate::tools::detect_modified_configuration_file(&digest, &expected_digest)?;
189 }
190
191 let mut data: ScsiTapeChanger = config.lookup("changer", &name)?;
192
193 if let Some(delete) = delete {
194 for delete_prop in delete {
195 match delete_prop {
196 DeletableProperty::ExportSlots => {
197 data.export_slots = None;
198 }
199 DeletableProperty::EjectBeforeUnload => {
200 data.eject_before_unload = None;
201 }
202 }
203 }
204 }
205
206 if let Some(path) = update.path {
207 let changers = linux_tape_changer_list();
208 check_drive_path(&changers, &path)?;
209 data.path = path;
210 }
211
212 if let Some(export_slots) = update.export_slots {
213 let slots: Value = SLOT_ARRAY_SCHEMA.parse_property_string(&export_slots)?;
214 let mut slots: Vec<String> = slots
215 .as_array()
216 .unwrap()
217 .iter()
218 .map(|v| v.to_string())
219 .collect();
220 slots.sort();
221
222 if slots.is_empty() {
223 data.export_slots = None;
224 } else {
225 let slots = slots.join(",");
226 data.export_slots = Some(slots);
227 }
228 }
229
230 if let Some(eject_before_unload) = update.eject_before_unload {
231 data.eject_before_unload = Some(eject_before_unload);
232 }
233
234 config.set_data(&name, "changer", &data)?;
235
236 pbs_config::drive::save_config(&config)?;
237
238 Ok(())
239 }
240
241 #[api(
242 protected: true,
243 input: {
244 properties: {
245 name: {
246 schema: CHANGER_NAME_SCHEMA,
247 },
248 },
249 },
250 access: {
251 permission: &Permission::Privilege(&["tape", "device", "{name}"], PRIV_TAPE_MODIFY, false),
252 },
253 )]
254 /// Delete a tape changer configuration
255 pub fn delete_changer(name: String, _param: Value) -> Result<(), Error> {
256 let _lock = pbs_config::drive::lock()?;
257
258 let (mut config, _digest) = pbs_config::drive::config()?;
259
260 match config.sections.get(&name) {
261 Some((section_type, _)) => {
262 if section_type != "changer" {
263 param_bail!(
264 "name",
265 "Entry '{}' exists, but is not a changer device",
266 name
267 );
268 }
269 config.sections.remove(&name);
270 }
271 None => http_bail!(
272 NOT_FOUND,
273 "Delete changer '{}' failed - no such entry",
274 name
275 ),
276 }
277
278 let drive_list: Vec<LtoTapeDrive> = config.convert_to_typed_array("lto")?;
279 for drive in drive_list {
280 if let Some(changer) = drive.changer {
281 if changer == name {
282 param_bail!(
283 "name",
284 "Delete changer '{}' failed - used by drive '{}'",
285 name,
286 drive.name
287 );
288 }
289 }
290 }
291
292 pbs_config::drive::save_config(&config)?;
293
294 Ok(())
295 }
296
297 const ITEM_ROUTER: Router = Router::new()
298 .get(&API_METHOD_GET_CONFIG)
299 .put(&API_METHOD_UPDATE_CHANGER)
300 .delete(&API_METHOD_DELETE_CHANGER);
301
302 pub const ROUTER: Router = Router::new()
303 .get(&API_METHOD_LIST_CHANGERS)
304 .post(&API_METHOD_CREATE_CHANGER)
305 .match_all("name", &ITEM_ROUTER);