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: b...@bobcom.io 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: j...@joeuser.io # 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': 'j...@joe.com'}, + ], + } + (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', 'j...@joe.com'] + 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)
_______________________________________________ Mailing list: https://launchpad.net/~cloud-init-dev Post to : cloud-init-dev@lists.launchpad.net Unsubscribe : https://launchpad.net/~cloud-init-dev More help : https://help.launchpad.net/ListHelp