Merge ~ubuntu-server/cloud-init:derived-repo-v3-new-apt-spec into cloud-init:master

Proposed by Scott Moser
Status: Merged
Merged at revision: d861415ff9ab816b1183b8c58ec35348be4fd458
Proposed branch: ~ubuntu-server/cloud-init:derived-repo-v3-new-apt-spec
Merge into: cloud-init:master
Diff against target: 4996 lines (+3325/-1029)
17 files modified
cloudinit/config/cc_apt_configure.py (+547/-156)
cloudinit/gpg.py (+4/-4)
cloudinit/util.py (+130/-17)
dev/null (+0/-516)
doc/examples/cloud-config-add-apt-repos.txt (+28/-14)
doc/examples/cloud-config-apt.txt (+328/-0)
doc/examples/cloud-config-chef-oneiric.txt (+34/-33)
doc/examples/cloud-config-chef.txt (+34/-33)
doc/examples/cloud-config.txt (+1/-250)
tests/configs/sample1.yaml (+0/-3)
tests/unittests/test_distros/test_generic.py (+0/-3)
tests/unittests/test_handler/test_handler_apt_conf_v1.py (+109/-0)
tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py (+200/-0)
tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py (+187/-0)
tests/unittests/test_handler/test_handler_apt_source_v1.py (+551/-0)
tests/unittests/test_handler/test_handler_apt_source_v3.py (+1103/-0)
tests/unittests/test_util.py (+69/-0)
Reviewer Review Type Date Requested Status
cloud-init Commiters Pending
Review via email: mp+303190@code.launchpad.net

Commit message

Apt: add new apt configuration format

This adds an improved apt configuration format that is fully backwards
compatible with previous behavior. This is mostly copied from curtin's
implementation.

It does:
 * clean up and centralizes many of the top level 'apt_*' values that
   previously existed into a single top level 'apt'key.
 * support a 'source' in apt/sources/entry that has only a key
 * documents new features and adds tests.

See the added doc/examples/cloud-config-apt.txt for more information.

To post a comment you must log in.
b7d9a53... by Scott Moser

fix error in disable_suites if empty lines in sources.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
2index 05ad4b0..609dbb5 100644
3--- a/cloudinit/config/cc_apt_configure.py
4+++ b/cloudinit/config/cc_apt_configure.py
5@@ -23,80 +23,182 @@ import os
6 import re
7
8 from cloudinit import gpg
9+from cloudinit import log as logging
10 from cloudinit import templater
11 from cloudinit import util
12
13-distros = ['ubuntu', 'debian']
14-
15-PROXY_TPL = "Acquire::HTTP::Proxy \"%s\";\n"
16-APT_CONFIG_FN = "/etc/apt/apt.conf.d/94cloud-init-config"
17-APT_PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy"
18+LOG = logging.getLogger(__name__)
19
20 # this will match 'XXX:YYY' (ie, 'cloud-archive:foo' or 'ppa:bar')
21 ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
22
23+# place where apt stores cached repository data
24+APT_LISTS = "/var/lib/apt/lists"
25
26-def handle(name, cfg, cloud, log, _args):
27- if util.is_false(cfg.get('apt_configure_enabled', True)):
28- log.debug("Skipping module named %s, disabled by config.", name)
29- return
30-
31- release = get_release()
32- mirrors = find_apt_mirror_info(cloud, cfg)
33- if not mirrors or "primary" not in mirrors:
34- log.debug(("Skipping module named %s,"
35- " no package 'mirror' located"), name)
36- return
37-
38- # backwards compatibility
39- mirror = mirrors["primary"]
40- mirrors["mirror"] = mirror
41-
42- log.debug("Mirror info: %s" % mirrors)
43-
44- if not util.get_cfg_option_bool(cfg,
45- 'apt_preserve_sources_list', False):
46- generate_sources_list(cfg, release, mirrors, cloud, log)
47- old_mirrors = cfg.get('apt_old_mirrors',
48- {"primary": "archive.ubuntu.com/ubuntu",
49- "security": "security.ubuntu.com/ubuntu"})
50- rename_apt_lists(old_mirrors, mirrors)
51+# Files to store proxy information
52+APT_CONFIG_FN = "/etc/apt/apt.conf.d/94cloud-init-config"
53+APT_PROXY_FN = "/etc/apt/apt.conf.d/90cloud-init-aptproxy"
54+
55+# Default keyserver to use
56+DEFAULT_KEYSERVER = "keyserver.ubuntu.com"
57+
58+# Default archive mirrors
59+PRIMARY_ARCH_MIRRORS = {"PRIMARY": "http://archive.ubuntu.com/ubuntu/",
60+ "SECURITY": "http://security.ubuntu.com/ubuntu/"}
61+PORTS_MIRRORS = {"PRIMARY": "http://ports.ubuntu.com/ubuntu-ports",
62+ "SECURITY": "http://ports.ubuntu.com/ubuntu-ports"}
63+PRIMARY_ARCHES = ['amd64', 'i386']
64+PORTS_ARCHES = ['s390x', 'arm64', 'armhf', 'powerpc', 'ppc64el']
65+
66+
67+def get_default_mirrors(arch=None, target=None):
68+ """returns the default mirrors for the target. These depend on the
69+ architecture, for more see:
70+ https://wiki.ubuntu.com/UbuntuDevelopment/PackageArchive#Ports"""
71+ if arch is None:
72+ arch = util.get_architecture(target)
73+ if arch in PRIMARY_ARCHES:
74+ return PRIMARY_ARCH_MIRRORS.copy()
75+ if arch in PORTS_ARCHES:
76+ return PORTS_MIRRORS.copy()
77+ raise ValueError("No default mirror known for arch %s" % arch)
78+
79+
80+def handle(name, ocfg, cloud, log, _):
81+ """process the config for apt_config. This can be called from
82+ curthooks if a global apt config was provided or via the "apt"
83+ standalone command."""
84+ # keeping code close to curtin codebase via entry handler
85+ target = None
86+ if log is not None:
87+ global LOG
88+ LOG = log
89+ # feed back converted config, but only work on the subset under 'apt'
90+ ocfg = convert_to_v3_apt_format(ocfg)
91+ cfg = ocfg.get('apt', {})
92+
93+ if not isinstance(cfg, dict):
94+ raise ValueError("Expected dictionary for 'apt' config, found %s",
95+ type(cfg))
96+
97+ LOG.debug("handling apt (module %s) with apt config '%s'", name, cfg)
98+
99+ release = util.lsb_release(target=target)['codename']
100+ arch = util.get_architecture(target)
101+ mirrors = find_apt_mirror_info(cfg, cloud, arch=arch)
102+ LOG.debug("Apt Mirror info: %s", mirrors)
103+
104+ apply_debconf_selections(cfg, target)
105+
106+ if util.is_false(cfg.get('preserve_sources_list', False)):
107+ generate_sources_list(cfg, release, mirrors, cloud)
108+ rename_apt_lists(mirrors, target)
109
110 try:
111 apply_apt_config(cfg, APT_PROXY_FN, APT_CONFIG_FN)
112- except Exception as e:
113- log.warn("failed to proxy or apt config info: %s", e)
114+ except (IOError, OSError):
115+ LOG.exception("Failed to apply proxy or apt config info:")
116
117- # Process 'apt_sources'
118- if 'apt_sources' in cfg:
119+ # Process 'apt_source -> sources {dict}'
120+ if 'sources' in cfg:
121 params = mirrors
122 params['RELEASE'] = release
123- params['MIRROR'] = mirror
124+ params['MIRROR'] = mirrors["MIRROR"]
125
126+ matcher = None
127 matchcfg = cfg.get('add_apt_repo_match', ADD_APT_REPO_MATCH)
128 if matchcfg:
129 matcher = re.compile(matchcfg).search
130+
131+ add_apt_sources(cfg['sources'], cloud, target=target,
132+ template_params=params, aa_repo_match=matcher)
133+
134+
135+def debconf_set_selections(selections, target=None):
136+ util.subp(['debconf-set-selections'], data=selections, target=target,
137+ capture=True)
138+
139+
140+def dpkg_reconfigure(packages, target=None):
141+ # For any packages that are already installed, but have preseed data
142+ # we populate the debconf database, but the filesystem configuration
143+ # would be preferred on a subsequent dpkg-reconfigure.
144+ # so, what we have to do is "know" information about certain packages
145+ # to unconfigure them.
146+ unhandled = []
147+ to_config = []
148+ for pkg in packages:
149+ if pkg in CONFIG_CLEANERS:
150+ LOG.debug("unconfiguring %s", pkg)
151+ CONFIG_CLEANERS[pkg](target)
152+ to_config.append(pkg)
153 else:
154- def matcher(x):
155- return False
156+ unhandled.append(pkg)
157+
158+ if len(unhandled):
159+ LOG.warn("The following packages were installed and preseeded, "
160+ "but cannot be unconfigured: %s", unhandled)
161+
162+ if len(to_config):
163+ util.subp(['dpkg-reconfigure', '--frontend=noninteractive'] +
164+ list(to_config), data=None, target=target, capture=True)
165+
166+
167+def apply_debconf_selections(cfg, target=None):
168+ """apply_debconf_selections - push content to debconf"""
169+ # debconf_selections:
170+ # set1: |
171+ # cloud-init cloud-init/datasources multiselect MAAS
172+ # set2: pkg pkg/value string bar
173+ selsets = cfg.get('debconf_selections')
174+ if not selsets:
175+ LOG.debug("debconf_selections was not set in config")
176+ return
177
178- errors = add_apt_sources(cfg['apt_sources'], params,
179- aa_repo_match=matcher)
180- for e in errors:
181- log.warn("Add source error: %s", ':'.join(e))
182+ selections = '\n'.join(
183+ [selsets[key] for key in sorted(selsets.keys())])
184+ debconf_set_selections(selections.encode() + b"\n", target=target)
185
186- dconf_sel = util.get_cfg_option_str(cfg, 'debconf_selections', False)
187- if dconf_sel:
188- log.debug("Setting debconf selections per cloud config")
189- try:
190- util.subp(('debconf-set-selections', '-'), dconf_sel)
191- except Exception:
192- util.logexc(log, "Failed to run debconf-set-selections")
193+ # get a complete list of packages listed in input
194+ pkgs_cfgd = set()
195+ for key, content in selsets.items():
196+ for line in content.splitlines():
197+ if line.startswith("#"):
198+ continue
199+ pkg = re.sub(r"[:\s].*", "", line)
200+ pkgs_cfgd.add(pkg)
201+
202+ pkgs_installed = util.get_installed_packages(target)
203+
204+ LOG.debug("pkgs_cfgd: %s", pkgs_cfgd)
205+ need_reconfig = pkgs_cfgd.intersection(pkgs_installed)
206+
207+ if len(need_reconfig) == 0:
208+ LOG.debug("no need for reconfig")
209+ return
210+
211+ dpkg_reconfigure(need_reconfig, target=target)
212+
213+
214+def clean_cloud_init(target):
215+ """clean out any local cloud-init config"""
216+ flist = glob.glob(
217+ util.target_path(target, "/etc/cloud/cloud.cfg.d/*dpkg*"))
218+
219+ LOG.debug("cleaning cloud-init config from: %s", flist)
220+ for dpkg_cfg in flist:
221+ os.unlink(dpkg_cfg)
222
223
224 def mirrorurl_to_apt_fileprefix(mirror):
225+ """mirrorurl_to_apt_fileprefix
226+ Convert a mirror url to the file prefix used by apt on disk to
227+ store cache information for that mirror.
228+ To do so do:
229+ - take off ???://
230+ - drop tailing /
231+ - convert in string / to _"""
232 string = mirror
233- # take off http:// or ftp://
234 if string.endswith("/"):
235 string = string[0:-1]
236 pos = string.find("://")
237@@ -106,174 +208,365 @@ def mirrorurl_to_apt_fileprefix(mirror):
238 return string
239
240
241-def rename_apt_lists(old_mirrors, new_mirrors, lists_d="/var/lib/apt/lists"):
242- for (name, omirror) in old_mirrors.items():
243+def rename_apt_lists(new_mirrors, target=None):
244+ """rename_apt_lists - rename apt lists to preserve old cache data"""
245+ default_mirrors = get_default_mirrors(util.get_architecture(target))
246+
247+ pre = util.target_path(target, APT_LISTS)
248+ for (name, omirror) in default_mirrors.items():
249 nmirror = new_mirrors.get(name)
250 if not nmirror:
251 continue
252- oprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(omirror))
253- nprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(nmirror))
254+
255+ oprefix = pre + os.path.sep + mirrorurl_to_apt_fileprefix(omirror)
256+ nprefix = pre + os.path.sep + mirrorurl_to_apt_fileprefix(nmirror)
257 if oprefix == nprefix:
258 continue
259 olen = len(oprefix)
260 for filename in glob.glob("%s_*" % oprefix):
261- util.rename(filename, "%s%s" % (nprefix, filename[olen:]))
262-
263-
264-def get_release():
265- (stdout, _stderr) = util.subp(['lsb_release', '-cs'])
266- return stdout.strip()
267-
268-
269-def generate_sources_list(cfg, codename, mirrors, cloud, log):
270- params = {'codename': codename}
271+ newname = "%s%s" % (nprefix, filename[olen:])
272+ LOG.debug("Renaming apt list %s to %s", filename, newname)
273+ try:
274+ os.rename(filename, newname)
275+ except OSError:
276+ # since this is a best effort task, warn with but don't fail
277+ LOG.warn("Failed to rename apt list:", exc_info=True)
278+
279+
280+def mirror_to_placeholder(tmpl, mirror, placeholder):
281+ """mirror_to_placeholder
282+ replace the specified mirror in a template with a placeholder string
283+ Checks for existance of the expected mirror and warns if not found"""
284+ if mirror not in tmpl:
285+ LOG.warn("Expected mirror '%s' not found in: %s", mirror, tmpl)
286+ return tmpl.replace(mirror, placeholder)
287+
288+
289+def map_known_suites(suite):
290+ """there are a few default names which will be auto-extended.
291+ This comes at the inability to use those names literally as suites,
292+ but on the other hand increases readability of the cfg quite a lot"""
293+ mapping = {'updates': '$RELEASE-updates',
294+ 'backports': '$RELEASE-backports',
295+ 'security': '$RELEASE-security',
296+ 'proposed': '$RELEASE-proposed',
297+ 'release': '$RELEASE'}
298+ try:
299+ retsuite = mapping[suite]
300+ except KeyError:
301+ retsuite = suite
302+ return retsuite
303+
304+
305+def disable_suites(disabled, src, release):
306+ """reads the config for suites to be disabled and removes those
307+ from the template"""
308+ if not disabled:
309+ return src
310+
311+ retsrc = src
312+ for suite in disabled:
313+ suite = map_known_suites(suite)
314+ releasesuite = templater.render_string(suite, {'RELEASE': release})
315+ LOG.debug("Disabling suite %s as %s", suite, releasesuite)
316+
317+ newsrc = ""
318+ for line in retsrc.splitlines(True):
319+ if line.startswith("#"):
320+ newsrc += line
321+ continue
322+
323+ # sources.list allow options in cols[1] which can have spaces
324+ # so the actual suite can be [2] or later. example:
325+ # deb [ arch=amd64,armel k=v ] http://example.com/debian
326+ cols = line.split()
327+ if len(cols) > 1:
328+ pcol = 2
329+ if cols[1].startswith("["):
330+ for col in cols[1:]:
331+ pcol += 1
332+ if col.endswith("]"):
333+ break
334+
335+ if cols[pcol] == releasesuite:
336+ line = '# suite disabled by cloud-init: %s' % line
337+ newsrc += line
338+ retsrc = newsrc
339+
340+ return retsrc
341+
342+
343+def generate_sources_list(cfg, release, mirrors, cloud):
344+ """generate_sources_list
345+ create a source.list file based on a custom or default template
346+ by replacing mirrors and release in the template"""
347+ aptsrc = "/etc/apt/sources.list"
348+ params = {'RELEASE': release, 'codename': release}
349 for k in mirrors:
350 params[k] = mirrors[k]
351+ params[k.lower()] = mirrors[k]
352
353- custtmpl = cfg.get('apt_custom_sources_list', None)
354- if custtmpl is not None:
355- templater.render_string_to_file(custtmpl,
356- '/etc/apt/sources.list', params)
357- return
358-
359- template_fn = cloud.get_template_filename('sources.list.%s' %
360- (cloud.distro.name))
361- if not template_fn:
362- template_fn = cloud.get_template_filename('sources.list')
363+ tmpl = cfg.get('sources_list', None)
364+ if tmpl is None:
365+ LOG.info("No custom template provided, fall back to builtin")
366+ template_fn = cloud.get_template_filename('sources.list.%s' %
367+ (cloud.distro.name))
368 if not template_fn:
369- log.warn("No template found, not rendering /etc/apt/sources.list")
370+ template_fn = cloud.get_template_filename('sources.list')
371+ if not template_fn:
372+ LOG.warn("No template found, not rendering /etc/apt/sources.list")
373 return
374+ tmpl = util.load_file(template_fn)
375
376- templater.render_to_file(template_fn, '/etc/apt/sources.list', params)
377+ rendered = templater.render_string(tmpl, params)
378+ disabled = disable_suites(cfg.get('disable_suites'), rendered, release)
379+ util.write_file(aptsrc, disabled, mode=0o644)
380
381
382-def add_apt_key_raw(key):
383+def add_apt_key_raw(key, target=None):
384 """
385 actual adding of a key as defined in key argument
386 to the system
387 """
388+ LOG.debug("Adding key:\n'%s'", key)
389 try:
390- util.subp(('apt-key', 'add', '-'), key)
391+ util.subp(['apt-key', 'add', '-'], data=key.encode(), target=target)
392 except util.ProcessExecutionError:
393- raise ValueError('failed to add apt GPG Key to apt keyring')
394+ LOG.exception("failed to add apt GPG Key to apt keyring")
395+ raise
396
397
398-def add_apt_key(ent):
399+def add_apt_key(ent, target=None):
400 """
401- add key to the system as defined in ent (if any)
402- supports raw keys or keyid's
403- The latter will as a first step fetch the raw key from a keyserver
404+ Add key to the system as defined in ent (if any).
405+ Supports raw keys or keyid's
406+ The latter will as a first step fetched to get the raw key
407 """
408 if 'keyid' in ent and 'key' not in ent:
409- keyserver = "keyserver.ubuntu.com"
410+ keyserver = DEFAULT_KEYSERVER
411 if 'keyserver' in ent:
412 keyserver = ent['keyserver']
413- ent['key'] = gpg.get_key_by_id(ent['keyid'], keyserver)
414+
415+ ent['key'] = gpg.getkeybyid(ent['keyid'], keyserver)
416
417 if 'key' in ent:
418- add_apt_key_raw(ent['key'])
419+ add_apt_key_raw(ent['key'], target)
420
421
422-def convert_to_new_format(srclist):
423- """convert_to_new_format
424- convert the old list based format to the new dict based one
425- """
426- srcdict = {}
427- if isinstance(srclist, list):
428- for srcent in srclist:
429- if 'filename' not in srcent:
430- # file collides for multiple !filename cases for compatibility
431- # yet we need them all processed, so not same dictionary key
432- srcent['filename'] = "cloud_config_sources.list"
433- key = util.rand_dict_key(srcdict, "cloud_config_sources.list")
434- else:
435- # all with filename use that as key (matching new format)
436- key = srcent['filename']
437- srcdict[key] = srcent
438- elif isinstance(srclist, dict):
439- srcdict = srclist
440- else:
441- raise ValueError("unknown apt_sources format")
442-
443- return srcdict
444+def update_packages(cloud):
445+ cloud.distro.update_package_sources()
446
447
448-def add_apt_sources(srclist, template_params=None, aa_repo_match=None):
449+def add_apt_sources(srcdict, cloud, target=None, template_params=None,
450+ aa_repo_match=None):
451 """
452 add entries in /etc/apt/sources.list.d for each abbreviated
453- sources.list entry in 'srclist'. When rendering template, also
454+ sources.list entry in 'srcdict'. When rendering template, also
455 include the values in dictionary searchList
456 """
457 if template_params is None:
458 template_params = {}
459
460 if aa_repo_match is None:
461- def _aa_repo_match(x):
462- return False
463- aa_repo_match = _aa_repo_match
464+ raise ValueError('did not get a valid repo matcher')
465
466- errorlist = []
467- srcdict = convert_to_new_format(srclist)
468+ if not isinstance(srcdict, dict):
469+ raise TypeError('unknown apt format: %s' % (srcdict))
470
471 for filename in srcdict:
472 ent = srcdict[filename]
473+ LOG.debug("adding source/key '%s'", ent)
474 if 'filename' not in ent:
475 ent['filename'] = filename
476
477- # keys can be added without specifying a source
478- try:
479- add_apt_key(ent)
480- except ValueError as detail:
481- errorlist.append([ent, detail])
482+ add_apt_key(ent, target)
483
484 if 'source' not in ent:
485- errorlist.append(["", "missing source"])
486 continue
487 source = ent['source']
488 source = templater.render_string(source, template_params)
489
490- if not ent['filename'].startswith(os.path.sep):
491+ if not ent['filename'].startswith("/"):
492 ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
493 ent['filename'])
494+ if not ent['filename'].endswith(".list"):
495+ ent['filename'] += ".list"
496
497 if aa_repo_match(source):
498 try:
499- util.subp(["add-apt-repository", source])
500- except util.ProcessExecutionError as e:
501- errorlist.append([source,
502- ("add-apt-repository failed. " + str(e))])
503+ util.subp(["add-apt-repository", source], target=target)
504+ except util.ProcessExecutionError:
505+ LOG.exception("add-apt-repository failed.")
506+ raise
507 continue
508
509+ sourcefn = util.target_path(target, ent['filename'])
510 try:
511 contents = "%s\n" % (source)
512- util.write_file(ent['filename'], contents, omode="ab")
513- except Exception:
514- errorlist.append([source,
515- "failed write to file %s" % ent['filename']])
516+ util.write_file(sourcefn, contents, omode="a")
517+ except IOError as detail:
518+ LOG.exception("failed write to file %s: %s", sourcefn, detail)
519+ raise
520
521- return errorlist
522+ update_packages(cloud)
523
524+ return
525
526-def find_apt_mirror_info(cloud, cfg):
527- """find an apt_mirror given the cloud and cfg provided."""
528
529- mirror = None
530+def convert_v1_to_v2_apt_format(srclist):
531+ """convert v1 apt format to v2 (dict in apt_sources)"""
532+ srcdict = {}
533+ if isinstance(srclist, list):
534+ LOG.debug("apt config: convert V1 to V2 format (source list to dict)")
535+ for srcent in srclist:
536+ if 'filename' not in srcent:
537+ # file collides for multiple !filename cases for compatibility
538+ # yet we need them all processed, so not same dictionary key
539+ srcent['filename'] = "cloud_config_sources.list"
540+ key = util.rand_dict_key(srcdict, "cloud_config_sources.list")
541+ else:
542+ # all with filename use that as key (matching new format)
543+ key = srcent['filename']
544+ srcdict[key] = srcent
545+ elif isinstance(srclist, dict):
546+ srcdict = srclist
547+ else:
548+ raise ValueError("unknown apt_sources format")
549+
550+ return srcdict
551+
552+
553+def convert_key(oldcfg, aptcfg, oldkey, newkey):
554+ """convert an old key to the new one if the old one exists
555+ returns true if a key was found and converted"""
556+ if oldcfg.get(oldkey, None) is not None:
557+ aptcfg[newkey] = oldcfg.get(oldkey)
558+ del oldcfg[oldkey]
559+ return True
560+ return False
561+
562+
563+def convert_mirror(oldcfg, aptcfg):
564+ """convert old apt_mirror keys into the new more advanced mirror spec"""
565+ keymap = [('apt_mirror', 'uri'),
566+ ('apt_mirror_search', 'search'),
567+ ('apt_mirror_search_dns', 'search_dns')]
568+ converted = False
569+ newmcfg = {'arches': ['default']}
570+ for oldkey, newkey in keymap:
571+ if convert_key(oldcfg, newmcfg, oldkey, newkey):
572+ converted = True
573+
574+ # only insert new style config if anything was converted
575+ if converted:
576+ aptcfg['primary'] = [newmcfg]
577+
578+
579+def convert_v2_to_v3_apt_format(oldcfg):
580+ """convert old to new keys and adapt restructured mirror spec"""
581+ oldkeys = ['apt_sources', 'apt_mirror', 'apt_mirror_search',
582+ 'apt_mirror_search_dns', 'apt_proxy', 'apt_http_proxy',
583+ 'apt_ftp_proxy', 'apt_https_proxy',
584+ 'apt_preserve_sources_list', 'apt_custom_sources_list',
585+ 'add_apt_repo_match']
586+ needtoconvert = []
587+ for oldkey in oldkeys:
588+ if oldcfg.get(oldkey, None) is not None:
589+ needtoconvert.append(oldkey)
590+
591+ # no old config, so no new one to be created
592+ if not needtoconvert:
593+ return oldcfg
594+ LOG.debug("apt config: convert V2 to V3 format for keys '%s'",
595+ ", ".join(needtoconvert))
596+
597+ if oldcfg.get('apt', None) is not None:
598+ msg = ("Error in apt configuration: "
599+ "old and new format of apt features are mutually exclusive "
600+ "('apt':'%s' vs '%s' key)" % (oldcfg.get('apt', None),
601+ ", ".join(needtoconvert)))
602+ LOG.error(msg)
603+ raise ValueError(msg)
604+
605+ # create new format from old keys
606+ aptcfg = {}
607+
608+ # renames / moves under the apt key
609+ convert_key(oldcfg, aptcfg, 'add_apt_repo_match', 'add_apt_repo_match')
610+ convert_key(oldcfg, aptcfg, 'apt_proxy', 'proxy')
611+ convert_key(oldcfg, aptcfg, 'apt_http_proxy', 'http_proxy')
612+ convert_key(oldcfg, aptcfg, 'apt_https_proxy', 'https_proxy')
613+ convert_key(oldcfg, aptcfg, 'apt_ftp_proxy', 'ftp_proxy')
614+ convert_key(oldcfg, aptcfg, 'apt_custom_sources_list', 'sources_list')
615+ convert_key(oldcfg, aptcfg, 'apt_preserve_sources_list',
616+ 'preserve_sources_list')
617+ # dict format not changed since v2, just renamed and moved
618+ convert_key(oldcfg, aptcfg, 'apt_sources', 'sources')
619+
620+ convert_mirror(oldcfg, aptcfg)
621+
622+ for oldkey in oldkeys:
623+ if oldcfg.get(oldkey, None) is not None:
624+ raise ValueError("old apt key '%s' left after conversion" % oldkey)
625+
626+ # insert new format into config and return full cfg with only v3 content
627+ oldcfg['apt'] = aptcfg
628+ return oldcfg
629+
630+
631+def convert_to_v3_apt_format(cfg):
632+ """convert the old list based format to the new dict based one. After that
633+ convert the old dict keys/format to v3 a.k.a 'new apt config'"""
634+ # V1 -> V2, the apt_sources entry from list to dict
635+ apt_sources = cfg.get('apt_sources', None)
636+ if apt_sources is not None:
637+ cfg['apt_sources'] = convert_v1_to_v2_apt_format(apt_sources)
638+
639+ # V2 -> V3, move all former globals under the "apt" key
640+ # Restructure into new key names and mirror hierarchy
641+ cfg = convert_v2_to_v3_apt_format(cfg)
642+
643+ return cfg
644+
645+
646+def search_for_mirror(candidates):
647+ """
648+ Search through a list of mirror urls for one that works
649+ This needs to return quickly.
650+ """
651+ if candidates is None:
652+ return None
653+
654+ LOG.debug("search for mirror in candidates: '%s'", candidates)
655+ for cand in candidates:
656+ try:
657+ if util.is_resolvable_url(cand):
658+ LOG.debug("found working mirror: '%s'", cand)
659+ return cand
660+ except Exception:
661+ pass
662+ return None
663
664- # this is less preferred way of specifying mirror preferred would be to
665- # use the distro's search or package_mirror.
666- mirror = cfg.get("apt_mirror", None)
667
668- search = cfg.get("apt_mirror_search", None)
669- if not mirror and search:
670- mirror = util.search_for_mirror(search)
671+def search_for_mirror_dns(configured, mirrortype, cfg, cloud):
672+ """
673+ Try to resolve a list of predefines DNS names to pick mirrors
674+ """
675+ mirror = None
676
677- if (not mirror and
678- util.get_cfg_option_bool(cfg, "apt_mirror_search_dns", False)):
679+ if configured:
680 mydom = ""
681 doms = []
682
683+ if mirrortype == "primary":
684+ mirrordns = "mirror"
685+ elif mirrortype == "security":
686+ mirrordns = "security-mirror"
687+ else:
688+ raise ValueError("unknown mirror type")
689+
690 # if we have a fqdn, then search its domain portion first
691- (_hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)
692+ (_, fqdn) = util.get_hostname_fqdn(cfg, cloud)
693 mydom = ".".join(fqdn.split(".")[1:])
694 if mydom:
695 doms.append(".%s" % mydom)
696@@ -282,38 +575,136 @@ def find_apt_mirror_info(cloud, cfg):
697
698 mirror_list = []
699 distro = cloud.distro.name
700- mirrorfmt = "http://%s-mirror%s/%s" % (distro, "%s", distro)
701+ mirrorfmt = "http://%s-%s%s/%s" % (distro, mirrordns, "%s", distro)
702 for post in doms:
703 mirror_list.append(mirrorfmt % (post))
704
705- mirror = util.search_for_mirror(mirror_list)
706+ mirror = search_for_mirror(mirror_list)
707+
708+ return mirror
709
710+
711+def update_mirror_info(pmirror, smirror, arch, cloud):
712+ """sets security mirror to primary if not defined.
713+ returns defaults if no mirrors are defined"""
714+ if pmirror is not None:
715+ if smirror is None:
716+ smirror = pmirror
717+ return {'PRIMARY': pmirror,
718+ 'SECURITY': smirror}
719+
720+ # None specified at all, get default mirrors from cloud
721 mirror_info = cloud.datasource.get_package_mirror_info()
722+ if mirror_info:
723+ # get_package_mirror_info() returns a dictionary with
724+ # arbitrary key/value pairs including 'primary' and 'security' keys.
725+ # caller expects dict with PRIMARY and SECURITY.
726+ m = mirror_info.copy()
727+ m['PRIMARY'] = m['primary']
728+ m['SECURITY'] = m['security']
729+
730+ return m
731+
732+ # if neither apt nor cloud configured mirrors fall back to
733+ return get_default_mirrors(arch)
734+
735+
736+def get_arch_mirrorconfig(cfg, mirrortype, arch):
737+ """out of a list of potential mirror configurations select
738+ and return the one matching the architecture (or default)"""
739+ # select the mirror specification (if-any)
740+ mirror_cfg_list = cfg.get(mirrortype, None)
741+ if mirror_cfg_list is None:
742+ return None
743+
744+ # select the specification matching the target arch
745+ default = None
746+ for mirror_cfg_elem in mirror_cfg_list:
747+ arches = mirror_cfg_elem.get("arches")
748+ if arch in arches:
749+ return mirror_cfg_elem
750+ if "default" in arches:
751+ default = mirror_cfg_elem
752+ return default
753+
754+
755+def get_mirror(cfg, mirrortype, arch, cloud):
756+ """pass the three potential stages of mirror specification
757+ returns None is neither of them found anything otherwise the first
758+ hit is returned"""
759+ mcfg = get_arch_mirrorconfig(cfg, mirrortype, arch)
760+ if mcfg is None:
761+ return None
762+
763+ # directly specified
764+ mirror = mcfg.get("uri", None)
765+
766+ # fallback to search if specified
767+ if mirror is None:
768+ # list of mirrors to try to resolve
769+ mirror = search_for_mirror(mcfg.get("search", None))
770+
771+ # fallback to search_dns if specified
772+ if mirror is None:
773+ # list of mirrors to try to resolve
774+ mirror = search_for_mirror_dns(mcfg.get("search_dns", None),
775+ mirrortype, cfg, cloud)
776+
777+ return mirror
778+
779+
780+def find_apt_mirror_info(cfg, cloud, arch=None):
781+ """find_apt_mirror_info
782+ find an apt_mirror given the cfg provided.
783+ It can check for separate config of primary and security mirrors
784+ If only primary is given security is assumed to be equal to primary
785+ If the generic apt_mirror is given that is defining for both
786+ """
787
788- # this is a bit strange.
789- # if mirror is set, then one of the legacy options above set it
790- # but they do not cover security. so we need to get that from
791- # get_package_mirror_info
792- if mirror:
793- mirror_info.update({'primary': mirror})
794+ if arch is None:
795+ arch = util.get_architecture()
796+ LOG.debug("got arch for mirror selection: %s", arch)
797+ pmirror = get_mirror(cfg, "primary", arch, cloud)
798+ LOG.debug("got primary mirror: %s", pmirror)
799+ smirror = get_mirror(cfg, "security", arch, cloud)
800+ LOG.debug("got security mirror: %s", smirror)
801+
802+ mirror_info = update_mirror_info(pmirror, smirror, arch, cloud)
803+
804+ # less complex replacements use only MIRROR, derive from primary
805+ mirror_info["MIRROR"] = mirror_info["PRIMARY"]
806
807 return mirror_info
808
809
810 def apply_apt_config(cfg, proxy_fname, config_fname):
811+ """apply_apt_config
812+ Applies any apt*proxy config from if specified
813+ """
814 # Set up any apt proxy
815- cfgs = (('apt_proxy', 'Acquire::HTTP::Proxy "%s";'),
816- ('apt_http_proxy', 'Acquire::HTTP::Proxy "%s";'),
817- ('apt_ftp_proxy', 'Acquire::FTP::Proxy "%s";'),
818- ('apt_https_proxy', 'Acquire::HTTPS::Proxy "%s";'))
819+ cfgs = (('proxy', 'Acquire::http::Proxy "%s";'),
820+ ('http_proxy', 'Acquire::http::Proxy "%s";'),
821+ ('ftp_proxy', 'Acquire::ftp::Proxy "%s";'),
822+ ('https_proxy', 'Acquire::https::Proxy "%s";'))
823
824 proxies = [fmt % cfg.get(name) for (name, fmt) in cfgs if cfg.get(name)]
825 if len(proxies):
826+ LOG.debug("write apt proxy info to %s", proxy_fname)
827 util.write_file(proxy_fname, '\n'.join(proxies) + '\n')
828 elif os.path.isfile(proxy_fname):
829 util.del_file(proxy_fname)
830+ LOG.debug("no apt proxy configured, removed %s", proxy_fname)
831
832- if cfg.get('apt_config', None):
833- util.write_file(config_fname, cfg.get('apt_config'))
834+ if cfg.get('conf', None):
835+ LOG.debug("write apt config info to %s", config_fname)
836+ util.write_file(config_fname, cfg.get('conf'))
837 elif os.path.isfile(config_fname):
838 util.del_file(config_fname)
839+ LOG.debug("no apt config configured, removed %s", config_fname)
840+
841+
842+CONFIG_CLEANERS = {
843+ 'cloud-init': clean_cloud_init,
844+}
845+
846+# vi: ts=4 expandtab syntax=python
847diff --git a/cloudinit/gpg.py b/cloudinit/gpg.py
848index 6a76d78..5bbff51 100644
849--- a/cloudinit/gpg.py
850+++ b/cloudinit/gpg.py
851@@ -36,11 +36,11 @@ def export_armour(key):
852 return armour
853
854
855-def receive_key(key, keyserver):
856+def recv_key(key, keyserver):
857 """Receive gpg key from the specified keyserver"""
858 LOG.debug('Receive gpg key "%s"', key)
859 try:
860- util.subp(["gpg", "--keyserver", keyserver, "--recv-keys", key],
861+ util.subp(["gpg", "--keyserver", keyserver, "--recv", key],
862 capture=True)
863 except util.ProcessExecutionError as error:
864 raise ValueError(('Failed to import key "%s" '
865@@ -57,12 +57,12 @@ def delete_key(key):
866 LOG.warn('Failed delete key "%s": %s', key, error)
867
868
869-def get_key_by_id(keyid, keyserver="keyserver.ubuntu.com"):
870+def getkeybyid(keyid, keyserver='keyserver.ubuntu.com'):
871 """get gpg keyid from keyserver"""
872 armour = export_armour(keyid)
873 if not armour:
874 try:
875- receive_key(keyid, keyserver=keyserver)
876+ recv_key(keyid, keyserver=keyserver)
877 armour = export_armour(keyid)
878 except ValueError:
879 LOG.exception('Failed to obtain gpg key %s', keyid)
880diff --git a/cloudinit/util.py b/cloudinit/util.py
881index 226628c..db80ca9 100644
882--- a/cloudinit/util.py
883+++ b/cloudinit/util.py
884@@ -61,6 +61,10 @@ from cloudinit import version
885
886 from cloudinit.settings import (CFG_BUILTIN)
887
888+try:
889+ string_types = (basestring,)
890+except NameError:
891+ string_types = (str,)
892
893 _DNS_REDIRECT_IP = None
894 LOG = logging.getLogger(__name__)
895@@ -82,6 +86,71 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
896
897 PROC_CMDLINE = None
898
899+_LSB_RELEASE = {}
900+
901+
902+def get_architecture(target=None):
903+ out, _ = subp(['dpkg', '--print-architecture'], capture=True,
904+ target=target)
905+ return out.strip()
906+
907+
908+def _lsb_release(target=None):
909+ fmap = {'Codename': 'codename', 'Description': 'description',
910+ 'Distributor ID': 'id', 'Release': 'release'}
911+
912+ data = {}
913+ try:
914+ out, _ = subp(['lsb_release', '--all'], capture=True, target=target)
915+ for line in out.splitlines():
916+ fname, _, val = line.partition(":")
917+ if fname in fmap:
918+ data[fmap[fname]] = val.strip()
919+ missing = [k for k in fmap.values() if k not in data]
920+ if len(missing):
921+ LOG.warn("Missing fields in lsb_release --all output: %s",
922+ ','.join(missing))
923+
924+ except ProcessExecutionError as err:
925+ LOG.warn("Unable to get lsb_release --all: %s", err)
926+ data = {v: "UNAVAILABLE" for v in fmap.values()}
927+
928+ return data
929+
930+
931+def lsb_release(target=None):
932+ if target_path(target) != "/":
933+ # do not use or update cache if target is provided
934+ return _lsb_release(target)
935+
936+ global _LSB_RELEASE
937+ if not _LSB_RELEASE:
938+ data = _lsb_release()
939+ _LSB_RELEASE.update(data)
940+ return _LSB_RELEASE
941+
942+
943+def target_path(target, path=None):
944+ # return 'path' inside target, accepting target as None
945+ if target in (None, ""):
946+ target = "/"
947+ elif not isinstance(target, string_types):
948+ raise ValueError("Unexpected input for target: %s" % target)
949+ else:
950+ target = os.path.abspath(target)
951+ # abspath("//") returns "//" specifically for 2 slashes.
952+ if target.startswith("//"):
953+ target = target[1:]
954+
955+ if not path:
956+ return target
957+
958+ # os.path.join("/etc", "/foo") returns "/foo". Chomp all leading /.
959+ while len(path) and path[0] == "/":
960+ path = path[1:]
961+
962+ return os.path.join(target, path)
963+
964
965 def decode_binary(blob, encoding='utf-8'):
966 # Converts a binary type into a text type using given encoding.
967@@ -1688,10 +1757,20 @@ def delete_dir_contents(dirname):
968
969
970 def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
971- logstring=False):
972+ logstring=False, decode="replace", target=None):
973+
974+ # not supported in cloud-init (yet), for now kept in the call signature
975+ # to ease maintaining code shared between cloud-init and curtin
976+ if target is not None:
977+ raise ValueError("target arg not supported by cloud-init")
978+
979 if rcs is None:
980 rcs = [0]
981+
982+ devnull_fp = None
983 try:
984+ if target_path(target) != "/":
985+ args = ['chroot', target] + list(args)
986
987 if not logstring:
988 LOG.debug(("Running command %s with allowed return codes %s"
989@@ -1700,33 +1779,52 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
990 LOG.debug(("Running hidden command to protect sensitive "
991 "input/output logstring: %s"), logstring)
992
993- if not capture:
994- stdout = None
995- stderr = None
996- else:
997+ stdin = None
998+ stdout = None
999+ stderr = None
1000+ if capture:
1001 stdout = subprocess.PIPE
1002 stderr = subprocess.PIPE
1003- stdin = subprocess.PIPE
1004- kws = dict(stdout=stdout, stderr=stderr, stdin=stdin,
1005- env=env, shell=shell)
1006- if six.PY3:
1007- # Use this so subprocess output will be (Python 3) str, not bytes.
1008- kws['universal_newlines'] = True
1009- sp = subprocess.Popen(args, **kws)
1010+ if data is None:
1011+ # using devnull assures any reads get null, rather
1012+ # than possibly waiting on input.
1013+ devnull_fp = open(os.devnull)
1014+ stdin = devnull_fp
1015+ else:
1016+ stdin = subprocess.PIPE
1017+ if not isinstance(data, bytes):
1018+ data = data.encode()
1019+
1020+ sp = subprocess.Popen(args, stdout=stdout,
1021+ stderr=stderr, stdin=stdin,
1022+ env=env, shell=shell)
1023 (out, err) = sp.communicate(data)
1024+
1025+ # Just ensure blank instead of none.
1026+ if not out and capture:
1027+ out = b''
1028+ if not err and capture:
1029+ err = b''
1030+ if decode:
1031+ def ldecode(data, m='utf-8'):
1032+ if not isinstance(data, bytes):
1033+ return data
1034+ return data.decode(m, errors=decode)
1035+
1036+ out = ldecode(out)
1037+ err = ldecode(err)
1038 except OSError as e:
1039 raise ProcessExecutionError(cmd=args, reason=e,
1040 errno=e.errno)
1041+ finally:
1042+ if devnull_fp:
1043+ devnull_fp.close()
1044+
1045 rc = sp.returncode
1046 if rc not in rcs:
1047 raise ProcessExecutionError(stdout=out, stderr=err,
1048 exit_code=rc,
1049 cmd=args)
1050- # Just ensure blank instead of none?? (iff capturing)
1051- if not out and capture:
1052- out = ''
1053- if not err and capture:
1054- err = ''
1055 return (out, err)
1056
1057
1058@@ -2251,3 +2349,18 @@ def message_from_string(string):
1059 if sys.version_info[:2] < (2, 7):
1060 return email.message_from_file(six.StringIO(string))
1061 return email.message_from_string(string)
1062+
1063+
1064+def get_installed_packages(target=None):
1065+ (out, _) = subp(['dpkg-query', '--list'], target=target, capture=True)
1066+
1067+ pkgs_inst = set()
1068+ for line in out.splitlines():
1069+ try:
1070+ (state, pkg, _) = line.split(None, 2)
1071+ except ValueError:
1072+ continue
1073+ if state.startswith("hi") or state.startswith("ii"):
1074+ pkgs_inst.add(re.sub(":.*", "", pkg))
1075+
1076+ return pkgs_inst
1077diff --git a/doc/examples/cloud-config-add-apt-repos.txt b/doc/examples/cloud-config-add-apt-repos.txt
1078index be9d547..22ef761 100644
1079--- a/doc/examples/cloud-config-add-apt-repos.txt
1080+++ b/doc/examples/cloud-config-add-apt-repos.txt
1081@@ -4,18 +4,21 @@
1082 #
1083 # Default: auto select based on cloud metadata
1084 # in ec2, the default is <region>.archive.ubuntu.com
1085-# apt_mirror:
1086-# use the provided mirror
1087-# apt_mirror_search:
1088-# search the list for the first mirror.
1089-# this is currently very limited, only verifying that
1090-# the mirror is dns resolvable or an IP address
1091+# apt:
1092+# primary:
1093+# - arches [default]
1094+# uri:
1095+# use the provided mirror
1096+# search:
1097+# search the list for the first mirror.
1098+# this is currently very limited, only verifying that
1099+# the mirror is dns resolvable or an IP address
1100 #
1101-# if neither apt_mirror nor apt_mirror search is set (the default)
1102+# if neither mirror is set (the default)
1103 # then use the mirror provided by the DataSource found.
1104 # In EC2, that means using <region>.ec2.archive.ubuntu.com
1105-#
1106-# if no mirror is provided by the DataSource, and 'apt_mirror_search_dns' is
1107+#
1108+# if no mirror is provided by the DataSource, but 'search_dns' is
1109 # true, then search for dns names '<distro>-mirror' in each of
1110 # - fqdn of this host per cloud metadata
1111 # - localdomain
1112@@ -27,8 +30,19 @@
1113 # up and expose them only by creating dns entries.
1114 #
1115 # if none of that is found, then the default distro mirror is used
1116-apt_mirror: http://us.archive.ubuntu.com/ubuntu/
1117-apt_mirror_search:
1118- - http://local-mirror.mydomain
1119- - http://archive.ubuntu.com
1120-apt_mirror_search_dns: False
1121+apt:
1122+ primary:
1123+ - arches: [default]
1124+ uri: http://us.archive.ubuntu.com/ubuntu/
1125+# or
1126+apt:
1127+ primary:
1128+ - arches: [default]
1129+ search:
1130+ - http://local-mirror.mydomain
1131+ - http://archive.ubuntu.com
1132+# or
1133+apt:
1134+ primary:
1135+ - arches: [default]
1136+ search_dns: True
1137diff --git a/doc/examples/cloud-config-apt.txt b/doc/examples/cloud-config-apt.txt
1138new file mode 100644
1139index 0000000..1a0fc6f
1140--- /dev/null
1141+++ b/doc/examples/cloud-config-apt.txt
1142@@ -0,0 +1,328 @@
1143+# apt_pipelining (configure Acquire::http::Pipeline-Depth)
1144+# Default: disables HTTP pipelining. Certain web servers, such
1145+# as S3 do not pipeline properly (LP: #948461).
1146+# Valid options:
1147+# False/default: Disables pipelining for APT
1148+# None/Unchanged: Use OS default
1149+# Number: Set pipelining to some number (not recommended)
1150+apt_pipelining: False
1151+
1152+## apt config via system_info:
1153+# under the 'system_info', you can customize cloud-init's interaction
1154+# with apt.
1155+# system_info:
1156+# apt_get_command: [command, argument, argument]
1157+# apt_get_upgrade_subcommand: dist-upgrade
1158+#
1159+# apt_get_command:
1160+# To specify a different 'apt-get' command, set 'apt_get_command'.
1161+# This must be a list, and the subcommand (update, upgrade) is appended to it.
1162+# default is:
1163+# ['apt-get', '--option=Dpkg::Options::=--force-confold',
1164+# '--option=Dpkg::options::=--force-unsafe-io', '--assume-yes', '--quiet']
1165+#
1166+# apt_get_upgrade_subcommand: "dist-upgrade"
1167+# Specify a different subcommand for 'upgrade. The default is 'dist-upgrade'.
1168+# This is the subcommand that is invoked for package_upgrade.
1169+#
1170+# apt_get_wrapper:
1171+# command: eatmydata
1172+# enabled: [True, False, "auto"]
1173+#
1174+
1175+# Install additional packages on first boot
1176+#
1177+# Default: none
1178+#
1179+# if packages are specified, this apt_update will be set to true
1180+
1181+packages: ['pastebinit']
1182+
1183+apt:
1184+ # The apt config consists of two major "areas".
1185+ #
1186+ # On one hand there is the global configuration for the apt feature.
1187+ #
1188+ # On one hand (down in this file) there is the source dictionary which allows
1189+ # to define various entries to be considered by apt.
1190+
1191+ ##############################################################################
1192+ # Section 1: global apt configuration
1193+ #
1194+ # The following examples number the top keys to ease identification in
1195+ # discussions.
1196+
1197+ # 1.1 preserve_sources_list
1198+ #
1199+ # Preserves the existing /etc/apt/sources.list
1200+ # Default: false - do overwrite sources_list. If set to true then any
1201+ # "mirrors" configuration will have no effect.
1202+ # Set to true to avoid affecting sources.list. In that case only
1203+ # "extra" source specifications will be written into
1204+ # /etc/apt/sources.list.d/*
1205+ preserve_sources_list: true
1206+
1207+ # 1.2 disable_suites
1208+ #
1209+ # This is an empty list by default, so nothing is disabled.
1210+ #
1211+ # If given, those suites are removed from sources.list after all other
1212+ # modifications have been made.
1213+ # Suites are even disabled if no other modification was made,
1214+ # but not if is preserve_sources_list is active.
1215+ # There is a special alias “$RELEASE” as in the sources that will be replace
1216+ # by the matching release.
1217+ #
1218+ # To ease configuration and improve readability the following common ubuntu
1219+ # suites will be automatically mapped to their full definition.
1220+ # updates => $RELEASE-updates
1221+ # backports => $RELEASE-backports
1222+ # security => $RELEASE-security
1223+ # proposed => $RELEASE-proposed
1224+ # release => $RELEASE
1225+ #
1226+ # There is no harm in specifying a suite to be disabled that is not found in
1227+ # the source.list file (just a no-op then)
1228+ #
1229+ # Note: Lines don’t get deleted, but disabled by being converted to a comment.
1230+ # The following example disables all usual defaults except $RELEASE-security.
1231+ # On top it disables a custom suite called "mysuite"
1232+ disable_suites: [$RELEASE-updates, backports, $RELEASE, mysuite]
1233+
1234+ # 1.3 primary/security archives
1235+ #
1236+ # Default: none - instead it is auto select based on cloud metadata
1237+ # so if neither "uri" nor "search", nor "search_dns" is set (the default)
1238+ # then use the mirror provided by the DataSource found.
1239+ # In EC2, that means using <region>.ec2.archive.ubuntu.com
1240+ #
1241+ # define a custom (e.g. localized) mirror that will be used in sources.list
1242+ # and any custom sources entries for deb / deb-src lines.
1243+ #
1244+ # One can set primary and security mirror to different uri's
1245+ # the child elements to the keys primary and secondary are equivalent
1246+ primary:
1247+ # arches is list of architectures the following config applies to
1248+ # the special keyword "default" applies to any architecture not explicitly
1249+ # listed.
1250+ - arches: [amd64, i386, default]
1251+ # uri is just defining the target as-is
1252+ uri: http://us.archive.ubuntu.com/ubuntu
1253+ #
1254+ # via search one can define lists that are tried one by one.
1255+ # The first with a working DNS resolution (or if it is an IP) will be
1256+ # picked. That way one can keep one configuration for multiple
1257+ # subenvironments that select the working one.
1258+ search:
1259+ - http://cool.but-sometimes-unreachable.com/ubuntu
1260+ - http://us.archive.ubuntu.com/ubuntu
1261+ # if no mirror is provided by uri or search but 'search_dns' is
1262+ # true, then search for dns names '<distro>-mirror' in each of
1263+ # - fqdn of this host per cloud metadata
1264+ # - localdomain
1265+ # - no domain (which would search domains listed in /etc/resolv.conf)
1266+ # If there is a dns entry for <distro>-mirror, then it is assumed that
1267+ # there is a distro mirror at http://<distro>-mirror.<domain>/<distro>
1268+ #
1269+ # That gives the cloud provider the opportunity to set mirrors of a distro
1270+ # up and expose them only by creating dns entries.
1271+ #
1272+ # if none of that is found, then the default distro mirror is used
1273+ search_dns: true
1274+ #
1275+ # If multiple of a category are given
1276+ # 1. uri
1277+ # 2. search
1278+ # 3. search_dns
1279+ # the first defining a valid mirror wins (in the order as defined here,
1280+ # not the order as listed in the config).
1281+ #
1282+ - arches: [s390x, arm64]
1283+ # as above, allowing to have one config for different per arch mirrors
1284+ # security is optional, if not defined it is set to the same value as primary
1285+ security:
1286+ uri: http://security.ubuntu.com/ubuntu
1287+ # If search_dns is set for security the searched pattern is:
1288+ # <distro>-security-mirror
1289+
1290+ # if no mirrors are specified at all, or all lookups fail it will try
1291+ # to get them from the cloud datasource and if those neither provide one fall
1292+ # back to:
1293+ # primary: http://archive.ubuntu.com/ubuntu
1294+ # security: http://security.ubuntu.com/ubuntu
1295+
1296+ # 1.4 sources_list
1297+ #
1298+ # Provide a custom template for rendering sources.list
1299+ # without one provided cloud-init uses builtin templates for
1300+ # ubuntu and debian.
1301+ # Within these sources.list templates you can use the following replacement
1302+ # variables (all have sane Ubuntu defaults, but mirrors can be overwritten
1303+ # as needed (see above)):
1304+ # => $RELEASE, $MIRROR, $PRIMARY, $SECURITY
1305+ sources_list: | # written by cloud-init custom template
1306+ deb $MIRROR $RELEASE main restricted
1307+ deb-src $MIRROR $RELEASE main restricted
1308+ deb $PRIMARY $RELEASE universe restricted
1309+ deb $SECURITY $RELEASE-security multiverse
1310+
1311+ # 1.5 conf
1312+ #
1313+ # Any apt config string that will be made available to apt
1314+ # see the APT.CONF(5) man page for details what can be specified
1315+ conf: | # APT config
1316+ APT {
1317+ Get {
1318+ Assume-Yes "true";
1319+ Fix-Broken "true";
1320+ };
1321+ };
1322+
1323+ # 1.6 (http_|ftp_|https_)proxy
1324+ #
1325+ # Proxies are the most common apt.conf option, so that for simplified use
1326+ # there is a shortcut for those. Those get automatically translated into the
1327+ # correct Acquire::*::Proxy statements.
1328+ #
1329+ # note: proxy actually being a short synonym to http_proxy
1330+ proxy: http://[[user][:pass]@]host[:port]/
1331+ http_proxy: http://[[user][:pass]@]host[:port]/
1332+ ftp_proxy: ftp://[[user][:pass]@]host[:port]/
1333+ https_proxy: https://[[user][:pass]@]host[:port]/
1334+
1335+ # 1.7 add_apt_repo_match
1336+ #
1337+ # 'source' entries in apt-sources that match this python regex
1338+ # expression will be passed to add-apt-repository
1339+ # The following example is also the builtin default if nothing is specified
1340+ add_apt_repo_match: '^[\w-]+:\w'
1341+
1342+
1343+ ##############################################################################
1344+ # Section 2: source list entries
1345+ #
1346+ # This is a dictionary (unlike most block/net which are lists)
1347+ #
1348+ # The key of each source entry is the filename and will be prepended by
1349+ # /etc/apt/sources.list.d/ if it doesn't start with a '/'.
1350+ # If it doesn't end with .list it will be appended so that apt picks up it's
1351+ # configuration.
1352+ #
1353+ # Whenever there is no content to be written into such a file, the key is
1354+ # not used as filename - yet it can still be used as index for merging
1355+ # configuration.
1356+ #
1357+ # The values inside the entries consost of the following optional entries:
1358+ # 'source': a sources.list entry (some variable replacements apply)
1359+ # 'keyid': providing a key to import via shortid or fingerprint
1360+ # 'key': providing a raw PGP key
1361+ # 'keyserver': specify an alternate keyserver to pull keys from that
1362+ # were specified by keyid
1363+
1364+ # This allows merging between multiple input files than a list like:
1365+ # cloud-config1
1366+ # sources:
1367+ # s1: {'key': 'key1', 'source': 'source1'}
1368+ # cloud-config2
1369+ # sources:
1370+ # s2: {'key': 'key2'}
1371+ # s1: {'keyserver': 'foo'}
1372+ # This would be merged to
1373+ # sources:
1374+ # s1:
1375+ # keyserver: foo
1376+ # key: key1
1377+ # source: source1
1378+ # s2:
1379+ # key: key2
1380+ #
1381+ # The following examples number the subfeatures per sources entry to ease
1382+ # identification in discussions.
1383+
1384+
1385+ sources:
1386+ curtin-dev-ppa.list:
1387+ # 2.1 source
1388+ #
1389+ # Creates a file in /etc/apt/sources.list.d/ for the sources list entry
1390+ # based on the key: "/etc/apt/sources.list.d/curtin-dev-ppa.list"
1391+ source: "deb http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu xenial main"
1392+
1393+ # 2.2 keyid
1394+ #
1395+ # Importing a gpg key for a given key id. Used keyserver defaults to
1396+ # keyserver.ubuntu.com
1397+ keyid: F430BBA5 # GPG key ID published on a key server
1398+
1399+ ignored1:
1400+ # 2.3 PPA shortcut
1401+ #
1402+ # Setup correct apt sources.list line and Auto-Import the signing key
1403+ # from LP
1404+ #
1405+ # See https://help.launchpad.net/Packaging/PPA for more information
1406+ # this requires 'add-apt-repository'. This will create a file in
1407+ # /etc/apt/sources.list.d automatically, therefore the key here is
1408+ # ignored as filename in those cases.
1409+ source: "ppa:curtin-dev/test-archive" # Quote the string
1410+
1411+ my-repo2.list:
1412+ # 2.4 replacement variables
1413+ #
1414+ # sources can use $MIRROR, $PRIMARY, $SECURITY and $RELEASE replacement
1415+ # variables.
1416+ # They will be replaced with the default or specified mirrors and the
1417+ # running release.
1418+ # The entry below would be possibly turned into:
1419+ # source: deb http://archive.ubuntu.com/ubuntu xenial multiverse
1420+ source: deb $MIRROR $RELEASE multiverse
1421+
1422+ my-repo3.list:
1423+ # this would have the same end effect as 'ppa:curtin-dev/test-archive'
1424+ source: "deb http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu xenial main"
1425+ keyid: F430BBA5 # GPG key ID published on the key server
1426+ filename: curtin-dev-ppa.list
1427+
1428+ ignored2:
1429+ # 2.5 key only
1430+ #
1431+ # this would only import the key without adding a ppa or other source spec
1432+ # since this doesn't generate a source.list file the filename key is ignored
1433+ keyid: F430BBA5 # GPG key ID published on a key server
1434+
1435+ ignored3:
1436+ # 2.6 key id alternatives
1437+ #
1438+ # Keyid's can also be specified via their long fingerprints
1439+ keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
1440+
1441+ ignored4:
1442+ # 2.7 alternative keyservers
1443+ #
1444+ # One can also specify alternative keyservers to fetch keys from.
1445+ keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
1446+ keyserver: pgp.mit.edu
1447+
1448+
1449+ my-repo4.list:
1450+ # 2.8 raw key
1451+ #
1452+ # The apt signing key can also be specified by providing a pgp public key
1453+ # block. Providing the PGP key this way is the most robust method for
1454+ # specifying a key, as it removes dependency on a remote key server.
1455+ #
1456+ # As with keyid's this can be specified with or without some actual source
1457+ # content.
1458+ key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
1459+ -----BEGIN PGP PUBLIC KEY BLOCK-----
1460+ Version: SKS 1.0.10
1461+
1462+ mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
1463+ qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
1464+ 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
1465+ IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
1466+ 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
1467+ t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
1468+ uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
1469+ =Y2oI
1470+ -----END PGP PUBLIC KEY BLOCK-----
1471diff --git a/doc/examples/cloud-config-chef-oneiric.txt b/doc/examples/cloud-config-chef-oneiric.txt
1472index 2e5f4b1..75c9aee 100644
1473--- a/doc/examples/cloud-config-chef-oneiric.txt
1474+++ b/doc/examples/cloud-config-chef-oneiric.txt
1475@@ -11,39 +11,40 @@
1476 # The default is to install from packages.
1477
1478 # Key from http://apt.opscode.com/packages@opscode.com.gpg.key
1479-apt_sources:
1480- - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
1481- key: |
1482- -----BEGIN PGP PUBLIC KEY BLOCK-----
1483- Version: GnuPG v1.4.9 (GNU/Linux)
1484-
1485- mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
1486- twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
1487- dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
1488- JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
1489- ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
1490- XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
1491- DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
1492- sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
1493- Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
1494- YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
1495- CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
1496- +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
1497- lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
1498- DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
1499- wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
1500- EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
1501- w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
1502- AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
1503- QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
1504- Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
1505- 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
1506- Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
1507- zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
1508- DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
1509- 0GLl8EkfA8uhluM=
1510- =zKAm
1511- -----END PGP PUBLIC KEY BLOCK-----
1512+apt:
1513+ sources:
1514+ - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
1515+ key: |
1516+ -----BEGIN PGP PUBLIC KEY BLOCK-----
1517+ Version: GnuPG v1.4.9 (GNU/Linux)
1518+
1519+ mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
1520+ twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
1521+ dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
1522+ JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
1523+ ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
1524+ XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
1525+ DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
1526+ sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
1527+ Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
1528+ YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
1529+ CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
1530+ +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
1531+ lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
1532+ DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
1533+ wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
1534+ EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
1535+ w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
1536+ AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
1537+ QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
1538+ Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
1539+ 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
1540+ Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
1541+ zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
1542+ DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
1543+ 0GLl8EkfA8uhluM=
1544+ =zKAm
1545+ -----END PGP PUBLIC KEY BLOCK-----
1546
1547 chef:
1548
1549diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
1550index b886cba..75d78a1 100644
1551--- a/doc/examples/cloud-config-chef.txt
1552+++ b/doc/examples/cloud-config-chef.txt
1553@@ -11,39 +11,40 @@
1554 # The default is to install from packages.
1555
1556 # Key from http://apt.opscode.com/packages@opscode.com.gpg.key
1557-apt_sources:
1558- - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
1559- key: |
1560- -----BEGIN PGP PUBLIC KEY BLOCK-----
1561- Version: GnuPG v1.4.9 (GNU/Linux)
1562-
1563- mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
1564- twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
1565- dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
1566- JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
1567- ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
1568- XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
1569- DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
1570- sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
1571- Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
1572- YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
1573- CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
1574- +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
1575- lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
1576- DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
1577- wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
1578- EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
1579- w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
1580- AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
1581- QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
1582- Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
1583- 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
1584- Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
1585- zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
1586- DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
1587- 0GLl8EkfA8uhluM=
1588- =zKAm
1589- -----END PGP PUBLIC KEY BLOCK-----
1590+apt:
1591+ sources:
1592+ - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
1593+ key: |
1594+ -----BEGIN PGP PUBLIC KEY BLOCK-----
1595+ Version: GnuPG v1.4.9 (GNU/Linux)
1596+
1597+ mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
1598+ twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
1599+ dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
1600+ JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
1601+ ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
1602+ XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
1603+ DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
1604+ sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
1605+ Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
1606+ YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
1607+ CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
1608+ +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
1609+ lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
1610+ DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
1611+ wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
1612+ EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
1613+ w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
1614+ AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
1615+ QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
1616+ Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
1617+ 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
1618+ Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
1619+ zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
1620+ DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
1621+ 0GLl8EkfA8uhluM=
1622+ =zKAm
1623+ -----END PGP PUBLIC KEY BLOCK-----
1624
1625 chef:
1626
1627diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
1628index 3cc9c05..190029e 100644
1629--- a/doc/examples/cloud-config.txt
1630+++ b/doc/examples/cloud-config.txt
1631@@ -18,256 +18,7 @@ package_upgrade: true
1632 # Aliases: apt_reboot_if_required
1633 package_reboot_if_required: true
1634
1635-# Add apt repositories
1636-#
1637-# Default: auto select based on cloud metadata
1638-# in ec2, the default is <region>.archive.ubuntu.com
1639-# apt_mirror:
1640-# use the provided mirror
1641-# apt_mirror_search:
1642-# search the list for the first mirror.
1643-# this is currently very limited, only verifying that
1644-# the mirror is dns resolvable or an IP address
1645-#
1646-# if neither apt_mirror nor apt_mirror search is set (the default)
1647-# then use the mirror provided by the DataSource found.
1648-# In EC2, that means using <region>.ec2.archive.ubuntu.com
1649-#
1650-# if no mirror is provided by the DataSource, and 'apt_mirror_search_dns' is
1651-# true, then search for dns names '<distro>-mirror' in each of
1652-# - fqdn of this host per cloud metadata
1653-# - localdomain
1654-# - no domain (which would search domains listed in /etc/resolv.conf)
1655-# If there is a dns entry for <distro>-mirror, then it is assumed that there
1656-# is a distro mirror at http://<distro>-mirror.<domain>/<distro>
1657-#
1658-# That gives the cloud provider the opportunity to set mirrors of a distro
1659-# up and expose them only by creating dns entries.
1660-#
1661-# if none of that is found, then the default distro mirror is used
1662-apt_mirror: http://us.archive.ubuntu.com/ubuntu/
1663-apt_mirror_search:
1664- - http://local-mirror.mydomain
1665- - http://archive.ubuntu.com
1666-
1667-apt_mirror_search_dns: False
1668-
1669-# apt_proxy (configure Acquire::HTTP::Proxy)
1670-# 'apt_http_proxy' is an alias for 'apt_proxy'.
1671-# Also, available are 'apt_ftp_proxy' and 'apt_https_proxy'.
1672-# These affect Acquire::FTP::Proxy and Acquire::HTTPS::Proxy respectively
1673-apt_proxy: http://my.apt.proxy:3128
1674-
1675-# apt_pipelining (configure Acquire::http::Pipeline-Depth)
1676-# Default: disables HTTP pipelining. Certain web servers, such
1677-# as S3 do not pipeline properly (LP: #948461).
1678-# Valid options:
1679-# False/default: Disables pipelining for APT
1680-# None/Unchanged: Use OS default
1681-# Number: Set pipelining to some number (not recommended)
1682-apt_pipelining: False
1683-
1684-# Preserve existing /etc/apt/sources.list
1685-# Default: overwrite sources_list with mirror. If this is true
1686-# then apt_mirror above will have no effect
1687-apt_preserve_sources_list: true
1688-
1689-# Provide a custom template for rendering sources.list
1690-# Default: a default template for Ubuntu/Debain will be used as packaged in
1691-# Ubuntu: /etc/cloud/templates/sources.list.ubuntu.tmpl
1692-# Debian: /etc/cloud/templates/sources.list.debian.tmpl
1693-# Others: n/a
1694-# This will follow the normal mirror/codename replacement rules before
1695-# being written to disk.
1696-apt_custom_sources_list: |
1697- ## template:jinja
1698- ## Note, this file is written by cloud-init on first boot of an instance
1699- ## modifications made here will not survive a re-bundle.
1700- ## if you wish to make changes you can:
1701- ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
1702- ## or do the same in user-data
1703- ## b.) add sources in /etc/apt/sources.list.d
1704- ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
1705- deb {{mirror}} {{codename}} main restricted
1706- deb-src {{mirror}} {{codename}} main restricted
1707-
1708- # could drop some of the usually used entries
1709-
1710- # could refer to other mirrors
1711- deb http://ddebs.ubuntu.com {{codename}} main restricted universe multiverse
1712- deb http://ddebs.ubuntu.com {{codename}}-updates main restricted universe multiverse
1713- deb http://ddebs.ubuntu.com {{codename}}-proposed main restricted universe multiverse
1714-
1715- # or even more uncommon examples like local or NFS mounted repos,
1716- # eventually whatever is compatible with sources.list syntax
1717- deb file:/home/apt/debian unstable main contrib non-free
1718-
1719-# 'source' entries in apt-sources that match this python regex
1720-# expression will be passed to add-apt-repository
1721-add_apt_repo_match: '^[\w-]+:\w'
1722-
1723-# 'apt_sources' is a dictionary
1724-# The key is the filename and will be prepended by /etc/apt/sources.list.d/ if
1725-# it doesn't start with a '/'.
1726-# There are certain cases - where no content is written into a source.list file
1727-# where the filename will be ignored - yet it can still be used as index for
1728-# merging.
1729-# The value it maps to is a dictionary with the following optional entries:
1730-# source: a sources.list entry (some variable replacements apply)
1731-# keyid: providing a key to import via shortid or fingerprint
1732-# key: providing a raw PGP key
1733-# keyserver: keyserver to fetch keys from, default is keyserver.ubuntu.com
1734-# filename: for compatibility with the older format (now the key to this
1735-# dictionary is the filename). If specified this overwrites the
1736-# filename given as key.
1737-
1738-# the new "filename: {specification-dictionary}, filename2: ..." format allows
1739-# better merging between multiple input files than a list like:
1740-# cloud-config1
1741-# sources:
1742-# s1: {'key': 'key1', 'source': 'source1'}
1743-# cloud-config2
1744-# sources:
1745-# s2: {'key': 'key2'}
1746-# s1: {filename: 'foo'}
1747-# this would be merged to
1748-#sources:
1749-# s1:
1750-# filename: foo
1751-# key: key1
1752-# source: source1
1753-# s2:
1754-# key: key2
1755-# Be aware that this style of merging is not the default (for backward
1756-# compatibility reasons). You should specify the following merge_how to get
1757-# this more complete and modern merging behaviour:
1758-# merge_how: "list()+dict()+str()"
1759-# This would then also be equivalent to the config merging used in curtin
1760-# (https://launchpad.net/curtin).
1761-
1762-# for more details see below in the various examples
1763-
1764-apt_sources:
1765- byobu-ppa.list:
1766- source: "deb http://ppa.launchpad.net/byobu/ppa/ubuntu karmic main"
1767- keyid: F430BBA5 # GPG key ID published on a key server
1768- # adding a source.list line, importing a gpg key for a given key id and
1769- # storing it in the file /etc/apt/sources.list.d/byobu-ppa.list
1770-
1771- # PPA shortcut:
1772- # * Setup correct apt sources.list line
1773- # * Import the signing key from LP
1774- #
1775- # See https://help.launchpad.net/Packaging/PPA for more information
1776- # this requires 'add-apt-repository'
1777- # due to that the filename key is ignored in this case
1778- ignored1:
1779- source: "ppa:smoser/ppa" # Quote the string
1780-
1781- # Custom apt repository:
1782- # * all that is required is 'source'
1783- # * Creates a file in /etc/apt/sources.list.d/ for the sources list entry
1784- # * [optional] Import the apt signing key from the keyserver
1785- # * Defaults:
1786- # + keyserver: keyserver.ubuntu.com
1787- #
1788- # See sources.list man page for more information about the format
1789- my-repo.list:
1790- source: deb http://archive.ubuntu.com/ubuntu karmic-backports main universe multiverse restricted
1791-
1792- # sources can use $MIRROR and $RELEASE and they will be replaced
1793- # with the local mirror for this cloud, and the running release
1794- # the entry below would be possibly turned into:
1795- # source: deb http://us-east-1.ec2.archive.ubuntu.com/ubuntu natty multiverse
1796- my-repo.list:
1797- source: deb $MIRROR $RELEASE multiverse
1798-
1799- # this would have the same end effect as 'ppa:byobu/ppa'
1800- my-repo.list:
1801- source: "deb http://ppa.launchpad.net/byobu/ppa/ubuntu karmic main"
1802- keyid: F430BBA5 # GPG key ID published on a key server
1803- filename: byobu-ppa.list
1804-
1805- # this would only import the key without adding a ppa or other source spec
1806- # since this doesn't generate a source.list file the filename key is ignored
1807- ignored2:
1808- keyid: F430BBA5 # GPG key ID published on a key server
1809-
1810- # In general keyid's can also be specified via their long fingerprints
1811- # since this doesn't generate a source.list file the filename key is ignored
1812- ignored3:
1813- keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
1814-
1815- # Custom apt repository:
1816- # * The apt signing key can also be specified
1817- # by providing a pgp public key block
1818- # * Providing the PGP key here is the most robust method for
1819- # specifying a key, as it removes dependency on a remote key server
1820- my-repo.list:
1821- source: deb http://ppa.launchpad.net/alestic/ppa/ubuntu karmic main
1822- key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
1823- -----BEGIN PGP PUBLIC KEY BLOCK-----
1824- Version: SKS 1.0.10
1825-
1826- mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
1827- qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
1828- 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
1829- IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
1830- 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
1831- t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
1832- uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
1833- =Y2oI
1834- -----END PGP PUBLIC KEY BLOCK-----
1835-
1836- # Custom gpg key:
1837- # * As with keyid, a key may also be specified without a related source.
1838- # * all other facts mentioned above still apply
1839- # since this doesn't generate a source.list file the filename key is ignored
1840- ignored4:
1841- key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
1842- -----BEGIN PGP PUBLIC KEY BLOCK-----
1843- Version: SKS 1.0.10
1844-
1845- mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
1846- qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
1847- 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
1848- IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
1849- 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
1850- t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
1851- uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
1852- =Y2oI
1853- -----END PGP PUBLIC KEY BLOCK-----
1854-
1855-
1856-## apt config via system_info:
1857-# under the 'system_info', you can further customize cloud-init's interaction
1858-# with apt.
1859-# system_info:
1860-# apt_get_command: [command, argument, argument]
1861-# apt_get_upgrade_subcommand: dist-upgrade
1862-#
1863-# apt_get_command:
1864-# To specify a different 'apt-get' command, set 'apt_get_command'.
1865-# This must be a list, and the subcommand (update, upgrade) is appended to it.
1866-# default is:
1867-# ['apt-get', '--option=Dpkg::Options::=--force-confold',
1868-# '--option=Dpkg::options::=--force-unsafe-io', '--assume-yes', '--quiet']
1869-#
1870-# apt_get_upgrade_subcommand:
1871-# Specify a different subcommand for 'upgrade. The default is 'dist-upgrade'.
1872-# This is the subcommand that is invoked if package_upgrade is set to true above.
1873-#
1874-# apt_get_wrapper:
1875-# command: eatmydata
1876-# enabled: [True, False, "auto"]
1877-#
1878-
1879-# Install additional packages on first boot
1880-#
1881-# Default: none
1882-#
1883-# if packages are specified, this apt_update will be set to true
1884-#
1885+# For 'apt' specific config, see cloud-config-apt.txt
1886 packages:
1887 - pwgen
1888 - pastebinit
1889diff --git a/tests/configs/sample1.yaml b/tests/configs/sample1.yaml
1890index 6231f29..ae935cc 100644
1891--- a/tests/configs/sample1.yaml
1892+++ b/tests/configs/sample1.yaml
1893@@ -3,9 +3,6 @@
1894 #apt_upgrade: true
1895 packages: [ bzr, pastebinit, ubuntu-dev-tools, ccache, bzr-builddeb, vim-nox, git-core, lftp ]
1896
1897-#apt_sources:
1898-# - source: ppa:smoser/ppa
1899-
1900 #disable_root: False
1901
1902 # mounts:
1903diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py
1904index 96fa081..24ad115 100644
1905--- a/tests/unittests/test_distros/test_generic.py
1906+++ b/tests/unittests/test_distros/test_generic.py
1907@@ -226,8 +226,5 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
1908 os.symlink('/', '/run/systemd/system')
1909 self.assertFalse(d.uses_systemd())
1910
1911-# def _get_package_mirror_info(mirror_info, availability_zone=None,
1912-# mirror_filter=util.search_for_mirror):
1913-
1914
1915 # vi: ts=4 expandtab
1916diff --git a/tests/unittests/test_handler/test_handler_apt_conf_v1.py b/tests/unittests/test_handler/test_handler_apt_conf_v1.py
1917new file mode 100644
1918index 0000000..95fd1da
1919--- /dev/null
1920+++ b/tests/unittests/test_handler/test_handler_apt_conf_v1.py
1921@@ -0,0 +1,109 @@
1922+from cloudinit.config import cc_apt_configure
1923+from cloudinit import util
1924+
1925+from ..helpers import TestCase
1926+
1927+import os
1928+import re
1929+import shutil
1930+import tempfile
1931+
1932+
1933+def load_tfile_or_url(*args, **kwargs):
1934+ return(util.decode_binary(util.read_file_or_url(*args, **kwargs).contents))
1935+
1936+
1937+class TestAptProxyConfig(TestCase):
1938+ def setUp(self):
1939+ super(TestAptProxyConfig, self).setUp()
1940+ self.tmp = tempfile.mkdtemp()
1941+ self.addCleanup(shutil.rmtree, self.tmp)
1942+ self.pfile = os.path.join(self.tmp, "proxy.cfg")
1943+ self.cfile = os.path.join(self.tmp, "config.cfg")
1944+
1945+ def _search_apt_config(self, contents, ptype, value):
1946+ return re.search(
1947+ r"acquire::%s::proxy\s+[\"']%s[\"'];\n" % (ptype, value),
1948+ contents, flags=re.IGNORECASE)
1949+
1950+ def test_apt_proxy_written(self):
1951+ cfg = {'proxy': 'myproxy'}
1952+ cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
1953+
1954+ self.assertTrue(os.path.isfile(self.pfile))
1955+ self.assertFalse(os.path.isfile(self.cfile))
1956+
1957+ contents = load_tfile_or_url(self.pfile)
1958+ self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
1959+
1960+ def test_apt_http_proxy_written(self):
1961+ cfg = {'http_proxy': 'myproxy'}
1962+ cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
1963+
1964+ self.assertTrue(os.path.isfile(self.pfile))
1965+ self.assertFalse(os.path.isfile(self.cfile))
1966+
1967+ contents = load_tfile_or_url(self.pfile)
1968+ self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
1969+
1970+ def test_apt_all_proxy_written(self):
1971+ cfg = {'http_proxy': 'myproxy_http_proxy',
1972+ 'https_proxy': 'myproxy_https_proxy',
1973+ 'ftp_proxy': 'myproxy_ftp_proxy'}
1974+
1975+ values = {'http': cfg['http_proxy'],
1976+ 'https': cfg['https_proxy'],
1977+ 'ftp': cfg['ftp_proxy'],
1978+ }
1979+
1980+ cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
1981+
1982+ self.assertTrue(os.path.isfile(self.pfile))
1983+ self.assertFalse(os.path.isfile(self.cfile))
1984+
1985+ contents = load_tfile_or_url(self.pfile)
1986+
1987+ for ptype, pval in values.items():
1988+ self.assertTrue(self._search_apt_config(contents, ptype, pval))
1989+
1990+ def test_proxy_deleted(self):
1991+ util.write_file(self.cfile, "content doesnt matter")
1992+ cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
1993+ self.assertFalse(os.path.isfile(self.pfile))
1994+ self.assertFalse(os.path.isfile(self.cfile))
1995+
1996+ def test_proxy_replaced(self):
1997+ util.write_file(self.cfile, "content doesnt matter")
1998+ cc_apt_configure.apply_apt_config({'proxy': "foo"},
1999+ self.pfile, self.cfile)
2000+ self.assertTrue(os.path.isfile(self.pfile))
2001+ contents = load_tfile_or_url(self.pfile)
2002+ self.assertTrue(self._search_apt_config(contents, "http", "foo"))
2003+
2004+ def test_config_written(self):
2005+ payload = 'this is my apt config'
2006+ cfg = {'conf': payload}
2007+
2008+ cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
2009+
2010+ self.assertTrue(os.path.isfile(self.cfile))
2011+ self.assertFalse(os.path.isfile(self.pfile))
2012+
2013+ self.assertEqual(load_tfile_or_url(self.cfile), payload)
2014+
2015+ def test_config_replaced(self):
2016+ util.write_file(self.pfile, "content doesnt matter")
2017+ cc_apt_configure.apply_apt_config({'conf': "foo"},
2018+ self.pfile, self.cfile)
2019+ self.assertTrue(os.path.isfile(self.cfile))
2020+ self.assertEqual(load_tfile_or_url(self.cfile), "foo")
2021+
2022+ def test_config_deleted(self):
2023+ # if no 'conf' is provided, delete any previously written file
2024+ util.write_file(self.pfile, "content doesnt matter")
2025+ cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
2026+ self.assertFalse(os.path.isfile(self.pfile))
2027+ self.assertFalse(os.path.isfile(self.cfile))
2028+
2029+
2030+# vi: ts=4 expandtab
2031diff --git a/tests/unittests/test_handler/test_handler_apt_configure.py b/tests/unittests/test_handler/test_handler_apt_configure.py
2032deleted file mode 100644
2033index d1dca2c..0000000
2034--- a/tests/unittests/test_handler/test_handler_apt_configure.py
2035+++ /dev/null
2036@@ -1,109 +0,0 @@
2037-from cloudinit.config import cc_apt_configure
2038-from cloudinit import util
2039-
2040-from ..helpers import TestCase
2041-
2042-import os
2043-import re
2044-import shutil
2045-import tempfile
2046-
2047-
2048-def load_tfile_or_url(*args, **kwargs):
2049- return(util.decode_binary(util.read_file_or_url(*args, **kwargs).contents))
2050-
2051-
2052-class TestAptProxyConfig(TestCase):
2053- def setUp(self):
2054- super(TestAptProxyConfig, self).setUp()
2055- self.tmp = tempfile.mkdtemp()
2056- self.addCleanup(shutil.rmtree, self.tmp)
2057- self.pfile = os.path.join(self.tmp, "proxy.cfg")
2058- self.cfile = os.path.join(self.tmp, "config.cfg")
2059-
2060- def _search_apt_config(self, contents, ptype, value):
2061- return re.search(
2062- r"acquire::%s::proxy\s+[\"']%s[\"'];\n" % (ptype, value),
2063- contents, flags=re.IGNORECASE)
2064-
2065- def test_apt_proxy_written(self):
2066- cfg = {'apt_proxy': 'myproxy'}
2067- cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
2068-
2069- self.assertTrue(os.path.isfile(self.pfile))
2070- self.assertFalse(os.path.isfile(self.cfile))
2071-
2072- contents = load_tfile_or_url(self.pfile)
2073- self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
2074-
2075- def test_apt_http_proxy_written(self):
2076- cfg = {'apt_http_proxy': 'myproxy'}
2077- cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
2078-
2079- self.assertTrue(os.path.isfile(self.pfile))
2080- self.assertFalse(os.path.isfile(self.cfile))
2081-
2082- contents = load_tfile_or_url(self.pfile)
2083- self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
2084-
2085- def test_apt_all_proxy_written(self):
2086- cfg = {'apt_http_proxy': 'myproxy_http_proxy',
2087- 'apt_https_proxy': 'myproxy_https_proxy',
2088- 'apt_ftp_proxy': 'myproxy_ftp_proxy'}
2089-
2090- values = {'http': cfg['apt_http_proxy'],
2091- 'https': cfg['apt_https_proxy'],
2092- 'ftp': cfg['apt_ftp_proxy'],
2093- }
2094-
2095- cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
2096-
2097- self.assertTrue(os.path.isfile(self.pfile))
2098- self.assertFalse(os.path.isfile(self.cfile))
2099-
2100- contents = load_tfile_or_url(self.pfile)
2101-
2102- for ptype, pval in values.items():
2103- self.assertTrue(self._search_apt_config(contents, ptype, pval))
2104-
2105- def test_proxy_deleted(self):
2106- util.write_file(self.cfile, "content doesnt matter")
2107- cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
2108- self.assertFalse(os.path.isfile(self.pfile))
2109- self.assertFalse(os.path.isfile(self.cfile))
2110-
2111- def test_proxy_replaced(self):
2112- util.write_file(self.cfile, "content doesnt matter")
2113- cc_apt_configure.apply_apt_config({'apt_proxy': "foo"},
2114- self.pfile, self.cfile)
2115- self.assertTrue(os.path.isfile(self.pfile))
2116- contents = load_tfile_or_url(self.pfile)
2117- self.assertTrue(self._search_apt_config(contents, "http", "foo"))
2118-
2119- def test_config_written(self):
2120- payload = 'this is my apt config'
2121- cfg = {'apt_config': payload}
2122-
2123- cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
2124-
2125- self.assertTrue(os.path.isfile(self.cfile))
2126- self.assertFalse(os.path.isfile(self.pfile))
2127-
2128- self.assertEqual(load_tfile_or_url(self.cfile), payload)
2129-
2130- def test_config_replaced(self):
2131- util.write_file(self.pfile, "content doesnt matter")
2132- cc_apt_configure.apply_apt_config({'apt_config': "foo"},
2133- self.pfile, self.cfile)
2134- self.assertTrue(os.path.isfile(self.cfile))
2135- self.assertEqual(load_tfile_or_url(self.cfile), "foo")
2136-
2137- def test_config_deleted(self):
2138- # if no 'apt_config' is provided, delete any previously written file
2139- util.write_file(self.pfile, "content doesnt matter")
2140- cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
2141- self.assertFalse(os.path.isfile(self.pfile))
2142- self.assertFalse(os.path.isfile(self.cfile))
2143-
2144-
2145-# vi: ts=4 expandtab
2146diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list.py b/tests/unittests/test_handler/test_handler_apt_configure_sources_list.py
2147deleted file mode 100644
2148index acde086..0000000
2149--- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list.py
2150+++ /dev/null
2151@@ -1,180 +0,0 @@
2152-""" test_handler_apt_configure_sources_list
2153-Test templating of sources list
2154-"""
2155-import logging
2156-import os
2157-import shutil
2158-import tempfile
2159-
2160-try:
2161- from unittest import mock
2162-except ImportError:
2163- import mock
2164-
2165-from cloudinit import cloud
2166-from cloudinit import distros
2167-from cloudinit import helpers
2168-from cloudinit import templater
2169-from cloudinit import util
2170-
2171-from cloudinit.config import cc_apt_configure
2172-from cloudinit.sources import DataSourceNone
2173-
2174-from cloudinit.distros.debian import Distro
2175-
2176-from .. import helpers as t_help
2177-
2178-LOG = logging.getLogger(__name__)
2179-
2180-YAML_TEXT_CUSTOM_SL = """
2181-apt_mirror: http://archive.ubuntu.com/ubuntu/
2182-apt_custom_sources_list: |
2183- ## template:jinja
2184- ## Note, this file is written by cloud-init on first boot of an instance
2185- ## modifications made here will not survive a re-bundle.
2186- ## if you wish to make changes you can:
2187- ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
2188- ## or do the same in user-data
2189- ## b.) add sources in /etc/apt/sources.list.d
2190- ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
2191-
2192- # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2193- # newer versions of the distribution.
2194- deb {{mirror}} {{codename}} main restricted
2195- deb-src {{mirror}} {{codename}} main restricted
2196- # FIND_SOMETHING_SPECIAL
2197-"""
2198-
2199-EXPECTED_CONVERTED_CONTENT = (
2200- """## Note, this file is written by cloud-init on first boot of an instance
2201-## modifications made here will not survive a re-bundle.
2202-## if you wish to make changes you can:
2203-## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
2204-## or do the same in user-data
2205-## b.) add sources in /etc/apt/sources.list.d
2206-## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
2207-
2208-# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2209-# newer versions of the distribution.
2210-deb http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
2211-deb-src http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
2212-# FIND_SOMETHING_SPECIAL
2213-""")
2214-
2215-
2216-def load_tfile_or_url(*args, **kwargs):
2217- """load_tfile_or_url
2218- load file and return content after decoding
2219- """
2220- return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
2221-
2222-
2223-class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
2224- """TestAptSourceConfigSourceList
2225- Main Class to test sources list rendering
2226- """
2227- def setUp(self):
2228- super(TestAptSourceConfigSourceList, self).setUp()
2229- self.subp = util.subp
2230- self.new_root = tempfile.mkdtemp()
2231- self.addCleanup(shutil.rmtree, self.new_root)
2232-
2233- def _get_cloud(self, distro, metadata=None):
2234- self.patchUtils(self.new_root)
2235- paths = helpers.Paths({})
2236- cls = distros.fetch(distro)
2237- mydist = cls(distro, {}, paths)
2238- myds = DataSourceNone.DataSourceNone({}, mydist, paths)
2239- if metadata:
2240- myds.metadata.update(metadata)
2241- return cloud.Cloud(myds, paths, {}, mydist, None)
2242-
2243- def apt_source_list(self, distro, mirror, mirrorcheck=None):
2244- """apt_source_list
2245- Test rendering of a source.list from template for a given distro
2246- """
2247- if mirrorcheck is None:
2248- mirrorcheck = mirror
2249-
2250- if isinstance(mirror, list):
2251- cfg = {'apt_mirror_search': mirror}
2252- else:
2253- cfg = {'apt_mirror': mirror}
2254- mycloud = self._get_cloud(distro)
2255-
2256- with mock.patch.object(templater, 'render_to_file') as mocktmpl:
2257- with mock.patch.object(os.path, 'isfile',
2258- return_value=True) as mockisfile:
2259- with mock.patch.object(util, 'rename'):
2260- cc_apt_configure.handle("notimportant", cfg, mycloud,
2261- LOG, None)
2262-
2263- mockisfile.assert_any_call(
2264- ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
2265- mocktmpl.assert_called_once_with(
2266- ('/etc/cloud/templates/sources.list.%s.tmpl' % distro),
2267- '/etc/apt/sources.list',
2268- {'codename': '', 'primary': mirrorcheck, 'mirror': mirrorcheck})
2269-
2270- def test_apt_source_list_debian(self):
2271- """Test rendering of a source.list from template for debian"""
2272- self.apt_source_list('debian', 'http://httpredir.debian.org/debian')
2273-
2274- def test_apt_source_list_ubuntu(self):
2275- """Test rendering of a source.list from template for ubuntu"""
2276- self.apt_source_list('ubuntu', 'http://archive.ubuntu.com/ubuntu/')
2277-
2278- @staticmethod
2279- def myresolve(name):
2280- """Fake util.is_resolvable for mirrorfail tests"""
2281- if name == "does.not.exist":
2282- print("Faking FAIL for '%s'" % name)
2283- return False
2284- else:
2285- print("Faking SUCCESS for '%s'" % name)
2286- return True
2287-
2288- def test_apt_srcl_debian_mirrorfail(self):
2289- """Test rendering of a source.list from template for debian"""
2290- with mock.patch.object(util, 'is_resolvable',
2291- side_effect=self.myresolve) as mockresolve:
2292- self.apt_source_list('debian',
2293- ['http://does.not.exist',
2294- 'http://httpredir.debian.org/debian'],
2295- 'http://httpredir.debian.org/debian')
2296- mockresolve.assert_any_call("does.not.exist")
2297- mockresolve.assert_any_call("httpredir.debian.org")
2298-
2299- def test_apt_srcl_ubuntu_mirrorfail(self):
2300- """Test rendering of a source.list from template for ubuntu"""
2301- with mock.patch.object(util, 'is_resolvable',
2302- side_effect=self.myresolve) as mockresolve:
2303- self.apt_source_list('ubuntu',
2304- ['http://does.not.exist',
2305- 'http://archive.ubuntu.com/ubuntu/'],
2306- 'http://archive.ubuntu.com/ubuntu/')
2307- mockresolve.assert_any_call("does.not.exist")
2308- mockresolve.assert_any_call("archive.ubuntu.com")
2309-
2310- def test_apt_srcl_custom(self):
2311- """Test rendering from a custom source.list template"""
2312- cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
2313- mycloud = self._get_cloud('ubuntu')
2314-
2315- # the second mock restores the original subp
2316- with mock.patch.object(util, 'write_file') as mockwrite:
2317- with mock.patch.object(util, 'subp', self.subp):
2318- with mock.patch.object(cc_apt_configure, 'get_release',
2319- return_value='fakerelease'):
2320- with mock.patch.object(Distro, 'get_primary_arch',
2321- return_value='amd64'):
2322- cc_apt_configure.handle("notimportant", cfg, mycloud,
2323- LOG, None)
2324-
2325- mockwrite.assert_called_once_with(
2326- '/etc/apt/sources.list',
2327- EXPECTED_CONVERTED_CONTENT,
2328- mode=420)
2329-
2330-
2331-# vi: ts=4 expandtab
2332diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
2333new file mode 100644
2334index 0000000..f441186
2335--- /dev/null
2336+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
2337@@ -0,0 +1,200 @@
2338+""" test_handler_apt_configure_sources_list
2339+Test templating of sources list
2340+"""
2341+import logging
2342+import os
2343+import shutil
2344+import tempfile
2345+
2346+try:
2347+ from unittest import mock
2348+except ImportError:
2349+ import mock
2350+
2351+from cloudinit import cloud
2352+from cloudinit import distros
2353+from cloudinit import helpers
2354+from cloudinit import templater
2355+from cloudinit import util
2356+
2357+from cloudinit.config import cc_apt_configure
2358+from cloudinit.sources import DataSourceNone
2359+
2360+from cloudinit.distros.debian import Distro
2361+
2362+from .. import helpers as t_help
2363+
2364+LOG = logging.getLogger(__name__)
2365+
2366+YAML_TEXT_CUSTOM_SL = """
2367+apt_mirror: http://archive.ubuntu.com/ubuntu/
2368+apt_custom_sources_list: |
2369+ ## template:jinja
2370+ ## Note, this file is written by cloud-init on first boot of an instance
2371+ ## modifications made here will not survive a re-bundle.
2372+ ## if you wish to make changes you can:
2373+ ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
2374+ ## or do the same in user-data
2375+ ## b.) add sources in /etc/apt/sources.list.d
2376+ ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
2377+
2378+ # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2379+ # newer versions of the distribution.
2380+ deb {{mirror}} {{codename}} main restricted
2381+ deb-src {{mirror}} {{codename}} main restricted
2382+ # FIND_SOMETHING_SPECIAL
2383+"""
2384+
2385+EXPECTED_CONVERTED_CONTENT = (
2386+ """## Note, this file is written by cloud-init on first boot of an instance
2387+## modifications made here will not survive a re-bundle.
2388+## if you wish to make changes you can:
2389+## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
2390+## or do the same in user-data
2391+## b.) add sources in /etc/apt/sources.list.d
2392+## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
2393+
2394+# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2395+# newer versions of the distribution.
2396+deb http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
2397+deb-src http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
2398+# FIND_SOMETHING_SPECIAL
2399+""")
2400+
2401+
2402+def load_tfile_or_url(*args, **kwargs):
2403+ """load_tfile_or_url
2404+ load file and return content after decoding
2405+ """
2406+ return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
2407+
2408+
2409+class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
2410+ """TestAptSourceConfigSourceList
2411+ Main Class to test sources list rendering
2412+ """
2413+ def setUp(self):
2414+ super(TestAptSourceConfigSourceList, self).setUp()
2415+ self.subp = util.subp
2416+ self.new_root = tempfile.mkdtemp()
2417+ self.addCleanup(shutil.rmtree, self.new_root)
2418+
2419+ rpatcher = mock.patch("cloudinit.util.lsb_release")
2420+ get_rel = rpatcher.start()
2421+ get_rel.return_value = {'codename': "fakerelease"}
2422+ self.addCleanup(rpatcher.stop)
2423+ apatcher = mock.patch("cloudinit.util.get_architecture")
2424+ get_arch = apatcher.start()
2425+ get_arch.return_value = 'amd64'
2426+ self.addCleanup(apatcher.stop)
2427+
2428+ def _get_cloud(self, distro, metadata=None):
2429+ self.patchUtils(self.new_root)
2430+ paths = helpers.Paths({})
2431+ cls = distros.fetch(distro)
2432+ mydist = cls(distro, {}, paths)
2433+ myds = DataSourceNone.DataSourceNone({}, mydist, paths)
2434+ if metadata:
2435+ myds.metadata.update(metadata)
2436+ return cloud.Cloud(myds, paths, {}, mydist, None)
2437+
2438+ def apt_source_list(self, distro, mirror, mirrorcheck=None):
2439+ """apt_source_list
2440+ Test rendering of a source.list from template for a given distro
2441+ """
2442+ if mirrorcheck is None:
2443+ mirrorcheck = mirror
2444+
2445+ if isinstance(mirror, list):
2446+ cfg = {'apt_mirror_search': mirror}
2447+ else:
2448+ cfg = {'apt_mirror': mirror}
2449+ mycloud = self._get_cloud(distro)
2450+
2451+ with mock.patch.object(util, 'write_file') as mockwf:
2452+ with mock.patch.object(util, 'load_file',
2453+ return_value="faketmpl") as mocklf:
2454+ with mock.patch.object(os.path, 'isfile',
2455+ return_value=True) as mockisfile:
2456+ with mock.patch.object(templater, 'render_string',
2457+ return_value="fake") as mockrnd:
2458+ with mock.patch.object(util, 'rename'):
2459+ cc_apt_configure.handle("test", cfg, mycloud,
2460+ LOG, None)
2461+
2462+ mockisfile.assert_any_call(
2463+ ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
2464+ mocklf.assert_any_call(
2465+ ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
2466+ mockrnd.assert_called_once_with('faketmpl',
2467+ {'RELEASE': 'fakerelease',
2468+ 'PRIMARY': mirrorcheck,
2469+ 'MIRROR': mirrorcheck,
2470+ 'SECURITY': mirrorcheck,
2471+ 'codename': 'fakerelease',
2472+ 'primary': mirrorcheck,
2473+ 'mirror': mirrorcheck,
2474+ 'security': mirrorcheck})
2475+ mockwf.assert_called_once_with('/etc/apt/sources.list', 'fake',
2476+ mode=0o644)
2477+
2478+ def test_apt_v1_source_list_debian(self):
2479+ """Test rendering of a source.list from template for debian"""
2480+ self.apt_source_list('debian', 'http://httpredir.debian.org/debian')
2481+
2482+ def test_apt_v1_source_list_ubuntu(self):
2483+ """Test rendering of a source.list from template for ubuntu"""
2484+ self.apt_source_list('ubuntu', 'http://archive.ubuntu.com/ubuntu/')
2485+
2486+ @staticmethod
2487+ def myresolve(name):
2488+ """Fake util.is_resolvable for mirrorfail tests"""
2489+ if name == "does.not.exist":
2490+ print("Faking FAIL for '%s'" % name)
2491+ return False
2492+ else:
2493+ print("Faking SUCCESS for '%s'" % name)
2494+ return True
2495+
2496+ def test_apt_v1_srcl_debian_mirrorfail(self):
2497+ """Test rendering of a source.list from template for debian"""
2498+ with mock.patch.object(util, 'is_resolvable',
2499+ side_effect=self.myresolve) as mockresolve:
2500+ self.apt_source_list('debian',
2501+ ['http://does.not.exist',
2502+ 'http://httpredir.debian.org/debian'],
2503+ 'http://httpredir.debian.org/debian')
2504+ mockresolve.assert_any_call("does.not.exist")
2505+ mockresolve.assert_any_call("httpredir.debian.org")
2506+
2507+ def test_apt_v1_srcl_ubuntu_mirrorfail(self):
2508+ """Test rendering of a source.list from template for ubuntu"""
2509+ with mock.patch.object(util, 'is_resolvable',
2510+ side_effect=self.myresolve) as mockresolve:
2511+ self.apt_source_list('ubuntu',
2512+ ['http://does.not.exist',
2513+ 'http://archive.ubuntu.com/ubuntu/'],
2514+ 'http://archive.ubuntu.com/ubuntu/')
2515+ mockresolve.assert_any_call("does.not.exist")
2516+ mockresolve.assert_any_call("archive.ubuntu.com")
2517+
2518+ def test_apt_v1_srcl_custom(self):
2519+ """Test rendering from a custom source.list template"""
2520+ cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
2521+ mycloud = self._get_cloud('ubuntu')
2522+
2523+ # the second mock restores the original subp
2524+ with mock.patch.object(util, 'write_file') as mockwrite:
2525+ with mock.patch.object(util, 'subp', self.subp):
2526+ with mock.patch.object(Distro, 'get_primary_arch',
2527+ return_value='amd64'):
2528+ cc_apt_configure.handle("notimportant", cfg, mycloud,
2529+ LOG, None)
2530+
2531+ mockwrite.assert_called_once_with(
2532+ '/etc/apt/sources.list',
2533+ EXPECTED_CONVERTED_CONTENT,
2534+ mode=420)
2535+
2536+
2537+# vi: ts=4 expandtab
2538diff --git a/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
2539new file mode 100644
2540index 0000000..e53b045
2541--- /dev/null
2542+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
2543@@ -0,0 +1,187 @@
2544+""" test_apt_custom_sources_list
2545+Test templating of custom sources list
2546+"""
2547+import logging
2548+import os
2549+import shutil
2550+import tempfile
2551+
2552+try:
2553+ from unittest import mock
2554+except ImportError:
2555+ import mock
2556+from mock import call
2557+
2558+from cloudinit import cloud
2559+from cloudinit import distros
2560+from cloudinit import helpers
2561+from cloudinit import util
2562+
2563+from cloudinit.config import cc_apt_configure
2564+from cloudinit.sources import DataSourceNone
2565+
2566+from cloudinit.distros.debian import Distro
2567+
2568+from .. import helpers as t_help
2569+
2570+LOG = logging.getLogger(__name__)
2571+
2572+TARGET = "/"
2573+
2574+# Input and expected output for the custom template
2575+YAML_TEXT_CUSTOM_SL = """
2576+apt:
2577+ primary:
2578+ - arches: [default]
2579+ uri: http://test.ubuntu.com/ubuntu/
2580+ security:
2581+ - arches: [default]
2582+ uri: http://testsec.ubuntu.com/ubuntu/
2583+ sources_list: |
2584+
2585+ # Note, this file is written by cloud-init at install time. It should not
2586+ # end up on the installed system itself.
2587+ # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2588+ # newer versions of the distribution.
2589+ deb $MIRROR $RELEASE main restricted
2590+ deb-src $MIRROR $RELEASE main restricted
2591+ deb $PRIMARY $RELEASE universe restricted
2592+ deb $SECURITY $RELEASE-security multiverse
2593+ # FIND_SOMETHING_SPECIAL
2594+"""
2595+
2596+EXPECTED_CONVERTED_CONTENT = """
2597+# Note, this file is written by cloud-init at install time. It should not
2598+# end up on the installed system itself.
2599+# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
2600+# newer versions of the distribution.
2601+deb http://test.ubuntu.com/ubuntu/ fakerel main restricted
2602+deb-src http://test.ubuntu.com/ubuntu/ fakerel main restricted
2603+deb http://test.ubuntu.com/ubuntu/ fakerel universe restricted
2604+deb http://testsec.ubuntu.com/ubuntu/ fakerel-security multiverse
2605+# FIND_SOMETHING_SPECIAL
2606+"""
2607+
2608+# mocked to be independent to the unittest system
2609+MOCKED_APT_SRC_LIST = """
2610+deb http://test.ubuntu.com/ubuntu/ notouched main restricted
2611+deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
2612+deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
2613+deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
2614+"""
2615+
2616+EXPECTED_BASE_CONTENT = ("""
2617+deb http://test.ubuntu.com/ubuntu/ notouched main restricted
2618+deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
2619+deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
2620+deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
2621+""")
2622+
2623+EXPECTED_MIRROR_CONTENT = ("""
2624+deb http://test.ubuntu.com/ubuntu/ notouched main restricted
2625+deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
2626+deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
2627+deb http://test.ubuntu.com/ubuntu/ notouched-security main restricted
2628+""")
2629+
2630+EXPECTED_PRIMSEC_CONTENT = ("""
2631+deb http://test.ubuntu.com/ubuntu/ notouched main restricted
2632+deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
2633+deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
2634+deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
2635+""")
2636+
2637+
2638+class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
2639+ """TestAptSourceConfigSourceList - Class to test sources list rendering"""
2640+ def setUp(self):
2641+ super(TestAptSourceConfigSourceList, self).setUp()
2642+ self.subp = util.subp
2643+ self.new_root = tempfile.mkdtemp()
2644+ self.addCleanup(shutil.rmtree, self.new_root)
2645+
2646+ rpatcher = mock.patch("cloudinit.util.lsb_release")
2647+ get_rel = rpatcher.start()
2648+ get_rel.return_value = {'codename': "fakerel"}
2649+ self.addCleanup(rpatcher.stop)
2650+ apatcher = mock.patch("cloudinit.util.get_architecture")
2651+ get_arch = apatcher.start()
2652+ get_arch.return_value = 'amd64'
2653+ self.addCleanup(apatcher.stop)
2654+
2655+ def _get_cloud(self, distro, metadata=None):
2656+ self.patchUtils(self.new_root)
2657+ paths = helpers.Paths({})
2658+ cls = distros.fetch(distro)
2659+ mydist = cls(distro, {}, paths)
2660+ myds = DataSourceNone.DataSourceNone({}, mydist, paths)
2661+ if metadata:
2662+ myds.metadata.update(metadata)
2663+ return cloud.Cloud(myds, paths, {}, mydist, None)
2664+
2665+ def _apt_source_list(self, cfg, expected, distro):
2666+ "_apt_source_list - Test rendering from template (generic)"
2667+
2668+ # entry at top level now, wrap in 'apt' key
2669+ cfg = {'apt': cfg}
2670+ mycloud = self._get_cloud(distro)
2671+ with mock.patch.object(util, 'write_file') as mockwf:
2672+ with mock.patch.object(util, 'load_file',
2673+ return_value=MOCKED_APT_SRC_LIST) as mocklf:
2674+ with mock.patch.object(os.path, 'isfile',
2675+ return_value=True) as mockisfile:
2676+ with mock.patch.object(util, 'rename'):
2677+ cc_apt_configure.handle("test", cfg, mycloud,
2678+ LOG, None)
2679+
2680+ # check if it would have loaded the distro template
2681+ mockisfile.assert_any_call(
2682+ ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
2683+ mocklf.assert_any_call(
2684+ ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
2685+ # check expected content in result
2686+ mockwf.assert_called_once_with('/etc/apt/sources.list', expected,
2687+ mode=0o644)
2688+
2689+ def test_apt_v3_source_list_debian(self):
2690+ """test_apt_v3_source_list_debian - without custom sources or parms"""
2691+ cfg = {}
2692+ self._apt_source_list(cfg, EXPECTED_BASE_CONTENT, 'debian')
2693+
2694+ def test_apt_v3_source_list_ubuntu(self):
2695+ """test_apt_v3_source_list_ubuntu - without custom sources or parms"""
2696+ cfg = {}
2697+ self._apt_source_list(cfg, EXPECTED_BASE_CONTENT, 'ubuntu')
2698+
2699+ def test_apt_v3_source_list_psm(self):
2700+ """test_apt_v3_source_list_psm - Test specifying prim+sec mirrors"""
2701+ pm = 'http://test.ubuntu.com/ubuntu/'
2702+ sm = 'http://testsec.ubuntu.com/ubuntu/'
2703+ cfg = {'preserve_sources_list': False,
2704+ 'primary': [{'arches': ["default"],
2705+ 'uri': pm}],
2706+ 'security': [{'arches': ["default"],
2707+ 'uri': sm}]}
2708+
2709+ self._apt_source_list(cfg, EXPECTED_PRIMSEC_CONTENT, 'ubuntu')
2710+
2711+ def test_apt_v3_srcl_custom(self):
2712+ """test_apt_v3_srcl_custom - Test rendering a custom source template"""
2713+ cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
2714+ mycloud = self._get_cloud('ubuntu')
2715+
2716+ # the second mock restores the original subp
2717+ with mock.patch.object(util, 'write_file') as mockwrite:
2718+ with mock.patch.object(util, 'subp', self.subp):
2719+ with mock.patch.object(Distro, 'get_primary_arch',
2720+ return_value='amd64'):
2721+ cc_apt_configure.handle("notimportant", cfg, mycloud,
2722+ LOG, None)
2723+
2724+ calls = [call('/etc/apt/sources.list',
2725+ EXPECTED_CONVERTED_CONTENT,
2726+ mode=0o644)]
2727+ mockwrite.assert_has_calls(calls)
2728+
2729+
2730+# vi: ts=4 expandtab
2731diff --git a/tests/unittests/test_handler/test_handler_apt_source.py b/tests/unittests/test_handler/test_handler_apt_source.py
2732deleted file mode 100644
2733index 99a4d86..0000000
2734--- a/tests/unittests/test_handler/test_handler_apt_source.py
2735+++ /dev/null
2736@@ -1,516 +0,0 @@
2737-""" test_handler_apt_source
2738-Testing various config variations of the apt_source config
2739-"""
2740-import os
2741-import re
2742-import shutil
2743-import tempfile
2744-
2745-try:
2746- from unittest import mock
2747-except ImportError:
2748- import mock
2749-from mock import call
2750-
2751-from cloudinit.config import cc_apt_configure
2752-from cloudinit import gpg
2753-from cloudinit import util
2754-
2755-from ..helpers import TestCase
2756-
2757-EXPECTEDKEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
2758-Version: GnuPG v1
2759-
2760-mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
2761-NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
2762-8PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
2763-HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
2764-CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
2765-OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
2766-FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
2767-S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
2768-=ACB2
2769------END PGP PUBLIC KEY BLOCK-----"""
2770-
2771-
2772-def load_tfile_or_url(*args, **kwargs):
2773- """load_tfile_or_url
2774- load file and return content after decoding
2775- """
2776- return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
2777-
2778-
2779-class TestAptSourceConfig(TestCase):
2780- """TestAptSourceConfig
2781- Main Class to test apt_source configs
2782- """
2783- release = "fantastic"
2784-
2785- def setUp(self):
2786- super(TestAptSourceConfig, self).setUp()
2787- self.tmp = tempfile.mkdtemp()
2788- self.addCleanup(shutil.rmtree, self.tmp)
2789- self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
2790- self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
2791- self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
2792- self.join = os.path.join
2793- # mock fallback filename into writable tmp dir
2794- self.fallbackfn = os.path.join(self.tmp, "etc/apt/sources.list.d/",
2795- "cloud_config_sources.list")
2796-
2797- patcher = mock.patch("cloudinit.config.cc_apt_configure.get_release")
2798- get_rel = patcher.start()
2799- get_rel.return_value = self.release
2800- self.addCleanup(patcher.stop)
2801-
2802- @staticmethod
2803- def _get_default_params():
2804- """get_default_params
2805- Get the most basic default mrror and release info to be used in tests
2806- """
2807- params = {}
2808- params['RELEASE'] = cc_apt_configure.get_release()
2809- params['MIRROR'] = "http://archive.ubuntu.com/ubuntu"
2810- return params
2811-
2812- def myjoin(self, *args, **kwargs):
2813- """myjoin - redir into writable tmpdir"""
2814- if (args[0] == "/etc/apt/sources.list.d/" and
2815- args[1] == "cloud_config_sources.list" and
2816- len(args) == 2):
2817- return self.join(self.tmp, args[0].lstrip("/"), args[1])
2818- else:
2819- return self.join(*args, **kwargs)
2820-
2821- def apt_src_basic(self, filename, cfg):
2822- """apt_src_basic
2823- Test Fix deb source string, has to overwrite mirror conf in params
2824- """
2825- params = self._get_default_params()
2826-
2827- cc_apt_configure.add_apt_sources(cfg, params)
2828-
2829- self.assertTrue(os.path.isfile(filename))
2830-
2831- contents = load_tfile_or_url(filename)
2832- self.assertTrue(re.search(r"%s %s %s %s\n" %
2833- ("deb", "http://archive.ubuntu.com/ubuntu",
2834- "karmic-backports",
2835- "main universe multiverse restricted"),
2836- contents, flags=re.IGNORECASE))
2837-
2838- def test_apt_src_basic(self):
2839- """Test deb source string, overwrite mirror and filename"""
2840- cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
2841- ' karmic-backports'
2842- ' main universe multiverse restricted'),
2843- 'filename': self.aptlistfile}
2844- self.apt_src_basic(self.aptlistfile, [cfg])
2845-
2846- def test_apt_src_basic_dict(self):
2847- """Test deb source string, overwrite mirror and filename (dict)"""
2848- cfg = {self.aptlistfile: {'source':
2849- ('deb http://archive.ubuntu.com/ubuntu'
2850- ' karmic-backports'
2851- ' main universe multiverse restricted')}}
2852- self.apt_src_basic(self.aptlistfile, cfg)
2853-
2854- def apt_src_basic_tri(self, cfg):
2855- """apt_src_basic_tri
2856- Test Fix three deb source string, has to overwrite mirror conf in
2857- params. Test with filenames provided in config.
2858- generic part to check three files with different content
2859- """
2860- self.apt_src_basic(self.aptlistfile, cfg)
2861-
2862- # extra verify on two extra files of this test
2863- contents = load_tfile_or_url(self.aptlistfile2)
2864- self.assertTrue(re.search(r"%s %s %s %s\n" %
2865- ("deb", "http://archive.ubuntu.com/ubuntu",
2866- "precise-backports",
2867- "main universe multiverse restricted"),
2868- contents, flags=re.IGNORECASE))
2869- contents = load_tfile_or_url(self.aptlistfile3)
2870- self.assertTrue(re.search(r"%s %s %s %s\n" %
2871- ("deb", "http://archive.ubuntu.com/ubuntu",
2872- "lucid-backports",
2873- "main universe multiverse restricted"),
2874- contents, flags=re.IGNORECASE))
2875-
2876- def test_apt_src_basic_tri(self):
2877- """Test Fix three deb source string with filenames"""
2878- cfg1 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
2879- ' karmic-backports'
2880- ' main universe multiverse restricted'),
2881- 'filename': self.aptlistfile}
2882- cfg2 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
2883- ' precise-backports'
2884- ' main universe multiverse restricted'),
2885- 'filename': self.aptlistfile2}
2886- cfg3 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
2887- ' lucid-backports'
2888- ' main universe multiverse restricted'),
2889- 'filename': self.aptlistfile3}
2890- self.apt_src_basic_tri([cfg1, cfg2, cfg3])
2891-
2892- def test_apt_src_basic_dict_tri(self):
2893- """Test Fix three deb source string with filenames (dict)"""
2894- cfg = {self.aptlistfile: {'source':
2895- ('deb http://archive.ubuntu.com/ubuntu'
2896- ' karmic-backports'
2897- ' main universe multiverse restricted')},
2898- self.aptlistfile2: {'source':
2899- ('deb http://archive.ubuntu.com/ubuntu'
2900- ' precise-backports'
2901- ' main universe multiverse restricted')},
2902- self.aptlistfile3: {'source':
2903- ('deb http://archive.ubuntu.com/ubuntu'
2904- ' lucid-backports'
2905- ' main universe multiverse restricted')}}
2906- self.apt_src_basic_tri(cfg)
2907-
2908- def test_apt_src_basic_nofn(self):
2909- """Test Fix three deb source string without filenames (dict)"""
2910- cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
2911- ' karmic-backports'
2912- ' main universe multiverse restricted')}
2913- with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
2914- self.apt_src_basic(self.fallbackfn, [cfg])
2915-
2916- def apt_src_replacement(self, filename, cfg):
2917- """apt_src_replace
2918- Test Autoreplacement of MIRROR and RELEASE in source specs
2919- """
2920- params = self._get_default_params()
2921- cc_apt_configure.add_apt_sources(cfg, params)
2922-
2923- self.assertTrue(os.path.isfile(filename))
2924-
2925- contents = load_tfile_or_url(filename)
2926- self.assertTrue(re.search(r"%s %s %s %s\n" %
2927- ("deb", params['MIRROR'], params['RELEASE'],
2928- "multiverse"),
2929- contents, flags=re.IGNORECASE))
2930-
2931- def test_apt_src_replace(self):
2932- """Test Autoreplacement of MIRROR and RELEASE in source specs"""
2933- cfg = {'source': 'deb $MIRROR $RELEASE multiverse',
2934- 'filename': self.aptlistfile}
2935- self.apt_src_replacement(self.aptlistfile, [cfg])
2936-
2937- def apt_src_replace_tri(self, cfg):
2938- """apt_src_replace_tri
2939- Test three autoreplacements of MIRROR and RELEASE in source specs with
2940- generic part
2941- """
2942- self.apt_src_replacement(self.aptlistfile, cfg)
2943-
2944- # extra verify on two extra files of this test
2945- params = self._get_default_params()
2946- contents = load_tfile_or_url(self.aptlistfile2)
2947- self.assertTrue(re.search(r"%s %s %s %s\n" %
2948- ("deb", params['MIRROR'], params['RELEASE'],
2949- "main"),
2950- contents, flags=re.IGNORECASE))
2951- contents = load_tfile_or_url(self.aptlistfile3)
2952- self.assertTrue(re.search(r"%s %s %s %s\n" %
2953- ("deb", params['MIRROR'], params['RELEASE'],
2954- "universe"),
2955- contents, flags=re.IGNORECASE))
2956-
2957- def test_apt_src_replace_tri(self):
2958- """Test triple Autoreplacement of MIRROR and RELEASE in source specs"""
2959- cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
2960- 'filename': self.aptlistfile}
2961- cfg2 = {'source': 'deb $MIRROR $RELEASE main',
2962- 'filename': self.aptlistfile2}
2963- cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
2964- 'filename': self.aptlistfile3}
2965- self.apt_src_replace_tri([cfg1, cfg2, cfg3])
2966-
2967- def test_apt_src_replace_dict_tri(self):
2968- """Test triple Autoreplacement in source specs (dict)"""
2969- cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
2970- 'notused': {'source': 'deb $MIRROR $RELEASE main',
2971- 'filename': self.aptlistfile2},
2972- self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
2973- self.apt_src_replace_tri(cfg)
2974-
2975- def test_apt_src_replace_nofn(self):
2976- """Test Autoreplacement of MIRROR and RELEASE in source specs nofile"""
2977- cfg = {'source': 'deb $MIRROR $RELEASE multiverse'}
2978- with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
2979- self.apt_src_replacement(self.fallbackfn, [cfg])
2980-
2981- def apt_src_keyid(self, filename, cfg, keynum):
2982- """apt_src_keyid
2983- Test specification of a source + keyid
2984- """
2985- params = self._get_default_params()
2986-
2987- with mock.patch.object(util, 'subp',
2988- return_value=('fakekey 1234', '')) as mockobj:
2989- cc_apt_configure.add_apt_sources(cfg, params)
2990-
2991- # check if it added the right ammount of keys
2992- calls = []
2993- for _ in range(keynum):
2994- calls.append(call(('apt-key', 'add', '-'), 'fakekey 1234'))
2995- mockobj.assert_has_calls(calls, any_order=True)
2996-
2997- self.assertTrue(os.path.isfile(filename))
2998-
2999- contents = load_tfile_or_url(filename)
3000- self.assertTrue(re.search(r"%s %s %s %s\n" %
3001- ("deb",
3002- ('http://ppa.launchpad.net/smoser/'
3003- 'cloud-init-test/ubuntu'),
3004- "xenial", "main"),
3005- contents, flags=re.IGNORECASE))
3006-
3007- def test_apt_src_keyid(self):
3008- """Test specification of a source + keyid with filename being set"""
3009- cfg = {'source': ('deb '
3010- 'http://ppa.launchpad.net/'
3011- 'smoser/cloud-init-test/ubuntu'
3012- ' xenial main'),
3013- 'keyid': "03683F77",
3014- 'filename': self.aptlistfile}
3015- self.apt_src_keyid(self.aptlistfile, [cfg], 1)
3016-
3017- def test_apt_src_keyid_tri(self):
3018- """Test 3x specification of a source + keyid with filename being set"""
3019- cfg1 = {'source': ('deb '
3020- 'http://ppa.launchpad.net/'
3021- 'smoser/cloud-init-test/ubuntu'
3022- ' xenial main'),
3023- 'keyid': "03683F77",
3024- 'filename': self.aptlistfile}
3025- cfg2 = {'source': ('deb '
3026- 'http://ppa.launchpad.net/'
3027- 'smoser/cloud-init-test/ubuntu'
3028- ' xenial universe'),
3029- 'keyid': "03683F77",
3030- 'filename': self.aptlistfile2}
3031- cfg3 = {'source': ('deb '
3032- 'http://ppa.launchpad.net/'
3033- 'smoser/cloud-init-test/ubuntu'
3034- ' xenial multiverse'),
3035- 'keyid': "03683F77",
3036- 'filename': self.aptlistfile3}
3037-
3038- self.apt_src_keyid(self.aptlistfile, [cfg1, cfg2, cfg3], 3)
3039- contents = load_tfile_or_url(self.aptlistfile2)
3040- self.assertTrue(re.search(r"%s %s %s %s\n" %
3041- ("deb",
3042- ('http://ppa.launchpad.net/smoser/'
3043- 'cloud-init-test/ubuntu'),
3044- "xenial", "universe"),
3045- contents, flags=re.IGNORECASE))
3046- contents = load_tfile_or_url(self.aptlistfile3)
3047- self.assertTrue(re.search(r"%s %s %s %s\n" %
3048- ("deb",
3049- ('http://ppa.launchpad.net/smoser/'
3050- 'cloud-init-test/ubuntu'),
3051- "xenial", "multiverse"),
3052- contents, flags=re.IGNORECASE))
3053-
3054- def test_apt_src_keyid_nofn(self):
3055- """Test specification of a source + keyid without filename being set"""
3056- cfg = {'source': ('deb '
3057- 'http://ppa.launchpad.net/'
3058- 'smoser/cloud-init-test/ubuntu'
3059- ' xenial main'),
3060- 'keyid': "03683F77"}
3061- with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3062- self.apt_src_keyid(self.fallbackfn, [cfg], 1)
3063-
3064- def apt_src_key(self, filename, cfg):
3065- """apt_src_key
3066- Test specification of a source + key
3067- """
3068- params = self._get_default_params()
3069-
3070- with mock.patch.object(util, 'subp') as mockobj:
3071- cc_apt_configure.add_apt_sources([cfg], params)
3072-
3073- mockobj.assert_called_with(('apt-key', 'add', '-'), 'fakekey 4321')
3074-
3075- self.assertTrue(os.path.isfile(filename))
3076-
3077- contents = load_tfile_or_url(filename)
3078- self.assertTrue(re.search(r"%s %s %s %s\n" %
3079- ("deb",
3080- ('http://ppa.launchpad.net/smoser/'
3081- 'cloud-init-test/ubuntu'),
3082- "xenial", "main"),
3083- contents, flags=re.IGNORECASE))
3084-
3085- def test_apt_src_key(self):
3086- """Test specification of a source + key with filename being set"""
3087- cfg = {'source': ('deb '
3088- 'http://ppa.launchpad.net/'
3089- 'smoser/cloud-init-test/ubuntu'
3090- ' xenial main'),
3091- 'key': "fakekey 4321",
3092- 'filename': self.aptlistfile}
3093- self.apt_src_key(self.aptlistfile, cfg)
3094-
3095- def test_apt_src_key_nofn(self):
3096- """Test specification of a source + key without filename being set"""
3097- cfg = {'source': ('deb '
3098- 'http://ppa.launchpad.net/'
3099- 'smoser/cloud-init-test/ubuntu'
3100- ' xenial main'),
3101- 'key': "fakekey 4321"}
3102- with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3103- self.apt_src_key(self.fallbackfn, cfg)
3104-
3105- def test_apt_src_keyonly(self):
3106- """Test specifying key without source"""
3107- params = self._get_default_params()
3108- cfg = {'key': "fakekey 4242",
3109- 'filename': self.aptlistfile}
3110-
3111- with mock.patch.object(util, 'subp') as mockobj:
3112- cc_apt_configure.add_apt_sources([cfg], params)
3113-
3114- mockobj.assert_called_once_with(('apt-key', 'add', '-'),
3115- 'fakekey 4242')
3116-
3117- # filename should be ignored on key only
3118- self.assertFalse(os.path.isfile(self.aptlistfile))
3119-
3120- def test_apt_src_keyidonly(self):
3121- """Test specification of a keyid without source"""
3122- params = self._get_default_params()
3123- cfg = {'keyid': "03683F77",
3124- 'filename': self.aptlistfile}
3125-
3126- with mock.patch.object(util, 'subp',
3127- return_value=('fakekey 1212', '')) as mockobj:
3128- cc_apt_configure.add_apt_sources([cfg], params)
3129-
3130- mockobj.assert_called_with(('apt-key', 'add', '-'), 'fakekey 1212')
3131-
3132- # filename should be ignored on key only
3133- self.assertFalse(os.path.isfile(self.aptlistfile))
3134-
3135- def apt_src_keyid_real(self, cfg, expectedkey):
3136- """apt_src_keyid_real
3137- Test specification of a keyid without source including
3138- up to addition of the key (add_apt_key_raw mocked to keep the
3139- environment as is)
3140- """
3141- params = self._get_default_params()
3142-
3143- with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
3144- with mock.patch.object(gpg, 'get_key_by_id',
3145- return_value=expectedkey) as mockgetkey:
3146- cc_apt_configure.add_apt_sources([cfg], params)
3147-
3148- mockgetkey.assert_called_with(cfg['keyid'],
3149- cfg.get('keyserver',
3150- 'keyserver.ubuntu.com'))
3151- mockkey.assert_called_with(expectedkey)
3152-
3153- # filename should be ignored on key only
3154- self.assertFalse(os.path.isfile(self.aptlistfile))
3155-
3156- def test_apt_src_keyid_real(self):
3157- """test_apt_src_keyid_real - Test keyid including key add"""
3158- keyid = "03683F77"
3159- cfg = {'keyid': keyid,
3160- 'filename': self.aptlistfile}
3161-
3162- self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3163-
3164- def test_apt_src_longkeyid_real(self):
3165- """test_apt_src_longkeyid_real - Test long keyid including key add"""
3166- keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
3167- cfg = {'keyid': keyid,
3168- 'filename': self.aptlistfile}
3169-
3170- self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3171-
3172- def test_apt_src_longkeyid_ks_real(self):
3173- """test_apt_src_longkeyid_ks_real - Test long keyid from other ks"""
3174- keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
3175- cfg = {'keyid': keyid,
3176- 'keyserver': 'keys.gnupg.net',
3177- 'filename': self.aptlistfile}
3178-
3179- self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3180-
3181- def test_apt_src_ppa(self):
3182- """Test adding a ppa"""
3183- params = self._get_default_params()
3184- cfg = {'source': 'ppa:smoser/cloud-init-test',
3185- 'filename': self.aptlistfile}
3186-
3187- # default matcher needed for ppa
3188- matcher = re.compile(r'^[\w-]+:\w').search
3189-
3190- with mock.patch.object(util, 'subp') as mockobj:
3191- cc_apt_configure.add_apt_sources([cfg], params,
3192- aa_repo_match=matcher)
3193- mockobj.assert_called_once_with(['add-apt-repository',
3194- 'ppa:smoser/cloud-init-test'])
3195-
3196- # adding ppa should ignore filename (uses add-apt-repository)
3197- self.assertFalse(os.path.isfile(self.aptlistfile))
3198-
3199- def test_apt_src_ppa_tri(self):
3200- """Test adding three ppa's"""
3201- params = self._get_default_params()
3202- cfg1 = {'source': 'ppa:smoser/cloud-init-test',
3203- 'filename': self.aptlistfile}
3204- cfg2 = {'source': 'ppa:smoser/cloud-init-test2',
3205- 'filename': self.aptlistfile2}
3206- cfg3 = {'source': 'ppa:smoser/cloud-init-test3',
3207- 'filename': self.aptlistfile3}
3208-
3209- # default matcher needed for ppa
3210- matcher = re.compile(r'^[\w-]+:\w').search
3211-
3212- with mock.patch.object(util, 'subp') as mockobj:
3213- cc_apt_configure.add_apt_sources([cfg1, cfg2, cfg3], params,
3214- aa_repo_match=matcher)
3215- calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test']),
3216- call(['add-apt-repository', 'ppa:smoser/cloud-init-test2']),
3217- call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'])]
3218- mockobj.assert_has_calls(calls, any_order=True)
3219-
3220- # adding ppa should ignore all filenames (uses add-apt-repository)
3221- self.assertFalse(os.path.isfile(self.aptlistfile))
3222- self.assertFalse(os.path.isfile(self.aptlistfile2))
3223- self.assertFalse(os.path.isfile(self.aptlistfile3))
3224-
3225- def test_convert_to_new_format(self):
3226- """Test the conversion of old to new format"""
3227- cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
3228- 'filename': self.aptlistfile}
3229- cfg2 = {'source': 'deb $MIRROR $RELEASE main',
3230- 'filename': self.aptlistfile2}
3231- cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
3232- 'filename': self.aptlistfile3}
3233- checkcfg = {self.aptlistfile: {'filename': self.aptlistfile,
3234- 'source': 'deb $MIRROR $RELEASE '
3235- 'multiverse'},
3236- self.aptlistfile2: {'filename': self.aptlistfile2,
3237- 'source': 'deb $MIRROR $RELEASE main'},
3238- self.aptlistfile3: {'filename': self.aptlistfile3,
3239- 'source': 'deb $MIRROR $RELEASE '
3240- 'universe'}}
3241-
3242- newcfg = cc_apt_configure.convert_to_new_format([cfg1, cfg2, cfg3])
3243- self.assertEqual(newcfg, checkcfg)
3244-
3245- newcfg2 = cc_apt_configure.convert_to_new_format(newcfg)
3246- self.assertEqual(newcfg2, checkcfg)
3247-
3248- with self.assertRaises(ValueError):
3249- cc_apt_configure.convert_to_new_format(5)
3250-
3251-
3252-# vi: ts=4 expandtab
3253diff --git a/tests/unittests/test_handler/test_handler_apt_source_v1.py b/tests/unittests/test_handler/test_handler_apt_source_v1.py
3254new file mode 100644
3255index 0000000..d96779c
3256--- /dev/null
3257+++ b/tests/unittests/test_handler/test_handler_apt_source_v1.py
3258@@ -0,0 +1,551 @@
3259+""" test_handler_apt_source_v1
3260+Testing various config variations of the apt_source config
3261+This calls all things with v1 format to stress the conversion code on top of
3262+the actually tested code.
3263+"""
3264+import os
3265+import re
3266+import shutil
3267+import tempfile
3268+
3269+try:
3270+ from unittest import mock
3271+except ImportError:
3272+ import mock
3273+from mock import call
3274+
3275+from cloudinit.config import cc_apt_configure
3276+from cloudinit import gpg
3277+from cloudinit import util
3278+
3279+from ..helpers import TestCase
3280+
3281+EXPECTEDKEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
3282+Version: GnuPG v1
3283+
3284+mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
3285+NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
3286+8PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
3287+HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
3288+CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
3289+OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
3290+FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
3291+S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
3292+=ACB2
3293+-----END PGP PUBLIC KEY BLOCK-----"""
3294+
3295+ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
3296+
3297+
3298+def load_tfile_or_url(*args, **kwargs):
3299+ """load_tfile_or_url
3300+ load file and return content after decoding
3301+ """
3302+ return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
3303+
3304+
3305+class FakeDistro(object):
3306+ """Fake Distro helper object"""
3307+ def update_package_sources(self):
3308+ """Fake update_package_sources helper method"""
3309+ return
3310+
3311+
3312+class FakeCloud(object):
3313+ """Fake Cloud helper object"""
3314+ def __init__(self):
3315+ self.distro = FakeDistro()
3316+
3317+
3318+class TestAptSourceConfig(TestCase):
3319+ """TestAptSourceConfig
3320+ Main Class to test apt_source configs
3321+ """
3322+ release = "fantastic"
3323+
3324+ def setUp(self):
3325+ super(TestAptSourceConfig, self).setUp()
3326+ self.tmp = tempfile.mkdtemp()
3327+ self.addCleanup(shutil.rmtree, self.tmp)
3328+ self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
3329+ self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
3330+ self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
3331+ self.join = os.path.join
3332+ self.matcher = re.compile(ADD_APT_REPO_MATCH).search
3333+ # mock fallback filename into writable tmp dir
3334+ self.fallbackfn = os.path.join(self.tmp, "etc/apt/sources.list.d/",
3335+ "cloud_config_sources.list")
3336+
3337+ self.fakecloud = FakeCloud()
3338+
3339+ rpatcher = mock.patch("cloudinit.util.lsb_release")
3340+ get_rel = rpatcher.start()
3341+ get_rel.return_value = {'codename': self.release}
3342+ self.addCleanup(rpatcher.stop)
3343+ apatcher = mock.patch("cloudinit.util.get_architecture")
3344+ get_arch = apatcher.start()
3345+ get_arch.return_value = 'amd64'
3346+ self.addCleanup(apatcher.stop)
3347+
3348+ def _get_default_params(self):
3349+ """get_default_params
3350+ Get the most basic default mrror and release info to be used in tests
3351+ """
3352+ params = {}
3353+ params['RELEASE'] = self.release
3354+ params['MIRROR'] = "http://archive.ubuntu.com/ubuntu"
3355+ return params
3356+
3357+ def wrapv1conf(self, cfg):
3358+ params = self._get_default_params()
3359+ # old v1 list format under old keys, but callabe to main handler
3360+ # disable source.list rendering and set mirror to avoid other code
3361+ return {'apt_preserve_sources_list': True,
3362+ 'apt_mirror': params['MIRROR'],
3363+ 'apt_sources': cfg}
3364+
3365+ def myjoin(self, *args, **kwargs):
3366+ """myjoin - redir into writable tmpdir"""
3367+ if (args[0] == "/etc/apt/sources.list.d/" and
3368+ args[1] == "cloud_config_sources.list" and
3369+ len(args) == 2):
3370+ return self.join(self.tmp, args[0].lstrip("/"), args[1])
3371+ else:
3372+ return self.join(*args, **kwargs)
3373+
3374+ def apt_src_basic(self, filename, cfg):
3375+ """apt_src_basic
3376+ Test Fix deb source string, has to overwrite mirror conf in params
3377+ """
3378+ cfg = self.wrapv1conf(cfg)
3379+
3380+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3381+
3382+ self.assertTrue(os.path.isfile(filename))
3383+
3384+ contents = load_tfile_or_url(filename)
3385+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3386+ ("deb", "http://archive.ubuntu.com/ubuntu",
3387+ "karmic-backports",
3388+ "main universe multiverse restricted"),
3389+ contents, flags=re.IGNORECASE))
3390+
3391+ def test_apt_src_basic(self):
3392+ """Test deb source string, overwrite mirror and filename"""
3393+ cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
3394+ ' karmic-backports'
3395+ ' main universe multiverse restricted'),
3396+ 'filename': self.aptlistfile}
3397+ self.apt_src_basic(self.aptlistfile, [cfg])
3398+
3399+ def test_apt_src_basic_dict(self):
3400+ """Test deb source string, overwrite mirror and filename (dict)"""
3401+ cfg = {self.aptlistfile: {'source':
3402+ ('deb http://archive.ubuntu.com/ubuntu'
3403+ ' karmic-backports'
3404+ ' main universe multiverse restricted')}}
3405+ self.apt_src_basic(self.aptlistfile, cfg)
3406+
3407+ def apt_src_basic_tri(self, cfg):
3408+ """apt_src_basic_tri
3409+ Test Fix three deb source string, has to overwrite mirror conf in
3410+ params. Test with filenames provided in config.
3411+ generic part to check three files with different content
3412+ """
3413+ self.apt_src_basic(self.aptlistfile, cfg)
3414+
3415+ # extra verify on two extra files of this test
3416+ contents = load_tfile_or_url(self.aptlistfile2)
3417+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3418+ ("deb", "http://archive.ubuntu.com/ubuntu",
3419+ "precise-backports",
3420+ "main universe multiverse restricted"),
3421+ contents, flags=re.IGNORECASE))
3422+ contents = load_tfile_or_url(self.aptlistfile3)
3423+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3424+ ("deb", "http://archive.ubuntu.com/ubuntu",
3425+ "lucid-backports",
3426+ "main universe multiverse restricted"),
3427+ contents, flags=re.IGNORECASE))
3428+
3429+ def test_apt_src_basic_tri(self):
3430+ """Test Fix three deb source string with filenames"""
3431+ cfg1 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
3432+ ' karmic-backports'
3433+ ' main universe multiverse restricted'),
3434+ 'filename': self.aptlistfile}
3435+ cfg2 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
3436+ ' precise-backports'
3437+ ' main universe multiverse restricted'),
3438+ 'filename': self.aptlistfile2}
3439+ cfg3 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
3440+ ' lucid-backports'
3441+ ' main universe multiverse restricted'),
3442+ 'filename': self.aptlistfile3}
3443+ self.apt_src_basic_tri([cfg1, cfg2, cfg3])
3444+
3445+ def test_apt_src_basic_dict_tri(self):
3446+ """Test Fix three deb source string with filenames (dict)"""
3447+ cfg = {self.aptlistfile: {'source':
3448+ ('deb http://archive.ubuntu.com/ubuntu'
3449+ ' karmic-backports'
3450+ ' main universe multiverse restricted')},
3451+ self.aptlistfile2: {'source':
3452+ ('deb http://archive.ubuntu.com/ubuntu'
3453+ ' precise-backports'
3454+ ' main universe multiverse restricted')},
3455+ self.aptlistfile3: {'source':
3456+ ('deb http://archive.ubuntu.com/ubuntu'
3457+ ' lucid-backports'
3458+ ' main universe multiverse restricted')}}
3459+ self.apt_src_basic_tri(cfg)
3460+
3461+ def test_apt_src_basic_nofn(self):
3462+ """Test Fix three deb source string without filenames (dict)"""
3463+ cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
3464+ ' karmic-backports'
3465+ ' main universe multiverse restricted')}
3466+ with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3467+ self.apt_src_basic(self.fallbackfn, [cfg])
3468+
3469+ def apt_src_replacement(self, filename, cfg):
3470+ """apt_src_replace
3471+ Test Autoreplacement of MIRROR and RELEASE in source specs
3472+ """
3473+ cfg = self.wrapv1conf(cfg)
3474+ params = self._get_default_params()
3475+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3476+
3477+ self.assertTrue(os.path.isfile(filename))
3478+
3479+ contents = load_tfile_or_url(filename)
3480+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3481+ ("deb", params['MIRROR'], params['RELEASE'],
3482+ "multiverse"),
3483+ contents, flags=re.IGNORECASE))
3484+
3485+ def test_apt_src_replace(self):
3486+ """Test Autoreplacement of MIRROR and RELEASE in source specs"""
3487+ cfg = {'source': 'deb $MIRROR $RELEASE multiverse',
3488+ 'filename': self.aptlistfile}
3489+ self.apt_src_replacement(self.aptlistfile, [cfg])
3490+
3491+ def apt_src_replace_tri(self, cfg):
3492+ """apt_src_replace_tri
3493+ Test three autoreplacements of MIRROR and RELEASE in source specs with
3494+ generic part
3495+ """
3496+ self.apt_src_replacement(self.aptlistfile, cfg)
3497+
3498+ # extra verify on two extra files of this test
3499+ params = self._get_default_params()
3500+ contents = load_tfile_or_url(self.aptlistfile2)
3501+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3502+ ("deb", params['MIRROR'], params['RELEASE'],
3503+ "main"),
3504+ contents, flags=re.IGNORECASE))
3505+ contents = load_tfile_or_url(self.aptlistfile3)
3506+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3507+ ("deb", params['MIRROR'], params['RELEASE'],
3508+ "universe"),
3509+ contents, flags=re.IGNORECASE))
3510+
3511+ def test_apt_src_replace_tri(self):
3512+ """Test triple Autoreplacement of MIRROR and RELEASE in source specs"""
3513+ cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
3514+ 'filename': self.aptlistfile}
3515+ cfg2 = {'source': 'deb $MIRROR $RELEASE main',
3516+ 'filename': self.aptlistfile2}
3517+ cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
3518+ 'filename': self.aptlistfile3}
3519+ self.apt_src_replace_tri([cfg1, cfg2, cfg3])
3520+
3521+ def test_apt_src_replace_dict_tri(self):
3522+ """Test triple Autoreplacement in source specs (dict)"""
3523+ cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
3524+ 'notused': {'source': 'deb $MIRROR $RELEASE main',
3525+ 'filename': self.aptlistfile2},
3526+ self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
3527+ self.apt_src_replace_tri(cfg)
3528+
3529+ def test_apt_src_replace_nofn(self):
3530+ """Test Autoreplacement of MIRROR and RELEASE in source specs nofile"""
3531+ cfg = {'source': 'deb $MIRROR $RELEASE multiverse'}
3532+ with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3533+ self.apt_src_replacement(self.fallbackfn, [cfg])
3534+
3535+ def apt_src_keyid(self, filename, cfg, keynum):
3536+ """apt_src_keyid
3537+ Test specification of a source + keyid
3538+ """
3539+ cfg = self.wrapv1conf(cfg)
3540+
3541+ with mock.patch.object(util, 'subp',
3542+ return_value=('fakekey 1234', '')) as mockobj:
3543+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3544+
3545+ # check if it added the right ammount of keys
3546+ calls = []
3547+ for _ in range(keynum):
3548+ calls.append(call(['apt-key', 'add', '-'],
3549+ data=b'fakekey 1234',
3550+ target=None))
3551+ mockobj.assert_has_calls(calls, any_order=True)
3552+
3553+ self.assertTrue(os.path.isfile(filename))
3554+
3555+ contents = load_tfile_or_url(filename)
3556+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3557+ ("deb",
3558+ ('http://ppa.launchpad.net/smoser/'
3559+ 'cloud-init-test/ubuntu'),
3560+ "xenial", "main"),
3561+ contents, flags=re.IGNORECASE))
3562+
3563+ def test_apt_src_keyid(self):
3564+ """Test specification of a source + keyid with filename being set"""
3565+ cfg = {'source': ('deb '
3566+ 'http://ppa.launchpad.net/'
3567+ 'smoser/cloud-init-test/ubuntu'
3568+ ' xenial main'),
3569+ 'keyid': "03683F77",
3570+ 'filename': self.aptlistfile}
3571+ self.apt_src_keyid(self.aptlistfile, [cfg], 1)
3572+
3573+ def test_apt_src_keyid_tri(self):
3574+ """Test 3x specification of a source + keyid with filename being set"""
3575+ cfg1 = {'source': ('deb '
3576+ 'http://ppa.launchpad.net/'
3577+ 'smoser/cloud-init-test/ubuntu'
3578+ ' xenial main'),
3579+ 'keyid': "03683F77",
3580+ 'filename': self.aptlistfile}
3581+ cfg2 = {'source': ('deb '
3582+ 'http://ppa.launchpad.net/'
3583+ 'smoser/cloud-init-test/ubuntu'
3584+ ' xenial universe'),
3585+ 'keyid': "03683F77",
3586+ 'filename': self.aptlistfile2}
3587+ cfg3 = {'source': ('deb '
3588+ 'http://ppa.launchpad.net/'
3589+ 'smoser/cloud-init-test/ubuntu'
3590+ ' xenial multiverse'),
3591+ 'keyid': "03683F77",
3592+ 'filename': self.aptlistfile3}
3593+
3594+ self.apt_src_keyid(self.aptlistfile, [cfg1, cfg2, cfg3], 3)
3595+ contents = load_tfile_or_url(self.aptlistfile2)
3596+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3597+ ("deb",
3598+ ('http://ppa.launchpad.net/smoser/'
3599+ 'cloud-init-test/ubuntu'),
3600+ "xenial", "universe"),
3601+ contents, flags=re.IGNORECASE))
3602+ contents = load_tfile_or_url(self.aptlistfile3)
3603+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3604+ ("deb",
3605+ ('http://ppa.launchpad.net/smoser/'
3606+ 'cloud-init-test/ubuntu'),
3607+ "xenial", "multiverse"),
3608+ contents, flags=re.IGNORECASE))
3609+
3610+ def test_apt_src_keyid_nofn(self):
3611+ """Test specification of a source + keyid without filename being set"""
3612+ cfg = {'source': ('deb '
3613+ 'http://ppa.launchpad.net/'
3614+ 'smoser/cloud-init-test/ubuntu'
3615+ ' xenial main'),
3616+ 'keyid': "03683F77"}
3617+ with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3618+ self.apt_src_keyid(self.fallbackfn, [cfg], 1)
3619+
3620+ def apt_src_key(self, filename, cfg):
3621+ """apt_src_key
3622+ Test specification of a source + key
3623+ """
3624+ cfg = self.wrapv1conf([cfg])
3625+
3626+ with mock.patch.object(util, 'subp') as mockobj:
3627+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3628+
3629+ mockobj.assert_called_with(['apt-key', 'add', '-'],
3630+ data=b'fakekey 4321', target=None)
3631+
3632+ self.assertTrue(os.path.isfile(filename))
3633+
3634+ contents = load_tfile_or_url(filename)
3635+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3636+ ("deb",
3637+ ('http://ppa.launchpad.net/smoser/'
3638+ 'cloud-init-test/ubuntu'),
3639+ "xenial", "main"),
3640+ contents, flags=re.IGNORECASE))
3641+
3642+ def test_apt_src_key(self):
3643+ """Test specification of a source + key with filename being set"""
3644+ cfg = {'source': ('deb '
3645+ 'http://ppa.launchpad.net/'
3646+ 'smoser/cloud-init-test/ubuntu'
3647+ ' xenial main'),
3648+ 'key': "fakekey 4321",
3649+ 'filename': self.aptlistfile}
3650+ self.apt_src_key(self.aptlistfile, cfg)
3651+
3652+ def test_apt_src_key_nofn(self):
3653+ """Test specification of a source + key without filename being set"""
3654+ cfg = {'source': ('deb '
3655+ 'http://ppa.launchpad.net/'
3656+ 'smoser/cloud-init-test/ubuntu'
3657+ ' xenial main'),
3658+ 'key': "fakekey 4321"}
3659+ with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
3660+ self.apt_src_key(self.fallbackfn, cfg)
3661+
3662+ def test_apt_src_keyonly(self):
3663+ """Test specifying key without source"""
3664+ cfg = {'key': "fakekey 4242",
3665+ 'filename': self.aptlistfile}
3666+ cfg = self.wrapv1conf([cfg])
3667+
3668+ with mock.patch.object(util, 'subp') as mockobj:
3669+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3670+
3671+ mockobj.assert_called_once_with(['apt-key', 'add', '-'],
3672+ data=b'fakekey 4242', target=None)
3673+
3674+ # filename should be ignored on key only
3675+ self.assertFalse(os.path.isfile(self.aptlistfile))
3676+
3677+ def test_apt_src_keyidonly(self):
3678+ """Test specification of a keyid without source"""
3679+ cfg = {'keyid': "03683F77",
3680+ 'filename': self.aptlistfile}
3681+ cfg = self.wrapv1conf([cfg])
3682+
3683+ with mock.patch.object(util, 'subp',
3684+ return_value=('fakekey 1212', '')) as mockobj:
3685+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3686+
3687+ mockobj.assert_called_with(['apt-key', 'add', '-'],
3688+ data=b'fakekey 1212', target=None)
3689+
3690+ # filename should be ignored on key only
3691+ self.assertFalse(os.path.isfile(self.aptlistfile))
3692+
3693+ def apt_src_keyid_real(self, cfg, expectedkey):
3694+ """apt_src_keyid_real
3695+ Test specification of a keyid without source including
3696+ up to addition of the key (add_apt_key_raw mocked to keep the
3697+ environment as is)
3698+ """
3699+ key = cfg['keyid']
3700+ keyserver = cfg.get('keyserver', 'keyserver.ubuntu.com')
3701+ cfg = self.wrapv1conf([cfg])
3702+
3703+ with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
3704+ with mock.patch.object(gpg, 'getkeybyid',
3705+ return_value=expectedkey) as mockgetkey:
3706+ cc_apt_configure.handle("test", cfg, self.fakecloud,
3707+ None, None)
3708+
3709+ mockgetkey.assert_called_with(key, keyserver)
3710+ mockkey.assert_called_with(expectedkey, None)
3711+
3712+ # filename should be ignored on key only
3713+ self.assertFalse(os.path.isfile(self.aptlistfile))
3714+
3715+ def test_apt_src_keyid_real(self):
3716+ """test_apt_src_keyid_real - Test keyid including key add"""
3717+ keyid = "03683F77"
3718+ cfg = {'keyid': keyid,
3719+ 'filename': self.aptlistfile}
3720+
3721+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3722+
3723+ def test_apt_src_longkeyid_real(self):
3724+ """test_apt_src_longkeyid_real - Test long keyid including key add"""
3725+ keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
3726+ cfg = {'keyid': keyid,
3727+ 'filename': self.aptlistfile}
3728+
3729+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3730+
3731+ def test_apt_src_longkeyid_ks_real(self):
3732+ """test_apt_src_longkeyid_ks_real - Test long keyid from other ks"""
3733+ keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
3734+ cfg = {'keyid': keyid,
3735+ 'keyserver': 'keys.gnupg.net',
3736+ 'filename': self.aptlistfile}
3737+
3738+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
3739+
3740+ def test_apt_src_ppa(self):
3741+ """Test adding a ppa"""
3742+ cfg = {'source': 'ppa:smoser/cloud-init-test',
3743+ 'filename': self.aptlistfile}
3744+ cfg = self.wrapv1conf([cfg])
3745+
3746+ with mock.patch.object(util, 'subp') as mockobj:
3747+ cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
3748+ mockobj.assert_called_once_with(['add-apt-repository',
3749+ 'ppa:smoser/cloud-init-test'],
3750+ target=None)
3751+
3752+ # adding ppa should ignore filename (uses add-apt-repository)
3753+ self.assertFalse(os.path.isfile(self.aptlistfile))
3754+
3755+ def test_apt_src_ppa_tri(self):
3756+ """Test adding three ppa's"""
3757+ cfg1 = {'source': 'ppa:smoser/cloud-init-test',
3758+ 'filename': self.aptlistfile}
3759+ cfg2 = {'source': 'ppa:smoser/cloud-init-test2',
3760+ 'filename': self.aptlistfile2}
3761+ cfg3 = {'source': 'ppa:smoser/cloud-init-test3',
3762+ 'filename': self.aptlistfile3}
3763+ cfg = self.wrapv1conf([cfg1, cfg2, cfg3])
3764+
3765+ with mock.patch.object(util, 'subp') as mockobj:
3766+ cc_apt_configure.handle("test", cfg, self.fakecloud,
3767+ None, None)
3768+ calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test'],
3769+ target=None),
3770+ call(['add-apt-repository', 'ppa:smoser/cloud-init-test2'],
3771+ target=None),
3772+ call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'],
3773+ target=None)]
3774+ mockobj.assert_has_calls(calls, any_order=True)
3775+
3776+ # adding ppa should ignore all filenames (uses add-apt-repository)
3777+ self.assertFalse(os.path.isfile(self.aptlistfile))
3778+ self.assertFalse(os.path.isfile(self.aptlistfile2))
3779+ self.assertFalse(os.path.isfile(self.aptlistfile3))
3780+
3781+ def test_convert_to_new_format(self):
3782+ """Test the conversion of old to new format"""
3783+ cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
3784+ 'filename': self.aptlistfile}
3785+ cfg2 = {'source': 'deb $MIRROR $RELEASE main',
3786+ 'filename': self.aptlistfile2}
3787+ cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
3788+ 'filename': self.aptlistfile3}
3789+ checkcfg = {self.aptlistfile: {'filename': self.aptlistfile,
3790+ 'source': 'deb $MIRROR $RELEASE '
3791+ 'multiverse'},
3792+ self.aptlistfile2: {'filename': self.aptlistfile2,
3793+ 'source': 'deb $MIRROR $RELEASE main'},
3794+ self.aptlistfile3: {'filename': self.aptlistfile3,
3795+ 'source': 'deb $MIRROR $RELEASE '
3796+ 'universe'}}
3797+
3798+ newcfg = cc_apt_configure.convert_v1_to_v2_apt_format([cfg1, cfg2,
3799+ cfg3])
3800+ self.assertEqual(newcfg, checkcfg)
3801+
3802+ newcfg2 = cc_apt_configure.convert_v1_to_v2_apt_format(newcfg)
3803+ self.assertEqual(newcfg2, checkcfg)
3804+
3805+ with self.assertRaises(ValueError):
3806+ cc_apt_configure.convert_v1_to_v2_apt_format(5)
3807+
3808+
3809+# vi: ts=4 expandtab
3810diff --git a/tests/unittests/test_handler/test_handler_apt_source_v3.py b/tests/unittests/test_handler/test_handler_apt_source_v3.py
3811new file mode 100644
3812index 0000000..75556b6
3813--- /dev/null
3814+++ b/tests/unittests/test_handler/test_handler_apt_source_v3.py
3815@@ -0,0 +1,1103 @@
3816+"""test_handler_apt_source_v3
3817+Testing various config variations of the apt_source custom config
3818+This tries to call all in the new v3 format and cares about new features
3819+"""
3820+import glob
3821+import os
3822+import re
3823+import shutil
3824+import socket
3825+import tempfile
3826+
3827+from unittest import TestCase
3828+
3829+try:
3830+ from unittest import mock
3831+except ImportError:
3832+ import mock
3833+from mock import call
3834+
3835+from cloudinit import cloud
3836+from cloudinit import distros
3837+from cloudinit import gpg
3838+from cloudinit import helpers
3839+from cloudinit import util
3840+
3841+from cloudinit.config import cc_apt_configure
3842+from cloudinit.sources import DataSourceNone
3843+
3844+from .. import helpers as t_help
3845+
3846+EXPECTEDKEY = u"""-----BEGIN PGP PUBLIC KEY BLOCK-----
3847+Version: GnuPG v1
3848+
3849+mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
3850+NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
3851+8PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
3852+HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
3853+CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
3854+OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
3855+FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
3856+S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
3857+=ACB2
3858+-----END PGP PUBLIC KEY BLOCK-----"""
3859+
3860+ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
3861+
3862+TARGET = None
3863+
3864+
3865+def load_tfile(*args, **kwargs):
3866+ """load_tfile_or_url
3867+ load file and return content after decoding
3868+ """
3869+ return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
3870+
3871+
3872+class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
3873+ """TestAptSourceConfig
3874+ Main Class to test apt configs
3875+ """
3876+ def setUp(self):
3877+ super(TestAptSourceConfig, self).setUp()
3878+ self.tmp = tempfile.mkdtemp()
3879+ self.new_root = tempfile.mkdtemp()
3880+ self.addCleanup(shutil.rmtree, self.tmp)
3881+ self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
3882+ self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
3883+ self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
3884+ self.join = os.path.join
3885+ self.matcher = re.compile(ADD_APT_REPO_MATCH).search
3886+
3887+ @staticmethod
3888+ def _add_apt_sources(*args, **kwargs):
3889+ with mock.patch.object(cc_apt_configure, 'update_packages'):
3890+ cc_apt_configure.add_apt_sources(*args, **kwargs)
3891+
3892+ @staticmethod
3893+ def _get_default_params():
3894+ """get_default_params
3895+ Get the most basic default mrror and release info to be used in tests
3896+ """
3897+ params = {}
3898+ params['RELEASE'] = util.lsb_release()['codename']
3899+ arch = 'amd64'
3900+ params['MIRROR'] = cc_apt_configure.\
3901+ get_default_mirrors(arch)["PRIMARY"]
3902+ return params
3903+
3904+ def _myjoin(self, *args, **kwargs):
3905+ """_myjoin - redir into writable tmpdir"""
3906+ if (args[0] == "/etc/apt/sources.list.d/" and
3907+ args[1] == "cloud_config_sources.list" and
3908+ len(args) == 2):
3909+ return self.join(self.tmp, args[0].lstrip("/"), args[1])
3910+ else:
3911+ return self.join(*args, **kwargs)
3912+
3913+ def _get_cloud(self, distro, metadata=None):
3914+ self.patchUtils(self.new_root)
3915+ paths = helpers.Paths({})
3916+ cls = distros.fetch(distro)
3917+ mydist = cls(distro, {}, paths)
3918+ myds = DataSourceNone.DataSourceNone({}, mydist, paths)
3919+ if metadata:
3920+ myds.metadata.update(metadata)
3921+ return cloud.Cloud(myds, paths, {}, mydist, None)
3922+
3923+ def _apt_src_basic(self, filename, cfg):
3924+ """_apt_src_basic
3925+ Test Fix deb source string, has to overwrite mirror conf in params
3926+ """
3927+ params = self._get_default_params()
3928+
3929+ self._add_apt_sources(cfg, TARGET, template_params=params,
3930+ aa_repo_match=self.matcher)
3931+
3932+ self.assertTrue(os.path.isfile(filename))
3933+
3934+ contents = load_tfile(filename)
3935+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3936+ ("deb", "http://test.ubuntu.com/ubuntu",
3937+ "karmic-backports",
3938+ "main universe multiverse restricted"),
3939+ contents, flags=re.IGNORECASE))
3940+
3941+ def test_apt_v3_src_basic(self):
3942+ """test_apt_v3_src_basic - Test fix deb source string"""
3943+ cfg = {self.aptlistfile: {'source':
3944+ ('deb http://test.ubuntu.com/ubuntu'
3945+ ' karmic-backports'
3946+ ' main universe multiverse restricted')}}
3947+ self._apt_src_basic(self.aptlistfile, cfg)
3948+
3949+ def test_apt_v3_src_basic_tri(self):
3950+ """test_apt_v3_src_basic_tri - Test multiple fix deb source strings"""
3951+ cfg = {self.aptlistfile: {'source':
3952+ ('deb http://test.ubuntu.com/ubuntu'
3953+ ' karmic-backports'
3954+ ' main universe multiverse restricted')},
3955+ self.aptlistfile2: {'source':
3956+ ('deb http://test.ubuntu.com/ubuntu'
3957+ ' precise-backports'
3958+ ' main universe multiverse restricted')},
3959+ self.aptlistfile3: {'source':
3960+ ('deb http://test.ubuntu.com/ubuntu'
3961+ ' lucid-backports'
3962+ ' main universe multiverse restricted')}}
3963+ self._apt_src_basic(self.aptlistfile, cfg)
3964+
3965+ # extra verify on two extra files of this test
3966+ contents = load_tfile(self.aptlistfile2)
3967+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3968+ ("deb", "http://test.ubuntu.com/ubuntu",
3969+ "precise-backports",
3970+ "main universe multiverse restricted"),
3971+ contents, flags=re.IGNORECASE))
3972+ contents = load_tfile(self.aptlistfile3)
3973+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3974+ ("deb", "http://test.ubuntu.com/ubuntu",
3975+ "lucid-backports",
3976+ "main universe multiverse restricted"),
3977+ contents, flags=re.IGNORECASE))
3978+
3979+ def _apt_src_replacement(self, filename, cfg):
3980+ """apt_src_replace
3981+ Test Autoreplacement of MIRROR and RELEASE in source specs
3982+ """
3983+ params = self._get_default_params()
3984+ self._add_apt_sources(cfg, TARGET, template_params=params,
3985+ aa_repo_match=self.matcher)
3986+
3987+ self.assertTrue(os.path.isfile(filename))
3988+
3989+ contents = load_tfile(filename)
3990+ self.assertTrue(re.search(r"%s %s %s %s\n" %
3991+ ("deb", params['MIRROR'], params['RELEASE'],
3992+ "multiverse"),
3993+ contents, flags=re.IGNORECASE))
3994+
3995+ def test_apt_v3_src_replace(self):
3996+ """test_apt_v3_src_replace - Test replacement of MIRROR & RELEASE"""
3997+ cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'}}
3998+ self._apt_src_replacement(self.aptlistfile, cfg)
3999+
4000+ def test_apt_v3_src_replace_fn(self):
4001+ """test_apt_v3_src_replace_fn - Test filename overwritten in dict"""
4002+ cfg = {'ignored': {'source': 'deb $MIRROR $RELEASE multiverse',
4003+ 'filename': self.aptlistfile}}
4004+ # second file should overwrite the dict key
4005+ self._apt_src_replacement(self.aptlistfile, cfg)
4006+
4007+ def _apt_src_replace_tri(self, cfg):
4008+ """_apt_src_replace_tri
4009+ Test three autoreplacements of MIRROR and RELEASE in source specs with
4010+ generic part
4011+ """
4012+ self._apt_src_replacement(self.aptlistfile, cfg)
4013+
4014+ # extra verify on two extra files of this test
4015+ params = self._get_default_params()
4016+ contents = load_tfile(self.aptlistfile2)
4017+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4018+ ("deb", params['MIRROR'], params['RELEASE'],
4019+ "main"),
4020+ contents, flags=re.IGNORECASE))
4021+ contents = load_tfile(self.aptlistfile3)
4022+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4023+ ("deb", params['MIRROR'], params['RELEASE'],
4024+ "universe"),
4025+ contents, flags=re.IGNORECASE))
4026+
4027+ def test_apt_v3_src_replace_tri(self):
4028+ """test_apt_v3_src_replace_tri - Test multiple replace/overwrites"""
4029+ cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
4030+ 'notused': {'source': 'deb $MIRROR $RELEASE main',
4031+ 'filename': self.aptlistfile2},
4032+ self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
4033+ self._apt_src_replace_tri(cfg)
4034+
4035+ def _apt_src_keyid(self, filename, cfg, keynum):
4036+ """_apt_src_keyid
4037+ Test specification of a source + keyid
4038+ """
4039+ params = self._get_default_params()
4040+
4041+ with mock.patch("cloudinit.util.subp",
4042+ return_value=('fakekey 1234', '')) as mockobj:
4043+ self._add_apt_sources(cfg, TARGET, template_params=params,
4044+ aa_repo_match=self.matcher)
4045+
4046+ # check if it added the right ammount of keys
4047+ calls = []
4048+ for _ in range(keynum):
4049+ calls.append(call(['apt-key', 'add', '-'], data=b'fakekey 1234',
4050+ target=TARGET))
4051+ mockobj.assert_has_calls(calls, any_order=True)
4052+
4053+ self.assertTrue(os.path.isfile(filename))
4054+
4055+ contents = load_tfile(filename)
4056+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4057+ ("deb",
4058+ ('http://ppa.launchpad.net/smoser/'
4059+ 'cloud-init-test/ubuntu'),
4060+ "xenial", "main"),
4061+ contents, flags=re.IGNORECASE))
4062+
4063+ def test_apt_v3_src_keyid(self):
4064+ """test_apt_v3_src_keyid - Test source + keyid with filename"""
4065+ cfg = {self.aptlistfile: {'source': ('deb '
4066+ 'http://ppa.launchpad.net/'
4067+ 'smoser/cloud-init-test/ubuntu'
4068+ ' xenial main'),
4069+ 'keyid': "03683F77"}}
4070+ self._apt_src_keyid(self.aptlistfile, cfg, 1)
4071+
4072+ def test_apt_v3_src_keyid_tri(self):
4073+ """test_apt_v3_src_keyid_tri - Test multiple src+key+filen writes"""
4074+ cfg = {self.aptlistfile: {'source': ('deb '
4075+ 'http://ppa.launchpad.net/'
4076+ 'smoser/cloud-init-test/ubuntu'
4077+ ' xenial main'),
4078+ 'keyid': "03683F77"},
4079+ 'ignored': {'source': ('deb '
4080+ 'http://ppa.launchpad.net/'
4081+ 'smoser/cloud-init-test/ubuntu'
4082+ ' xenial universe'),
4083+ 'keyid': "03683F77",
4084+ 'filename': self.aptlistfile2},
4085+ self.aptlistfile3: {'source': ('deb '
4086+ 'http://ppa.launchpad.net/'
4087+ 'smoser/cloud-init-test/ubuntu'
4088+ ' xenial multiverse'),
4089+ 'keyid': "03683F77"}}
4090+
4091+ self._apt_src_keyid(self.aptlistfile, cfg, 3)
4092+ contents = load_tfile(self.aptlistfile2)
4093+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4094+ ("deb",
4095+ ('http://ppa.launchpad.net/smoser/'
4096+ 'cloud-init-test/ubuntu'),
4097+ "xenial", "universe"),
4098+ contents, flags=re.IGNORECASE))
4099+ contents = load_tfile(self.aptlistfile3)
4100+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4101+ ("deb",
4102+ ('http://ppa.launchpad.net/smoser/'
4103+ 'cloud-init-test/ubuntu'),
4104+ "xenial", "multiverse"),
4105+ contents, flags=re.IGNORECASE))
4106+
4107+ def test_apt_v3_src_key(self):
4108+ """test_apt_v3_src_key - Test source + key"""
4109+ params = self._get_default_params()
4110+ cfg = {self.aptlistfile: {'source': ('deb '
4111+ 'http://ppa.launchpad.net/'
4112+ 'smoser/cloud-init-test/ubuntu'
4113+ ' xenial main'),
4114+ 'key': "fakekey 4321"}}
4115+
4116+ with mock.patch.object(util, 'subp') as mockobj:
4117+ self._add_apt_sources(cfg, TARGET, template_params=params,
4118+ aa_repo_match=self.matcher)
4119+
4120+ mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 4321',
4121+ target=TARGET)
4122+
4123+ self.assertTrue(os.path.isfile(self.aptlistfile))
4124+
4125+ contents = load_tfile(self.aptlistfile)
4126+ self.assertTrue(re.search(r"%s %s %s %s\n" %
4127+ ("deb",
4128+ ('http://ppa.launchpad.net/smoser/'
4129+ 'cloud-init-test/ubuntu'),
4130+ "xenial", "main"),
4131+ contents, flags=re.IGNORECASE))
4132+
4133+ def test_apt_v3_src_keyonly(self):
4134+ """test_apt_v3_src_keyonly - Test key without source"""
4135+ params = self._get_default_params()
4136+ cfg = {self.aptlistfile: {'key': "fakekey 4242"}}
4137+
4138+ with mock.patch.object(util, 'subp') as mockobj:
4139+ self._add_apt_sources(cfg, TARGET, template_params=params,
4140+ aa_repo_match=self.matcher)
4141+
4142+ mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 4242',
4143+ target=TARGET)
4144+
4145+ # filename should be ignored on key only
4146+ self.assertFalse(os.path.isfile(self.aptlistfile))
4147+
4148+ def test_apt_v3_src_keyidonly(self):
4149+ """test_apt_v3_src_keyidonly - Test keyid without source"""
4150+ params = self._get_default_params()
4151+ cfg = {self.aptlistfile: {'keyid': "03683F77"}}
4152+
4153+ with mock.patch.object(util, 'subp',
4154+ return_value=('fakekey 1212', '')) as mockobj:
4155+ self._add_apt_sources(cfg, TARGET, template_params=params,
4156+ aa_repo_match=self.matcher)
4157+
4158+ mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 1212',
4159+ target=TARGET)
4160+
4161+ # filename should be ignored on key only
4162+ self.assertFalse(os.path.isfile(self.aptlistfile))
4163+
4164+ def apt_src_keyid_real(self, cfg, expectedkey):
4165+ """apt_src_keyid_real
4166+ Test specification of a keyid without source including
4167+ up to addition of the key (add_apt_key_raw mocked to keep the
4168+ environment as is)
4169+ """
4170+ params = self._get_default_params()
4171+
4172+ with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
4173+ with mock.patch.object(gpg, 'getkeybyid',
4174+ return_value=expectedkey) as mockgetkey:
4175+ self._add_apt_sources(cfg, TARGET, template_params=params,
4176+ aa_repo_match=self.matcher)
4177+
4178+ keycfg = cfg[self.aptlistfile]
4179+ mockgetkey.assert_called_with(keycfg['keyid'],
4180+ keycfg.get('keyserver',
4181+ 'keyserver.ubuntu.com'))
4182+ mockkey.assert_called_with(expectedkey, TARGET)
4183+
4184+ # filename should be ignored on key only
4185+ self.assertFalse(os.path.isfile(self.aptlistfile))
4186+
4187+ def test_apt_v3_src_keyid_real(self):
4188+ """test_apt_v3_src_keyid_real - Test keyid including key add"""
4189+ keyid = "03683F77"
4190+ cfg = {self.aptlistfile: {'keyid': keyid}}
4191+
4192+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
4193+
4194+ def test_apt_v3_src_longkeyid_real(self):
4195+ """test_apt_v3_src_longkeyid_real Test long keyid including key add"""
4196+ keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
4197+ cfg = {self.aptlistfile: {'keyid': keyid}}
4198+
4199+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
4200+
4201+ def test_apt_v3_src_longkeyid_ks_real(self):
4202+ """test_apt_v3_src_longkeyid_ks_real Test long keyid from other ks"""
4203+ keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
4204+ cfg = {self.aptlistfile: {'keyid': keyid,
4205+ 'keyserver': 'keys.gnupg.net'}}
4206+
4207+ self.apt_src_keyid_real(cfg, EXPECTEDKEY)
4208+
4209+ def test_apt_v3_src_keyid_keyserver(self):
4210+ """test_apt_v3_src_keyid_keyserver - Test custom keyserver"""
4211+ keyid = "03683F77"
4212+ params = self._get_default_params()
4213+ cfg = {self.aptlistfile: {'keyid': keyid,
4214+ 'keyserver': 'test.random.com'}}
4215+
4216+ # in some test environments only *.ubuntu.com is reachable
4217+ # so mock the call and check if the config got there
4218+ with mock.patch.object(gpg, 'getkeybyid',
4219+ return_value="fakekey") as mockgetkey:
4220+ with mock.patch.object(cc_apt_configure,
4221+ 'add_apt_key_raw') as mockadd:
4222+ self._add_apt_sources(cfg, TARGET, template_params=params,
4223+ aa_repo_match=self.matcher)
4224+
4225+ mockgetkey.assert_called_with('03683F77', 'test.random.com')
4226+ mockadd.assert_called_with('fakekey', TARGET)
4227+
4228+ # filename should be ignored on key only
4229+ self.assertFalse(os.path.isfile(self.aptlistfile))
4230+
4231+ def test_apt_v3_src_ppa(self):
4232+ """test_apt_v3_src_ppa - Test specification of a ppa"""
4233+ params = self._get_default_params()
4234+ cfg = {self.aptlistfile: {'source': 'ppa:smoser/cloud-init-test'}}
4235+
4236+ with mock.patch("cloudinit.util.subp") as mockobj:
4237+ self._add_apt_sources(cfg, TARGET, template_params=params,
4238+ aa_repo_match=self.matcher)
4239+ mockobj.assert_any_call(['add-apt-repository',
4240+ 'ppa:smoser/cloud-init-test'], target=TARGET)
4241+
4242+ # adding ppa should ignore filename (uses add-apt-repository)
4243+ self.assertFalse(os.path.isfile(self.aptlistfile))
4244+
4245+ def test_apt_v3_src_ppa_tri(self):
4246+ """test_apt_v3_src_ppa_tri - Test specification of multiple ppa's"""
4247+ params = self._get_default_params()
4248+ cfg = {self.aptlistfile: {'source': 'ppa:smoser/cloud-init-test'},
4249+ self.aptlistfile2: {'source': 'ppa:smoser/cloud-init-test2'},
4250+ self.aptlistfile3: {'source': 'ppa:smoser/cloud-init-test3'}}
4251+
4252+ with mock.patch("cloudinit.util.subp") as mockobj:
4253+ self._add_apt_sources(cfg, TARGET, template_params=params,
4254+ aa_repo_match=self.matcher)
4255+ calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test'],
4256+ target=TARGET),
4257+ call(['add-apt-repository', 'ppa:smoser/cloud-init-test2'],
4258+ target=TARGET),
4259+ call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'],
4260+ target=TARGET)]
4261+ mockobj.assert_has_calls(calls, any_order=True)
4262+
4263+ # adding ppa should ignore all filenames (uses add-apt-repository)
4264+ self.assertFalse(os.path.isfile(self.aptlistfile))
4265+ self.assertFalse(os.path.isfile(self.aptlistfile2))
4266+ self.assertFalse(os.path.isfile(self.aptlistfile3))
4267+
4268+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
4269+ def test_apt_v3_list_rename(self, m_get_architecture):
4270+ """test_apt_v3_list_rename - Test find mirror and apt list renaming"""
4271+ pre = "/var/lib/apt/lists"
4272+ # filenames are archive dependent
4273+
4274+ arch = 's390x'
4275+ m_get_architecture.return_value = arch
4276+ component = "ubuntu-ports"
4277+ archive = "ports.ubuntu.com"
4278+
4279+ cfg = {'primary': [{'arches': ["default"],
4280+ 'uri':
4281+ 'http://test.ubuntu.com/%s/' % component}],
4282+ 'security': [{'arches': ["default"],
4283+ 'uri':
4284+ 'http://testsec.ubuntu.com/%s/' % component}]}
4285+ post = ("%s_dists_%s-updates_InRelease" %
4286+ (component, util.lsb_release()['codename']))
4287+ fromfn = ("%s/%s_%s" % (pre, archive, post))
4288+ tofn = ("%s/test.ubuntu.com_%s" % (pre, post))
4289+
4290+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
4291+
4292+ self.assertEqual(mirrors['MIRROR'],
4293+ "http://test.ubuntu.com/%s/" % component)
4294+ self.assertEqual(mirrors['PRIMARY'],
4295+ "http://test.ubuntu.com/%s/" % component)
4296+ self.assertEqual(mirrors['SECURITY'],
4297+ "http://testsec.ubuntu.com/%s/" % component)
4298+
4299+ with mock.patch.object(os, 'rename') as mockren:
4300+ with mock.patch.object(glob, 'glob',
4301+ return_value=[fromfn]):
4302+ cc_apt_configure.rename_apt_lists(mirrors, TARGET)
4303+
4304+ mockren.assert_any_call(fromfn, tofn)
4305+
4306+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
4307+ def test_apt_v3_list_rename_non_slash(self, m_get_architecture):
4308+ target = os.path.join(self.tmp, "rename_non_slash")
4309+ apt_lists_d = os.path.join(target, "./" + cc_apt_configure.APT_LISTS)
4310+
4311+ m_get_architecture.return_value = 'amd64'
4312+
4313+ mirror_path = "some/random/path/"
4314+ primary = "http://test.ubuntu.com/" + mirror_path
4315+ security = "http://test-security.ubuntu.com/" + mirror_path
4316+ mirrors = {'PRIMARY': primary, 'SECURITY': security}
4317+
4318+ # these match default archive prefixes
4319+ opri_pre = "archive.ubuntu.com_ubuntu_dists_xenial"
4320+ osec_pre = "security.ubuntu.com_ubuntu_dists_xenial"
4321+ # this one won't match and should not be renamed defaults.
4322+ other_pre = "dl.google.com_linux_chrome_deb_dists_stable"
4323+ # these are our new expected prefixes
4324+ npri_pre = "test.ubuntu.com_some_random_path_dists_xenial"
4325+ nsec_pre = "test-security.ubuntu.com_some_random_path_dists_xenial"
4326+
4327+ files = [
4328+ # orig prefix, new prefix, suffix
4329+ (opri_pre, npri_pre, "_main_binary-amd64_Packages"),
4330+ (opri_pre, npri_pre, "_main_binary-amd64_InRelease"),
4331+ (opri_pre, npri_pre, "-updates_main_binary-amd64_Packages"),
4332+ (opri_pre, npri_pre, "-updates_main_binary-amd64_InRelease"),
4333+ (other_pre, other_pre, "_main_binary-amd64_Packages"),
4334+ (other_pre, other_pre, "_Release"),
4335+ (other_pre, other_pre, "_Release.gpg"),
4336+ (osec_pre, nsec_pre, "_InRelease"),
4337+ (osec_pre, nsec_pre, "_main_binary-amd64_Packages"),
4338+ (osec_pre, nsec_pre, "_universe_binary-amd64_Packages"),
4339+ ]
4340+
4341+ expected = sorted([npre + suff for opre, npre, suff in files])
4342+ # create files
4343+ for (opre, npre, suff) in files:
4344+ fpath = os.path.join(apt_lists_d, opre + suff)
4345+ util.write_file(fpath, content=fpath)
4346+
4347+ cc_apt_configure.rename_apt_lists(mirrors, target)
4348+ found = sorted(os.listdir(apt_lists_d))
4349+ self.assertEqual(expected, found)
4350+
4351+ @staticmethod
4352+ def test_apt_v3_proxy():
4353+ """test_apt_v3_proxy - Test apt_*proxy configuration"""
4354+ cfg = {"proxy": "foobar1",
4355+ "http_proxy": "foobar2",
4356+ "ftp_proxy": "foobar3",
4357+ "https_proxy": "foobar4"}
4358+
4359+ with mock.patch.object(util, 'write_file') as mockobj:
4360+ cc_apt_configure.apply_apt_config(cfg, "proxyfn", "notused")
4361+
4362+ mockobj.assert_called_with('proxyfn',
4363+ ('Acquire::http::Proxy "foobar1";\n'
4364+ 'Acquire::http::Proxy "foobar2";\n'
4365+ 'Acquire::ftp::Proxy "foobar3";\n'
4366+ 'Acquire::https::Proxy "foobar4";\n'))
4367+
4368+ def test_apt_v3_mirror(self):
4369+ """test_apt_v3_mirror - Test defining a mirror"""
4370+ pmir = "http://us.archive.ubuntu.com/ubuntu/"
4371+ smir = "http://security.ubuntu.com/ubuntu/"
4372+ cfg = {"primary": [{'arches': ["default"],
4373+ "uri": pmir}],
4374+ "security": [{'arches': ["default"],
4375+ "uri": smir}]}
4376+
4377+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, 'amd64')
4378+
4379+ self.assertEqual(mirrors['MIRROR'],
4380+ pmir)
4381+ self.assertEqual(mirrors['PRIMARY'],
4382+ pmir)
4383+ self.assertEqual(mirrors['SECURITY'],
4384+ smir)
4385+
4386+ def test_apt_v3_mirror_default(self):
4387+ """test_apt_v3_mirror_default - Test without defining a mirror"""
4388+ arch = 'amd64'
4389+ default_mirrors = cc_apt_configure.get_default_mirrors(arch)
4390+ pmir = default_mirrors["PRIMARY"]
4391+ smir = default_mirrors["SECURITY"]
4392+ mycloud = self._get_cloud('ubuntu')
4393+ mirrors = cc_apt_configure.find_apt_mirror_info({}, mycloud, arch)
4394+
4395+ self.assertEqual(mirrors['MIRROR'],
4396+ pmir)
4397+ self.assertEqual(mirrors['PRIMARY'],
4398+ pmir)
4399+ self.assertEqual(mirrors['SECURITY'],
4400+ smir)
4401+
4402+ def test_apt_v3_mirror_arches(self):
4403+ """test_apt_v3_mirror_arches - Test arches selection of mirror"""
4404+ pmir = "http://my-primary.ubuntu.com/ubuntu/"
4405+ smir = "http://my-security.ubuntu.com/ubuntu/"
4406+ arch = 'ppc64el'
4407+ cfg = {"primary": [{'arches': ["default"], "uri": "notthis-primary"},
4408+ {'arches': [arch], "uri": pmir}],
4409+ "security": [{'arches': ["default"], "uri": "nothis-security"},
4410+ {'arches': [arch], "uri": smir}]}
4411+
4412+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
4413+
4414+ self.assertEqual(mirrors['PRIMARY'], pmir)
4415+ self.assertEqual(mirrors['MIRROR'], pmir)
4416+ self.assertEqual(mirrors['SECURITY'], smir)
4417+
4418+ def test_apt_v3_mirror_arches_default(self):
4419+ """test_apt_v3_mirror_arches - Test falling back to default arch"""
4420+ pmir = "http://us.archive.ubuntu.com/ubuntu/"
4421+ smir = "http://security.ubuntu.com/ubuntu/"
4422+ cfg = {"primary": [{'arches': ["default"],
4423+ "uri": pmir},
4424+ {'arches': ["thisarchdoesntexist"],
4425+ "uri": "notthis"}],
4426+ "security": [{'arches': ["thisarchdoesntexist"],
4427+ "uri": "nothat"},
4428+ {'arches': ["default"],
4429+ "uri": smir}]}
4430+
4431+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, 'amd64')
4432+
4433+ self.assertEqual(mirrors['MIRROR'],
4434+ pmir)
4435+ self.assertEqual(mirrors['PRIMARY'],
4436+ pmir)
4437+ self.assertEqual(mirrors['SECURITY'],
4438+ smir)
4439+
4440+ @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
4441+ def test_apt_v3_get_def_mir_non_intel_no_arch(self, m_get_architecture):
4442+ arch = 'ppc64el'
4443+ m_get_architecture.return_value = arch
4444+ expected = {'PRIMARY': 'http://ports.ubuntu.com/ubuntu-ports',
4445+ 'SECURITY': 'http://ports.ubuntu.com/ubuntu-ports'}
4446+ self.assertEqual(expected, cc_apt_configure.get_default_mirrors())
4447+
4448+ def test_apt_v3_get_default_mirrors_non_intel_with_arch(self):
4449+ found = cc_apt_configure.get_default_mirrors('ppc64el')
4450+
4451+ expected = {'PRIMARY': 'http://ports.ubuntu.com/ubuntu-ports',
4452+ 'SECURITY': 'http://ports.ubuntu.com/ubuntu-ports'}
4453+ self.assertEqual(expected, found)
4454+
4455+ def test_apt_v3_mirror_arches_sysdefault(self):
4456+ """test_apt_v3_mirror_arches - Test arches fallback to sys default"""
4457+ arch = 'amd64'
4458+ default_mirrors = cc_apt_configure.get_default_mirrors(arch)
4459+ pmir = default_mirrors["PRIMARY"]
4460+ smir = default_mirrors["SECURITY"]
4461+ mycloud = self._get_cloud('ubuntu')
4462+ cfg = {"primary": [{'arches': ["thisarchdoesntexist_64"],
4463+ "uri": "notthis"},
4464+ {'arches': ["thisarchdoesntexist"],
4465+ "uri": "notthiseither"}],
4466+ "security": [{'arches': ["thisarchdoesntexist"],
4467+ "uri": "nothat"},
4468+ {'arches': ["thisarchdoesntexist_64"],
4469+ "uri": "nothateither"}]}
4470+
4471+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
4472+
4473+ self.assertEqual(mirrors['MIRROR'], pmir)
4474+ self.assertEqual(mirrors['PRIMARY'], pmir)
4475+ self.assertEqual(mirrors['SECURITY'], smir)
4476+
4477+ def test_apt_v3_mirror_search(self):
4478+ """test_apt_v3_mirror_search - Test searching mirrors in a list
4479+ mock checks to avoid relying on network connectivity"""
4480+ pmir = "http://us.archive.ubuntu.com/ubuntu/"
4481+ smir = "http://security.ubuntu.com/ubuntu/"
4482+ cfg = {"primary": [{'arches': ["default"],
4483+ "search": ["pfailme", pmir]}],
4484+ "security": [{'arches': ["default"],
4485+ "search": ["sfailme", smir]}]}
4486+
4487+ with mock.patch.object(cc_apt_configure, 'search_for_mirror',
4488+ side_effect=[pmir, smir]) as mocksearch:
4489+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None,
4490+ 'amd64')
4491+
4492+ calls = [call(["pfailme", pmir]),
4493+ call(["sfailme", smir])]
4494+ mocksearch.assert_has_calls(calls)
4495+
4496+ self.assertEqual(mirrors['MIRROR'],
4497+ pmir)
4498+ self.assertEqual(mirrors['PRIMARY'],
4499+ pmir)
4500+ self.assertEqual(mirrors['SECURITY'],
4501+ smir)
4502+
4503+ def test_apt_v3_mirror_search_many2(self):
4504+ """test_apt_v3_mirror_search_many3 - Test both mirrors specs at once"""
4505+ pmir = "http://us.archive.ubuntu.com/ubuntu/"
4506+ smir = "http://security.ubuntu.com/ubuntu/"
4507+ cfg = {"primary": [{'arches': ["default"],
4508+ "uri": pmir,
4509+ "search": ["pfailme", "foo"]}],
4510+ "security": [{'arches': ["default"],
4511+ "uri": smir,
4512+ "search": ["sfailme", "bar"]}]}
4513+
4514+ arch = 'amd64'
4515+
4516+ # should be called only once per type, despite two mirror configs
4517+ mycloud = None
4518+ with mock.patch.object(cc_apt_configure, 'get_mirror',
4519+ return_value="http://mocked/foo") as mockgm:
4520+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
4521+ calls = [call(cfg, 'primary', arch, mycloud),
4522+ call(cfg, 'security', arch, mycloud)]
4523+ mockgm.assert_has_calls(calls)
4524+
4525+ # should not be called, since primary is specified
4526+ with mock.patch.object(cc_apt_configure,
4527+ 'search_for_mirror') as mockse:
4528+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
4529+ mockse.assert_not_called()
4530+
4531+ self.assertEqual(mirrors['MIRROR'],
4532+ pmir)
4533+ self.assertEqual(mirrors['PRIMARY'],
4534+ pmir)
4535+ self.assertEqual(mirrors['SECURITY'],
4536+ smir)
4537+
4538+ def test_apt_v3_url_resolvable(self):
4539+ """test_apt_v3_url_resolvable - Test resolving urls"""
4540+
4541+ with mock.patch.object(util, 'is_resolvable') as mockresolve:
4542+ util.is_resolvable_url("http://1.2.3.4/ubuntu")
4543+ mockresolve.assert_called_with("1.2.3.4")
4544+
4545+ with mock.patch.object(util, 'is_resolvable') as mockresolve:
4546+ util.is_resolvable_url("http://us.archive.ubuntu.com/ubuntu")
4547+ mockresolve.assert_called_with("us.archive.ubuntu.com")
4548+
4549+ # former tests can leave this set (or not if the test is ran directly)
4550+ # do a hard reset to ensure a stable result
4551+ util._DNS_REDIRECT_IP = None
4552+ bad = [(None, None, None, "badname", ["10.3.2.1"])]
4553+ good = [(None, None, None, "goodname", ["10.2.3.4"])]
4554+ with mock.patch.object(socket, 'getaddrinfo',
4555+ side_effect=[bad, bad, bad, good,
4556+ good]) as mocksock:
4557+ ret = util.is_resolvable_url("http://us.archive.ubuntu.com/ubuntu")
4558+ ret2 = util.is_resolvable_url("http://1.2.3.4/ubuntu")
4559+ mocksock.assert_any_call('does-not-exist.example.com.', None,
4560+ 0, 0, 1, 2)
4561+ mocksock.assert_any_call('example.invalid.', None, 0, 0, 1, 2)
4562+ mocksock.assert_any_call('us.archive.ubuntu.com', None)
4563+ mocksock.assert_any_call('1.2.3.4', None)
4564+
4565+ self.assertTrue(ret)
4566+ self.assertTrue(ret2)
4567+
4568+ # side effect need only bad ret after initial call
4569+ with mock.patch.object(socket, 'getaddrinfo',
4570+ side_effect=[bad]) as mocksock:
4571+ ret3 = util.is_resolvable_url("http://failme.com/ubuntu")
4572+ calls = [call('failme.com', None)]
4573+ mocksock.assert_has_calls(calls)
4574+ self.assertFalse(ret3)
4575+
4576+ def test_apt_v3_disable_suites(self):
4577+ """test_disable_suites - disable_suites with many configurations"""
4578+ release = "xenial"
4579+ orig = """deb http://ubuntu.com//ubuntu xenial main
4580+deb http://ubuntu.com//ubuntu xenial-updates main
4581+deb http://ubuntu.com//ubuntu xenial-security main
4582+deb-src http://ubuntu.com//ubuntu universe multiverse
4583+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4584+
4585+ # disable nothing
4586+ disabled = []
4587+ expect = """deb http://ubuntu.com//ubuntu xenial main
4588+deb http://ubuntu.com//ubuntu xenial-updates main
4589+deb http://ubuntu.com//ubuntu xenial-security main
4590+deb-src http://ubuntu.com//ubuntu universe multiverse
4591+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4592+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4593+ self.assertEqual(expect, result)
4594+
4595+ # single disable release suite
4596+ disabled = ["$RELEASE"]
4597+ expect = """\
4598+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu xenial main
4599+deb http://ubuntu.com//ubuntu xenial-updates main
4600+deb http://ubuntu.com//ubuntu xenial-security main
4601+deb-src http://ubuntu.com//ubuntu universe multiverse
4602+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4603+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4604+ self.assertEqual(expect, result)
4605+
4606+ # single disable other suite
4607+ disabled = ["$RELEASE-updates"]
4608+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4609+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu"""
4610+ """ xenial-updates main
4611+deb http://ubuntu.com//ubuntu xenial-security main
4612+deb-src http://ubuntu.com//ubuntu universe multiverse
4613+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4614+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4615+ self.assertEqual(expect, result)
4616+
4617+ # multi disable
4618+ disabled = ["$RELEASE-updates", "$RELEASE-security"]
4619+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4620+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4621+ """xenial-updates main
4622+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4623+ """xenial-security main
4624+deb-src http://ubuntu.com//ubuntu universe multiverse
4625+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4626+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4627+ self.assertEqual(expect, result)
4628+
4629+ # multi line disable (same suite multiple times in input)
4630+ disabled = ["$RELEASE-updates", "$RELEASE-security"]
4631+ orig = """deb http://ubuntu.com//ubuntu xenial main
4632+deb http://ubuntu.com//ubuntu xenial-updates main
4633+deb http://ubuntu.com//ubuntu xenial-security main
4634+deb-src http://ubuntu.com//ubuntu universe multiverse
4635+deb http://UBUNTU.com//ubuntu xenial-updates main
4636+deb http://UBUNTU.COM//ubuntu xenial-updates main
4637+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4638+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4639+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4640+ """xenial-updates main
4641+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4642+ """xenial-security main
4643+deb-src http://ubuntu.com//ubuntu universe multiverse
4644+# suite disabled by cloud-init: deb http://UBUNTU.com//ubuntu """
4645+ """xenial-updates main
4646+# suite disabled by cloud-init: deb http://UBUNTU.COM//ubuntu """
4647+ """xenial-updates main
4648+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4649+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4650+ self.assertEqual(expect, result)
4651+
4652+ # comment in input
4653+ disabled = ["$RELEASE-updates", "$RELEASE-security"]
4654+ orig = """deb http://ubuntu.com//ubuntu xenial main
4655+deb http://ubuntu.com//ubuntu xenial-updates main
4656+deb http://ubuntu.com//ubuntu xenial-security main
4657+deb-src http://ubuntu.com//ubuntu universe multiverse
4658+#foo
4659+#deb http://UBUNTU.com//ubuntu xenial-updates main
4660+deb http://UBUNTU.COM//ubuntu xenial-updates main
4661+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4662+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4663+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4664+ """xenial-updates main
4665+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4666+ """xenial-security main
4667+deb-src http://ubuntu.com//ubuntu universe multiverse
4668+#foo
4669+#deb http://UBUNTU.com//ubuntu xenial-updates main
4670+# suite disabled by cloud-init: deb http://UBUNTU.COM//ubuntu """
4671+ """xenial-updates main
4672+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4673+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4674+ self.assertEqual(expect, result)
4675+
4676+ # single disable custom suite
4677+ disabled = ["foobar"]
4678+ orig = """deb http://ubuntu.com//ubuntu xenial main
4679+deb http://ubuntu.com//ubuntu xenial-updates main
4680+deb http://ubuntu.com//ubuntu xenial-security main
4681+deb http://ubuntu.com/ubuntu/ foobar main"""
4682+ expect = """deb http://ubuntu.com//ubuntu xenial main
4683+deb http://ubuntu.com//ubuntu xenial-updates main
4684+deb http://ubuntu.com//ubuntu xenial-security main
4685+# suite disabled by cloud-init: deb http://ubuntu.com/ubuntu/ foobar main"""
4686+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4687+ self.assertEqual(expect, result)
4688+
4689+ # single disable non existing suite
4690+ disabled = ["foobar"]
4691+ orig = """deb http://ubuntu.com//ubuntu xenial main
4692+deb http://ubuntu.com//ubuntu xenial-updates main
4693+deb http://ubuntu.com//ubuntu xenial-security main
4694+deb http://ubuntu.com/ubuntu/ notfoobar main"""
4695+ expect = """deb http://ubuntu.com//ubuntu xenial main
4696+deb http://ubuntu.com//ubuntu xenial-updates main
4697+deb http://ubuntu.com//ubuntu xenial-security main
4698+deb http://ubuntu.com/ubuntu/ notfoobar main"""
4699+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4700+ self.assertEqual(expect, result)
4701+
4702+ # single disable suite with option
4703+ disabled = ["$RELEASE-updates"]
4704+ orig = """deb http://ubuntu.com//ubuntu xenial main
4705+deb [a=b] http://ubu.com//ubu xenial-updates main
4706+deb http://ubuntu.com//ubuntu xenial-security main
4707+deb-src http://ubuntu.com//ubuntu universe multiverse
4708+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4709+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4710+# suite disabled by cloud-init: deb [a=b] http://ubu.com//ubu """
4711+ """xenial-updates main
4712+deb http://ubuntu.com//ubuntu xenial-security main
4713+deb-src http://ubuntu.com//ubuntu universe multiverse
4714+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4715+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4716+ self.assertEqual(expect, result)
4717+
4718+ # single disable suite with more options and auto $RELEASE expansion
4719+ disabled = ["updates"]
4720+ orig = """deb http://ubuntu.com//ubuntu xenial main
4721+deb [a=b c=d] http://ubu.com//ubu xenial-updates main
4722+deb http://ubuntu.com//ubuntu xenial-security main
4723+deb-src http://ubuntu.com//ubuntu universe multiverse
4724+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4725+ expect = """deb http://ubuntu.com//ubuntu xenial main
4726+# suite disabled by cloud-init: deb [a=b c=d] \
4727+http://ubu.com//ubu xenial-updates main
4728+deb http://ubuntu.com//ubuntu xenial-security main
4729+deb-src http://ubuntu.com//ubuntu universe multiverse
4730+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4731+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4732+ self.assertEqual(expect, result)
4733+
4734+ # single disable suite while options at others
4735+ disabled = ["$RELEASE-security"]
4736+ orig = """deb http://ubuntu.com//ubuntu xenial main
4737+deb [arch=foo] http://ubuntu.com//ubuntu xenial-updates main
4738+deb http://ubuntu.com//ubuntu xenial-security main
4739+deb-src http://ubuntu.com//ubuntu universe multiverse
4740+deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
4741+ expect = ("""deb http://ubuntu.com//ubuntu xenial main
4742+deb [arch=foo] http://ubuntu.com//ubuntu xenial-updates main
4743+# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
4744+ """xenial-security main
4745+deb-src http://ubuntu.com//ubuntu universe multiverse
4746+deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
4747+ result = cc_apt_configure.disable_suites(disabled, orig, release)
4748+ self.assertEqual(expect, result)
4749+
4750+ def test_disable_suites_blank_lines(self):
4751+ """test_disable_suites_blank_lines - ensure blank lines allowed"""
4752+ lines = ["deb %(repo)s %(rel)s main universe",
4753+ "",
4754+ "deb %(repo)s %(rel)s-updates main universe",
4755+ " # random comment",
4756+ "#comment here",
4757+ ""]
4758+ rel = "trusty"
4759+ repo = 'http://example.com/mirrors/ubuntu'
4760+ orig = "\n".join(lines) % {'repo': repo, 'rel': rel}
4761+ self.assertEqual(
4762+ orig, cc_apt_configure.disable_suites(["proposed"], orig, rel))
4763+
4764+ def test_apt_v3_mirror_search_dns(self):
4765+ """test_apt_v3_mirror_search_dns - Test searching dns patterns"""
4766+ pmir = "phit"
4767+ smir = "shit"
4768+ arch = 'amd64'
4769+ mycloud = self._get_cloud('ubuntu')
4770+ cfg = {"primary": [{'arches': ["default"],
4771+ "search_dns": True}],
4772+ "security": [{'arches': ["default"],
4773+ "search_dns": True}]}
4774+
4775+ with mock.patch.object(cc_apt_configure, 'get_mirror',
4776+ return_value="http://mocked/foo") as mockgm:
4777+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
4778+ calls = [call(cfg, 'primary', arch, mycloud),
4779+ call(cfg, 'security', arch, mycloud)]
4780+ mockgm.assert_has_calls(calls)
4781+
4782+ with mock.patch.object(cc_apt_configure, 'search_for_mirror_dns',
4783+ return_value="http://mocked/foo") as mocksdns:
4784+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
4785+ calls = [call(True, 'primary', cfg, mycloud),
4786+ call(True, 'security', cfg, mycloud)]
4787+ mocksdns.assert_has_calls(calls)
4788+
4789+ # first return is for the non-dns call before
4790+ with mock.patch.object(cc_apt_configure, 'search_for_mirror',
4791+ side_effect=[None, pmir, None, smir]) as mockse:
4792+ mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
4793+
4794+ calls = [call(None),
4795+ call(['http://ubuntu-mirror.localdomain/ubuntu',
4796+ 'http://ubuntu-mirror/ubuntu']),
4797+ call(None),
4798+ call(['http://ubuntu-security-mirror.localdomain/ubuntu',
4799+ 'http://ubuntu-security-mirror/ubuntu'])]
4800+ mockse.assert_has_calls(calls)
4801+
4802+ self.assertEqual(mirrors['MIRROR'],
4803+ pmir)
4804+ self.assertEqual(mirrors['PRIMARY'],
4805+ pmir)
4806+ self.assertEqual(mirrors['SECURITY'],
4807+ smir)
4808+
4809+
4810+class TestDebconfSelections(TestCase):
4811+
4812+ @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
4813+ def test_no_set_sel_if_none_to_set(self, m_set_sel):
4814+ cc_apt_configure.apply_debconf_selections({'foo': 'bar'})
4815+ m_set_sel.assert_not_called()
4816+
4817+ @mock.patch("cloudinit.config.cc_apt_configure."
4818+ "debconf_set_selections")
4819+ @mock.patch("cloudinit.config.cc_apt_configure."
4820+ "util.get_installed_packages")
4821+ def test_set_sel_call_has_expected_input(self, m_get_inst, m_set_sel):
4822+ data = {
4823+ 'set1': 'pkga pkga/q1 mybool false',
4824+ 'set2': ('pkgb\tpkgb/b1\tstr\tthis is a string\n'
4825+ 'pkgc\tpkgc/ip\tstring\t10.0.0.1')}
4826+ lines = '\n'.join(data.values()).split('\n')
4827+
4828+ m_get_inst.return_value = ["adduser", "apparmor"]
4829+ m_set_sel.return_value = None
4830+
4831+ cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
4832+ self.assertTrue(m_get_inst.called)
4833+ self.assertEqual(m_set_sel.call_count, 1)
4834+
4835+ # assumes called with *args value.
4836+ selections = m_set_sel.call_args_list[0][0][0].decode()
4837+
4838+ missing = [l for l in lines if l not in selections.splitlines()]
4839+ self.assertEqual([], missing)
4840+
4841+ @mock.patch("cloudinit.config.cc_apt_configure.dpkg_reconfigure")
4842+ @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
4843+ @mock.patch("cloudinit.config.cc_apt_configure."
4844+ "util.get_installed_packages")
4845+ def test_reconfigure_if_intersection(self, m_get_inst, m_set_sel,
4846+ m_dpkg_r):
4847+ data = {
4848+ 'set1': 'pkga pkga/q1 mybool false',
4849+ 'set2': ('pkgb\tpkgb/b1\tstr\tthis is a string\n'
4850+ 'pkgc\tpkgc/ip\tstring\t10.0.0.1'),
4851+ 'cloud-init': ('cloud-init cloud-init/datasources'
4852+ 'multiselect MAAS')}
4853+
4854+ m_set_sel.return_value = None
4855+ m_get_inst.return_value = ["adduser", "apparmor", "pkgb",
4856+ "cloud-init", 'zdog']
4857+
4858+ cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
4859+
4860+ # reconfigure should be called with the intersection
4861+ # of (packages in config, packages installed)
4862+ self.assertEqual(m_dpkg_r.call_count, 1)
4863+ # assumes called with *args (dpkg_reconfigure([a,b,c], target=))
4864+ packages = m_dpkg_r.call_args_list[0][0][0]
4865+ self.assertEqual(set(['cloud-init', 'pkgb']), set(packages))
4866+
4867+ @mock.patch("cloudinit.config.cc_apt_configure.dpkg_reconfigure")
4868+ @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
4869+ @mock.patch("cloudinit.config.cc_apt_configure."
4870+ "util.get_installed_packages")
4871+ def test_reconfigure_if_no_intersection(self, m_get_inst, m_set_sel,
4872+ m_dpkg_r):
4873+ data = {'set1': 'pkga pkga/q1 mybool false'}
4874+
4875+ m_get_inst.return_value = ["adduser", "apparmor", "pkgb",
4876+ "cloud-init", 'zdog']
4877+ m_set_sel.return_value = None
4878+
4879+ cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
4880+
4881+ self.assertTrue(m_get_inst.called)
4882+ self.assertEqual(m_dpkg_r.call_count, 0)
4883+
4884+ @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
4885+ def test_dpkg_reconfigure_does_reconfigure(self, m_subp):
4886+ target = "/foo-target"
4887+
4888+ # due to the way the cleaners are called (via dictionary reference)
4889+ # mocking clean_cloud_init directly does not work. So we mock
4890+ # the CONFIG_CLEANERS dictionary and assert our cleaner is called.
4891+ ci_cleaner = mock.MagicMock()
4892+ with mock.patch.dict(("cloudinit.config.cc_apt_configure."
4893+ "CONFIG_CLEANERS"),
4894+ values={'cloud-init': ci_cleaner}, clear=True):
4895+ cc_apt_configure.dpkg_reconfigure(['pkga', 'cloud-init'],
4896+ target=target)
4897+ # cloud-init is actually the only package we have a cleaner for
4898+ # so for now, its the only one that should reconfigured
4899+ self.assertTrue(m_subp.called)
4900+ ci_cleaner.assert_called_with(target)
4901+ self.assertEqual(m_subp.call_count, 1)
4902+ found = m_subp.call_args_list[0][0][0]
4903+ expected = ['dpkg-reconfigure', '--frontend=noninteractive',
4904+ 'cloud-init']
4905+ self.assertEqual(expected, found)
4906+
4907+ @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
4908+ def test_dpkg_reconfigure_not_done_on_no_data(self, m_subp):
4909+ cc_apt_configure.dpkg_reconfigure([])
4910+ m_subp.assert_not_called()
4911+
4912+ @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
4913+ def test_dpkg_reconfigure_not_done_if_no_cleaners(self, m_subp):
4914+ cc_apt_configure.dpkg_reconfigure(['pkgfoo', 'pkgbar'])
4915+ m_subp.assert_not_called()
4916+
4917+#
4918+# vi: ts=4 expandtab
4919diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
4920index 73369cd..d2031f5 100644
4921--- a/tests/unittests/test_util.py
4922+++ b/tests/unittests/test_util.py
4923@@ -508,4 +508,73 @@ class TestReadSeeded(helpers.TestCase):
4924 self.assertEqual(found_md, {'key1': 'val1'})
4925 self.assertEqual(found_ud, ud)
4926
4927+
4928+class TestSubp(helpers.TestCase):
4929+
4930+ stdin2err = ['bash', '-c', 'cat >&2']
4931+ stdin2out = ['cat']
4932+ utf8_invalid = b'ab\xaadef'
4933+ utf8_valid = b'start \xc3\xa9 end'
4934+ utf8_valid_2 = b'd\xc3\xa9j\xc8\xa7'
4935+
4936+ def printf_cmd(self, *args):
4937+ # bash's printf supports \xaa. So does /usr/bin/printf
4938+ # but by using bash, we remove dependency on another program.
4939+ return(['bash', '-c', 'printf "$@"', 'printf'] + list(args))
4940+
4941+ def test_subp_handles_utf8(self):
4942+ # The given bytes contain utf-8 accented characters as seen in e.g.
4943+ # the "deja dup" package in Ubuntu.
4944+ cmd = self.printf_cmd(self.utf8_valid_2)
4945+ (out, _err) = util.subp(cmd, capture=True)
4946+ self.assertEqual(out, self.utf8_valid_2.decode('utf-8'))
4947+
4948+ def test_subp_respects_decode_false(self):
4949+ (out, err) = util.subp(self.stdin2out, capture=True, decode=False,
4950+ data=self.utf8_valid)
4951+ self.assertTrue(isinstance(out, bytes))
4952+ self.assertTrue(isinstance(err, bytes))
4953+ self.assertEqual(out, self.utf8_valid)
4954+
4955+ def test_subp_decode_ignore(self):
4956+ # this executes a string that writes invalid utf-8 to stdout
4957+ (out, _err) = util.subp(self.printf_cmd('abc\\xaadef'),
4958+ capture=True, decode='ignore')
4959+ self.assertEqual(out, 'abcdef')
4960+
4961+ def test_subp_decode_strict_valid_utf8(self):
4962+ (out, _err) = util.subp(self.stdin2out, capture=True,
4963+ decode='strict', data=self.utf8_valid)
4964+ self.assertEqual(out, self.utf8_valid.decode('utf-8'))
4965+
4966+ def test_subp_decode_invalid_utf8_replaces(self):
4967+ (out, _err) = util.subp(self.stdin2out, capture=True,
4968+ data=self.utf8_invalid)
4969+ expected = self.utf8_invalid.decode('utf-8', errors='replace')
4970+ self.assertEqual(out, expected)
4971+
4972+ def test_subp_decode_strict_raises(self):
4973+ args = []
4974+ kwargs = {'args': self.stdin2out, 'capture': True,
4975+ 'decode': 'strict', 'data': self.utf8_invalid}
4976+ self.assertRaises(UnicodeDecodeError, util.subp, *args, **kwargs)
4977+
4978+ def test_subp_capture_stderr(self):
4979+ data = b'hello world'
4980+ (out, err) = util.subp(self.stdin2err, capture=True,
4981+ decode=False, data=data)
4982+ self.assertEqual(err, data)
4983+ self.assertEqual(out, b'')
4984+
4985+ def test_returns_none_if_no_capture(self):
4986+ (out, err) = util.subp(self.stdin2out, data=b'', capture=False)
4987+ self.assertEqual(err, None)
4988+ self.assertEqual(out, None)
4989+
4990+ def test_bunch_of_slashes_in_path(self):
4991+ self.assertEqual("/target/my/path/",
4992+ util.target_path("/target/", "//my/path/"))
4993+ self.assertEqual("/target/my/path/",
4994+ util.target_path("/target/", "///my/path/"))
4995+
4996 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches