cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #01165
[Merge] ~raharper/cloud-init:snapuser-create into cloud-init:master
Ryan Harper has proposed merging ~raharper/cloud-init:snapuser-create into cloud-init:master.
Requested reviews:
cloud init development team (cloud-init-dev)
Related bugs:
Bug #1619393 in cloud-init: "cloud-init useradd/groupadd fails on ubuntu-core-16 with readonly /etc/passwd"
https://bugs.launchpad.net/cloud-init/+bug/1619393
For more details, see:
https://code.launchpad.net/~raharper/cloud-init/+git/cloud-init/+merge/304700
distros: add support for snap create-user on Ubuntu Core images
Ubuntu Core images use the `snap create-user` to add users to a Ubuntu
Core system. Add support for creating snap users by added a key to
the users dictionary:
users:
- name: bob
snapuser: bob@xxxxxxxxx
Additionally, Ubuntu Core systems have a read-only /etc/passwd such that
the normal useradd/groupadd commands do not function with an additional
flag, '--extrausers' which redirects the pwd to /var/lib/extrausers.
Move the system_is_snappy() check from cc_snappy module to util for
re-use and then update the Distro class to append '--extrausers' if
the system is Ubuntu Core.
--
Your team cloud init development team is requested to review the proposed merge of ~raharper/cloud-init:snapuser-create into cloud-init:master.
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index 6bcd838..d870425 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -230,18 +230,6 @@ def disable_enable_ssh(enabled):
util.write_file(not_to_be_run, "cloud-init\n")
-def system_is_snappy():
- # channel.ini is configparser loadable.
- # snappy will move to using /etc/system-image/config.d/*.ini
- # this is certainly not a perfect test, but good enough for now.
- content = util.load_file("/etc/system-image/channel.ini", quiet=True)
- if 'ubuntu-core' in content.lower():
- return True
- if os.path.isdir("/etc/system-image/config.d/"):
- return True
- return False
-
-
def set_snappy_command():
global SNAPPY_CMD
if util.which("snappy-go"):
@@ -262,7 +250,7 @@ def handle(name, cfg, cloud, log, args):
LOG.debug("%s: System is not snappy. disabling", name)
return
- if sys_snappy.lower() == "auto" and not(system_is_snappy()):
+ if sys_snappy.lower() == "auto" and not(util.system_is_snappy()):
LOG.debug("%s: 'auto' mode, and system not snappy", name)
return
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index b1192e8..042bc05 100644
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -367,6 +367,9 @@ class Distro(object):
adduser_cmd = ['useradd', name]
log_adduser_cmd = ['useradd', name]
+ if util.system_is_snappy():
+ adduser_cmd.append('--extrausers')
+ log_adduser_cmd.append('--extrausers')
# Since we are creating users, we want to carefully validate the
# inputs. If something goes wrong, we can end up with a system
@@ -445,6 +448,28 @@ class Distro(object):
util.logexc(LOG, "Failed to create user %s", name)
raise e
+ def add_snap_user(self, name, **kwargs):
+ """
+ Add a snappy user to the system using snappy tools
+ """
+
+ snapuser = kwargs.get('snapuser')
+ adduser_cmd = ["snap", "create-user", "--sudoer", "--json", snapuser]
+
+ # Run the command
+ LOG.debug("Adding snap user %s", name)
+ try:
+ (out, err) = util.subp(adduser_cmd, logstring=adduser_cmd,
+ capture=True)
+ LOG.debug("snap create-user returned: %s:%s", out, err)
+ jobj = util.load_json(out)
+ username = jobj.get('username', None)
+ except Exception as e:
+ util.logexc(LOG, "Failed to create snap user %s", name)
+ raise e
+
+ return username
+
def create_user(self, name, **kwargs):
"""
Creates users for the system using the GNU passwd tools. This
@@ -452,6 +477,10 @@ class Distro(object):
distros where useradd is not desirable or not available.
"""
+ # Add a snap user, if requested
+ if 'snapuser' in kwargs:
+ return self.add_snap_user(name, **kwargs)
+
# Add the user
self.add_user(name, **kwargs)
@@ -602,6 +631,8 @@ class Distro(object):
def create_group(self, name, members=None):
group_add_cmd = ['groupadd', name]
+ if util.system_is_snappy():
+ group_add_cmd.append('--extrausers')
if not members:
members = []
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 7c37eb8..07bdd03 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2369,3 +2369,15 @@ def get_installed_packages(target=None):
pkgs_inst.add(re.sub(":.*", "", pkg))
return pkgs_inst
+
+
+def system_is_snappy():
+ # channel.ini is configparser loadable.
+ # snappy will move to using /etc/system-image/config.d/*.ini
+ # this is certainly not a perfect test, but good enough for now.
+ content = load_file("/etc/system-image/channel.ini", quiet=True)
+ if 'ubuntu-core' in content.lower():
+ return True
+ if os.path.isdir("/etc/system-image/config.d/"):
+ return True
+ return False
diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
index 0e8ed24..4a9a768 100644
--- a/doc/examples/cloud-config-user-groups.txt
+++ b/doc/examples/cloud-config-user-groups.txt
@@ -30,6 +30,8 @@ users:
gecos: Magic Cloud App Daemon User
inactive: true
system: true
+ - name: joe
+ snapuser: joe@xxxxxxxxxx
# Valid Values:
# name: The user's login name
@@ -80,6 +82,13 @@ users:
# 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.
+# snapuser: Create a Snappy (Ubuntu-Core) user via the snap create-user
+# command available on Ubuntu systems. If the user has an account
+# on the Ubuntu SSO, specifying the email will allow snap to
+# request a username and any public ssh keys and will import
+# these into the system with username specifed by SSO account.
+# If 'username' is not set in SSO, then username will be the
+# shortname before the email domain.
#
# Default user creation:
diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py
index a887a93..8be374f 100644
--- a/tests/unittests/test_distros/test_user_data_normalize.py
+++ b/tests/unittests/test_distros/test_user_data_normalize.py
@@ -3,6 +3,7 @@ from cloudinit import helpers
from cloudinit import settings
from ..helpers import TestCase
+import mock
bcfg = {
@@ -295,3 +296,47 @@ class TestUGNormalize(TestCase):
self.assertIn('bob', users)
self.assertEqual({'default': False}, users['joe'])
self.assertEqual({'default': False}, users['bob'])
+
+ @mock.patch('cloudinit.util.subp')
+ def test_create_snap_user(self, mock_subp):
+ mock_subp.side_effect = [('{"username": "joe", "ssh-key-count": 1}\n',
+ '')]
+ distro = self._make_distro('ubuntu')
+ ug_cfg = {
+ 'users': [
+ {'name': 'joe', 'snapuser': 'joe@xxxxxxx'},
+ ],
+ }
+ (users, _groups) = self._norm(ug_cfg, distro)
+ for (user, config) in users.items():
+ print('user=%s config=%s' % (user, config))
+ username = distro.create_user(user, **config)
+
+ snapcmd = ['snap', 'create-user', '--sudoer', '--json', 'joe@xxxxxxx']
+ mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
+ self.assertEqual(username, 'joe')
+
+ @mock.patch('cloudinit.util.system_is_snappy')
+ @mock.patch('cloudinit.util.is_group')
+ @mock.patch('cloudinit.util.subp')
+ def test_add_user_on_snappy_system(self, mock_subp, mock_isgrp,
+ mock_snappy):
+ mock_isgrp.return_value = False
+ mock_subp.return_value = True
+ mock_snappy.return_value = True
+ distro = self._make_distro('ubuntu')
+ ug_cfg = {
+ 'users': [
+ {'name': 'joe', 'groups': 'users', 'create_groups': True},
+ ],
+ }
+ (users, _groups) = self._norm(ug_cfg, distro)
+ for (user, config) in users.items():
+ print('user=%s config=%s' % (user, config))
+ distro.add_user(user, **config)
+
+ groupcmd = ['groupadd', 'users', '--extrausers']
+ addcmd = ['useradd', 'joe', '--extrausers', '--groups', 'users', '-m']
+
+ mock_subp.assert_any_call(groupcmd)
+ mock_subp.assert_any_call(addcmd, logstring=addcmd)
Follow ups