Coverage for nova/virt/disk/api.py: 79%

291 statements  

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

1# Copyright 2010 United States Government as represented by the 

2# Administrator of the National Aeronautics and Space Administration. 

3# 

4# Copyright 2011, Piston Cloud Computing, Inc. 

5# 

6# All Rights Reserved. 

7# 

8# Licensed under the Apache License, Version 2.0 (the "License"); you may 

9# not use this file except in compliance with the License. You may obtain 

10# a copy of the License at 

11# 

12# http://www.apache.org/licenses/LICENSE-2.0 

13# 

14# Unless required by applicable law or agreed to in writing, software 

15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

17# License for the specific language governing permissions and limitations 

18# under the License. 

19""" 

20Utility methods to resize, repartition, and modify disk images. 

21 

22Includes injection of SSH PGP keys into authorized_keys file. 

23 

24""" 

25 

26import os 

27import random 

28import tempfile 

29 

30from oslo_concurrency import processutils 

31from oslo_log import log as logging 

32from oslo_serialization import jsonutils 

33from oslo_utils import secretutils 

34 

35import nova.conf 

36from nova import exception 

37from nova.i18n import _ 

38import nova.privsep.fs 

39import nova.privsep.libvirt 

40from nova.virt.disk.mount import api as mount 

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

42from nova.virt.image import model as imgmodel 

43from nova.virt import images 

44 

45 

46LOG = logging.getLogger(__name__) 

47 

48CONF = nova.conf.CONF 

49 

50 

51# NOTE(mikal): Here as a transition step 

52SUPPORTED_FS_TO_EXTEND = nova.privsep.fs.SUPPORTED_FS_TO_EXTEND 

53 

54 

55for s in CONF.virt_mkfs: 55 ↛ 58line 55 didn't jump to line 58 because the loop on line 55 never started

56 # NOTE(yamahata): mkfs command may includes '=' for its options. 

57 # So item.partition('=') doesn't work here 

58 os_type, mkfs_command = s.split('=', 1) 

59 if os_type: 

60 nova.privsep.fs.load_mkfs_command(os_type, mkfs_command) 

61 

62 

63def mkfs(os_type, fs_label, target, run_as_root=True, specified_fs=None): 

64 nova.privsep.fs.configurable_mkfs( 

65 os_type, fs_label, target, run_as_root, 

66 CONF.default_ephemeral_format, specified_fs) 

67 

68 

69def resize2fs(image, check_exit_code=False, run_as_root=False): 

70 # NOTE(mikal): note that the check_exit_code kwarg here only refers to 

71 # resize2fs, not the precursor e2fsck. Yes, I agree it's confusing. 

72 try: 

73 if run_as_root: 

74 nova.privsep.fs.e2fsck(image) 

75 else: 

76 nova.privsep.fs.unprivileged_e2fsck(image) 

77 

78 except processutils.ProcessExecutionError as exc: 

79 LOG.debug("Checking the file system with e2fsck has failed, " 

80 "the resize will be aborted. (%s)", exc) 

81 

82 else: 

83 if run_as_root: 

84 nova.privsep.fs.resize2fs(image, check_exit_code) 

85 else: 

86 nova.privsep.fs.unprivileged_resize2fs(image, check_exit_code) 

87 

88 

89def get_disk_info(path): 

90 """Get QEMU info of a disk image 

91 

92 :param path: Path to the disk image 

93 :returns: oslo_utils.imageutils.QemuImgInfo object for the image. 

94 """ 

95 return images.qemu_img_info(path) 

96 

97 

98def get_disk_size(path): 

99 """Get the (virtual) size of a disk image 

100 

101 :param path: Path to the disk image 

102 :returns: Size (in bytes) of the given disk image as it would be seen 

103 by a virtual machine. 

104 """ 

105 return images.qemu_img_info(path).virtual_size 

106 

107 

108def extend(image, size): 

109 """Increase image to size. 

110 

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

112 :param size: image size in bytes 

113 """ 

114 

115 # Currently can only resize FS in local images 

116 if not isinstance(image, imgmodel.LocalImage): 116 ↛ 117line 116 didn't jump to line 117 because the condition on line 116 was never true

117 return 

118 

119 if not can_resize_image(image.path, size): 119 ↛ 120line 119 didn't jump to line 120 because the condition on line 119 was never true

120 return 

121 

122 if (image.format == imgmodel.FORMAT_PLOOP): 

123 nova.privsep.libvirt.ploop_resize(image.path, size) 

124 return 

125 

126 processutils.execute('qemu-img', 'resize', image.path, size) 

127 

128 if (image.format != imgmodel.FORMAT_RAW and 

129 not CONF.resize_fs_using_block_device): 

130 return 

131 

132 # if we can't access the filesystem, we can't do anything more 

133 if not is_image_extendable(image): 133 ↛ 134line 133 didn't jump to line 134 because the condition on line 133 was never true

134 return 

135 

136 def safe_resize2fs(dev, run_as_root=False, finally_call=lambda: None): 

137 try: 

138 resize2fs(dev, run_as_root=run_as_root, check_exit_code=[0]) 

139 except processutils.ProcessExecutionError as exc: 

140 LOG.debug("Resizing the file system with resize2fs " 

141 "has failed with error: %s", exc) 

142 finally: 

143 finally_call() 

144 

145 # NOTE(vish): attempts to resize filesystem 

146 if image.format != imgmodel.FORMAT_RAW: 

147 # in case of non-raw disks we can't just resize the image, but 

148 # rather the mounted device instead 

149 mounter = mount.Mount.instance_for_format( 

150 image, None, None) 

151 if mounter.get_dev(): 151 ↛ exitline 151 didn't return from function 'extend' because the condition on line 151 was always true

152 safe_resize2fs(mounter.device, 

153 run_as_root=True, 

154 finally_call=mounter.unget_dev) 

155 else: 

156 safe_resize2fs(image.path) 

157 

158 

159def can_resize_image(image, size): 

160 """Check whether we can resize the container image file. 

161 :param image: path to local image file 

162 :param size: the image size in bytes 

163 """ 

164 LOG.debug('Checking if we can resize image %(image)s. ' 

165 'size=%(size)s', {'image': image, 'size': size}) 

166 

167 # Check that we're increasing the size 

168 virt_size = get_disk_size(image) 

169 if virt_size >= size: 

170 LOG.debug('Cannot resize image %s to a smaller size.', 

171 image) 

172 return False 

173 return True 

174 

175 

176def is_image_extendable(image): 

177 """Check whether we can extend the image.""" 

178 LOG.debug('Checking if we can extend filesystem inside %(image)s.', 

179 {'image': image}) 

180 

181 # For anything except a local raw file we must 

182 # go via the VFS layer 

183 if (not isinstance(image, imgmodel.LocalImage) or 

184 image.format != imgmodel.FORMAT_RAW): 

185 fs = None 

186 try: 

187 fs = vfs.VFS.instance_for_image(image, None) 

188 fs.setup(mount=False) 

189 if fs.get_image_fs() in SUPPORTED_FS_TO_EXTEND: 189 ↛ 190line 189 didn't jump to line 190 because the condition on line 189 was never true

190 return True 

191 except exception.NovaException as e: 

192 # FIXME(sahid): At this step we probably want to break the 

193 # process if something wrong happens however our CI 

194 # provides a bad configuration for libguestfs reported in 

195 # the bug lp#1413142. When resolved we should remove this 

196 # except to let the error to be propagated. 

197 LOG.warning('Unable to mount image %(image)s with ' 

198 'error %(error)s. Cannot resize.', 

199 {'image': image, 'error': e}) 

200 finally: 

201 if fs is not None: 201 ↛ 204line 201 didn't jump to line 204 because the condition on line 201 was always true

202 fs.teardown() 

203 

204 return False 

205 else: 

206 # For raw, we can directly inspect the file system 

207 try: 

208 processutils.execute('e2label', image.path) 

209 except processutils.ProcessExecutionError as e: 

210 LOG.debug('Unable to determine label for image %(image)s with ' 

211 'error %(error)s. Cannot resize.', 

212 {'image': image, 

213 'error': e}) 

214 return False 

215 

216 return True 

217 

218 

219class _DiskImage(object): 

220 """Provide operations on a disk image file.""" 

221 

222 tmp_prefix = 'openstack-disk-mount-tmp' 

223 

224 def __init__(self, image, partition=None, mount_dir=None): 

225 """Create a new _DiskImage object instance 

226 

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

228 :param partition: the partition number within the image 

229 :param mount_dir: the directory to mount the image on 

230 """ 

231 

232 # These passed to each mounter 

233 self.partition = partition 

234 self.mount_dir = mount_dir 

235 self.image = image 

236 

237 # Internal 

238 self._mkdir = False 

239 self._mounter = None 

240 self._errors = [] 

241 

242 if mount_dir: 242 ↛ exitline 242 didn't return from function '__init__' because the condition on line 242 was always true

243 device = self._device_for_path(mount_dir) 

244 if device: 

245 self._reset(device) 

246 else: 

247 LOG.debug('No device found for path: %s', mount_dir) 

248 

249 @staticmethod 

250 def _device_for_path(path): 

251 device = None 

252 path = os.path.realpath(path) 

253 with open("/proc/mounts", 'r') as ifp: 

254 for line in ifp: 

255 fields = line.split() 

256 if fields[1] == path: 256 ↛ 257line 256 didn't jump to line 257 because the condition on line 256 was never true

257 device = fields[0] 

258 break 

259 return device 

260 

261 def _reset(self, device): 

262 """Reset internal state for a previously mounted directory.""" 

263 self._mounter = mount.Mount.instance_for_device(self.image, 

264 self.mount_dir, 

265 self.partition, 

266 device) 

267 

268 mount_name = os.path.basename(self.mount_dir or '') 

269 self._mkdir = mount_name.startswith(self.tmp_prefix) 

270 

271 @property 

272 def errors(self): 

273 """Return the collated errors from all operations.""" 

274 return '\n--\n'.join([''] + self._errors) 

275 

276 def mount(self): 

277 """Mount a disk image, using the object attributes. 

278 

279 The first supported means provided by the mount classes is used. 

280 

281 True, or False is returned and the 'errors' attribute 

282 contains any diagnostics. 

283 """ 

284 if self._mounter: 284 ↛ 285line 284 didn't jump to line 285 because the condition on line 284 was never true

285 raise exception.NovaException(_('image already mounted')) 

286 

287 if not self.mount_dir: 287 ↛ 288line 287 didn't jump to line 288 because the condition on line 287 was never true

288 self.mount_dir = tempfile.mkdtemp(prefix=self.tmp_prefix) 

289 self._mkdir = True 

290 

291 mounter = mount.Mount.instance_for_format(self.image, 

292 self.mount_dir, 

293 self.partition) 

294 

295 if mounter.do_mount(): 295 ↛ 299line 295 didn't jump to line 299 because the condition on line 295 was always true

296 self._mounter = mounter 

297 return self._mounter.device 

298 else: 

299 LOG.debug(mounter.error) 

300 self._errors.append(mounter.error) 

301 return None 

302 

303 def umount(self): 

304 """Umount a mount point from the filesystem.""" 

305 if self._mounter: 

306 self._mounter.do_umount() 

307 self._mounter = None 

308 

309 def teardown(self): 

310 """Remove a disk image from the file system.""" 

311 try: 

312 if self._mounter: 

313 self._mounter.do_teardown() 

314 self._mounter = None 

315 finally: 

316 if self._mkdir: 316 ↛ 317line 316 didn't jump to line 317 because the condition on line 316 was never true

317 os.rmdir(self.mount_dir) 

318 

319 

320# Public module functions 

321 

322def inject_data(image, key=None, net=None, metadata=None, admin_password=None, 

323 files=None, partition=None, mandatory=()): 

324 """Inject the specified items into a disk image. 

325 

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

327 :param key: the SSH public key to inject 

328 :param net: the network configuration to inject 

329 :param metadata: the user metadata to inject 

330 :param admin_password: the root password to set 

331 :param files: the files to copy into the image 

332 :param partition: the partition number to access 

333 :param mandatory: the list of parameters which must not fail to inject 

334 

335 If an item name is not specified in the MANDATORY iterable, then a warning 

336 is logged on failure to inject that item, rather than raising an exception. 

337 

338 it will mount the image as a fully partitioned disk and attempt to inject 

339 into the specified partition number. 

340 

341 If PARTITION is not specified the image is mounted as a single partition. 

342 

343 Returns True if all requested operations completed without issue. 

344 Raises an exception if a mandatory item can't be injected. 

345 """ 

346 items = {'image': image, 'key': key, 'net': net, 'metadata': metadata, 

347 'files': files, 'partition': partition} 

348 LOG.debug("Inject data image=%(image)s key=%(key)s net=%(net)s " 

349 "metadata=%(metadata)s admin_password=<SANITIZED> " 

350 "files=%(files)s partition=%(partition)s", items) 

351 try: 

352 fs = vfs.VFS.instance_for_image(image, partition) 

353 fs.setup() 

354 except Exception as e: 

355 # If a mandatory item is passed to this function, 

356 # then reraise the exception to indicate the error. 

357 for inject in mandatory: 357 ↛ 358line 357 didn't jump to line 358 because the loop on line 357 never started

358 inject_val = items[inject] 

359 if inject_val: 

360 raise 

361 LOG.warning('Ignoring error injecting data into image %(image)s ' 

362 '(%(e)s)', {'image': image, 'e': e}) 

363 return False 

364 

365 try: 

366 return inject_data_into_fs(fs, key, net, metadata, admin_password, 

367 files, mandatory) 

368 finally: 

369 fs.teardown() 

370 

371 

372def setup_container(image, container_dir): 

373 """Setup the LXC container. 

374 

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

376 :param container_dir: directory to mount the image at 

377 

378 It will mount the loopback image to the container directory in order 

379 to create the root filesystem for the container. 

380 

381 Returns path of image device which is mounted to the container directory. 

382 """ 

383 img = _DiskImage(image=image, mount_dir=container_dir) 

384 dev = img.mount() 

385 if dev is None: 385 ↛ 386line 385 didn't jump to line 386 because the condition on line 385 was never true

386 LOG.error("Failed to mount container filesystem '%(image)s' " 

387 "on '%(target)s': %(errors)s", 

388 {"image": img, "target": container_dir, 

389 "errors": img.errors}) 

390 raise exception.NovaException(img.errors) 

391 

392 return dev 

393 

394 

395def teardown_container(container_dir, container_root_device=None): 

396 """Teardown the container rootfs mounting once it is spawned. 

397 

398 It will umount the container that is mounted, 

399 and delete any linked devices. 

400 """ 

401 try: 

402 img = _DiskImage(image=None, mount_dir=container_dir) 

403 img.teardown() 

404 

405 # Make sure container_root_device is released when teardown container. 

406 if container_root_device: 

407 if 'loop' in container_root_device: 

408 LOG.debug("Release loop device %s", container_root_device) 

409 nova.privsep.fs.loopremove(container_root_device) 

410 elif 'nbd' in container_root_device: 410 ↛ 414line 410 didn't jump to line 414 because the condition on line 410 was always true

411 LOG.debug('Release nbd device %s', container_root_device) 

412 nova.privsep.fs.nbd_disconnect(container_root_device) 

413 else: 

414 LOG.debug('No release necessary for block device %s', 

415 container_root_device) 

416 except Exception: 

417 LOG.exception('Failed to teardown container filesystem') 

418 

419 

420def clean_lxc_namespace(container_dir): 

421 """Clean up the container namespace rootfs mounting one spawned. 

422 

423 It will umount the mounted names that are mounted 

424 but leave the linked devices alone. 

425 """ 

426 try: 

427 img = _DiskImage(image=None, mount_dir=container_dir) 

428 img.umount() 

429 except Exception: 

430 LOG.exception('Failed to umount container filesystem') 

431 

432 

433def inject_data_into_fs(fs, key, net, metadata, admin_password, files, 

434 mandatory=()): 

435 """Injects data into a filesystem already mounted by the caller. 

436 Virt connections can call this directly if they mount their fs 

437 in a different way to inject_data. 

438 

439 If an item name is not specified in the MANDATORY iterable, then a warning 

440 is logged on failure to inject that item, rather than raising an exception. 

441 

442 Returns True if all requested operations completed without issue. 

443 Raises an exception if a mandatory item can't be injected. 

444 """ 

445 items = {'key': key, 'net': net, 'metadata': metadata, 

446 'admin_password': admin_password, 'files': files} 

447 functions = { 

448 'key': _inject_key_into_fs, 

449 'net': _inject_net_into_fs, 

450 'metadata': _inject_metadata_into_fs, 

451 'admin_password': _inject_admin_password_into_fs, 

452 'files': _inject_files_into_fs, 

453 } 

454 status = True 

455 for inject, inject_val in items.items(): 

456 if inject_val: 

457 try: 

458 inject_func = functions[inject] 

459 inject_func(inject_val, fs) 

460 except Exception as e: 

461 if inject in mandatory: 

462 raise 

463 LOG.warning('Ignoring error injecting %(inject)s into ' 

464 'image (%(e)s)', {'inject': inject, 'e': e}) 

465 status = False 

466 return status 

467 

468 

469def _inject_files_into_fs(files, fs): 

470 for (path, contents) in files: 

471 # NOTE(wangpan): Ensure the parent dir of injecting file exists 

472 parent_dir = os.path.dirname(path) 

473 if (len(parent_dir) > 0 and parent_dir != "/" and 

474 not fs.has_file(parent_dir)): 

475 fs.make_path(parent_dir) 

476 fs.set_ownership(parent_dir, "root", "root") 

477 fs.set_permissions(parent_dir, 0o744) 

478 _inject_file_into_fs(fs, path, contents) 

479 

480 

481def _inject_file_into_fs(fs, path, contents, append=False): 

482 LOG.debug("Inject file fs=%(fs)s path=%(path)s append=%(append)s", 

483 {'fs': fs, 'path': path, 'append': append}) 

484 if append: 

485 fs.append_file(path, contents) 

486 else: 

487 fs.replace_file(path, contents) 

488 

489 

490def _inject_metadata_into_fs(metadata, fs): 

491 LOG.debug("Inject metadata fs=%(fs)s metadata=%(metadata)s", 

492 {'fs': fs, 'metadata': metadata}) 

493 _inject_file_into_fs(fs, 'meta.js', jsonutils.dumps(metadata)) 

494 

495 

496def _setup_selinux_for_keys(fs, sshdir): 

497 """Get selinux guests to ensure correct context on injected keys.""" 

498 

499 if not fs.has_file(os.path.join("etc", "selinux")): 

500 return 

501 

502 rclocal = os.path.join('etc', 'rc.local') 

503 rc_d = os.path.join('etc', 'rc.d') 

504 

505 if not fs.has_file(rclocal) and fs.has_file(rc_d): 505 ↛ 511line 505 didn't jump to line 511 because the condition on line 505 was always true

506 rclocal = os.path.join(rc_d, 'rc.local') 

507 

508 # Note some systems end rc.local with "exit 0" 

509 # and so to append there you'd need something like: 

510 # utils.execute('sed', '-i', '${/^exit 0$/d}' rclocal, run_as_root=True) 

511 restorecon = [ 

512 '\n', 

513 '# Added by Nova to ensure injected ssh keys have the right context\n', 

514 'restorecon -RF %s 2>/dev/null || :\n' % sshdir, 

515 ] 

516 

517 if not fs.has_file(rclocal): 

518 restorecon.insert(0, '#!/bin/sh') 

519 

520 _inject_file_into_fs(fs, rclocal, ''.join(restorecon), append=True) 

521 fs.set_permissions(rclocal, 0o700) 

522 

523 

524def _inject_key_into_fs(key, fs): 

525 """Add the given public ssh key to root's authorized_keys. 

526 

527 key is an ssh key string. 

528 fs is the path to the base of the filesystem into which to inject the key. 

529 """ 

530 

531 LOG.debug("Inject key fs=%(fs)s key=%(key)s", {'fs': fs, 'key': key}) 

532 sshdir = os.path.join('root', '.ssh') 

533 fs.make_path(sshdir) 

534 fs.set_ownership(sshdir, "root", "root") 

535 fs.set_permissions(sshdir, 0o700) 

536 

537 keyfile = os.path.join(sshdir, 'authorized_keys') 

538 

539 key_data = ''.join([ 

540 '\n', 

541 '# The following ssh key was injected by Nova', 

542 '\n', 

543 key.strip(), 

544 '\n', 

545 ]) 

546 

547 _inject_file_into_fs(fs, keyfile, key_data, append=True) 

548 fs.set_permissions(keyfile, 0o600) 

549 

550 _setup_selinux_for_keys(fs, sshdir) 

551 

552 

553def _inject_net_into_fs(net, fs): 

554 """Inject /etc/network/interfaces into the filesystem rooted at fs. 

555 

556 net is the contents of /etc/network/interfaces. 

557 """ 

558 

559 LOG.debug("Inject key fs=%(fs)s net=%(net)s", {'fs': fs, 'net': net}) 

560 netdir = os.path.join('etc', 'network') 

561 fs.make_path(netdir) 

562 fs.set_ownership(netdir, "root", "root") 

563 fs.set_permissions(netdir, 0o744) 

564 

565 netfile = os.path.join('etc', 'network', 'interfaces') 

566 _inject_file_into_fs(fs, netfile, net) 

567 

568 

569def _inject_admin_password_into_fs(admin_passwd, fs): 

570 """Set the root password to admin_passwd 

571 

572 admin_password is a root password 

573 fs is the path to the base of the filesystem into which to inject 

574 the key. 

575 

576 This method modifies the instance filesystem directly, 

577 and does not require a guest agent running in the instance. 

578 

579 """ 

580 # The approach used here is to copy the password and shadow 

581 # files from the instance filesystem to local files, make any 

582 # necessary changes, and then copy them back. 

583 

584 LOG.debug("Inject admin password fs=%(fs)s " 

585 "admin_passwd=<SANITIZED>", {'fs': fs}) 

586 admin_user = 'root' 

587 

588 passwd_path = os.path.join('etc', 'passwd') 

589 shadow_path = os.path.join('etc', 'shadow') 

590 

591 passwd_data = fs.read_file(passwd_path) 

592 shadow_data = fs.read_file(shadow_path) 

593 

594 new_shadow_data = _set_passwd(admin_user, admin_passwd, 

595 passwd_data, shadow_data) 

596 

597 fs.replace_file(shadow_path, new_shadow_data) 

598 

599 

600def _generate_salt(): 

601 salt_set = ('abcdefghijklmnopqrstuvwxyz' 

602 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' 

603 '0123456789./') 

604 salt = 16 * ' ' 

605 return ''.join([random.choice(salt_set) for c in salt]) 

606 

607 

608def _set_passwd(username, admin_passwd, passwd_data, shadow_data): 

609 """set the password for username to admin_passwd 

610 

611 The passwd_file is not modified. The shadow_file is updated. 

612 if the username is not found in both files, an exception is raised. 

613 

614 :param username: the username 

615 :param admin_passwd: the admin password 

616 :param passwd_data: Data from the passwd file decoded as a string 

617 :param shadow_data: Data from the shadow file decoded as a string 

618 :returns: nothing 

619 :raises: exception.NovaException(), IOError() 

620 

621 """ 

622 # encryption algo - id pairs for crypt() 

623 algos = {'SHA-512': '$6$', 'SHA-256': '$5$', 'MD5': '$1$', 'DES': ''} 

624 

625 salt = _generate_salt() 

626 

627 # crypt() depends on the underlying libc, and may not support all 

628 # forms of hash. We try md5 first. If we get only 13 characters back, 

629 # then the underlying crypt() didn't understand the '$n$salt' magic, 

630 # so we fall back to DES. 

631 # md5 is the default because it's widely supported. Although the 

632 # local crypt() might support stronger SHA, the target instance 

633 # might not. 

634 encrypted_passwd = secretutils.crypt_password( 

635 admin_passwd, algos['MD5'] + salt 

636 ) 

637 if len(encrypted_passwd) == 13: 637 ↛ 638line 637 didn't jump to line 638 because the condition on line 637 was never true

638 encrypted_passwd = secretutils.crypt_password( 

639 admin_passwd, algos['DES'] + salt 

640 ) 

641 

642 p_file = passwd_data.split("\n") 

643 s_file = shadow_data.split("\n") 

644 

645 # username MUST exist in passwd file or it's an error 

646 for entry in p_file: 646 ↛ 651line 646 didn't jump to line 651 because the loop on line 646 didn't complete

647 split_entry = entry.split(':') 

648 if split_entry[0] == username: 648 ↛ 646line 648 didn't jump to line 646 because the condition on line 648 was always true

649 break 

650 else: 

651 msg = _('User %(username)s not found in password file.') 

652 raise exception.NovaException(msg % username) 

653 

654 # update password in the shadow file. It's an error if the 

655 # user doesn't exist. 

656 new_shadow = list() 

657 found = False 

658 for entry in s_file: 

659 split_entry = entry.split(':') 

660 if split_entry[0] == username: 

661 split_entry[1] = encrypted_passwd 

662 found = True 

663 new_entry = ':'.join(split_entry) 

664 new_shadow.append(new_entry) 

665 

666 if not found: 666 ↛ 667line 666 didn't jump to line 667 because the condition on line 666 was never true

667 msg = _('User %(username)s not found in shadow file.') 

668 raise exception.NovaException(msg % username) 

669 

670 return "\n".join(new_shadow)