From 336ddbe13bdfc729495f5bfb8cc89b4360916157 Mon Sep 17 00:00:00 2001 From: Ben Howard Date: Mon, 20 Aug 2012 14:52:31 -0600 Subject: Added "userless" mode to cloud-init for handling the creation of the users and the default user on Ubuntu. cloudinit/config/cc_users_groups.py: new cloud-config module for creating users and groups on instance initialization. - Creates users and group - Sets "user" directive used in ssh_import_id cloudinit/config/cc_ssh_import_id.py: module will rely upon users_groups for setting the default user. Removed assumption of 'ubuntu' user. cloudinit/distros/__init__.py: Added new abstract methods for getting and creating the default user. cloudinit/distros/ubuntu.py: Defined abstract methods for getting and and creating the default 'ubuntu' user on Ubuntu instances. cloudinit/util.py: Added ability to hide command run through util.subp to prevent the commands from showing in the logs. Used by user_groups cloud-config module. config/cloud.cfg: Removed "user: ubuntu" directive and replaced with new user-less syntax. doc/examples/cloud-config.txt: Documented the creation of users and groups. --- cloudinit/config/cc_ssh_import_id.py | 7 +- cloudinit/config/cc_users_groups.py | 256 +++++++++++++++++++++++++++++++++++ cloudinit/distros/__init__.py | 8 ++ cloudinit/distros/ubuntu.py | 63 ++++++++- cloudinit/util.py | 12 +- config/cloud.cfg | 6 +- doc/examples/cloud-config.txt | 92 ++++++++++++- 7 files changed, 435 insertions(+), 9 deletions(-) create mode 100644 cloudinit/config/cc_users_groups.py diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py index c58b28ec..f18e1fc5 100644 --- a/cloudinit/config/cc_ssh_import_id.py +++ b/cloudinit/config/cc_ssh_import_id.py @@ -32,7 +32,12 @@ def handle(name, cfg, _cloud, log, args): if len(args) > 1: ids = args[1:] else: - user = util.get_cfg_option_str(cfg, "user", "ubuntu") + try: + user = cloud.distro.get_default_username() + except NotImplementedError: + pass + + user = None ids = util.get_cfg_option_list(cfg, "ssh_import_id", []) if len(ids) == 0: diff --git a/cloudinit/config/cc_users_groups.py b/cloudinit/config/cc_users_groups.py new file mode 100644 index 00000000..1a428217 --- /dev/null +++ b/cloudinit/config/cc_users_groups.py @@ -0,0 +1,256 @@ +# vi: ts=4 expandtab +# +# Copyright (C) 2012 Canonical Ltd. +# +# Author: Ben Howard +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import grp +import pwd +import os +import traceback + +from cloudinit import templater +from cloudinit import util +from cloudinit import ssh_util +from cloudinit.settings import PER_INSTANCE + +frequency = PER_INSTANCE + +def handle(name, cfg, cloud, log, _args): + + groups_cfg = None + users_cfg = None + user_zero = None + + if 'groups' in cfg: + groups_cfg = cfg['groups'] + create_groups(groups_cfg, log) + + if 'users' in cfg: + users_cfg = cfg['users'] + user_zero = users_cfg.keys()[0] + + for name, user_config in users_cfg.iteritems(): + if name == "default" and user_config: + log.info("Creating default user") + + # Create the default user if so defined + try: + cloud.distro.add_default_user() + + except NotImplementedError as e: + log.warn(("Distro has not implemented default user" + "creation. No default user will be created")) + + # Get the distro user + if user_zero == 'default': + try: + user_zero = cloud.distro.get_default_username() + + except NotImplementedError: + pass + + else: + create_user(name, user_config, log, cloud) + + # Override user directive + if user_zero and check_user(user_zero): + cfg['user'] = user_zero + log.info("Override user directive with '%s'" % user_zero) + + +def check_user(user): + try: + user = pwd.getpwnam(user) + return True + + except KeyError: + return False + + return False + +def create_user(user, user_config, log, cloud): + # Iterate over the users definition and create the users + + if check_user(user): + log.warn("User %s already exists, skipping." % user) + + else: + log.info("Creating user %s" % user) + + adduser_cmd = ['useradd', user] + adduser_opts = { + "gecos": '-c', + "homedir": '--home', + "primary-group": '-g', + "groups": '-G', + "passwd": '-p', + "shell": '-s', + "expiredate": '-e', + "inactive": '-f', + } + + adduser_opts_flags = { + "no-user-group": '-N', + "system": '-r', + "no-log-init": '-l', + "no-create-home": "-M", + } + + # Now check the value and create the command + for option in user_config: + value = user_config[option] + if option in adduser_opts and value \ + and type(value).__name__ == "str": + adduser_cmd.extend([adduser_opts[option], value]) + + if option in adduser_opts_flags and value: + adduser_cmd.append(adduser_opts_flags[option]) + + # Default to creating home directory unless otherwise directed + # Also, we do not create home directories for system users. + if "no-create-home" not in user_config and \ + "system" not in user_config: + adduser_cmd.append('-m') + + print adduser_cmd + + # Create the user + try: + util.subp(adduser_cmd, + hidden="cloudinit.user_config.cc_users_groups(%s)" % user) + + except Exception as e: + log.warn("Failed to create user %s due to error.\n%s" % user) + + + # Double check to make sure that the user exists + if not check_user(user): + log.warn("User creation for %s failed for unknown reasons" % user) + return False + + # unlock the password if so-user_configured + if 'lock-passwd' not in user_config or \ + user_config['lock-passwd']: + + try: + util.subp(['passwd', '-l', user]) + + except Exception as e: + log.warn("Failed to disable password logins for user %s\n%s" \ + % (user, e)) + + # write out sudo options + if 'sudo' in user_config: + write_sudo(user, user_config['sudo'], log) + + # import ssh id's from launchpad + if 'ssh-import-id' in user_config: + import_ssh_id(user, user_config['ssh-import-id'], log) + + # write ssh-authorized-keys + if 'ssh-authorized-keys' in user_config: + keys = set(user_config['ssh-authorized-keys']) or [] + user_home = pwd.getpwnam(user).pw_dir + ssh_util.setup_user_keys(keys, user, None, cloud.paths) + +def import_ssh_id(user, keys, log): + + if not os.path.exists('/usr/bin/ssh-import-id'): + log.warn("ssh-import-id does not exist on this system, skipping") + return + + cmd = ["sudo", "-Hu", user, "ssh-import-id"] + keys + log.debug("Importing ssh ids for user %s.", user) + + try: + util.subp(cmd, capture=False) + + except util.ProcessExecutionError as e: + log.warn("Failed to run command to import %s ssh ids", user) + log.warn(traceback.print_exc(e)) + + +def write_sudo(user, rules, log): + sudo_file = "/etc/sudoers.d/90-cloud-init-users" + + content = "%s %s" % (user, rules) + if type(rules).__name__ == "list": + content = "" + for rule in rules: + content += "%s %s\n" % (user, rule) + + if not os.path.exists(sudo_file): + content = "# Added by cloud-init\n%s\n" % content + util.write_file(sudo_file, content, 0644) + + else: + old_content = None + try: + with open(sudo_file, 'r') as f: + old_content = f.read() + f.close() + + except IOError as e: + log.warn("Failed to read %s, not adding sudo rules for %s" % \ + (sudo_file, user)) + + content = "%s\n\n%s" % (old_content, content) + util.write_file(sudo_file, content, 0644) + +def create_groups(groups, log): + existing_groups = [x.gr_name for x in grp.getgrall()] + existing_users = [x.pw_name for x in pwd.getpwall()] + + for group in groups: + + group_add_cmd = ['groupadd'] + group_name = None + group_members = [] + + if type(group).__name__ == "dict": + group_name = [ x for x in group ][0] + for user in group[group_name]: + if user in existing_users: + group_members.append(user) + else: + log.warn("Unable to add non-existant user '%s' to" \ + " group '%s'" % (user, group_name)) + else: + group_name = group + group_add_cmd.append(group) + + group_add_cmd.append(group_name) + + # Check if group exists, and then add it doesn't + if group_name in existing_groups: + log.warn("Group '%s' already exists, skipping creation." % \ + group_name) + + else: + try: + util.subp(group_add_cmd) + log.info("Created new group %s" % group) + + except Exception as e: + log.warn("Failed to create group %s\n%s" % (group, e)) + + # Add members to the group, if so defined + if len(group_members) > 0: + for member in group_members: + util.subp(['usermod', '-a', '-G', group_name, member]) + log.info("Added user '%s' to group '%s'" % (member, group)) + + diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index da4d0180..8aec1199 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -46,6 +46,14 @@ class Distro(object): self._cfg = cfg self.name = name + @abc.abstractmethod + def add_default_user(self): + raise NotImplementedError() + + @abc.abstractmethod + def get_default_username(self): + raise NotImplementedError() + @abc.abstractmethod def install_packages(self, pkglist): raise NotImplementedError() diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py index 77c2aff4..e6672c4f 100644 --- a/cloudinit/distros/ubuntu.py +++ b/cloudinit/distros/ubuntu.py @@ -7,6 +7,7 @@ # Author: Scott Moser # Author: Juerg Haefliger # Author: Joshua Harlow +# Author: Ben Howard # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License version 3, as @@ -20,12 +21,70 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from cloudinit import distros from cloudinit.distros import debian - +from cloudinit import helpers from cloudinit import log as logging +from cloudinit import util +from cloudinit.settings import PER_INSTANCE + +import pwd LOG = logging.getLogger(__name__) class Distro(debian.Distro): - pass + + distro_name = 'ubuntu' + __default_user_name__ = 'ubuntu-test' + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + + def get_default_username(self): + return self.__default_user_name__ + + def add_default_user(self): + # Adds the ubuntu user using the rules: + # - Password is 'ubuntu', but is locked + # - nopasswd sudo access + + + if self.__default_user_name__ in [x[0] for x in pwd.getpwall()]: + LOG.warn("'%s' user already exists, not creating it." % \ + self.__default_user_name__) + return + + try: + util.subp(['adduser', + '--shell', '/bin/bash', + '--home', '/home/%s' % self.__default_user_name__, + '--disabled-password', + '--gecos', 'Ubuntu', + self.__default_user_name__, + ]) + + pass_string = '%(u)s:%(u)s' % {'u': self.__default_user_name__} + util.subp(['chpasswd'], pass_string) + util.subp(['passwd', '-l', self.__default_user_name__]) + + ubuntu_sudoers=""" +# Added by cloud-init +# %(user)s user is default user in cloud-images. +# It needs passwordless sudo functionality. +%(user)s ALL=(ALL) NOPASSWD:ALL +""" % { 'user': self.__default_user_name__ } + + util.write_file('/etc/sudoers.d/90-cloud-init-ubuntu', + ubuntu_sudoers, + mode=0440) + + LOG.info("Added default 'ubuntu' user with passwordless sudo") + + except Exception as e: + util.logexc(LOG, "Failed to create %s user\n%s" % + (self.__default_user_name__, e)) diff --git a/cloudinit/util.py b/cloudinit/util.py index a8c0cceb..0fbf9832 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -1329,12 +1329,18 @@ def delete_dir_contents(dirname): del_file(node_fullpath) -def subp(args, data=None, rcs=None, env=None, capture=True, shell=False): +def subp(args, data=None, rcs=None, env=None, capture=True, shell=False, hidden=False): if rcs is None: rcs = [0] try: - LOG.debug(("Running command %s with allowed return codes %s" - " (shell=%s, capture=%s)"), args, rcs, shell, capture) + + if not hidden: + LOG.debug(("Running command %s with allowed return codes %s" + " (shell=%s, capture=%s)"), args, rcs, shell, capture) + else: + LOG.debug(("Running hidden command to protect sensative output " + " Calling function: %s" ), hidden) + if not capture: stdout = None stderr = None diff --git a/config/cloud.cfg b/config/cloud.cfg index 2b4d9e63..7933b4ce 100644 --- a/config/cloud.cfg +++ b/config/cloud.cfg @@ -1,8 +1,9 @@ # The top level settings are used as module # and system configuration. -# This user will have its password adjusted -user: ubuntu +# Implement for Ubuntu only: create the default 'ubuntu' user +users: + default: true # If this is set, 'root' will not be able to ssh in and they # will get a message to login instead as the above $user (ubuntu) @@ -36,6 +37,7 @@ cloud_config_modules: # this can be used by upstart jobs for 'start on cloud-config'. - emit_upstart - mounts + - users-groups - ssh-import-id - locale - set-passwords diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt index 1e6628d2..9a2ed27a 100644 --- a/doc/examples/cloud-config.txt +++ b/doc/examples/cloud-config.txt @@ -167,7 +167,97 @@ mounts: # complete. This must be an array, and must have 7 fields. mount_default_fields: [ None, None, "auto", "defaults,nobootwait", "0", "2" ] -# add each entry to ~/.ssh/authorized_keys for the configured user +# add groups to the system +# The following example adds the ubuntu group with members foo and bar and +# the group cloud-users. +groups: + ubuntu: [foo,bar] + cloud-users + +# add users to the system. Users are added after groups are added. +users: + foobar: + gecos: Foo B. Bar + primary-group: foobar + groups: users + expiredate: 2012-09-01 + ssh-import-id: foobar + lock-passwd: false + passwd: $6$j212wezy$7H/1LT4f9/N3wpgNunhsIqtMj62OKiS3nyNwuizouQc3u7MbYCarYeAHWYPYb2FT.lbioDm2RrkJPb9BZMN1O/ + barfoo: + gecos: Bar B. Foo + sudo: ALL=(ALL) NOPASSWD:ALL + groups: users, admin + ssh-import-id: None + lock-passwd: true + ssh-authorized-keys: + - + - + cloudy: + gecos: Magic Cloud App Daemon User + inactive: true + system: true + +# Valid Values: +# gecos: The user name's real name, i.e. "Bob B. Smith" +# homedir: Optional. Set to the local path you want to use. Defaults to +# /home/ +# primary-group: define the primary group. Defaults to a new group created +# named after the user. +# groups: Optional. Additional groups to add the user to. Defaults to none +# lock-passwd: Defaults to true. Lock the password to disable password login +# inactive: Create the user as inactive +# passwd: The hash -- not the password itself -- of the password you want +# to use for this user. You can generate a safe hash via: +# mkpasswd -m SHA-512 -s 4096 +# (the above command would create a password SHA512 password hash +# with 4096 salt rounds) +# +# Please note: while the use of a hashed password is better than +# plain text, the use of this feature is not ideal. Also, +# using a high number of salting rounds will help, but it should +# not be relied upon. +# +# To highlight this risk, running John the Ripper against the +# example hash above, with a readily available wordlist, revealed +# the true password in 12 seconds on a i7-2620QM. +# +# In other words, this feature is a potential security risk and is +# provided for your convenience only. If you do not fully trust the +# medium over which your cloud-config will be transmitted, then you +# should use SSH authentication only. +# +# You have thus been warned. +# no-create-home: When set to true, do not create home directory. +# no-user-group: When set to true, do not create a group named after the user. +# no-log-init: When set to true, do not initialize lastlog and faillog database. +# ssh-import-id: Optional. Import SSH ids +# ssh-authorized-key: Optional. Add key to user's ssh authorized keys file +# sudo: Defaults to none. Set to the sudo string you want to use, i.e. +# ALL=(ALL) NOPASSWD:ALL. To add multiple rules, use the following +# format. + sudo: + - ALL=(ALL) NOPASSWD:/bin/mysql + - ALL=(ALL) ALL +# Note: Please double check your syntax and make sure it is valid. +# cloud-init does not parse/check the syntax of the sudo +# directive. +# system: Create the user as a system user. This means no home directory. +# +# Default user creation: Ubuntu Only +# Unless you define users, you will get a Ubuntu user on Ubuntu systems with the +# legacy permission (no password sudo, locked user, etc). If however, you want +# to have the ubuntu user in addition to other users, you need to instruct +# cloud-init that you also want the default user. To do this use the following +# syntax: +users: + default: True + foobar: ... +# +# users[0] (the first user in users) overrides the user directive. + +# add each entry to ~/.ssh/authorized_keys for the configured user or the +# first user defined in the user definition directive. ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAGEA3FSyQwBI6Z+nCSjUUk8EEAnnkhXlukKoUPND/RRClWz2s5TCzIkd3Ou5+Cyz71X0XmazM3l5WgeErvtIwQMyT1KjNoMhoJMrJnWqQPOt5Q8zWd9qG7PBl9+eiH5qV7NZ mykey@host - ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies -- cgit v1.2.3