702 lines
24 KiB
Python
702 lines
24 KiB
Python
# Copyright 2010 United States Government as represented by the
|
|
# Administrator of the National Aeronautics and Space Administration.
|
|
#
|
|
# Copyright 2011, Piston Cloud Computing, Inc.
|
|
#
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
"""
|
|
Utility methods to resize, repartition, and modify disk images.
|
|
|
|
Includes injection of SSH PGP keys into authorized_keys file.
|
|
|
|
"""
|
|
|
|
import os
|
|
import random
|
|
import tempfile
|
|
|
|
if os.name != 'nt':
|
|
import crypt
|
|
|
|
from oslo_concurrency import processutils
|
|
from oslo_log import log as logging
|
|
from oslo_serialization import jsonutils
|
|
|
|
import nova.conf
|
|
from nova import exception
|
|
from nova.i18n import _
|
|
import nova.privsep.fs
|
|
import nova.privsep.libvirt
|
|
from nova import utils
|
|
from nova.virt.disk.mount import api as mount
|
|
from nova.virt.disk.vfs import api as vfs
|
|
from nova.virt.image import model as imgmodel
|
|
from nova.virt import images
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
CONF = nova.conf.CONF
|
|
|
|
_MKFS_COMMAND = {}
|
|
_DEFAULT_MKFS_COMMAND = None
|
|
|
|
FS_FORMAT_EXT2 = "ext2"
|
|
FS_FORMAT_EXT3 = "ext3"
|
|
FS_FORMAT_EXT4 = "ext4"
|
|
FS_FORMAT_XFS = "xfs"
|
|
FS_FORMAT_NTFS = "ntfs"
|
|
FS_FORMAT_VFAT = "vfat"
|
|
|
|
SUPPORTED_FS_TO_EXTEND = (
|
|
FS_FORMAT_EXT2,
|
|
FS_FORMAT_EXT3,
|
|
FS_FORMAT_EXT4)
|
|
|
|
_DEFAULT_FILE_SYSTEM = FS_FORMAT_VFAT
|
|
_DEFAULT_FS_BY_OSTYPE = {'linux': FS_FORMAT_EXT4,
|
|
'windows': FS_FORMAT_NTFS}
|
|
|
|
for s in CONF.virt_mkfs:
|
|
# NOTE(yamahata): mkfs command may includes '=' for its options.
|
|
# So item.partition('=') doesn't work here
|
|
os_type, mkfs_command = s.split('=', 1)
|
|
if os_type:
|
|
_MKFS_COMMAND[os_type] = mkfs_command
|
|
if os_type == 'default':
|
|
_DEFAULT_MKFS_COMMAND = mkfs_command
|
|
|
|
|
|
def get_fs_type_for_os_type(os_type):
|
|
return os_type if _MKFS_COMMAND.get(os_type) else 'default'
|
|
|
|
|
|
def get_file_extension_for_os_type(os_type, specified_fs=None):
|
|
mkfs_command = _MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND)
|
|
if mkfs_command:
|
|
extension = mkfs_command
|
|
else:
|
|
if not specified_fs:
|
|
specified_fs = CONF.default_ephemeral_format
|
|
if not specified_fs:
|
|
specified_fs = _DEFAULT_FS_BY_OSTYPE.get(os_type,
|
|
_DEFAULT_FILE_SYSTEM)
|
|
extension = specified_fs
|
|
return utils.get_hash_str(extension)[:7]
|
|
|
|
|
|
def mkfs(os_type, fs_label, target, run_as_root=True, specified_fs=None):
|
|
"""Format a file or block device using
|
|
a user provided command for each os type.
|
|
If user has not provided any configuration,
|
|
format type will be used according to a
|
|
default_ephemeral_format configuration
|
|
or a system defaults.
|
|
"""
|
|
|
|
mkfs_command = (_MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) or
|
|
'') % {'fs_label': fs_label, 'target': target}
|
|
if mkfs_command:
|
|
utils.execute(*mkfs_command.split(), run_as_root=run_as_root)
|
|
else:
|
|
if not specified_fs:
|
|
specified_fs = CONF.default_ephemeral_format
|
|
if not specified_fs:
|
|
specified_fs = _DEFAULT_FS_BY_OSTYPE.get(os_type,
|
|
_DEFAULT_FILE_SYSTEM)
|
|
|
|
utils.mkfs(specified_fs, target, fs_label, run_as_root=run_as_root)
|
|
|
|
|
|
def resize2fs(image, check_exit_code=False, run_as_root=False):
|
|
if run_as_root:
|
|
nova.privsep.fs.resize2fs(image, check_exit_code)
|
|
else:
|
|
nova.privsep.fs.unprivileged_resize2fs(image, check_exit_code)
|
|
|
|
|
|
def get_disk_size(path):
|
|
"""Get the (virtual) size of a disk image
|
|
|
|
:param path: Path to the disk image
|
|
:returns: Size (in bytes) of the given disk image as it would be seen
|
|
by a virtual machine.
|
|
"""
|
|
return images.qemu_img_info(path).virtual_size
|
|
|
|
|
|
def extend(image, size):
|
|
"""Increase image to size.
|
|
|
|
:param image: instance of nova.virt.image.model.Image
|
|
:param size: image size in bytes
|
|
"""
|
|
|
|
# Currently can only resize FS in local images
|
|
if not isinstance(image, imgmodel.LocalImage):
|
|
return
|
|
|
|
if not can_resize_image(image.path, size):
|
|
return
|
|
|
|
if (image.format == imgmodel.FORMAT_PLOOP):
|
|
nova.privsep.libvirt.ploop_resize(image.path, size)
|
|
return
|
|
|
|
utils.execute('qemu-img', 'resize', image.path, size)
|
|
|
|
if (image.format != imgmodel.FORMAT_RAW and
|
|
not CONF.resize_fs_using_block_device):
|
|
return
|
|
|
|
# if we can't access the filesystem, we can't do anything more
|
|
if not is_image_extendable(image):
|
|
return
|
|
|
|
def safe_resize2fs(dev, run_as_root=False, finally_call=lambda: None):
|
|
try:
|
|
resize2fs(dev, run_as_root=run_as_root, check_exit_code=[0])
|
|
except processutils.ProcessExecutionError as exc:
|
|
LOG.debug("Resizing the file system with resize2fs "
|
|
"has failed with error: %s", exc)
|
|
finally:
|
|
finally_call()
|
|
|
|
# NOTE(vish): attempts to resize filesystem
|
|
if image.format != imgmodel.FORMAT_RAW:
|
|
# in case of non-raw disks we can't just resize the image, but
|
|
# rather the mounted device instead
|
|
mounter = mount.Mount.instance_for_format(
|
|
image, None, None)
|
|
if mounter.get_dev():
|
|
safe_resize2fs(mounter.device,
|
|
run_as_root=True,
|
|
finally_call=mounter.unget_dev)
|
|
else:
|
|
safe_resize2fs(image.path)
|
|
|
|
|
|
def can_resize_image(image, size):
|
|
"""Check whether we can resize the container image file.
|
|
:param image: path to local image file
|
|
:param size: the image size in bytes
|
|
"""
|
|
LOG.debug('Checking if we can resize image %(image)s. '
|
|
'size=%(size)s', {'image': image, 'size': size})
|
|
|
|
# Check that we're increasing the size
|
|
virt_size = get_disk_size(image)
|
|
if virt_size >= size:
|
|
LOG.debug('Cannot resize image %s to a smaller size.',
|
|
image)
|
|
return False
|
|
return True
|
|
|
|
|
|
def is_image_extendable(image):
|
|
"""Check whether we can extend the image."""
|
|
LOG.debug('Checking if we can extend filesystem inside %(image)s.',
|
|
{'image': image})
|
|
|
|
# For anything except a local raw file we must
|
|
# go via the VFS layer
|
|
if (not isinstance(image, imgmodel.LocalImage) or
|
|
image.format != imgmodel.FORMAT_RAW):
|
|
fs = None
|
|
try:
|
|
fs = vfs.VFS.instance_for_image(image, None)
|
|
fs.setup(mount=False)
|
|
if fs.get_image_fs() in SUPPORTED_FS_TO_EXTEND:
|
|
return True
|
|
except exception.NovaException as e:
|
|
# FIXME(sahid): At this step we probably want to break the
|
|
# process if something wrong happens however our CI
|
|
# provides a bad configuration for libguestfs reported in
|
|
# the bug lp#1413142. When resolved we should remove this
|
|
# except to let the error to be propagated.
|
|
LOG.warning('Unable to mount image %(image)s with '
|
|
'error %(error)s. Cannot resize.',
|
|
{'image': image, 'error': e})
|
|
finally:
|
|
if fs is not None:
|
|
fs.teardown()
|
|
|
|
return False
|
|
else:
|
|
# For raw, we can directly inspect the file system
|
|
try:
|
|
utils.execute('e2label', image.path)
|
|
except processutils.ProcessExecutionError as e:
|
|
LOG.debug('Unable to determine label for image %(image)s with '
|
|
'error %(error)s. Cannot resize.',
|
|
{'image': image,
|
|
'error': e})
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
class _DiskImage(object):
|
|
"""Provide operations on a disk image file."""
|
|
|
|
tmp_prefix = 'openstack-disk-mount-tmp'
|
|
|
|
def __init__(self, image, partition=None, mount_dir=None):
|
|
"""Create a new _DiskImage object instance
|
|
|
|
:param image: instance of nova.virt.image.model.Image
|
|
:param partition: the partition number within the image
|
|
:param mount_dir: the directory to mount the image on
|
|
"""
|
|
|
|
# These passed to each mounter
|
|
self.partition = partition
|
|
self.mount_dir = mount_dir
|
|
self.image = image
|
|
|
|
# Internal
|
|
self._mkdir = False
|
|
self._mounter = None
|
|
self._errors = []
|
|
|
|
if mount_dir:
|
|
device = self._device_for_path(mount_dir)
|
|
if device:
|
|
self._reset(device)
|
|
else:
|
|
LOG.debug('No device found for path: %s', mount_dir)
|
|
|
|
@staticmethod
|
|
def _device_for_path(path):
|
|
device = None
|
|
path = os.path.realpath(path)
|
|
with open("/proc/mounts", 'r') as ifp:
|
|
for line in ifp:
|
|
fields = line.split()
|
|
if fields[1] == path:
|
|
device = fields[0]
|
|
break
|
|
return device
|
|
|
|
def _reset(self, device):
|
|
"""Reset internal state for a previously mounted directory."""
|
|
self._mounter = mount.Mount.instance_for_device(self.image,
|
|
self.mount_dir,
|
|
self.partition,
|
|
device)
|
|
|
|
mount_name = os.path.basename(self.mount_dir or '')
|
|
self._mkdir = mount_name.startswith(self.tmp_prefix)
|
|
|
|
@property
|
|
def errors(self):
|
|
"""Return the collated errors from all operations."""
|
|
return '\n--\n'.join([''] + self._errors)
|
|
|
|
def mount(self):
|
|
"""Mount a disk image, using the object attributes.
|
|
|
|
The first supported means provided by the mount classes is used.
|
|
|
|
True, or False is returned and the 'errors' attribute
|
|
contains any diagnostics.
|
|
"""
|
|
if self._mounter:
|
|
raise exception.NovaException(_('image already mounted'))
|
|
|
|
if not self.mount_dir:
|
|
self.mount_dir = tempfile.mkdtemp(prefix=self.tmp_prefix)
|
|
self._mkdir = True
|
|
|
|
mounter = mount.Mount.instance_for_format(self.image,
|
|
self.mount_dir,
|
|
self.partition)
|
|
|
|
if mounter.do_mount():
|
|
self._mounter = mounter
|
|
return self._mounter.device
|
|
else:
|
|
LOG.debug(mounter.error)
|
|
self._errors.append(mounter.error)
|
|
return None
|
|
|
|
def umount(self):
|
|
"""Umount a mount point from the filesystem."""
|
|
if self._mounter:
|
|
self._mounter.do_umount()
|
|
self._mounter = None
|
|
|
|
def teardown(self):
|
|
"""Remove a disk image from the file system."""
|
|
try:
|
|
if self._mounter:
|
|
self._mounter.do_teardown()
|
|
self._mounter = None
|
|
finally:
|
|
if self._mkdir:
|
|
os.rmdir(self.mount_dir)
|
|
|
|
|
|
# Public module functions
|
|
|
|
def inject_data(image, key=None, net=None, metadata=None, admin_password=None,
|
|
files=None, partition=None, mandatory=()):
|
|
"""Inject the specified items into a disk image.
|
|
|
|
:param image: instance of nova.virt.image.model.Image
|
|
:param key: the SSH public key to inject
|
|
:param net: the network configuration to inject
|
|
:param metadata: the user metadata to inject
|
|
:param admin_password: the root password to set
|
|
:param files: the files to copy into the image
|
|
:param partition: the partition number to access
|
|
:param mandatory: the list of parameters which must not fail to inject
|
|
|
|
If an item name is not specified in the MANDATORY iterable, then a warning
|
|
is logged on failure to inject that item, rather than raising an exception.
|
|
|
|
it will mount the image as a fully partitioned disk and attempt to inject
|
|
into the specified partition number.
|
|
|
|
If PARTITION is not specified the image is mounted as a single partition.
|
|
|
|
Returns True if all requested operations completed without issue.
|
|
Raises an exception if a mandatory item can't be injected.
|
|
"""
|
|
items = {'image': image, 'key': key, 'net': net, 'metadata': metadata,
|
|
'files': files, 'partition': partition}
|
|
LOG.debug("Inject data image=%(image)s key=%(key)s net=%(net)s "
|
|
"metadata=%(metadata)s admin_password=<SANITIZED> "
|
|
"files=%(files)s partition=%(partition)s", items)
|
|
try:
|
|
fs = vfs.VFS.instance_for_image(image, partition)
|
|
fs.setup()
|
|
except Exception as e:
|
|
# If a mandatory item is passed to this function,
|
|
# then reraise the exception to indicate the error.
|
|
for inject in mandatory:
|
|
inject_val = items[inject]
|
|
if inject_val:
|
|
raise
|
|
LOG.warning('Ignoring error injecting data into image %(image)s '
|
|
'(%(e)s)', {'image': image, 'e': e})
|
|
return False
|
|
|
|
try:
|
|
return inject_data_into_fs(fs, key, net, metadata, admin_password,
|
|
files, mandatory)
|
|
finally:
|
|
fs.teardown()
|
|
|
|
|
|
def setup_container(image, container_dir):
|
|
"""Setup the LXC container.
|
|
|
|
:param image: instance of nova.virt.image.model.Image
|
|
:param container_dir: directory to mount the image at
|
|
|
|
It will mount the loopback image to the container directory in order
|
|
to create the root filesystem for the container.
|
|
|
|
Returns path of image device which is mounted to the container directory.
|
|
"""
|
|
img = _DiskImage(image=image, mount_dir=container_dir)
|
|
dev = img.mount()
|
|
if dev is None:
|
|
LOG.error("Failed to mount container filesystem '%(image)s' "
|
|
"on '%(target)s': %(errors)s",
|
|
{"image": img, "target": container_dir,
|
|
"errors": img.errors})
|
|
raise exception.NovaException(img.errors)
|
|
|
|
return dev
|
|
|
|
|
|
def teardown_container(container_dir, container_root_device=None):
|
|
"""Teardown the container rootfs mounting once it is spawned.
|
|
|
|
It will umount the container that is mounted,
|
|
and delete any linked devices.
|
|
"""
|
|
try:
|
|
img = _DiskImage(image=None, mount_dir=container_dir)
|
|
img.teardown()
|
|
|
|
# Make sure container_root_device is released when teardown container.
|
|
if container_root_device:
|
|
if 'loop' in container_root_device:
|
|
LOG.debug("Release loop device %s", container_root_device)
|
|
nova.privsep.fs.loopremove(container_root_device)
|
|
elif 'nbd' in container_root_device:
|
|
LOG.debug('Release nbd device %s', container_root_device)
|
|
nova.privsep.fs.nbd_disconnect(container_root_device)
|
|
else:
|
|
LOG.debug('No release necessary for block device %s',
|
|
container_root_device)
|
|
except Exception:
|
|
LOG.exception(_('Failed to teardown container filesystem'))
|
|
|
|
|
|
def clean_lxc_namespace(container_dir):
|
|
"""Clean up the container namespace rootfs mounting one spawned.
|
|
|
|
It will umount the mounted names that are mounted
|
|
but leave the linked devices alone.
|
|
"""
|
|
try:
|
|
img = _DiskImage(image=None, mount_dir=container_dir)
|
|
img.umount()
|
|
except Exception:
|
|
LOG.exception(_('Failed to umount container filesystem'))
|
|
|
|
|
|
def inject_data_into_fs(fs, key, net, metadata, admin_password, files,
|
|
mandatory=()):
|
|
"""Injects data into a filesystem already mounted by the caller.
|
|
Virt connections can call this directly if they mount their fs
|
|
in a different way to inject_data.
|
|
|
|
If an item name is not specified in the MANDATORY iterable, then a warning
|
|
is logged on failure to inject that item, rather than raising an exception.
|
|
|
|
Returns True if all requested operations completed without issue.
|
|
Raises an exception if a mandatory item can't be injected.
|
|
"""
|
|
items = {'key': key, 'net': net, 'metadata': metadata,
|
|
'admin_password': admin_password, 'files': files}
|
|
functions = {
|
|
'key': _inject_key_into_fs,
|
|
'net': _inject_net_into_fs,
|
|
'metadata': _inject_metadata_into_fs,
|
|
'admin_password': _inject_admin_password_into_fs,
|
|
'files': _inject_files_into_fs,
|
|
}
|
|
status = True
|
|
for inject, inject_val in items.items():
|
|
if inject_val:
|
|
try:
|
|
inject_func = functions[inject]
|
|
inject_func(inject_val, fs)
|
|
except Exception as e:
|
|
if inject in mandatory:
|
|
raise
|
|
LOG.warning('Ignoring error injecting %(inject)s into '
|
|
'image (%(e)s)', {'inject': inject, 'e': e})
|
|
status = False
|
|
return status
|
|
|
|
|
|
def _inject_files_into_fs(files, fs):
|
|
for (path, contents) in files:
|
|
# NOTE(wangpan): Ensure the parent dir of injecting file exists
|
|
parent_dir = os.path.dirname(path)
|
|
if (len(parent_dir) > 0 and parent_dir != "/"
|
|
and not fs.has_file(parent_dir)):
|
|
fs.make_path(parent_dir)
|
|
fs.set_ownership(parent_dir, "root", "root")
|
|
fs.set_permissions(parent_dir, 0o744)
|
|
_inject_file_into_fs(fs, path, contents)
|
|
|
|
|
|
def _inject_file_into_fs(fs, path, contents, append=False):
|
|
LOG.debug("Inject file fs=%(fs)s path=%(path)s append=%(append)s",
|
|
{'fs': fs, 'path': path, 'append': append})
|
|
if append:
|
|
fs.append_file(path, contents)
|
|
else:
|
|
fs.replace_file(path, contents)
|
|
|
|
|
|
def _inject_metadata_into_fs(metadata, fs):
|
|
LOG.debug("Inject metadata fs=%(fs)s metadata=%(metadata)s",
|
|
{'fs': fs, 'metadata': metadata})
|
|
_inject_file_into_fs(fs, 'meta.js', jsonutils.dumps(metadata))
|
|
|
|
|
|
def _setup_selinux_for_keys(fs, sshdir):
|
|
"""Get selinux guests to ensure correct context on injected keys."""
|
|
|
|
if not fs.has_file(os.path.join("etc", "selinux")):
|
|
return
|
|
|
|
rclocal = os.path.join('etc', 'rc.local')
|
|
rc_d = os.path.join('etc', 'rc.d')
|
|
|
|
if not fs.has_file(rclocal) and fs.has_file(rc_d):
|
|
rclocal = os.path.join(rc_d, 'rc.local')
|
|
|
|
# Note some systems end rc.local with "exit 0"
|
|
# and so to append there you'd need something like:
|
|
# utils.execute('sed', '-i', '${/^exit 0$/d}' rclocal, run_as_root=True)
|
|
restorecon = [
|
|
'\n',
|
|
'# Added by Nova to ensure injected ssh keys have the right context\n',
|
|
'restorecon -RF %s 2>/dev/null || :\n' % sshdir,
|
|
]
|
|
|
|
if not fs.has_file(rclocal):
|
|
restorecon.insert(0, '#!/bin/sh')
|
|
|
|
_inject_file_into_fs(fs, rclocal, ''.join(restorecon), append=True)
|
|
fs.set_permissions(rclocal, 0o700)
|
|
|
|
|
|
def _inject_key_into_fs(key, fs):
|
|
"""Add the given public ssh key to root's authorized_keys.
|
|
|
|
key is an ssh key string.
|
|
fs is the path to the base of the filesystem into which to inject the key.
|
|
"""
|
|
|
|
LOG.debug("Inject key fs=%(fs)s key=%(key)s", {'fs': fs, 'key': key})
|
|
sshdir = os.path.join('root', '.ssh')
|
|
fs.make_path(sshdir)
|
|
fs.set_ownership(sshdir, "root", "root")
|
|
fs.set_permissions(sshdir, 0o700)
|
|
|
|
keyfile = os.path.join(sshdir, 'authorized_keys')
|
|
|
|
key_data = ''.join([
|
|
'\n',
|
|
'# The following ssh key was injected by Nova',
|
|
'\n',
|
|
key.strip(),
|
|
'\n',
|
|
])
|
|
|
|
_inject_file_into_fs(fs, keyfile, key_data, append=True)
|
|
fs.set_permissions(keyfile, 0o600)
|
|
|
|
_setup_selinux_for_keys(fs, sshdir)
|
|
|
|
|
|
def _inject_net_into_fs(net, fs):
|
|
"""Inject /etc/network/interfaces into the filesystem rooted at fs.
|
|
|
|
net is the contents of /etc/network/interfaces.
|
|
"""
|
|
|
|
LOG.debug("Inject key fs=%(fs)s net=%(net)s", {'fs': fs, 'net': net})
|
|
netdir = os.path.join('etc', 'network')
|
|
fs.make_path(netdir)
|
|
fs.set_ownership(netdir, "root", "root")
|
|
fs.set_permissions(netdir, 0o744)
|
|
|
|
netfile = os.path.join('etc', 'network', 'interfaces')
|
|
_inject_file_into_fs(fs, netfile, net)
|
|
|
|
|
|
def _inject_admin_password_into_fs(admin_passwd, fs):
|
|
"""Set the root password to admin_passwd
|
|
|
|
admin_password is a root password
|
|
fs is the path to the base of the filesystem into which to inject
|
|
the key.
|
|
|
|
This method modifies the instance filesystem directly,
|
|
and does not require a guest agent running in the instance.
|
|
|
|
"""
|
|
# The approach used here is to copy the password and shadow
|
|
# files from the instance filesystem to local files, make any
|
|
# necessary changes, and then copy them back.
|
|
|
|
LOG.debug("Inject admin password fs=%(fs)s "
|
|
"admin_passwd=<SANITIZED>", {'fs': fs})
|
|
admin_user = 'root'
|
|
|
|
passwd_path = os.path.join('etc', 'passwd')
|
|
shadow_path = os.path.join('etc', 'shadow')
|
|
|
|
passwd_data = fs.read_file(passwd_path)
|
|
shadow_data = fs.read_file(shadow_path)
|
|
|
|
new_shadow_data = _set_passwd(admin_user, admin_passwd,
|
|
passwd_data, shadow_data)
|
|
|
|
fs.replace_file(shadow_path, new_shadow_data)
|
|
|
|
|
|
def _generate_salt():
|
|
salt_set = ('abcdefghijklmnopqrstuvwxyz'
|
|
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
'0123456789./')
|
|
salt = 16 * ' '
|
|
return ''.join([random.choice(salt_set) for c in salt])
|
|
|
|
|
|
def _set_passwd(username, admin_passwd, passwd_data, shadow_data):
|
|
"""set the password for username to admin_passwd
|
|
|
|
The passwd_file is not modified. The shadow_file is updated.
|
|
if the username is not found in both files, an exception is raised.
|
|
|
|
:param username: the username
|
|
:param admin_passwd: the admin password
|
|
:param passwd_data: path to the passwd file
|
|
:param shadow_data: path to the shadow password file
|
|
:returns: nothing
|
|
:raises: exception.NovaException(), IOError()
|
|
|
|
"""
|
|
if os.name == 'nt':
|
|
raise exception.NovaException(_('Not implemented on Windows'))
|
|
|
|
# encryption algo - id pairs for crypt()
|
|
algos = {'SHA-512': '$6$', 'SHA-256': '$5$', 'MD5': '$1$', 'DES': ''}
|
|
|
|
salt = _generate_salt()
|
|
|
|
# crypt() depends on the underlying libc, and may not support all
|
|
# forms of hash. We try md5 first. If we get only 13 characters back,
|
|
# then the underlying crypt() didn't understand the '$n$salt' magic,
|
|
# so we fall back to DES.
|
|
# md5 is the default because it's widely supported. Although the
|
|
# local crypt() might support stronger SHA, the target instance
|
|
# might not.
|
|
encrypted_passwd = crypt.crypt(admin_passwd, algos['MD5'] + salt)
|
|
if len(encrypted_passwd) == 13:
|
|
encrypted_passwd = crypt.crypt(admin_passwd, algos['DES'] + salt)
|
|
|
|
p_file = passwd_data.split("\n")
|
|
s_file = shadow_data.split("\n")
|
|
|
|
# username MUST exist in passwd file or it's an error
|
|
for entry in p_file:
|
|
split_entry = entry.split(':')
|
|
if split_entry[0] == username:
|
|
break
|
|
else:
|
|
msg = _('User %(username)s not found in password file.')
|
|
raise exception.NovaException(msg % username)
|
|
|
|
# update password in the shadow file. It's an error if the
|
|
# user doesn't exist.
|
|
new_shadow = list()
|
|
found = False
|
|
for entry in s_file:
|
|
split_entry = entry.split(':')
|
|
if split_entry[0] == username:
|
|
split_entry[1] = encrypted_passwd
|
|
found = True
|
|
new_entry = ':'.join(split_entry)
|
|
new_shadow.append(new_entry)
|
|
|
|
if not found:
|
|
msg = _('User %(username)s not found in shadow file.')
|
|
raise exception.NovaException(msg % username)
|
|
|
|
return "\n".join(new_shadow)
|