← Back to team overview

cloud-init-dev team mailing list archive

[Merge] lp:~utlemming/cloud-init/userless into lp:cloud-init

 

Ben Howard has proposed merging lp:~utlemming/cloud-init/userless into lp:cloud-init.

Requested reviews:
  cloud init development team (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~utlemming/cloud-init/userless/+merge/120469

Added "userless" mode to cloud-init for handling the creation of the 
users and the default user on Ubuntu. The end goal of this is to remove the need for the 'ubuntu' user in the cloud images and to allow individuals to choose the default user name. 

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.


-- 
https://code.launchpad.net/~utlemming/cloud-init/userless/+merge/120469
Your team cloud init development team is requested to review the proposed merge of lp:~utlemming/cloud-init/userless into lp:cloud-init.
=== modified file 'cloudinit/config/cc_ssh_import_id.py'
--- cloudinit/config/cc_ssh_import_id.py	2012-06-21 16:12:16 +0000
+++ cloudinit/config/cc_ssh_import_id.py	2012-08-20 21:02:41 +0000
@@ -32,7 +32,12 @@
         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:

=== added file 'cloudinit/config/cc_users_groups.py'
--- cloudinit/config/cc_users_groups.py	1970-01-01 00:00:00 +0000
+++ cloudinit/config/cc_users_groups.py	2012-08-20 21:02:41 +0000
@@ -0,0 +1,256 @@
+# vi: ts=4 expandtab
+#
+#    Copyright (C) 2012 Canonical Ltd.
+#
+#    Author: Ben Howard <ben.howard@xxxxxxxxxxxxx>
+#
+#    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 <http://www.gnu.org/licenses/>.
+
+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))
+
+

=== modified file 'cloudinit/distros/__init__.py'
--- cloudinit/distros/__init__.py	2012-06-30 00:06:32 +0000
+++ cloudinit/distros/__init__.py	2012-08-20 21:02:41 +0000
@@ -47,6 +47,14 @@
         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()
 

=== modified file 'cloudinit/distros/ubuntu.py'
--- cloudinit/distros/ubuntu.py	2012-06-22 19:52:26 +0000
+++ cloudinit/distros/ubuntu.py	2012-08-20 21:02:41 +0000
@@ -7,6 +7,7 @@
 #    Author: Scott Moser <scott.moser@xxxxxxxxxxxxx>
 #    Author: Juerg Haefliger <juerg.haefliger@xxxxxx>
 #    Author: Joshua Harlow <harlowja@xxxxxxxxxxxxx>
+#    Author: Ben Howard <ben.howard@xxxxxxxxxxxxx>
 #
 #    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 <http://www.gnu.org/licenses/>.
 
+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))

=== modified file 'cloudinit/util.py'
--- cloudinit/util.py	2012-08-06 19:59:41 +0000
+++ cloudinit/util.py	2012-08-20 21:02:41 +0000
@@ -1329,12 +1329,18 @@
             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

=== modified file 'config/cloud.cfg'
--- config/cloud.cfg	2012-08-20 19:58:19 +0000
+++ config/cloud.cfg	2012-08-20 21:02:41 +0000
@@ -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 @@
 # this can be used by upstart jobs for 'start on cloud-config'.
  - emit_upstart
  - mounts
+ - users-groups
  - ssh-import-id
  - locale
  - set-passwords

=== modified file 'doc/examples/cloud-config.txt'
--- doc/examples/cloud-config.txt	2012-08-03 18:34:29 +0000
+++ doc/examples/cloud-config.txt	2012-08-20 21:02:41 +0000
@@ -167,7 +167,97 @@
 # 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:
+      - <ssh pub key 1>
+      - <ssh pub key 2>
+  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/<username>
+#   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