Skip to main content

nydus_storage/backend/
localdisk.rs

1// Copyright (C) 2022 Alibaba Cloud. All rights reserved.
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Storage backend driver to access blobs on local disks.
6
7use std::collections::HashMap;
8use std::fmt;
9use std::fs::{File, OpenOptions};
10use std::io::Result;
11use std::os::unix::io::AsRawFd;
12use std::path::Path;
13use std::sync::{Arc, RwLock};
14
15use fuse_backend_rs::file_buf::FileVolatileSlice;
16use nix::sys::uio;
17use nydus_api::LocalDiskConfig;
18use nydus_utils::metrics::BackendMetrics;
19
20use crate::backend::{BackendError, BackendResult, BlobBackend, BlobReader};
21use crate::utils::{readv, MemSliceCursor};
22
23type LocalDiskResult<T> = std::result::Result<T, LocalDiskError>;
24
25/// Error codes related to localdisk storage backend.
26#[derive(Debug)]
27pub enum LocalDiskError {
28    BlobFile(String),
29    ReadBlob(String),
30}
31
32impl fmt::Display for LocalDiskError {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            LocalDiskError::BlobFile(s) => write!(f, "{}", s),
36            LocalDiskError::ReadBlob(s) => write!(f, "{}", s),
37        }
38    }
39}
40
41impl From<LocalDiskError> for BackendError {
42    fn from(error: LocalDiskError) -> Self {
43        BackendError::LocalDisk(error)
44    }
45}
46
47#[derive(Debug)]
48struct LocalDiskBlob {
49    // The file descriptor of the disk
50    device_file: File,
51    // Start offset of the partition
52    blob_offset: u64,
53    // Length of the partition
54    blob_length: u64,
55    // The identifier for the corresponding blob.
56    blob_id: String,
57    // Metrics collector.
58    metrics: Arc<BackendMetrics>,
59}
60
61impl BlobReader for LocalDiskBlob {
62    fn blob_size(&self) -> BackendResult<u64> {
63        Ok(self.blob_length)
64    }
65
66    fn try_read(&self, buf: &mut [u8], offset: u64) -> BackendResult<usize> {
67        let msg = format!(
68            "localdisk: invalid offset 0x{:x}, base 0x{:x}, length 0x{:x}",
69            offset, self.blob_offset, self.blob_length
70        );
71        if offset >= self.blob_length {
72            return Ok(0);
73        }
74        let actual_offset = self
75            .blob_offset
76            .checked_add(offset)
77            .ok_or(LocalDiskError::ReadBlob(msg))?;
78        let len = std::cmp::min(self.blob_length - offset, buf.len() as u64) as usize;
79
80        uio::pread(
81            self.device_file.as_raw_fd(),
82            &mut buf[..len],
83            actual_offset as i64,
84        )
85        .map_err(|e| {
86            let msg = format!(
87                "localdisk: failed to read data from blob {}, {}",
88                self.blob_id, e
89            );
90            LocalDiskError::ReadBlob(msg).into()
91        })
92    }
93
94    fn readv(
95        &self,
96        bufs: &[FileVolatileSlice],
97        offset: u64,
98        max_size: usize,
99    ) -> BackendResult<usize> {
100        let msg = format!(
101            "localdisk: invalid offset 0x{:x}, base 0x{:x}, length 0x{:x}",
102            offset, self.blob_offset, self.blob_length
103        );
104        if offset >= self.blob_length {
105            return Ok(0);
106        }
107        let actual_offset = self
108            .blob_offset
109            .checked_add(offset)
110            .ok_or(LocalDiskError::ReadBlob(msg.clone()))?;
111
112        let mut c = MemSliceCursor::new(bufs);
113        let mut iovec = c.consume(max_size);
114        let mut len = 0;
115        for buf in bufs {
116            len += buf.len();
117        }
118
119        // Guarantees that reads do not exceed the size of the blob
120        if offset.checked_add(len as u64).is_none() || offset + len as u64 > self.blob_length {
121            return Err(LocalDiskError::ReadBlob(msg).into());
122        }
123
124        readv(self.device_file.as_raw_fd(), &mut iovec, actual_offset).map_err(|e| {
125            let msg = format!(
126                "localdisk: failed to read data from blob {}, {}",
127                self.blob_id, e
128            );
129            LocalDiskError::ReadBlob(msg).into()
130        })
131    }
132
133    fn metrics(&self) -> &BackendMetrics {
134        &self.metrics
135    }
136}
137
138/// Storage backend based on local disk.
139pub struct LocalDisk {
140    // A reference to an open device
141    device_file: File,
142    // The disk device path specified by the user
143    device_path: String,
144    // Size of the block device.
145    device_capacity: u64,
146    // Blobs are discovered by scanning GPT or not.
147    is_gpt_mode: bool,
148    // Metrics collector.
149    metrics: Arc<BackendMetrics>,
150    // Hashmap to map blob id to disk entry.
151    entries: RwLock<HashMap<String, Arc<LocalDiskBlob>>>,
152}
153
154impl LocalDisk {
155    pub fn new(config: &LocalDiskConfig, id: Option<&str>) -> Result<LocalDisk> {
156        let id = id.ok_or_else(|| einval!("localdisk: argument `id` is empty"))?;
157        let path = &config.device_path;
158        let path_buf = Path::new(path).to_path_buf().canonicalize().map_err(|e| {
159            einval!(format!(
160                "localdisk: invalid disk device path {}, {}",
161                path, e
162            ))
163        })?;
164        let device_file = OpenOptions::new().read(true).open(path_buf).map_err(|e| {
165            einval!(format!(
166                "localdisk: can not open disk device at {}, {}",
167                path, e
168            ))
169        })?;
170        let md = device_file.metadata().map_err(|e| {
171            eio!(format!(
172                "localdisk: can not get file meta data about disk device {}, {}",
173                path, e
174            ))
175        })?;
176        let mut local_disk = LocalDisk {
177            device_file,
178            device_path: path.clone(),
179            device_capacity: md.len(),
180            is_gpt_mode: false,
181            metrics: BackendMetrics::new(id, "localdisk"),
182            entries: RwLock::new(HashMap::new()),
183        };
184
185        if !config.disable_gpt {
186            local_disk.scan_blobs_by_gpt()?;
187        }
188
189        Ok(local_disk)
190    }
191
192    pub fn add_blob(&self, blob_id: &str, offset: u64, length: u64) -> LocalDiskResult<()> {
193        if self.is_gpt_mode {
194            let msg = format!(
195                "localdisk: device {} is in legacy gpt mode",
196                self.device_path
197            );
198            return Err(LocalDiskError::BlobFile(msg));
199        }
200        if offset.checked_add(length).is_none() || offset + length > self.device_capacity {
201            let msg = format!(
202                "localdisk: add blob {} with invalid offset 0x{:x} and length 0x{:x}, device size 0x{:x}",
203                blob_id, offset, length, self.device_capacity
204            );
205            return Err(LocalDiskError::BlobFile(msg));
206        };
207
208        let device_file = self.device_file.try_clone().map_err(|e| {
209            LocalDiskError::BlobFile(format!("localdisk: can not duplicate file, {}", e))
210        })?;
211        let blob = Arc::new(LocalDiskBlob {
212            blob_id: blob_id.to_string(),
213            device_file,
214            blob_offset: offset,
215            blob_length: length,
216            metrics: self.metrics.clone(),
217        });
218
219        let mut table_guard = self.entries.write().unwrap();
220        if table_guard.contains_key(blob_id) {
221            let msg = format!("localdisk: blob {} already exists", blob_id);
222            return Err(LocalDiskError::BlobFile(msg));
223        }
224        table_guard.insert(blob_id.to_string(), blob);
225
226        Ok(())
227    }
228
229    fn get_blob(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
230        // Don't expect poisoned lock here.
231        if let Some(entry) = self.entries.read().unwrap().get(blob_id) {
232            Ok(entry.clone())
233        } else {
234            self.get_blob_from_gpt(blob_id)
235        }
236    }
237}
238
239#[cfg(feature = "backend-localdisk-gpt")]
240impl LocalDisk {
241    // Disk names in GPT tables cannot store full 64-byte blob ids, so we should truncate them to 32 bytes.
242    fn truncate_blob_id(blob_id: &str) -> Option<&str> {
243        const LOCALDISK_BLOB_ID_LEN: usize = 32;
244        if blob_id.len() >= LOCALDISK_BLOB_ID_LEN {
245            let new_blob_id = &blob_id[0..LOCALDISK_BLOB_ID_LEN];
246            Some(new_blob_id)
247        } else {
248            None
249        }
250    }
251
252    fn get_blob_from_gpt(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
253        if self.is_gpt_mode {
254            if let Some(localdisk_blob_id) = LocalDisk::truncate_blob_id(blob_id) {
255                // Don't expect poisoned lock here.
256                if let Some(entry) = self.entries.read().unwrap().get(localdisk_blob_id) {
257                    return Ok(entry.clone());
258                }
259            }
260        }
261
262        let msg = format!("localdisk: can not find such blob: {}", blob_id);
263        Err(LocalDiskError::ReadBlob(msg))
264    }
265
266    fn scan_blobs_by_gpt(&mut self) -> Result<()> {
267        // Open disk image.
268        let cfg = gpt::GptConfig::new().writable(false);
269        let disk = cfg.open(&self.device_path)?;
270        let partitions = disk.partitions();
271        let sector_size = gpt::disk::DEFAULT_SECTOR_SIZE;
272        info!(
273            "Localdisk initializing storage backend for device {} with {} partitions, GUID: {}",
274            self.device_path,
275            partitions.len(),
276            disk.guid()
277        );
278
279        let mut table_guard = self.entries.write().unwrap();
280        for (k, v) in partitions {
281            let length = v.bytes_len(sector_size)?;
282            let base_offset = v.bytes_start(sector_size)?;
283            if base_offset.checked_add(length).is_none()
284                || base_offset + length > self.device_capacity
285            {
286                let msg = format!(
287                    "localdisk: partition {} with invalid offset and length",
288                    v.part_guid
289                );
290                return Err(einval!(msg));
291            };
292            let guid = v.part_guid;
293            let mut is_gpt_mode = false;
294            let name = if v.part_type_guid == gpt::partition_types::BASIC {
295                is_gpt_mode = true;
296                // Compatible with old versions of localdisk image
297                v.name.clone()
298            } else {
299                // The 64-byte blob_id is stored in two parts
300                v.name.clone() + guid.simple().to_string().as_str()
301            };
302
303            if name.is_empty() {
304                let msg = format!("localdisk: partition {} has empty blob id", v.part_guid);
305                return Err(einval!(msg));
306            }
307            if table_guard.contains_key(&name) {
308                let msg = format!("localdisk: blob {} already exists", name);
309                return Err(einval!(msg));
310            }
311
312            let device_file = self.device_file.try_clone()?;
313            let partition = Arc::new(LocalDiskBlob {
314                blob_id: name.clone(),
315                device_file,
316                blob_offset: base_offset,
317                blob_length: length,
318                metrics: self.metrics.clone(),
319            });
320
321            debug!(
322                "Localdisk partition {} initialized, blob id: {}, offset {}, length {}",
323                k, partition.blob_id, partition.blob_offset, partition.blob_length
324            );
325            table_guard.insert(name, partition);
326            if is_gpt_mode {
327                self.is_gpt_mode = true;
328            }
329        }
330
331        Ok(())
332    }
333}
334
335#[cfg(not(feature = "backend-localdisk-gpt"))]
336impl LocalDisk {
337    fn get_blob_from_gpt(&self, blob_id: &str) -> LocalDiskResult<Arc<dyn BlobReader>> {
338        Err(LocalDiskError::ReadBlob(format!(
339            "can not find such blob: {}, this image might be corrupted",
340            blob_id
341        )))
342    }
343
344    fn scan_blobs_by_gpt(&mut self) -> Result<()> {
345        Ok(())
346    }
347}
348
349impl BlobBackend for LocalDisk {
350    fn shutdown(&self) {}
351
352    fn metrics(&self) -> &BackendMetrics {
353        &self.metrics
354    }
355
356    fn get_reader(&self, blob_id: &str) -> BackendResult<Arc<dyn BlobReader>> {
357        self.get_blob(blob_id).map_err(|e| e.into())
358    }
359}
360
361impl Drop for LocalDisk {
362    fn drop(&mut self) {
363        self.metrics.release().unwrap_or_else(|e| error!("{:?}", e));
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    #[test]
372    fn test_invalid_localdisk_new() {
373        let config = LocalDiskConfig {
374            device_path: "".to_string(),
375            disable_gpt: true,
376        };
377        assert!(LocalDisk::new(&config, Some("test")).is_err());
378
379        let config = LocalDiskConfig {
380            device_path: "/a/b/c".to_string(),
381            disable_gpt: true,
382        };
383        assert!(LocalDisk::new(&config, None).is_err());
384    }
385
386    #[test]
387    fn test_add_disk_blob() {
388        let root_dir = &std::env::var("CARGO_MANIFEST_DIR").expect("$CARGO_MANIFEST_DIR");
389        let root_dir = Path::new(root_dir).join("../tests/texture/blobs/");
390
391        let config = LocalDiskConfig {
392            device_path: root_dir.join("nonexist_blob_file").display().to_string(),
393            disable_gpt: true,
394        };
395        assert!(LocalDisk::new(&config, Some("test")).is_err());
396
397        let blob_id = "be7d77eeb719f70884758d1aa800ed0fb09d701aaec469964e9d54325f0d5fef";
398        let config = LocalDiskConfig {
399            device_path: root_dir.join(blob_id).display().to_string(),
400            disable_gpt: true,
401        };
402        let disk = LocalDisk::new(&config, Some("test")).unwrap();
403
404        assert!(disk.add_blob(blob_id, u64::MAX, 1).is_err());
405        assert!(disk.add_blob(blob_id, 14553, 2).is_err());
406        assert!(disk.add_blob(blob_id, 14554, 1).is_err());
407        assert!(disk.add_blob(blob_id, 0, 4096).is_ok());
408        assert!(disk.add_blob(blob_id, 0, 4096).is_err());
409        let blob = disk.get_blob(blob_id).unwrap();
410        assert_eq!(blob.blob_size().unwrap(), 4096);
411
412        let mut buf = vec![0u8; 4096];
413        let sz = blob.read(&mut buf, 0).unwrap();
414        assert_eq!(sz, 4096);
415        let sz = blob.read(&mut buf, 4095).unwrap();
416        assert_eq!(sz, 1);
417        let sz = blob.read(&mut buf, 4096).unwrap();
418        assert_eq!(sz, 0);
419        let sz = blob.read(&mut buf, 4097).unwrap();
420        assert_eq!(sz, 0);
421    }
422
423    #[cfg(feature = "backend-localdisk-gpt")]
424    #[test]
425    fn test_truncate_blob_id() {
426        let guid = "50ad3c8243e0a08ecdebde0ef8afcc6f2abca44498ad15491acbe58c83acb66f";
427        let guid_truncated = "50ad3c8243e0a08ecdebde0ef8afcc6f";
428
429        let result = LocalDisk::truncate_blob_id(guid).unwrap();
430        assert_eq!(result, guid_truncated)
431    }
432}