diff --git a/nova/virt/disk/api.py b/nova/virt/disk/api.py index 060f64e1c3d..51e8f1907ec 100644 --- a/nova/virt/disk/api.py +++ b/nova/virt/disk/api.py @@ -25,8 +25,10 @@ """ +import crypt import json import os +import random import tempfile from nova import exception @@ -207,7 +209,8 @@ def umount(self): # Public module functions -def inject_data(image, key=None, net=None, metadata=None, +def inject_data(image, + key=None, net=None, metadata=None, admin_password=None, partition=None, use_cow=False): """Injects a ssh key and optionally net data into a disk image. @@ -220,7 +223,8 @@ def inject_data(image, key=None, net=None, metadata=None, img = _DiskImage(image=image, partition=partition, use_cow=use_cow) if img.mount(): try: - inject_data_into_fs(img.mount_dir, key, net, metadata, + inject_data_into_fs(img.mount_dir, + key, net, metadata, admin_password, utils.execute) finally: img.umount() @@ -274,7 +278,7 @@ def destroy_container(img): LOG.exception(_('Failed to remove container: %s'), exn) -def inject_data_into_fs(fs, key, net, metadata, execute): +def inject_data_into_fs(fs, key, net, metadata, admin_password, execute): """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 @@ -285,6 +289,8 @@ def inject_data_into_fs(fs, key, net, metadata, execute): _inject_net_into_fs(net, fs, execute=execute) if metadata: _inject_metadata_into_fs(metadata, fs, execute=execute) + if admin_password: + _inject_admin_password_into_fs(admin_password, fs, execute=execute) def _inject_file_into_fs(fs, path, contents): @@ -336,3 +342,110 @@ def _inject_net_into_fs(net, fs, execute=None): utils.execute('chmod', 755, netdir, run_as_root=True) netfile = os.path.join(netdir, 'interfaces') utils.execute('tee', netfile, process_input=net, run_as_root=True) + + +def _inject_admin_password_into_fs(admin_passwd, fs, execute=None): + """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. + + admin_user = 'root' + + fd, tmp_passwd = tempfile.mkstemp() + os.close(fd) + fd, tmp_shadow = tempfile.mkstemp() + os.close(fd) + + utils.execute('cp', os.path.join(fs, 'etc', 'passwd'), tmp_passwd, + run_as_root=True) + utils.execute('cp', os.path.join(fs, 'etc', 'shadow'), tmp_shadow, + run_as_root=True) + _set_passwd(admin_user, admin_passwd, tmp_passwd, tmp_shadow) + utils.execute('cp', tmp_passwd, os.path.join(fs, 'etc', 'passwd'), + run_as_root=True) + utils.execute('rm', tmp_passwd, run_as_root=True) + utils.execute('cp', tmp_shadow, os.path.join(fs, 'etc', 'shadow'), + run_as_root=True) + utils.execute('rm', tmp_shadow, run_as_root=True) + + +def _set_passwd(username, admin_passwd, passwd_file, shadow_file): + """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 encrypted_passwd: the encrypted password + :param passwd_file: path to the passwd file + :param shadow_file: path to the shadow password file + :returns: nothing + :raises: exception.Error(), IOError() + + """ + salt_set = ('abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789./') + # encryption algo - id pairs for crypt() + algos = {'SHA-512': '$6$', 'SHA-256': '$5$', 'MD5': '$1$', 'DES': '' } + + salt = 16 * ' ' + salt = ''.join([random.choice(salt_set) for c in 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) + + try: + p_file = open(passwd_file, 'rb') + s_file = open(shadow_file, 'rb') + + # username MUST exist in passwd file or it's an error + found = False + for entry in p_file: + split_entry = entry.split(':') + if split_entry[0] == username: + found = True + break + if not found: + msg = _('User %(username)s not found in password file.') + raise exception.Error(msg % username) + + # update password in the shadow file.It's an error if the + # 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) + s_file.close() + if not found: + msg = _('User %(username)s not found in shadow file.') + raise exception.Error(msg % username) + s_file = open(shadow_file, 'wb') + for entry in new_shadow: + s_file.write(entry) + finally: + p_file.close() + s_file.close() diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index b89b6737204..b1cd27c10dc 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -100,6 +100,10 @@ default='', help='Override the default libvirt URI ' '(which is dependent on libvirt_type)'), + cfg.BoolOpt('libvirt_inject_password', + default=False, + help='Inject the admin password at boot time, ' + 'without an agent.'), cfg.BoolOpt('use_usb_tablet', default=True, help='Sync virtual and real mouse cursors in Windows VMs'), @@ -1155,7 +1159,14 @@ def basepath(fname='', suffix=suffix): 'use_ipv6': FLAGS.use_ipv6}])) metadata = instance.get('metadata') - if any((key, net, metadata)): + + if FLAGS.libvirt_inject_password: + admin_password = instance.get('admin_pass') + else: + admin_password = None + + if any((key, net, metadata, admin_password)): + instance_name = instance['name'] if config_drive: # Should be True or None by now. @@ -1165,12 +1176,13 @@ def basepath(fname='', suffix=suffix): injection_path = basepath('disk') img_id = instance.image_ref - for injection in ('metadata', 'key', 'net'): + for injection in ('metadata', 'key', 'net', 'admin_password'): if locals()[injection]: LOG.info(_('Injecting %(injection)s into image %(img_id)s' % locals()), instance=instance) try: - disk.inject_data(injection_path, key, net, metadata, + disk.inject_data(injection_path, + key, net, metadata, admin_password, partition=target_partition, use_cow=FLAGS.use_cow_images) diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index dc9340e6b30..0451c82b9b9 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -1704,8 +1704,11 @@ def _mounted_processing(device, key, net, metadata): if not _find_guest_agent(tmpdir, FLAGS.xenapi_agent_path): LOG.info(_('Manipulating interface files ' 'directly')) - disk.inject_data_into_fs(tmpdir, key, net, metadata, - utils.execute) + # for xenapi, we don't 'inject' admin_password here, + # it's handled at instance startup time + disk.inject_data_into_fs(tmpdir, + key, net, None, metadata, + utils.execute) finally: utils.execute('umount', dev_path, run_as_root=True) else: