Coverage for nova/virt/disk/vfs/guestfs.py: 81%
196 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 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.
15import os
17from eventlet import tpool
18from oslo_log import log as logging
19from oslo_utils import importutils
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
28LOG = logging.getLogger(__name__)
30guestfs = None
31forceTCG = False
33CONF = nova.conf.CONF
36def force_tcg(force=True):
37 """Prevent libguestfs trying to use KVM acceleration
39 It is a good idea to call this if it is known that
40 KVM is not desired, even if technically available.
41 """
43 global forceTCG
44 forceTCG = force
47class VFSGuestFS(vfs.VFS):
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 """
55 def __init__(self, image, partition=None):
56 """Create a new local VFS instance
58 :param image: instance of nova.virt.image.model.Image
59 :param partition: the partition number of access
60 """
62 super(VFSGuestFS, self).__init__(image, partition)
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)
72 self.handle = None
73 self.mount = False
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)
97 return self
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})
113 events = (guestfs.EVENT_APPLIANCE | guestfs.EVENT_LIBRARY |
114 guestfs.EVENT_WARNING | guestfs.EVENT_TRACE)
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)
120 def setup_os(self):
121 if self.partition == -1:
122 self.setup_os_inspect()
123 else:
124 self.setup_os_static()
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)})
130 if self.partition:
131 self.handle.mount_options("", "/dev/sda%d" % self.partition, "/")
132 else:
133 self.handle.mount_options("", "/dev/sda", "/")
135 def setup_os_inspect(self):
136 LOG.debug("Inspecting guest OS image %s", self.image)
137 roots = self.handle.inspect_os()
139 if len(roots) == 0:
140 raise exception.NovaException(_("No operating system found in %s")
141 % self.image)
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)
149 self.setup_os_root(roots[0])
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)
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})
160 # the root directory must be mounted first
161 mounts.sort(key=lambda mount: mount[0])
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)
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
196 if CONF.guestfs.debug:
197 self.configure_debug()
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
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__)
236 self.handle.launch()
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
257 def teardown(self):
258 LOG.debug("Tearing down appliance")
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)
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)
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
286 @staticmethod
287 def _canonicalize_path(path):
288 if path[0] != '/':
289 return '/' + path
290 return path
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)
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)
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)
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
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
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)
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
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)
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)
360 def get_image_fs(self):
361 return self.handle.vfs_type('/dev/sda')