← Back to team overview

cloud-init-dev team mailing list archive

[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