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

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. 

15 

16"""Config Drive v2 helper.""" 

17 

18import os 

19import shutil 

20 

21from oslo_concurrency import processutils 

22from oslo_utils import fileutils 

23from oslo_utils import units 

24 

25import nova.conf 

26from nova import exception 

27from nova.objects import fields 

28import nova.privsep.fs 

29from nova import utils 

30from nova import version 

31 

32CONF = nova.conf.CONF 

33 

34# Config drives are 64mb, if we can't size to the exact size of the data 

35CONFIGDRIVESIZE_BYTES = 64 * units.Mi 

36 

37 

38class ConfigDriveBuilder(object): 

39 """Build config drives, optionally as a context manager.""" 

40 

41 def __init__(self, instance_md=None): 

42 self.imagefile = None 

43 self.mdfiles = [] 

44 

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) 

47 

48 def __enter__(self): 

49 return self 

50 

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

58 

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) 

69 

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

73 

74 def _write_md_files(self, basedir): 

75 for data in self.mdfiles: 

76 self._add_file(basedir, data[0], data[1]) 

77 

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 } 

83 

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) 

99 

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) 

105 

106 nova.privsep.fs.unprivileged_mkfs('vfat', path, label='config-2') 

107 

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 

118 

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

125 

126 finally: 

127 if mounted: 127 ↛ exitline 127 didn't jump to the function exit

128 nova.privsep.fs.umount(mountdir) 

129 

130 def make_drive(self, path): 

131 """Make the config drive. 

132 

133 :param path: the path to place the config drive image at 

134 

135 :raises ProcessExecuteError if a helper process has failed. 

136 """ 

137 with utils.tempdir() as tmpdir: 

138 self._write_md_files(tmpdir) 

139 

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) 

147 

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) 

151 

152 def __repr__(self): 

153 return "<ConfigDriveBuilder: " + str(self.mdfiles) + ">" 

154 

155 

156def required_by(instance): 

157 

158 image_prop = instance.image_meta.properties.get( 

159 "img_config_drive", 

160 fields.ConfigDrivePolicy.OPTIONAL) 

161 

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 ) 

171 

172 

173def update_instance(instance): 

174 """Update the instance config_drive setting if necessary 

175 

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