summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/cc_ssh_import_id.py7
-rw-r--r--cloudinit/config/cc_users_groups.py256
-rw-r--r--cloudinit/distros/__init__.py8
-rw-r--r--cloudinit/distros/ubuntu.py63
-rw-r--r--cloudinit/util.py12
-rw-r--r--config/cloud.cfg6
-rw-r--r--doc/examples/cloud-config.txt92
7 files changed, 435 insertions, 9 deletions
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 <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/>.
+
+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
@@ -47,6 +47,14 @@ class Distro(object):
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 <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
@@ -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))
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:
+ - <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