Coverage for nova/privsep/fs.py: 91%

182 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-04-24 11:16 +0000

1# Copyright 2016 Red Hat, Inc 

2# Copyright 2017 Rackspace Australia 

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

17Helpers for filesystem related routines. 

18""" 

19 

20import hashlib 

21 

22from oslo_concurrency import processutils 

23from oslo_log import log as logging 

24 

25import nova.privsep 

26 

27 

28LOG = logging.getLogger(__name__) 

29 

30 

31@nova.privsep.sys_admin_pctxt.entrypoint 

32def mount(fstype, device, mountpoint, options): 

33 mount_cmd = ['mount'] 

34 if fstype: 

35 mount_cmd.extend(['-t', fstype]) 

36 if options is not None: 

37 mount_cmd.extend(options) 

38 mount_cmd.extend([device, mountpoint]) 

39 return processutils.execute(*mount_cmd) 

40 

41 

42@nova.privsep.sys_admin_pctxt.entrypoint 

43def umount(mountpoint): 

44 processutils.execute('umount', mountpoint, attempts=3, delay_on_retry=True) 

45 

46 

47@nova.privsep.sys_admin_pctxt.entrypoint 

48def lvcreate(size, lv, vg, preallocated=None): 

49 cmd = ['lvcreate'] 

50 if not preallocated: 

51 cmd.extend(['-L', '%db' % size]) 

52 else: 

53 cmd.extend(['-L', '%db' % preallocated, 

54 '--virtualsize', '%db' % size]) 

55 cmd.extend(['-n', lv, vg]) 

56 processutils.execute(*cmd, attempts=3) 

57 

58 

59@nova.privsep.sys_admin_pctxt.entrypoint 

60def vginfo(vg): 

61 # NOTE(gibi): We see intermittent faults querying volume groups failing 

62 # with error code -11, hence the retry. See bug 1931710 

63 return processutils.execute( 

64 'vgs', '--noheadings', '--nosuffix', 

65 '--separator', '|', '--units', 'b', 

66 '-o', 'vg_size,vg_free', vg, 

67 attempts=3, delay_on_retry=True, 

68 ) 

69 

70 

71@nova.privsep.sys_admin_pctxt.entrypoint 

72def lvlist(vg): 

73 return processutils.execute( 

74 'lvs', '--noheadings', '-o', 'lv_name', vg, 

75 attempts=3, delay_on_retry=True) 

76 

77 

78@nova.privsep.sys_admin_pctxt.entrypoint 

79def lvinfo(path): 

80 return processutils.execute('lvs', '-o', 'vg_all,lv_all', 

81 '--separator', '|', path) 

82 

83 

84@nova.privsep.sys_admin_pctxt.entrypoint 

85def lvremove(path): 

86 processutils.execute('lvremove', '-f', path, attempts=3) 

87 

88 

89@nova.privsep.sys_admin_pctxt.entrypoint 

90def blockdev_size(path): 

91 return processutils.execute('blockdev', '--getsize64', path) 

92 

93 

94@nova.privsep.sys_admin_pctxt.entrypoint 

95def blockdev_flush(path): 

96 return processutils.execute('blockdev', '--flushbufs', path) 

97 

98 

99@nova.privsep.sys_admin_pctxt.entrypoint 

100def clear(path, volume_size, shred=False): 

101 cmd = ['shred'] 

102 if shred: 

103 cmd.extend(['-n3']) 

104 else: 

105 cmd.extend(['-n0', '-z']) 

106 cmd.extend(['-s%d' % volume_size, path]) 

107 processutils.execute(*cmd) 

108 

109 

110@nova.privsep.sys_admin_pctxt.entrypoint 

111def loopsetup(path): 

112 return processutils.execute('losetup', '--find', '--show', path) 

113 

114 

115@nova.privsep.sys_admin_pctxt.entrypoint 

116def loopremove(device): 

117 return processutils.execute('losetup', '--detach', device, attempts=3) 

118 

119 

120@nova.privsep.sys_admin_pctxt.entrypoint 

121def nbd_connect(device, image): 

122 return processutils.execute('qemu-nbd', '-c', device, image) 

123 

124 

125@nova.privsep.sys_admin_pctxt.entrypoint 

126def nbd_disconnect(device): 

127 return processutils.execute('qemu-nbd', '-d', device) 

128 

129 

130@nova.privsep.sys_admin_pctxt.entrypoint 

131def create_device_maps(device): 

132 return processutils.execute('kpartx', '-a', device) 

133 

134 

135@nova.privsep.sys_admin_pctxt.entrypoint 

136def remove_device_maps(device): 

137 return processutils.execute('kpartx', '-d', device) 

138 

139 

140@nova.privsep.sys_admin_pctxt.entrypoint 

141def e2fsck(image, flags='-fp'): 

142 unprivileged_e2fsck(image, flags=flags) 

143 

144 

145# NOTE(mikal): this method is deliberately not wrapped in a privsep 

146# entrypoint. This is not for unit testing, there are some callers who do 

147# not require elevated permissions when calling this. 

148def unprivileged_e2fsck(image, flags='-fp'): 

149 processutils.execute('e2fsck', flags, image, check_exit_code=[0, 1, 2]) 

150 

151 

152@nova.privsep.sys_admin_pctxt.entrypoint 

153def resize2fs(image, check_exit_code, size=None): 

154 unprivileged_resize2fs(image, check_exit_code=check_exit_code, size=size) 

155 

156 

157# NOTE(mikal): this method is deliberately not wrapped in a privsep 

158# entrypoint. This is not for unit testing, there are some callers who do 

159# not require elevated permissions when calling this. 

160def unprivileged_resize2fs(image, check_exit_code, size=None): 

161 if size: 

162 cmd = ['resize2fs', image, size] 

163 else: 

164 cmd = ['resize2fs', image] 

165 

166 processutils.execute(*cmd, check_exit_code=check_exit_code) 

167 

168 

169@nova.privsep.sys_admin_pctxt.entrypoint 

170def create_partition_table(device, style, check_exit_code=True): 

171 processutils.execute('parted', '--script', device, 'mklabel', style, 

172 check_exit_code=check_exit_code) 

173 

174 

175@nova.privsep.sys_admin_pctxt.entrypoint 

176def create_partition(device, style, start, end, check_exit_code=True): 

177 processutils.execute('parted', '--script', device, '--', 

178 'mkpart', style, start, end, 

179 check_exit_code=check_exit_code) 

180 

181 

182@nova.privsep.sys_admin_pctxt.entrypoint 

183def list_partitions(device): 

184 return unprivileged_list_partitions(device) 

185 

186 

187# NOTE(mikal): this method is deliberately not wrapped in a privsep 

188# entrypoint. This is not for unit testing, there are some callers who do 

189# not require elevated permissions when calling this. 

190def unprivileged_list_partitions(device): 

191 """Return partition information (num, size, type) for a device.""" 

192 

193 out, _err = processutils.execute('parted', '--script', '--machine', 

194 device, 'unit s', 'print') 

195 lines = [line for line in out.split('\n') if line] 

196 partitions = [] 

197 

198 LOG.debug('Partitions:') 

199 for line in lines[2:]: 

200 line = line.rstrip(';') 

201 num, start, end, size, fstype, name, flags = line.split(':') 

202 num = int(num) 

203 start = int(start.rstrip('s')) 

204 end = int(end.rstrip('s')) 

205 size = int(size.rstrip('s')) 

206 LOG.debug(' %(num)s: %(fstype)s %(size)d sectors', 

207 {'num': num, 'fstype': fstype, 'size': size}) 

208 partitions.append((num, start, size, fstype, name, flags)) 

209 

210 return partitions 

211 

212 

213@nova.privsep.sys_admin_pctxt.entrypoint 

214def resize_partition(device, start, end, bootable): 

215 processutils.execute('parted', '--script', device, 'rm', '1') 

216 processutils.execute('parted', '--script', device, 'mkpart', 

217 'primary', '%ds' % start, '%ds' % end) 

218 if bootable: 218 ↛ exitline 218 didn't return from function 'resize_partition' because the condition on line 218 was always true

219 processutils.execute('parted', '--script', device, 

220 'set', '1', 'boot', 'on') 

221 

222 

223@nova.privsep.sys_admin_pctxt.entrypoint 

224def ext_journal_disable(device): 

225 processutils.execute('tune2fs', '-O ^has_journal', device) 

226 

227 

228@nova.privsep.sys_admin_pctxt.entrypoint 

229def ext_journal_enable(device): 

230 processutils.execute('tune2fs', '-j', device) 

231 

232 

233# NOTE(mikal): nova allows deployers to configure the command line which is 

234# used to create a filesystem of a given type. This is frankly a little bit 

235# weird, but its also historical and probably should be in some sort of 

236# museum. So, we do that thing here, but it requires a funny dance in order 

237# to load that configuration at startup. 

238 

239# NOTE(mikal): I really feel like this whole thing should be deprecated, I 

240# just don't think its a great idea to let people specify a command in a 

241# configuration option to run as root. 

242 

243_MKFS_COMMAND = {} 

244_DEFAULT_MKFS_COMMAND = None 

245 

246FS_FORMAT_EXT2 = "ext2" 

247FS_FORMAT_EXT3 = "ext3" 

248FS_FORMAT_EXT4 = "ext4" 

249FS_FORMAT_XFS = "xfs" 

250FS_FORMAT_NTFS = "ntfs" 

251FS_FORMAT_VFAT = "vfat" 

252 

253SUPPORTED_FS_TO_EXTEND = ( 

254 FS_FORMAT_EXT2, 

255 FS_FORMAT_EXT3, 

256 FS_FORMAT_EXT4) 

257 

258_DEFAULT_FILE_SYSTEM = FS_FORMAT_VFAT 

259_DEFAULT_FS_BY_OSTYPE = {'linux': FS_FORMAT_EXT4, 

260 'windows': FS_FORMAT_NTFS} 

261 

262 

263def load_mkfs_command(os_type, command): 

264 global _MKFS_COMMAND 

265 global _DEFAULT_MKFS_COMMAND 

266 

267 _MKFS_COMMAND[os_type] = command 

268 if os_type == 'default': 

269 _DEFAULT_MKFS_COMMAND = command 

270 

271 

272def get_fs_type_for_os_type(os_type): 

273 global _MKFS_COMMAND 

274 

275 return os_type if _MKFS_COMMAND.get(os_type) else 'default' 

276 

277 

278# NOTE(mikal): this method needs to be duplicated from utils because privsep 

279# can't depend on code outside the privsep directory. 

280def _get_hash_str(base_str): 

281 """Returns string that represents MD5 hash of base_str (in hex format). 

282 

283 If base_str is a Unicode string, encode it to UTF-8. 

284 """ 

285 if isinstance(base_str, str): 285 ↛ 287line 285 didn't jump to line 287 because the condition on line 285 was always true

286 base_str = base_str.encode('utf-8') 

287 return hashlib.md5(base_str, usedforsecurity=False).hexdigest() 

288 

289 

290def get_file_extension_for_os_type(os_type, default_ephemeral_format, 

291 specified_fs=None): 

292 global _MKFS_COMMAND 

293 global _DEFAULT_MKFS_COMMAND 

294 

295 mkfs_command = _MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) 

296 if mkfs_command: 

297 extension = mkfs_command 

298 else: 

299 if not specified_fs: 299 ↛ 304line 299 didn't jump to line 304 because the condition on line 299 was always true

300 specified_fs = default_ephemeral_format 

301 if not specified_fs: 

302 specified_fs = _DEFAULT_FS_BY_OSTYPE.get(os_type, 

303 _DEFAULT_FILE_SYSTEM) 

304 extension = specified_fs 

305 return _get_hash_str(extension)[:7] 

306 

307 

308@nova.privsep.sys_admin_pctxt.entrypoint 

309def mkfs(fs, path, label=None): 

310 unprivileged_mkfs(fs, path, label=None) 

311 

312 

313# NOTE(mikal): this method is deliberately not wrapped in a privsep 

314# entrypoint. This is not for unit testing, there are some callers who do 

315# not require elevated permissions when calling this. 

316def unprivileged_mkfs(fs, path, label=None): 

317 """Format a file or block device 

318 

319 :param fs: Filesystem type (examples include 'swap', 'ext3', 'ext4' 

320 'btrfs', etc.) 

321 :param path: Path to file or block device to format 

322 :param label: Volume label to use 

323 """ 

324 if fs == 'swap': 

325 args = ['mkswap'] 

326 else: 

327 args = ['mkfs', '-t', fs] 

328 # add -F to force no interactive execute on non-block device. 

329 if fs in ('ext3', 'ext4', 'ntfs'): 

330 args.extend(['-F']) 

331 if label: 

332 if fs in ('msdos', 'vfat'): 

333 label_opt = '-n' 

334 else: 

335 label_opt = '-L' 

336 args.extend([label_opt, label]) 

337 args.append(path) 

338 processutils.execute(*args) 

339 

340 

341@nova.privsep.sys_admin_pctxt.entrypoint 

342def _inner_configurable_mkfs(os_type, fs_label, target): 

343 mkfs_command = (_MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) or 

344 '') % {'fs_label': fs_label, 'target': target} 

345 processutils.execute(*mkfs_command.split()) 

346 

347 

348# NOTE(mikal): this method is deliberately not wrapped in a privsep entrypoint 

349def configurable_mkfs(os_type, fs_label, target, run_as_root, 

350 default_ephemeral_format, specified_fs=None): 

351 # Format a file or block device using a user provided command for each 

352 # os type. If user has not provided any configuration, format type will 

353 # be used according to a default_ephemeral_format configuration or a 

354 # system default. 

355 global _MKFS_COMMAND 

356 global _DEFAULT_MKFS_COMMAND 

357 

358 mkfs_command = (_MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) or 

359 '') % {'fs_label': fs_label, 'target': target} 

360 if mkfs_command: 360 ↛ 361line 360 didn't jump to line 361 because the condition on line 360 was never true

361 if run_as_root: 

362 _inner_configurable_mkfs(os_type, fs_label, target) 

363 else: 

364 processutils.execute(*mkfs_command.split()) 

365 

366 else: 

367 if not specified_fs: 

368 specified_fs = default_ephemeral_format 

369 if not specified_fs: 

370 specified_fs = _DEFAULT_FS_BY_OSTYPE.get(os_type, 

371 _DEFAULT_FILE_SYSTEM) 

372 

373 if run_as_root: 373 ↛ 376line 373 didn't jump to line 376 because the condition on line 373 was always true

374 mkfs(specified_fs, target, fs_label) 

375 else: 

376 unprivileged_mkfs(specified_fs, target, fs_label)