summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2015-03-26 20:39:25 -0400
committerScott Moser <smoser@ubuntu.com>2015-03-26 20:39:25 -0400
commita373e1097f6be460914e6cbbc897c6aa8e4aaefe (patch)
tree9fd00ec178b19349c7ba04d8e726b5d460208685
parent24fdc71db366dc4ea9a0d164bb6af7d38609478a (diff)
downloadvyos-cloud-init-a373e1097f6be460914e6cbbc897c6aa8e4aaefe.tar.gz
vyos-cloud-init-a373e1097f6be460914e6cbbc897c6aa8e4aaefe.zip
commit work in progress. tests pass.
-rw-r--r--cloudinit/config/cc_snappy.py159
-rw-r--r--tests/unittests/test_handler/test_handler_snappy.py163
2 files changed, 285 insertions, 37 deletions
diff --git a/cloudinit/config/cc_snappy.py b/cloudinit/config/cc_snappy.py
index 133336d4..bef8c170 100644
--- a/cloudinit/config/cc_snappy.py
+++ b/cloudinit/config/cc_snappy.py
@@ -7,18 +7,21 @@ from cloudinit import util
from cloudinit.settings import PER_INSTANCE
import glob
+import six
+import tempfile
import os
LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
-SNAPPY_ENV_PATH = "/writable/system-data/etc/snappy.env"
+SNAPPY_CMD = "snappy"
BUILTIN_CFG = {
'packages': [],
'packages_dir': '/writable/user-data/cloud-init/click_packages',
'ssh_enabled': False,
- 'system_snappy': "auto"
+ 'system_snappy': "auto",
+ 'configs': {},
}
"""
@@ -27,43 +30,111 @@ snappy:
ssh_enabled: True
packages:
- etcd
- - {'name': 'pkg1', 'config': "wark"}
+ - pkg2
+ configs:
+ pkgname: config-blob
+ pkgname2: config-blob
"""
-def install_package(pkg_name, config=None):
- cmd = ["snappy", "install"]
- if config:
- if os.path.isfile(config):
- cmd.append("--config-file=" + config)
+def get_fs_package_ops(fspath):
+ if not fspath:
+ return []
+ ops = []
+ for snapfile in glob.glob(os.path.sep.join([fspath, '*.snap'])):
+ cfg = snapfile.rpartition(".")[0] + ".config"
+ name = os.path.basename(snapfile).rpartition(".")[0]
+ if not os.path.isfile(cfg):
+ cfg = None
+ ops.append(makeop('install', name, config=None,
+ path=snapfile, cfgfile=cfg))
+ return ops
+
+
+def makeop(op, name, config=None, path=None, cfgfile=None):
+ return({'op': op, 'name': name, 'config': config, 'path': path,
+ 'cfgfile': cfgfile})
+
+
+def get_package_ops(packages, configs, installed=None, fspath=None):
+ # get the install an config operations that should be done
+ if installed is None:
+ installed = read_installed_packages()
+
+ if not packages:
+ packages = []
+ if not configs:
+ configs = {}
+
+ ops = []
+ ops += get_fs_package_ops(fspath)
+
+ for name in packages:
+ ops.append(makeop('install', name, configs.get('name')))
+
+ to_install = [f['name'] for f in ops]
+
+ for name in configs:
+ if name in installed and name not in to_install:
+ ops.append(makeop('config', name, config=configs[name]))
+
+ # prefer config entries to filepath entries
+ for op in ops:
+ name = op['name']
+ if name in configs and op['op'] == 'install' and 'cfgfile' in op:
+ LOG.debug("preferring configs[%s] over '%s'", name, op['cfgfile'])
+ op['cfgfile'] = None
+ op['config'] = configs[op['name']]
+
+ return ops
+
+
+def render_snap_op(op, name, path=None, cfgfile=None, config=None):
+ if op not in ('install', 'config'):
+ raise ValueError("cannot render op '%s'" % op)
+
+ try:
+ cfg_tmpf = None
+ if config is not None:
+ if isinstance(config, six.binary_type):
+ cfg_bytes = config
+ elif isinstance(config, six.text_type):
+ cfg_bytes = config_data.encode()
+ else:
+ cfg_bytes = yaml.safe_dump(config).encode()
+
+ (fd, cfg_tmpf) = tempfile.mkstemp()
+ os.write(fd, config_data)
+ os.close(fd)
+ cfgfile = cfg_tmpf
+
+ cmd = [SNAPPY_CMD, op]
+ if op == 'install' and cfgfile:
+ cmd.append('--config=' + cfgfile)
+ elif op == 'config':
+ cmd.append(cfgfile)
+
+ util.subp(cmd)
+
+ finally:
+ if tmpfile:
+ os.unlink(tmpfile)
+
+
+def read_installed_packages():
+ return [p[0] for p in read_pkg_data()]
+
+
+def read_pkg_data():
+ out, err = util.subp([SNAPPY_CMD, "list"])
+ for line in out.splitlines()[1:]:
+ toks = line.split(sep=None, maxsplit=3)
+ if len(toks) == 3:
+ (name, date, version) = toks
+ dev = None
else:
- cmd.append("--config=" + config)
- cmd.append(pkg_name)
- util.subp(cmd)
-
-
-def install_packages(package_dir, packages):
- local_pkgs = glob.glob(os.path.sep.join([package_dir, '*.click']))
- LOG.debug("installing local packages %s" % local_pkgs)
- if local_pkgs:
- for pkg in local_pkgs:
- cfg = pkg.replace(".click", ".config")
- if not os.path.isfile(cfg):
- cfg = None
- install_package(pkg, config=cfg)
-
- LOG.debug("installing click packages")
- if packages:
- for pkg in packages:
- if not pkg:
- continue
- if isinstance(pkg, str):
- name = pkg
- config = None
- elif pkg:
- name = pkg.get('name', pkg)
- config = pkg.get('config')
- install_package(pkg_name=name, config=config)
+ (name, date, version, dev) = toks
+ pkgs.append((name, date, version, dev,))
def disable_enable_ssh(enabled):
@@ -107,7 +178,21 @@ def handle(name, cfg, cloud, log, args):
LOG.debug("%s: 'auto' mode, and system not snappy", name)
return
- install_packages(mycfg['packages_dir'],
- mycfg['packages'])
+ pkg_ops = get_package_ops(packages=mycfg['packages'],
+ configs=mycfg['configs'],
+ fspath=mycfg['packages_dir'])
+
+ fails = []
+ for pkg_op in pkg_ops:
+ try:
+ render_snap_op(op=pkg_op['op'], name=pkg_op['name'],
+ cfgfile=pkg_op['cfgfile'], config=pkg_op['config'])
+ except Exception as e:
+ fails.append((pkg_op, e,))
+ LOG.warn("'%s' failed for '%s': %s",
+ pkg_op['op'], pkg_op['name'], e)
disable_enable_ssh(mycfg.get('ssh_enabled', False))
+
+ if fails:
+ raise Exception("failed to install/configure snaps")
diff --git a/tests/unittests/test_handler/test_handler_snappy.py b/tests/unittests/test_handler/test_handler_snappy.py
new file mode 100644
index 00000000..6b6d3584
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_snappy.py
@@ -0,0 +1,163 @@
+from cloudinit.config.cc_snappy import (makeop, get_package_ops)
+from cloudinit import util
+from .. import helpers as t_help
+
+import os
+import tempfile
+
+class TestInstallPackages(t_help.TestCase):
+ def setUp(self):
+ super(TestInstallPackages, self).setUp()
+ self.unapply = []
+
+ # by default 'which' has nothing in its path
+ self.apply_patches([(util, 'subp', self._subp)])
+ self.subp_called = []
+ self.snapcmds = []
+ self.tmp = tempfile.mkdtemp()
+
+ def tearDown(self):
+ apply_patches([i for i in reversed(self.unapply)])
+
+ def apply_patches(self, patches):
+ ret = apply_patches(patches)
+ self.unapply += ret
+
+ def _subp(self, *args, **kwargs):
+ # supports subp calling with cmd as args or kwargs
+ if 'args' not in kwargs:
+ kwargs['args'] = args[0]
+ self.subp_called.append(kwargs)
+ snap_cmds = []
+ args = kwargs['args']
+ if args[0:2] == ['snappy', 'config']:
+ if args[3] == "-":
+ config = kwargs.get('data', '')
+ else:
+ with open(args[3], "rb") as fp:
+ config = fp.read()
+ snap_cmds.append(('config', args[2], config,))
+ elif args[0:2] == ['snappy', 'install']:
+ # basically parse the snappy command and add
+ # to snap_installs a tuple (pkg, config)
+ config = None
+ pkg = None
+ for arg in args[2:]:
+ if arg.startswith("--config="):
+ cfgfile = arg.partition("=")[2]
+ if cfgfile == "-":
+ config = kwargs.get('data', '')
+ elif cfgfile:
+ with open(cfgfile, "rb") as fp:
+ config = fp.read()
+ elif not pkg and not arg.startswith("-"):
+ pkg = os.path.basename(arg)
+ self.snap_installs.append(('install', pkg, config,))
+
+ def test_package_ops_1(self):
+ ret = get_package_ops(
+ packages=['pkg1', 'pkg2', 'pkg3'],
+ configs={'pkg2': b'mycfg2'}, installed=[])
+ self.assertEqual(ret,
+ [makeop('install', 'pkg1', None, None),
+ makeop('install', 'pkg2', b'mycfg2', None),
+ makeop('install', 'pkg3', None, None)])
+
+ def test_package_ops_config_only(self):
+ ret = get_package_ops(
+ packages=None,
+ configs={'pkg2': b'mycfg2'}, installed=['pkg1', 'pkg2'])
+ self.assertEqual(ret,
+ [makeop('config', 'pkg2', b'mycfg2')])
+
+ def test_package_ops_install_and_config(self):
+ ret = get_package_ops(
+ packages=['pkg3', 'pkg2'],
+ configs={'pkg2': b'mycfg2', 'xinstalled': b'xcfg'},
+ installed=['xinstalled'])
+ self.assertEqual(ret,
+ [makeop('install', 'pkg3'),
+ makeop('install', 'pkg2', b'mycfg2'),
+ makeop('config', 'xinstalled', b'xcfg')])
+
+ def test_package_ops_with_file(self):
+ t_help.populate_dir(self.tmp,
+ {"snapf1.snap": b"foo1", "snapf1.config": b"snapf1cfg",
+ "snapf2.snap": b"foo2", "foo.bar": "ignored"})
+ ret = get_package_ops(
+ packages=['pkg1'], configs={}, installed=[], fspath=self.tmp)
+ self.assertEqual(ret,
+ [makeop_tmpd(self.tmp, 'install', 'snapf1', path="snapf1.snap",
+ cfgfile="snapf1.config"),
+ makeop_tmpd(self.tmp, 'install', 'snapf2', path="snapf2.snap"),
+ makeop('install', 'pkg1')])
+
+
+def makeop_tmpd(tmpd, op, name, config=None, path=None, cfgfile=None):
+ if cfgfile:
+ cfgfile = os.path.sep.join([tmpd, cfgfile])
+ if path:
+ path = os.path.sep.join([tmpd, path])
+ return(makeop(op=op, name=name, config=config, path=path, cfgfile=cfgfile))
+
+# def test_local_snaps_no_config(self):
+# t_help.populate_dir(self.tmp,
+# {"snap1.snap": b"foo", "snap2.snap": b"foo", "foosnap.txt": b"foo"})
+# cc_snappy.install_packages(self.tmp, None)
+# self.assertEqual(self.snap_installs,
+# [("snap1.snap", None), ("snap2.snap", None)])
+#
+# def test_local_snaps_mixed_config(self):
+# t_help.populate_dir(self.tmp,
+# {"snap1.snap": b"foo", "snap2.snap": b"snap2",
+# "snap1.config": b"snap1config"})
+# cc_snappy.install_packages(self.tmp, None)
+# self.assertEqual(self.snap_installs,
+# [("snap1.snap", b"snap1config"), ("snap2.snap", None)])
+#
+# def test_local_snaps_all_config(self):
+# t_help.populate_dir(self.tmp,
+# {"snap1.snap": "foo", "snap1.config": b"snap1config",
+# "snap2.snap": "snap2", "snap2.config": b"snap2config"})
+# cc_snappy.install_packages(self.tmp, None)
+# self.assertEqual(self.snap_installs,
+# [("snap1.snap", b"snap1config"), ("snap2.snap", b"snap2config")])
+#
+# def test_local_snaps_and_packages(self):
+# t_help.populate_dir(self.tmp,
+# {"snap1.snap": "foo", "snap1.config": b"snap1config"})
+# cc_snappy.install_packages(self.tmp, ["snap-in-store"])
+# self.assertEqual(self.snap_installs,
+# [("snap1.snap", b"snap1config"), ("snap-in-store", None)])
+#
+# def test_packages_no_config(self):
+# cc_snappy.install_packages(self.tmp, ["snap-in-store"])
+# self.assertEqual(self.snap_installs,
+# [("snap-in-store", None)])
+#
+# def test_packages_mixed_config(self):
+# cc_snappy.install_packages(self.tmp,
+# ["snap-in-store",
+# {'name': 'snap2-in-store', 'config': b"foo"}])
+# self.assertEqual(self.snap_installs,
+# [("snap-in-store", None), ("snap2-in-store", b"foo")])
+#
+# def test_packages_all_config(self):
+# cc_snappy.install_packages(self.tmp,
+# [{'name': 'snap1-in-store', 'config': b"boo"},
+# {'name': 'snap2-in-store', 'config': b"wark"}])
+# self.assertEqual(self.snap_installs,
+# [("snap1-in-store", b"boo"), ("snap2-in-store", b"wark")])
+#
+#
+
+def apply_patches(patches):
+ ret = []
+ for (ref, name, replace) in patches:
+ if replace is None:
+ continue
+ orig = getattr(ref, name)
+ setattr(ref, name, replace)
+ ret.append((ref, name, orig))
+ return ret
+