nydus_service/
blob_cache.rs

1// Copyright (C) 2022 Alibaba Cloud. All rights reserved.
2//
3// SPDX-License-Identifier: (Apache-2.0 AND BSD-3-Clause)
4
5//! Blob cache manager to cache RAFS meta/data blob objects.
6
7use std::collections::HashMap;
8use std::fs::OpenOptions;
9use std::io::{Error, ErrorKind, Result};
10use std::os::fd::FromRawFd;
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicU32, Ordering};
13use std::sync::{Arc, Mutex, MutexGuard};
14
15use nydus_api::{
16    BlobCacheEntry, BlobCacheList, BlobCacheObjectId, ConfigV2, BLOB_CACHE_TYPE_DATA_BLOB,
17    BLOB_CACHE_TYPE_META_BLOB,
18};
19use nydus_rafs::metadata::layout::v6::{EROFS_BLOCK_BITS_12, EROFS_BLOCK_SIZE_4096};
20use nydus_rafs::metadata::{RafsBlobExtraInfo, RafsSuper, RafsSuperFlags};
21use nydus_storage::cache::BlobCache;
22use nydus_storage::device::BlobInfo;
23use nydus_storage::factory::BLOB_FACTORY;
24use tokio_uring::buf::IoBufMut;
25use tokio_uring::fs::File;
26
27const ID_SPLITTER: &str = "/";
28
29/// Generate keys for cached blob objects from domain identifiers and blob identifiers.
30pub fn generate_blob_key(domain_id: &str, blob_id: &str) -> String {
31    if domain_id.is_empty() {
32        blob_id.to_string()
33    } else {
34        format!("{}{}{}", domain_id, ID_SPLITTER, blob_id)
35    }
36}
37
38/// Configuration information for a cached metadata blob.
39pub struct MetaBlobConfig {
40    blob_id: String,
41    scoped_blob_id: String,
42    path: PathBuf,
43    config: Arc<ConfigV2>,
44    blobs: Mutex<Vec<Arc<DataBlobConfig>>>,
45    blob_extra_infos: HashMap<String, RafsBlobExtraInfo>,
46    is_tarfs_mode: bool,
47}
48
49impl MetaBlobConfig {
50    /// Get blob id.
51    pub fn blob_id(&self) -> &str {
52        &self.blob_id
53    }
54
55    /// Get file path to access the meta blob.
56    pub fn path(&self) -> &Path {
57        &self.path
58    }
59
60    /// Get the ['ConfigV2'] object associated with the cached data blob.
61    pub fn config_v2(&self) -> &Arc<ConfigV2> {
62        &self.config
63    }
64
65    pub fn get_blobs(&self) -> Vec<Arc<DataBlobConfig>> {
66        self.blobs.lock().unwrap().clone()
67    }
68
69    /// Get optional extra information associated with a blob object.
70    pub fn get_blob_extra_info(&self, blob_id: &str) -> Option<&RafsBlobExtraInfo> {
71        self.blob_extra_infos.get(blob_id)
72    }
73
74    /// Check whether the filesystem is in `TARFS` mode.
75    pub fn is_tarfs_mode(&self) -> bool {
76        self.is_tarfs_mode
77    }
78
79    fn add_data_blob(&self, blob: Arc<DataBlobConfig>) {
80        self.blobs.lock().unwrap().push(blob);
81    }
82}
83
84/// Configuration information for a cached data blob.
85pub struct DataBlobConfig {
86    scoped_blob_id: String,
87    blob_info: Arc<BlobInfo>,
88    config: Arc<ConfigV2>,
89    ref_count: AtomicU32,
90}
91
92impl DataBlobConfig {
93    /// Get the [`BlobInfo`](https://docs.rs/nydus-storage/latest/nydus_storage/device/struct.BlobInfo.html) object associated with the cached data blob.
94    pub fn blob_info(&self) -> &Arc<BlobInfo> {
95        &self.blob_info
96    }
97
98    /// Get the ['ConfigV2'] object associated with the cached data blob.
99    pub fn config_v2(&self) -> &Arc<ConfigV2> {
100        &self.config
101    }
102}
103
104/// Configuration information for a cached metadata/data blob.
105#[derive(Clone)]
106pub enum BlobConfig {
107    /// Configuration information for cached meta blob objects.
108    MetaBlob(Arc<MetaBlobConfig>),
109    /// Configuration information for cached data blob objects.
110    DataBlob(Arc<DataBlobConfig>),
111}
112
113impl BlobConfig {
114    /// Get the ['ConfigV2'] object associated with the cached data blob.
115    pub fn config_v2(&self) -> &Arc<ConfigV2> {
116        match self {
117            BlobConfig::MetaBlob(v) => v.config_v2(),
118            BlobConfig::DataBlob(v) => v.config_v2(),
119        }
120    }
121
122    fn new_data_blob(domain_id: String, blob_info: Arc<BlobInfo>, config: Arc<ConfigV2>) -> Self {
123        let scoped_blob_id = generate_blob_key(&domain_id, &blob_info.blob_id());
124
125        BlobConfig::DataBlob(Arc::new(DataBlobConfig {
126            blob_info,
127            scoped_blob_id,
128            config,
129            ref_count: AtomicU32::new(1),
130        }))
131    }
132
133    fn new_meta_blob(
134        domain_id: String,
135        blob_id: String,
136        path: PathBuf,
137        config: Arc<ConfigV2>,
138        blob_extra_infos: HashMap<String, RafsBlobExtraInfo>,
139        is_tarfs_mode: bool,
140    ) -> Self {
141        let scoped_blob_id = generate_blob_key(&domain_id, &blob_id);
142
143        BlobConfig::MetaBlob(Arc::new(MetaBlobConfig {
144            blob_id,
145            scoped_blob_id,
146            path,
147            config,
148            blobs: Mutex::new(Vec::new()),
149            blob_extra_infos,
150            is_tarfs_mode,
151        }))
152    }
153
154    fn key(&self) -> &str {
155        match self {
156            BlobConfig::MetaBlob(o) => &o.scoped_blob_id,
157            BlobConfig::DataBlob(o) => &o.scoped_blob_id,
158        }
159    }
160
161    fn meta_config(&self) -> Option<Arc<MetaBlobConfig>> {
162        match self {
163            BlobConfig::MetaBlob(o) => Some(o.clone()),
164            BlobConfig::DataBlob(_o) => None,
165        }
166    }
167}
168
169#[derive(Default)]
170struct BlobCacheState {
171    id_to_config_map: HashMap<String, BlobConfig>,
172}
173
174impl BlobCacheState {
175    fn new() -> Self {
176        Self::default()
177    }
178
179    fn try_add(&mut self, config: BlobConfig) -> Result<()> {
180        let key = config.key();
181
182        if let Some(entry) = self.id_to_config_map.get(key) {
183            match entry {
184                BlobConfig::MetaBlob(_o) => {
185                    // Meta blob must be unique.
186                    return Err(Error::new(
187                        ErrorKind::AlreadyExists,
188                        "blob_cache: bootstrap blob already exists",
189                    ));
190                }
191                BlobConfig::DataBlob(o) => {
192                    // Data blob is reference counted.
193                    o.ref_count.fetch_add(1, Ordering::AcqRel);
194                }
195            }
196        } else {
197            self.id_to_config_map.insert(key.to_owned(), config);
198        }
199
200        Ok(())
201    }
202
203    fn remove(&mut self, param: &BlobCacheObjectId) -> Result<()> {
204        if param.blob_id.is_empty() && !param.domain_id.is_empty() {
205            // Remove all blobs associated with the domain.
206            let scoped_blob_prefix = format!("{}{}", param.domain_id, ID_SPLITTER);
207            self.id_to_config_map.retain(|_k, v| match v {
208                BlobConfig::MetaBlob(o) => !o.scoped_blob_id.starts_with(&scoped_blob_prefix),
209                BlobConfig::DataBlob(o) => !o.scoped_blob_id.starts_with(&scoped_blob_prefix),
210            });
211        } else {
212            let mut data_blobs = Vec::new();
213            let mut is_meta = false;
214            let scoped_blob_prefix = generate_blob_key(&param.domain_id, &param.blob_id);
215
216            match self.id_to_config_map.get(&scoped_blob_prefix) {
217                None => return Err(enoent!("blob_cache: cache entry not found")),
218                Some(BlobConfig::MetaBlob(o)) => {
219                    is_meta = true;
220                    data_blobs = o.blobs.lock().unwrap().clone();
221                }
222                Some(BlobConfig::DataBlob(o)) => {
223                    data_blobs.push(o.clone());
224                }
225            }
226
227            for entry in data_blobs {
228                if entry.ref_count.fetch_sub(1, Ordering::AcqRel) == 1 {
229                    self.id_to_config_map.remove(&entry.scoped_blob_id);
230                }
231            }
232
233            if is_meta {
234                self.id_to_config_map.remove(&scoped_blob_prefix);
235            }
236        }
237
238        Ok(())
239    }
240
241    fn get(&self, key: &str) -> Option<BlobConfig> {
242        self.id_to_config_map.get(key).cloned()
243    }
244}
245
246/// Structure to manage and cache RAFS meta/data blob objects.
247#[derive(Default)]
248pub struct BlobCacheMgr {
249    state: Mutex<BlobCacheState>,
250}
251
252impl BlobCacheMgr {
253    /// Create a new instance of `BlobCacheMgr`.
254    pub fn new() -> Self {
255        BlobCacheMgr {
256            state: Mutex::new(BlobCacheState::new()),
257        }
258    }
259
260    /// Add a meta/data blob to be managed by the cache manager.
261    ///
262    /// When adding a RAFS meta blob to the cache manager, all data blobs referenced by the
263    /// bootstrap blob will also be added to the cache manager too. It may be used to add a RAFS
264    /// container image to the cache manager.
265    ///
266    /// Domains are used to control the blob sharing scope. All meta and data blobs associated
267    /// with the same domain will be shared/reused, but blobs associated with different domains are
268    /// isolated. The `domain_id` is used to identify the associated domain.
269    pub fn add_blob_entry(&self, entry: &BlobCacheEntry) -> Result<()> {
270        match entry.blob_type.as_str() {
271            BLOB_CACHE_TYPE_META_BLOB => {
272                let (path, config) = self.get_meta_info(entry)?;
273                self.add_meta_object(&entry.domain_id, &entry.blob_id, path, config)
274                    .inspect_err(|_e| {
275                        warn!(
276                            "blob_cache: failed to add cache entry for meta blob: {:?}",
277                            entry
278                        );
279                    })
280            }
281            BLOB_CACHE_TYPE_DATA_BLOB => Err(einval!(format!(
282                "blob_cache: invalid data blob cache entry: {:?}",
283                entry
284            ))),
285            _ => Err(einval!(format!(
286                "blob_cache: invalid blob cache entry, {:?}",
287                entry
288            ))),
289        }
290    }
291
292    /// Add a list of meta/data blobs to be cached by the cache manager.
293    ///
294    /// If failed to add some blob, the blobs already added won't be rolled back.
295    pub fn add_blob_list(&self, blobs: &BlobCacheList) -> Result<()> {
296        for entry in blobs.blobs.iter() {
297            self.add_blob_entry(entry)?;
298        }
299
300        Ok(())
301    }
302
303    /// Remove a meta/data blob object from the cache manager.
304    pub fn remove_blob_entry(&self, param: &BlobCacheObjectId) -> Result<()> {
305        self.get_state().remove(param)
306    }
307
308    /// Get configuration information of the cached blob with specified `key`.
309    pub fn get_config(&self, key: &str) -> Option<BlobConfig> {
310        self.get_state().get(key)
311    }
312
313    #[inline]
314    fn get_state(&self) -> MutexGuard<'_, BlobCacheState> {
315        self.state.lock().unwrap()
316    }
317
318    fn get_meta_info(&self, entry: &BlobCacheEntry) -> Result<(PathBuf, Arc<ConfigV2>)> {
319        let config = entry
320            .blob_config
321            .as_ref()
322            .ok_or_else(|| einval!("blob_cache: missing blob cache configuration information"))?;
323
324        if entry.blob_id.contains(ID_SPLITTER) {
325            return Err(einval!("blob_cache: `blob_id` for meta blob is invalid"));
326        } else if entry.domain_id.contains(ID_SPLITTER) {
327            return Err(einval!("blob_cache: `domain_id` for meta blob is invalid"));
328        }
329
330        let path = config.metadata_path.clone().unwrap_or_default();
331        if path.is_empty() {
332            return Err(einval!(
333                "blob_cache: `config.metadata_path` for meta blob is empty"
334            ));
335        }
336        let path = Path::new(&path).canonicalize().map_err(|_e| {
337            einval!(format!(
338                "blob_cache: `config.metadata_path={}` for meta blob is invalid",
339                path
340            ))
341        })?;
342        if !path.is_file() {
343            return Err(einval!(
344                "blob_cache: `config.metadata_path` for meta blob is not a file"
345            ));
346        }
347
348        // Validate type of backend and cache.
349        if config.cache.is_fscache() {
350            // Validate the working directory for fscache
351            let cache_config = config.cache.get_fscache_config()?;
352            let path2 = Path::new(&cache_config.work_dir);
353            let path2 = path2
354                .canonicalize()
355                .map_err(|_e| eio!("blob_cache: `config.cache_config.work_dir` is invalid"))?;
356            if !path2.is_dir() {
357                return Err(einval!(
358                    "blob_cache: `config.cache_config.work_dir` is not a directory"
359                ));
360            }
361        } else if config.cache.is_filecache() {
362            // Validate the working directory for filecache
363            let cache_config = config.cache.get_filecache_config()?;
364            let path2 = Path::new(&cache_config.work_dir);
365            let path2 = path2
366                .canonicalize()
367                .map_err(|_e| eio!("blob_cache: `config.cache_config.work_dir` is invalid"))?;
368            if !path2.is_dir() {
369                return Err(einval!(
370                    "blob_cache: `config.cache_config.work_dir` is not a directory"
371                ));
372            }
373        } else {
374            return Err(einval!("blob_cache: unknown cache type"));
375        }
376
377        let config: Arc<ConfigV2> = Arc::new(config.into());
378        config.internal.set_blob_accessible(true);
379
380        Ok((path, config))
381    }
382
383    fn add_meta_object(
384        &self,
385        domain_id: &str,
386        id: &str,
387        path: PathBuf,
388        config: Arc<ConfigV2>,
389    ) -> Result<()> {
390        let (rs, _) = RafsSuper::load_from_file(&path, config.clone(), false)?;
391        if rs.meta.is_v5() {
392            return Err(einval!("blob_cache: RAFSv5 image is not supported"));
393        }
394
395        let blob_extra_infos = rs.superblock.get_blob_extra_infos()?;
396        let meta = BlobConfig::new_meta_blob(
397            domain_id.to_string(),
398            id.to_string(),
399            path,
400            config,
401            blob_extra_infos,
402            rs.meta.flags.contains(RafsSuperFlags::TARTFS_MODE),
403        );
404        // Safe to unwrap because it's a meta blob object.
405        let meta_obj = meta.meta_config().unwrap();
406        let mut state = self.get_state();
407        state.try_add(meta)?;
408
409        // Try to add the referenced data blob object if it doesn't exist yet.
410        for bi in rs.superblock.get_blob_infos() {
411            debug!(
412                "blob_cache: add data blob {} to domain {}",
413                &bi.blob_id(),
414                domain_id
415            );
416            let data_blob =
417                BlobConfig::new_data_blob(domain_id.to_string(), bi, meta_obj.config.clone());
418            let data_blob_config = match &data_blob {
419                BlobConfig::DataBlob(entry) => entry.clone(),
420                _ => panic!("blob_cache: internal error"),
421            };
422
423            if let Err(e) = state.try_add(data_blob) {
424                // Rollback added bootstrap/data blobs.
425                let id = BlobCacheObjectId {
426                    domain_id: domain_id.to_string(),
427                    blob_id: id.to_string(),
428                };
429                let _ = state.remove(&id);
430                return Err(e);
431            }
432
433            // Associate the data blob with the bootstrap blob.
434            meta_obj.add_data_blob(data_blob_config);
435        }
436
437        Ok(())
438    }
439}
440
441/// Structure representing a cached metadata blob.
442pub struct MetaBlob {
443    file: File,
444    size: u64,
445}
446
447impl MetaBlob {
448    /// Create a new [MetaBlob] object from
449    pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
450        let file = OpenOptions::new()
451            .read(true)
452            .write(false)
453            .open(path.as_ref())
454            .inspect_err(|_e| {
455                warn!(
456                    "blob_cache: failed to open metadata blob {}",
457                    path.as_ref().display()
458                );
459            })?;
460        let md = file.metadata().inspect_err(|_e| {
461            warn!(
462                "blob_cache: failed to get metadata about metadata blob {}",
463                path.as_ref().display()
464            );
465        })?;
466        let size = md.len();
467        if size % EROFS_BLOCK_SIZE_4096 != 0 || (size >> EROFS_BLOCK_BITS_12) > u32::MAX as u64 {
468            return Err(einval!(format!(
469                "blob_cache: metadata blob size (0x{:x}) is invalid",
470                size
471            )));
472        }
473
474        Ok(MetaBlob {
475            file: File::from_std(file),
476            size,
477        })
478    }
479
480    /// Get number of blocks in unit of EROFS_BLOCK_SIZE.
481    pub fn blocks(&self) -> u32 {
482        (self.size >> EROFS_BLOCK_BITS_12) as u32
483    }
484
485    /// Read data from the cached metadata blob in asynchronous mode.
486    pub async fn async_read<T: IoBufMut>(&self, pos: u64, buf: T) -> (Result<usize>, T) {
487        self.file.read_at(buf, pos).await
488    }
489}
490
491/// Structure representing a cached data blob.
492pub struct DataBlob {
493    blob_id: String,
494    blob: Arc<dyn BlobCache>,
495    file: File,
496}
497
498impl DataBlob {
499    /// Create a new instance of [DataBlob].
500    pub fn new(config: &Arc<DataBlobConfig>) -> Result<Self> {
501        let blob_id = config.blob_info().blob_id();
502        let blob = BLOB_FACTORY
503            .new_blob_cache(config.config_v2(), &config.blob_info)
504            .inspect_err(|_e| {
505                warn!(
506                    "blob_cache: failed to create cache object for blob {}",
507                    blob_id
508                );
509            })?;
510
511        match blob.get_blob_object() {
512            Some(obj) => {
513                let fd = nix::unistd::dup(obj.as_raw_fd())?;
514                // Safe because the `fd` is valid.
515                let file = unsafe { File::from_raw_fd(fd) };
516                Ok(DataBlob {
517                    blob_id,
518                    blob,
519                    file,
520                })
521            }
522            None => Err(eio!(format!(
523                "blob_cache: failed to get BlobObject for blob {}",
524                blob_id
525            ))),
526        }
527    }
528
529    /// Read data from the cached data blob in asynchronous mode.
530    pub async fn async_read<T: IoBufMut>(&self, pos: u64, buf: T) -> (Result<usize>, T) {
531        match self.blob.get_blob_object() {
532            Some(obj) => match obj.fetch_range_uncompressed(pos, buf.bytes_total() as u64) {
533                Ok(_) => self.file.read_at(buf, pos).await,
534                Err(e) => (Err(e), buf),
535            },
536            None => (
537                Err(eio!(format!(
538                    "blob_cache: failed to get BlobObject for blob {}",
539                    self.blob_id
540                ))),
541                buf,
542            ),
543        }
544    }
545}
546
547#[cfg(test)]
548mod tests {
549    use super::*;
550    use vmm_sys_util::tempdir::TempDir;
551
552    fn create_factory_config() -> String {
553        let config = r#"
554        {
555            "type": "bootstrap",
556            "id": "bootstrap1",
557            "domain_id": "userid1",
558            "config": {
559                "id": "factory1",
560                "backend_type": "localfs",
561                "backend_config": {
562                    "dir": "/tmp/nydus"
563                },
564                "cache_type": "fscache",
565                "cache_config": {
566                    "work_dir": "/tmp/nydus"
567                },
568                "metadata_path": "/tmp/nydus/bootstrap1"
569            }
570          }"#;
571
572        config.to_string()
573    }
574
575    #[test]
576    fn test_generate_blob_key() {
577        assert_eq!(&generate_blob_key("", "blob1"), "blob1");
578        assert_eq!(&generate_blob_key("domain1", "blob1"), "domain1/blob1");
579    }
580
581    #[test]
582    fn test_blob_cache_entry() {
583        let tmpdir = TempDir::new().unwrap();
584        let path = tmpdir.as_path().join("bootstrap1");
585        std::fs::write(path, "metadata").unwrap();
586        let cfg = create_factory_config();
587        let content = cfg.replace("/tmp/nydus", tmpdir.as_path().to_str().unwrap());
588        let mut entry: BlobCacheEntry = serde_json::from_str(&content).unwrap();
589        assert!(entry.prepare_configuration_info());
590        let blob_config = entry.blob_config.as_ref().unwrap();
591
592        assert_eq!(&entry.blob_type, "bootstrap");
593        assert_eq!(&entry.blob_id, "bootstrap1");
594        assert_eq!(&entry.domain_id, "userid1");
595        assert_eq!(&blob_config.id, "factory1");
596        assert_eq!(&blob_config.backend.backend_type, "localfs");
597        assert_eq!(&blob_config.cache.cache_type, "fscache");
598        assert!(blob_config.metadata_path.is_some());
599        assert!(blob_config.backend.localfs.is_some());
600        assert!(blob_config.cache.fs_cache.is_some());
601
602        let mgr = BlobCacheMgr::new();
603        let (path, config) = mgr.get_meta_info(&entry).unwrap();
604        let backend_cfg = config.get_backend_config().unwrap();
605        let cache_cfg = config.get_cache_config().unwrap();
606        assert_eq!(path, tmpdir.as_path().join("bootstrap1"));
607        assert_eq!(&config.id, "factory1");
608        assert_eq!(&backend_cfg.backend_type, "localfs");
609        assert_eq!(&cache_cfg.cache_type, "fscache");
610
611        let blob = MetaBlobConfig {
612            blob_id: "123456789-123".to_string(),
613            scoped_blob_id: "domain1".to_string(),
614            path: path.clone(),
615            config,
616            blobs: Mutex::new(Vec::new()),
617            blob_extra_infos: HashMap::new(),
618            is_tarfs_mode: false,
619        };
620        assert_eq!(blob.path(), &path);
621        assert_eq!(blob.blob_id(), "123456789-123");
622    }
623
624    #[test]
625    fn test_invalid_blob_id() {
626        let tmpdir = TempDir::new().unwrap();
627        let path = tmpdir.as_path().join("bootstrap1");
628        std::fs::write(path, "metadata").unwrap();
629        let config = create_factory_config();
630        let content = config.replace("/tmp/nydus", tmpdir.as_path().to_str().unwrap());
631        let mut entry: BlobCacheEntry = serde_json::from_str(&content).unwrap();
632        let mgr = BlobCacheMgr::new();
633
634        entry.blob_id = "domain2/blob1".to_string();
635        mgr.get_meta_info(&entry).unwrap_err();
636    }
637
638    #[test]
639    fn test_blob_cache_list() {
640        let config = r#"
641         {
642            "blobs" : [
643                {
644                    "type": "bootstrap",
645                    "id": "bootstrap1",
646                    "domain_id": "userid1",
647                    "config": {
648                        "id": "factory1",
649                        "backend_type": "localfs",
650                        "backend_config": {
651                            "dir": "/tmp/nydus"
652                        },
653                        "cache_type": "fscache",
654                        "cache_config": {
655                            "work_dir": "/tmp/nydus"
656                        },
657                        "metadata_path": "/tmp/nydus/bootstrap1"
658                    }
659                },
660                {
661                    "type": "bootstrap",
662                    "id": "bootstrap2",
663                    "domain_id": "userid2",
664                    "config": {
665                        "id": "factory1",
666                        "backend_type": "localfs",
667                        "backend_config": {
668                            "dir": "/tmp/nydus"
669                        },
670                        "cache_type": "fscache",
671                        "cache_config": {
672                            "work_dir": "/tmp/nydus"
673                        },
674                        "metadata_path": "/tmp/nydus/bootstrap2"
675                    }
676                }
677            ]
678         }"#;
679        let mut list: BlobCacheList = serde_json::from_str(config).unwrap();
680        assert!(list.blobs[0].prepare_configuration_info());
681
682        assert_eq!(list.blobs.len(), 2);
683        assert_eq!(&list.blobs[0].blob_type, "bootstrap");
684        assert_eq!(&list.blobs[0].blob_id, "bootstrap1");
685        let blob_config = &list.blobs[0].blob_config.as_ref().unwrap();
686        assert_eq!(&blob_config.id, "factory1");
687        assert_eq!(&blob_config.backend.backend_type, "localfs");
688        assert_eq!(&blob_config.cache.cache_type, "fscache");
689        assert_eq!(&list.blobs[1].blob_type, "bootstrap");
690        assert_eq!(&list.blobs[1].blob_id, "bootstrap2");
691    }
692
693    #[test]
694    fn test_add_bootstrap() {
695        let tmpdir = TempDir::new().unwrap();
696        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
697        let mut source_path = PathBuf::from(root_dir);
698        source_path.push("../tests/texture/bootstrap/rafs-v6-2.2.boot");
699
700        let config = r#"
701        {
702            "type": "bootstrap",
703            "id": "rafs-v6",
704            "domain_id": "domain2",
705            "config_v2": {
706                "version": 2,
707                "id": "factory1",
708                "backend": {
709                    "type": "localfs",
710                    "localfs": {
711                        "dir": "/tmp/nydus"
712                    }
713                },
714                "cache": {
715                    "type": "fscache",
716                    "fscache": {
717                        "work_dir": "/tmp/nydus"
718                    }
719                },
720                "metadata_path": "RAFS_V5"
721            }
722          }"#;
723        let content = config
724            .replace("/tmp/nydus", tmpdir.as_path().to_str().unwrap())
725            .replace("RAFS_V5", &source_path.display().to_string());
726        let mut entry: BlobCacheEntry = serde_json::from_str(&content).unwrap();
727        assert!(entry.prepare_configuration_info());
728
729        let mgr = BlobCacheMgr::new();
730        mgr.add_blob_entry(&entry).unwrap();
731        let blob_id = generate_blob_key(&entry.domain_id, &entry.blob_id);
732        assert!(mgr.get_config(&blob_id).is_some());
733
734        // add the same entry will trigger an error
735        assert!(mgr.add_blob_entry(&entry).is_err());
736
737        // Check existence of data blob referenced by the bootstrap.
738        let key = generate_blob_key(
739            &entry.domain_id,
740            "be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef",
741        );
742        assert!(mgr.get_config(&key).is_some());
743
744        assert_eq!(mgr.get_state().id_to_config_map.len(), 2);
745
746        entry.blob_id = "rafs-v6-cloned".to_string();
747        let blob_id_cloned = generate_blob_key(&entry.domain_id, &entry.blob_id);
748        mgr.add_blob_entry(&entry).unwrap();
749        assert_eq!(mgr.get_state().id_to_config_map.len(), 3);
750        assert!(mgr.get_config(&blob_id).is_some());
751        assert!(mgr.get_config(&blob_id_cloned).is_some());
752
753        mgr.remove_blob_entry(&BlobCacheObjectId {
754            domain_id: entry.domain_id.clone(),
755            blob_id: "rafs-v6".to_string(),
756        })
757        .unwrap();
758        assert_eq!(mgr.get_state().id_to_config_map.len(), 2);
759        assert!(mgr.get_config(&blob_id).is_none());
760        assert!(mgr.get_config(&blob_id_cloned).is_some());
761
762        mgr.remove_blob_entry(&BlobCacheObjectId {
763            domain_id: entry.domain_id,
764            blob_id: "rafs-v6-cloned".to_string(),
765        })
766        .unwrap();
767        assert_eq!(mgr.get_state().id_to_config_map.len(), 0);
768        assert!(mgr.get_config(&blob_id).is_none());
769        assert!(mgr.get_config(&blob_id_cloned).is_none());
770    }
771
772    #[test]
773    fn test_meta_blob() {
774        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
775        let mut source_path = PathBuf::from(root_dir);
776        source_path.push("../tests/texture/bootstrap/rafs-v6-2.2.boot");
777
778        tokio_uring::start(async move {
779            let meta_blob = MetaBlob::new(&source_path).unwrap();
780            assert_eq!(meta_blob.blocks(), 5);
781            let buf = vec![0u8; 4096];
782            let (res, buf) = meta_blob.async_read(0, buf).await;
783            assert_eq!(res.unwrap(), 4096);
784            assert_eq!(buf[0], 0);
785            assert_eq!(buf[1023], 0);
786            assert_eq!(buf[1024], 0xe2);
787            assert_eq!(buf[1027], 0xe0);
788            let (res, _buf) = meta_blob.async_read(0x6000, buf).await;
789            assert_eq!(res.unwrap(), 0);
790        });
791    }
792}