]> git.proxmox.com Git - proxmox-backup.git/blame - src/tape/media_pool.rs
tape: lto: increase default timeout to 10 minutes
[proxmox-backup.git] / src / tape / media_pool.rs
CommitLineData
c4d8542e
DM
1//! Media Pool
2//!
3//! A set of backup medias.
4//!
5//! This struct manages backup media state during backup. The main
d1d74c43 6//! purpose is to allocate media sets and assign new tapes to it.
c4d8542e
DM
7//!
8//!
9
1057b1f5 10use std::path::{PathBuf, Path};
30316192 11
c4d8542e
DM
12use anyhow::{bail, Error};
13use ::serde::{Deserialize, Serialize};
14
15use proxmox::tools::Uuid;
16
17use crate::{
7526d864 18 backup::{Fingerprint, BackupLockGuard},
c4d8542e
DM
19 api2::types::{
20 MediaStatus,
c1c2c8f6 21 MediaLocation,
c4d8542e
DM
22 MediaSetPolicy,
23 RetentionPolicy,
24 MediaPoolConfig,
25 },
26 tools::systemd::time::compute_next_event,
27 tape::{
28 MediaId,
29 MediaSet,
c4d8542e 30 Inventory,
f281b8d3 31 MediaCatalog,
30316192
DM
32 lock_media_set,
33 lock_media_pool,
34 lock_unassigned_media_pool,
c4d8542e 35 file_formats::{
a78348ac 36 MediaLabel,
c4d8542e
DM
37 MediaSetLabel,
38 },
39 }
40};
41
c4d8542e
DM
42/// Media Pool
43pub struct MediaPool {
44
45 name: String,
1057b1f5 46 state_path: PathBuf,
c4d8542e
DM
47
48 media_set_policy: MediaSetPolicy,
49 retention: RetentionPolicy,
cdf39e62
DM
50
51 changer_name: Option<String>,
ab77d660 52 force_media_availability: bool,
cdf39e62 53
30316192
DM
54 // Set this if you do not need to allocate writeable media - this
55 // is useful for list_media()
56 no_media_set_locking: bool,
57
8a0046f5 58 encrypt_fingerprint: Option<Fingerprint>,
c4d8542e
DM
59
60 inventory: Inventory,
c4d8542e
DM
61
62 current_media_set: MediaSet,
7526d864 63 current_media_set_lock: Option<BackupLockGuard>,
c4d8542e
DM
64}
65
66impl MediaPool {
67
68 /// Creates a new instance
cdf39e62
DM
69 ///
70 /// If you specify a `changer_name`, only media accessible via
71 /// that changer is considered available. If you pass `None` for
72 /// `changer`, all offline media is considered available (backups
73 /// to standalone drives may not use media from inside a tape
74 /// library).
c4d8542e
DM
75 pub fn new(
76 name: &str,
77 state_path: &Path,
78 media_set_policy: MediaSetPolicy,
79 retention: RetentionPolicy,
cdf39e62 80 changer_name: Option<String>,
8a0046f5 81 encrypt_fingerprint: Option<Fingerprint>,
30316192 82 no_media_set_locking: bool, // for list_media()
c4d8542e
DM
83 ) -> Result<Self, Error> {
84
30316192
DM
85 let _pool_lock = if no_media_set_locking {
86 None
87 } else {
88 Some(lock_media_pool(state_path, name)?)
89 };
90
c4d8542e
DM
91 let inventory = Inventory::load(state_path)?;
92
93 let current_media_set = match inventory.latest_media_set(name) {
94 Some(set_uuid) => inventory.compute_media_set_members(&set_uuid)?,
95 None => MediaSet::new(),
96 };
97
30316192
DM
98 let current_media_set_lock = if no_media_set_locking {
99 None
100 } else {
101 Some(lock_media_set(state_path, current_media_set.uuid(), None)?)
102 };
103
c4d8542e
DM
104 Ok(MediaPool {
105 name: String::from(name),
1057b1f5 106 state_path: state_path.to_owned(),
c4d8542e
DM
107 media_set_policy,
108 retention,
cdf39e62 109 changer_name,
c4d8542e 110 inventory,
c4d8542e 111 current_media_set,
30316192 112 current_media_set_lock,
8a0046f5 113 encrypt_fingerprint,
ab77d660 114 force_media_availability: false,
30316192 115 no_media_set_locking,
c4d8542e
DM
116 })
117 }
118
ab77d660
DM
119 /// Pretend all Online(x) and Offline media is available
120 ///
121 /// Only media in Vault(y) is considered unavailable.
122 pub fn force_media_availability(&mut self) {
123 self.force_media_availability = true;
124 }
125
32b75d36
DM
126 /// Returns the the current media set
127 pub fn current_media_set(&self) -> &MediaSet {
128 &self.current_media_set
90e16be3
DM
129 }
130
c4d8542e
DM
131 /// Creates a new instance using the media pool configuration
132 pub fn with_config(
c4d8542e
DM
133 state_path: &Path,
134 config: &MediaPoolConfig,
cdf39e62 135 changer_name: Option<String>,
30316192 136 no_media_set_locking: bool, // for list_media()
c4d8542e
DM
137 ) -> Result<Self, Error> {
138
e062ebbc 139 let allocation = config.allocation.clone().unwrap_or_else(|| String::from("continue")).parse()?;
c4d8542e 140
e062ebbc 141 let retention = config.retention.clone().unwrap_or_else(|| String::from("keep")).parse()?;
c4d8542e 142
8a0046f5
DM
143 let encrypt_fingerprint = match config.encrypt {
144 Some(ref fingerprint) => Some(fingerprint.parse()?),
145 None => None,
146 };
147
148 MediaPool::new(
149 &config.name,
150 state_path,
151 allocation,
152 retention,
cdf39e62 153 changer_name,
8a0046f5 154 encrypt_fingerprint,
30316192 155 no_media_set_locking,
8a0046f5 156 )
c4d8542e
DM
157 }
158
159 /// Returns the pool name
160 pub fn name(&self) -> &str {
161 &self.name
162 }
163
d1d74c43 164 /// Returns encryption settings
8a0046f5
DM
165 pub fn encrypt_fingerprint(&self) -> Option<Fingerprint> {
166 self.encrypt_fingerprint.clone()
167 }
168
25350f33
DM
169 pub fn set_media_status_damaged(&mut self, uuid: &Uuid) -> Result<(), Error> {
170 self.inventory.set_media_status_damaged(uuid)
171 }
8a0046f5 172
c4d8542e
DM
173 fn compute_media_state(&self, media_id: &MediaId) -> (MediaStatus, MediaLocation) {
174
cfae8f06 175 let (status, location) = self.inventory.status_and_location(&media_id.label.uuid);
c4d8542e
DM
176
177 match status {
178 MediaStatus::Full | MediaStatus::Damaged | MediaStatus::Retired => {
179 return (status, location);
180 }
181 MediaStatus::Unknown | MediaStatus::Writable => {
182 /* possibly writable - fall through to check */
183 }
184 }
185
186 let set = match media_id.media_set_label {
187 None => return (MediaStatus::Writable, location), // not assigned to any pool
188 Some(ref set) => set,
189 };
190
191 if set.pool != self.name { // should never trigger
192 return (MediaStatus::Unknown, location); // belong to another pool
193 }
194 if set.uuid.as_ref() == [0u8;16] { // not assigned to any pool
195 return (MediaStatus::Writable, location);
196 }
197
198 if &set.uuid != self.current_media_set.uuid() {
199 return (MediaStatus::Full, location); // assume FULL
200 }
201
202 // media is member of current set
203 if self.current_media_set.is_last_media(&media_id.label.uuid) {
204 (MediaStatus::Writable, location) // last set member is writable
205 } else {
206 (MediaStatus::Full, location)
207 }
208 }
209
210 /// Returns the 'MediaId' with associated state
211 pub fn lookup_media(&self, uuid: &Uuid) -> Result<BackupMedia, Error> {
212 let media_id = match self.inventory.lookup_media(uuid) {
213 None => bail!("unable to lookup media {}", uuid),
214 Some(media_id) => media_id.clone(),
215 };
216
217 if let Some(ref set) = media_id.media_set_label {
218 if set.pool != self.name {
219 bail!("media does not belong to pool ({} != {})", set.pool, self.name);
220 }
221 }
222
223 let (status, location) = self.compute_media_state(&media_id);
224
225 Ok(BackupMedia::with_media_id(
226 media_id,
227 location,
228 status,
229 ))
230 }
231
232 /// List all media associated with this pool
233 pub fn list_media(&self) -> Vec<BackupMedia> {
234 let media_id_list = self.inventory.list_pool_media(&self.name);
235
236 media_id_list.into_iter()
237 .map(|media_id| {
238 let (status, location) = self.compute_media_state(&media_id);
239 BackupMedia::with_media_id(
240 media_id,
241 location,
242 status,
243 )
244 })
245 .collect()
246 }
247
248 /// Set media status to FULL.
249 pub fn set_media_status_full(&mut self, uuid: &Uuid) -> Result<(), Error> {
250 let media = self.lookup_media(uuid)?; // check if media belongs to this pool
251 if media.status() != &MediaStatus::Full {
cfae8f06 252 self.inventory.set_media_status_full(uuid)?;
c4d8542e
DM
253 }
254 Ok(())
255 }
256
257 /// Make sure the current media set is usable for writing
258 ///
259 /// If not, starts a new media set. Also creates a new
e953029e 260 /// set if media_set_policy implies it, or if 'force' is true.
ab77d660
DM
261 ///
262 /// Note: We also call this in list_media to compute correct media
263 /// status, so this must not change persistent/saved state.
90e16be3
DM
264 ///
265 /// Returns the reason why we started a new media set (if we do)
30316192
DM
266 pub fn start_write_session(
267 &mut self,
268 current_time: i64,
e953029e 269 force: bool,
30316192
DM
270 ) -> Result<Option<String>, Error> {
271
272 let _pool_lock = if self.no_media_set_locking {
273 None
274 } else {
275 Some(lock_media_pool(&self.state_path, &self.name)?)
276 };
90e16be3 277
30316192
DM
278 self.inventory.reload()?;
279
e953029e
DC
280 let mut create_new_set = if force {
281 Some(String::from("forced"))
282 } else {
283 match self.current_set_usable() {
284 Err(err) => {
285 Some(err.to_string())
286 }
287 Ok(_) => None,
288 }
c4d8542e
DM
289 };
290
90e16be3 291 if create_new_set.is_none() {
c4d8542e
DM
292 match &self.media_set_policy {
293 MediaSetPolicy::AlwaysCreate => {
90e16be3 294 create_new_set = Some(String::from("policy is AlwaysCreate"));
c4d8542e
DM
295 }
296 MediaSetPolicy::CreateAt(event) => {
297 if let Some(set_start_time) = self.inventory.media_set_start_time(&self.current_media_set.uuid()) {
298 if let Ok(Some(alloc_time)) = compute_next_event(event, set_start_time as i64, false) {
90e16be3
DM
299 if current_time >= alloc_time {
300 create_new_set = Some(String::from("policy CreateAt event triggered"));
c4d8542e
DM
301 }
302 }
303 }
304 }
305 MediaSetPolicy::ContinueCurrent => { /* do nothing here */ }
306 }
307 }
308
90e16be3 309 if create_new_set.is_some() {
c4d8542e 310 let media_set = MediaSet::new();
30316192
DM
311
312 let current_media_set_lock = if self.no_media_set_locking {
313 None
314 } else {
315 Some(lock_media_set(&self.state_path, media_set.uuid(), None)?)
316 };
317
318 self.current_media_set_lock = current_media_set_lock;
c4d8542e
DM
319 self.current_media_set = media_set;
320 }
321
90e16be3 322 Ok(create_new_set)
c4d8542e
DM
323 }
324
325 /// List media in current media set
326 pub fn current_media_list(&self) -> Result<Vec<&Uuid>, Error> {
327 let mut list = Vec::new();
328 for opt_uuid in self.current_media_set.media_list().iter() {
329 match opt_uuid {
330 Some(ref uuid) => list.push(uuid),
331 None => bail!("current_media_list failed - media set is incomplete"),
332 }
333 }
334 Ok(list)
335 }
336
d1d74c43 337 // tests if the media data is considered as expired at specified time
c4d8542e
DM
338 pub fn media_is_expired(&self, media: &BackupMedia, current_time: i64) -> bool {
339 if media.status() != &MediaStatus::Full {
340 return false;
341 }
342
343 let expire_time = self.inventory.media_expire_time(
344 media.id(), &self.media_set_policy, &self.retention);
345
1bed3aed 346 current_time >= expire_time
c4d8542e
DM
347 }
348
8a0046f5
DM
349 // check if a location is considered on site
350 pub fn location_is_available(&self, location: &MediaLocation) -> bool {
351 match location {
cdf39e62 352 MediaLocation::Online(name) => {
ab77d660
DM
353 if self.force_media_availability {
354 true
cdf39e62 355 } else {
ab77d660
DM
356 if let Some(ref changer_name) = self.changer_name {
357 name == changer_name
358 } else {
359 // a standalone drive cannot use media currently inside a library
360 false
361 }
cdf39e62
DM
362 }
363 }
364 MediaLocation::Offline => {
ab77d660
DM
365 if self.force_media_availability {
366 true
367 } else {
368 // consider available for standalone drives
369 self.changer_name.is_none()
370 }
cdf39e62 371 }
8a0046f5
DM
372 MediaLocation::Vault(_) => false,
373 }
374 }
375
376 fn add_media_to_current_set(&mut self, mut media_id: MediaId, current_time: i64) -> Result<(), Error> {
377
30316192
DM
378 if self.current_media_set_lock.is_none() {
379 bail!("add_media_to_current_set: media set is not locked - internal error");
380 }
381
8a0046f5
DM
382 let seq_nr = self.current_media_set.media_list().len() as u64;
383
384 let pool = self.name.clone();
385
386 let encrypt_fingerprint = self.encrypt_fingerprint();
387
388 let set = MediaSetLabel::with_data(
389 &pool,
390 self.current_media_set.uuid().clone(),
391 seq_nr,
392 current_time,
393 encrypt_fingerprint,
394 );
395
396 media_id.media_set_label = Some(set);
397
398 let uuid = media_id.label.uuid.clone();
399
f281b8d3 400 MediaCatalog::overwrite(&self.state_path, &media_id, false)?; // overwite catalog
8a0046f5
DM
401 let clear_media_status = true; // remove Full status
402 self.inventory.store(media_id, clear_media_status)?; // store persistently
403
404 self.current_media_set.add_media(uuid);
405
406 Ok(())
407 }
408
409
c4d8542e
DM
410 /// Allocates a writable media to the current media set
411 pub fn alloc_writable_media(&mut self, current_time: i64) -> Result<Uuid, Error> {
412
30316192
DM
413 if self.current_media_set_lock.is_none() {
414 bail!("alloc_writable_media: media set is not locked - internal error");
415 }
416
c4d8542e
DM
417 let last_is_writable = self.current_set_usable()?;
418
c4d8542e
DM
419 if last_is_writable {
420 let last_uuid = self.current_media_set.last_media_uuid().unwrap();
421 let media = self.lookup_media(last_uuid)?;
422 return Ok(media.uuid().clone());
423 }
424
425 // try to find empty media in pool, add to media set
426
30316192
DM
427 { // limit pool lock scope
428 let _pool_lock = lock_media_pool(&self.state_path, &self.name)?;
c4d8542e 429
30316192 430 self.inventory.reload()?;
8a0046f5 431
30316192
DM
432 let media_list = self.list_media();
433
434 let mut empty_media = Vec::new();
435 let mut used_media = Vec::new();
436
437 for media in media_list.into_iter() {
438 if !self.location_is_available(media.location()) {
439 continue;
440 }
441 // already part of a media set?
442 if media.media_set_label().is_some() {
443 used_media.push(media);
444 } else {
445 // only consider writable empty media
446 if media.status() == &MediaStatus::Writable {
447 empty_media.push(media);
448 }
8a0046f5 449 }
c4d8542e 450 }
c4d8542e 451
30316192
DM
452 // sort empty_media, newest first -> oldest last
453 empty_media.sort_unstable_by(|a, b| {
454 let mut res = b.label().ctime.cmp(&a.label().ctime);
455 if res == std::cmp::Ordering::Equal {
456 res = b.label().label_text.cmp(&a.label().label_text);
457 }
458 res
459 });
460
461 if let Some(media) = empty_media.pop() {
462 // found empty media, add to media set an use it
463 let uuid = media.uuid().clone();
464 self.add_media_to_current_set(media.into_id(), current_time)?;
465 return Ok(uuid);
af762341 466 }
c4d8542e 467
30316192 468 println!("no empty media in pool, try to reuse expired media");
c4d8542e 469
30316192 470 let mut expired_media = Vec::new();
c4d8542e 471
30316192
DM
472 for media in used_media.into_iter() {
473 if let Some(set) = media.media_set_label() {
474 if &set.uuid == self.current_media_set.uuid() {
475 continue;
476 }
477 } else {
c4d8542e
DM
478 continue;
479 }
8a0046f5 480
30316192
DM
481 if self.media_is_expired(&media, current_time) {
482 println!("found expired media on media '{}'", media.label_text());
483 expired_media.push(media);
484 }
c4d8542e 485 }
c4d8542e 486
30316192
DM
487 // sort expired_media, newest first -> oldest last
488 expired_media.sort_unstable_by(|a, b| {
489 let mut res = b.media_set_label().unwrap().ctime.cmp(&a.media_set_label().unwrap().ctime);
490 if res == std::cmp::Ordering::Equal {
491 res = b.label().label_text.cmp(&a.label().label_text);
492 }
493 res
494 });
495
496 while let Some(media) = expired_media.pop() {
497 // check if we can modify the media-set (i.e. skip
498 // media used by a restore job)
499 if let Ok(_media_set_lock) = lock_media_set(
500 &self.state_path,
501 &media.media_set_label().unwrap().uuid,
502 Some(std::time::Duration::new(0, 0)), // do not wait
503 ) {
504 println!("reuse expired media '{}'", media.label_text());
505 let uuid = media.uuid().clone();
506 self.add_media_to_current_set(media.into_id(), current_time)?;
507 return Ok(uuid);
508 }
af762341 509 }
25e464c5 510 }
c4d8542e 511
25e464c5 512 println!("no expired media in pool, try to find unassigned/free media");
c4d8542e 513
25e464c5 514 // try unassigned media
30316192 515 let _lock = lock_unassigned_media_pool(&self.state_path)?;
1057b1f5
DM
516
517 self.inventory.reload()?;
518
25e464c5
DM
519 let mut free_media = Vec::new();
520
521 for media_id in self.inventory.list_unassigned_media() {
522
523 let (status, location) = self.compute_media_state(&media_id);
524 if media_id.media_set_label.is_some() { continue; } // should not happen
525
8a0046f5
DM
526 if !self.location_is_available(&location) {
527 continue;
c4d8542e 528 }
25e464c5
DM
529
530 // only consider writable media
531 if status != MediaStatus::Writable { continue; }
532
533 free_media.push(media_id);
534 }
535
af762341
DM
536 // sort free_media, newest first -> oldest last
537 free_media.sort_unstable_by(|a, b| {
538 let mut res = b.label.ctime.cmp(&a.label.ctime);
539 if res == std::cmp::Ordering::Equal {
540 res = b.label.label_text.cmp(&a.label.label_text);
541 }
542 res
543 });
544
8a0046f5
DM
545 if let Some(media_id) = free_media.pop() {
546 println!("use free media '{}'", media_id.label.label_text);
547 let uuid = media_id.label.uuid.clone();
548 self.add_media_to_current_set(media_id, current_time)?;
549 return Ok(uuid);
c4d8542e 550 }
25e464c5 551
25e464c5 552 bail!("alloc writable media in pool '{}' failed: no usable media found", self.name());
c4d8542e
DM
553 }
554
555 /// check if the current media set is usable for writing
556 ///
557 /// This does several consistency checks, and return if
558 /// the last media in the current set is in writable state.
559 ///
560 /// This return error when the media set must not be used any
561 /// longer because of consistency errors.
562 pub fn current_set_usable(&self) -> Result<bool, Error> {
563
6dd05135
DM
564 let media_list = self.current_media_set.media_list();
565
566 let media_count = media_list.len();
c4d8542e
DM
567 if media_count == 0 {
568 return Ok(false);
569 }
570
571 let set_uuid = self.current_media_set.uuid();
572 let mut last_is_writable = false;
573
6dd05135
DM
574 let mut last_enc: Option<Option<Fingerprint>> = None;
575
576 for (seq, opt_uuid) in media_list.iter().enumerate() {
c4d8542e
DM
577 let uuid = match opt_uuid {
578 None => bail!("media set is incomplete (missing media information)"),
579 Some(uuid) => uuid,
580 };
581 let media = self.lookup_media(uuid)?;
582 match media.media_set_label() {
583 Some(MediaSetLabel { seq_nr, uuid, ..}) if *seq_nr == seq as u64 && uuid == set_uuid => { /* OK */ },
584 Some(MediaSetLabel { seq_nr, uuid, ..}) if uuid == set_uuid => {
585 bail!("media sequence error ({} != {})", *seq_nr, seq);
586 },
587 Some(MediaSetLabel { uuid, ..}) => bail!("media owner error ({} != {}", uuid, set_uuid),
588 None => bail!("media owner error (no owner)"),
589 }
6dd05135
DM
590
591 if let Some(set) = media.media_set_label() { // always true here
592 if set.encryption_key_fingerprint != self.encrypt_fingerprint {
593 bail!("pool encryption key changed");
594 }
595 match last_enc {
596 None => {
597 last_enc = Some(set.encryption_key_fingerprint.clone());
598 }
599 Some(ref last_enc) => {
600 if last_enc != &set.encryption_key_fingerprint {
601 bail!("inconsistent media encryption key");
602 }
603 }
604 }
605 }
606
c4d8542e
DM
607 match media.status() {
608 MediaStatus::Full => { /* OK */ },
609 MediaStatus::Writable if (seq + 1) == media_count => {
b81e37f6
DM
610 let media_location = media.location();
611 if self.location_is_available(media_location) {
612 last_is_writable = true;
613 } else {
614 if let MediaLocation::Vault(vault) = media_location {
c4d8542e
DM
615 bail!("writable media offsite in vault '{}'", vault);
616 }
617 }
618 },
619 _ => bail!("unable to use media set - wrong media status {:?}", media.status()),
620 }
621 }
ab77d660 622
c4d8542e
DM
623 Ok(last_is_writable)
624 }
625
626 /// Generate a human readable name for the media set
627 pub fn generate_media_set_name(
628 &self,
629 media_set_uuid: &Uuid,
630 template: Option<String>,
631 ) -> Result<String, Error> {
632 self.inventory.generate_media_set_name(media_set_uuid, template)
633 }
634
c4d8542e
DM
635}
636
637/// Backup media
638///
639/// Combines 'MediaId' with 'MediaLocation' and 'MediaStatus'
640/// information.
641#[derive(Debug,Serialize,Deserialize,Clone)]
642pub struct BackupMedia {
643 /// Media ID
644 id: MediaId,
645 /// Media location
646 location: MediaLocation,
647 /// Media status
648 status: MediaStatus,
649}
650
651impl BackupMedia {
652
653 /// Creates a new instance
654 pub fn with_media_id(
655 id: MediaId,
656 location: MediaLocation,
657 status: MediaStatus,
658 ) -> Self {
659 Self { id, location, status }
660 }
661
662 /// Returns the media location
663 pub fn location(&self) -> &MediaLocation {
664 &self.location
665 }
666
667 /// Returns the media status
668 pub fn status(&self) -> &MediaStatus {
669 &self.status
670 }
671
672 /// Returns the media uuid
673 pub fn uuid(&self) -> &Uuid {
674 &self.id.label.uuid
675 }
676
677 /// Returns the media set label
6543214d
DM
678 pub fn media_set_label(&self) -> Option<&MediaSetLabel> {
679 self.id.media_set_label.as_ref()
680 }
681
682 /// Returns the media creation time
683 pub fn ctime(&self) -> i64 {
684 self.id.label.ctime
c4d8542e
DM
685 }
686
687 /// Updates the media set label
688 pub fn set_media_set_label(&mut self, set_label: MediaSetLabel) {
689 self.id.media_set_label = Some(set_label);
690 }
54f4ecd4 691
c4d8542e 692 /// Returns the drive label
a78348ac 693 pub fn label(&self) -> &MediaLabel {
c4d8542e
DM
694 &self.id.label
695 }
696
697 /// Returns the media id (drive label + media set label)
698 pub fn id(&self) -> &MediaId {
699 &self.id
700 }
701
8a0046f5
DM
702 /// Returns the media id, consumes self)
703 pub fn into_id(self) -> MediaId {
704 self.id
705 }
706
c4d8542e 707 /// Returns the media label (Barcode)
8446fbca
DM
708 pub fn label_text(&self) -> &str {
709 &self.id.label.label_text
c4d8542e
DM
710 }
711}