PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/cloudinit/config/cc_snappy.py

https://gitlab.com/bertjwregeer/cloud-init
Python | 321 lines | 263 code | 7 blank | 51 comment | 4 complexity | c532af1f4450edfc5842720570b4f282 MD5 | raw file
  1. # This file is part of cloud-init. See LICENSE file for license information.
  2. # RELEASE_BLOCKER: Remove this deprecated module in 18.3
  3. """
  4. Snappy
  5. ------
  6. **Summary:** snappy modules allows configuration of snappy.
  7. **Deprecated**: Use :ref:`snap` module instead. This module will not exist
  8. in cloud-init 18.3.
  9. The below example config config would install ``etcd``, and then install
  10. ``pkg2.smoser`` with a ``<config-file>`` argument where ``config-file`` has
  11. ``config-blob`` inside it. If ``pkgname`` is installed already, then
  12. ``snappy config pkgname <file>``
  13. will be called where ``file`` has ``pkgname-config-blob`` as its content.
  14. Entries in ``config`` can be namespaced or non-namespaced for a package.
  15. In either case, the config provided to snappy command is non-namespaced.
  16. The package name is provided as it appears.
  17. If ``packages_dir`` has files in it that end in ``.snap``, then they are
  18. installed. Given 3 files:
  19. - <packages_dir>/foo.snap
  20. - <packages_dir>/foo.config
  21. - <packages_dir>/bar.snap
  22. cloud-init will invoke:
  23. - snappy install <packages_dir>/foo.snap <packages_dir>/foo.config
  24. - snappy install <packages_dir>/bar.snap
  25. .. note::
  26. that if provided a ``config`` entry for ``ubuntu-core``, then
  27. cloud-init will invoke: snappy config ubuntu-core <config>
  28. Allowing you to configure ubuntu-core in this way.
  29. The ``ssh_enabled`` key controls the system's ssh service. The default value
  30. is ``auto``. Options are:
  31. - **True:** enable ssh service
  32. - **False:** disable ssh service
  33. - **auto:** enable ssh service if either ssh keys have been provided
  34. or user has requested password authentication (ssh_pwauth).
  35. **Internal name:** ``cc_snappy``
  36. **Module frequency:** per instance
  37. **Supported distros:** ubuntu
  38. **Config keys**::
  39. #cloud-config
  40. snappy:
  41. system_snappy: auto
  42. ssh_enabled: auto
  43. packages: [etcd, pkg2.smoser]
  44. config:
  45. pkgname:
  46. key2: value2
  47. pkg2:
  48. key1: value1
  49. packages_dir: '/writable/user-data/cloud-init/snaps'
  50. """
  51. from cloudinit import log as logging
  52. from cloudinit.settings import PER_INSTANCE
  53. from cloudinit import temp_utils
  54. from cloudinit import util
  55. import glob
  56. import os
  57. LOG = logging.getLogger(__name__)
  58. frequency = PER_INSTANCE
  59. SNAPPY_CMD = "snappy"
  60. NAMESPACE_DELIM = '.'
  61. BUILTIN_CFG = {
  62. 'packages': [],
  63. 'packages_dir': '/writable/user-data/cloud-init/snaps',
  64. 'ssh_enabled': "auto",
  65. 'system_snappy': "auto",
  66. 'config': {},
  67. }
  68. distros = ['ubuntu']
  69. def parse_filename(fname):
  70. fname = os.path.basename(fname)
  71. fname_noext = fname.rpartition(".")[0]
  72. name = fname_noext.partition("_")[0]
  73. shortname = name.partition(".")[0]
  74. return(name, shortname, fname_noext)
  75. def get_fs_package_ops(fspath):
  76. if not fspath:
  77. return []
  78. ops = []
  79. for snapfile in sorted(glob.glob(os.path.sep.join([fspath, '*.snap']))):
  80. (name, shortname, fname_noext) = parse_filename(snapfile)
  81. cfg = None
  82. for cand in (fname_noext, name, shortname):
  83. fpcand = os.path.sep.join([fspath, cand]) + ".config"
  84. if os.path.isfile(fpcand):
  85. cfg = fpcand
  86. break
  87. ops.append(makeop('install', name, config=None,
  88. path=snapfile, cfgfile=cfg))
  89. return ops
  90. def makeop(op, name, config=None, path=None, cfgfile=None):
  91. return({'op': op, 'name': name, 'config': config, 'path': path,
  92. 'cfgfile': cfgfile})
  93. def get_package_config(configs, name):
  94. # load the package's config from the configs dict.
  95. # prefer full-name entry (config-example.canonical)
  96. # over short name entry (config-example)
  97. if name in configs:
  98. return configs[name]
  99. return configs.get(name.partition(NAMESPACE_DELIM)[0])
  100. def get_package_ops(packages, configs, installed=None, fspath=None):
  101. # get the install an config operations that should be done
  102. if installed is None:
  103. installed = read_installed_packages()
  104. short_installed = [p.partition(NAMESPACE_DELIM)[0] for p in installed]
  105. if not packages:
  106. packages = []
  107. if not configs:
  108. configs = {}
  109. ops = []
  110. ops += get_fs_package_ops(fspath)
  111. for name in packages:
  112. ops.append(makeop('install', name, get_package_config(configs, name)))
  113. to_install = [f['name'] for f in ops]
  114. short_to_install = [f['name'].partition(NAMESPACE_DELIM)[0] for f in ops]
  115. for name in configs:
  116. if name in to_install:
  117. continue
  118. shortname = name.partition(NAMESPACE_DELIM)[0]
  119. if shortname in short_to_install:
  120. continue
  121. if name in installed or shortname in short_installed:
  122. ops.append(makeop('config', name,
  123. config=get_package_config(configs, name)))
  124. # prefer config entries to filepath entries
  125. for op in ops:
  126. if op['op'] != 'install' or not op['cfgfile']:
  127. continue
  128. name = op['name']
  129. fromcfg = get_package_config(configs, op['name'])
  130. if fromcfg:
  131. LOG.debug("preferring configs[%(name)s] over '%(cfgfile)s'", op)
  132. op['cfgfile'] = None
  133. op['config'] = fromcfg
  134. return ops
  135. def render_snap_op(op, name, path=None, cfgfile=None, config=None):
  136. if op not in ('install', 'config'):
  137. raise ValueError("cannot render op '%s'" % op)
  138. shortname = name.partition(NAMESPACE_DELIM)[0]
  139. try:
  140. cfg_tmpf = None
  141. if config is not None:
  142. # input to 'snappy config packagename' must have nested data. odd.
  143. # config:
  144. # packagename:
  145. # config
  146. # Note, however, we do not touch config files on disk.
  147. nested_cfg = {'config': {shortname: config}}
  148. (fd, cfg_tmpf) = temp_utils.mkstemp()
  149. os.write(fd, util.yaml_dumps(nested_cfg).encode())
  150. os.close(fd)
  151. cfgfile = cfg_tmpf
  152. cmd = [SNAPPY_CMD, op]
  153. if op == 'install':
  154. if path:
  155. cmd.append("--allow-unauthenticated")
  156. cmd.append(path)
  157. else:
  158. cmd.append(name)
  159. if cfgfile:
  160. cmd.append(cfgfile)
  161. elif op == 'config':
  162. cmd += [name, cfgfile]
  163. util.subp(cmd)
  164. finally:
  165. if cfg_tmpf:
  166. os.unlink(cfg_tmpf)
  167. def read_installed_packages():
  168. ret = []
  169. for (name, _date, _version, dev) in read_pkg_data():
  170. if dev:
  171. ret.append(NAMESPACE_DELIM.join([name, dev]))
  172. else:
  173. ret.append(name)
  174. return ret
  175. def read_pkg_data():
  176. out, _err = util.subp([SNAPPY_CMD, "list"])
  177. pkg_data = []
  178. for line in out.splitlines()[1:]:
  179. toks = line.split(sep=None, maxsplit=3)
  180. if len(toks) == 3:
  181. (name, date, version) = toks
  182. dev = None
  183. else:
  184. (name, date, version, dev) = toks
  185. pkg_data.append((name, date, version, dev,))
  186. return pkg_data
  187. def disable_enable_ssh(enabled):
  188. LOG.debug("setting enablement of ssh to: %s", enabled)
  189. # do something here that would enable or disable
  190. not_to_be_run = "/etc/ssh/sshd_not_to_be_run"
  191. if enabled:
  192. util.del_file(not_to_be_run)
  193. # this is an indempotent operation
  194. util.subp(["systemctl", "start", "ssh"])
  195. else:
  196. # this is an indempotent operation
  197. util.subp(["systemctl", "stop", "ssh"])
  198. util.write_file(not_to_be_run, "cloud-init\n")
  199. def set_snappy_command():
  200. global SNAPPY_CMD
  201. if util.which("snappy-go"):
  202. SNAPPY_CMD = "snappy-go"
  203. elif util.which("snappy"):
  204. SNAPPY_CMD = "snappy"
  205. else:
  206. SNAPPY_CMD = "snap"
  207. LOG.debug("snappy command is '%s'", SNAPPY_CMD)
  208. def handle(name, cfg, cloud, log, args):
  209. cfgin = cfg.get('snappy')
  210. if not cfgin:
  211. cfgin = {}
  212. mycfg = util.mergemanydict([cfgin, BUILTIN_CFG])
  213. sys_snappy = str(mycfg.get("system_snappy", "auto"))
  214. if util.is_false(sys_snappy):
  215. LOG.debug("%s: System is not snappy. disabling", name)
  216. return
  217. if sys_snappy.lower() == "auto" and not(util.system_is_snappy()):
  218. LOG.debug("%s: 'auto' mode, and system not snappy", name)
  219. return
  220. log.warning(
  221. 'DEPRECATION: snappy module will be dropped in 18.3 release.'
  222. ' Use snap module instead')
  223. set_snappy_command()
  224. pkg_ops = get_package_ops(packages=mycfg['packages'],
  225. configs=mycfg['config'],
  226. fspath=mycfg['packages_dir'])
  227. fails = []
  228. for pkg_op in pkg_ops:
  229. try:
  230. render_snap_op(**pkg_op)
  231. except Exception as e:
  232. fails.append((pkg_op, e,))
  233. LOG.warning("'%s' failed for '%s': %s",
  234. pkg_op['op'], pkg_op['name'], e)
  235. # Default to disabling SSH
  236. ssh_enabled = mycfg.get('ssh_enabled', "auto")
  237. # If the user has not explicitly enabled or disabled SSH, then enable it
  238. # when password SSH authentication is requested or there are SSH keys
  239. if ssh_enabled == "auto":
  240. user_ssh_keys = cloud.get_public_ssh_keys() or None
  241. password_auth_enabled = cfg.get('ssh_pwauth', False)
  242. if user_ssh_keys:
  243. LOG.debug("Enabling SSH, ssh keys found in datasource")
  244. ssh_enabled = True
  245. elif cfg.get('ssh_authorized_keys'):
  246. LOG.debug("Enabling SSH, ssh keys found in config")
  247. elif password_auth_enabled:
  248. LOG.debug("Enabling SSH, password authentication requested")
  249. ssh_enabled = True
  250. elif ssh_enabled not in (True, False):
  251. LOG.warning("Unknown value '%s' in ssh_enabled", ssh_enabled)
  252. disable_enable_ssh(ssh_enabled)
  253. if fails:
  254. raise Exception("failed to install/configure snaps")
  255. # vi: ts=4 expandtab