Coverage for nova/virt/disk/vfs/guestfs.py: 81%

196 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-17 15:08 +0000

1# Copyright 2012 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 

15import os 

16 

17from eventlet import tpool 

18from oslo_log import log as logging 

19from oslo_utils import importutils 

20 

21import nova.conf 

22from nova import exception 

23from nova.i18n import _ 

24from nova.virt.disk.vfs import api as vfs 

25from nova.virt.image import model as imgmodel 

26 

27 

28LOG = logging.getLogger(__name__) 

29 

30guestfs = None 

31forceTCG = False 

32 

33CONF = nova.conf.CONF 

34 

35 

36def force_tcg(force=True): 

37 """Prevent libguestfs trying to use KVM acceleration 

38 

39 It is a good idea to call this if it is known that 

40 KVM is not desired, even if technically available. 

41 """ 

42 

43 global forceTCG 

44 forceTCG = force 

45 

46 

47class VFSGuestFS(vfs.VFS): 

48 

49 """This class implements a VFS module that uses the libguestfs APIs 

50 to access the disk image. The disk image is never mapped into 

51 the host filesystem, thus avoiding any potential for symlink 

52 attacks from the guest filesystem. 

53 """ 

54 

55 def __init__(self, image, partition=None): 

56 """Create a new local VFS instance 

57 

58 :param image: instance of nova.virt.image.model.Image 

59 :param partition: the partition number of access 

60 """ 

61 

62 super(VFSGuestFS, self).__init__(image, partition) 

63 

64 global guestfs 

65 if guestfs is None: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true

66 try: 

67 guestfs = importutils.import_module('guestfs') 

68 except Exception as e: 

69 raise exception.NovaException( 

70 _("libguestfs is not installed (%s)") % e) 

71 

72 self.handle = None 

73 self.mount = False 

74 

75 def inspect_capabilities(self): 

76 """Determines whether guestfs is well configured.""" 

77 try: 

78 # If guestfs debug is enabled, we can't launch in a thread because 

79 # the debug logging callback can make eventlet try to switch 

80 # threads and then the launch hangs, causing eternal sadness. 

81 if CONF.guestfs.debug: 

82 LOG.debug('Inspecting guestfs capabilities non-threaded.') 

83 g = guestfs.GuestFS() 

84 else: 

85 g = tpool.Proxy(guestfs.GuestFS()) 

86 g.add_drive("/dev/null") # sic 

87 g.launch() 

88 except Exception as e: 

89 kernel_file = "/boot/vmlinuz-%s" % os.uname().release 

90 if not os.access(kernel_file, os.R_OK): 90 ↛ 94line 90 didn't jump to line 94 because the condition on line 90 was always true

91 raise exception.LibguestfsCannotReadKernel( 

92 _("Please change permissions on %s to 0x644") 

93 % kernel_file) 

94 raise exception.NovaException( 

95 _("libguestfs installed but not usable (%s)") % e) 

96 

97 return self 

98 

99 def configure_debug(self): 

100 """Configures guestfs to be verbose.""" 

101 if not self.handle: 101 ↛ 102line 101 didn't jump to line 102 because the condition on line 101 was never true

102 LOG.warning("Please consider to execute setup before trying " 

103 "to configure debug log message.") 

104 else: 

105 def log_callback(ev, eh, buf, array): 

106 if ev == guestfs.EVENT_APPLIANCE: 

107 buf = buf.rstrip() 

108 LOG.debug("event=%(event)s eh=%(eh)d buf='%(buf)s' " 

109 "array=%(array)s", { 

110 "event": guestfs.event_to_string(ev), 

111 "eh": eh, "buf": buf, "array": array}) 

112 

113 events = (guestfs.EVENT_APPLIANCE | guestfs.EVENT_LIBRARY | 

114 guestfs.EVENT_WARNING | guestfs.EVENT_TRACE) 

115 

116 self.handle.set_trace(True) # just traces libguestfs API calls 

117 self.handle.set_verbose(True) 

118 self.handle.set_event_callback(log_callback, events) 

119 

120 def setup_os(self): 

121 if self.partition == -1: 

122 self.setup_os_inspect() 

123 else: 

124 self.setup_os_static() 

125 

126 def setup_os_static(self): 

127 LOG.debug("Mount guest OS image %(image)s partition %(part)s", 

128 {'image': self.image, 'part': str(self.partition)}) 

129 

130 if self.partition: 

131 self.handle.mount_options("", "/dev/sda%d" % self.partition, "/") 

132 else: 

133 self.handle.mount_options("", "/dev/sda", "/") 

134 

135 def setup_os_inspect(self): 

136 LOG.debug("Inspecting guest OS image %s", self.image) 

137 roots = self.handle.inspect_os() 

138 

139 if len(roots) == 0: 

140 raise exception.NovaException(_("No operating system found in %s") 

141 % self.image) 

142 

143 if len(roots) != 1: 

144 LOG.debug("Multi-boot OS %(roots)s", {'roots': str(roots)}) 

145 raise exception.NovaException( 

146 _("Multi-boot operating system found in %s") % 

147 self.image) 

148 

149 self.setup_os_root(roots[0]) 

150 

151 def setup_os_root(self, root): 

152 LOG.debug("Inspecting guest OS root filesystem %s", root) 

153 mounts = self.handle.inspect_get_mountpoints(root) 

154 

155 if len(mounts) == 0: 155 ↛ 156line 155 didn't jump to line 156 because the condition on line 155 was never true

156 raise exception.NovaException( 

157 _("No mount points found in %(root)s of %(image)s") % 

158 {'root': root, 'image': self.image}) 

159 

160 # the root directory must be mounted first 

161 mounts.sort(key=lambda mount: mount[0]) 

162 

163 root_mounted = False 

164 for mount in mounts: 

165 LOG.debug("Mounting %(dev)s at %(dir)s", 

166 {'dev': mount[1], 'dir': mount[0]}) 

167 try: 

168 self.handle.mount_options("", mount[1], mount[0]) 

169 root_mounted = True 

170 except RuntimeError as e: 

171 msg = _("Error mounting %(device)s to %(dir)s in image" 

172 " %(image)s with libguestfs (%(e)s)") % \ 

173 {'image': self.image, 'device': mount[1], 

174 'dir': mount[0], 'e': e} 

175 if root_mounted: 

176 LOG.debug(msg) 

177 else: 

178 raise exception.NovaException(msg) 

179 

180 def setup(self, mount=True): 

181 LOG.debug("Setting up appliance for %(image)s", 

182 {'image': self.image}) 

183 try: 

184 self.handle = tpool.Proxy( 

185 guestfs.GuestFS(python_return_dict=False, 

186 close_on_exit=False)) 

187 except TypeError as e: 

188 if 'close_on_exit' in str(e) or 'python_return_dict' in str(e): 188 ↛ 194line 188 didn't jump to line 194 because the condition on line 188 was always true

189 # NOTE(russellb) In case we're not using a version of 

190 # libguestfs new enough to support parameters close_on_exit 

191 # and python_return_dict which were added in libguestfs 1.20. 

192 self.handle = tpool.Proxy(guestfs.GuestFS()) 

193 else: 

194 raise 

195 

196 if CONF.guestfs.debug: 

197 self.configure_debug() 

198 

199 try: 

200 if forceTCG: 

201 # TODO(mriedem): Should we be using set_backend_setting 

202 # instead to just set the single force_tcg setting? Because 

203 # according to the guestfs docs, set_backend_settings will 

204 # overwrite all backend settings. The question is, what would 

205 # the value be? True? "set_backend_setting" is available 

206 # starting in 1.27.2 which should be new enough at this point 

207 # on modern distributions. 

208 ret = self.handle.set_backend_settings(["force_tcg"]) 

209 if ret != 0: 209 ↛ 220line 209 didn't jump to line 220 because the condition on line 209 was always true

210 LOG.warning('Failed to force guestfs TCG mode. ' 

211 'guestfs_set_backend_settings returned: %s', 

212 ret) 

213 except AttributeError as ex: 

214 # set_backend_settings method doesn't exist in older 

215 # libguestfs versions, so nothing we can do but ignore 

216 LOG.warning("Unable to force TCG mode, " 

217 "libguestfs too old? %s", ex) 

218 pass 

219 

220 try: 

221 if isinstance(self.image, imgmodel.LocalImage): 

222 self.handle.add_drive_opts(self.image.path, 

223 format=self.image.format) 

224 elif isinstance(self.image, imgmodel.RBDImage): 224 ↛ 233line 224 didn't jump to line 233 because the condition on line 224 was always true

225 self.handle.add_drive_opts("%s/%s" % (self.image.pool, 

226 self.image.name), 

227 protocol="rbd", 

228 format=imgmodel.FORMAT_RAW, 

229 server=self.image.servers, 

230 username=self.image.user, 

231 secret=self.image.password) 

232 else: 

233 raise exception.UnsupportedImageModel( 

234 self.image.__class__.__name__) 

235 

236 self.handle.launch() 

237 

238 if mount: 

239 self.setup_os() 

240 self.handle.aug_init("/", 0) 

241 self.mount = True 

242 except RuntimeError as e: 

243 # explicitly teardown instead of implicit close() 

244 # to prevent orphaned VMs in cases when an implicit 

245 # close() is not enough 

246 self.teardown() 

247 raise exception.NovaException( 

248 _("Error mounting %(image)s with libguestfs (%(e)s)") % 

249 {'image': self.image, 'e': e}) 

250 except Exception: 

251 # explicitly teardown instead of implicit close() 

252 # to prevent orphaned VMs in cases when an implicit 

253 # close() is not enough 

254 self.teardown() 

255 raise 

256 

257 def teardown(self): 

258 LOG.debug("Tearing down appliance") 

259 

260 try: 

261 try: 

262 if self.mount: 

263 self.handle.aug_close() 

264 except RuntimeError as e: 

265 LOG.warning("Failed to close augeas %s", e) 

266 

267 try: 

268 self.handle.shutdown() 

269 except AttributeError: 

270 # Older libguestfs versions haven't an explicit shutdown 

271 pass 

272 except RuntimeError as e: 

273 LOG.warning("Failed to shutdown appliance %s", e) 

274 

275 try: 

276 self.handle.close() 

277 except AttributeError: 

278 # Older libguestfs versions haven't an explicit close 

279 pass 

280 except RuntimeError as e: 

281 LOG.warning("Failed to close guest handle %s", e) 

282 finally: 

283 # dereference object and implicitly close() 

284 self.handle = None 

285 

286 @staticmethod 

287 def _canonicalize_path(path): 

288 if path[0] != '/': 

289 return '/' + path 

290 return path 

291 

292 def make_path(self, path): 

293 LOG.debug("Make directory path=%s", path) 

294 path = self._canonicalize_path(path) 

295 self.handle.mkdir_p(path) 

296 

297 def append_file(self, path, content): 

298 LOG.debug("Append file path=%s", path) 

299 path = self._canonicalize_path(path) 

300 self.handle.write_append(path, content) 

301 

302 def replace_file(self, path, content): 

303 LOG.debug("Replace file path=%s", path) 

304 path = self._canonicalize_path(path) 

305 self.handle.write(path, content) 

306 

307 def read_file(self, path): 

308 LOG.debug("Read file path=%s", path) 

309 path = self._canonicalize_path(path) 

310 data = self.handle.read_file(path) 

311 # NOTE(lyarwood): libguestfs v1.41.1 (0ee02e0117527) switched the 

312 # return type of read_file from string to bytes and as such we need to 

313 # handle both here, decoding and returning a string if bytes is 

314 # provided. https://bugzilla.redhat.com/show_bug.cgi?id=1661871 

315 if isinstance(data, bytes): 315 ↛ 317line 315 didn't jump to line 317 because the condition on line 315 was always true

316 return data.decode() 

317 return data 

318 

319 def has_file(self, path): 

320 LOG.debug("Has file path=%s", path) 

321 path = self._canonicalize_path(path) 

322 try: 

323 self.handle.stat(path) 

324 return True 

325 except RuntimeError: 

326 return False 

327 

328 def set_permissions(self, path, mode): 

329 LOG.debug("Set permissions path=%(path)s mode=%(mode)s", 

330 {'path': path, 'mode': mode}) 

331 path = self._canonicalize_path(path) 

332 self.handle.chmod(mode, path) 

333 

334 def set_ownership(self, path, user, group): 

335 LOG.debug("Set ownership path=%(path)s " 

336 "user=%(user)s group=%(group)s", 

337 {'path': path, 'user': user, 'group': group}) 

338 path = self._canonicalize_path(path) 

339 uid = -1 

340 gid = -1 

341 

342 def _get_item_id(id_path): 

343 try: 

344 return int(self.handle.aug_get("/files/etc/" + id_path)) 

345 except RuntimeError as e: 

346 msg = _("Error obtaining uid/gid for %(user)s/%(group)s: " 

347 " path %(id_path)s not found (%(e)s)") % { 

348 'id_path': "/files/etc/" + id_path, 'user': user, 

349 'group': group, 'e': e} 

350 raise exception.NovaException(msg) 

351 

352 if user is not None: 

353 uid = _get_item_id('passwd/' + user + '/uid') 

354 if group is not None: 

355 gid = _get_item_id('group/' + group + '/gid') 

356 LOG.debug("chown uid=%(uid)d gid=%(gid)s", 

357 {'uid': uid, 'gid': gid}) 

358 self.handle.chown(uid, gid, path) 

359 

360 def get_image_fs(self): 

361 return self.handle.vfs_type('/dev/sda')