Coverage for nova/virt/disk/mount/api.py: 88%
150 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-04-24 11:16 +0000
1# Copyright 2011 Red Hat, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14"""Support for mounting virtual image files."""
16import os
17import time
19from oslo_log import log as logging
20from oslo_service import loopingcall
21from oslo_utils import importutils
23from nova import exception
24from nova.i18n import _
25import nova.privsep.fs
26from nova.virt.image import model as imgmodel
28LOG = logging.getLogger(__name__)
30MAX_DEVICE_WAIT = 30
31MAX_FILE_CHECKS = 6
32FILE_CHECK_INTERVAL = 0.25
35class Mount(object):
36 """Standard mounting operations, that can be overridden by subclasses.
38 The basic device operations provided are get, map and mount,
39 to be called in that order.
40 """
42 mode = None # to be overridden in subclasses
44 @staticmethod
45 def instance_for_format(image, mountdir, partition):
46 """Get a Mount instance for the image type
48 :param image: instance of nova.virt.image.model.Image
49 :param mountdir: path to mount the image at
50 :param partition: partition number to mount
51 """
52 LOG.debug("Instance for format image=%(image)s "
53 "mountdir=%(mountdir)s partition=%(partition)s",
54 {'image': image, 'mountdir': mountdir,
55 'partition': partition})
57 if isinstance(image, imgmodel.LocalFileImage):
58 if image.format == imgmodel.FORMAT_RAW:
59 LOG.debug("Using LoopMount")
60 return importutils.import_object(
61 "nova.virt.disk.mount.loop.LoopMount",
62 image, mountdir, partition)
63 else:
64 LOG.debug("Using NbdMount")
65 return importutils.import_object(
66 "nova.virt.disk.mount.nbd.NbdMount",
67 image, mountdir, partition)
68 elif isinstance(image, imgmodel.LocalBlockImage): 68 ↛ 81line 68 didn't jump to line 81 because the condition on line 68 was always true
69 LOG.debug("Using BlockMount")
70 return importutils.import_object(
71 "nova.virt.disk.mount.block.BlockMount",
72 image, mountdir, partition)
73 else:
74 # TODO(berrange) We could mount RBDImage directly
75 # using kernel RBD block dev support.
76 #
77 # This is left as an enhancement for future
78 # motivated developers todo, since raising
79 # an exception is on par with what this
80 # code did historically
81 raise exception.UnsupportedImageModel(
82 image.__class__.__name__)
84 @staticmethod
85 def instance_for_device(image, mountdir, partition, device):
86 """Get a Mount instance for the device type
88 :param image: instance of nova.virt.image.model.Image
89 :param mountdir: path to mount the image at
90 :param partition: partition number to mount
91 :param device: mounted device path
92 """
94 LOG.debug("Instance for device image=%(image)s "
95 "mountdir=%(mountdir)s partition=%(partition)s "
96 "device=%(device)s",
97 {'image': image, 'mountdir': mountdir,
98 'partition': partition, 'device': device})
100 if "loop" in device:
101 LOG.debug("Using LoopMount")
102 return importutils.import_object(
103 "nova.virt.disk.mount.loop.LoopMount",
104 image, mountdir, partition, device)
105 elif "nbd" in device:
106 LOG.debug("Using NbdMount")
107 return importutils.import_object(
108 "nova.virt.disk.mount.nbd.NbdMount",
109 image, mountdir, partition, device)
110 else:
111 LOG.debug("Using BlockMount")
112 return importutils.import_object(
113 "nova.virt.disk.mount.block.BlockMount",
114 image, mountdir, partition, device)
116 def __init__(self, image, mount_dir, partition=None, device=None):
117 """Create a new Mount instance
119 :param image: instance of nova.virt.image.model.Image
120 :param mount_dir: path to mount the image at
121 :param partition: partition number to mount
122 :param device: mounted device path
123 """
125 # Input
126 self.image = image
127 self.partition = partition
128 self.mount_dir = mount_dir
130 # Output
131 self.error = ""
133 # Internal
134 self.linked = self.mapped = self.mounted = self.automapped = False
135 self.device = self.mapped_device = device
137 # Reset to mounted dir if possible
138 self.reset_dev()
140 def reset_dev(self):
141 """Reset device paths to allow unmounting."""
142 if not self.device:
143 return
145 self.linked = self.mapped = self.mounted = True
147 device = self.device
148 if os.path.isabs(device) and os.path.exists(device):
149 if device.startswith('/dev/mapper/'):
150 device = os.path.basename(device)
151 if 'p' in device: 151 ↛ exitline 151 didn't return from function 'reset_dev' because the condition on line 151 was always true
152 device, self.partition = device.rsplit('p', 1)
153 self.device = os.path.join('/dev', device)
155 def get_dev(self):
156 """Make the image available as a block device in the file system."""
157 self.device = None
158 self.linked = True
159 return True
161 def _get_dev_retry_helper(self):
162 """Some implementations need to retry their get_dev."""
163 # NOTE(mikal): This method helps implement retries. The implementation
164 # simply calls _get_dev_retry_helper from their get_dev, and implements
165 # _inner_get_dev with their device acquisition logic. The NBD
166 # implementation has an example.
167 start_time = time.time()
168 device = self._inner_get_dev()
169 while not device:
170 LOG.info('Device allocation failed. Will retry in 2 seconds.')
171 time.sleep(2)
172 if time.time() - start_time > MAX_DEVICE_WAIT: 172 ↛ 175line 172 didn't jump to line 175 because the condition on line 172 was always true
173 LOG.warning('Device allocation failed after repeated retries.')
174 return False
175 device = self._inner_get_dev()
176 return True
178 def _inner_get_dev(self):
179 raise NotImplementedError()
181 def unget_dev(self):
182 """Release the block device from the file system namespace."""
183 self.linked = False
185 def map_dev(self):
186 """Map partitions of the device to the file system namespace."""
187 assert os.path.exists(self.device)
188 LOG.debug("Map dev %s", self.device)
189 automapped_path = '/dev/%sp%s' % (os.path.basename(self.device),
190 self.partition)
192 if self.partition == -1:
193 self.error = _('partition search unsupported with %s') % self.mode
194 elif self.partition and not os.path.exists(automapped_path):
195 map_path = '/dev/mapper/%sp%s' % (os.path.basename(self.device),
196 self.partition)
197 assert not os.path.exists(map_path)
199 # Note kpartx can output warnings to stderr and succeed
200 # Also it can output failures to stderr and "succeed"
201 # So we just go on the existence of the mapped device
202 _out, err = nova.privsep.fs.create_device_maps(self.device)
204 @loopingcall.RetryDecorator(
205 max_retry_count=MAX_FILE_CHECKS - 1,
206 max_sleep_time=FILE_CHECK_INTERVAL,
207 exceptions=IOError)
208 def recheck_path(map_path):
209 if not os.path.exists(map_path):
210 raise IOError()
212 # Note kpartx does nothing when presented with a raw image,
213 # so given we only use it when we expect a partitioned image, fail
214 try:
215 recheck_path(map_path)
216 self.mapped_device = map_path
217 self.mapped = True
218 except IOError:
219 if not err: 219 ↛ 221line 219 didn't jump to line 221 because the condition on line 219 was always true
220 err = _('partition %s not found') % self.partition
221 self.error = _('Failed to map partitions: %s') % err
222 elif self.partition and os.path.exists(automapped_path):
223 # Note auto mapping can be enabled with the 'max_part' option
224 # to the nbd or loop kernel modules. Beware of possible races
225 # in the partition scanning for _loop_ devices though
226 # (details in bug 1024586), which are currently uncatered for.
227 self.mapped_device = automapped_path
228 self.mapped = True
229 self.automapped = True
230 else:
231 self.mapped_device = self.device
232 self.mapped = True
234 return self.mapped
236 def unmap_dev(self):
237 """Remove partitions of the device from the file system namespace."""
238 if not self.mapped: 238 ↛ 239line 238 didn't jump to line 239 because the condition on line 238 was never true
239 return
240 LOG.debug("Unmap dev %s", self.device)
241 if self.partition and not self.automapped:
242 nova.privsep.fs.remove_device_maps(self.device)
243 self.mapped = False
244 self.automapped = False
246 def mnt_dev(self):
247 """Mount the device into the file system."""
248 LOG.debug("Mount %(dev)s on %(dir)s",
249 {'dev': self.mapped_device, 'dir': self.mount_dir})
250 out, err = nova.privsep.fs.mount(None, self.mapped_device,
251 self.mount_dir, None)
252 if err: 252 ↛ 257line 252 didn't jump to line 257 because the condition on line 252 was always true
253 self.error = _('Failed to mount filesystem: %s') % err
254 LOG.debug(self.error)
255 return False
257 self.mounted = True
258 return True
260 def unmnt_dev(self):
261 """Unmount the device from the file system."""
262 if not self.mounted: 262 ↛ 263line 262 didn't jump to line 263 because the condition on line 262 was never true
263 return
264 self.flush_dev()
265 LOG.debug("Umount %s", self.mapped_device)
266 nova.privsep.fs.umount(self.mapped_device)
267 self.mounted = False
269 def flush_dev(self):
270 pass
272 def do_mount(self):
273 """Call the get, map and mnt operations."""
274 status = False
275 try:
276 status = self.get_dev() and self.map_dev() and self.mnt_dev()
277 finally:
278 if not status: 278 ↛ 281line 278 didn't jump to line 281 because the condition on line 278 was always true
279 LOG.debug("Fail to mount, tearing back down")
280 self.do_teardown()
281 return status
283 def do_umount(self):
284 """Call the unmnt operation."""
285 if self.mounted:
286 self.unmnt_dev()
288 def do_teardown(self):
289 """Call the umnt, unmap, and unget operations."""
290 if self.mounted:
291 self.unmnt_dev()
292 if self.mapped:
293 self.unmap_dev()
294 if self.linked:
295 self.unget_dev()