summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2012-08-22 23:48:23 -0400
committerScott Moser <smoser@ubuntu.com>2012-08-22 23:48:23 -0400
commit55f6e4cb09d568d1a0679c41570b5c6d570a16f0 (patch)
tree9c10edc2a67205b72d5b1fdf4c77b0f4fd8508eb
parent56979d20b9c56c45bfbcaf93bc5f93fa505ece50 (diff)
parentbbbaeca0c375dc166ef8ffe0598d5f384b722c00 (diff)
downloadvyos-cloud-init-55f6e4cb09d568d1a0679c41570b5c6d570a16f0.tar.gz
vyos-cloud-init-55f6e4cb09d568d1a0679c41570b5c6d570a16f0.zip
add support for creating initial users and groups
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. LP: #1028503
-rw-r--r--ChangeLog2
-rw-r--r--cloudinit/config/cc_set_passwords.py12
-rw-r--r--cloudinit/config/cc_ssh.py15
-rw-r--r--cloudinit/config/cc_ssh_import_id.py11
-rw-r--r--cloudinit/config/cc_users_groups.py70
-rw-r--r--cloudinit/distros/__init__.py196
-rw-r--r--cloudinit/distros/ubuntu.py25
-rw-r--r--cloudinit/util.py13
-rw-r--r--config/cloud.cfg6
-rw-r--r--doc/examples/cloud-config.txt3
10 files changed, 339 insertions, 14 deletions
diff --git a/ChangeLog b/ChangeLog
index 91f3834a..0dae0ca5 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,4 +1,6 @@
0.7.0:
+ - support creating users, including the default user.
+ [Ben Howard] (LP: #1028503)
- add apt_reboot_if_required to reboot if an upgrade or package installation
forced the need for one (LP: #1038108)
- allow distro mirror selection to include availability-zone (LP: #1037727)
diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py
index ab266741..7d0fbd9f 100644
--- a/cloudinit/config/cc_set_passwords.py
+++ b/cloudinit/config/cc_set_passwords.py
@@ -50,8 +50,16 @@ def handle(_name, cfg, cloud, log, args):
expire = util.get_cfg_option_bool(chfg, 'expire', expire)
if not plist and password:
- user = util.get_cfg_option_str(cfg, "user", "ubuntu")
- plist = "%s:%s" % (user, password)
+ user = cloud.distro.get_default_user()
+
+ if 'users' in cfg:
+ user_zero = cfg['users'].keys()[0]
+
+ if user_zero != "default":
+ user = user_zero
+
+ if user:
+ plist = "%s:%s" % (user, password)
errors = []
if plist:
diff --git a/cloudinit/config/cc_ssh.py b/cloudinit/config/cc_ssh.py
index 3431bd2a..439c8eb8 100644
--- a/cloudinit/config/cc_ssh.py
+++ b/cloudinit/config/cc_ssh.py
@@ -102,7 +102,16 @@ def handle(_name, cfg, cloud, log, _args):
" %s to file %s"), keytype, keyfile)
try:
- user = util.get_cfg_option_str(cfg, 'user')
+ # TODO(utlemming): consolidate this stanza that occurs in:
+ # cc_ssh_import_id, cc_set_passwords, maybe cc_users_groups.py
+ user = cloud.distro.get_default_user()
+
+ if 'users' in cfg:
+ user_zero = cfg['users'].keys()[0]
+
+ if user_zero != "default":
+ user = user_zero
+
disable_root = util.get_cfg_option_bool(cfg, "disable_root", True)
disable_root_opts = util.get_cfg_option_str(cfg, "disable_root_opts",
DISABLE_ROOT_OPTS)
@@ -124,7 +133,9 @@ def apply_credentials(keys, user, paths, disable_root, disable_root_opts):
if user:
ssh_util.setup_user_keys(keys, user, '', paths)
- if disable_root and user:
+ if disable_root:
+ if not user:
+ user = "NONE"
key_prefix = disable_root_opts.replace('$USER', user)
else:
key_prefix = ''
diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py
index c58b28ec..cd97b99b 100644
--- a/cloudinit/config/cc_ssh_import_id.py
+++ b/cloudinit/config/cc_ssh_import_id.py
@@ -25,14 +25,21 @@ from cloudinit import util
distros = ['ubuntu']
-def handle(name, cfg, _cloud, log, args):
+def handle(name, cfg, cloud, log, args):
if len(args) != 0:
user = args[0]
ids = []
if len(args) > 1:
ids = args[1:]
else:
- user = util.get_cfg_option_str(cfg, "user", "ubuntu")
+ user = cloud.distro.get_default_user()
+
+ if 'users' in cfg:
+ user_zero = cfg['users'].keys()[0]
+
+ if user_zero != "default":
+ user = user_zero
+
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..1e241623
--- /dev/null
+++ b/cloudinit/config/cc_users_groups.py
@@ -0,0 +1,70 @@
+# vi: ts=4 expandtab
+#
+# Copyright (C) 2012 Canonical Ltd.
+#
+# Author: Ben Howard <ben.howard@canonical.com>
+#
+# 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/>.
+
+from cloudinit.settings import PER_INSTANCE
+
+frequency = PER_INSTANCE
+
+
+def handle(name, cfg, cloud, log, _args):
+ user_zero = None
+
+ if 'groups' in cfg:
+ for group in cfg['groups']:
+ if isinstance(group, dict):
+ for name, values in group.iteritems():
+ if isinstance(values, list):
+ cloud.distro.create_group(name, values)
+ elif isinstance(values, str):
+ cloud.distro.create_group(name, values.split(','))
+ else:
+ cloud.distro.create_group(group, [])
+
+ if 'users' in cfg:
+ user_zero = None
+
+ for name, user_config in cfg['users'].iteritems():
+ if not user_zero:
+ user_zero = name
+
+ # Handle the default user creation
+ if name == "default" and user_config:
+ log.info("Creating default user")
+
+ # Create the default user if so defined
+ try:
+ cloud.distro.add_default_user()
+
+ if user_zero == name:
+ user_zero = cloud.distro.get_default_user()
+
+ except NotImplementedError:
+
+ if user_zero == name:
+ user_zero = None
+
+ log.warn("Distro has not implemented default user "
+ "creation. No default user will be created")
+ else:
+ # Make options friendly for distro.create_user
+ new_opts = {}
+ if isinstance(user_config, dict):
+ for opt in user_config:
+ new_opts[opt.replace('-', '')] = user_config[opt]
+
+ cloud.distro.create_user(name, **new_opts)
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 357209a4..686c6a9b 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -7,6 +7,7 @@
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+# Author: Ben Howard <ben.howard@canonical.com>
#
# 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
@@ -23,11 +24,14 @@
from StringIO import StringIO
import abc
+import grp
import os
+import pwd
import re
from cloudinit import importer
from cloudinit import log as logging
+from cloudinit import ssh_util
from cloudinit import util
# TODO(harlowja): Make this via config??
@@ -42,12 +46,32 @@ LOG = logging.getLogger(__name__)
class Distro(object):
__metaclass__ = abc.ABCMeta
+ default_user = None
def __init__(self, name, cfg, paths):
self._paths = paths
self._cfg = cfg
self.name = name
+ def add_default_user(self):
+ # Adds the distro user using the rules:
+ # - Password is same as username but is locked
+ # - nopasswd sudo access
+
+ user = self.get_default_user()
+ if not user:
+ raise NotImplementedError("No Default user")
+
+ self.create_user(user,
+ plain_text_passwd=user,
+ home="/home/%s" % user,
+ shell="/bin/bash",
+ lockpasswd=True,
+ gecos="%s%s" % (user[0:1].upper(), user[1:]),
+ sudo="ALL=(ALL) NOPASSWD:ALL")
+
+ LOG.info("Added default '%s' user with passwordless sudo", user)
+
@abc.abstractmethod
def install_packages(self, pkglist):
raise NotImplementedError()
@@ -170,6 +194,178 @@ class Distro(object):
util.logexc(LOG, "Running interface command %s failed", cmd)
return False
+ def isuser(self, name):
+ try:
+ if pwd.getpwnam(name):
+ return True
+ except KeyError:
+ return False
+
+ def get_default_user(self):
+ return self.default_user
+
+ def create_user(self, name, **kwargs):
+ """
+ Creates users for the system using the GNU passwd tools. This
+ will work on an GNU system. This should be overriden on
+ distros where useradd is not desirable or not available.
+ """
+
+ adduser_cmd = ['useradd', name]
+ x_adduser_cmd = ['useradd', name]
+
+ # Since we are creating users, we want to carefully validate the
+ # inputs. If something goes wrong, we can end up with a system
+ # that nobody can login to.
+ adduser_opts = {
+ "gecos": '--comment',
+ "homedir": '--home',
+ "primarygroup": '--gid',
+ "groups": '--groups',
+ "passwd": '--password',
+ "shell": '--shell',
+ "expiredate": '--expiredate',
+ "inactive": '--inactive',
+ }
+
+ adduser_opts_flags = {
+ "nousergroup": '--no-user-group',
+ "system": '--system',
+ "nologinit": '--no-log-init',
+ "nocreatehome": "-M",
+ }
+
+ # Now check the value and create the command
+ for option in kwargs:
+ value = kwargs[option]
+ if option in adduser_opts and value \
+ and isinstance(value, str):
+ adduser_cmd.extend([adduser_opts[option], value])
+
+ # Redact the password field from the logs
+ if option != "password":
+ x_adduser_cmd.extend([adduser_opts[option], value])
+ else:
+ x_adduser_cmd.extend([adduser_opts[option], 'REDACTED'])
+
+ elif option in adduser_opts_flags and value:
+ adduser_cmd.append(adduser_opts_flags[option])
+ x_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 "nocreatehome" not in kwargs and "system" not in kwargs:
+ adduser_cmd.append('-m')
+
+ # Create the user
+ if self.isuser(name):
+ LOG.warn("User %s already exists, skipping." % name)
+ else:
+ LOG.debug("Creating name %s" % name)
+ try:
+ util.subp(adduser_cmd, logstring=x_adduser_cmd)
+ except Exception as e:
+ util.logexc(LOG, "Failed to create user %s due to error.", e)
+ raise e
+
+ # Set password if plain-text password provided
+ if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']:
+ self.set_passwd(name, kwargs['plain_text_passwd'])
+
+ # Default locking down the account.
+ if ('lockpasswd' not in kwargs and
+ ('lockpasswd' in kwargs and kwargs['lockpasswd']) or
+ 'system' not in kwargs):
+ try:
+ util.subp(['passwd', '--lock', name])
+ except Exception as e:
+ util.logexc(LOG, ("Failed to disable password logins for"
+ "user %s" % name), e)
+ raise e
+
+ # Configure sudo access
+ if 'sudo' in kwargs:
+ self.write_sudo_rules(name, kwargs['sudo'])
+
+ # Import SSH keys
+ if 'sshauthorizedkeys' in kwargs:
+ keys = set(kwargs['sshauthorizedkeys']) or []
+ ssh_util.setup_user_keys(keys, name, None, self._paths)
+
+ return True
+
+ def set_passwd(self, user, passwd, hashed=False):
+ pass_string = '%s:%s' % (user, passwd)
+ cmd = ['chpasswd']
+
+ if hashed:
+ cmd.append('--encrypted')
+
+ try:
+ util.subp(cmd, pass_string, logstring="chpasswd for %s" % user)
+ except Exception as e:
+ util.logexc(LOG, "Failed to set password for %s" % user)
+ raise e
+
+ return True
+
+ def write_sudo_rules(self,
+ user,
+ rules,
+ sudo_file="/etc/sudoers.d/90-cloud-init-users",
+ ):
+
+ content_header = "# user rules for %s" % user
+ content = "%s\n%s %s\n\n" % (content_header, user, rules)
+
+ if isinstance(rules, list):
+ content = "%s\n" % content_header
+ for rule in rules:
+ content += "%s %s\n" % (user, rule)
+ content += "\n"
+
+ if not os.path.exists(sudo_file):
+ util.write_file(sudo_file, content, 0644)
+
+ else:
+ try:
+ with open(sudo_file, 'a') as f:
+ f.write(content)
+ except IOError as e:
+ util.logexc(LOG, "Failed to write %s" % sudo_file, e)
+ raise e
+
+ def isgroup(self, name):
+ try:
+ if grp.getgrnam(name):
+ return True
+ except:
+ return False
+
+ def create_group(self, name, members):
+ group_add_cmd = ['groupadd', name]
+
+ # Check if group exists, and then add it doesn't
+ if self.isgroup(name):
+ LOG.warn("Skipping creation of existing group '%s'" % name)
+ else:
+ try:
+ util.subp(group_add_cmd)
+ LOG.info("Created new group %s" % name)
+ except Exception as e:
+ util.logexc("Failed to create group %s" % name, e)
+
+ # Add members to the group, if so defined
+ if len(members) > 0:
+ for member in members:
+ if not self.isuser(member):
+ LOG.warn("Unable to add group member '%s' to group '%s'"
+ "; user does not exist." % (member, name))
+ continue
+
+ util.subp(['usermod', '-a', '-G', name, member])
+ LOG.info("Added user '%s' to group '%s'" % (member, name))
+
def _get_package_mirror_info(mirror_info, availability_zone=None,
mirror_filter=util.search_for_mirror):
diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py
index 77c2aff4..4b3f8572 100644
--- a/cloudinit/distros/ubuntu.py
+++ b/cloudinit/distros/ubuntu.py
@@ -7,6 +7,7 @@
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Juerg Haefliger <juerg.haefliger@hp.com>
# Author: Joshua Harlow <harlowja@yahoo-inc.com>
+# Author: Ben Howard <ben.howard@canonical.com>
#
# 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
@@ -21,11 +22,31 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from cloudinit.distros import debian
-
from cloudinit import log as logging
+from cloudinit import util
LOG = logging.getLogger(__name__)
class Distro(debian.Distro):
- pass
+
+ distro_name = 'ubuntu'
+ default_user = 'ubuntu'
+
+ def create_user(self, name, **kargs):
+
+ if not super(Distro, self).create_user(name, **kargs):
+ return False
+
+ if 'sshimportid' in kargs:
+ cmd = ["sudo", "-Hu", name, "ssh-import-id"] + kargs['sshimportid']
+ LOG.debug("Importing ssh ids for user %s, post user creation."
+ % name)
+
+ try:
+ util.subp(cmd, capture=True)
+ except util.ProcessExecutionError as e:
+ util.logexc(LOG, "Failed to import %s ssh ids", name)
+ raise e
+
+ return True
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 825867a7..6872cc31 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -1330,12 +1330,19 @@ 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,
+ logstring=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 logstring:
+ 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 sensitive "
+ "input/output logstring: %s"), logstring)
+
if not capture:
stdout = None
stderr = None
diff --git a/config/cloud.cfg b/config/cloud.cfg
index 106ab01a..2744c940 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..56a6c35a 100644
--- a/doc/examples/cloud-config.txt
+++ b/doc/examples/cloud-config.txt
@@ -167,7 +167,8 @@ 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 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