summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cloudinit/config/cc_snap_config.py184
-rw-r--r--cloudinit/config/cc_snappy.py18
-rwxr-xr-xcloudinit/distros/__init__.py35
-rw-r--r--cloudinit/util.py12
-rw-r--r--config/cloud.cfg1
-rw-r--r--doc/examples/cloud-config-user-groups.txt8
-rwxr-xr-xtests/unittests/test_distros/test_user_data_normalize.py65
-rw-r--r--tests/unittests/test_handler/test_handler_snappy.py293
8 files changed, 601 insertions, 15 deletions
diff --git a/cloudinit/config/cc_snap_config.py b/cloudinit/config/cc_snap_config.py
new file mode 100644
index 00000000..275a2d09
--- /dev/null
+++ b/cloudinit/config/cc_snap_config.py
@@ -0,0 +1,184 @@
+# 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)
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index 36db9e67..e03ec483 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -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
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 78adf5f9..4a726430 100755
--- 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,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 = []
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 4cff83c5..4b3fd0cb 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -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
diff --git a/config/cloud.cfg b/config/cloud.cfg
index d608dc86..1b93e7f9 100644
--- a/config/cloud.cfg
+++ b/config/cloud.cfg
@@ -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
diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
index 0e8ed243..9c5202f5 100644
--- a/doc/examples/cloud-config-user-groups.txt
+++ b/doc/examples/cloud-config-user-groups.txt
@@ -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:
diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py
index b24888fc..33bf922d 100755
--- a/tests/unittests/test_distros/test_user_data_normalize.py
+++ b/tests/unittests/test_distros/test_user_data_normalize.py
@@ -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)
diff --git a/tests/unittests/test_handler/test_handler_snappy.py b/tests/unittests/test_handler/test_handler_snappy.py
index 57dce1bc..e320dd82 100644
--- a/tests/unittests/test_handler/test_handler_snappy.py
+++ b/tests/unittests/test_handler/test_handler_snappy.py
@@ -1,14 +1,22 @@
from cloudinit.config.cc_snappy import (
makeop, get_package_ops, render_snap_op)
-from cloudinit import util
+from cloudinit.config.cc_snap_config import (
+ add_assertions, add_snap_user, ASSERTIONS_FILE)
+from cloudinit import (distros, helpers, cloud, util)
+from cloudinit.config.cc_snap_config import handle as snap_handle
+from cloudinit.sources import DataSourceNone
+from ..helpers import FilesystemMockingTestCase, mock
from .. import helpers as t_help
+import logging
import os
import shutil
import tempfile
+import textwrap
import yaml
+LOG = logging.getLogger(__name__)
ALLOWED = (dict, list, int, str)
@@ -287,6 +295,289 @@ class TestInstallPackages(t_help.TestCase):
self.assertEqual(yaml.safe_load(mydata), data_found)
+class TestSnapConfig(FilesystemMockingTestCase):
+
+ SYSTEM_USER_ASSERTION = textwrap.dedent("""
+ type: system-user
+ authority-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
+ brand-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
+ email: foo@bar.com
+ password: $6$E5YiAuMIPAwX58jG$miomhVNui/vf7f/3ctB/f0RWSKFxG0YXzrJ9rtJ1ikvzt
+ series:
+ - 16
+ since: 2016-09-10T16:34:00+03:00
+ until: 2017-11-10T16:34:00+03:00
+ username: baz
+ sign-key-sha3-384: RuVvnp4n52GilycjfbbTCI3_L8Y6QlIE75wxMc0KzGV3AUQqVd9GuXoj
+
+ AcLBXAQAAQoABgUCV/UU1wAKCRBKnlMoJQLkZVeLD/9/+hIeVywtzsDA3oxl+P+u9D13y9s6svP
+ Jd6Wnf4FTw6sq1GjBE4ZA7lrwSaRCUJ9Vcsvf2q9OGPY7mOb2TBxaDe0PbUMjrSrqllSSQwhpNI
+ zG+NxkkKuxsUmLzFa+k9m6cyojNbw5LFhQZBQCGlr3JYqC0tIREq/UsZxj+90TUC87lDJwkU8GF
+ s4CR+rejZj4itIcDcVxCSnJH6hv6j2JrJskJmvObqTnoOlcab+JXdamXqbldSP3UIhWoyVjqzkj
+ +to7mXgx+cCUA9+ngNCcfUG+1huGGTWXPCYkZ78HvErcRlIdeo4d3xwtz1cl/w3vYnq9og1XwsP
+ Yfetr3boig2qs1Y+j/LpsfYBYncgWjeDfAB9ZZaqQz/oc8n87tIPZDJHrusTlBfop8CqcM4xsKS
+ d+wnEY8e/F24mdSOYmS1vQCIDiRU3MKb6x138Ud6oHXFlRBbBJqMMctPqWDunWzb5QJ7YR0I39q
+ BrnEqv5NE0G7w6HOJ1LSPG5Hae3P4T2ea+ATgkb03RPr3KnXnzXg4TtBbW1nytdlgoNc/BafE1H
+ f3NThcq9gwX4xWZ2PAWnqVPYdDMyCtzW3Ck+o6sIzx+dh4gDLPHIi/6TPe/pUuMop9CBpWwez7V
+ v1z+1+URx6Xlq3Jq18y5pZ6fY3IDJ6km2nQPMzcm4Q==""")
+
+ ACCOUNT_ASSERTION = textwrap.dedent("""
+ type: account-key
+ authority-id: canonical
+ revision: 2
+ public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0
+ account-id: canonical
+ name: store
+ since: 2016-04-01T00:00:00.0Z
+ body-length: 717
+ sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswH
+
+ AcbBTQRWhcGAARAA0KKYYQWuHOrsFVi4p4l7ZzSvX7kLgJFFeFgOkzdWKBTHEnsMKjl5mefFe9j
+ qe8NlmJdfY7BenP7XeBtwKp700H/t9lLrZbpTNAPHXYxEWFJp5bPqIcJYBZ+29oLVLN1Tc5X482
+ vCiDqL8+pPYqBrK2fNlyPlNNSum9wI70rDDL4r6FVvr+osTnGejibdV8JphWX+lrSQDnRSdM8KJ
+ UM43vTgLGTi9W54oRhsA2OFexRfRksTrnqGoonCjqX5wO3OFSaMDzMsO2MJ/hPfLgDqw53qjzuK
+ Iec9OL3k5basvu2cj5u9tKwVFDsCKK2GbKUsWWpx2KTpOifmhmiAbzkTHbH9KaoMS7p0kJwhTQG
+ o9aJ9VMTWHJc/NCBx7eu451u6d46sBPCXS/OMUh2766fQmoRtO1OwCTxsRKG2kkjbMn54UdFULl
+ VfzvyghMNRKIezsEkmM8wueTqGUGZWa6CEZqZKwhe/PROxOPYzqtDH18XZknbU1n5lNb7vNfem9
+ 2ai+3+JyFnW9UhfvpVF7gzAgdyCqNli4C6BIN43uwoS8HkykocZS/+Gv52aUQ/NZ8BKOHLw+7an
+ Q0o8W9ltSLZbEMxFIPSN0stiZlkXAp6DLyvh1Y4wXSynDjUondTpej2fSvSlCz/W5v5V7qA4nIc
+ vUvV7RjVzv17ut0AEQEAAQ==
+
+ AcLDXAQAAQoABgUCV83k9QAKCRDUpVvql9g3IBT8IACKZ7XpiBZ3W4lqbPssY6On81WmxQLtvsM
+ WTp6zZpl/wWOSt2vMNUk9pvcmrNq1jG9CuhDfWFLGXEjcrrmVkN3YuCOajMSPFCGrxsIBLSRt/b
+ nrKykdLAAzMfG8rP1d82bjFFiIieE+urQ0Kcv09Jtdvavq3JT1Tek5mFyyfhHNlQEKOzWqmRWiL
+ 3c3VOZUs1ZD8TSlnuq/x+5T0X0YtOyGjSlVxk7UybbyMNd6MZfNaMpIG4x+mxD3KHFtBAC7O6kL
+ eX3i6j5nCY5UABfA3DZEAkWP4zlmdBEOvZ9t293NaDdOpzsUHRkoi0Zez/9BHQ/kwx/uNc2WqrY
+ inCmu16JGNeXqsyinnLl7Ghn2RwhvDMlLxF6RTx8xdx1yk6p3PBTwhZMUvuZGjUtN/AG8BmVJQ1
+ rsGSRkkSywvnhVJRB2sudnrMBmNS2goJbzSbmJnOlBrd2WsV0T9SgNMWZBiov3LvU4o2SmAb6b+
+ rYwh8H5QHcuuYJuxDjFhPswIp6Wes5T6hUicf3SWtObcDS4HSkVS4ImBjjX9YgCuFy7QdnooOWE
+ aPvkRw3XCVeYq0K6w9GRsk1YFErD4XmXXZjDYY650MX9v42Sz5MmphHV8jdIY5ssbadwFSe2rCQ
+ 6UX08zy7RsIb19hTndE6ncvSNDChUR9eEnCm73eYaWTWTnq1cxdVP/s52r8uss++OYOkPWqh5nO
+ haRn7INjH/yZX4qXjNXlTjo0PnHH0q08vNKDwLhxS+D9du+70FeacXFyLIbcWllSbJ7DmbumGpF
+ yYbtj3FDDPzachFQdIG3lSt+cSUGeyfSs6wVtc3cIPka/2Urx7RprfmoWSI6+a5NcLdj0u2z8O9
+ HxeIgxDpg/3gT8ZIuFKePMcLDM19Fh/p0ysCsX+84B9chNWtsMSmIaE57V+959MVtsLu7SLb9gi
+ skrju0pQCwsu2wHMLTNd1f3PTHmrr49hxetTus07HSQUApMtAGKzQilF5zqFjbyaTd4xgQbd+PK
+ CjFyzQTDOcUhXpuUGt/IzlqiFfsCsmbj2K4KdSNYMlqIgZ3Azu8KvZLIhsyN7v5vNIZSPfEbjde
+ ClU9r0VRiJmtYBUjcSghD9LWn+yRLwOxhfQVjm0cBwIt5R/yPF/qC76yIVuWUtM5Y2/zJR1J8OF
+ qWchvlImHtvDzS9FQeLyzJAOjvZ2CnWp2gILgUz0WQdOk1Dq8ax7KS9BQ42zxw9EZAEPw3PEFqR
+ IQsRTONp+iVS8YxSmoYZjDlCgRMWUmawez/Fv5b9Fb/XkO5Eq4e+KfrpUujXItaipb+tV8h5v3t
+ oG3Ie3WOHrVjCLXIdYslpL1O4nadqR6Xv58pHj6k""")
+
+ test_assertions = [ACCOUNT_ASSERTION, SYSTEM_USER_ASSERTION]
+
+ def setUp(self):
+ super(TestSnapConfig, self).setUp()
+ self.subp = util.subp
+ self.new_root = tempfile.mkdtemp()
+ self.addCleanup(shutil.rmtree, self.new_root)
+
+ def _get_cloud(self, distro, metadata=None):
+ self.patchUtils(self.new_root)
+ paths = helpers.Paths({})
+ cls = distros.fetch(distro)
+ mydist = cls(distro, {}, paths)
+ myds = DataSourceNone.DataSourceNone({}, mydist, paths)
+ if metadata:
+ myds.metadata.update(metadata)
+ return cloud.Cloud(myds, paths, {}, mydist, None)
+
+ @mock.patch('cloudinit.util.write_file')
+ @mock.patch('cloudinit.util.subp')
+ def test_snap_config_add_assertions(self, msubp, mwrite):
+ add_assertions(self.test_assertions)
+
+ combined = "\n".join(self.test_assertions)
+ mwrite.assert_any_call(ASSERTIONS_FILE, combined.encode('utf-8'))
+ msubp.assert_called_with(['snap', 'ack', ASSERTIONS_FILE],
+ capture=True)
+
+ def test_snap_config_add_assertions_empty(self):
+ self.assertRaises(ValueError, add_assertions, [])
+
+ def test_add_assertions_nonlist(self):
+ self.assertRaises(ValueError, add_assertions, {})
+
+ @mock.patch('cloudinit.util.write_file')
+ @mock.patch('cloudinit.util.subp')
+ def test_snap_config_add_assertions_ack_fails(self, msubp, mwrite):
+ msubp.side_effect = [util.ProcessExecutionError("Invalid assertion")]
+ self.assertRaises(util.ProcessExecutionError, add_assertions,
+ self.test_assertions)
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_no_config(self, mock_util, mock_add):
+ cfg = {}
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ snap_handle('snap_config', cfg, cc, LOG, None)
+ mock_add.assert_not_called()
+
+ def test_snap_config_add_snap_user_no_config(self):
+ usercfg = add_snap_user(cfg=None)
+ self.assertEqual(usercfg, None)
+
+ def test_snap_config_add_snap_user_not_dict(self):
+ cfg = ['foobar']
+ self.assertRaises(ValueError, add_snap_user, cfg)
+
+ def test_snap_config_add_snap_user_no_email(self):
+ cfg = {'assertions': [], 'known': True}
+ usercfg = add_snap_user(cfg=cfg)
+ self.assertEqual(usercfg, None)
+
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_add_snap_user_email_only(self, mock_util):
+ email = 'janet@planetjanet.org'
+ cfg = {'email': email}
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("false\n", ""), # snap managed
+ ]
+
+ usercfg = add_snap_user(cfg=cfg)
+
+ self.assertEqual(usercfg, {'snapuser': email, 'known': False})
+
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_add_snap_user_email_known(self, mock_util):
+ email = 'janet@planetjanet.org'
+ known = True
+ cfg = {'email': email, 'known': known}
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("false\n", ""), # snap managed
+ (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
+ ]
+
+ usercfg = add_snap_user(cfg=cfg)
+
+ self.assertEqual(usercfg, {'snapuser': email, 'known': known})
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_system_not_snappy(self, mock_util, mock_add):
+ cfg = {'snappy': {'assertions': self.test_assertions}}
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = False
+
+ snap_handle('snap_config', cfg, cc, LOG, None)
+
+ mock_add.assert_not_called()
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_snapuser(self, mock_util, mock_add):
+ email = 'janet@planetjanet.org'
+ cfg = {
+ 'snappy': {
+ 'assertions': self.test_assertions,
+ 'email': email,
+ }
+ }
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("false\n", ""), # snap managed
+ ]
+
+ snap_handle('snap_config', cfg, cc, LOG, None)
+
+ mock_add.assert_called_with(self.test_assertions)
+ usercfg = {'snapuser': email, 'known': False}
+ cc.distro.create_user.assert_called_with(email, **usercfg)
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_snapuser_known(self, mock_util, mock_add):
+ email = 'janet@planetjanet.org'
+ cfg = {
+ 'snappy': {
+ 'assertions': self.test_assertions,
+ 'email': email,
+ 'known': True,
+ }
+ }
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("false\n", ""), # snap managed
+ (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
+ ]
+
+ snap_handle('snap_config', cfg, cc, LOG, None)
+
+ mock_add.assert_called_with(self.test_assertions)
+ usercfg = {'snapuser': email, 'known': True}
+ cc.distro.create_user.assert_called_with(email, **usercfg)
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_snapuser_known_managed(self, mock_util,
+ mock_add):
+ email = 'janet@planetjanet.org'
+ cfg = {
+ 'snappy': {
+ 'assertions': self.test_assertions,
+ 'email': email,
+ 'known': True,
+ }
+ }
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("true\n", ""), # snap managed
+ ]
+
+ snap_handle('snap_config', cfg, cc, LOG, None)
+
+ mock_add.assert_called_with(self.test_assertions)
+ cc.distro.create_user.assert_not_called()
+
+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
+ @mock.patch('cloudinit.config.cc_snap_config.util')
+ def test_snap_config_handle_snapuser_known_no_assertion(self, mock_util,
+ mock_add):
+ email = 'janet@planetjanet.org'
+ cfg = {
+ 'snappy': {
+ 'assertions': [self.ACCOUNT_ASSERTION],
+ 'email': email,
+ 'known': True,
+ }
+ }
+ cc = self._get_cloud('ubuntu')
+ cc.distro = mock.MagicMock()
+ cc.distro.name = 'ubuntu'
+ mock_util.which.return_value = None
+ mock_util.system_is_snappy.return_value = True
+ mock_util.subp.side_effect = [
+ ("true\n", ""), # snap managed
+ ("", ""), # snap known system-user
+ ]
+
+ snap_handle('snap_config', cfg, cc, LOG, None)
+
+ mock_add.assert_called_with([self.ACCOUNT_ASSERTION])
+ cc.distro.create_user.assert_not_called()
+
+
def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):
if cfgfile:
cfgfile = os.path.sep.join([tmpd, cfgfile])