1use std::collections::HashMap;
8#[cfg(test)]
9use std::collections::HashSet;
10
11use chrono::{DateTime, Duration, Utc};
12use rand::{rng, seq::IteratorRandom};
13
14use devicemapper::{Bytes, Device, Sectors};
15
16use crate::{
17 engine::{
18 shared::gather_encryption_info,
19 strat_engine::{
20 backstore::{
21 blockdev::{v1, v2, InternalBlockDev},
22 devices::{
23 initialize_devices, initialize_devices_legacy, wipe_blockdevs, UnownedDevices,
24 },
25 shared::{BlkDevSegment, Segment},
26 },
27 crypt::handle::v1::CryptHandle,
28 metadata::{MDADataSize, BDA},
29 serde_structs::{BaseBlockDevSave, Recordable},
30 shared::bds_to_bdas,
31 },
32 types::{
33 DevUuid, InputEncryptionInfo, Name, PoolEncryptionInfo, PoolUuid,
34 ValidatedIntegritySpec,
35 },
36 },
37 stratis::{StratisError, StratisResult},
38};
39
40const MAX_NUM_TO_WRITE: usize = 10;
41
42#[derive(Debug)]
43struct TimeStamp {
44 inner: Option<DateTime<Utc>>,
45}
46
47impl From<Option<DateTime<Utc>>> for TimeStamp {
48 fn from(time: Option<DateTime<Utc>>) -> Self {
49 TimeStamp { inner: time }
50 }
51}
52
53impl TimeStamp {
54 fn next(&self) -> DateTime<Utc> {
57 let current_time = Utc::now();
58 if Some(current_time) <= self.inner {
59 self.inner
60 .expect("self.inner >= Some(current_time")
61 .checked_add_signed(Duration::nanoseconds(1))
62 .expect("self.inner << maximum representable DateTime")
63 } else {
64 current_time
65 }
66 }
67
68 fn set(&mut self, time: DateTime<Utc>) {
70 self.inner = Some(time);
71 }
72}
73
74#[derive(Debug)]
75pub struct BlockDevMgr<B> {
76 block_devs: Vec<B>,
78 last_update_time: TimeStamp,
81}
82
83impl BlockDevMgr<v1::StratBlockDev> {
84 pub fn initialize(
86 pool_name: Name,
87 pool_uuid: PoolUuid,
88 devices: UnownedDevices,
89 mda_data_size: MDADataSize,
90 encryption_info: Option<&InputEncryptionInfo>,
91 sector_size: Option<u32>,
92 ) -> StratisResult<BlockDevMgr<v1::StratBlockDev>> {
93 Ok(BlockDevMgr::new(
94 initialize_devices_legacy(
95 devices,
96 pool_name,
97 pool_uuid,
98 mda_data_size,
99 encryption_info,
100 sector_size,
101 )?,
102 None,
103 ))
104 }
105
106 pub fn add(
110 &mut self,
111 pool_name: Name,
112 pool_uuid: PoolUuid,
113 devices: UnownedDevices,
114 sector_size: Option<u32>,
115 ) -> StratisResult<Vec<DevUuid>> {
116 let this_pool_uuid = self.block_devs.first().map(|bd| bd.pool_uuid());
117 if this_pool_uuid.is_some() && this_pool_uuid != Some(pool_uuid) {
118 return Err(StratisError::Msg(
119 format!("block devices being managed have pool UUID {} but new devices are to be added with pool UUID {}",
120 this_pool_uuid.expect("guarded by if-expression"),
121 pool_uuid)
122 ));
123 }
124
125 let encryption_info = pool_enc_to_enc!(self.encryption_info());
126 if let Some(ref ei) = encryption_info {
127 if !CryptHandle::can_unlock(
128 self.block_devs
129 .first()
130 .expect("Must have at least one blockdev")
131 .physical_path(),
132 ei.single_key_description().is_some(),
133 ei.single_clevis_info().is_some(),
134 ) {
135 return Err(StratisError::Msg(
136 "Either the key in the kernel keyring, Clevis, or both could not be used to perform encryption operations on the devices in the pool; check that the appropriate key in the keyring is set and that the Clevis key storage method is available depending on your provided unlock methods".to_string(),
137 ));
138 }
139 }
140
141 let bds = initialize_devices_legacy(
146 devices,
147 pool_name,
148 pool_uuid,
149 MDADataSize::default(),
150 encryption_info.map(InputEncryptionInfo::from).as_ref(),
151 sector_size,
152 )?;
153 let bdev_uuids = bds.iter().map(|bd| bd.uuid()).collect();
154 self.block_devs.extend(bds);
155 Ok(bdev_uuids)
156 }
157
158 pub fn encryption_info(&self) -> Option<PoolEncryptionInfo> {
160 gather_encryption_info(
161 self.block_devs.len(),
162 self.block_devs.iter().map(|bd| bd.encryption_info()),
163 )
164 .expect("Cannot create a pool out of both encrypted and unencrypted devices")
165 }
166
167 pub fn is_encrypted(&self) -> bool {
168 self.encryption_info().is_some()
169 }
170
171 pub fn grow(&mut self, dev: DevUuid) -> StratisResult<bool> {
172 let bd = self
173 .block_devs
174 .iter_mut()
175 .find(|bd| bd.uuid() == dev)
176 .ok_or_else(|| StratisError::Msg(format!("Block device with UUID {dev} not found")))?;
177 bd.grow()
178 }
179
180 #[cfg(test)]
181 fn invariant(&self) {
182 let pool_uuids = self
183 .block_devs
184 .iter()
185 .map(|bd| bd.pool_uuid())
186 .collect::<HashSet<_>>();
187 assert!(pool_uuids.len() == 1);
188
189 let encryption_infos = self
190 .block_devs
191 .iter()
192 .filter_map(|bd| bd.encryption_info())
193 .collect::<Vec<_>>();
194 if encryption_infos.is_empty() {
195 assert_eq!(self.encryption_info(), None);
196 } else {
197 assert_eq!(encryption_infos.len(), self.block_devs.len());
198
199 let info_set = encryption_infos.iter().collect::<HashSet<_>>();
200 assert!(info_set.len() == 1);
201 }
202
203 for bd in self.block_devs.iter() {
204 bd.invariant();
205 }
206 }
207}
208
209impl BlockDevMgr<v2::StratBlockDev> {
210 pub fn initialize(
212 pool_uuid: PoolUuid,
213 devices: UnownedDevices,
214 mda_data_size: MDADataSize,
215 ) -> StratisResult<BlockDevMgr<v2::StratBlockDev>> {
216 Ok(BlockDevMgr::new(
217 initialize_devices(devices, pool_uuid, mda_data_size)?,
218 None,
219 ))
220 }
221
222 pub fn add(
226 &mut self,
227 pool_uuid: PoolUuid,
228 devices: UnownedDevices,
229 ) -> StratisResult<Vec<DevUuid>> {
230 let this_pool_uuid = self.block_devs.first().map(|bd| bd.pool_uuid());
231 if this_pool_uuid.is_some() && this_pool_uuid != Some(pool_uuid) {
232 return Err(StratisError::Msg(
233 format!("block devices being managed have pool UUID {} but new devices are to be added with pool UUID {}",
234 this_pool_uuid.expect("guarded by if-expression"),
235 pool_uuid)
236 ));
237 }
238
239 let bds = initialize_devices(devices, pool_uuid, MDADataSize::default())?;
244 let bdev_uuids = bds.iter().map(|bd| bd.uuid()).collect();
245 self.block_devs.extend(bds);
246 Ok(bdev_uuids)
247 }
248
249 pub fn grow(
250 &mut self,
251 dev: DevUuid,
252 integrity_spec: ValidatedIntegritySpec,
253 ) -> StratisResult<bool> {
254 let bd = self
255 .block_devs
256 .iter_mut()
257 .find(|bd| bd.uuid() == dev)
258 .ok_or_else(|| StratisError::Msg(format!("Block device with UUID {dev} not found")))?;
259 bd.grow(integrity_spec)
260 }
261
262 #[cfg(test)]
263 fn invariant(&self) {
264 let pool_uuids = self
265 .block_devs
266 .iter()
267 .map(|bd| bd.pool_uuid())
268 .collect::<HashSet<_>>();
269 assert!(pool_uuids.len() == 1);
270
271 for bd in self.block_devs.iter() {
272 bd.invariant();
273 }
274 }
275}
276
277impl<B> BlockDevMgr<B>
278where
279 B: InternalBlockDev,
280{
281 pub fn new(block_devs: Vec<B>, last_update_time: Option<DateTime<Utc>>) -> BlockDevMgr<B> {
283 BlockDevMgr {
284 block_devs,
285 last_update_time: last_update_time.into(),
286 }
287 }
288
289 pub fn into_bdas(self) -> HashMap<DevUuid, BDA> {
291 bds_to_bdas(self.block_devs)
292 }
293
294 pub fn uuid_to_devno(&self) -> HashMap<DevUuid, Device> {
296 self.block_devs
297 .iter()
298 .map(|bd| (bd.uuid(), *bd.device()))
299 .collect()
300 }
301
302 pub fn destroy_all(&mut self) -> StratisResult<()> {
303 wipe_blockdevs(&mut self.block_devs)
304 }
305
306 pub fn drain_bds(&mut self) -> Vec<B> {
308 self.block_devs.drain(..).collect::<Vec<_>>()
309 }
310
311 pub fn blockdevs(&self) -> Vec<(DevUuid, &B)> {
313 self.block_devs.iter().map(|bd| (bd.uuid(), bd)).collect()
314 }
315
316 pub fn blockdevs_mut(&mut self) -> Vec<(DevUuid, &mut B)> {
317 self.block_devs
318 .iter_mut()
319 .map(|bd| (bd.uuid(), bd))
320 .collect()
321 }
322
323 pub fn get_blockdev_by_uuid(&self, uuid: DevUuid) -> Option<&B> {
324 self.block_devs.iter().find(|bd| bd.uuid() == uuid)
325 }
326
327 pub fn get_mut_blockdev_by_uuid(&mut self, uuid: DevUuid) -> Option<&mut B> {
328 self.block_devs.iter_mut().find(|bd| bd.uuid() == uuid)
329 }
330
331 pub(super) fn remove_blockdevs(&mut self, uuids: &[DevUuid]) -> StratisResult<()> {
345 let mut removed = Vec::new();
346 for uuid in uuids {
347 let mut found = false;
348 let blockdevs_last_index = self.block_devs.len() - 1;
349 for i in 0..blockdevs_last_index {
350 let index = blockdevs_last_index - i;
351 if self.block_devs[index].uuid() == *uuid {
352 removed.push(self.block_devs.swap_remove(index));
353 found = true;
354 break;
355 }
356 }
357 if !found {
358 return Err(StratisError::Msg(format!(
359 "Blockdev corresponding to UUID: {uuid} not found."
360 )));
361 }
362 }
363 wipe_blockdevs(&mut removed)?;
364 Ok(())
365 }
366
367 pub fn alloc(&mut self, sizes: &[Sectors]) -> Option<Vec<Vec<BlkDevSegment>>> {
373 let total_needed: Sectors = sizes.iter().cloned().sum();
374 if self.avail_space() < total_needed {
375 return None;
376 }
377
378 let mut lists = Vec::new();
379
380 for &needed in sizes.iter() {
381 let mut alloc = Sectors(0);
382 let mut segs = Vec::new();
383 for bd in &mut self.block_devs {
391 if alloc == needed {
392 break;
393 }
394
395 let r_segs = bd.alloc(needed - alloc);
396 let blkdev_segs = r_segs.iter().map(|(&start, &length)| {
397 BlkDevSegment::new(bd.uuid(), Segment::new(*bd.device(), start, length))
398 });
399 segs.extend(blkdev_segs);
400 alloc += r_segs.sum();
401 }
402 assert_eq!(alloc, needed);
403 lists.push(segs);
404 }
405
406 Some(lists)
407 }
408
409 pub fn save_state(&mut self, metadata: &[u8]) -> StratisResult<()> {
417 let stamp_time = self.last_update_time.next();
418
419 let data_size = Bytes::from(metadata.len());
420 let candidates = self
421 .block_devs
422 .iter_mut()
423 .filter(|b| b.max_stratis_metadata_size().bytes() >= data_size);
424
425 debug!("Writing {data_size} of pool level metadata to devices in pool");
426
427 let saved = candidates
430 .choose_multiple(&mut rng(), MAX_NUM_TO_WRITE)
431 .iter_mut()
432 .fold(false, |acc, b| {
433 acc | b.save_state(&stamp_time, metadata).is_ok()
434 });
435
436 if saved {
437 self.last_update_time.set(stamp_time);
438 Ok(())
439 } else {
440 let err_msg = "Failed to save metadata to even one device in pool";
441 Err(StratisError::Msg(err_msg.into()))
442 }
443 }
444
445 pub fn load_state(&self) -> StratisResult<Vec<u8>> {
449 let (mut last_updated, mut result) = (None, None);
450 for metadata in self.block_devs.iter().map(|dev| dev.load_state()) {
451 match metadata {
452 Ok(Some((metadata, updated))) => {
453 if Some(updated) > last_updated {
454 (last_updated, result) = (Some(updated), Some(metadata));
455 }
456 }
457 Ok(None) => {}
458 Err(err) => return Err(err),
459 }
460 }
461
462 result.ok_or_else(|| {
463 StratisError::Msg(
464 "No pool-level metadata could be obtained for the specified pool".into(),
465 )
466 })
467 }
468
469 pub fn avail_space(&self) -> Sectors {
473 self.block_devs.iter().map(|bd| bd.available()).sum()
474 }
475
476 pub fn size(&self) -> Sectors {
480 self.block_devs
481 .iter()
482 .map(|b| b.total_size().sectors())
483 .sum()
484 }
485
486 pub fn metadata_size(&self) -> Sectors {
489 self.block_devs.iter().map(|bd| bd.metadata_size()).sum()
490 }
491
492 pub fn teardown(&mut self) -> StratisResult<()> {
494 let errs = self.block_devs.iter_mut().fold(Vec::new(), |mut errs, bd| {
495 if let Err(e) = bd.teardown() {
496 errs.push(e);
497 }
498 errs
499 });
500
501 if errs.is_empty() {
502 Ok(())
503 } else {
504 Err(StratisError::BestEffortError("Failed to remove devicemapper devices for some or all physical devices in the pool".to_string(), errs))
505 }
506 }
507}
508
509impl<B> Recordable<Vec<BaseBlockDevSave>> for BlockDevMgr<B>
510where
511 B: Recordable<BaseBlockDevSave>,
512{
513 fn record(&self) -> Vec<BaseBlockDevSave> {
514 self.block_devs.iter().map(|bd| bd.record()).collect()
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use std::path::Path;
521
522 use crate::engine::{
523 strat_engine::{
524 backstore::{
525 blockdev,
526 devices::{ProcessedPathInfos, UnownedDevices},
527 },
528 cmd,
529 tests::{crypt, loopbacked, real},
530 },
531 types::KeyDescription,
532 };
533
534 use super::*;
535
536 fn get_devices(paths: &[&Path]) -> StratisResult<UnownedDevices> {
537 ProcessedPathInfos::try_from(paths)
538 .map(|ps| ps.unpack())
539 .and_then(|(sds, uds)| sds.error_on_not_empty().map(|_| uds))
540 }
541
542 mod v1 {
543 use super::*;
544
545 fn test_blockdevmgr_used(paths: &[&Path]) {
550 let pool_uuid = PoolUuid::new_v4();
551 let pool_name = Name::new("pool_name".to_string());
552 let devices = get_devices(paths).unwrap();
553 let mut mgr = BlockDevMgr::<blockdev::v1::StratBlockDev>::initialize(
554 pool_name,
555 pool_uuid,
556 devices,
557 MDADataSize::default(),
558 None,
559 None,
560 )
561 .unwrap();
562 assert_eq!(mgr.avail_space() + mgr.metadata_size(), mgr.size());
563
564 let allocated = Sectors(2);
565 mgr.alloc(&[allocated]).unwrap();
566 assert_eq!(
567 mgr.avail_space() + allocated + mgr.metadata_size(),
568 mgr.size()
569 );
570 }
571
572 #[test]
573 fn loop_test_blockdevmgr_used() {
574 loopbacked::test_with_spec(
575 &loopbacked::DeviceLimits::Range(1, 3, None),
576 test_blockdevmgr_used,
577 );
578 }
579
580 #[test]
581 fn real_test_blockdevmgr_used() {
582 real::test_with_spec(
583 &real::DeviceLimits::AtLeast(1, None, None),
584 test_blockdevmgr_used,
585 );
586 }
587
588 fn test_blockdevmgr_same_key(paths: &[&Path]) {
591 fn test_with_key(paths: &[&Path], key_desc: &KeyDescription) {
592 let pool_uuid = PoolUuid::new_v4();
593
594 let devices1 = get_devices(&paths[..2]).unwrap();
595 let devices2 = get_devices(&paths[2..3]).unwrap();
596
597 let pool_name = Name::new("pool_name".to_string());
598 let mut bdm = BlockDevMgr::<blockdev::v1::StratBlockDev>::initialize(
599 pool_name.clone(),
600 pool_uuid,
601 devices1,
602 MDADataSize::default(),
603 InputEncryptionInfo::new_legacy(Some(key_desc.clone()), None).as_ref(),
604 None,
605 )
606 .unwrap();
607
608 if bdm.add(pool_name, pool_uuid, devices2, None).is_err() {
609 panic!(
610 "Adding a blockdev with the same key to an encrypted pool should succeed"
611 )
612 }
613 }
614
615 crypt::insert_and_cleanup_key(paths, test_with_key);
616 }
617
618 #[test]
619 fn loop_test_blockdevmgr_same_key() {
620 loopbacked::test_with_spec(
621 &loopbacked::DeviceLimits::Exactly(3, None),
622 test_blockdevmgr_same_key,
623 );
624 }
625
626 #[test]
627 fn real_test_blockdevmgr_same_key() {
628 real::test_with_spec(
629 &real::DeviceLimits::Exactly(3, None, None),
630 test_blockdevmgr_same_key,
631 );
632 }
633
634 fn test_blockdevmgr_changed_key(paths: &[&Path]) {
638 fn test_with_key(paths: &[&Path], key_desc: &KeyDescription) {
639 let pool_uuid = PoolUuid::new_v4();
640
641 let devices1 = get_devices(&paths[..2]).unwrap();
642 let devices2 = get_devices(&paths[2..3]).unwrap();
643
644 let pool_name = Name::new("pool_name".to_string());
645 let mut bdm = BlockDevMgr::<blockdev::v1::StratBlockDev>::initialize(
646 pool_name.clone(),
647 pool_uuid,
648 devices1,
649 MDADataSize::default(),
650 InputEncryptionInfo::new_legacy(Some(key_desc.clone()), None).as_ref(),
651 None,
652 )
653 .unwrap();
654
655 crypt::change_key(key_desc);
656
657 if bdm.add(pool_name, pool_uuid, devices2, None).is_ok() {
658 panic!("Adding a blockdev with a new key to an encrypted pool should fail")
659 }
660 }
661
662 crypt::insert_and_cleanup_key(paths, test_with_key);
663 }
664
665 #[test]
666 fn loop_test_blockdevmgr_changed_key() {
667 loopbacked::test_with_spec(
668 &loopbacked::DeviceLimits::Exactly(3, None),
669 test_blockdevmgr_changed_key,
670 );
671 }
672
673 #[test]
674 fn real_test_blockdevmgr_changed_key() {
675 real::test_with_spec(
676 &real::DeviceLimits::Exactly(3, None, None),
677 test_blockdevmgr_changed_key,
678 );
679 }
680
681 fn test_initialization_add_stratis(paths: &[&Path]) {
687 assert!(paths.len() > 1);
688 let (paths1, paths2) = paths.split_at(paths.len() / 2);
689
690 let uuid = PoolUuid::new_v4();
691 let uuid2 = PoolUuid::new_v4();
692 let pool_name1 = Name::new("pool_name1".to_string());
693 let pool_name2 = Name::new("pool_name2".to_string());
694
695 let bd_mgr = BlockDevMgr::<blockdev::v1::StratBlockDev>::initialize(
696 pool_name1,
697 uuid,
698 get_devices(paths1).unwrap(),
699 MDADataSize::default(),
700 None,
701 None,
702 )
703 .unwrap();
704 cmd::udev_settle().unwrap();
705
706 assert_matches!(get_devices(paths1), Err(_));
707
708 assert!(ProcessedPathInfos::try_from(paths1)
709 .unwrap()
710 .unpack()
711 .0
712 .partition(uuid2)
713 .0
714 .is_empty());
715
716 assert!(!ProcessedPathInfos::try_from(paths1)
717 .unwrap()
718 .unpack()
719 .0
720 .partition(uuid)
721 .0
722 .is_empty());
723
724 BlockDevMgr::<blockdev::v1::StratBlockDev>::initialize(
725 pool_name2,
726 uuid,
727 get_devices(paths2).unwrap(),
728 MDADataSize::default(),
729 None,
730 None,
731 )
732 .unwrap();
733
734 cmd::udev_settle().unwrap();
735
736 assert!(!ProcessedPathInfos::try_from(paths2)
737 .unwrap()
738 .unpack()
739 .0
740 .partition(uuid)
741 .0
742 .is_empty());
743
744 bd_mgr.invariant()
745 }
746
747 #[test]
748 fn loop_test_initialization_stratis() {
749 loopbacked::test_with_spec(
750 &loopbacked::DeviceLimits::Range(2, 3, None),
751 test_initialization_add_stratis,
752 );
753 }
754
755 #[test]
756 fn real_test_initialization_stratis() {
757 real::test_with_spec(
758 &real::DeviceLimits::AtLeast(2, None, None),
759 test_initialization_add_stratis,
760 );
761 }
762 }
763
764 mod v2 {
765 use super::*;
766
767 fn test_blockdevmgr_used(paths: &[&Path]) {
772 let pool_uuid = PoolUuid::new_v4();
773 let devices = get_devices(paths).unwrap();
774 let mut mgr = BlockDevMgr::<blockdev::v2::StratBlockDev>::initialize(
775 pool_uuid,
776 devices,
777 MDADataSize::default(),
778 )
779 .unwrap();
780 assert_eq!(mgr.avail_space() + mgr.metadata_size(), mgr.size());
781
782 let allocated = Sectors(2);
783 mgr.alloc(&[allocated]).unwrap();
784 assert_eq!(
785 mgr.avail_space() + allocated + mgr.metadata_size(),
786 mgr.size()
787 );
788 }
789
790 #[test]
791 fn loop_test_blockdevmgr_used() {
792 loopbacked::test_with_spec(
793 &loopbacked::DeviceLimits::Range(1, 3, None),
794 test_blockdevmgr_used,
795 );
796 }
797
798 #[test]
799 fn real_test_blockdevmgr_used() {
800 real::test_with_spec(
801 &real::DeviceLimits::AtLeast(1, None, None),
802 test_blockdevmgr_used,
803 );
804 }
805
806 fn test_initialization_add_stratis(paths: &[&Path]) {
812 assert!(paths.len() > 1);
813 let (paths1, paths2) = paths.split_at(paths.len() / 2);
814
815 let uuid = PoolUuid::new_v4();
816 let uuid2 = PoolUuid::new_v4();
817
818 let bd_mgr = BlockDevMgr::<blockdev::v2::StratBlockDev>::initialize(
819 uuid,
820 get_devices(paths1).unwrap(),
821 MDADataSize::default(),
822 )
823 .unwrap();
824 cmd::udev_settle().unwrap();
825
826 assert_matches!(get_devices(paths1), Err(_));
827
828 assert!(ProcessedPathInfos::try_from(paths1)
829 .unwrap()
830 .unpack()
831 .0
832 .partition(uuid2)
833 .0
834 .is_empty());
835
836 assert!(!ProcessedPathInfos::try_from(paths1)
837 .unwrap()
838 .unpack()
839 .0
840 .partition(uuid)
841 .0
842 .is_empty());
843
844 BlockDevMgr::<blockdev::v2::StratBlockDev>::initialize(
845 uuid,
846 get_devices(paths2).unwrap(),
847 MDADataSize::default(),
848 )
849 .unwrap();
850
851 cmd::udev_settle().unwrap();
852
853 assert!(!ProcessedPathInfos::try_from(paths2)
854 .unwrap()
855 .unpack()
856 .0
857 .partition(uuid)
858 .0
859 .is_empty());
860
861 bd_mgr.invariant()
862 }
863
864 #[test]
865 fn loop_test_initialization_stratis() {
866 loopbacked::test_with_spec(
867 &loopbacked::DeviceLimits::Range(2, 3, None),
868 test_initialization_add_stratis,
869 );
870 }
871
872 #[test]
873 fn real_test_initialization_stratis() {
874 real::test_with_spec(
875 &real::DeviceLimits::AtLeast(2, None, None),
876 test_initialization_add_stratis,
877 );
878 }
879 }
880}