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

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.""" 

15 

16import os 

17import time 

18 

19from oslo_log import log as logging 

20from oslo_service import loopingcall 

21from oslo_utils import importutils 

22 

23from nova import exception 

24from nova.i18n import _ 

25import nova.privsep.fs 

26from nova.virt.image import model as imgmodel 

27 

28LOG = logging.getLogger(__name__) 

29 

30MAX_DEVICE_WAIT = 30 

31MAX_FILE_CHECKS = 6 

32FILE_CHECK_INTERVAL = 0.25 

33 

34 

35class Mount(object): 

36 """Standard mounting operations, that can be overridden by subclasses. 

37 

38 The basic device operations provided are get, map and mount, 

39 to be called in that order. 

40 """ 

41 

42 mode = None # to be overridden in subclasses 

43 

44 @staticmethod 

45 def instance_for_format(image, mountdir, partition): 

46 """Get a Mount instance for the image type 

47 

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}) 

56 

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__) 

83 

84 @staticmethod 

85 def instance_for_device(image, mountdir, partition, device): 

86 """Get a Mount instance for the device type 

87 

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 """ 

93 

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}) 

99 

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) 

115 

116 def __init__(self, image, mount_dir, partition=None, device=None): 

117 """Create a new Mount instance 

118 

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 """ 

124 

125 # Input 

126 self.image = image 

127 self.partition = partition 

128 self.mount_dir = mount_dir 

129 

130 # Output 

131 self.error = "" 

132 

133 # Internal 

134 self.linked = self.mapped = self.mounted = self.automapped = False 

135 self.device = self.mapped_device = device 

136 

137 # Reset to mounted dir if possible 

138 self.reset_dev() 

139 

140 def reset_dev(self): 

141 """Reset device paths to allow unmounting.""" 

142 if not self.device: 

143 return 

144 

145 self.linked = self.mapped = self.mounted = True 

146 

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) 

154 

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 

160 

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 

177 

178 def _inner_get_dev(self): 

179 raise NotImplementedError() 

180 

181 def unget_dev(self): 

182 """Release the block device from the file system namespace.""" 

183 self.linked = False 

184 

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) 

191 

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) 

198 

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) 

203 

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() 

211 

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 

233 

234 return self.mapped 

235 

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 

245 

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 

256 

257 self.mounted = True 

258 return True 

259 

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 

268 

269 def flush_dev(self): 

270 pass 

271 

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 

282 

283 def do_umount(self): 

284 """Call the unmnt operation.""" 

285 if self.mounted: 

286 self.unmnt_dev() 

287 

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()