Coverage for nova/virt/configdrive.py: 89%
76 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 Michael Still and Canonical Inc
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16"""Config Drive v2 helper."""
18import os
19import shutil
21from oslo_concurrency import processutils
22from oslo_utils import fileutils
23from oslo_utils import units
25import nova.conf
26from nova import exception
27from nova.objects import fields
28import nova.privsep.fs
29from nova import utils
30from nova import version
32CONF = nova.conf.CONF
34# Config drives are 64mb, if we can't size to the exact size of the data
35CONFIGDRIVESIZE_BYTES = 64 * units.Mi
38class ConfigDriveBuilder(object):
39 """Build config drives, optionally as a context manager."""
41 def __init__(self, instance_md=None):
42 self.imagefile = None
43 self.mdfiles = []
45 if instance_md is not None: 45 ↛ exitline 45 didn't return from function '__init__' because the condition on line 45 was always true
46 self.add_instance_metadata(instance_md)
48 def __enter__(self):
49 return self
51 def __exit__(self, exctype, excval, exctb):
52 if exctype is not None: 52 ↛ 56line 52 didn't jump to line 56 because the condition on line 52 was never true
53 # NOTE(mikal): this means we're being cleaned up because an
54 # exception was thrown. All bets are off now, and we should not
55 # swallow the exception
56 return False
57 self.cleanup()
59 def _add_file(self, basedir, path, data):
60 filepath = os.path.join(basedir, path)
61 dirname = os.path.dirname(filepath)
62 fileutils.ensure_tree(dirname)
63 with open(filepath, 'wb') as f:
64 # the given data can be either text or bytes. we can only write
65 # bytes into files.
66 if isinstance(data, str):
67 data = data.encode('utf-8')
68 f.write(data)
70 def add_instance_metadata(self, instance_md):
71 for (path, data) in instance_md.metadata_for_config_drive():
72 self.mdfiles.append((path, data))
74 def _write_md_files(self, basedir):
75 for data in self.mdfiles:
76 self._add_file(basedir, data[0], data[1])
78 def _make_iso9660(self, path, tmpdir):
79 publisher = "%(product)s %(version)s" % {
80 'product': version.product_string(),
81 'version': version.version_string_with_package()
82 }
84 processutils.execute(CONF.mkisofs_cmd,
85 '-o', path,
86 '-ldots',
87 '-allow-lowercase',
88 '-allow-multidot',
89 '-l',
90 '-publisher',
91 publisher,
92 '-quiet',
93 '-J',
94 '-r',
95 '-V', 'config-2',
96 tmpdir,
97 attempts=1,
98 run_as_root=False)
100 def _make_vfat(self, path, tmpdir):
101 # NOTE(mikal): This is a little horrible, but I couldn't find an
102 # equivalent to genisoimage for vfat filesystems.
103 with open(path, 'wb') as f:
104 f.truncate(CONFIGDRIVESIZE_BYTES)
106 nova.privsep.fs.unprivileged_mkfs('vfat', path, label='config-2')
108 with utils.tempdir() as mountdir:
109 mounted = False
110 try:
111 _, err = nova.privsep.fs.mount(
112 None, path, mountdir,
113 ['-o', 'loop,uid=%d,gid=%d' % (os.getuid(), os.getgid())])
114 if err: 114 ↛ 115line 114 didn't jump to line 115 because the condition on line 114 was never true
115 raise exception.ConfigDriveMountFailed(operation='mount',
116 error=err)
117 mounted = True
119 # NOTE(mikal): I can't just use shutils.copytree here,
120 # because the destination directory already
121 # exists. This is annoying.
122 for ent in os.listdir(tmpdir):
123 shutil.copytree(os.path.join(tmpdir, ent),
124 os.path.join(mountdir, ent))
126 finally:
127 if mounted: 127 ↛ exitline 127 didn't jump to the function exit
128 nova.privsep.fs.umount(mountdir)
130 def make_drive(self, path):
131 """Make the config drive.
133 :param path: the path to place the config drive image at
135 :raises ProcessExecuteError if a helper process has failed.
136 """
137 with utils.tempdir() as tmpdir:
138 self._write_md_files(tmpdir)
140 if CONF.config_drive_format == 'iso9660':
141 self._make_iso9660(path, tmpdir)
142 elif CONF.config_drive_format == 'vfat': 142 ↛ 145line 142 didn't jump to line 145 because the condition on line 142 was always true
143 self._make_vfat(path, tmpdir)
144 else:
145 raise exception.ConfigDriveUnknownFormat(
146 format=CONF.config_drive_format)
148 def cleanup(self):
149 if self.imagefile: 149 ↛ 150line 149 didn't jump to line 150 because the condition on line 149 was never true
150 fileutils.delete_if_exists(self.imagefile)
152 def __repr__(self):
153 return "<ConfigDriveBuilder: " + str(self.mdfiles) + ">"
156def required_by(instance):
158 image_prop = instance.image_meta.properties.get(
159 "img_config_drive",
160 fields.ConfigDrivePolicy.OPTIONAL)
162 # NOTE(pandatt): Option CONF.force_config_drive only applies to newly
163 # being-built VMs. And already launched VMs shouldn't be forced a config
164 # drive, because they may have been cloud-inited via metadata service, and
165 # do not need and have any config drive device. The `launched_at` property
166 # is an apparent flag to tell VMs being built from launched ones.
167 return (instance.config_drive or
168 (CONF.force_config_drive and not instance.launched_at) or
169 image_prop == fields.ConfigDrivePolicy.MANDATORY
170 )
173def update_instance(instance):
174 """Update the instance config_drive setting if necessary
176 The image or configuration file settings may override the default instance
177 setting. In this case the instance needs to mirror the actual
178 virtual machine configuration.
179 """
180 if not instance.config_drive and required_by(instance):
181 instance.config_drive = True