Merge ~raharper/cloud-init:snapuser-create into cloud-init:master

Proposed by Ryan Harper on 2016-09-01
Status: Merged
Merged at revision: 21632972df034c200578e1fbc121a07f20bb8774
Proposed branch: ~raharper/cloud-init:snapuser-create
Merge into: cloud-init:master
Diff against target: 753 lines (+600/-15)
8 files modified
cloudinit/config/cc_snap_config.py (+183/-0)
cloudinit/config/cc_snappy.py (+4/-14)
cloudinit/distros/__init__.py (+35/-0)
cloudinit/util.py (+12/-0)
config/cloud.cfg (+1/-0)
doc/examples/cloud-config-user-groups.txt (+8/-0)
tests/unittests/test_distros/test_user_data_normalize.py (+65/-0)
tests/unittests/test_handler/test_handler_snappy.py (+292/-1)
Reviewer Review Type Date Requested Status
cloud-init commiters 2016-09-01 Pending
Review via email: mp+304700@code.launchpad.net

Description of the change

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: <email address hidden>

Or via the 'snappy' dictionary:

snappy:
  email: <email address hidden>

These methods will contact the Ubuntu SSO infrastructure to request
user information (including public ssh keys if present) for creating
the user.

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

snappy:
  email: <email address hidden>
  known: true
  assertions:
  - |
    <assertion text here>

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.

To post a comment you must log in.
~raharper/cloud-init:snapuser-create updated on 2016-09-16
90577ef... by Ryan Harper on 2016-09-16

Update snapuser example

Scott Moser (smoser) wrote :

Is this still needed or desired ?

~raharper/cloud-init:snapuser-create updated on 2016-10-13
ba627b5... by Ryan Harper on 2016-10-06

Merge remote-tracking branch 'origin/master' into snapuser-create

e5a6164... by Ryan Harper on 2016-10-06

config/cc_snap_config: Add snap config module for early config of snapd

During init, we want to import snap assertions which can be used to
create a snap 'system-user'.

9fb5a34... by Ryan Harper on 2016-10-07

Fix cc_snap_config: run later and encode assertions

snapd requires cloud-init stage to complete, so we cannot
run snap_config during init stage, instead we can run
early during config stage.

When writing out assertions, the default write mode is 'w+b'
and thus the input needs to be bytes[], so encode with utf-8.

a9b54d0... by Ryan Harper on 2016-10-07

Merge remote-tracking branch 'origin/master' into snapuser-create

76774db... by Ryan Harper on 2016-10-12

snappy: try harder to import system-user assertion

b543c0f... by Ryan Harper on 2016-10-12

unittest: add test for when snappy system-user assertion is provided

3db49a7... by Ryan Harper on 2016-10-13

config/cc_snap_config: Add some config checking and unittests

- Make sure we only run on system_is_snappy == True
- Handle some expected scenarios for various levels of config input.
- Handle the case where the device is already managed
- Add unittests to cover snap_config module.

Jon Grimm (jgrimm) wrote :

Quick look just because I wanted to know more about the impl.

Ryan Harper (raharper) wrote :

Thanks for looking at the code!

~raharper/cloud-init:snapuser-create updated on 2016-10-14
a55ae1f... by Ryan Harper on 2016-10-14

debian: add explicit dependencies for required binaries

Jon Grimm (jgrimm) wrote :

Thanks, it was enlightening and easy to read.

~raharper/cloud-init:snapuser-create updated on 2016-10-14
1b96428... by Ryan Harper on 2016-10-14

Merge branch 'fix-lp1619423' into snapuser-create

Scott Moser (smoser) wrote :

over all, looks good.
you dont have to clean up the handle, but if you see easy way to do that that'd be nice.

is the snappy path now valid on non-snappy system ? (ubuntu server with 'snap' support).

lastly, please just review your commit message and make sure its up to date with all changes (it may well be, just think you made some changes since you wrote it).

Ryan Harper (raharper) wrote :
Download full text (10.4 KiB)

On Wed, Oct 19, 2016 at 8:04 AM, Scott Moser <email address hidden> wrote:

> over all, looks good.
> you dont have to clean up the handle, but if you see easy way to do that
> that'd be nice.
>
> is the snappy path now valid on non-snappy system ? (ubuntu server with
> 'snap' support).
>

It is, and util.is_system_snappy() I think passes where it works; I'll
confirm.

>
>
> lastly, please just review your commit message and make sure its up to
> date with all changes (it may well be, just think you made some changes
> since you wrote it).
>

ACK, it needs updating to account for the snappy: namespace changes.

>
>
> Diff comments:
>
> > diff --git a/cloudinit/config/cc_snap_config.py
> b/cloudinit/config/cc_snap_config.py
> > new file mode 100644
> > index 0000000..667b9c6
> > --- /dev/null
> > +++ b/cloudinit/config/cc_snap_config.py
> > @@ -0,0 +1,177 @@
> > +# vi: ts=4 expandtab
> > +#
> > +# Copyright (C) 2016 Canonical Ltd.
> > +#
> > +# Author: Ryan Harper <email address hidden>
> > +#
> > +# 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:** ubuntu
> > +
> > +**Config keys**::
> > +
> > + #cloud-confi...

Ryan Harper (raharper) wrote :

On Wed, Oct 19, 2016 at 8:52 AM, Ryan Harper <email address hidden>
wrote:

>
>
> On Wed, Oct 19, 2016 at 8:04 AM, Scott Moser <email address hidden> wrote:
>
>> over all, looks good.
>> you dont have to clean up the handle, but if you see easy way to do that
>> that'd be nice.
>>
>> is the snappy path now valid on non-snappy system ? (ubuntu server with
>> 'snap' support).
>>
>
> It is, and util.is_system_snappy() I think passes where it works; I'll
> confirm.
>

is_system_snappy is looking for 'all-snap' style images; so snaps on
classic don't
respond. We'd need to design something for validating that it works on
snaps on classic
setups.

One key difference is that all-snap systems have non-writable /etc ; where
as snapd on classic doesn't. If we added classic systems to the
is_system_snappy check, then users would get added
to /var/lib/extrausers instead of /etc; I think that's non-optimal and
introduces changes to existing
behavior.

Ryan

~raharper/cloud-init:snapuser-create updated on 2016-10-19
53d8cb4... by Ryan Harper on 2016-10-19

cc_snap_config: move snapuser creation out of handle and other cleanups

- Dropped distros = ['ubuntu']; snapd runs on other distros as well
- Dropped set_snappy_cmd; we require 'snap' for function
- Default assertions parameter to None to prevent object init
- Moved snapuser creation into method out of handle into add_snap_user
- Added unittests for new add_snap_user method

Scott Moser (smoser) wrote :

over all, looks good.
you dont have to clean up the handle, but if you see easy way to do that that'd be nice.

is the snappy path now valid on non-snappy system ? (ubuntu server with 'snap' support).

lastly, please just review your commit message and make sure its up to date with all changes (it may well be, just think you made some changes since you wrote it).

~raharper/cloud-init:snapuser-create updated on 2016-10-20
a353f18... by Ryan Harper on 2016-10-20

Revert "Merge branch 'fix-lp1619423' into snapuser-create"

This reverts commit 1b964281b506a7c5ba156730fd22d69091be957f, reversing
changes made to 3db49a7a586cab7df957ca0fca979b01431e49b3.

Scott Moser (smoser) wrote :

I merged this.
Had to fix flake8 tox and made one code change. basically in add_snap_user i returned if there was no 'email' and then un-indented the rest of that block.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_snap_config.py b/cloudinit/config/cc_snap_config.py
2new file mode 100644
3index 0000000..ffb0768
4--- /dev/null
5+++ b/cloudinit/config/cc_snap_config.py
6@@ -0,0 +1,183 @@
7+# vi: ts=4 expandtab
8+#
9+# Copyright (C) 2016 Canonical Ltd.
10+#
11+# Author: Ryan Harper <ryan.harper@canonical.com>
12+#
13+# This program is free software: you can redistribute it and/or modify
14+# it under the terms of the GNU General Public License version 3, as
15+# published by the Free Software Foundation.
16+#
17+# This program is distributed in the hope that it will be useful,
18+# but WITHOUT ANY WARRANTY; without even the implied warranty of
19+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+# GNU General Public License for more details.
21+#
22+# You should have received a copy of the GNU General Public License
23+# along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
25+"""
26+Snappy
27+------
28+**Summary:** snap_config modules allows configuration of snapd.
29+
30+This module uses the same ``snappy`` namespace for configuration but
31+acts only only a subset of the configuration.
32+
33+If ``assertions`` is set and the user has included a list of assertions
34+then cloud-init will collect the assertions into a single assertion file
35+and invoke ``snap ack <path to file with assertions>`` which will attempt
36+to load the provided assertions into the snapd assertion database.
37+
38+If ``email`` is set, this value is used to create an authorized user for
39+contacting and installing snaps from the Ubuntu Store. This is done by
40+calling ``snap create-user`` command.
41+
42+If ``known`` is set to True, then it is expected the user also included
43+an assertion of type ``system-user``. When ``snap create-user`` is called
44+cloud-init will append '--known' flag which instructs snapd to look for
45+a system-user assertion with the details. If ``known`` is not set, then
46+``snap create-user`` will contact the Ubuntu SSO for validating and importing
47+a system-user for the instance.
48+
49+.. note::
50+ If the system is already managed, then cloud-init will not attempt to
51+ create a system-user.
52+
53+**Internal name:** ``cc_snap_config``
54+
55+**Module frequency:** per instance
56+
57+**Supported distros:** any with 'snapd' available
58+
59+**Config keys**::
60+
61+ #cloud-config
62+ snappy:
63+ assertions:
64+ - |
65+ <assertion 1>
66+ - |
67+ <assertion 2>
68+ email: user@user.org
69+ known: true
70+
71+"""
72+
73+from cloudinit import log as logging
74+from cloudinit.settings import PER_INSTANCE
75+from cloudinit import util
76+
77+LOG = logging.getLogger(__name__)
78+
79+frequency = PER_INSTANCE
80+SNAPPY_CMD = "snap"
81+ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions"
82+
83+
84+"""
85+snappy:
86+ assertions:
87+ - |
88+ <snap assertion 1>
89+ - |
90+ <snap assertion 2>
91+ email: foo@foo.io
92+ known: true
93+"""
94+
95+
96+def add_assertions(assertions=None):
97+ """ Import list of assertions by concatenating each
98+ assertion into a string separated by a '\n'.
99+ Write this string to a instance file and
100+ then invoke `snap ack /path/to/file`
101+ and check for errors.
102+
103+ If snap exits 0, then all assertions are imported.
104+ """
105+ if not assertions:
106+ assertions = []
107+
108+ if not isinstance(assertions, list):
109+ raise ValueError('assertion parameter was not a list: %s', assertions)
110+
111+ snap_cmd = [SNAPPY_CMD, 'ack']
112+ combined = "\n".join(assertions)
113+ if len(combined) == 0:
114+ raise ValueError("Assertion list is empty")
115+
116+ for asrt in assertions:
117+ LOG.debug('Acking: %s', asrt.split('\n')[0:2])
118+
119+ util.write_file(ASSERTIONS_FILE, combined.encode('utf-8'))
120+ util.subp(snap_cmd + [ASSERTIONS_FILE], capture=True)
121+
122+
123+def add_snap_user(cfg=None):
124+ """ Add a snap system-user if provided with email value
125+ under snappy config.
126+ - Check that system is not already managed.
127+ - Check that if using a system-user assertion, that it's
128+ imported into snapd.
129+
130+ Returns a dictionary to be passed to Distro.create_user
131+ """
132+
133+ if not cfg:
134+ cfg = {}
135+
136+ if not isinstance(cfg, dict):
137+ raise ValueError('configuration parameter was not a dict: %s', cfg)
138+
139+ snapuser = cfg.get('email', None)
140+ if snapuser:
141+ usercfg = {
142+ 'snapuser': snapuser,
143+ 'known': cfg.get('known', False),
144+ }
145+
146+ # query if we're already registered
147+ out, _ = util.subp([SNAPPY_CMD, 'managed'], capture=True)
148+ if out.strip() == "true":
149+ LOG.warning('This device is already managed. '
150+ 'Skipping system-user creation')
151+ return
152+
153+ if usercfg.get('known'):
154+ # Check that we imported a system-user assertion
155+ out, _ = util.subp([SNAPPY_CMD, 'known', 'system-user'],
156+ capture=True)
157+ if len(out) == 0:
158+ LOG.error('Missing "system-user" assertion. '
159+ 'Check "snappy" user-data assertions.')
160+ return
161+
162+ return usercfg
163+
164+
165+def handle(name, cfg, cloud, log, args):
166+ cfgin = cfg.get('snappy')
167+ if not cfgin:
168+ LOG.debug('No snappy config provided, skipping')
169+ return
170+
171+ if not(util.system_is_snappy()):
172+ LOG.debug("%s: system not snappy", name)
173+ return
174+
175+ assertions = cfgin.get('assertions', [])
176+ if len(assertions) > 0:
177+ LOG.debug('Importing user-provided snap assertions')
178+ add_assertions(assertions)
179+
180+ # Create a snap user if requested.
181+ # Snap systems contact the store with a user's email
182+ # and extract information needed to create a local user.
183+ # A user may provide a 'system-user' assertion which includes
184+ # the required information. Using such an assertion to create
185+ # a local user requires specifying 'known: true' in the supplied
186+ # user-data.
187+ usercfg = add_snap_user(cfg=cfgin)
188+ if usercfg:
189+ cloud.distro.create_user(usercfg.get('snapuser'), **usercfg)
190diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
191index 36db9e6..e03ec48 100644
192--- a/cloudinit/config/cc_snappy.py
193+++ b/cloudinit/config/cc_snappy.py
194@@ -257,24 +257,14 @@ def disable_enable_ssh(enabled):
195 util.write_file(not_to_be_run, "cloud-init\n")
196
197
198-def system_is_snappy():
199- # channel.ini is configparser loadable.
200- # snappy will move to using /etc/system-image/config.d/*.ini
201- # this is certainly not a perfect test, but good enough for now.
202- content = util.load_file("/etc/system-image/channel.ini", quiet=True)
203- if 'ubuntu-core' in content.lower():
204- return True
205- if os.path.isdir("/etc/system-image/config.d/"):
206- return True
207- return False
208-
209-
210 def set_snappy_command():
211 global SNAPPY_CMD
212 if util.which("snappy-go"):
213 SNAPPY_CMD = "snappy-go"
214- else:
215+ elif util.which("snappy"):
216 SNAPPY_CMD = "snappy"
217+ else:
218+ SNAPPY_CMD = "snap"
219 LOG.debug("snappy command is '%s'", SNAPPY_CMD)
220
221
222@@ -289,7 +279,7 @@ def handle(name, cfg, cloud, log, args):
223 LOG.debug("%s: System is not snappy. disabling", name)
224 return
225
226- if sys_snappy.lower() == "auto" and not(system_is_snappy()):
227+ if sys_snappy.lower() == "auto" and not(util.system_is_snappy()):
228 LOG.debug("%s: 'auto' mode, and system not snappy", name)
229 return
230
231diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
232index 78adf5f..4a72643 100755
233--- a/cloudinit/distros/__init__.py
234+++ b/cloudinit/distros/__init__.py
235@@ -367,6 +367,9 @@ class Distro(object):
236
237 adduser_cmd = ['useradd', name]
238 log_adduser_cmd = ['useradd', name]
239+ if util.system_is_snappy():
240+ adduser_cmd.append('--extrausers')
241+ log_adduser_cmd.append('--extrausers')
242
243 # Since we are creating users, we want to carefully validate the
244 # inputs. If something goes wrong, we can end up with a system
245@@ -445,6 +448,32 @@ class Distro(object):
246 util.logexc(LOG, "Failed to create user %s", name)
247 raise e
248
249+ def add_snap_user(self, name, **kwargs):
250+ """
251+ Add a snappy user to the system using snappy tools
252+ """
253+
254+ snapuser = kwargs.get('snapuser')
255+ known = kwargs.get('known', False)
256+ adduser_cmd = ["snap", "create-user", "--sudoer", "--json"]
257+ if known:
258+ adduser_cmd.append("--known")
259+ adduser_cmd.append(snapuser)
260+
261+ # Run the command
262+ LOG.debug("Adding snap user %s", name)
263+ try:
264+ (out, err) = util.subp(adduser_cmd, logstring=adduser_cmd,
265+ capture=True)
266+ LOG.debug("snap create-user returned: %s:%s", out, err)
267+ jobj = util.load_json(out)
268+ username = jobj.get('username', None)
269+ except Exception as e:
270+ util.logexc(LOG, "Failed to create snap user %s", name)
271+ raise e
272+
273+ return username
274+
275 def create_user(self, name, **kwargs):
276 """
277 Creates users for the system using the GNU passwd tools. This
278@@ -452,6 +481,10 @@ class Distro(object):
279 distros where useradd is not desirable or not available.
280 """
281
282+ # Add a snap user, if requested
283+ if 'snapuser' in kwargs:
284+ return self.add_snap_user(name, **kwargs)
285+
286 # Add the user
287 self.add_user(name, **kwargs)
288
289@@ -602,6 +635,8 @@ class Distro(object):
290
291 def create_group(self, name, members=None):
292 group_add_cmd = ['groupadd', name]
293+ if util.system_is_snappy():
294+ group_add_cmd.append('--extrausers')
295 if not members:
296 members = []
297
298diff --git a/cloudinit/util.py b/cloudinit/util.py
299index eb3e589..3a698df 100644
300--- a/cloudinit/util.py
301+++ b/cloudinit/util.py
302@@ -2377,3 +2377,15 @@ def get_installed_packages(target=None):
303 pkgs_inst.add(re.sub(":.*", "", pkg))
304
305 return pkgs_inst
306+
307+
308+def system_is_snappy():
309+ # channel.ini is configparser loadable.
310+ # snappy will move to using /etc/system-image/config.d/*.ini
311+ # this is certainly not a perfect test, but good enough for now.
312+ content = load_file("/etc/system-image/channel.ini", quiet=True)
313+ if 'ubuntu-core' in content.lower():
314+ return True
315+ if os.path.isdir("/etc/system-image/config.d/"):
316+ return True
317+ return False
318diff --git a/config/cloud.cfg b/config/cloud.cfg
319index d608dc8..1b93e7f 100644
320--- a/config/cloud.cfg
321+++ b/config/cloud.cfg
322@@ -45,6 +45,7 @@ cloud_config_modules:
323 # Emit the cloud config ready event
324 # this can be used by upstart jobs for 'start on cloud-config'.
325 - emit_upstart
326+ - snap_config
327 - ssh-import-id
328 - locale
329 - set-passwords
330diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
331index 0e8ed24..d0082f3 100644
332--- a/doc/examples/cloud-config-user-groups.txt
333+++ b/doc/examples/cloud-config-user-groups.txt
334@@ -30,6 +30,7 @@ users:
335 gecos: Magic Cloud App Daemon User
336 inactive: true
337 system: true
338+ - snapuser: joe@joeuser.io
339
340 # Valid Values:
341 # name: The user's login name
342@@ -80,6 +81,13 @@ users:
343 # cloud-init does not parse/check the syntax of the sudo
344 # directive.
345 # system: Create the user as a system user. This means no home directory.
346+# snapuser: Create a Snappy (Ubuntu-Core) user via the snap create-user
347+# command available on Ubuntu systems. If the user has an account
348+# on the Ubuntu SSO, specifying the email will allow snap to
349+# request a username and any public ssh keys and will import
350+# these into the system with username specifed by SSO account.
351+# If 'username' is not set in SSO, then username will be the
352+# shortname before the email domain.
353 #
354
355 # Default user creation:
356diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py
357index b24888f..33bf922 100755
358--- a/tests/unittests/test_distros/test_user_data_normalize.py
359+++ b/tests/unittests/test_distros/test_user_data_normalize.py
360@@ -4,6 +4,7 @@ from cloudinit import helpers
361 from cloudinit import settings
362
363 from ..helpers import TestCase
364+import mock
365
366
367 bcfg = {
368@@ -296,3 +297,67 @@ class TestUGNormalize(TestCase):
369 self.assertIn('bob', users)
370 self.assertEqual({'default': False}, users['joe'])
371 self.assertEqual({'default': False}, users['bob'])
372+
373+ @mock.patch('cloudinit.util.subp')
374+ def test_create_snap_user(self, mock_subp):
375+ mock_subp.side_effect = [('{"username": "joe", "ssh-key-count": 1}\n',
376+ '')]
377+ distro = self._make_distro('ubuntu')
378+ ug_cfg = {
379+ 'users': [
380+ {'name': 'joe', 'snapuser': 'joe@joe.com'},
381+ ],
382+ }
383+ (users, _groups) = self._norm(ug_cfg, distro)
384+ for (user, config) in users.items():
385+ print('user=%s config=%s' % (user, config))
386+ username = distro.create_user(user, **config)
387+
388+ snapcmd = ['snap', 'create-user', '--sudoer', '--json', 'joe@joe.com']
389+ mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
390+ self.assertEqual(username, 'joe')
391+
392+ @mock.patch('cloudinit.util.subp')
393+ def test_create_snap_user_known(self, mock_subp):
394+ mock_subp.side_effect = [('{"username": "joe", "ssh-key-count": 1}\n',
395+ '')]
396+ distro = self._make_distro('ubuntu')
397+ ug_cfg = {
398+ 'users': [
399+ {'name': 'joe', 'snapuser': 'joe@joe.com', 'known': True},
400+ ],
401+ }
402+ (users, _groups) = self._norm(ug_cfg, distro)
403+ for (user, config) in users.items():
404+ print('user=%s config=%s' % (user, config))
405+ username = distro.create_user(user, **config)
406+
407+ snapcmd = ['snap', 'create-user', '--sudoer', '--json', '--known',
408+ 'joe@joe.com']
409+ mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
410+ self.assertEqual(username, 'joe')
411+
412+ @mock.patch('cloudinit.util.system_is_snappy')
413+ @mock.patch('cloudinit.util.is_group')
414+ @mock.patch('cloudinit.util.subp')
415+ def test_add_user_on_snappy_system(self, mock_subp, mock_isgrp,
416+ mock_snappy):
417+ mock_isgrp.return_value = False
418+ mock_subp.return_value = True
419+ mock_snappy.return_value = True
420+ distro = self._make_distro('ubuntu')
421+ ug_cfg = {
422+ 'users': [
423+ {'name': 'joe', 'groups': 'users', 'create_groups': True},
424+ ],
425+ }
426+ (users, _groups) = self._norm(ug_cfg, distro)
427+ for (user, config) in users.items():
428+ print('user=%s config=%s' % (user, config))
429+ distro.add_user(user, **config)
430+
431+ groupcmd = ['groupadd', 'users', '--extrausers']
432+ addcmd = ['useradd', 'joe', '--extrausers', '--groups', 'users', '-m']
433+
434+ mock_subp.assert_any_call(groupcmd)
435+ mock_subp.assert_any_call(addcmd, logstring=addcmd)
436diff --git a/tests/unittests/test_handler/test_handler_snappy.py b/tests/unittests/test_handler/test_handler_snappy.py
437index 57dce1b..e320dd8 100644
438--- a/tests/unittests/test_handler/test_handler_snappy.py
439+++ b/tests/unittests/test_handler/test_handler_snappy.py
440@@ -1,14 +1,22 @@
441 from cloudinit.config.cc_snappy import (
442 makeop, get_package_ops, render_snap_op)
443-from cloudinit import util
444+from cloudinit.config.cc_snap_config import (
445+ add_assertions, add_snap_user, ASSERTIONS_FILE)
446+from cloudinit import (distros, helpers, cloud, util)
447+from cloudinit.config.cc_snap_config import handle as snap_handle
448+from cloudinit.sources import DataSourceNone
449+from ..helpers import FilesystemMockingTestCase, mock
450
451 from .. import helpers as t_help
452
453+import logging
454 import os
455 import shutil
456 import tempfile
457+import textwrap
458 import yaml
459
460+LOG = logging.getLogger(__name__)
461 ALLOWED = (dict, list, int, str)
462
463
464@@ -287,6 +295,289 @@ class TestInstallPackages(t_help.TestCase):
465 self.assertEqual(yaml.safe_load(mydata), data_found)
466
467
468+class TestSnapConfig(FilesystemMockingTestCase):
469+
470+ SYSTEM_USER_ASSERTION = textwrap.dedent("""
471+ type: system-user
472+ authority-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
473+ brand-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
474+ email: foo@bar.com
475+ password: $6$E5YiAuMIPAwX58jG$miomhVNui/vf7f/3ctB/f0RWSKFxG0YXzrJ9rtJ1ikvzt
476+ series:
477+ - 16
478+ since: 2016-09-10T16:34:00+03:00
479+ until: 2017-11-10T16:34:00+03:00
480+ username: baz
481+ sign-key-sha3-384: RuVvnp4n52GilycjfbbTCI3_L8Y6QlIE75wxMc0KzGV3AUQqVd9GuXoj
482+
483+ AcLBXAQAAQoABgUCV/UU1wAKCRBKnlMoJQLkZVeLD/9/+hIeVywtzsDA3oxl+P+u9D13y9s6svP
484+ Jd6Wnf4FTw6sq1GjBE4ZA7lrwSaRCUJ9Vcsvf2q9OGPY7mOb2TBxaDe0PbUMjrSrqllSSQwhpNI
485+ zG+NxkkKuxsUmLzFa+k9m6cyojNbw5LFhQZBQCGlr3JYqC0tIREq/UsZxj+90TUC87lDJwkU8GF
486+ s4CR+rejZj4itIcDcVxCSnJH6hv6j2JrJskJmvObqTnoOlcab+JXdamXqbldSP3UIhWoyVjqzkj
487+ +to7mXgx+cCUA9+ngNCcfUG+1huGGTWXPCYkZ78HvErcRlIdeo4d3xwtz1cl/w3vYnq9og1XwsP
488+ Yfetr3boig2qs1Y+j/LpsfYBYncgWjeDfAB9ZZaqQz/oc8n87tIPZDJHrusTlBfop8CqcM4xsKS
489+ d+wnEY8e/F24mdSOYmS1vQCIDiRU3MKb6x138Ud6oHXFlRBbBJqMMctPqWDunWzb5QJ7YR0I39q
490+ BrnEqv5NE0G7w6HOJ1LSPG5Hae3P4T2ea+ATgkb03RPr3KnXnzXg4TtBbW1nytdlgoNc/BafE1H
491+ f3NThcq9gwX4xWZ2PAWnqVPYdDMyCtzW3Ck+o6sIzx+dh4gDLPHIi/6TPe/pUuMop9CBpWwez7V
492+ v1z+1+URx6Xlq3Jq18y5pZ6fY3IDJ6km2nQPMzcm4Q==""")
493+
494+ ACCOUNT_ASSERTION = textwrap.dedent("""
495+ type: account-key
496+ authority-id: canonical
497+ revision: 2
498+ public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0
499+ account-id: canonical
500+ name: store
501+ since: 2016-04-01T00:00:00.0Z
502+ body-length: 717
503+ sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswH
504+
505+ AcbBTQRWhcGAARAA0KKYYQWuHOrsFVi4p4l7ZzSvX7kLgJFFeFgOkzdWKBTHEnsMKjl5mefFe9j
506+ qe8NlmJdfY7BenP7XeBtwKp700H/t9lLrZbpTNAPHXYxEWFJp5bPqIcJYBZ+29oLVLN1Tc5X482
507+ vCiDqL8+pPYqBrK2fNlyPlNNSum9wI70rDDL4r6FVvr+osTnGejibdV8JphWX+lrSQDnRSdM8KJ
508+ UM43vTgLGTi9W54oRhsA2OFexRfRksTrnqGoonCjqX5wO3OFSaMDzMsO2MJ/hPfLgDqw53qjzuK
509+ Iec9OL3k5basvu2cj5u9tKwVFDsCKK2GbKUsWWpx2KTpOifmhmiAbzkTHbH9KaoMS7p0kJwhTQG
510+ o9aJ9VMTWHJc/NCBx7eu451u6d46sBPCXS/OMUh2766fQmoRtO1OwCTxsRKG2kkjbMn54UdFULl
511+ VfzvyghMNRKIezsEkmM8wueTqGUGZWa6CEZqZKwhe/PROxOPYzqtDH18XZknbU1n5lNb7vNfem9
512+ 2ai+3+JyFnW9UhfvpVF7gzAgdyCqNli4C6BIN43uwoS8HkykocZS/+Gv52aUQ/NZ8BKOHLw+7an
513+ Q0o8W9ltSLZbEMxFIPSN0stiZlkXAp6DLyvh1Y4wXSynDjUondTpej2fSvSlCz/W5v5V7qA4nIc
514+ vUvV7RjVzv17ut0AEQEAAQ==
515+
516+ AcLDXAQAAQoABgUCV83k9QAKCRDUpVvql9g3IBT8IACKZ7XpiBZ3W4lqbPssY6On81WmxQLtvsM
517+ WTp6zZpl/wWOSt2vMNUk9pvcmrNq1jG9CuhDfWFLGXEjcrrmVkN3YuCOajMSPFCGrxsIBLSRt/b
518+ nrKykdLAAzMfG8rP1d82bjFFiIieE+urQ0Kcv09Jtdvavq3JT1Tek5mFyyfhHNlQEKOzWqmRWiL
519+ 3c3VOZUs1ZD8TSlnuq/x+5T0X0YtOyGjSlVxk7UybbyMNd6MZfNaMpIG4x+mxD3KHFtBAC7O6kL
520+ eX3i6j5nCY5UABfA3DZEAkWP4zlmdBEOvZ9t293NaDdOpzsUHRkoi0Zez/9BHQ/kwx/uNc2WqrY
521+ inCmu16JGNeXqsyinnLl7Ghn2RwhvDMlLxF6RTx8xdx1yk6p3PBTwhZMUvuZGjUtN/AG8BmVJQ1
522+ rsGSRkkSywvnhVJRB2sudnrMBmNS2goJbzSbmJnOlBrd2WsV0T9SgNMWZBiov3LvU4o2SmAb6b+
523+ rYwh8H5QHcuuYJuxDjFhPswIp6Wes5T6hUicf3SWtObcDS4HSkVS4ImBjjX9YgCuFy7QdnooOWE
524+ aPvkRw3XCVeYq0K6w9GRsk1YFErD4XmXXZjDYY650MX9v42Sz5MmphHV8jdIY5ssbadwFSe2rCQ
525+ 6UX08zy7RsIb19hTndE6ncvSNDChUR9eEnCm73eYaWTWTnq1cxdVP/s52r8uss++OYOkPWqh5nO
526+ haRn7INjH/yZX4qXjNXlTjo0PnHH0q08vNKDwLhxS+D9du+70FeacXFyLIbcWllSbJ7DmbumGpF
527+ yYbtj3FDDPzachFQdIG3lSt+cSUGeyfSs6wVtc3cIPka/2Urx7RprfmoWSI6+a5NcLdj0u2z8O9
528+ HxeIgxDpg/3gT8ZIuFKePMcLDM19Fh/p0ysCsX+84B9chNWtsMSmIaE57V+959MVtsLu7SLb9gi
529+ skrju0pQCwsu2wHMLTNd1f3PTHmrr49hxetTus07HSQUApMtAGKzQilF5zqFjbyaTd4xgQbd+PK
530+ CjFyzQTDOcUhXpuUGt/IzlqiFfsCsmbj2K4KdSNYMlqIgZ3Azu8KvZLIhsyN7v5vNIZSPfEbjde
531+ ClU9r0VRiJmtYBUjcSghD9LWn+yRLwOxhfQVjm0cBwIt5R/yPF/qC76yIVuWUtM5Y2/zJR1J8OF
532+ qWchvlImHtvDzS9FQeLyzJAOjvZ2CnWp2gILgUz0WQdOk1Dq8ax7KS9BQ42zxw9EZAEPw3PEFqR
533+ IQsRTONp+iVS8YxSmoYZjDlCgRMWUmawez/Fv5b9Fb/XkO5Eq4e+KfrpUujXItaipb+tV8h5v3t
534+ oG3Ie3WOHrVjCLXIdYslpL1O4nadqR6Xv58pHj6k""")
535+
536+ test_assertions = [ACCOUNT_ASSERTION, SYSTEM_USER_ASSERTION]
537+
538+ def setUp(self):
539+ super(TestSnapConfig, self).setUp()
540+ self.subp = util.subp
541+ self.new_root = tempfile.mkdtemp()
542+ self.addCleanup(shutil.rmtree, self.new_root)
543+
544+ def _get_cloud(self, distro, metadata=None):
545+ self.patchUtils(self.new_root)
546+ paths = helpers.Paths({})
547+ cls = distros.fetch(distro)
548+ mydist = cls(distro, {}, paths)
549+ myds = DataSourceNone.DataSourceNone({}, mydist, paths)
550+ if metadata:
551+ myds.metadata.update(metadata)
552+ return cloud.Cloud(myds, paths, {}, mydist, None)
553+
554+ @mock.patch('cloudinit.util.write_file')
555+ @mock.patch('cloudinit.util.subp')
556+ def test_snap_config_add_assertions(self, msubp, mwrite):
557+ add_assertions(self.test_assertions)
558+
559+ combined = "\n".join(self.test_assertions)
560+ mwrite.assert_any_call(ASSERTIONS_FILE, combined.encode('utf-8'))
561+ msubp.assert_called_with(['snap', 'ack', ASSERTIONS_FILE],
562+ capture=True)
563+
564+ def test_snap_config_add_assertions_empty(self):
565+ self.assertRaises(ValueError, add_assertions, [])
566+
567+ def test_add_assertions_nonlist(self):
568+ self.assertRaises(ValueError, add_assertions, {})
569+
570+ @mock.patch('cloudinit.util.write_file')
571+ @mock.patch('cloudinit.util.subp')
572+ def test_snap_config_add_assertions_ack_fails(self, msubp, mwrite):
573+ msubp.side_effect = [util.ProcessExecutionError("Invalid assertion")]
574+ self.assertRaises(util.ProcessExecutionError, add_assertions,
575+ self.test_assertions)
576+
577+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
578+ @mock.patch('cloudinit.config.cc_snap_config.util')
579+ def test_snap_config_handle_no_config(self, mock_util, mock_add):
580+ cfg = {}
581+ cc = self._get_cloud('ubuntu')
582+ cc.distro = mock.MagicMock()
583+ cc.distro.name = 'ubuntu'
584+ mock_util.which.return_value = None
585+ snap_handle('snap_config', cfg, cc, LOG, None)
586+ mock_add.assert_not_called()
587+
588+ def test_snap_config_add_snap_user_no_config(self):
589+ usercfg = add_snap_user(cfg=None)
590+ self.assertEqual(usercfg, None)
591+
592+ def test_snap_config_add_snap_user_not_dict(self):
593+ cfg = ['foobar']
594+ self.assertRaises(ValueError, add_snap_user, cfg)
595+
596+ def test_snap_config_add_snap_user_no_email(self):
597+ cfg = {'assertions': [], 'known': True}
598+ usercfg = add_snap_user(cfg=cfg)
599+ self.assertEqual(usercfg, None)
600+
601+ @mock.patch('cloudinit.config.cc_snap_config.util')
602+ def test_snap_config_add_snap_user_email_only(self, mock_util):
603+ email = 'janet@planetjanet.org'
604+ cfg = {'email': email}
605+ mock_util.which.return_value = None
606+ mock_util.system_is_snappy.return_value = True
607+ mock_util.subp.side_effect = [
608+ ("false\n", ""), # snap managed
609+ ]
610+
611+ usercfg = add_snap_user(cfg=cfg)
612+
613+ self.assertEqual(usercfg, {'snapuser': email, 'known': False})
614+
615+ @mock.patch('cloudinit.config.cc_snap_config.util')
616+ def test_snap_config_add_snap_user_email_known(self, mock_util):
617+ email = 'janet@planetjanet.org'
618+ known = True
619+ cfg = {'email': email, 'known': known}
620+ mock_util.which.return_value = None
621+ mock_util.system_is_snappy.return_value = True
622+ mock_util.subp.side_effect = [
623+ ("false\n", ""), # snap managed
624+ (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
625+ ]
626+
627+ usercfg = add_snap_user(cfg=cfg)
628+
629+ self.assertEqual(usercfg, {'snapuser': email, 'known': known})
630+
631+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
632+ @mock.patch('cloudinit.config.cc_snap_config.util')
633+ def test_snap_config_handle_system_not_snappy(self, mock_util, mock_add):
634+ cfg = {'snappy': {'assertions': self.test_assertions}}
635+ cc = self._get_cloud('ubuntu')
636+ cc.distro = mock.MagicMock()
637+ cc.distro.name = 'ubuntu'
638+ mock_util.which.return_value = None
639+ mock_util.system_is_snappy.return_value = False
640+
641+ snap_handle('snap_config', cfg, cc, LOG, None)
642+
643+ mock_add.assert_not_called()
644+
645+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
646+ @mock.patch('cloudinit.config.cc_snap_config.util')
647+ def test_snap_config_handle_snapuser(self, mock_util, mock_add):
648+ email = 'janet@planetjanet.org'
649+ cfg = {
650+ 'snappy': {
651+ 'assertions': self.test_assertions,
652+ 'email': email,
653+ }
654+ }
655+ cc = self._get_cloud('ubuntu')
656+ cc.distro = mock.MagicMock()
657+ cc.distro.name = 'ubuntu'
658+ mock_util.which.return_value = None
659+ mock_util.system_is_snappy.return_value = True
660+ mock_util.subp.side_effect = [
661+ ("false\n", ""), # snap managed
662+ ]
663+
664+ snap_handle('snap_config', cfg, cc, LOG, None)
665+
666+ mock_add.assert_called_with(self.test_assertions)
667+ usercfg = {'snapuser': email, 'known': False}
668+ cc.distro.create_user.assert_called_with(email, **usercfg)
669+
670+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
671+ @mock.patch('cloudinit.config.cc_snap_config.util')
672+ def test_snap_config_handle_snapuser_known(self, mock_util, mock_add):
673+ email = 'janet@planetjanet.org'
674+ cfg = {
675+ 'snappy': {
676+ 'assertions': self.test_assertions,
677+ 'email': email,
678+ 'known': True,
679+ }
680+ }
681+ cc = self._get_cloud('ubuntu')
682+ cc.distro = mock.MagicMock()
683+ cc.distro.name = 'ubuntu'
684+ mock_util.which.return_value = None
685+ mock_util.system_is_snappy.return_value = True
686+ mock_util.subp.side_effect = [
687+ ("false\n", ""), # snap managed
688+ (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
689+ ]
690+
691+ snap_handle('snap_config', cfg, cc, LOG, None)
692+
693+ mock_add.assert_called_with(self.test_assertions)
694+ usercfg = {'snapuser': email, 'known': True}
695+ cc.distro.create_user.assert_called_with(email, **usercfg)
696+
697+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
698+ @mock.patch('cloudinit.config.cc_snap_config.util')
699+ def test_snap_config_handle_snapuser_known_managed(self, mock_util,
700+ mock_add):
701+ email = 'janet@planetjanet.org'
702+ cfg = {
703+ 'snappy': {
704+ 'assertions': self.test_assertions,
705+ 'email': email,
706+ 'known': True,
707+ }
708+ }
709+ cc = self._get_cloud('ubuntu')
710+ cc.distro = mock.MagicMock()
711+ cc.distro.name = 'ubuntu'
712+ mock_util.which.return_value = None
713+ mock_util.system_is_snappy.return_value = True
714+ mock_util.subp.side_effect = [
715+ ("true\n", ""), # snap managed
716+ ]
717+
718+ snap_handle('snap_config', cfg, cc, LOG, None)
719+
720+ mock_add.assert_called_with(self.test_assertions)
721+ cc.distro.create_user.assert_not_called()
722+
723+ @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
724+ @mock.patch('cloudinit.config.cc_snap_config.util')
725+ def test_snap_config_handle_snapuser_known_no_assertion(self, mock_util,
726+ mock_add):
727+ email = 'janet@planetjanet.org'
728+ cfg = {
729+ 'snappy': {
730+ 'assertions': [self.ACCOUNT_ASSERTION],
731+ 'email': email,
732+ 'known': True,
733+ }
734+ }
735+ cc = self._get_cloud('ubuntu')
736+ cc.distro = mock.MagicMock()
737+ cc.distro.name = 'ubuntu'
738+ mock_util.which.return_value = None
739+ mock_util.system_is_snappy.return_value = True
740+ mock_util.subp.side_effect = [
741+ ("true\n", ""), # snap managed
742+ ("", ""), # snap known system-user
743+ ]
744+
745+ snap_handle('snap_config', cfg, cc, LOG, None)
746+
747+ mock_add.assert_called_with([self.ACCOUNT_ASSERTION])
748+ cc.distro.create_user.assert_not_called()
749+
750+
751 def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):
752 if cfgfile:
753 cfgfile = os.path.sep.join([tmpd, cfgfile])

Subscribers

People subscribed via source and target branches