Commit d8534561 authored by Ryan Harper's avatar Ryan Harper Committed by Scott Moser

Add support for snap create-user on Ubuntu Core images.

Ubuntu Core images use the `snap create-user` to add users to an
Ubuntu Core system. Add support for creating snap users by adding
a key to the users dictionary.
  users:
    - name: bob
      snapuser: bob@bobcom.io

Or via the 'snappy' dictionary:
  snappy:
    email: bob@bobcom.io

Users may also create a snap user without contacting the SSO by
providing a 'system-user' assertion by importing them into snapd.

Additionally, Ubuntu Core systems have a read-only /etc/passwd such that
the normal useradd/groupadd commands do not function without 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.
parent ba0adb9b
# vi: ts=4 expandtab
#
# Copyright (C) 2016 Canonical Ltd.
#
# Author: Ryan Harper <ryan.harper@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/>.
"""
Snappy
------
**Summary:** snap_config modules allows configuration of snapd.
This module uses the same ``snappy`` namespace for configuration but
acts only only a subset of the configuration.
If ``assertions`` is set and the user has included a list of assertions
then cloud-init will collect the assertions into a single assertion file
and invoke ``snap ack <path to file with assertions>`` which will attempt
to load the provided assertions into the snapd assertion database.
If ``email`` is set, this value is used to create an authorized user for
contacting and installing snaps from the Ubuntu Store. This is done by
calling ``snap create-user`` command.
If ``known`` is set to True, then it is expected the user also included
an assertion of type ``system-user``. When ``snap create-user`` is called
cloud-init will append '--known' flag which instructs snapd to look for
a system-user assertion with the details. If ``known`` is not set, then
``snap create-user`` will contact the Ubuntu SSO for validating and importing
a system-user for the instance.
.. note::
If the system is already managed, then cloud-init will not attempt to
create a system-user.
**Internal name:** ``cc_snap_config``
**Module frequency:** per instance
**Supported distros:** any with 'snapd' available
**Config keys**::
#cloud-config
snappy:
assertions:
- |
<assertion 1>
- |
<assertion 2>
email: user@user.org
known: true
"""
from cloudinit import log as logging
from cloudinit.settings import PER_INSTANCE
from cloudinit import util
LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
SNAPPY_CMD = "snap"
ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions"
"""
snappy:
assertions:
- |
<snap assertion 1>
- |
<snap assertion 2>
email: foo@foo.io
known: true
"""
def add_assertions(assertions=None):
"""Import list of assertions.
Import assertions by concatenating each assertion into a
string separated by a '\n'. Write this string to a instance file and
then invoke `snap ack /path/to/file` and check for errors.
If snap exits 0, then all assertions are imported.
"""
if not assertions:
assertions = []
if not isinstance(assertions, list):
raise ValueError('assertion parameter was not a list: %s', assertions)
snap_cmd = [SNAPPY_CMD, 'ack']
combined = "\n".join(assertions)
if len(combined) == 0:
raise ValueError("Assertion list is empty")
for asrt in assertions:
LOG.debug('Acking: %s', asrt.split('\n')[0:2])
util.write_file(ASSERTIONS_FILE, combined.encode('utf-8'))
util.subp(snap_cmd + [ASSERTIONS_FILE], capture=True)
def add_snap_user(cfg=None):
"""Add a snap system-user if provided with email under snappy config.
- Check that system is not already managed.
- Check that if using a system-user assertion, that it's
imported into snapd.
Returns a dictionary to be passed to Distro.create_user
"""
if not cfg:
cfg = {}
if not isinstance(cfg, dict):
raise ValueError('configuration parameter was not a dict: %s', cfg)
snapuser = cfg.get('email', None)
if not snapuser:
return
usercfg = {
'snapuser': snapuser,
'known': cfg.get('known', False),
}
# query if we're already registered
out, _ = util.subp([SNAPPY_CMD, 'managed'], capture=True)
if out.strip() == "true":
LOG.warning('This device is already managed. '
'Skipping system-user creation')
return
if usercfg.get('known'):
# Check that we imported a system-user assertion
out, _ = util.subp([SNAPPY_CMD, 'known', 'system-user'],
capture=True)
if len(out) == 0:
LOG.error('Missing "system-user" assertion. '
'Check "snappy" user-data assertions.')
return
return usercfg
def handle(name, cfg, cloud, log, args):
cfgin = cfg.get('snappy')
if not cfgin:
LOG.debug('No snappy config provided, skipping')
return
if not(util.system_is_snappy()):
LOG.debug("%s: system not snappy", name)
return
assertions = cfgin.get('assertions', [])
if len(assertions) > 0:
LOG.debug('Importing user-provided snap assertions')
add_assertions(assertions)
# Create a snap user if requested.
# Snap systems contact the store with a user's email
# and extract information needed to create a local user.
# A user may provide a 'system-user' assertion which includes
# the required information. Using such an assertion to create
# a local user requires specifying 'known: true' in the supplied
# user-data.
usercfg = add_snap_user(cfg=cfgin)
if usercfg:
cloud.distro.create_user(usercfg.get('snapuser'), **usercfg)
......@@ -257,24 +257,14 @@ 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"):
SNAPPY_CMD = "snappy-go"
else:
elif util.which("snappy"):
SNAPPY_CMD = "snappy"
else:
SNAPPY_CMD = "snap"
LOG.debug("snappy command is '%s'", SNAPPY_CMD)
......@@ -289,7 +279,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
......
......@@ -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,32 @@ 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')
known = kwargs.get('known', False)
adduser_cmd = ["snap", "create-user", "--sudoer", "--json"]
if known:
adduser_cmd.append("--known")
adduser_cmd.append(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 +481,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 +635,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 = []
......
......@@ -2374,3 +2374,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
......@@ -45,6 +45,7 @@ cloud_config_modules:
# Emit the cloud config ready event
# this can be used by upstart jobs for 'start on cloud-config'.
- emit_upstart
- snap_config
- ssh-import-id
- locale
- set-passwords
......
......@@ -30,6 +30,7 @@ users:
gecos: Magic Cloud App Daemon User
inactive: true
system: true
- snapuser: joe@joeuser.io
# Valid Values:
# name: The user's login name
......@@ -80,6 +81,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:
......
......@@ -4,6 +4,7 @@ from cloudinit import helpers
from cloudinit import settings
from ..helpers import TestCase
import mock
bcfg = {
......@@ -296,3 +297,67 @@ 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@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', 'joe@joe.com']
mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
self.assertEqual(username, 'joe')
@mock.patch('cloudinit.util.subp')
def test_create_snap_user_known(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@joe.com', 'known': True},
],
}
(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', '--known',
'joe@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)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment