diff -Nru ols-vms-1.1.7/debian/bzr-builder.manifest ols-vms-1.1.8/debian/bzr-builder.manifest --- ols-vms-1.1.7/debian/bzr-builder.manifest 2017-01-15 13:46:45.000000000 +0000 +++ ols-vms-1.1.8/debian/bzr-builder.manifest 2017-01-19 17:07:10.000000000 +0000 @@ -1,2 +1,2 @@ -# bzr-builder format 0.3 deb-version {debupstream}-0~273 -lp:ols-vms revid:vila+ols@canonical.com-20170114135700-id8iuqikkmubg10p +# bzr-builder format 0.3 deb-version {debupstream}-0~276 +lp:ols-vms revid:vila+ols@canonical.com-20170119170258-ivl3cqm2upwgttnw diff -Nru ols-vms-1.1.7/debian/changelog ols-vms-1.1.8/debian/changelog --- ols-vms-1.1.7/debian/changelog 2017-01-15 13:46:45.000000000 +0000 +++ ols-vms-1.1.8/debian/changelog 2017-01-19 17:07:10.000000000 +0000 @@ -1,8 +1,29 @@ -ols-vms (1.1.7-0~273~ubuntu17.04.1) zesty; urgency=low +ols-vms (1.1.8-0~276~ubuntu17.04.1) zesty; urgency=low * Auto build. - -- Vincent Ladeuil Sun, 15 Jan 2017 13:46:45 +0000 + -- Vincent Ladeuil Thu, 19 Jan 2017 17:07:10 +0000 + +ols-vms (1.1.8) unstable; urgency=medium + + * ubuntu wily has reached EOL. + + * Add 'lxd.user_mounts' to mount host paths inside lxd containers. This + is a first release of the feature (i.e. experimental but tested in + nested unprivileged containers), rough edges expected. Since this + requires the user to configure /etc/subuid and /etc/subgid + appropriately with 'root::1' lines, this is checked before + configuring the mounts. + + * Add 'lxc.bind_home' (formerly 'vm.bind_home) and 'lxc.user' to + separate the lxc specific feature from 'vm.user'. + + * 'lxd.nesting' is now an integer option specifying the number of + testing the vm is expected to be configured with. Since this requires + the user to configure /etc/subuid and /etc/subgid appropriately for + 'root' and 'lxd' this is checked before creating the vm. + + -- Vincent Ladeuil Thu, 19 Jan 2017 16:36:41 +0100 ols-vms (1.1.7) unstable; urgency=medium diff -Nru ols-vms-1.1.7/NEWS.rst ols-vms-1.1.8/NEWS.rst --- ols-vms-1.1.7/NEWS.rst 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/NEWS.rst 2017-01-19 17:07:09.000000000 +0000 @@ -4,18 +4,25 @@ Overview of changes to ols-vms in reverse chronological order. -dev -=== +1.1.8 +===== * ubuntu wily has reached EOL. * Add 'lxd.user_mounts' to mount host paths inside lxd containers. This is a first release of the feature (i.e. experimental but tested in nested - unprivileged containers), rough edges expected. + unprivileged containers), rough edges expected. Since this requires the + user to configure /etc/subuid and /etc/subgid appropriately with + 'root::1' lines, this is checked before configuring the mounts. * Add 'lxc.bind_home' (formerly 'vm.bind_home) and 'lxc.user' to separate the lxc specific feature from 'vm.user'. +* 'lxd.nesting' is now an integer option specifying the number of testing + the vm is expected to be configured with. Since this requires the user to + configure /etc/subuid and /etc/subgid appropriately for 'root' and 'lxd' + this is checked before creating the vm. + 1.1.7 ===== diff -Nru ols-vms-1.1.7/olsvms/config.py ols-vms-1.1.8/olsvms/config.py --- ols-vms-1.1.7/olsvms/config.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/config.py 2017-01-19 17:07:09.000000000 +0000 @@ -723,13 +723,19 @@ with id being the correct user (and group) id (i.e. the user running the 'setup' command). ''')) -register(options.Option('lxd.nesting', default=False, - from_unicode=options.bool_from_store, +register(options.Option('lxd.nesting', default=0, + from_unicode=options.int_from_store, help_string='''\ -Whether or not nested containers can be created. +How many level of nesting containers should be allowed for the vm. -An unprivileged container can create nested privileged containers which cannot -compromise the host. +This is used to configure the container for allowing nesting containers and +check that the /etc/subuid and /etc/subgid files provision enough ids for the +nested containers. + +'0' means no nested containers. + +Note that an unprivileged container can create nested privileged containers +which cannot compromise the host. ''')) register(options.ListOption('lxd.setup.digest.options', diff -Nru ols-vms-1.1.7/olsvms/errors.py ols-vms-1.1.8/olsvms/errors.py --- ols-vms-1.1.7/olsvms/errors.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/errors.py 2017-01-19 17:07:09.000000000 +0000 @@ -1,6 +1,6 @@ # This file is part of Online Services virtual machine tools. # -# Copyright 2014, 2015, 2016 Canonical Ltd. +# Copyright 2014-2017 Canonical Ltd. # # 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 @@ -13,6 +13,7 @@ # # You should have received a copy of the GNU General Public License along with # this program. If not, see . +from __future__ import unicode_literals class BaseError(Exception): @@ -121,3 +122,11 @@ def __init__(self, line): super(CloudInitError, self).__init__(line=line) + + +class SubIdError(BaseError): + + fmt = 'The /etc/sub{typ} file has no "root:{subid}:1" line' + + def __init__(self, typ, subid): + super(SubIdError, self).__init__(typ=typ, subid=subid) diff -Nru ols-vms-1.1.7/olsvms/__init__.py ols-vms-1.1.8/olsvms/__init__.py --- ols-vms-1.1.7/olsvms/__init__.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/__init__.py 2017-01-19 17:07:09.000000000 +0000 @@ -19,4 +19,4 @@ # the release level is 'dev' or 'final'. The # version_info value corresponding to the olsvms version 2.0 is (2, 0, 0, # 'final', 0). -__version__ = (1, 1, 8, 'dev', 0) +__version__ = (1, 1, 8, 'final', 0) diff -Nru ols-vms-1.1.7/olsvms/tests/features.py ols-vms-1.1.8/olsvms/tests/features.py --- ols-vms-1.1.7/olsvms/tests/features.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/tests/features.py 2017-01-19 17:07:09.000000000 +0000 @@ -188,6 +188,21 @@ return 'User-provided configuration to setup test vms' +class LxdNesting(features.Feature): + + def __init__(self, level): + super(LxdNesting, self).__init__() + self.level = level + + def _probe(self): + from olsvms.vms import lxd + return lxd.check_nesting(self.level) + + def feature_name(self): + return ('lxd configured to nest containers' + ' up to {} levels'.format(self.level)) + + # There is always a single instance shared by all tests, they are all declared # below to be easier to find. qemu_img_feature = features.ExecutableFeature('qemu-img') @@ -198,3 +213,4 @@ nova_compute = NovaCompute() nova_creds = NovaCredentials() tests_config = TestsConfig() +lxd_nesting_1 = LxdNesting(1) diff -Nru ols-vms-1.1.7/olsvms/tests/test_lxd.py ols-vms-1.1.8/olsvms/tests/test_lxd.py --- ols-vms-1.1.7/olsvms/tests/test_lxd.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/tests/test_lxd.py 2017-01-19 17:07:09.000000000 +0000 @@ -151,6 +151,18 @@ vm.conf.get('vm.name'))))) self.assertEqual('', err) + def test_bad_nesting_config(self): + vm = lxd.Lxd(config.VmStack(self.vm_name)) + # Short of installing testing /etc/sub[ug]id files, using an insane + # level of nesting. + vm.conf.set('lxd.nesting', '1024') + vm.conf.set('vm.release', 'xenial') + vm.conf.set('vm.architecture', 'amd64') + with self.assertRaises(errors.OlsVmsError) as cm: + vm.init() + self.assertEqual('Lxd needs more ids for 1024 levels of nesting', + '{}'.format(cm.exception)) + @features.requires(vms_features.tests_config) @features.requires(vms_features.lxd_client_feature) @@ -189,12 +201,13 @@ # By default, nesting is not set ret, out, err = lxd_config_get(self.vm_name, 'security.nesting') self.assertEqual(0, ret) - self.assertEqual('False\n', out) + self.assertEqual('\n', out) self.assertEqual('', err) + @features.requires(vms_features.lxd_nesting_1) def test_nesting(self): vm = lxd.Lxd(config.VmStack(self.vm_name)) - vm.conf.set('lxd.nesting', 'True') + vm.conf.set('lxd.nesting', '1') self.addCleanup(vm.teardown) self.addCleanup(vm.stop) vm.setup() @@ -249,6 +262,61 @@ @features.requires(vms_features.tests_config) @features.requires(vms_features.lxd_client_feature) +class TestCheckSubidsForMountsErrors(unittest.TestCase): + """Test errors related to sub ids. + + Strictly speaking this should happen in a nested vm so we can change + /etc/subuid and /ets/subgid. Doing it in the temp directory is easier. + """ + def setUp(self): + super(TestCheckSubidsForMountsErrors, self).setUp() + vms_fixtures.setup_tests_config(self) + vms_fixtures.set_uniq_vm_name(self) + config_dir = os.path.join(self.uniq_dir, 'config') + # Create a shared config + conf = config.VmStack(None) + conf.store._load_from_string(''' +vm.config_dir={config_dir} +[{vm_name}] +vm.name = {vm_name} +vm.class = lxd +vm.release = xenial +vm.architecture = amd64 +lxd.user_mounts = .,/work +'''.format(config_dir=config_dir, vm_name=self.vm_name)) + conf.store.save() + conf.store.unload() + + def create_ids(self, uids, gids): + with open(os.path.join(self.etc_dir, 'subuid'), 'w') as f: + f.write(uids) + with open(os.path.join(self.etc_dir, 'subgid'), 'w') as f: + f.write(gids) + + def test_root_cant_map_uid(self): + vm = lxd.Lxd(config.VmStack(self.vm_name)) + self.create_ids('', 'root:42:1') + with self.assertRaises(errors.SubIdError) as cm: + vm.check_subids_for_mounts(888, 0, self.etc_dir) + self.assertEqual('uid', cm.exception.typ) + self.assertEqual(888, cm.exception.subid) + + def test_root_cant_map_gid(self): + vm = lxd.Lxd(config.VmStack(self.vm_name)) + self.create_ids('root:888:1', 'root:42:1') + with self.assertRaises(errors.SubIdError) as cm: + vm.check_subids_for_mounts(888, 888, self.etc_dir) + self.assertEqual('gid', cm.exception.typ) + self.assertEqual(888, cm.exception.subid) + + def test_root_can_map(self): + vm = lxd.Lxd(config.VmStack(self.vm_name)) + self.create_ids('root:888:1', 'root:999:1') + vm.check_subids_for_mounts(888, 999, self.etc_dir) + + +@features.requires(vms_features.tests_config) +@features.requires(vms_features.lxd_client_feature) class TestPublish(unittest.TestCase): def setUp(self): @@ -324,3 +392,92 @@ # There is a tiny window during which the container can be seen STOPPED # before being deleted self.assertIn(vm.state(), ('UNKNOWN', 'STOPPED')) + + +class TestNextSubuids(unittest.TestCase): + + def setUp(self): + super(TestNextSubuids, self).setUp() + vms_fixtures.isolate_from_disk(self) + + def assertIdRanges(self, expected, content): + with open(os.path.join(self.etc_dir, 'subuid'), 'w') as f: + f.write(content) + self.assertEqual(expected, list(lxd.next_subids('uid', self.etc_dir))) + + def test_unknown(self): + with self.assertRaises(IOError) as cm: + list(lxd.next_subids('uid', self.etc_dir)) + # aka FileNotFound + self.assertEqual(2, cm.exception.args[0]) + + def test_empty(self): + self.assertIdRanges([], '') + self.assertIdRanges([], '\n\n') + + def test_garbage(self): + self.assertIdRanges([], 'root:foo:2') + self.assertIdRanges([], 'root:2:foo') + + def test_no_newline(self): + self.assertIdRanges([('root', 1000, 1)], 'root:1000:1') + + def test_ignore_comments(self): + self.assertIdRanges([('root', 1000, 1), ('foo', 12, 42)], + 'root:1000:1\n# foo\nfoo:12:42') + + +class TestCheckIdFor(unittest.TestCase): + + def setUp(self): + super(TestCheckIdFor, self).setUp() + vms_fixtures.isolate_from_disk(self) + + def create_ids(self, typ, ids): + with open(os.path.join(self.etc_dir, 'sub{}'.format(typ)), 'w') as f: + f.write(ids) + + def test_present(self): + self.create_ids('uid', 'root:1000:1') + self.assertTrue(lxd.check_id_for('root', 1000, 'uid', self.etc_dir)) + + def test_absent_from_subgid(self): + self.create_ids('gid', '') + self.assertFalse(lxd.check_id_for('root', 1000, 'gid', self.etc_dir)) + + def test_absent_from_subuid(self): + self.create_ids('uid', '') + self.assertFalse(lxd.check_id_for('root', 1000, 'uid', self.etc_dir)) + + +class TestCheckNesting(unittest.TestCase): + + def setUp(self): + super(TestCheckNesting, self).setUp() + vms_fixtures.isolate_from_disk(self) + + def create_ids(self, uids, gids): + with open(os.path.join(self.etc_dir, 'subuid'), 'w') as f: + f.write(uids) + with open(os.path.join(self.etc_dir, 'subgid'), 'w') as f: + f.write(gids) + + def test_no_nesting(self): + self.create_ids('root:100000:65536\nlxd:100000:65536\n', + 'root:100000:65536\nlxd:100000:65536\n') + self.assertTrue(lxd.check_nesting(0, self.etc_dir)) + + def test_nesting_one(self): + self.create_ids('root:100000:131072\nlxd:100000:131072\n', + 'root:100000:131072\nlxd:100000:131072\n') + self.assertTrue(lxd.check_nesting(1, self.etc_dir)) + + def test_nesting_two(self): + self.create_ids('root:100000:196608\nlxd:100000:196608\n', + 'root:100000:196608\nlxd:100000:196608\n') + self.assertTrue(lxd.check_nesting(2, self.etc_dir)) + + def test_not_enough(self): + self.create_ids('root:100000:131072\nlxd:100000:131072\n', + 'root:100000:131072\nlxd:100000:131072\n') + self.assertFalse(lxd.check_nesting(2, self.etc_dir)) diff -Nru ols-vms-1.1.7/olsvms/vms/lxd.py ols-vms-1.1.8/olsvms/vms/lxd.py --- ols-vms-1.1.7/olsvms/vms/lxd.py 2017-01-15 13:46:44.000000000 +0000 +++ ols-vms-1.1.8/olsvms/vms/lxd.py 2017-01-19 17:07:09.000000000 +0000 @@ -31,6 +31,54 @@ logger = logging.getLogger(__name__) +def next_subids(typ, etc_dir=None): + if etc_dir is None: + etc_dir = '/etc' + fname = 'sub{}'.format(typ) + with open(os.path.join(etc_dir, fname)) as f: + for line in f.readlines(): + if line.startswith('#'): + continue + try: + user, first, nb = line.strip().split(':') + first = int(first) + nb = int(nb) + except ValueError: + continue + yield user, first, nb + + +def check_id_for(name, the_id, typ, etc_dir=None): + for user, first, nb in next_subids(typ, etc_dir): + if user == name and first <= the_id < (first + nb): + return True + return False + + +def available_ids(name, typ, etc_dir=None): + total = 0 + for user, _, nb in next_subids(typ, etc_dir): + if user == name: + # FIXME: Recovering range definitions ? -- vila 2017-01-19 + total += nb + return total + + +def user_can_nest(user, level, etc_dir=None): + # For each level of nesting, a full additional range is needed + # No nesting still requires a full range + needed_ids = (level + 1) * 65536 + uids = available_ids(user, 'uid', etc_dir) + gids = available_ids(user, 'gid', etc_dir) + return (uids >= needed_ids and gids >= needed_ids) + + +def check_nesting(level, etc_dir=None): + root = user_can_nest('root', level, etc_dir) + lxd = user_can_nest('lxd', level, etc_dir) + return root and lxd + + def lxd_info(vm_name, source=None): info = dict(state='UNKNOWN') if source is None: @@ -92,9 +140,14 @@ for net in network: init_command.extend(['-p', net]) nesting = self.conf.get('lxd.nesting') - if nesting is not None: + if not check_nesting(nesting): + raise errors.OlsVmsError( + 'Lxd needs more ids for {} levels of nesting'.format(nesting)) + if self.conf.get('lxd.user_mounts'): + self.check_subids_for_mounts(os.getuid(), os.getgid()) + if nesting: init_command.extend(['--config', - 'security.nesting={}'.format(nesting)]) + 'security.nesting=True']) # FIXME: Log out & err ? -- vila 2016-01-05 # FIXME: This can hang IRL (apparently when a new image needs to be # downloaded requiring an lxd restart, but this cannot be reliably @@ -110,18 +163,25 @@ ' '.join(config_command))) subprocesses.run(config_command, cmd_input=self.ci_user_data.dump()) + def check_subids_for_mounts(self, uid, gid, etc_dir=None): + if not check_id_for('root', uid, 'uid', etc_dir): + raise errors.SubIdError('uid', uid) + if not check_id_for('root', gid, 'gid', etc_dir): + raise errors.SubIdError('gid', gid) + def set_user_mounts(self): mounts = self.conf.get('lxd.user_mounts') if not mounts: return - + uid = os.getuid() + gid = os.getgid() # Declare the needed id mapping. # Assuming the ubuntu uid/gid is 1000/1000 inside the container. - # We can't use 'both {host uid/gid} {container uid}' as uid and gid may - # differ. + # We can't use 'both {host uid/gid} {container uid/gid}' as uid and gid + # may differ. mapping_template = '{map} {host_id}-{host_id} 1000-1000\n' - uid_map = mapping_template.format(map='uid', host_id=os.getuid()) - gid_map = mapping_template.format(map='gid', host_id=os.getgid()) + uid_map = mapping_template.format(map='uid', host_id=uid) + gid_map = mapping_template.format(map='gid', host_id=gid) config_command = ['lxc', 'config', 'set', self.conf.get('vm.name'), 'raw.idmap', '-'] mapping = uid_map + gid_map diff -Nru ols-vms-1.1.7/scripts/first-use/lxd ols-vms-1.1.8/scripts/first-use/lxd --- ols-vms-1.1.7/scripts/first-use/lxd 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.1.8/scripts/first-use/lxd 2017-01-19 17:07:09.000000000 +0000 @@ -0,0 +1,15 @@ +#!/bin/sh -e +# This script configure lxd on a host after its first install with the most +# common defaults. + +# Initial lxd setup +sudo lxd init --auto +# Make all containers use the lxd bridge which give them NAT'ed (ip v4 and v6) +# internet access via the host +lxc network create lxdbr0 +lxc network attach-profile lxdbr0 default eth0 +# For user mounts in lxd containers, 'root' needs to be able to handle uid +# 1000 (first created "normal" user on ubuntu (and probably elsewhere) in +# the id mapping maps. +echo 'root:$(id -u):1' | sudo tee -a /etc/subuid +echo 'root:$(id -g):1' | sudo tee -a /etc/subgid diff -Nru ols-vms-1.1.7/scripts/first-use/ols-vms ols-vms-1.1.8/scripts/first-use/ols-vms --- ols-vms-1.1.7/scripts/first-use/ols-vms 1970-01-01 00:00:00.000000000 +0000 +++ ols-vms-1.1.8/scripts/first-use/ols-vms 2017-01-19 17:07:09.000000000 +0000 @@ -0,0 +1,8 @@ +#!/bin/sh -e + +# This script configure ols-vms on a host for a given user after its first +# install with the most common defaults. + +# FIXME: Only if the key doesn't exist ? Or only if ols-vms config ssh.key +# is not set ? -- vila 2017-01-14 +(cd ~/.ssh && ssh-keygen -f id_rsa -N '' -t rsa)