summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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