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

Proposed by Ryan Harper
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 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
90577ef... by Ryan Harper

Update snapuser example

Revision history for this message
Scott Moser (smoser) wrote :

Is this still needed or desired ?

~raharper/cloud-init:snapuser-create updated
ba627b5... by Ryan Harper

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

e5a6164... by Ryan Harper

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

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

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

76774db... by Ryan Harper

snappy: try harder to import system-user assertion

b543c0f... by Ryan Harper

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

3db49a7... by Ryan Harper

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.

Revision history for this message
Jon Grimm (jgrimm) wrote :

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

Revision history for this message
Ryan Harper (raharper) wrote :

Thanks for looking at the code!

~raharper/cloud-init:snapuser-create updated
a55ae1f... by Ryan Harper

debian: add explicit dependencies for required binaries

Revision history for this message
Jon Grimm (jgrimm) wrote :

Thanks, it was enlightening and easy to read.

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

Merge branch 'fix-lp1619423' into snapuser-create

Revision history for this message
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).

Revision history for this message
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...

Revision history for this message
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
53d8cb4... by Ryan Harper

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

Revision history for this message
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
a353f18... by Ryan Harper

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

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

Revision history for this message
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
diff --git a/cloudinit/config/cc_snap_config.py b/cloudinit/config/cc_snap_config.py
0new file mode 1006440new file mode 100644
index 0000000..ffb0768
--- /dev/null
+++ b/cloudinit/config/cc_snap_config.py
@@ -0,0 +1,183 @@
1# vi: ts=4 expandtab
2#
3# Copyright (C) 2016 Canonical Ltd.
4#
5# Author: Ryan Harper <ryan.harper@canonical.com>
6#
7# This program is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License version 3, as
9# published by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""
20Snappy
21------
22**Summary:** snap_config modules allows configuration of snapd.
23
24This module uses the same ``snappy`` namespace for configuration but
25acts only only a subset of the configuration.
26
27If ``assertions`` is set and the user has included a list of assertions
28then cloud-init will collect the assertions into a single assertion file
29and invoke ``snap ack <path to file with assertions>`` which will attempt
30to load the provided assertions into the snapd assertion database.
31
32If ``email`` is set, this value is used to create an authorized user for
33contacting and installing snaps from the Ubuntu Store. This is done by
34calling ``snap create-user`` command.
35
36If ``known`` is set to True, then it is expected the user also included
37an assertion of type ``system-user``. When ``snap create-user`` is called
38cloud-init will append '--known' flag which instructs snapd to look for
39a system-user assertion with the details. If ``known`` is not set, then
40``snap create-user`` will contact the Ubuntu SSO for validating and importing
41a system-user for the instance.
42
43.. note::
44 If the system is already managed, then cloud-init will not attempt to
45 create a system-user.
46
47**Internal name:** ``cc_snap_config``
48
49**Module frequency:** per instance
50
51**Supported distros:** any with 'snapd' available
52
53**Config keys**::
54
55 #cloud-config
56 snappy:
57 assertions:
58 - |
59 <assertion 1>
60 - |
61 <assertion 2>
62 email: user@user.org
63 known: true
64
65"""
66
67from cloudinit import log as logging
68from cloudinit.settings import PER_INSTANCE
69from cloudinit import util
70
71LOG = logging.getLogger(__name__)
72
73frequency = PER_INSTANCE
74SNAPPY_CMD = "snap"
75ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions"
76
77
78"""
79snappy:
80 assertions:
81 - |
82 <snap assertion 1>
83 - |
84 <snap assertion 2>
85 email: foo@foo.io
86 known: true
87"""
88
89
90def add_assertions(assertions=None):
91 """ Import list of assertions by concatenating each
92 assertion into a string separated by a '\n'.
93 Write this string to a instance file and
94 then invoke `snap ack /path/to/file`
95 and check for errors.
96
97 If snap exits 0, then all assertions are imported.
98 """
99 if not assertions:
100 assertions = []
101
102 if not isinstance(assertions, list):
103 raise ValueError('assertion parameter was not a list: %s', assertions)
104
105 snap_cmd = [SNAPPY_CMD, 'ack']
106 combined = "\n".join(assertions)
107 if len(combined) == 0:
108 raise ValueError("Assertion list is empty")
109
110 for asrt in assertions:
111 LOG.debug('Acking: %s', asrt.split('\n')[0:2])
112
113 util.write_file(ASSERTIONS_FILE, combined.encode('utf-8'))
114 util.subp(snap_cmd + [ASSERTIONS_FILE], capture=True)
115
116
117def add_snap_user(cfg=None):
118 """ Add a snap system-user if provided with email value
119 under snappy config.
120 - Check that system is not already managed.
121 - Check that if using a system-user assertion, that it's
122 imported into snapd.
123
124 Returns a dictionary to be passed to Distro.create_user
125 """
126
127 if not cfg:
128 cfg = {}
129
130 if not isinstance(cfg, dict):
131 raise ValueError('configuration parameter was not a dict: %s', cfg)
132
133 snapuser = cfg.get('email', None)
134 if snapuser:
135 usercfg = {
136 'snapuser': snapuser,
137 'known': cfg.get('known', False),
138 }
139
140 # query if we're already registered
141 out, _ = util.subp([SNAPPY_CMD, 'managed'], capture=True)
142 if out.strip() == "true":
143 LOG.warning('This device is already managed. '
144 'Skipping system-user creation')
145 return
146
147 if usercfg.get('known'):
148 # Check that we imported a system-user assertion
149 out, _ = util.subp([SNAPPY_CMD, 'known', 'system-user'],
150 capture=True)
151 if len(out) == 0:
152 LOG.error('Missing "system-user" assertion. '
153 'Check "snappy" user-data assertions.')
154 return
155
156 return usercfg
157
158
159def handle(name, cfg, cloud, log, args):
160 cfgin = cfg.get('snappy')
161 if not cfgin:
162 LOG.debug('No snappy config provided, skipping')
163 return
164
165 if not(util.system_is_snappy()):
166 LOG.debug("%s: system not snappy", name)
167 return
168
169 assertions = cfgin.get('assertions', [])
170 if len(assertions) > 0:
171 LOG.debug('Importing user-provided snap assertions')
172 add_assertions(assertions)
173
174 # Create a snap user if requested.
175 # Snap systems contact the store with a user's email
176 # and extract information needed to create a local user.
177 # A user may provide a 'system-user' assertion which includes
178 # the required information. Using such an assertion to create
179 # a local user requires specifying 'known: true' in the supplied
180 # user-data.
181 usercfg = add_snap_user(cfg=cfgin)
182 if usercfg:
183 cloud.distro.create_user(usercfg.get('snapuser'), **usercfg)
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index 36db9e6..e03ec48 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -257,24 +257,14 @@ def disable_enable_ssh(enabled):
257 util.write_file(not_to_be_run, "cloud-init\n")257 util.write_file(not_to_be_run, "cloud-init\n")
258258
259259
260def system_is_snappy():
261 # channel.ini is configparser loadable.
262 # snappy will move to using /etc/system-image/config.d/*.ini
263 # this is certainly not a perfect test, but good enough for now.
264 content = util.load_file("/etc/system-image/channel.ini", quiet=True)
265 if 'ubuntu-core' in content.lower():
266 return True
267 if os.path.isdir("/etc/system-image/config.d/"):
268 return True
269 return False
270
271
272def set_snappy_command():260def set_snappy_command():
273 global SNAPPY_CMD261 global SNAPPY_CMD
274 if util.which("snappy-go"):262 if util.which("snappy-go"):
275 SNAPPY_CMD = "snappy-go"263 SNAPPY_CMD = "snappy-go"
276 else:264 elif util.which("snappy"):
277 SNAPPY_CMD = "snappy"265 SNAPPY_CMD = "snappy"
266 else:
267 SNAPPY_CMD = "snap"
278 LOG.debug("snappy command is '%s'", SNAPPY_CMD)268 LOG.debug("snappy command is '%s'", SNAPPY_CMD)
279269
280270
@@ -289,7 +279,7 @@ def handle(name, cfg, cloud, log, args):
289 LOG.debug("%s: System is not snappy. disabling", name)279 LOG.debug("%s: System is not snappy. disabling", name)
290 return280 return
291281
292 if sys_snappy.lower() == "auto" and not(system_is_snappy()):282 if sys_snappy.lower() == "auto" and not(util.system_is_snappy()):
293 LOG.debug("%s: 'auto' mode, and system not snappy", name)283 LOG.debug("%s: 'auto' mode, and system not snappy", name)
294 return284 return
295285
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py
index 78adf5f..4a72643 100755
--- a/cloudinit/distros/__init__.py
+++ b/cloudinit/distros/__init__.py
@@ -367,6 +367,9 @@ class Distro(object):
367367
368 adduser_cmd = ['useradd', name]368 adduser_cmd = ['useradd', name]
369 log_adduser_cmd = ['useradd', name]369 log_adduser_cmd = ['useradd', name]
370 if util.system_is_snappy():
371 adduser_cmd.append('--extrausers')
372 log_adduser_cmd.append('--extrausers')
370373
371 # Since we are creating users, we want to carefully validate the374 # Since we are creating users, we want to carefully validate the
372 # inputs. If something goes wrong, we can end up with a system375 # inputs. If something goes wrong, we can end up with a system
@@ -445,6 +448,32 @@ class Distro(object):
445 util.logexc(LOG, "Failed to create user %s", name)448 util.logexc(LOG, "Failed to create user %s", name)
446 raise e449 raise e
447450
451 def add_snap_user(self, name, **kwargs):
452 """
453 Add a snappy user to the system using snappy tools
454 """
455
456 snapuser = kwargs.get('snapuser')
457 known = kwargs.get('known', False)
458 adduser_cmd = ["snap", "create-user", "--sudoer", "--json"]
459 if known:
460 adduser_cmd.append("--known")
461 adduser_cmd.append(snapuser)
462
463 # Run the command
464 LOG.debug("Adding snap user %s", name)
465 try:
466 (out, err) = util.subp(adduser_cmd, logstring=adduser_cmd,
467 capture=True)
468 LOG.debug("snap create-user returned: %s:%s", out, err)
469 jobj = util.load_json(out)
470 username = jobj.get('username', None)
471 except Exception as e:
472 util.logexc(LOG, "Failed to create snap user %s", name)
473 raise e
474
475 return username
476
448 def create_user(self, name, **kwargs):477 def create_user(self, name, **kwargs):
449 """478 """
450 Creates users for the system using the GNU passwd tools. This479 Creates users for the system using the GNU passwd tools. This
@@ -452,6 +481,10 @@ class Distro(object):
452 distros where useradd is not desirable or not available.481 distros where useradd is not desirable or not available.
453 """482 """
454483
484 # Add a snap user, if requested
485 if 'snapuser' in kwargs:
486 return self.add_snap_user(name, **kwargs)
487
455 # Add the user488 # Add the user
456 self.add_user(name, **kwargs)489 self.add_user(name, **kwargs)
457490
@@ -602,6 +635,8 @@ class Distro(object):
602635
603 def create_group(self, name, members=None):636 def create_group(self, name, members=None):
604 group_add_cmd = ['groupadd', name]637 group_add_cmd = ['groupadd', name]
638 if util.system_is_snappy():
639 group_add_cmd.append('--extrausers')
605 if not members:640 if not members:
606 members = []641 members = []
607642
diff --git a/cloudinit/util.py b/cloudinit/util.py
index eb3e589..3a698df 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -2377,3 +2377,15 @@ def get_installed_packages(target=None):
2377 pkgs_inst.add(re.sub(":.*", "", pkg))2377 pkgs_inst.add(re.sub(":.*", "", pkg))
23782378
2379 return pkgs_inst2379 return pkgs_inst
2380
2381
2382def system_is_snappy():
2383 # channel.ini is configparser loadable.
2384 # snappy will move to using /etc/system-image/config.d/*.ini
2385 # this is certainly not a perfect test, but good enough for now.
2386 content = load_file("/etc/system-image/channel.ini", quiet=True)
2387 if 'ubuntu-core' in content.lower():
2388 return True
2389 if os.path.isdir("/etc/system-image/config.d/"):
2390 return True
2391 return False
diff --git a/config/cloud.cfg b/config/cloud.cfg
index d608dc8..1b93e7f 100644
--- a/config/cloud.cfg
+++ b/config/cloud.cfg
@@ -45,6 +45,7 @@ cloud_config_modules:
45# Emit the cloud config ready event45# Emit the cloud config ready event
46# this can be used by upstart jobs for 'start on cloud-config'.46# this can be used by upstart jobs for 'start on cloud-config'.
47 - emit_upstart47 - emit_upstart
48 - snap_config
48 - ssh-import-id49 - ssh-import-id
49 - locale50 - locale
50 - set-passwords51 - set-passwords
diff --git a/doc/examples/cloud-config-user-groups.txt b/doc/examples/cloud-config-user-groups.txt
index 0e8ed24..d0082f3 100644
--- a/doc/examples/cloud-config-user-groups.txt
+++ b/doc/examples/cloud-config-user-groups.txt
@@ -30,6 +30,7 @@ users:
30 gecos: Magic Cloud App Daemon User30 gecos: Magic Cloud App Daemon User
31 inactive: true31 inactive: true
32 system: true32 system: true
33 - snapuser: joe@joeuser.io
3334
34# Valid Values:35# Valid Values:
35# name: The user's login name36# name: The user's login name
@@ -80,6 +81,13 @@ users:
80# cloud-init does not parse/check the syntax of the sudo81# cloud-init does not parse/check the syntax of the sudo
81# directive.82# directive.
82# system: Create the user as a system user. This means no home directory.83# system: Create the user as a system user. This means no home directory.
84# snapuser: Create a Snappy (Ubuntu-Core) user via the snap create-user
85# command available on Ubuntu systems. If the user has an account
86# on the Ubuntu SSO, specifying the email will allow snap to
87# request a username and any public ssh keys and will import
88# these into the system with username specifed by SSO account.
89# If 'username' is not set in SSO, then username will be the
90# shortname before the email domain.
83#91#
8492
85# Default user creation:93# 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 b24888f..33bf922 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
4from cloudinit import settings4from cloudinit import settings
55
6from ..helpers import TestCase6from ..helpers import TestCase
7import mock
78
89
9bcfg = {10bcfg = {
@@ -296,3 +297,67 @@ class TestUGNormalize(TestCase):
296 self.assertIn('bob', users)297 self.assertIn('bob', users)
297 self.assertEqual({'default': False}, users['joe'])298 self.assertEqual({'default': False}, users['joe'])
298 self.assertEqual({'default': False}, users['bob'])299 self.assertEqual({'default': False}, users['bob'])
300
301 @mock.patch('cloudinit.util.subp')
302 def test_create_snap_user(self, mock_subp):
303 mock_subp.side_effect = [('{"username": "joe", "ssh-key-count": 1}\n',
304 '')]
305 distro = self._make_distro('ubuntu')
306 ug_cfg = {
307 'users': [
308 {'name': 'joe', 'snapuser': 'joe@joe.com'},
309 ],
310 }
311 (users, _groups) = self._norm(ug_cfg, distro)
312 for (user, config) in users.items():
313 print('user=%s config=%s' % (user, config))
314 username = distro.create_user(user, **config)
315
316 snapcmd = ['snap', 'create-user', '--sudoer', '--json', 'joe@joe.com']
317 mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
318 self.assertEqual(username, 'joe')
319
320 @mock.patch('cloudinit.util.subp')
321 def test_create_snap_user_known(self, mock_subp):
322 mock_subp.side_effect = [('{"username": "joe", "ssh-key-count": 1}\n',
323 '')]
324 distro = self._make_distro('ubuntu')
325 ug_cfg = {
326 'users': [
327 {'name': 'joe', 'snapuser': 'joe@joe.com', 'known': True},
328 ],
329 }
330 (users, _groups) = self._norm(ug_cfg, distro)
331 for (user, config) in users.items():
332 print('user=%s config=%s' % (user, config))
333 username = distro.create_user(user, **config)
334
335 snapcmd = ['snap', 'create-user', '--sudoer', '--json', '--known',
336 'joe@joe.com']
337 mock_subp.assert_called_with(snapcmd, capture=True, logstring=snapcmd)
338 self.assertEqual(username, 'joe')
339
340 @mock.patch('cloudinit.util.system_is_snappy')
341 @mock.patch('cloudinit.util.is_group')
342 @mock.patch('cloudinit.util.subp')
343 def test_add_user_on_snappy_system(self, mock_subp, mock_isgrp,
344 mock_snappy):
345 mock_isgrp.return_value = False
346 mock_subp.return_value = True
347 mock_snappy.return_value = True
348 distro = self._make_distro('ubuntu')
349 ug_cfg = {
350 'users': [
351 {'name': 'joe', 'groups': 'users', 'create_groups': True},
352 ],
353 }
354 (users, _groups) = self._norm(ug_cfg, distro)
355 for (user, config) in users.items():
356 print('user=%s config=%s' % (user, config))
357 distro.add_user(user, **config)
358
359 groupcmd = ['groupadd', 'users', '--extrausers']
360 addcmd = ['useradd', 'joe', '--extrausers', '--groups', 'users', '-m']
361
362 mock_subp.assert_any_call(groupcmd)
363 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 57dce1b..e320dd8 100644
--- a/tests/unittests/test_handler/test_handler_snappy.py
+++ b/tests/unittests/test_handler/test_handler_snappy.py
@@ -1,14 +1,22 @@
1from cloudinit.config.cc_snappy import (1from cloudinit.config.cc_snappy import (
2 makeop, get_package_ops, render_snap_op)2 makeop, get_package_ops, render_snap_op)
3from cloudinit import util3from cloudinit.config.cc_snap_config import (
4 add_assertions, add_snap_user, ASSERTIONS_FILE)
5from cloudinit import (distros, helpers, cloud, util)
6from cloudinit.config.cc_snap_config import handle as snap_handle
7from cloudinit.sources import DataSourceNone
8from ..helpers import FilesystemMockingTestCase, mock
49
5from .. import helpers as t_help10from .. import helpers as t_help
611
12import logging
7import os13import os
8import shutil14import shutil
9import tempfile15import tempfile
16import textwrap
10import yaml17import yaml
1118
19LOG = logging.getLogger(__name__)
12ALLOWED = (dict, list, int, str)20ALLOWED = (dict, list, int, str)
1321
1422
@@ -287,6 +295,289 @@ class TestInstallPackages(t_help.TestCase):
287 self.assertEqual(yaml.safe_load(mydata), data_found)295 self.assertEqual(yaml.safe_load(mydata), data_found)
288296
289297
298class TestSnapConfig(FilesystemMockingTestCase):
299
300 SYSTEM_USER_ASSERTION = textwrap.dedent("""
301 type: system-user
302 authority-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
303 brand-id: LqvZQdfyfGlYvtep4W6Oj6pFXP9t1Ksp
304 email: foo@bar.com
305 password: $6$E5YiAuMIPAwX58jG$miomhVNui/vf7f/3ctB/f0RWSKFxG0YXzrJ9rtJ1ikvzt
306 series:
307 - 16
308 since: 2016-09-10T16:34:00+03:00
309 until: 2017-11-10T16:34:00+03:00
310 username: baz
311 sign-key-sha3-384: RuVvnp4n52GilycjfbbTCI3_L8Y6QlIE75wxMc0KzGV3AUQqVd9GuXoj
312
313 AcLBXAQAAQoABgUCV/UU1wAKCRBKnlMoJQLkZVeLD/9/+hIeVywtzsDA3oxl+P+u9D13y9s6svP
314 Jd6Wnf4FTw6sq1GjBE4ZA7lrwSaRCUJ9Vcsvf2q9OGPY7mOb2TBxaDe0PbUMjrSrqllSSQwhpNI
315 zG+NxkkKuxsUmLzFa+k9m6cyojNbw5LFhQZBQCGlr3JYqC0tIREq/UsZxj+90TUC87lDJwkU8GF
316 s4CR+rejZj4itIcDcVxCSnJH6hv6j2JrJskJmvObqTnoOlcab+JXdamXqbldSP3UIhWoyVjqzkj
317 +to7mXgx+cCUA9+ngNCcfUG+1huGGTWXPCYkZ78HvErcRlIdeo4d3xwtz1cl/w3vYnq9og1XwsP
318 Yfetr3boig2qs1Y+j/LpsfYBYncgWjeDfAB9ZZaqQz/oc8n87tIPZDJHrusTlBfop8CqcM4xsKS
319 d+wnEY8e/F24mdSOYmS1vQCIDiRU3MKb6x138Ud6oHXFlRBbBJqMMctPqWDunWzb5QJ7YR0I39q
320 BrnEqv5NE0G7w6HOJ1LSPG5Hae3P4T2ea+ATgkb03RPr3KnXnzXg4TtBbW1nytdlgoNc/BafE1H
321 f3NThcq9gwX4xWZ2PAWnqVPYdDMyCtzW3Ck+o6sIzx+dh4gDLPHIi/6TPe/pUuMop9CBpWwez7V
322 v1z+1+URx6Xlq3Jq18y5pZ6fY3IDJ6km2nQPMzcm4Q==""")
323
324 ACCOUNT_ASSERTION = textwrap.dedent("""
325 type: account-key
326 authority-id: canonical
327 revision: 2
328 public-key-sha3-384: BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0
329 account-id: canonical
330 name: store
331 since: 2016-04-01T00:00:00.0Z
332 body-length: 717
333 sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswH
334
335 AcbBTQRWhcGAARAA0KKYYQWuHOrsFVi4p4l7ZzSvX7kLgJFFeFgOkzdWKBTHEnsMKjl5mefFe9j
336 qe8NlmJdfY7BenP7XeBtwKp700H/t9lLrZbpTNAPHXYxEWFJp5bPqIcJYBZ+29oLVLN1Tc5X482
337 vCiDqL8+pPYqBrK2fNlyPlNNSum9wI70rDDL4r6FVvr+osTnGejibdV8JphWX+lrSQDnRSdM8KJ
338 UM43vTgLGTi9W54oRhsA2OFexRfRksTrnqGoonCjqX5wO3OFSaMDzMsO2MJ/hPfLgDqw53qjzuK
339 Iec9OL3k5basvu2cj5u9tKwVFDsCKK2GbKUsWWpx2KTpOifmhmiAbzkTHbH9KaoMS7p0kJwhTQG
340 o9aJ9VMTWHJc/NCBx7eu451u6d46sBPCXS/OMUh2766fQmoRtO1OwCTxsRKG2kkjbMn54UdFULl
341 VfzvyghMNRKIezsEkmM8wueTqGUGZWa6CEZqZKwhe/PROxOPYzqtDH18XZknbU1n5lNb7vNfem9
342 2ai+3+JyFnW9UhfvpVF7gzAgdyCqNli4C6BIN43uwoS8HkykocZS/+Gv52aUQ/NZ8BKOHLw+7an
343 Q0o8W9ltSLZbEMxFIPSN0stiZlkXAp6DLyvh1Y4wXSynDjUondTpej2fSvSlCz/W5v5V7qA4nIc
344 vUvV7RjVzv17ut0AEQEAAQ==
345
346 AcLDXAQAAQoABgUCV83k9QAKCRDUpVvql9g3IBT8IACKZ7XpiBZ3W4lqbPssY6On81WmxQLtvsM
347 WTp6zZpl/wWOSt2vMNUk9pvcmrNq1jG9CuhDfWFLGXEjcrrmVkN3YuCOajMSPFCGrxsIBLSRt/b
348 nrKykdLAAzMfG8rP1d82bjFFiIieE+urQ0Kcv09Jtdvavq3JT1Tek5mFyyfhHNlQEKOzWqmRWiL
349 3c3VOZUs1ZD8TSlnuq/x+5T0X0YtOyGjSlVxk7UybbyMNd6MZfNaMpIG4x+mxD3KHFtBAC7O6kL
350 eX3i6j5nCY5UABfA3DZEAkWP4zlmdBEOvZ9t293NaDdOpzsUHRkoi0Zez/9BHQ/kwx/uNc2WqrY
351 inCmu16JGNeXqsyinnLl7Ghn2RwhvDMlLxF6RTx8xdx1yk6p3PBTwhZMUvuZGjUtN/AG8BmVJQ1
352 rsGSRkkSywvnhVJRB2sudnrMBmNS2goJbzSbmJnOlBrd2WsV0T9SgNMWZBiov3LvU4o2SmAb6b+
353 rYwh8H5QHcuuYJuxDjFhPswIp6Wes5T6hUicf3SWtObcDS4HSkVS4ImBjjX9YgCuFy7QdnooOWE
354 aPvkRw3XCVeYq0K6w9GRsk1YFErD4XmXXZjDYY650MX9v42Sz5MmphHV8jdIY5ssbadwFSe2rCQ
355 6UX08zy7RsIb19hTndE6ncvSNDChUR9eEnCm73eYaWTWTnq1cxdVP/s52r8uss++OYOkPWqh5nO
356 haRn7INjH/yZX4qXjNXlTjo0PnHH0q08vNKDwLhxS+D9du+70FeacXFyLIbcWllSbJ7DmbumGpF
357 yYbtj3FDDPzachFQdIG3lSt+cSUGeyfSs6wVtc3cIPka/2Urx7RprfmoWSI6+a5NcLdj0u2z8O9
358 HxeIgxDpg/3gT8ZIuFKePMcLDM19Fh/p0ysCsX+84B9chNWtsMSmIaE57V+959MVtsLu7SLb9gi
359 skrju0pQCwsu2wHMLTNd1f3PTHmrr49hxetTus07HSQUApMtAGKzQilF5zqFjbyaTd4xgQbd+PK
360 CjFyzQTDOcUhXpuUGt/IzlqiFfsCsmbj2K4KdSNYMlqIgZ3Azu8KvZLIhsyN7v5vNIZSPfEbjde
361 ClU9r0VRiJmtYBUjcSghD9LWn+yRLwOxhfQVjm0cBwIt5R/yPF/qC76yIVuWUtM5Y2/zJR1J8OF
362 qWchvlImHtvDzS9FQeLyzJAOjvZ2CnWp2gILgUz0WQdOk1Dq8ax7KS9BQ42zxw9EZAEPw3PEFqR
363 IQsRTONp+iVS8YxSmoYZjDlCgRMWUmawez/Fv5b9Fb/XkO5Eq4e+KfrpUujXItaipb+tV8h5v3t
364 oG3Ie3WOHrVjCLXIdYslpL1O4nadqR6Xv58pHj6k""")
365
366 test_assertions = [ACCOUNT_ASSERTION, SYSTEM_USER_ASSERTION]
367
368 def setUp(self):
369 super(TestSnapConfig, self).setUp()
370 self.subp = util.subp
371 self.new_root = tempfile.mkdtemp()
372 self.addCleanup(shutil.rmtree, self.new_root)
373
374 def _get_cloud(self, distro, metadata=None):
375 self.patchUtils(self.new_root)
376 paths = helpers.Paths({})
377 cls = distros.fetch(distro)
378 mydist = cls(distro, {}, paths)
379 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
380 if metadata:
381 myds.metadata.update(metadata)
382 return cloud.Cloud(myds, paths, {}, mydist, None)
383
384 @mock.patch('cloudinit.util.write_file')
385 @mock.patch('cloudinit.util.subp')
386 def test_snap_config_add_assertions(self, msubp, mwrite):
387 add_assertions(self.test_assertions)
388
389 combined = "\n".join(self.test_assertions)
390 mwrite.assert_any_call(ASSERTIONS_FILE, combined.encode('utf-8'))
391 msubp.assert_called_with(['snap', 'ack', ASSERTIONS_FILE],
392 capture=True)
393
394 def test_snap_config_add_assertions_empty(self):
395 self.assertRaises(ValueError, add_assertions, [])
396
397 def test_add_assertions_nonlist(self):
398 self.assertRaises(ValueError, add_assertions, {})
399
400 @mock.patch('cloudinit.util.write_file')
401 @mock.patch('cloudinit.util.subp')
402 def test_snap_config_add_assertions_ack_fails(self, msubp, mwrite):
403 msubp.side_effect = [util.ProcessExecutionError("Invalid assertion")]
404 self.assertRaises(util.ProcessExecutionError, add_assertions,
405 self.test_assertions)
406
407 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
408 @mock.patch('cloudinit.config.cc_snap_config.util')
409 def test_snap_config_handle_no_config(self, mock_util, mock_add):
410 cfg = {}
411 cc = self._get_cloud('ubuntu')
412 cc.distro = mock.MagicMock()
413 cc.distro.name = 'ubuntu'
414 mock_util.which.return_value = None
415 snap_handle('snap_config', cfg, cc, LOG, None)
416 mock_add.assert_not_called()
417
418 def test_snap_config_add_snap_user_no_config(self):
419 usercfg = add_snap_user(cfg=None)
420 self.assertEqual(usercfg, None)
421
422 def test_snap_config_add_snap_user_not_dict(self):
423 cfg = ['foobar']
424 self.assertRaises(ValueError, add_snap_user, cfg)
425
426 def test_snap_config_add_snap_user_no_email(self):
427 cfg = {'assertions': [], 'known': True}
428 usercfg = add_snap_user(cfg=cfg)
429 self.assertEqual(usercfg, None)
430
431 @mock.patch('cloudinit.config.cc_snap_config.util')
432 def test_snap_config_add_snap_user_email_only(self, mock_util):
433 email = 'janet@planetjanet.org'
434 cfg = {'email': email}
435 mock_util.which.return_value = None
436 mock_util.system_is_snappy.return_value = True
437 mock_util.subp.side_effect = [
438 ("false\n", ""), # snap managed
439 ]
440
441 usercfg = add_snap_user(cfg=cfg)
442
443 self.assertEqual(usercfg, {'snapuser': email, 'known': False})
444
445 @mock.patch('cloudinit.config.cc_snap_config.util')
446 def test_snap_config_add_snap_user_email_known(self, mock_util):
447 email = 'janet@planetjanet.org'
448 known = True
449 cfg = {'email': email, 'known': known}
450 mock_util.which.return_value = None
451 mock_util.system_is_snappy.return_value = True
452 mock_util.subp.side_effect = [
453 ("false\n", ""), # snap managed
454 (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
455 ]
456
457 usercfg = add_snap_user(cfg=cfg)
458
459 self.assertEqual(usercfg, {'snapuser': email, 'known': known})
460
461 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
462 @mock.patch('cloudinit.config.cc_snap_config.util')
463 def test_snap_config_handle_system_not_snappy(self, mock_util, mock_add):
464 cfg = {'snappy': {'assertions': self.test_assertions}}
465 cc = self._get_cloud('ubuntu')
466 cc.distro = mock.MagicMock()
467 cc.distro.name = 'ubuntu'
468 mock_util.which.return_value = None
469 mock_util.system_is_snappy.return_value = False
470
471 snap_handle('snap_config', cfg, cc, LOG, None)
472
473 mock_add.assert_not_called()
474
475 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
476 @mock.patch('cloudinit.config.cc_snap_config.util')
477 def test_snap_config_handle_snapuser(self, mock_util, mock_add):
478 email = 'janet@planetjanet.org'
479 cfg = {
480 'snappy': {
481 'assertions': self.test_assertions,
482 'email': email,
483 }
484 }
485 cc = self._get_cloud('ubuntu')
486 cc.distro = mock.MagicMock()
487 cc.distro.name = 'ubuntu'
488 mock_util.which.return_value = None
489 mock_util.system_is_snappy.return_value = True
490 mock_util.subp.side_effect = [
491 ("false\n", ""), # snap managed
492 ]
493
494 snap_handle('snap_config', cfg, cc, LOG, None)
495
496 mock_add.assert_called_with(self.test_assertions)
497 usercfg = {'snapuser': email, 'known': False}
498 cc.distro.create_user.assert_called_with(email, **usercfg)
499
500 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
501 @mock.patch('cloudinit.config.cc_snap_config.util')
502 def test_snap_config_handle_snapuser_known(self, mock_util, mock_add):
503 email = 'janet@planetjanet.org'
504 cfg = {
505 'snappy': {
506 'assertions': self.test_assertions,
507 'email': email,
508 'known': True,
509 }
510 }
511 cc = self._get_cloud('ubuntu')
512 cc.distro = mock.MagicMock()
513 cc.distro.name = 'ubuntu'
514 mock_util.which.return_value = None
515 mock_util.system_is_snappy.return_value = True
516 mock_util.subp.side_effect = [
517 ("false\n", ""), # snap managed
518 (self.SYSTEM_USER_ASSERTION, ""), # snap known system-user
519 ]
520
521 snap_handle('snap_config', cfg, cc, LOG, None)
522
523 mock_add.assert_called_with(self.test_assertions)
524 usercfg = {'snapuser': email, 'known': True}
525 cc.distro.create_user.assert_called_with(email, **usercfg)
526
527 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
528 @mock.patch('cloudinit.config.cc_snap_config.util')
529 def test_snap_config_handle_snapuser_known_managed(self, mock_util,
530 mock_add):
531 email = 'janet@planetjanet.org'
532 cfg = {
533 'snappy': {
534 'assertions': self.test_assertions,
535 'email': email,
536 'known': True,
537 }
538 }
539 cc = self._get_cloud('ubuntu')
540 cc.distro = mock.MagicMock()
541 cc.distro.name = 'ubuntu'
542 mock_util.which.return_value = None
543 mock_util.system_is_snappy.return_value = True
544 mock_util.subp.side_effect = [
545 ("true\n", ""), # snap managed
546 ]
547
548 snap_handle('snap_config', cfg, cc, LOG, None)
549
550 mock_add.assert_called_with(self.test_assertions)
551 cc.distro.create_user.assert_not_called()
552
553 @mock.patch('cloudinit.config.cc_snap_config.add_assertions')
554 @mock.patch('cloudinit.config.cc_snap_config.util')
555 def test_snap_config_handle_snapuser_known_no_assertion(self, mock_util,
556 mock_add):
557 email = 'janet@planetjanet.org'
558 cfg = {
559 'snappy': {
560 'assertions': [self.ACCOUNT_ASSERTION],
561 'email': email,
562 'known': True,
563 }
564 }
565 cc = self._get_cloud('ubuntu')
566 cc.distro = mock.MagicMock()
567 cc.distro.name = 'ubuntu'
568 mock_util.which.return_value = None
569 mock_util.system_is_snappy.return_value = True
570 mock_util.subp.side_effect = [
571 ("true\n", ""), # snap managed
572 ("", ""), # snap known system-user
573 ]
574
575 snap_handle('snap_config', cfg, cc, LOG, None)
576
577 mock_add.assert_called_with([self.ACCOUNT_ASSERTION])
578 cc.distro.create_user.assert_not_called()
579
580
290def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):581def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):
291 if cfgfile:582 if cfgfile:
292 cfgfile = os.path.sep.join([tmpd, cfgfile])583 cfgfile = os.path.sep.join([tmpd, cfgfile])

Subscribers

People subscribed via source and target branches