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
diff --git a/cloudinit/config/cc_apt_configure.py b/cloudinit/config/cc_apt_configure.py
index 05ad4b0..609dbb5 100644
--- a/cloudinit/config/cc_apt_configure.py
+++ b/cloudinit/config/cc_apt_configure.py
@@ -23,80 +23,182 @@ import os
23import re23import re
2424
25from cloudinit import gpg25from cloudinit import gpg
26from cloudinit import log as logging
26from cloudinit import templater27from cloudinit import templater
27from cloudinit import util28from cloudinit import util
2829
29distros = ['ubuntu', 'debian']30LOG = logging.getLogger(__name__)
30
31PROXY_TPL = "Acquire::HTTP::Proxy \"%s\";\n"
32APT_CONFIG_FN = "/etc/apt/apt.conf.d/94cloud-init-config"
33APT_PROXY_FN = "/etc/apt/apt.conf.d/95cloud-init-proxy"
3431
35# this will match 'XXX:YYY' (ie, 'cloud-archive:foo' or 'ppa:bar')32# this will match 'XXX:YYY' (ie, 'cloud-archive:foo' or 'ppa:bar')
36ADD_APT_REPO_MATCH = r"^[\w-]+:\w"33ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
3734
35# place where apt stores cached repository data
36APT_LISTS = "/var/lib/apt/lists"
3837
39def handle(name, cfg, cloud, log, _args):38# Files to store proxy information
40 if util.is_false(cfg.get('apt_configure_enabled', True)):39APT_CONFIG_FN = "/etc/apt/apt.conf.d/94cloud-init-config"
41 log.debug("Skipping module named %s, disabled by config.", name)40APT_PROXY_FN = "/etc/apt/apt.conf.d/90cloud-init-aptproxy"
42 return41
4342# Default keyserver to use
44 release = get_release()43DEFAULT_KEYSERVER = "keyserver.ubuntu.com"
45 mirrors = find_apt_mirror_info(cloud, cfg)44
46 if not mirrors or "primary" not in mirrors:45# Default archive mirrors
47 log.debug(("Skipping module named %s,"46PRIMARY_ARCH_MIRRORS = {"PRIMARY": "http://archive.ubuntu.com/ubuntu/",
48 " no package 'mirror' located"), name)47 "SECURITY": "http://security.ubuntu.com/ubuntu/"}
49 return48PORTS_MIRRORS = {"PRIMARY": "http://ports.ubuntu.com/ubuntu-ports",
5049 "SECURITY": "http://ports.ubuntu.com/ubuntu-ports"}
51 # backwards compatibility50PRIMARY_ARCHES = ['amd64', 'i386']
52 mirror = mirrors["primary"]51PORTS_ARCHES = ['s390x', 'arm64', 'armhf', 'powerpc', 'ppc64el']
53 mirrors["mirror"] = mirror52
5453
55 log.debug("Mirror info: %s" % mirrors)54def get_default_mirrors(arch=None, target=None):
5655 """returns the default mirrors for the target. These depend on the
57 if not util.get_cfg_option_bool(cfg,56 architecture, for more see:
58 'apt_preserve_sources_list', False):57 https://wiki.ubuntu.com/UbuntuDevelopment/PackageArchive#Ports"""
59 generate_sources_list(cfg, release, mirrors, cloud, log)58 if arch is None:
60 old_mirrors = cfg.get('apt_old_mirrors',59 arch = util.get_architecture(target)
61 {"primary": "archive.ubuntu.com/ubuntu",60 if arch in PRIMARY_ARCHES:
62 "security": "security.ubuntu.com/ubuntu"})61 return PRIMARY_ARCH_MIRRORS.copy()
63 rename_apt_lists(old_mirrors, mirrors)62 if arch in PORTS_ARCHES:
63 return PORTS_MIRRORS.copy()
64 raise ValueError("No default mirror known for arch %s" % arch)
65
66
67def handle(name, ocfg, cloud, log, _):
68 """process the config for apt_config. This can be called from
69 curthooks if a global apt config was provided or via the "apt"
70 standalone command."""
71 # keeping code close to curtin codebase via entry handler
72 target = None
73 if log is not None:
74 global LOG
75 LOG = log
76 # feed back converted config, but only work on the subset under 'apt'
77 ocfg = convert_to_v3_apt_format(ocfg)
78 cfg = ocfg.get('apt', {})
79
80 if not isinstance(cfg, dict):
81 raise ValueError("Expected dictionary for 'apt' config, found %s",
82 type(cfg))
83
84 LOG.debug("handling apt (module %s) with apt config '%s'", name, cfg)
85
86 release = util.lsb_release(target=target)['codename']
87 arch = util.get_architecture(target)
88 mirrors = find_apt_mirror_info(cfg, cloud, arch=arch)
89 LOG.debug("Apt Mirror info: %s", mirrors)
90
91 apply_debconf_selections(cfg, target)
92
93 if util.is_false(cfg.get('preserve_sources_list', False)):
94 generate_sources_list(cfg, release, mirrors, cloud)
95 rename_apt_lists(mirrors, target)
6496
65 try:97 try:
66 apply_apt_config(cfg, APT_PROXY_FN, APT_CONFIG_FN)98 apply_apt_config(cfg, APT_PROXY_FN, APT_CONFIG_FN)
67 except Exception as e:99 except (IOError, OSError):
68 log.warn("failed to proxy or apt config info: %s", e)100 LOG.exception("Failed to apply proxy or apt config info:")
69101
70 # Process 'apt_sources'102 # Process 'apt_source -> sources {dict}'
71 if 'apt_sources' in cfg:103 if 'sources' in cfg:
72 params = mirrors104 params = mirrors
73 params['RELEASE'] = release105 params['RELEASE'] = release
74 params['MIRROR'] = mirror106 params['MIRROR'] = mirrors["MIRROR"]
75107
108 matcher = None
76 matchcfg = cfg.get('add_apt_repo_match', ADD_APT_REPO_MATCH)109 matchcfg = cfg.get('add_apt_repo_match', ADD_APT_REPO_MATCH)
77 if matchcfg:110 if matchcfg:
78 matcher = re.compile(matchcfg).search111 matcher = re.compile(matchcfg).search
112
113 add_apt_sources(cfg['sources'], cloud, target=target,
114 template_params=params, aa_repo_match=matcher)
115
116
117def debconf_set_selections(selections, target=None):
118 util.subp(['debconf-set-selections'], data=selections, target=target,
119 capture=True)
120
121
122def dpkg_reconfigure(packages, target=None):
123 # For any packages that are already installed, but have preseed data
124 # we populate the debconf database, but the filesystem configuration
125 # would be preferred on a subsequent dpkg-reconfigure.
126 # so, what we have to do is "know" information about certain packages
127 # to unconfigure them.
128 unhandled = []
129 to_config = []
130 for pkg in packages:
131 if pkg in CONFIG_CLEANERS:
132 LOG.debug("unconfiguring %s", pkg)
133 CONFIG_CLEANERS[pkg](target)
134 to_config.append(pkg)
79 else:135 else:
80 def matcher(x):136 unhandled.append(pkg)
81 return False137
138 if len(unhandled):
139 LOG.warn("The following packages were installed and preseeded, "
140 "but cannot be unconfigured: %s", unhandled)
141
142 if len(to_config):
143 util.subp(['dpkg-reconfigure', '--frontend=noninteractive'] +
144 list(to_config), data=None, target=target, capture=True)
145
146
147def apply_debconf_selections(cfg, target=None):
148 """apply_debconf_selections - push content to debconf"""
149 # debconf_selections:
150 # set1: |
151 # cloud-init cloud-init/datasources multiselect MAAS
152 # set2: pkg pkg/value string bar
153 selsets = cfg.get('debconf_selections')
154 if not selsets:
155 LOG.debug("debconf_selections was not set in config")
156 return
82157
83 errors = add_apt_sources(cfg['apt_sources'], params,158 selections = '\n'.join(
84 aa_repo_match=matcher)159 [selsets[key] for key in sorted(selsets.keys())])
85 for e in errors:160 debconf_set_selections(selections.encode() + b"\n", target=target)
86 log.warn("Add source error: %s", ':'.join(e))
87161
88 dconf_sel = util.get_cfg_option_str(cfg, 'debconf_selections', False)162 # get a complete list of packages listed in input
89 if dconf_sel:163 pkgs_cfgd = set()
90 log.debug("Setting debconf selections per cloud config")164 for key, content in selsets.items():
91 try:165 for line in content.splitlines():
92 util.subp(('debconf-set-selections', '-'), dconf_sel)166 if line.startswith("#"):
93 except Exception:167 continue
94 util.logexc(log, "Failed to run debconf-set-selections")168 pkg = re.sub(r"[:\s].*", "", line)
169 pkgs_cfgd.add(pkg)
170
171 pkgs_installed = util.get_installed_packages(target)
172
173 LOG.debug("pkgs_cfgd: %s", pkgs_cfgd)
174 need_reconfig = pkgs_cfgd.intersection(pkgs_installed)
175
176 if len(need_reconfig) == 0:
177 LOG.debug("no need for reconfig")
178 return
179
180 dpkg_reconfigure(need_reconfig, target=target)
181
182
183def clean_cloud_init(target):
184 """clean out any local cloud-init config"""
185 flist = glob.glob(
186 util.target_path(target, "/etc/cloud/cloud.cfg.d/*dpkg*"))
187
188 LOG.debug("cleaning cloud-init config from: %s", flist)
189 for dpkg_cfg in flist:
190 os.unlink(dpkg_cfg)
95191
96192
97def mirrorurl_to_apt_fileprefix(mirror):193def mirrorurl_to_apt_fileprefix(mirror):
194 """mirrorurl_to_apt_fileprefix
195 Convert a mirror url to the file prefix used by apt on disk to
196 store cache information for that mirror.
197 To do so do:
198 - take off ???://
199 - drop tailing /
200 - convert in string / to _"""
98 string = mirror201 string = mirror
99 # take off http:// or ftp://
100 if string.endswith("/"):202 if string.endswith("/"):
101 string = string[0:-1]203 string = string[0:-1]
102 pos = string.find("://")204 pos = string.find("://")
@@ -106,174 +208,365 @@ def mirrorurl_to_apt_fileprefix(mirror):
106 return string208 return string
107209
108210
109def rename_apt_lists(old_mirrors, new_mirrors, lists_d="/var/lib/apt/lists"):211def rename_apt_lists(new_mirrors, target=None):
110 for (name, omirror) in old_mirrors.items():212 """rename_apt_lists - rename apt lists to preserve old cache data"""
213 default_mirrors = get_default_mirrors(util.get_architecture(target))
214
215 pre = util.target_path(target, APT_LISTS)
216 for (name, omirror) in default_mirrors.items():
111 nmirror = new_mirrors.get(name)217 nmirror = new_mirrors.get(name)
112 if not nmirror:218 if not nmirror:
113 continue219 continue
114 oprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(omirror))220
115 nprefix = os.path.join(lists_d, mirrorurl_to_apt_fileprefix(nmirror))221 oprefix = pre + os.path.sep + mirrorurl_to_apt_fileprefix(omirror)
222 nprefix = pre + os.path.sep + mirrorurl_to_apt_fileprefix(nmirror)
116 if oprefix == nprefix:223 if oprefix == nprefix:
117 continue224 continue
118 olen = len(oprefix)225 olen = len(oprefix)
119 for filename in glob.glob("%s_*" % oprefix):226 for filename in glob.glob("%s_*" % oprefix):
120 util.rename(filename, "%s%s" % (nprefix, filename[olen:]))227 newname = "%s%s" % (nprefix, filename[olen:])
121228 LOG.debug("Renaming apt list %s to %s", filename, newname)
122229 try:
123def get_release():230 os.rename(filename, newname)
124 (stdout, _stderr) = util.subp(['lsb_release', '-cs'])231 except OSError:
125 return stdout.strip()232 # since this is a best effort task, warn with but don't fail
126233 LOG.warn("Failed to rename apt list:", exc_info=True)
127234
128def generate_sources_list(cfg, codename, mirrors, cloud, log):235
129 params = {'codename': codename}236def mirror_to_placeholder(tmpl, mirror, placeholder):
237 """mirror_to_placeholder
238 replace the specified mirror in a template with a placeholder string
239 Checks for existance of the expected mirror and warns if not found"""
240 if mirror not in tmpl:
241 LOG.warn("Expected mirror '%s' not found in: %s", mirror, tmpl)
242 return tmpl.replace(mirror, placeholder)
243
244
245def map_known_suites(suite):
246 """there are a few default names which will be auto-extended.
247 This comes at the inability to use those names literally as suites,
248 but on the other hand increases readability of the cfg quite a lot"""
249 mapping = {'updates': '$RELEASE-updates',
250 'backports': '$RELEASE-backports',
251 'security': '$RELEASE-security',
252 'proposed': '$RELEASE-proposed',
253 'release': '$RELEASE'}
254 try:
255 retsuite = mapping[suite]
256 except KeyError:
257 retsuite = suite
258 return retsuite
259
260
261def disable_suites(disabled, src, release):
262 """reads the config for suites to be disabled and removes those
263 from the template"""
264 if not disabled:
265 return src
266
267 retsrc = src
268 for suite in disabled:
269 suite = map_known_suites(suite)
270 releasesuite = templater.render_string(suite, {'RELEASE': release})
271 LOG.debug("Disabling suite %s as %s", suite, releasesuite)
272
273 newsrc = ""
274 for line in retsrc.splitlines(True):
275 if line.startswith("#"):
276 newsrc += line
277 continue
278
279 # sources.list allow options in cols[1] which can have spaces
280 # so the actual suite can be [2] or later. example:
281 # deb [ arch=amd64,armel k=v ] http://example.com/debian
282 cols = line.split()
283 if len(cols) > 1:
284 pcol = 2
285 if cols[1].startswith("["):
286 for col in cols[1:]:
287 pcol += 1
288 if col.endswith("]"):
289 break
290
291 if cols[pcol] == releasesuite:
292 line = '# suite disabled by cloud-init: %s' % line
293 newsrc += line
294 retsrc = newsrc
295
296 return retsrc
297
298
299def generate_sources_list(cfg, release, mirrors, cloud):
300 """generate_sources_list
301 create a source.list file based on a custom or default template
302 by replacing mirrors and release in the template"""
303 aptsrc = "/etc/apt/sources.list"
304 params = {'RELEASE': release, 'codename': release}
130 for k in mirrors:305 for k in mirrors:
131 params[k] = mirrors[k]306 params[k] = mirrors[k]
307 params[k.lower()] = mirrors[k]
132308
133 custtmpl = cfg.get('apt_custom_sources_list', None)309 tmpl = cfg.get('sources_list', None)
134 if custtmpl is not None:310 if tmpl is None:
135 templater.render_string_to_file(custtmpl,311 LOG.info("No custom template provided, fall back to builtin")
136 '/etc/apt/sources.list', params)312 template_fn = cloud.get_template_filename('sources.list.%s' %
137 return313 (cloud.distro.name))
138
139 template_fn = cloud.get_template_filename('sources.list.%s' %
140 (cloud.distro.name))
141 if not template_fn:
142 template_fn = cloud.get_template_filename('sources.list')
143 if not template_fn:314 if not template_fn:
144 log.warn("No template found, not rendering /etc/apt/sources.list")315 template_fn = cloud.get_template_filename('sources.list')
316 if not template_fn:
317 LOG.warn("No template found, not rendering /etc/apt/sources.list")
145 return318 return
319 tmpl = util.load_file(template_fn)
146320
147 templater.render_to_file(template_fn, '/etc/apt/sources.list', params)321 rendered = templater.render_string(tmpl, params)
322 disabled = disable_suites(cfg.get('disable_suites'), rendered, release)
323 util.write_file(aptsrc, disabled, mode=0o644)
148324
149325
150def add_apt_key_raw(key):326def add_apt_key_raw(key, target=None):
151 """327 """
152 actual adding of a key as defined in key argument328 actual adding of a key as defined in key argument
153 to the system329 to the system
154 """330 """
331 LOG.debug("Adding key:\n'%s'", key)
155 try:332 try:
156 util.subp(('apt-key', 'add', '-'), key)333 util.subp(['apt-key', 'add', '-'], data=key.encode(), target=target)
157 except util.ProcessExecutionError:334 except util.ProcessExecutionError:
158 raise ValueError('failed to add apt GPG Key to apt keyring')335 LOG.exception("failed to add apt GPG Key to apt keyring")
336 raise
159337
160338
161def add_apt_key(ent):339def add_apt_key(ent, target=None):
162 """340 """
163 add key to the system as defined in ent (if any)341 Add key to the system as defined in ent (if any).
164 supports raw keys or keyid's342 Supports raw keys or keyid's
165 The latter will as a first step fetch the raw key from a keyserver343 The latter will as a first step fetched to get the raw key
166 """344 """
167 if 'keyid' in ent and 'key' not in ent:345 if 'keyid' in ent and 'key' not in ent:
168 keyserver = "keyserver.ubuntu.com"346 keyserver = DEFAULT_KEYSERVER
169 if 'keyserver' in ent:347 if 'keyserver' in ent:
170 keyserver = ent['keyserver']348 keyserver = ent['keyserver']
171 ent['key'] = gpg.get_key_by_id(ent['keyid'], keyserver)349
350 ent['key'] = gpg.getkeybyid(ent['keyid'], keyserver)
172351
173 if 'key' in ent:352 if 'key' in ent:
174 add_apt_key_raw(ent['key'])353 add_apt_key_raw(ent['key'], target)
175354
176355
177def convert_to_new_format(srclist):356def update_packages(cloud):
178 """convert_to_new_format357 cloud.distro.update_package_sources()
179 convert the old list based format to the new dict based one
180 """
181 srcdict = {}
182 if isinstance(srclist, list):
183 for srcent in srclist:
184 if 'filename' not in srcent:
185 # file collides for multiple !filename cases for compatibility
186 # yet we need them all processed, so not same dictionary key
187 srcent['filename'] = "cloud_config_sources.list"
188 key = util.rand_dict_key(srcdict, "cloud_config_sources.list")
189 else:
190 # all with filename use that as key (matching new format)
191 key = srcent['filename']
192 srcdict[key] = srcent
193 elif isinstance(srclist, dict):
194 srcdict = srclist
195 else:
196 raise ValueError("unknown apt_sources format")
197
198 return srcdict
199358
200359
201def add_apt_sources(srclist, template_params=None, aa_repo_match=None):360def add_apt_sources(srcdict, cloud, target=None, template_params=None,
361 aa_repo_match=None):
202 """362 """
203 add entries in /etc/apt/sources.list.d for each abbreviated363 add entries in /etc/apt/sources.list.d for each abbreviated
204 sources.list entry in 'srclist'. When rendering template, also364 sources.list entry in 'srcdict'. When rendering template, also
205 include the values in dictionary searchList365 include the values in dictionary searchList
206 """366 """
207 if template_params is None:367 if template_params is None:
208 template_params = {}368 template_params = {}
209369
210 if aa_repo_match is None:370 if aa_repo_match is None:
211 def _aa_repo_match(x):371 raise ValueError('did not get a valid repo matcher')
212 return False
213 aa_repo_match = _aa_repo_match
214372
215 errorlist = []373 if not isinstance(srcdict, dict):
216 srcdict = convert_to_new_format(srclist)374 raise TypeError('unknown apt format: %s' % (srcdict))
217375
218 for filename in srcdict:376 for filename in srcdict:
219 ent = srcdict[filename]377 ent = srcdict[filename]
378 LOG.debug("adding source/key '%s'", ent)
220 if 'filename' not in ent:379 if 'filename' not in ent:
221 ent['filename'] = filename380 ent['filename'] = filename
222381
223 # keys can be added without specifying a source382 add_apt_key(ent, target)
224 try:
225 add_apt_key(ent)
226 except ValueError as detail:
227 errorlist.append([ent, detail])
228383
229 if 'source' not in ent:384 if 'source' not in ent:
230 errorlist.append(["", "missing source"])
231 continue385 continue
232 source = ent['source']386 source = ent['source']
233 source = templater.render_string(source, template_params)387 source = templater.render_string(source, template_params)
234388
235 if not ent['filename'].startswith(os.path.sep):389 if not ent['filename'].startswith("/"):
236 ent['filename'] = os.path.join("/etc/apt/sources.list.d/",390 ent['filename'] = os.path.join("/etc/apt/sources.list.d/",
237 ent['filename'])391 ent['filename'])
392 if not ent['filename'].endswith(".list"):
393 ent['filename'] += ".list"
238394
239 if aa_repo_match(source):395 if aa_repo_match(source):
240 try:396 try:
241 util.subp(["add-apt-repository", source])397 util.subp(["add-apt-repository", source], target=target)
242 except util.ProcessExecutionError as e:398 except util.ProcessExecutionError:
243 errorlist.append([source,399 LOG.exception("add-apt-repository failed.")
244 ("add-apt-repository failed. " + str(e))])400 raise
245 continue401 continue
246402
403 sourcefn = util.target_path(target, ent['filename'])
247 try:404 try:
248 contents = "%s\n" % (source)405 contents = "%s\n" % (source)
249 util.write_file(ent['filename'], contents, omode="ab")406 util.write_file(sourcefn, contents, omode="a")
250 except Exception:407 except IOError as detail:
251 errorlist.append([source,408 LOG.exception("failed write to file %s: %s", sourcefn, detail)
252 "failed write to file %s" % ent['filename']])409 raise
253410
254 return errorlist411 update_packages(cloud)
255412
413 return
256414
257def find_apt_mirror_info(cloud, cfg):
258 """find an apt_mirror given the cloud and cfg provided."""
259415
260 mirror = None416def convert_v1_to_v2_apt_format(srclist):
417 """convert v1 apt format to v2 (dict in apt_sources)"""
418 srcdict = {}
419 if isinstance(srclist, list):
420 LOG.debug("apt config: convert V1 to V2 format (source list to dict)")
421 for srcent in srclist:
422 if 'filename' not in srcent:
423 # file collides for multiple !filename cases for compatibility
424 # yet we need them all processed, so not same dictionary key
425 srcent['filename'] = "cloud_config_sources.list"
426 key = util.rand_dict_key(srcdict, "cloud_config_sources.list")
427 else:
428 # all with filename use that as key (matching new format)
429 key = srcent['filename']
430 srcdict[key] = srcent
431 elif isinstance(srclist, dict):
432 srcdict = srclist
433 else:
434 raise ValueError("unknown apt_sources format")
435
436 return srcdict
437
438
439def convert_key(oldcfg, aptcfg, oldkey, newkey):
440 """convert an old key to the new one if the old one exists
441 returns true if a key was found and converted"""
442 if oldcfg.get(oldkey, None) is not None:
443 aptcfg[newkey] = oldcfg.get(oldkey)
444 del oldcfg[oldkey]
445 return True
446 return False
447
448
449def convert_mirror(oldcfg, aptcfg):
450 """convert old apt_mirror keys into the new more advanced mirror spec"""
451 keymap = [('apt_mirror', 'uri'),
452 ('apt_mirror_search', 'search'),
453 ('apt_mirror_search_dns', 'search_dns')]
454 converted = False
455 newmcfg = {'arches': ['default']}
456 for oldkey, newkey in keymap:
457 if convert_key(oldcfg, newmcfg, oldkey, newkey):
458 converted = True
459
460 # only insert new style config if anything was converted
461 if converted:
462 aptcfg['primary'] = [newmcfg]
463
464
465def convert_v2_to_v3_apt_format(oldcfg):
466 """convert old to new keys and adapt restructured mirror spec"""
467 oldkeys = ['apt_sources', 'apt_mirror', 'apt_mirror_search',
468 'apt_mirror_search_dns', 'apt_proxy', 'apt_http_proxy',
469 'apt_ftp_proxy', 'apt_https_proxy',
470 'apt_preserve_sources_list', 'apt_custom_sources_list',
471 'add_apt_repo_match']
472 needtoconvert = []
473 for oldkey in oldkeys:
474 if oldcfg.get(oldkey, None) is not None:
475 needtoconvert.append(oldkey)
476
477 # no old config, so no new one to be created
478 if not needtoconvert:
479 return oldcfg
480 LOG.debug("apt config: convert V2 to V3 format for keys '%s'",
481 ", ".join(needtoconvert))
482
483 if oldcfg.get('apt', None) is not None:
484 msg = ("Error in apt configuration: "
485 "old and new format of apt features are mutually exclusive "
486 "('apt':'%s' vs '%s' key)" % (oldcfg.get('apt', None),
487 ", ".join(needtoconvert)))
488 LOG.error(msg)
489 raise ValueError(msg)
490
491 # create new format from old keys
492 aptcfg = {}
493
494 # renames / moves under the apt key
495 convert_key(oldcfg, aptcfg, 'add_apt_repo_match', 'add_apt_repo_match')
496 convert_key(oldcfg, aptcfg, 'apt_proxy', 'proxy')
497 convert_key(oldcfg, aptcfg, 'apt_http_proxy', 'http_proxy')
498 convert_key(oldcfg, aptcfg, 'apt_https_proxy', 'https_proxy')
499 convert_key(oldcfg, aptcfg, 'apt_ftp_proxy', 'ftp_proxy')
500 convert_key(oldcfg, aptcfg, 'apt_custom_sources_list', 'sources_list')
501 convert_key(oldcfg, aptcfg, 'apt_preserve_sources_list',
502 'preserve_sources_list')
503 # dict format not changed since v2, just renamed and moved
504 convert_key(oldcfg, aptcfg, 'apt_sources', 'sources')
505
506 convert_mirror(oldcfg, aptcfg)
507
508 for oldkey in oldkeys:
509 if oldcfg.get(oldkey, None) is not None:
510 raise ValueError("old apt key '%s' left after conversion" % oldkey)
511
512 # insert new format into config and return full cfg with only v3 content
513 oldcfg['apt'] = aptcfg
514 return oldcfg
515
516
517def convert_to_v3_apt_format(cfg):
518 """convert the old list based format to the new dict based one. After that
519 convert the old dict keys/format to v3 a.k.a 'new apt config'"""
520 # V1 -> V2, the apt_sources entry from list to dict
521 apt_sources = cfg.get('apt_sources', None)
522 if apt_sources is not None:
523 cfg['apt_sources'] = convert_v1_to_v2_apt_format(apt_sources)
524
525 # V2 -> V3, move all former globals under the "apt" key
526 # Restructure into new key names and mirror hierarchy
527 cfg = convert_v2_to_v3_apt_format(cfg)
528
529 return cfg
530
531
532def search_for_mirror(candidates):
533 """
534 Search through a list of mirror urls for one that works
535 This needs to return quickly.
536 """
537 if candidates is None:
538 return None
539
540 LOG.debug("search for mirror in candidates: '%s'", candidates)
541 for cand in candidates:
542 try:
543 if util.is_resolvable_url(cand):
544 LOG.debug("found working mirror: '%s'", cand)
545 return cand
546 except Exception:
547 pass
548 return None
261549
262 # this is less preferred way of specifying mirror preferred would be to
263 # use the distro's search or package_mirror.
264 mirror = cfg.get("apt_mirror", None)
265550
266 search = cfg.get("apt_mirror_search", None)551def search_for_mirror_dns(configured, mirrortype, cfg, cloud):
267 if not mirror and search:552 """
268 mirror = util.search_for_mirror(search)553 Try to resolve a list of predefines DNS names to pick mirrors
554 """
555 mirror = None
269556
270 if (not mirror and557 if configured:
271 util.get_cfg_option_bool(cfg, "apt_mirror_search_dns", False)):
272 mydom = ""558 mydom = ""
273 doms = []559 doms = []
274560
561 if mirrortype == "primary":
562 mirrordns = "mirror"
563 elif mirrortype == "security":
564 mirrordns = "security-mirror"
565 else:
566 raise ValueError("unknown mirror type")
567
275 # if we have a fqdn, then search its domain portion first568 # if we have a fqdn, then search its domain portion first
276 (_hostname, fqdn) = util.get_hostname_fqdn(cfg, cloud)569 (_, fqdn) = util.get_hostname_fqdn(cfg, cloud)
277 mydom = ".".join(fqdn.split(".")[1:])570 mydom = ".".join(fqdn.split(".")[1:])
278 if mydom:571 if mydom:
279 doms.append(".%s" % mydom)572 doms.append(".%s" % mydom)
@@ -282,38 +575,136 @@ def find_apt_mirror_info(cloud, cfg):
282575
283 mirror_list = []576 mirror_list = []
284 distro = cloud.distro.name577 distro = cloud.distro.name
285 mirrorfmt = "http://%s-mirror%s/%s" % (distro, "%s", distro)578 mirrorfmt = "http://%s-%s%s/%s" % (distro, mirrordns, "%s", distro)
286 for post in doms:579 for post in doms:
287 mirror_list.append(mirrorfmt % (post))580 mirror_list.append(mirrorfmt % (post))
288581
289 mirror = util.search_for_mirror(mirror_list)582 mirror = search_for_mirror(mirror_list)
583
584 return mirror
290585
586
587def update_mirror_info(pmirror, smirror, arch, cloud):
588 """sets security mirror to primary if not defined.
589 returns defaults if no mirrors are defined"""
590 if pmirror is not None:
591 if smirror is None:
592 smirror = pmirror
593 return {'PRIMARY': pmirror,
594 'SECURITY': smirror}
595
596 # None specified at all, get default mirrors from cloud
291 mirror_info = cloud.datasource.get_package_mirror_info()597 mirror_info = cloud.datasource.get_package_mirror_info()
598 if mirror_info:
599 # get_package_mirror_info() returns a dictionary with
600 # arbitrary key/value pairs including 'primary' and 'security' keys.
601 # caller expects dict with PRIMARY and SECURITY.
602 m = mirror_info.copy()
603 m['PRIMARY'] = m['primary']
604 m['SECURITY'] = m['security']
605
606 return m
607
608 # if neither apt nor cloud configured mirrors fall back to
609 return get_default_mirrors(arch)
610
611
612def get_arch_mirrorconfig(cfg, mirrortype, arch):
613 """out of a list of potential mirror configurations select
614 and return the one matching the architecture (or default)"""
615 # select the mirror specification (if-any)
616 mirror_cfg_list = cfg.get(mirrortype, None)
617 if mirror_cfg_list is None:
618 return None
619
620 # select the specification matching the target arch
621 default = None
622 for mirror_cfg_elem in mirror_cfg_list:
623 arches = mirror_cfg_elem.get("arches")
624 if arch in arches:
625 return mirror_cfg_elem
626 if "default" in arches:
627 default = mirror_cfg_elem
628 return default
629
630
631def get_mirror(cfg, mirrortype, arch, cloud):
632 """pass the three potential stages of mirror specification
633 returns None is neither of them found anything otherwise the first
634 hit is returned"""
635 mcfg = get_arch_mirrorconfig(cfg, mirrortype, arch)
636 if mcfg is None:
637 return None
638
639 # directly specified
640 mirror = mcfg.get("uri", None)
641
642 # fallback to search if specified
643 if mirror is None:
644 # list of mirrors to try to resolve
645 mirror = search_for_mirror(mcfg.get("search", None))
646
647 # fallback to search_dns if specified
648 if mirror is None:
649 # list of mirrors to try to resolve
650 mirror = search_for_mirror_dns(mcfg.get("search_dns", None),
651 mirrortype, cfg, cloud)
652
653 return mirror
654
655
656def find_apt_mirror_info(cfg, cloud, arch=None):
657 """find_apt_mirror_info
658 find an apt_mirror given the cfg provided.
659 It can check for separate config of primary and security mirrors
660 If only primary is given security is assumed to be equal to primary
661 If the generic apt_mirror is given that is defining for both
662 """
292663
293 # this is a bit strange.664 if arch is None:
294 # if mirror is set, then one of the legacy options above set it665 arch = util.get_architecture()
295 # but they do not cover security. so we need to get that from666 LOG.debug("got arch for mirror selection: %s", arch)
296 # get_package_mirror_info667 pmirror = get_mirror(cfg, "primary", arch, cloud)
297 if mirror:668 LOG.debug("got primary mirror: %s", pmirror)
298 mirror_info.update({'primary': mirror})669 smirror = get_mirror(cfg, "security", arch, cloud)
670 LOG.debug("got security mirror: %s", smirror)
671
672 mirror_info = update_mirror_info(pmirror, smirror, arch, cloud)
673
674 # less complex replacements use only MIRROR, derive from primary
675 mirror_info["MIRROR"] = mirror_info["PRIMARY"]
299676
300 return mirror_info677 return mirror_info
301678
302679
303def apply_apt_config(cfg, proxy_fname, config_fname):680def apply_apt_config(cfg, proxy_fname, config_fname):
681 """apply_apt_config
682 Applies any apt*proxy config from if specified
683 """
304 # Set up any apt proxy684 # Set up any apt proxy
305 cfgs = (('apt_proxy', 'Acquire::HTTP::Proxy "%s";'),685 cfgs = (('proxy', 'Acquire::http::Proxy "%s";'),
306 ('apt_http_proxy', 'Acquire::HTTP::Proxy "%s";'),686 ('http_proxy', 'Acquire::http::Proxy "%s";'),
307 ('apt_ftp_proxy', 'Acquire::FTP::Proxy "%s";'),687 ('ftp_proxy', 'Acquire::ftp::Proxy "%s";'),
308 ('apt_https_proxy', 'Acquire::HTTPS::Proxy "%s";'))688 ('https_proxy', 'Acquire::https::Proxy "%s";'))
309689
310 proxies = [fmt % cfg.get(name) for (name, fmt) in cfgs if cfg.get(name)]690 proxies = [fmt % cfg.get(name) for (name, fmt) in cfgs if cfg.get(name)]
311 if len(proxies):691 if len(proxies):
692 LOG.debug("write apt proxy info to %s", proxy_fname)
312 util.write_file(proxy_fname, '\n'.join(proxies) + '\n')693 util.write_file(proxy_fname, '\n'.join(proxies) + '\n')
313 elif os.path.isfile(proxy_fname):694 elif os.path.isfile(proxy_fname):
314 util.del_file(proxy_fname)695 util.del_file(proxy_fname)
696 LOG.debug("no apt proxy configured, removed %s", proxy_fname)
315697
316 if cfg.get('apt_config', None):698 if cfg.get('conf', None):
317 util.write_file(config_fname, cfg.get('apt_config'))699 LOG.debug("write apt config info to %s", config_fname)
700 util.write_file(config_fname, cfg.get('conf'))
318 elif os.path.isfile(config_fname):701 elif os.path.isfile(config_fname):
319 util.del_file(config_fname)702 util.del_file(config_fname)
703 LOG.debug("no apt config configured, removed %s", config_fname)
704
705
706CONFIG_CLEANERS = {
707 'cloud-init': clean_cloud_init,
708}
709
710# vi: ts=4 expandtab syntax=python
diff --git a/cloudinit/gpg.py b/cloudinit/gpg.py
index 6a76d78..5bbff51 100644
--- a/cloudinit/gpg.py
+++ b/cloudinit/gpg.py
@@ -36,11 +36,11 @@ def export_armour(key):
36 return armour36 return armour
3737
3838
39def receive_key(key, keyserver):39def recv_key(key, keyserver):
40 """Receive gpg key from the specified keyserver"""40 """Receive gpg key from the specified keyserver"""
41 LOG.debug('Receive gpg key "%s"', key)41 LOG.debug('Receive gpg key "%s"', key)
42 try:42 try:
43 util.subp(["gpg", "--keyserver", keyserver, "--recv-keys", key],43 util.subp(["gpg", "--keyserver", keyserver, "--recv", key],
44 capture=True)44 capture=True)
45 except util.ProcessExecutionError as error:45 except util.ProcessExecutionError as error:
46 raise ValueError(('Failed to import key "%s" '46 raise ValueError(('Failed to import key "%s" '
@@ -57,12 +57,12 @@ def delete_key(key):
57 LOG.warn('Failed delete key "%s": %s', key, error)57 LOG.warn('Failed delete key "%s": %s', key, error)
5858
5959
60def get_key_by_id(keyid, keyserver="keyserver.ubuntu.com"):60def getkeybyid(keyid, keyserver='keyserver.ubuntu.com'):
61 """get gpg keyid from keyserver"""61 """get gpg keyid from keyserver"""
62 armour = export_armour(keyid)62 armour = export_armour(keyid)
63 if not armour:63 if not armour:
64 try:64 try:
65 receive_key(keyid, keyserver=keyserver)65 recv_key(keyid, keyserver=keyserver)
66 armour = export_armour(keyid)66 armour = export_armour(keyid)
67 except ValueError:67 except ValueError:
68 LOG.exception('Failed to obtain gpg key %s', keyid)68 LOG.exception('Failed to obtain gpg key %s', keyid)
diff --git a/cloudinit/util.py b/cloudinit/util.py
index 226628c..db80ca9 100644
--- a/cloudinit/util.py
+++ b/cloudinit/util.py
@@ -61,6 +61,10 @@ from cloudinit import version
6161
62from cloudinit.settings import (CFG_BUILTIN)62from cloudinit.settings import (CFG_BUILTIN)
6363
64try:
65 string_types = (basestring,)
66except NameError:
67 string_types = (str,)
6468
65_DNS_REDIRECT_IP = None69_DNS_REDIRECT_IP = None
66LOG = logging.getLogger(__name__)70LOG = logging.getLogger(__name__)
@@ -82,6 +86,71 @@ CONTAINER_TESTS = (['systemd-detect-virt', '--quiet', '--container'],
8286
83PROC_CMDLINE = None87PROC_CMDLINE = None
8488
89_LSB_RELEASE = {}
90
91
92def get_architecture(target=None):
93 out, _ = subp(['dpkg', '--print-architecture'], capture=True,
94 target=target)
95 return out.strip()
96
97
98def _lsb_release(target=None):
99 fmap = {'Codename': 'codename', 'Description': 'description',
100 'Distributor ID': 'id', 'Release': 'release'}
101
102 data = {}
103 try:
104 out, _ = subp(['lsb_release', '--all'], capture=True, target=target)
105 for line in out.splitlines():
106 fname, _, val = line.partition(":")
107 if fname in fmap:
108 data[fmap[fname]] = val.strip()
109 missing = [k for k in fmap.values() if k not in data]
110 if len(missing):
111 LOG.warn("Missing fields in lsb_release --all output: %s",
112 ','.join(missing))
113
114 except ProcessExecutionError as err:
115 LOG.warn("Unable to get lsb_release --all: %s", err)
116 data = {v: "UNAVAILABLE" for v in fmap.values()}
117
118 return data
119
120
121def lsb_release(target=None):
122 if target_path(target) != "/":
123 # do not use or update cache if target is provided
124 return _lsb_release(target)
125
126 global _LSB_RELEASE
127 if not _LSB_RELEASE:
128 data = _lsb_release()
129 _LSB_RELEASE.update(data)
130 return _LSB_RELEASE
131
132
133def target_path(target, path=None):
134 # return 'path' inside target, accepting target as None
135 if target in (None, ""):
136 target = "/"
137 elif not isinstance(target, string_types):
138 raise ValueError("Unexpected input for target: %s" % target)
139 else:
140 target = os.path.abspath(target)
141 # abspath("//") returns "//" specifically for 2 slashes.
142 if target.startswith("//"):
143 target = target[1:]
144
145 if not path:
146 return target
147
148 # os.path.join("/etc", "/foo") returns "/foo". Chomp all leading /.
149 while len(path) and path[0] == "/":
150 path = path[1:]
151
152 return os.path.join(target, path)
153
85154
86def decode_binary(blob, encoding='utf-8'):155def decode_binary(blob, encoding='utf-8'):
87 # Converts a binary type into a text type using given encoding.156 # Converts a binary type into a text type using given encoding.
@@ -1688,10 +1757,20 @@ def delete_dir_contents(dirname):
16881757
16891758
1690def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,1759def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1691 logstring=False):1760 logstring=False, decode="replace", target=None):
1761
1762 # not supported in cloud-init (yet), for now kept in the call signature
1763 # to ease maintaining code shared between cloud-init and curtin
1764 if target is not None:
1765 raise ValueError("target arg not supported by cloud-init")
1766
1692 if rcs is None:1767 if rcs is None:
1693 rcs = [0]1768 rcs = [0]
1769
1770 devnull_fp = None
1694 try:1771 try:
1772 if target_path(target) != "/":
1773 args = ['chroot', target] + list(args)
16951774
1696 if not logstring:1775 if not logstring:
1697 LOG.debug(("Running command %s with allowed return codes %s"1776 LOG.debug(("Running command %s with allowed return codes %s"
@@ -1700,33 +1779,52 @@ def subp(args, data=None, rcs=None, env=None, capture=True, shell=False,
1700 LOG.debug(("Running hidden command to protect sensitive "1779 LOG.debug(("Running hidden command to protect sensitive "
1701 "input/output logstring: %s"), logstring)1780 "input/output logstring: %s"), logstring)
17021781
1703 if not capture:1782 stdin = None
1704 stdout = None1783 stdout = None
1705 stderr = None1784 stderr = None
1706 else:1785 if capture:
1707 stdout = subprocess.PIPE1786 stdout = subprocess.PIPE
1708 stderr = subprocess.PIPE1787 stderr = subprocess.PIPE
1709 stdin = subprocess.PIPE1788 if data is None:
1710 kws = dict(stdout=stdout, stderr=stderr, stdin=stdin,1789 # using devnull assures any reads get null, rather
1711 env=env, shell=shell)1790 # than possibly waiting on input.
1712 if six.PY3:1791 devnull_fp = open(os.devnull)
1713 # Use this so subprocess output will be (Python 3) str, not bytes.1792 stdin = devnull_fp
1714 kws['universal_newlines'] = True1793 else:
1715 sp = subprocess.Popen(args, **kws)1794 stdin = subprocess.PIPE
1795 if not isinstance(data, bytes):
1796 data = data.encode()
1797
1798 sp = subprocess.Popen(args, stdout=stdout,
1799 stderr=stderr, stdin=stdin,
1800 env=env, shell=shell)
1716 (out, err) = sp.communicate(data)1801 (out, err) = sp.communicate(data)
1802
1803 # Just ensure blank instead of none.
1804 if not out and capture:
1805 out = b''
1806 if not err and capture:
1807 err = b''
1808 if decode:
1809 def ldecode(data, m='utf-8'):
1810 if not isinstance(data, bytes):
1811 return data
1812 return data.decode(m, errors=decode)
1813
1814 out = ldecode(out)
1815 err = ldecode(err)
1717 except OSError as e:1816 except OSError as e:
1718 raise ProcessExecutionError(cmd=args, reason=e,1817 raise ProcessExecutionError(cmd=args, reason=e,
1719 errno=e.errno)1818 errno=e.errno)
1819 finally:
1820 if devnull_fp:
1821 devnull_fp.close()
1822
1720 rc = sp.returncode1823 rc = sp.returncode
1721 if rc not in rcs:1824 if rc not in rcs:
1722 raise ProcessExecutionError(stdout=out, stderr=err,1825 raise ProcessExecutionError(stdout=out, stderr=err,
1723 exit_code=rc,1826 exit_code=rc,
1724 cmd=args)1827 cmd=args)
1725 # Just ensure blank instead of none?? (iff capturing)
1726 if not out and capture:
1727 out = ''
1728 if not err and capture:
1729 err = ''
1730 return (out, err)1828 return (out, err)
17311829
17321830
@@ -2251,3 +2349,18 @@ def message_from_string(string):
2251 if sys.version_info[:2] < (2, 7):2349 if sys.version_info[:2] < (2, 7):
2252 return email.message_from_file(six.StringIO(string))2350 return email.message_from_file(six.StringIO(string))
2253 return email.message_from_string(string)2351 return email.message_from_string(string)
2352
2353
2354def get_installed_packages(target=None):
2355 (out, _) = subp(['dpkg-query', '--list'], target=target, capture=True)
2356
2357 pkgs_inst = set()
2358 for line in out.splitlines():
2359 try:
2360 (state, pkg, _) = line.split(None, 2)
2361 except ValueError:
2362 continue
2363 if state.startswith("hi") or state.startswith("ii"):
2364 pkgs_inst.add(re.sub(":.*", "", pkg))
2365
2366 return pkgs_inst
diff --git a/doc/examples/cloud-config-add-apt-repos.txt b/doc/examples/cloud-config-add-apt-repos.txt
index be9d547..22ef761 100644
--- a/doc/examples/cloud-config-add-apt-repos.txt
+++ b/doc/examples/cloud-config-add-apt-repos.txt
@@ -4,18 +4,21 @@
4#4#
5# Default: auto select based on cloud metadata5# Default: auto select based on cloud metadata
6# in ec2, the default is <region>.archive.ubuntu.com6# in ec2, the default is <region>.archive.ubuntu.com
7# apt_mirror:7# apt:
8# use the provided mirror8# primary:
9# apt_mirror_search:9# - arches [default]
10# search the list for the first mirror.10# uri:
11# this is currently very limited, only verifying that11# use the provided mirror
12# the mirror is dns resolvable or an IP address12# search:
13# search the list for the first mirror.
14# this is currently very limited, only verifying that
15# the mirror is dns resolvable or an IP address
13#16#
14# if neither apt_mirror nor apt_mirror search is set (the default)17# if neither mirror is set (the default)
15# then use the mirror provided by the DataSource found.18# then use the mirror provided by the DataSource found.
16# In EC2, that means using <region>.ec2.archive.ubuntu.com19# In EC2, that means using <region>.ec2.archive.ubuntu.com
17# 20#
18# if no mirror is provided by the DataSource, and 'apt_mirror_search_dns' is21# if no mirror is provided by the DataSource, but 'search_dns' is
19# true, then search for dns names '<distro>-mirror' in each of22# true, then search for dns names '<distro>-mirror' in each of
20# - fqdn of this host per cloud metadata23# - fqdn of this host per cloud metadata
21# - localdomain24# - localdomain
@@ -27,8 +30,19 @@
27# up and expose them only by creating dns entries.30# up and expose them only by creating dns entries.
28#31#
29# if none of that is found, then the default distro mirror is used32# if none of that is found, then the default distro mirror is used
30apt_mirror: http://us.archive.ubuntu.com/ubuntu/33apt:
31apt_mirror_search: 34 primary:
32 - http://local-mirror.mydomain35 - arches: [default]
33 - http://archive.ubuntu.com36 uri: http://us.archive.ubuntu.com/ubuntu/
34apt_mirror_search_dns: False37# or
38apt:
39 primary:
40 - arches: [default]
41 search:
42 - http://local-mirror.mydomain
43 - http://archive.ubuntu.com
44# or
45apt:
46 primary:
47 - arches: [default]
48 search_dns: True
diff --git a/doc/examples/cloud-config-apt.txt b/doc/examples/cloud-config-apt.txt
35new file mode 10064449new file mode 100644
index 0000000..1a0fc6f
--- /dev/null
+++ b/doc/examples/cloud-config-apt.txt
@@ -0,0 +1,328 @@
1# apt_pipelining (configure Acquire::http::Pipeline-Depth)
2# Default: disables HTTP pipelining. Certain web servers, such
3# as S3 do not pipeline properly (LP: #948461).
4# Valid options:
5# False/default: Disables pipelining for APT
6# None/Unchanged: Use OS default
7# Number: Set pipelining to some number (not recommended)
8apt_pipelining: False
9
10## apt config via system_info:
11# under the 'system_info', you can customize cloud-init's interaction
12# with apt.
13# system_info:
14# apt_get_command: [command, argument, argument]
15# apt_get_upgrade_subcommand: dist-upgrade
16#
17# apt_get_command:
18# To specify a different 'apt-get' command, set 'apt_get_command'.
19# This must be a list, and the subcommand (update, upgrade) is appended to it.
20# default is:
21# ['apt-get', '--option=Dpkg::Options::=--force-confold',
22# '--option=Dpkg::options::=--force-unsafe-io', '--assume-yes', '--quiet']
23#
24# apt_get_upgrade_subcommand: "dist-upgrade"
25# Specify a different subcommand for 'upgrade. The default is 'dist-upgrade'.
26# This is the subcommand that is invoked for package_upgrade.
27#
28# apt_get_wrapper:
29# command: eatmydata
30# enabled: [True, False, "auto"]
31#
32
33# Install additional packages on first boot
34#
35# Default: none
36#
37# if packages are specified, this apt_update will be set to true
38
39packages: ['pastebinit']
40
41apt:
42 # The apt config consists of two major "areas".
43 #
44 # On one hand there is the global configuration for the apt feature.
45 #
46 # On one hand (down in this file) there is the source dictionary which allows
47 # to define various entries to be considered by apt.
48
49 ##############################################################################
50 # Section 1: global apt configuration
51 #
52 # The following examples number the top keys to ease identification in
53 # discussions.
54
55 # 1.1 preserve_sources_list
56 #
57 # Preserves the existing /etc/apt/sources.list
58 # Default: false - do overwrite sources_list. If set to true then any
59 # "mirrors" configuration will have no effect.
60 # Set to true to avoid affecting sources.list. In that case only
61 # "extra" source specifications will be written into
62 # /etc/apt/sources.list.d/*
63 preserve_sources_list: true
64
65 # 1.2 disable_suites
66 #
67 # This is an empty list by default, so nothing is disabled.
68 #
69 # If given, those suites are removed from sources.list after all other
70 # modifications have been made.
71 # Suites are even disabled if no other modification was made,
72 # but not if is preserve_sources_list is active.
73 # There is a special alias “$RELEASE” as in the sources that will be replace
74 # by the matching release.
75 #
76 # To ease configuration and improve readability the following common ubuntu
77 # suites will be automatically mapped to their full definition.
78 # updates => $RELEASE-updates
79 # backports => $RELEASE-backports
80 # security => $RELEASE-security
81 # proposed => $RELEASE-proposed
82 # release => $RELEASE
83 #
84 # There is no harm in specifying a suite to be disabled that is not found in
85 # the source.list file (just a no-op then)
86 #
87 # Note: Lines don’t get deleted, but disabled by being converted to a comment.
88 # The following example disables all usual defaults except $RELEASE-security.
89 # On top it disables a custom suite called "mysuite"
90 disable_suites: [$RELEASE-updates, backports, $RELEASE, mysuite]
91
92 # 1.3 primary/security archives
93 #
94 # Default: none - instead it is auto select based on cloud metadata
95 # so if neither "uri" nor "search", nor "search_dns" is set (the default)
96 # then use the mirror provided by the DataSource found.
97 # In EC2, that means using <region>.ec2.archive.ubuntu.com
98 #
99 # define a custom (e.g. localized) mirror that will be used in sources.list
100 # and any custom sources entries for deb / deb-src lines.
101 #
102 # One can set primary and security mirror to different uri's
103 # the child elements to the keys primary and secondary are equivalent
104 primary:
105 # arches is list of architectures the following config applies to
106 # the special keyword "default" applies to any architecture not explicitly
107 # listed.
108 - arches: [amd64, i386, default]
109 # uri is just defining the target as-is
110 uri: http://us.archive.ubuntu.com/ubuntu
111 #
112 # via search one can define lists that are tried one by one.
113 # The first with a working DNS resolution (or if it is an IP) will be
114 # picked. That way one can keep one configuration for multiple
115 # subenvironments that select the working one.
116 search:
117 - http://cool.but-sometimes-unreachable.com/ubuntu
118 - http://us.archive.ubuntu.com/ubuntu
119 # if no mirror is provided by uri or search but 'search_dns' is
120 # true, then search for dns names '<distro>-mirror' in each of
121 # - fqdn of this host per cloud metadata
122 # - localdomain
123 # - no domain (which would search domains listed in /etc/resolv.conf)
124 # If there is a dns entry for <distro>-mirror, then it is assumed that
125 # there is a distro mirror at http://<distro>-mirror.<domain>/<distro>
126 #
127 # That gives the cloud provider the opportunity to set mirrors of a distro
128 # up and expose them only by creating dns entries.
129 #
130 # if none of that is found, then the default distro mirror is used
131 search_dns: true
132 #
133 # If multiple of a category are given
134 # 1. uri
135 # 2. search
136 # 3. search_dns
137 # the first defining a valid mirror wins (in the order as defined here,
138 # not the order as listed in the config).
139 #
140 - arches: [s390x, arm64]
141 # as above, allowing to have one config for different per arch mirrors
142 # security is optional, if not defined it is set to the same value as primary
143 security:
144 uri: http://security.ubuntu.com/ubuntu
145 # If search_dns is set for security the searched pattern is:
146 # <distro>-security-mirror
147
148 # if no mirrors are specified at all, or all lookups fail it will try
149 # to get them from the cloud datasource and if those neither provide one fall
150 # back to:
151 # primary: http://archive.ubuntu.com/ubuntu
152 # security: http://security.ubuntu.com/ubuntu
153
154 # 1.4 sources_list
155 #
156 # Provide a custom template for rendering sources.list
157 # without one provided cloud-init uses builtin templates for
158 # ubuntu and debian.
159 # Within these sources.list templates you can use the following replacement
160 # variables (all have sane Ubuntu defaults, but mirrors can be overwritten
161 # as needed (see above)):
162 # => $RELEASE, $MIRROR, $PRIMARY, $SECURITY
163 sources_list: | # written by cloud-init custom template
164 deb $MIRROR $RELEASE main restricted
165 deb-src $MIRROR $RELEASE main restricted
166 deb $PRIMARY $RELEASE universe restricted
167 deb $SECURITY $RELEASE-security multiverse
168
169 # 1.5 conf
170 #
171 # Any apt config string that will be made available to apt
172 # see the APT.CONF(5) man page for details what can be specified
173 conf: | # APT config
174 APT {
175 Get {
176 Assume-Yes "true";
177 Fix-Broken "true";
178 };
179 };
180
181 # 1.6 (http_|ftp_|https_)proxy
182 #
183 # Proxies are the most common apt.conf option, so that for simplified use
184 # there is a shortcut for those. Those get automatically translated into the
185 # correct Acquire::*::Proxy statements.
186 #
187 # note: proxy actually being a short synonym to http_proxy
188 proxy: http://[[user][:pass]@]host[:port]/
189 http_proxy: http://[[user][:pass]@]host[:port]/
190 ftp_proxy: ftp://[[user][:pass]@]host[:port]/
191 https_proxy: https://[[user][:pass]@]host[:port]/
192
193 # 1.7 add_apt_repo_match
194 #
195 # 'source' entries in apt-sources that match this python regex
196 # expression will be passed to add-apt-repository
197 # The following example is also the builtin default if nothing is specified
198 add_apt_repo_match: '^[\w-]+:\w'
199
200
201 ##############################################################################
202 # Section 2: source list entries
203 #
204 # This is a dictionary (unlike most block/net which are lists)
205 #
206 # The key of each source entry is the filename and will be prepended by
207 # /etc/apt/sources.list.d/ if it doesn't start with a '/'.
208 # If it doesn't end with .list it will be appended so that apt picks up it's
209 # configuration.
210 #
211 # Whenever there is no content to be written into such a file, the key is
212 # not used as filename - yet it can still be used as index for merging
213 # configuration.
214 #
215 # The values inside the entries consost of the following optional entries:
216 # 'source': a sources.list entry (some variable replacements apply)
217 # 'keyid': providing a key to import via shortid or fingerprint
218 # 'key': providing a raw PGP key
219 # 'keyserver': specify an alternate keyserver to pull keys from that
220 # were specified by keyid
221
222 # This allows merging between multiple input files than a list like:
223 # cloud-config1
224 # sources:
225 # s1: {'key': 'key1', 'source': 'source1'}
226 # cloud-config2
227 # sources:
228 # s2: {'key': 'key2'}
229 # s1: {'keyserver': 'foo'}
230 # This would be merged to
231 # sources:
232 # s1:
233 # keyserver: foo
234 # key: key1
235 # source: source1
236 # s2:
237 # key: key2
238 #
239 # The following examples number the subfeatures per sources entry to ease
240 # identification in discussions.
241
242
243 sources:
244 curtin-dev-ppa.list:
245 # 2.1 source
246 #
247 # Creates a file in /etc/apt/sources.list.d/ for the sources list entry
248 # based on the key: "/etc/apt/sources.list.d/curtin-dev-ppa.list"
249 source: "deb http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu xenial main"
250
251 # 2.2 keyid
252 #
253 # Importing a gpg key for a given key id. Used keyserver defaults to
254 # keyserver.ubuntu.com
255 keyid: F430BBA5 # GPG key ID published on a key server
256
257 ignored1:
258 # 2.3 PPA shortcut
259 #
260 # Setup correct apt sources.list line and Auto-Import the signing key
261 # from LP
262 #
263 # See https://help.launchpad.net/Packaging/PPA for more information
264 # this requires 'add-apt-repository'. This will create a file in
265 # /etc/apt/sources.list.d automatically, therefore the key here is
266 # ignored as filename in those cases.
267 source: "ppa:curtin-dev/test-archive" # Quote the string
268
269 my-repo2.list:
270 # 2.4 replacement variables
271 #
272 # sources can use $MIRROR, $PRIMARY, $SECURITY and $RELEASE replacement
273 # variables.
274 # They will be replaced with the default or specified mirrors and the
275 # running release.
276 # The entry below would be possibly turned into:
277 # source: deb http://archive.ubuntu.com/ubuntu xenial multiverse
278 source: deb $MIRROR $RELEASE multiverse
279
280 my-repo3.list:
281 # this would have the same end effect as 'ppa:curtin-dev/test-archive'
282 source: "deb http://ppa.launchpad.net/curtin-dev/test-archive/ubuntu xenial main"
283 keyid: F430BBA5 # GPG key ID published on the key server
284 filename: curtin-dev-ppa.list
285
286 ignored2:
287 # 2.5 key only
288 #
289 # this would only import the key without adding a ppa or other source spec
290 # since this doesn't generate a source.list file the filename key is ignored
291 keyid: F430BBA5 # GPG key ID published on a key server
292
293 ignored3:
294 # 2.6 key id alternatives
295 #
296 # Keyid's can also be specified via their long fingerprints
297 keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
298
299 ignored4:
300 # 2.7 alternative keyservers
301 #
302 # One can also specify alternative keyservers to fetch keys from.
303 keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
304 keyserver: pgp.mit.edu
305
306
307 my-repo4.list:
308 # 2.8 raw key
309 #
310 # The apt signing key can also be specified by providing a pgp public key
311 # block. Providing the PGP key this way is the most robust method for
312 # specifying a key, as it removes dependency on a remote key server.
313 #
314 # As with keyid's this can be specified with or without some actual source
315 # content.
316 key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
317 -----BEGIN PGP PUBLIC KEY BLOCK-----
318 Version: SKS 1.0.10
319
320 mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
321 qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
322 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
323 IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
324 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
325 t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
326 uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
327 =Y2oI
328 -----END PGP PUBLIC KEY BLOCK-----
diff --git a/doc/examples/cloud-config-chef-oneiric.txt b/doc/examples/cloud-config-chef-oneiric.txt
index 2e5f4b1..75c9aee 100644
--- a/doc/examples/cloud-config-chef-oneiric.txt
+++ b/doc/examples/cloud-config-chef-oneiric.txt
@@ -11,39 +11,40 @@
11# The default is to install from packages. 11# The default is to install from packages.
1212
13# Key from http://apt.opscode.com/packages@opscode.com.gpg.key13# Key from http://apt.opscode.com/packages@opscode.com.gpg.key
14apt_sources:14apt:
15 - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"15 sources:
16 key: |16 - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
17 -----BEGIN PGP PUBLIC KEY BLOCK-----17 key: |
18 Version: GnuPG v1.4.9 (GNU/Linux)18 -----BEGIN PGP PUBLIC KEY BLOCK-----
19 19 Version: GnuPG v1.4.9 (GNU/Linux)
20 mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu20
21 twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn9921 mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
22 dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC22 twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
23 JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W23 dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
24 ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I24 JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
25 XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe25 ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
26 DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm26 XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
27 sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO27 DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
28 Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ28 sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
29 YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG29 Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
30 CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K30 YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
31 +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR31 CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
32 lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh32 +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
33 DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu33 lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
34 wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx34 DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
35 EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g35 wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
36 w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W836 EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
37 AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN37 w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
38 QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X38 AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
39 Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ39 QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
40 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V40 Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
41 Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL41 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
42 zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb42 Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
43 DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG43 zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
44 0GLl8EkfA8uhluM=44 DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
45 =zKAm45 0GLl8EkfA8uhluM=
46 -----END PGP PUBLIC KEY BLOCK-----46 =zKAm
47 -----END PGP PUBLIC KEY BLOCK-----
4748
48chef:49chef:
4950
diff --git a/doc/examples/cloud-config-chef.txt b/doc/examples/cloud-config-chef.txt
index b886cba..75d78a1 100644
--- a/doc/examples/cloud-config-chef.txt
+++ b/doc/examples/cloud-config-chef.txt
@@ -11,39 +11,40 @@
11# The default is to install from packages. 11# The default is to install from packages.
1212
13# Key from http://apt.opscode.com/packages@opscode.com.gpg.key13# Key from http://apt.opscode.com/packages@opscode.com.gpg.key
14apt_sources:14apt:
15 - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"15 sources:
16 key: |16 - source: "deb http://apt.opscode.com/ $RELEASE-0.10 main"
17 -----BEGIN PGP PUBLIC KEY BLOCK-----17 key: |
18 Version: GnuPG v1.4.9 (GNU/Linux)18 -----BEGIN PGP PUBLIC KEY BLOCK-----
19 19 Version: GnuPG v1.4.9 (GNU/Linux)
20 mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu20
21 twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn9921 mQGiBEppC7QRBADfsOkZU6KZK+YmKw4wev5mjKJEkVGlus+NxW8wItX5sGa6kdUu
22 dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC22 twAyj7Yr92rF+ICFEP3gGU6+lGo0Nve7KxkN/1W7/m3G4zuk+ccIKmjp8KS3qn99
23 JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W23 dxy64vcji9jIllVa+XXOGIp0G8GEaj7mbkixL/bMeGfdMlv8Gf2XPpp9vwCgn/GC
24 ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I24 JKacfnw7MpLKUHOYSlb//JsEAJqao3ViNfav83jJKEkD8cf59Y8xKia5OpZqTK5W
25 XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe25 ShVnNWS3U5IVQk10ZDH97Qn/YrK387H4CyhLE9mxPXs/ul18ioiaars/q2MEKU2I
26 DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm26 XKfV21eMLO9LYd6Ny/Kqj8o5WQK2J6+NAhSwvthZcIEphcFignIuobP+B5wNFQpe
27 sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO27 DbKfA/0WvN2OwFeWRcmmd3Hz7nHTpcnSF+4QX6yHRF/5BgxkG6IqBIACQbzPn6Hm
28 Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ28 sMtm/SVf11izmDqSsQptCrOZILfLX/mE+YOl+CwWSHhl+YsFts1WOuh1EhQD26aO
29 YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG29 Z84HuHV5HFRWjDLw9LriltBVQcXbpfSrRP5bdr7Wh8vhqJTPjrQnT3BzY29kZSBQ
30 CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K30 YWNrYWdlcyA8cGFja2FnZXNAb3BzY29kZS5jb20+iGAEExECACAFAkppC7QCGwMG
31 +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR31 CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRApQKupg++Caj8sAKCOXmdG36gWji/K
32 lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh32 +o+XtBfvdMnFYQCfTCEWxRy2BnzLoBBFCjDSK6sJqCu5Ag0ESmkLtBAIAIO2SwlR
33 DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu33 lU5i6gTOp42RHWW7/pmW78CwUqJnYqnXROrt3h9F9xrsGkH0Fh1FRtsnncgzIhvh
34 wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx34 DLQnRHnkXm0ws0jV0PF74ttoUT6BLAUsFi2SPP1zYNJ9H9fhhK/pjijtAcQwdgxu
35 EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g35 wwNJ5xCEscBZCjhSRXm0d30bK1o49Cow8ZIbHtnXVP41c9QWOzX/LaGZsKQZnaMx
36 w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W836 EzDk8dyyctR2f03vRSVyTFGgdpUcpbr9eTFVgikCa6ODEBv+0BnCH6yGTXwBid9g
37 AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN37 w0o1e/2DviKUWCC+AlAUOubLmOIGFBuI4UR+rux9affbHcLIOTiKQXv79lW3P7W8
38 QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X38 AAfniSQKfPWXrrcAAwUH/2XBqD4Uxhbs25HDUUiM/m6Gnlj6EsStg8n0nMggLhuN
39 Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ39 QmPfoNByMPUqvA7sULyfr6xCYzbzRNxABHSpf85FzGQ29RF4xsA4vOOU8RDIYQ9X
40 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V40 Q8NqqR6pydprRFqWe47hsAN7BoYuhWqTtOLSBmnAnzTR5pURoqcquWYiiEavZixJ
41 Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL41 3ZRAq/HMGioJEtMFrvsZjGXuzef7f0ytfR1zYeLVWnL9Bd32CueBlI7dhYwkFe+V
42 zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb42 Ep5jWOCj02C1wHcwt+uIRDJV6TdtbIiBYAdOMPk15+VBdweBXwMuYXr76+A7VeDL
43 DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG43 zIhi7tKFo6WiwjKZq0dzctsJJjtIfr4K4vbiD9Ojg1iISQQYEQIACQUCSmkLtAIb
44 0GLl8EkfA8uhluM=44 DAAKCRApQKupg++CauISAJ9CxYPOKhOxalBnVTLeNUkAHGg2gACeIsbobtaD4ZHG
45 =zKAm45 0GLl8EkfA8uhluM=
46 -----END PGP PUBLIC KEY BLOCK-----46 =zKAm
47 -----END PGP PUBLIC KEY BLOCK-----
4748
48chef:49chef:
4950
diff --git a/doc/examples/cloud-config.txt b/doc/examples/cloud-config.txt
index 3cc9c05..190029e 100644
--- a/doc/examples/cloud-config.txt
+++ b/doc/examples/cloud-config.txt
@@ -18,256 +18,7 @@ package_upgrade: true
18# Aliases: apt_reboot_if_required18# Aliases: apt_reboot_if_required
19package_reboot_if_required: true19package_reboot_if_required: true
2020
21# Add apt repositories21# For 'apt' specific config, see cloud-config-apt.txt
22#
23# Default: auto select based on cloud metadata
24# in ec2, the default is <region>.archive.ubuntu.com
25# apt_mirror:
26# use the provided mirror
27# apt_mirror_search:
28# search the list for the first mirror.
29# this is currently very limited, only verifying that
30# the mirror is dns resolvable or an IP address
31#
32# if neither apt_mirror nor apt_mirror search is set (the default)
33# then use the mirror provided by the DataSource found.
34# In EC2, that means using <region>.ec2.archive.ubuntu.com
35#
36# if no mirror is provided by the DataSource, and 'apt_mirror_search_dns' is
37# true, then search for dns names '<distro>-mirror' in each of
38# - fqdn of this host per cloud metadata
39# - localdomain
40# - no domain (which would search domains listed in /etc/resolv.conf)
41# If there is a dns entry for <distro>-mirror, then it is assumed that there
42# is a distro mirror at http://<distro>-mirror.<domain>/<distro>
43#
44# That gives the cloud provider the opportunity to set mirrors of a distro
45# up and expose them only by creating dns entries.
46#
47# if none of that is found, then the default distro mirror is used
48apt_mirror: http://us.archive.ubuntu.com/ubuntu/
49apt_mirror_search:
50 - http://local-mirror.mydomain
51 - http://archive.ubuntu.com
52
53apt_mirror_search_dns: False
54
55# apt_proxy (configure Acquire::HTTP::Proxy)
56# 'apt_http_proxy' is an alias for 'apt_proxy'.
57# Also, available are 'apt_ftp_proxy' and 'apt_https_proxy'.
58# These affect Acquire::FTP::Proxy and Acquire::HTTPS::Proxy respectively
59apt_proxy: http://my.apt.proxy:3128
60
61# apt_pipelining (configure Acquire::http::Pipeline-Depth)
62# Default: disables HTTP pipelining. Certain web servers, such
63# as S3 do not pipeline properly (LP: #948461).
64# Valid options:
65# False/default: Disables pipelining for APT
66# None/Unchanged: Use OS default
67# Number: Set pipelining to some number (not recommended)
68apt_pipelining: False
69
70# Preserve existing /etc/apt/sources.list
71# Default: overwrite sources_list with mirror. If this is true
72# then apt_mirror above will have no effect
73apt_preserve_sources_list: true
74
75# Provide a custom template for rendering sources.list
76# Default: a default template for Ubuntu/Debain will be used as packaged in
77# Ubuntu: /etc/cloud/templates/sources.list.ubuntu.tmpl
78# Debian: /etc/cloud/templates/sources.list.debian.tmpl
79# Others: n/a
80# This will follow the normal mirror/codename replacement rules before
81# being written to disk.
82apt_custom_sources_list: |
83 ## template:jinja
84 ## Note, this file is written by cloud-init on first boot of an instance
85 ## modifications made here will not survive a re-bundle.
86 ## if you wish to make changes you can:
87 ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
88 ## or do the same in user-data
89 ## b.) add sources in /etc/apt/sources.list.d
90 ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
91 deb {{mirror}} {{codename}} main restricted
92 deb-src {{mirror}} {{codename}} main restricted
93
94 # could drop some of the usually used entries
95
96 # could refer to other mirrors
97 deb http://ddebs.ubuntu.com {{codename}} main restricted universe multiverse
98 deb http://ddebs.ubuntu.com {{codename}}-updates main restricted universe multiverse
99 deb http://ddebs.ubuntu.com {{codename}}-proposed main restricted universe multiverse
100
101 # or even more uncommon examples like local or NFS mounted repos,
102 # eventually whatever is compatible with sources.list syntax
103 deb file:/home/apt/debian unstable main contrib non-free
104
105# 'source' entries in apt-sources that match this python regex
106# expression will be passed to add-apt-repository
107add_apt_repo_match: '^[\w-]+:\w'
108
109# 'apt_sources' is a dictionary
110# The key is the filename and will be prepended by /etc/apt/sources.list.d/ if
111# it doesn't start with a '/'.
112# There are certain cases - where no content is written into a source.list file
113# where the filename will be ignored - yet it can still be used as index for
114# merging.
115# The value it maps to is a dictionary with the following optional entries:
116# source: a sources.list entry (some variable replacements apply)
117# keyid: providing a key to import via shortid or fingerprint
118# key: providing a raw PGP key
119# keyserver: keyserver to fetch keys from, default is keyserver.ubuntu.com
120# filename: for compatibility with the older format (now the key to this
121# dictionary is the filename). If specified this overwrites the
122# filename given as key.
123
124# the new "filename: {specification-dictionary}, filename2: ..." format allows
125# better merging between multiple input files than a list like:
126# cloud-config1
127# sources:
128# s1: {'key': 'key1', 'source': 'source1'}
129# cloud-config2
130# sources:
131# s2: {'key': 'key2'}
132# s1: {filename: 'foo'}
133# this would be merged to
134#sources:
135# s1:
136# filename: foo
137# key: key1
138# source: source1
139# s2:
140# key: key2
141# Be aware that this style of merging is not the default (for backward
142# compatibility reasons). You should specify the following merge_how to get
143# this more complete and modern merging behaviour:
144# merge_how: "list()+dict()+str()"
145# This would then also be equivalent to the config merging used in curtin
146# (https://launchpad.net/curtin).
147
148# for more details see below in the various examples
149
150apt_sources:
151 byobu-ppa.list:
152 source: "deb http://ppa.launchpad.net/byobu/ppa/ubuntu karmic main"
153 keyid: F430BBA5 # GPG key ID published on a key server
154 # adding a source.list line, importing a gpg key for a given key id and
155 # storing it in the file /etc/apt/sources.list.d/byobu-ppa.list
156
157 # PPA shortcut:
158 # * Setup correct apt sources.list line
159 # * Import the signing key from LP
160 #
161 # See https://help.launchpad.net/Packaging/PPA for more information
162 # this requires 'add-apt-repository'
163 # due to that the filename key is ignored in this case
164 ignored1:
165 source: "ppa:smoser/ppa" # Quote the string
166
167 # Custom apt repository:
168 # * all that is required is 'source'
169 # * Creates a file in /etc/apt/sources.list.d/ for the sources list entry
170 # * [optional] Import the apt signing key from the keyserver
171 # * Defaults:
172 # + keyserver: keyserver.ubuntu.com
173 #
174 # See sources.list man page for more information about the format
175 my-repo.list:
176 source: deb http://archive.ubuntu.com/ubuntu karmic-backports main universe multiverse restricted
177
178 # sources can use $MIRROR and $RELEASE and they will be replaced
179 # with the local mirror for this cloud, and the running release
180 # the entry below would be possibly turned into:
181 # source: deb http://us-east-1.ec2.archive.ubuntu.com/ubuntu natty multiverse
182 my-repo.list:
183 source: deb $MIRROR $RELEASE multiverse
184
185 # this would have the same end effect as 'ppa:byobu/ppa'
186 my-repo.list:
187 source: "deb http://ppa.launchpad.net/byobu/ppa/ubuntu karmic main"
188 keyid: F430BBA5 # GPG key ID published on a key server
189 filename: byobu-ppa.list
190
191 # this would only import the key without adding a ppa or other source spec
192 # since this doesn't generate a source.list file the filename key is ignored
193 ignored2:
194 keyid: F430BBA5 # GPG key ID published on a key server
195
196 # In general keyid's can also be specified via their long fingerprints
197 # since this doesn't generate a source.list file the filename key is ignored
198 ignored3:
199 keyid: B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77
200
201 # Custom apt repository:
202 # * The apt signing key can also be specified
203 # by providing a pgp public key block
204 # * Providing the PGP key here is the most robust method for
205 # specifying a key, as it removes dependency on a remote key server
206 my-repo.list:
207 source: deb http://ppa.launchpad.net/alestic/ppa/ubuntu karmic main
208 key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
209 -----BEGIN PGP PUBLIC KEY BLOCK-----
210 Version: SKS 1.0.10
211
212 mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
213 qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
214 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
215 IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
216 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
217 t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
218 uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
219 =Y2oI
220 -----END PGP PUBLIC KEY BLOCK-----
221
222 # Custom gpg key:
223 # * As with keyid, a key may also be specified without a related source.
224 # * all other facts mentioned above still apply
225 # since this doesn't generate a source.list file the filename key is ignored
226 ignored4:
227 key: | # The value needs to start with -----BEGIN PGP PUBLIC KEY BLOCK-----
228 -----BEGIN PGP PUBLIC KEY BLOCK-----
229 Version: SKS 1.0.10
230
231 mI0ESpA3UQEEALdZKVIMq0j6qWAXAyxSlF63SvPVIgxHPb9Nk0DZUixn+akqytxG4zKCONz6
232 qLjoBBfHnynyVLfT4ihg9an1PqxRnTO+JKQxl8NgKGz6Pon569GtAOdWNKw15XKinJTDLjnj
233 9y96ljJqRcpV9t/WsIcdJPcKFR5voHTEoABE2aEXABEBAAG0GUxhdW5jaHBhZCBQUEEgZm9y
234 IEFsZXN0aWOItgQTAQIAIAUCSpA3UQIbAwYLCQgHAwIEFQIIAwQWAgMBAh4BAheAAAoJEA7H
235 5Qi+CcVxWZ8D/1MyYvfj3FJPZUm2Yo1zZsQ657vHI9+pPouqflWOayRR9jbiyUFIn0VdQBrP
236 t0FwvnOFArUovUWoKAEdqR8hPy3M3APUZjl5K4cMZR/xaMQeQRZ5CHpS4DBKURKAHC0ltS5o
237 uBJKQOZm5iltJp15cgyIkBkGe8Mx18VFyVglAZey
238 =Y2oI
239 -----END PGP PUBLIC KEY BLOCK-----
240
241
242## apt config via system_info:
243# under the 'system_info', you can further customize cloud-init's interaction
244# with apt.
245# system_info:
246# apt_get_command: [command, argument, argument]
247# apt_get_upgrade_subcommand: dist-upgrade
248#
249# apt_get_command:
250# To specify a different 'apt-get' command, set 'apt_get_command'.
251# This must be a list, and the subcommand (update, upgrade) is appended to it.
252# default is:
253# ['apt-get', '--option=Dpkg::Options::=--force-confold',
254# '--option=Dpkg::options::=--force-unsafe-io', '--assume-yes', '--quiet']
255#
256# apt_get_upgrade_subcommand:
257# Specify a different subcommand for 'upgrade. The default is 'dist-upgrade'.
258# This is the subcommand that is invoked if package_upgrade is set to true above.
259#
260# apt_get_wrapper:
261# command: eatmydata
262# enabled: [True, False, "auto"]
263#
264
265# Install additional packages on first boot
266#
267# Default: none
268#
269# if packages are specified, this apt_update will be set to true
270#
271packages:22packages:
272 - pwgen23 - pwgen
273 - pastebinit24 - pastebinit
diff --git a/tests/configs/sample1.yaml b/tests/configs/sample1.yaml
index 6231f29..ae935cc 100644
--- a/tests/configs/sample1.yaml
+++ b/tests/configs/sample1.yaml
@@ -3,9 +3,6 @@
3#apt_upgrade: true3#apt_upgrade: true
4packages: [ bzr, pastebinit, ubuntu-dev-tools, ccache, bzr-builddeb, vim-nox, git-core, lftp ]4packages: [ bzr, pastebinit, ubuntu-dev-tools, ccache, bzr-builddeb, vim-nox, git-core, lftp ]
55
6#apt_sources:
7# - source: ppa:smoser/ppa
8
9#disable_root: False6#disable_root: False
107
11# mounts:8# mounts:
diff --git a/tests/unittests/test_distros/test_generic.py b/tests/unittests/test_distros/test_generic.py
index 96fa081..24ad115 100644
--- a/tests/unittests/test_distros/test_generic.py
+++ b/tests/unittests/test_distros/test_generic.py
@@ -226,8 +226,5 @@ class TestGenericDistro(helpers.FilesystemMockingTestCase):
226 os.symlink('/', '/run/systemd/system')226 os.symlink('/', '/run/systemd/system')
227 self.assertFalse(d.uses_systemd())227 self.assertFalse(d.uses_systemd())
228228
229# def _get_package_mirror_info(mirror_info, availability_zone=None,
230# mirror_filter=util.search_for_mirror):
231
232229
233# vi: ts=4 expandtab230# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_apt_conf_v1.py b/tests/unittests/test_handler/test_handler_apt_conf_v1.py
234new file mode 100644231new file mode 100644
index 0000000..95fd1da
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_apt_conf_v1.py
@@ -0,0 +1,109 @@
1from cloudinit.config import cc_apt_configure
2from cloudinit import util
3
4from ..helpers import TestCase
5
6import os
7import re
8import shutil
9import tempfile
10
11
12def load_tfile_or_url(*args, **kwargs):
13 return(util.decode_binary(util.read_file_or_url(*args, **kwargs).contents))
14
15
16class TestAptProxyConfig(TestCase):
17 def setUp(self):
18 super(TestAptProxyConfig, self).setUp()
19 self.tmp = tempfile.mkdtemp()
20 self.addCleanup(shutil.rmtree, self.tmp)
21 self.pfile = os.path.join(self.tmp, "proxy.cfg")
22 self.cfile = os.path.join(self.tmp, "config.cfg")
23
24 def _search_apt_config(self, contents, ptype, value):
25 return re.search(
26 r"acquire::%s::proxy\s+[\"']%s[\"'];\n" % (ptype, value),
27 contents, flags=re.IGNORECASE)
28
29 def test_apt_proxy_written(self):
30 cfg = {'proxy': 'myproxy'}
31 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
32
33 self.assertTrue(os.path.isfile(self.pfile))
34 self.assertFalse(os.path.isfile(self.cfile))
35
36 contents = load_tfile_or_url(self.pfile)
37 self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
38
39 def test_apt_http_proxy_written(self):
40 cfg = {'http_proxy': 'myproxy'}
41 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
42
43 self.assertTrue(os.path.isfile(self.pfile))
44 self.assertFalse(os.path.isfile(self.cfile))
45
46 contents = load_tfile_or_url(self.pfile)
47 self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
48
49 def test_apt_all_proxy_written(self):
50 cfg = {'http_proxy': 'myproxy_http_proxy',
51 'https_proxy': 'myproxy_https_proxy',
52 'ftp_proxy': 'myproxy_ftp_proxy'}
53
54 values = {'http': cfg['http_proxy'],
55 'https': cfg['https_proxy'],
56 'ftp': cfg['ftp_proxy'],
57 }
58
59 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
60
61 self.assertTrue(os.path.isfile(self.pfile))
62 self.assertFalse(os.path.isfile(self.cfile))
63
64 contents = load_tfile_or_url(self.pfile)
65
66 for ptype, pval in values.items():
67 self.assertTrue(self._search_apt_config(contents, ptype, pval))
68
69 def test_proxy_deleted(self):
70 util.write_file(self.cfile, "content doesnt matter")
71 cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
72 self.assertFalse(os.path.isfile(self.pfile))
73 self.assertFalse(os.path.isfile(self.cfile))
74
75 def test_proxy_replaced(self):
76 util.write_file(self.cfile, "content doesnt matter")
77 cc_apt_configure.apply_apt_config({'proxy': "foo"},
78 self.pfile, self.cfile)
79 self.assertTrue(os.path.isfile(self.pfile))
80 contents = load_tfile_or_url(self.pfile)
81 self.assertTrue(self._search_apt_config(contents, "http", "foo"))
82
83 def test_config_written(self):
84 payload = 'this is my apt config'
85 cfg = {'conf': payload}
86
87 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
88
89 self.assertTrue(os.path.isfile(self.cfile))
90 self.assertFalse(os.path.isfile(self.pfile))
91
92 self.assertEqual(load_tfile_or_url(self.cfile), payload)
93
94 def test_config_replaced(self):
95 util.write_file(self.pfile, "content doesnt matter")
96 cc_apt_configure.apply_apt_config({'conf': "foo"},
97 self.pfile, self.cfile)
98 self.assertTrue(os.path.isfile(self.cfile))
99 self.assertEqual(load_tfile_or_url(self.cfile), "foo")
100
101 def test_config_deleted(self):
102 # if no 'conf' is provided, delete any previously written file
103 util.write_file(self.pfile, "content doesnt matter")
104 cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
105 self.assertFalse(os.path.isfile(self.pfile))
106 self.assertFalse(os.path.isfile(self.cfile))
107
108
109# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_apt_configure.py b/tests/unittests/test_handler/test_handler_apt_configure.py
0deleted file mode 100644110deleted file mode 100644
index d1dca2c..0000000
--- a/tests/unittests/test_handler/test_handler_apt_configure.py
+++ /dev/null
@@ -1,109 +0,0 @@
1from cloudinit.config import cc_apt_configure
2from cloudinit import util
3
4from ..helpers import TestCase
5
6import os
7import re
8import shutil
9import tempfile
10
11
12def load_tfile_or_url(*args, **kwargs):
13 return(util.decode_binary(util.read_file_or_url(*args, **kwargs).contents))
14
15
16class TestAptProxyConfig(TestCase):
17 def setUp(self):
18 super(TestAptProxyConfig, self).setUp()
19 self.tmp = tempfile.mkdtemp()
20 self.addCleanup(shutil.rmtree, self.tmp)
21 self.pfile = os.path.join(self.tmp, "proxy.cfg")
22 self.cfile = os.path.join(self.tmp, "config.cfg")
23
24 def _search_apt_config(self, contents, ptype, value):
25 return re.search(
26 r"acquire::%s::proxy\s+[\"']%s[\"'];\n" % (ptype, value),
27 contents, flags=re.IGNORECASE)
28
29 def test_apt_proxy_written(self):
30 cfg = {'apt_proxy': 'myproxy'}
31 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
32
33 self.assertTrue(os.path.isfile(self.pfile))
34 self.assertFalse(os.path.isfile(self.cfile))
35
36 contents = load_tfile_or_url(self.pfile)
37 self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
38
39 def test_apt_http_proxy_written(self):
40 cfg = {'apt_http_proxy': 'myproxy'}
41 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
42
43 self.assertTrue(os.path.isfile(self.pfile))
44 self.assertFalse(os.path.isfile(self.cfile))
45
46 contents = load_tfile_or_url(self.pfile)
47 self.assertTrue(self._search_apt_config(contents, "http", "myproxy"))
48
49 def test_apt_all_proxy_written(self):
50 cfg = {'apt_http_proxy': 'myproxy_http_proxy',
51 'apt_https_proxy': 'myproxy_https_proxy',
52 'apt_ftp_proxy': 'myproxy_ftp_proxy'}
53
54 values = {'http': cfg['apt_http_proxy'],
55 'https': cfg['apt_https_proxy'],
56 'ftp': cfg['apt_ftp_proxy'],
57 }
58
59 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
60
61 self.assertTrue(os.path.isfile(self.pfile))
62 self.assertFalse(os.path.isfile(self.cfile))
63
64 contents = load_tfile_or_url(self.pfile)
65
66 for ptype, pval in values.items():
67 self.assertTrue(self._search_apt_config(contents, ptype, pval))
68
69 def test_proxy_deleted(self):
70 util.write_file(self.cfile, "content doesnt matter")
71 cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
72 self.assertFalse(os.path.isfile(self.pfile))
73 self.assertFalse(os.path.isfile(self.cfile))
74
75 def test_proxy_replaced(self):
76 util.write_file(self.cfile, "content doesnt matter")
77 cc_apt_configure.apply_apt_config({'apt_proxy': "foo"},
78 self.pfile, self.cfile)
79 self.assertTrue(os.path.isfile(self.pfile))
80 contents = load_tfile_or_url(self.pfile)
81 self.assertTrue(self._search_apt_config(contents, "http", "foo"))
82
83 def test_config_written(self):
84 payload = 'this is my apt config'
85 cfg = {'apt_config': payload}
86
87 cc_apt_configure.apply_apt_config(cfg, self.pfile, self.cfile)
88
89 self.assertTrue(os.path.isfile(self.cfile))
90 self.assertFalse(os.path.isfile(self.pfile))
91
92 self.assertEqual(load_tfile_or_url(self.cfile), payload)
93
94 def test_config_replaced(self):
95 util.write_file(self.pfile, "content doesnt matter")
96 cc_apt_configure.apply_apt_config({'apt_config': "foo"},
97 self.pfile, self.cfile)
98 self.assertTrue(os.path.isfile(self.cfile))
99 self.assertEqual(load_tfile_or_url(self.cfile), "foo")
100
101 def test_config_deleted(self):
102 # if no 'apt_config' is provided, delete any previously written file
103 util.write_file(self.pfile, "content doesnt matter")
104 cc_apt_configure.apply_apt_config({}, self.pfile, self.cfile)
105 self.assertFalse(os.path.isfile(self.pfile))
106 self.assertFalse(os.path.isfile(self.cfile))
107
108
109# vi: ts=4 expandtab
diff --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
110deleted file mode 1006440deleted file mode 100644
index acde086..0000000
--- a/tests/unittests/test_handler/test_handler_apt_configure_sources_list.py
+++ /dev/null
@@ -1,180 +0,0 @@
1""" test_handler_apt_configure_sources_list
2Test templating of sources list
3"""
4import logging
5import os
6import shutil
7import tempfile
8
9try:
10 from unittest import mock
11except ImportError:
12 import mock
13
14from cloudinit import cloud
15from cloudinit import distros
16from cloudinit import helpers
17from cloudinit import templater
18from cloudinit import util
19
20from cloudinit.config import cc_apt_configure
21from cloudinit.sources import DataSourceNone
22
23from cloudinit.distros.debian import Distro
24
25from .. import helpers as t_help
26
27LOG = logging.getLogger(__name__)
28
29YAML_TEXT_CUSTOM_SL = """
30apt_mirror: http://archive.ubuntu.com/ubuntu/
31apt_custom_sources_list: |
32 ## template:jinja
33 ## Note, this file is written by cloud-init on first boot of an instance
34 ## modifications made here will not survive a re-bundle.
35 ## if you wish to make changes you can:
36 ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
37 ## or do the same in user-data
38 ## b.) add sources in /etc/apt/sources.list.d
39 ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
40
41 # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
42 # newer versions of the distribution.
43 deb {{mirror}} {{codename}} main restricted
44 deb-src {{mirror}} {{codename}} main restricted
45 # FIND_SOMETHING_SPECIAL
46"""
47
48EXPECTED_CONVERTED_CONTENT = (
49 """## Note, this file is written by cloud-init on first boot of an instance
50## modifications made here will not survive a re-bundle.
51## if you wish to make changes you can:
52## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
53## or do the same in user-data
54## b.) add sources in /etc/apt/sources.list.d
55## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
56
57# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
58# newer versions of the distribution.
59deb http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
60deb-src http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
61# FIND_SOMETHING_SPECIAL
62""")
63
64
65def load_tfile_or_url(*args, **kwargs):
66 """load_tfile_or_url
67 load file and return content after decoding
68 """
69 return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
70
71
72class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
73 """TestAptSourceConfigSourceList
74 Main Class to test sources list rendering
75 """
76 def setUp(self):
77 super(TestAptSourceConfigSourceList, self).setUp()
78 self.subp = util.subp
79 self.new_root = tempfile.mkdtemp()
80 self.addCleanup(shutil.rmtree, self.new_root)
81
82 def _get_cloud(self, distro, metadata=None):
83 self.patchUtils(self.new_root)
84 paths = helpers.Paths({})
85 cls = distros.fetch(distro)
86 mydist = cls(distro, {}, paths)
87 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
88 if metadata:
89 myds.metadata.update(metadata)
90 return cloud.Cloud(myds, paths, {}, mydist, None)
91
92 def apt_source_list(self, distro, mirror, mirrorcheck=None):
93 """apt_source_list
94 Test rendering of a source.list from template for a given distro
95 """
96 if mirrorcheck is None:
97 mirrorcheck = mirror
98
99 if isinstance(mirror, list):
100 cfg = {'apt_mirror_search': mirror}
101 else:
102 cfg = {'apt_mirror': mirror}
103 mycloud = self._get_cloud(distro)
104
105 with mock.patch.object(templater, 'render_to_file') as mocktmpl:
106 with mock.patch.object(os.path, 'isfile',
107 return_value=True) as mockisfile:
108 with mock.patch.object(util, 'rename'):
109 cc_apt_configure.handle("notimportant", cfg, mycloud,
110 LOG, None)
111
112 mockisfile.assert_any_call(
113 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
114 mocktmpl.assert_called_once_with(
115 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro),
116 '/etc/apt/sources.list',
117 {'codename': '', 'primary': mirrorcheck, 'mirror': mirrorcheck})
118
119 def test_apt_source_list_debian(self):
120 """Test rendering of a source.list from template for debian"""
121 self.apt_source_list('debian', 'http://httpredir.debian.org/debian')
122
123 def test_apt_source_list_ubuntu(self):
124 """Test rendering of a source.list from template for ubuntu"""
125 self.apt_source_list('ubuntu', 'http://archive.ubuntu.com/ubuntu/')
126
127 @staticmethod
128 def myresolve(name):
129 """Fake util.is_resolvable for mirrorfail tests"""
130 if name == "does.not.exist":
131 print("Faking FAIL for '%s'" % name)
132 return False
133 else:
134 print("Faking SUCCESS for '%s'" % name)
135 return True
136
137 def test_apt_srcl_debian_mirrorfail(self):
138 """Test rendering of a source.list from template for debian"""
139 with mock.patch.object(util, 'is_resolvable',
140 side_effect=self.myresolve) as mockresolve:
141 self.apt_source_list('debian',
142 ['http://does.not.exist',
143 'http://httpredir.debian.org/debian'],
144 'http://httpredir.debian.org/debian')
145 mockresolve.assert_any_call("does.not.exist")
146 mockresolve.assert_any_call("httpredir.debian.org")
147
148 def test_apt_srcl_ubuntu_mirrorfail(self):
149 """Test rendering of a source.list from template for ubuntu"""
150 with mock.patch.object(util, 'is_resolvable',
151 side_effect=self.myresolve) as mockresolve:
152 self.apt_source_list('ubuntu',
153 ['http://does.not.exist',
154 'http://archive.ubuntu.com/ubuntu/'],
155 'http://archive.ubuntu.com/ubuntu/')
156 mockresolve.assert_any_call("does.not.exist")
157 mockresolve.assert_any_call("archive.ubuntu.com")
158
159 def test_apt_srcl_custom(self):
160 """Test rendering from a custom source.list template"""
161 cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
162 mycloud = self._get_cloud('ubuntu')
163
164 # the second mock restores the original subp
165 with mock.patch.object(util, 'write_file') as mockwrite:
166 with mock.patch.object(util, 'subp', self.subp):
167 with mock.patch.object(cc_apt_configure, 'get_release',
168 return_value='fakerelease'):
169 with mock.patch.object(Distro, 'get_primary_arch',
170 return_value='amd64'):
171 cc_apt_configure.handle("notimportant", cfg, mycloud,
172 LOG, None)
173
174 mockwrite.assert_called_once_with(
175 '/etc/apt/sources.list',
176 EXPECTED_CONVERTED_CONTENT,
177 mode=420)
178
179
180# vi: ts=4 expandtab
diff --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
181new file mode 1006440new file mode 100644
index 0000000..f441186
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v1.py
@@ -0,0 +1,200 @@
1""" test_handler_apt_configure_sources_list
2Test templating of sources list
3"""
4import logging
5import os
6import shutil
7import tempfile
8
9try:
10 from unittest import mock
11except ImportError:
12 import mock
13
14from cloudinit import cloud
15from cloudinit import distros
16from cloudinit import helpers
17from cloudinit import templater
18from cloudinit import util
19
20from cloudinit.config import cc_apt_configure
21from cloudinit.sources import DataSourceNone
22
23from cloudinit.distros.debian import Distro
24
25from .. import helpers as t_help
26
27LOG = logging.getLogger(__name__)
28
29YAML_TEXT_CUSTOM_SL = """
30apt_mirror: http://archive.ubuntu.com/ubuntu/
31apt_custom_sources_list: |
32 ## template:jinja
33 ## Note, this file is written by cloud-init on first boot of an instance
34 ## modifications made here will not survive a re-bundle.
35 ## if you wish to make changes you can:
36 ## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
37 ## or do the same in user-data
38 ## b.) add sources in /etc/apt/sources.list.d
39 ## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
40
41 # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
42 # newer versions of the distribution.
43 deb {{mirror}} {{codename}} main restricted
44 deb-src {{mirror}} {{codename}} main restricted
45 # FIND_SOMETHING_SPECIAL
46"""
47
48EXPECTED_CONVERTED_CONTENT = (
49 """## Note, this file is written by cloud-init on first boot of an instance
50## modifications made here will not survive a re-bundle.
51## if you wish to make changes you can:
52## a.) add 'apt_preserve_sources_list: true' to /etc/cloud/cloud.cfg
53## or do the same in user-data
54## b.) add sources in /etc/apt/sources.list.d
55## c.) make changes to template file /etc/cloud/templates/sources.list.tmpl
56
57# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
58# newer versions of the distribution.
59deb http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
60deb-src http://archive.ubuntu.com/ubuntu/ fakerelease main restricted
61# FIND_SOMETHING_SPECIAL
62""")
63
64
65def load_tfile_or_url(*args, **kwargs):
66 """load_tfile_or_url
67 load file and return content after decoding
68 """
69 return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
70
71
72class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
73 """TestAptSourceConfigSourceList
74 Main Class to test sources list rendering
75 """
76 def setUp(self):
77 super(TestAptSourceConfigSourceList, self).setUp()
78 self.subp = util.subp
79 self.new_root = tempfile.mkdtemp()
80 self.addCleanup(shutil.rmtree, self.new_root)
81
82 rpatcher = mock.patch("cloudinit.util.lsb_release")
83 get_rel = rpatcher.start()
84 get_rel.return_value = {'codename': "fakerelease"}
85 self.addCleanup(rpatcher.stop)
86 apatcher = mock.patch("cloudinit.util.get_architecture")
87 get_arch = apatcher.start()
88 get_arch.return_value = 'amd64'
89 self.addCleanup(apatcher.stop)
90
91 def _get_cloud(self, distro, metadata=None):
92 self.patchUtils(self.new_root)
93 paths = helpers.Paths({})
94 cls = distros.fetch(distro)
95 mydist = cls(distro, {}, paths)
96 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
97 if metadata:
98 myds.metadata.update(metadata)
99 return cloud.Cloud(myds, paths, {}, mydist, None)
100
101 def apt_source_list(self, distro, mirror, mirrorcheck=None):
102 """apt_source_list
103 Test rendering of a source.list from template for a given distro
104 """
105 if mirrorcheck is None:
106 mirrorcheck = mirror
107
108 if isinstance(mirror, list):
109 cfg = {'apt_mirror_search': mirror}
110 else:
111 cfg = {'apt_mirror': mirror}
112 mycloud = self._get_cloud(distro)
113
114 with mock.patch.object(util, 'write_file') as mockwf:
115 with mock.patch.object(util, 'load_file',
116 return_value="faketmpl") as mocklf:
117 with mock.patch.object(os.path, 'isfile',
118 return_value=True) as mockisfile:
119 with mock.patch.object(templater, 'render_string',
120 return_value="fake") as mockrnd:
121 with mock.patch.object(util, 'rename'):
122 cc_apt_configure.handle("test", cfg, mycloud,
123 LOG, None)
124
125 mockisfile.assert_any_call(
126 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
127 mocklf.assert_any_call(
128 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
129 mockrnd.assert_called_once_with('faketmpl',
130 {'RELEASE': 'fakerelease',
131 'PRIMARY': mirrorcheck,
132 'MIRROR': mirrorcheck,
133 'SECURITY': mirrorcheck,
134 'codename': 'fakerelease',
135 'primary': mirrorcheck,
136 'mirror': mirrorcheck,
137 'security': mirrorcheck})
138 mockwf.assert_called_once_with('/etc/apt/sources.list', 'fake',
139 mode=0o644)
140
141 def test_apt_v1_source_list_debian(self):
142 """Test rendering of a source.list from template for debian"""
143 self.apt_source_list('debian', 'http://httpredir.debian.org/debian')
144
145 def test_apt_v1_source_list_ubuntu(self):
146 """Test rendering of a source.list from template for ubuntu"""
147 self.apt_source_list('ubuntu', 'http://archive.ubuntu.com/ubuntu/')
148
149 @staticmethod
150 def myresolve(name):
151 """Fake util.is_resolvable for mirrorfail tests"""
152 if name == "does.not.exist":
153 print("Faking FAIL for '%s'" % name)
154 return False
155 else:
156 print("Faking SUCCESS for '%s'" % name)
157 return True
158
159 def test_apt_v1_srcl_debian_mirrorfail(self):
160 """Test rendering of a source.list from template for debian"""
161 with mock.patch.object(util, 'is_resolvable',
162 side_effect=self.myresolve) as mockresolve:
163 self.apt_source_list('debian',
164 ['http://does.not.exist',
165 'http://httpredir.debian.org/debian'],
166 'http://httpredir.debian.org/debian')
167 mockresolve.assert_any_call("does.not.exist")
168 mockresolve.assert_any_call("httpredir.debian.org")
169
170 def test_apt_v1_srcl_ubuntu_mirrorfail(self):
171 """Test rendering of a source.list from template for ubuntu"""
172 with mock.patch.object(util, 'is_resolvable',
173 side_effect=self.myresolve) as mockresolve:
174 self.apt_source_list('ubuntu',
175 ['http://does.not.exist',
176 'http://archive.ubuntu.com/ubuntu/'],
177 'http://archive.ubuntu.com/ubuntu/')
178 mockresolve.assert_any_call("does.not.exist")
179 mockresolve.assert_any_call("archive.ubuntu.com")
180
181 def test_apt_v1_srcl_custom(self):
182 """Test rendering from a custom source.list template"""
183 cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
184 mycloud = self._get_cloud('ubuntu')
185
186 # the second mock restores the original subp
187 with mock.patch.object(util, 'write_file') as mockwrite:
188 with mock.patch.object(util, 'subp', self.subp):
189 with mock.patch.object(Distro, 'get_primary_arch',
190 return_value='amd64'):
191 cc_apt_configure.handle("notimportant", cfg, mycloud,
192 LOG, None)
193
194 mockwrite.assert_called_once_with(
195 '/etc/apt/sources.list',
196 EXPECTED_CONVERTED_CONTENT,
197 mode=420)
198
199
200# vi: ts=4 expandtab
diff --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
0new file mode 100644201new file mode 100644
index 0000000..e53b045
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_apt_configure_sources_list_v3.py
@@ -0,0 +1,187 @@
1""" test_apt_custom_sources_list
2Test templating of custom sources list
3"""
4import logging
5import os
6import shutil
7import tempfile
8
9try:
10 from unittest import mock
11except ImportError:
12 import mock
13from mock import call
14
15from cloudinit import cloud
16from cloudinit import distros
17from cloudinit import helpers
18from cloudinit import util
19
20from cloudinit.config import cc_apt_configure
21from cloudinit.sources import DataSourceNone
22
23from cloudinit.distros.debian import Distro
24
25from .. import helpers as t_help
26
27LOG = logging.getLogger(__name__)
28
29TARGET = "/"
30
31# Input and expected output for the custom template
32YAML_TEXT_CUSTOM_SL = """
33apt:
34 primary:
35 - arches: [default]
36 uri: http://test.ubuntu.com/ubuntu/
37 security:
38 - arches: [default]
39 uri: http://testsec.ubuntu.com/ubuntu/
40 sources_list: |
41
42 # Note, this file is written by cloud-init at install time. It should not
43 # end up on the installed system itself.
44 # See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
45 # newer versions of the distribution.
46 deb $MIRROR $RELEASE main restricted
47 deb-src $MIRROR $RELEASE main restricted
48 deb $PRIMARY $RELEASE universe restricted
49 deb $SECURITY $RELEASE-security multiverse
50 # FIND_SOMETHING_SPECIAL
51"""
52
53EXPECTED_CONVERTED_CONTENT = """
54# Note, this file is written by cloud-init at install time. It should not
55# end up on the installed system itself.
56# See http://help.ubuntu.com/community/UpgradeNotes for how to upgrade to
57# newer versions of the distribution.
58deb http://test.ubuntu.com/ubuntu/ fakerel main restricted
59deb-src http://test.ubuntu.com/ubuntu/ fakerel main restricted
60deb http://test.ubuntu.com/ubuntu/ fakerel universe restricted
61deb http://testsec.ubuntu.com/ubuntu/ fakerel-security multiverse
62# FIND_SOMETHING_SPECIAL
63"""
64
65# mocked to be independent to the unittest system
66MOCKED_APT_SRC_LIST = """
67deb http://test.ubuntu.com/ubuntu/ notouched main restricted
68deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
69deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
70deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
71"""
72
73EXPECTED_BASE_CONTENT = ("""
74deb http://test.ubuntu.com/ubuntu/ notouched main restricted
75deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
76deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
77deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
78""")
79
80EXPECTED_MIRROR_CONTENT = ("""
81deb http://test.ubuntu.com/ubuntu/ notouched main restricted
82deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
83deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
84deb http://test.ubuntu.com/ubuntu/ notouched-security main restricted
85""")
86
87EXPECTED_PRIMSEC_CONTENT = ("""
88deb http://test.ubuntu.com/ubuntu/ notouched main restricted
89deb-src http://test.ubuntu.com/ubuntu/ notouched main restricted
90deb http://test.ubuntu.com/ubuntu/ notouched-updates main restricted
91deb http://testsec.ubuntu.com/ubuntu/ notouched-security main restricted
92""")
93
94
95class TestAptSourceConfigSourceList(t_help.FilesystemMockingTestCase):
96 """TestAptSourceConfigSourceList - Class to test sources list rendering"""
97 def setUp(self):
98 super(TestAptSourceConfigSourceList, self).setUp()
99 self.subp = util.subp
100 self.new_root = tempfile.mkdtemp()
101 self.addCleanup(shutil.rmtree, self.new_root)
102
103 rpatcher = mock.patch("cloudinit.util.lsb_release")
104 get_rel = rpatcher.start()
105 get_rel.return_value = {'codename': "fakerel"}
106 self.addCleanup(rpatcher.stop)
107 apatcher = mock.patch("cloudinit.util.get_architecture")
108 get_arch = apatcher.start()
109 get_arch.return_value = 'amd64'
110 self.addCleanup(apatcher.stop)
111
112 def _get_cloud(self, distro, metadata=None):
113 self.patchUtils(self.new_root)
114 paths = helpers.Paths({})
115 cls = distros.fetch(distro)
116 mydist = cls(distro, {}, paths)
117 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
118 if metadata:
119 myds.metadata.update(metadata)
120 return cloud.Cloud(myds, paths, {}, mydist, None)
121
122 def _apt_source_list(self, cfg, expected, distro):
123 "_apt_source_list - Test rendering from template (generic)"
124
125 # entry at top level now, wrap in 'apt' key
126 cfg = {'apt': cfg}
127 mycloud = self._get_cloud(distro)
128 with mock.patch.object(util, 'write_file') as mockwf:
129 with mock.patch.object(util, 'load_file',
130 return_value=MOCKED_APT_SRC_LIST) as mocklf:
131 with mock.patch.object(os.path, 'isfile',
132 return_value=True) as mockisfile:
133 with mock.patch.object(util, 'rename'):
134 cc_apt_configure.handle("test", cfg, mycloud,
135 LOG, None)
136
137 # check if it would have loaded the distro template
138 mockisfile.assert_any_call(
139 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
140 mocklf.assert_any_call(
141 ('/etc/cloud/templates/sources.list.%s.tmpl' % distro))
142 # check expected content in result
143 mockwf.assert_called_once_with('/etc/apt/sources.list', expected,
144 mode=0o644)
145
146 def test_apt_v3_source_list_debian(self):
147 """test_apt_v3_source_list_debian - without custom sources or parms"""
148 cfg = {}
149 self._apt_source_list(cfg, EXPECTED_BASE_CONTENT, 'debian')
150
151 def test_apt_v3_source_list_ubuntu(self):
152 """test_apt_v3_source_list_ubuntu - without custom sources or parms"""
153 cfg = {}
154 self._apt_source_list(cfg, EXPECTED_BASE_CONTENT, 'ubuntu')
155
156 def test_apt_v3_source_list_psm(self):
157 """test_apt_v3_source_list_psm - Test specifying prim+sec mirrors"""
158 pm = 'http://test.ubuntu.com/ubuntu/'
159 sm = 'http://testsec.ubuntu.com/ubuntu/'
160 cfg = {'preserve_sources_list': False,
161 'primary': [{'arches': ["default"],
162 'uri': pm}],
163 'security': [{'arches': ["default"],
164 'uri': sm}]}
165
166 self._apt_source_list(cfg, EXPECTED_PRIMSEC_CONTENT, 'ubuntu')
167
168 def test_apt_v3_srcl_custom(self):
169 """test_apt_v3_srcl_custom - Test rendering a custom source template"""
170 cfg = util.load_yaml(YAML_TEXT_CUSTOM_SL)
171 mycloud = self._get_cloud('ubuntu')
172
173 # the second mock restores the original subp
174 with mock.patch.object(util, 'write_file') as mockwrite:
175 with mock.patch.object(util, 'subp', self.subp):
176 with mock.patch.object(Distro, 'get_primary_arch',
177 return_value='amd64'):
178 cc_apt_configure.handle("notimportant", cfg, mycloud,
179 LOG, None)
180
181 calls = [call('/etc/apt/sources.list',
182 EXPECTED_CONVERTED_CONTENT,
183 mode=0o644)]
184 mockwrite.assert_has_calls(calls)
185
186
187# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_apt_source.py b/tests/unittests/test_handler/test_handler_apt_source.py
0deleted file mode 100644188deleted file mode 100644
index 99a4d86..0000000
--- a/tests/unittests/test_handler/test_handler_apt_source.py
+++ /dev/null
@@ -1,516 +0,0 @@
1""" test_handler_apt_source
2Testing various config variations of the apt_source config
3"""
4import os
5import re
6import shutil
7import tempfile
8
9try:
10 from unittest import mock
11except ImportError:
12 import mock
13from mock import call
14
15from cloudinit.config import cc_apt_configure
16from cloudinit import gpg
17from cloudinit import util
18
19from ..helpers import TestCase
20
21EXPECTEDKEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
22Version: GnuPG v1
23
24mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
25NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
268PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
27HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
28CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
29OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
30FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
31S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
32=ACB2
33-----END PGP PUBLIC KEY BLOCK-----"""
34
35
36def load_tfile_or_url(*args, **kwargs):
37 """load_tfile_or_url
38 load file and return content after decoding
39 """
40 return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
41
42
43class TestAptSourceConfig(TestCase):
44 """TestAptSourceConfig
45 Main Class to test apt_source configs
46 """
47 release = "fantastic"
48
49 def setUp(self):
50 super(TestAptSourceConfig, self).setUp()
51 self.tmp = tempfile.mkdtemp()
52 self.addCleanup(shutil.rmtree, self.tmp)
53 self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
54 self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
55 self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
56 self.join = os.path.join
57 # mock fallback filename into writable tmp dir
58 self.fallbackfn = os.path.join(self.tmp, "etc/apt/sources.list.d/",
59 "cloud_config_sources.list")
60
61 patcher = mock.patch("cloudinit.config.cc_apt_configure.get_release")
62 get_rel = patcher.start()
63 get_rel.return_value = self.release
64 self.addCleanup(patcher.stop)
65
66 @staticmethod
67 def _get_default_params():
68 """get_default_params
69 Get the most basic default mrror and release info to be used in tests
70 """
71 params = {}
72 params['RELEASE'] = cc_apt_configure.get_release()
73 params['MIRROR'] = "http://archive.ubuntu.com/ubuntu"
74 return params
75
76 def myjoin(self, *args, **kwargs):
77 """myjoin - redir into writable tmpdir"""
78 if (args[0] == "/etc/apt/sources.list.d/" and
79 args[1] == "cloud_config_sources.list" and
80 len(args) == 2):
81 return self.join(self.tmp, args[0].lstrip("/"), args[1])
82 else:
83 return self.join(*args, **kwargs)
84
85 def apt_src_basic(self, filename, cfg):
86 """apt_src_basic
87 Test Fix deb source string, has to overwrite mirror conf in params
88 """
89 params = self._get_default_params()
90
91 cc_apt_configure.add_apt_sources(cfg, params)
92
93 self.assertTrue(os.path.isfile(filename))
94
95 contents = load_tfile_or_url(filename)
96 self.assertTrue(re.search(r"%s %s %s %s\n" %
97 ("deb", "http://archive.ubuntu.com/ubuntu",
98 "karmic-backports",
99 "main universe multiverse restricted"),
100 contents, flags=re.IGNORECASE))
101
102 def test_apt_src_basic(self):
103 """Test deb source string, overwrite mirror and filename"""
104 cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
105 ' karmic-backports'
106 ' main universe multiverse restricted'),
107 'filename': self.aptlistfile}
108 self.apt_src_basic(self.aptlistfile, [cfg])
109
110 def test_apt_src_basic_dict(self):
111 """Test deb source string, overwrite mirror and filename (dict)"""
112 cfg = {self.aptlistfile: {'source':
113 ('deb http://archive.ubuntu.com/ubuntu'
114 ' karmic-backports'
115 ' main universe multiverse restricted')}}
116 self.apt_src_basic(self.aptlistfile, cfg)
117
118 def apt_src_basic_tri(self, cfg):
119 """apt_src_basic_tri
120 Test Fix three deb source string, has to overwrite mirror conf in
121 params. Test with filenames provided in config.
122 generic part to check three files with different content
123 """
124 self.apt_src_basic(self.aptlistfile, cfg)
125
126 # extra verify on two extra files of this test
127 contents = load_tfile_or_url(self.aptlistfile2)
128 self.assertTrue(re.search(r"%s %s %s %s\n" %
129 ("deb", "http://archive.ubuntu.com/ubuntu",
130 "precise-backports",
131 "main universe multiverse restricted"),
132 contents, flags=re.IGNORECASE))
133 contents = load_tfile_or_url(self.aptlistfile3)
134 self.assertTrue(re.search(r"%s %s %s %s\n" %
135 ("deb", "http://archive.ubuntu.com/ubuntu",
136 "lucid-backports",
137 "main universe multiverse restricted"),
138 contents, flags=re.IGNORECASE))
139
140 def test_apt_src_basic_tri(self):
141 """Test Fix three deb source string with filenames"""
142 cfg1 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
143 ' karmic-backports'
144 ' main universe multiverse restricted'),
145 'filename': self.aptlistfile}
146 cfg2 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
147 ' precise-backports'
148 ' main universe multiverse restricted'),
149 'filename': self.aptlistfile2}
150 cfg3 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
151 ' lucid-backports'
152 ' main universe multiverse restricted'),
153 'filename': self.aptlistfile3}
154 self.apt_src_basic_tri([cfg1, cfg2, cfg3])
155
156 def test_apt_src_basic_dict_tri(self):
157 """Test Fix three deb source string with filenames (dict)"""
158 cfg = {self.aptlistfile: {'source':
159 ('deb http://archive.ubuntu.com/ubuntu'
160 ' karmic-backports'
161 ' main universe multiverse restricted')},
162 self.aptlistfile2: {'source':
163 ('deb http://archive.ubuntu.com/ubuntu'
164 ' precise-backports'
165 ' main universe multiverse restricted')},
166 self.aptlistfile3: {'source':
167 ('deb http://archive.ubuntu.com/ubuntu'
168 ' lucid-backports'
169 ' main universe multiverse restricted')}}
170 self.apt_src_basic_tri(cfg)
171
172 def test_apt_src_basic_nofn(self):
173 """Test Fix three deb source string without filenames (dict)"""
174 cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
175 ' karmic-backports'
176 ' main universe multiverse restricted')}
177 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
178 self.apt_src_basic(self.fallbackfn, [cfg])
179
180 def apt_src_replacement(self, filename, cfg):
181 """apt_src_replace
182 Test Autoreplacement of MIRROR and RELEASE in source specs
183 """
184 params = self._get_default_params()
185 cc_apt_configure.add_apt_sources(cfg, params)
186
187 self.assertTrue(os.path.isfile(filename))
188
189 contents = load_tfile_or_url(filename)
190 self.assertTrue(re.search(r"%s %s %s %s\n" %
191 ("deb", params['MIRROR'], params['RELEASE'],
192 "multiverse"),
193 contents, flags=re.IGNORECASE))
194
195 def test_apt_src_replace(self):
196 """Test Autoreplacement of MIRROR and RELEASE in source specs"""
197 cfg = {'source': 'deb $MIRROR $RELEASE multiverse',
198 'filename': self.aptlistfile}
199 self.apt_src_replacement(self.aptlistfile, [cfg])
200
201 def apt_src_replace_tri(self, cfg):
202 """apt_src_replace_tri
203 Test three autoreplacements of MIRROR and RELEASE in source specs with
204 generic part
205 """
206 self.apt_src_replacement(self.aptlistfile, cfg)
207
208 # extra verify on two extra files of this test
209 params = self._get_default_params()
210 contents = load_tfile_or_url(self.aptlistfile2)
211 self.assertTrue(re.search(r"%s %s %s %s\n" %
212 ("deb", params['MIRROR'], params['RELEASE'],
213 "main"),
214 contents, flags=re.IGNORECASE))
215 contents = load_tfile_or_url(self.aptlistfile3)
216 self.assertTrue(re.search(r"%s %s %s %s\n" %
217 ("deb", params['MIRROR'], params['RELEASE'],
218 "universe"),
219 contents, flags=re.IGNORECASE))
220
221 def test_apt_src_replace_tri(self):
222 """Test triple Autoreplacement of MIRROR and RELEASE in source specs"""
223 cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
224 'filename': self.aptlistfile}
225 cfg2 = {'source': 'deb $MIRROR $RELEASE main',
226 'filename': self.aptlistfile2}
227 cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
228 'filename': self.aptlistfile3}
229 self.apt_src_replace_tri([cfg1, cfg2, cfg3])
230
231 def test_apt_src_replace_dict_tri(self):
232 """Test triple Autoreplacement in source specs (dict)"""
233 cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
234 'notused': {'source': 'deb $MIRROR $RELEASE main',
235 'filename': self.aptlistfile2},
236 self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
237 self.apt_src_replace_tri(cfg)
238
239 def test_apt_src_replace_nofn(self):
240 """Test Autoreplacement of MIRROR and RELEASE in source specs nofile"""
241 cfg = {'source': 'deb $MIRROR $RELEASE multiverse'}
242 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
243 self.apt_src_replacement(self.fallbackfn, [cfg])
244
245 def apt_src_keyid(self, filename, cfg, keynum):
246 """apt_src_keyid
247 Test specification of a source + keyid
248 """
249 params = self._get_default_params()
250
251 with mock.patch.object(util, 'subp',
252 return_value=('fakekey 1234', '')) as mockobj:
253 cc_apt_configure.add_apt_sources(cfg, params)
254
255 # check if it added the right ammount of keys
256 calls = []
257 for _ in range(keynum):
258 calls.append(call(('apt-key', 'add', '-'), 'fakekey 1234'))
259 mockobj.assert_has_calls(calls, any_order=True)
260
261 self.assertTrue(os.path.isfile(filename))
262
263 contents = load_tfile_or_url(filename)
264 self.assertTrue(re.search(r"%s %s %s %s\n" %
265 ("deb",
266 ('http://ppa.launchpad.net/smoser/'
267 'cloud-init-test/ubuntu'),
268 "xenial", "main"),
269 contents, flags=re.IGNORECASE))
270
271 def test_apt_src_keyid(self):
272 """Test specification of a source + keyid with filename being set"""
273 cfg = {'source': ('deb '
274 'http://ppa.launchpad.net/'
275 'smoser/cloud-init-test/ubuntu'
276 ' xenial main'),
277 'keyid': "03683F77",
278 'filename': self.aptlistfile}
279 self.apt_src_keyid(self.aptlistfile, [cfg], 1)
280
281 def test_apt_src_keyid_tri(self):
282 """Test 3x specification of a source + keyid with filename being set"""
283 cfg1 = {'source': ('deb '
284 'http://ppa.launchpad.net/'
285 'smoser/cloud-init-test/ubuntu'
286 ' xenial main'),
287 'keyid': "03683F77",
288 'filename': self.aptlistfile}
289 cfg2 = {'source': ('deb '
290 'http://ppa.launchpad.net/'
291 'smoser/cloud-init-test/ubuntu'
292 ' xenial universe'),
293 'keyid': "03683F77",
294 'filename': self.aptlistfile2}
295 cfg3 = {'source': ('deb '
296 'http://ppa.launchpad.net/'
297 'smoser/cloud-init-test/ubuntu'
298 ' xenial multiverse'),
299 'keyid': "03683F77",
300 'filename': self.aptlistfile3}
301
302 self.apt_src_keyid(self.aptlistfile, [cfg1, cfg2, cfg3], 3)
303 contents = load_tfile_or_url(self.aptlistfile2)
304 self.assertTrue(re.search(r"%s %s %s %s\n" %
305 ("deb",
306 ('http://ppa.launchpad.net/smoser/'
307 'cloud-init-test/ubuntu'),
308 "xenial", "universe"),
309 contents, flags=re.IGNORECASE))
310 contents = load_tfile_or_url(self.aptlistfile3)
311 self.assertTrue(re.search(r"%s %s %s %s\n" %
312 ("deb",
313 ('http://ppa.launchpad.net/smoser/'
314 'cloud-init-test/ubuntu'),
315 "xenial", "multiverse"),
316 contents, flags=re.IGNORECASE))
317
318 def test_apt_src_keyid_nofn(self):
319 """Test specification of a source + keyid without filename being set"""
320 cfg = {'source': ('deb '
321 'http://ppa.launchpad.net/'
322 'smoser/cloud-init-test/ubuntu'
323 ' xenial main'),
324 'keyid': "03683F77"}
325 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
326 self.apt_src_keyid(self.fallbackfn, [cfg], 1)
327
328 def apt_src_key(self, filename, cfg):
329 """apt_src_key
330 Test specification of a source + key
331 """
332 params = self._get_default_params()
333
334 with mock.patch.object(util, 'subp') as mockobj:
335 cc_apt_configure.add_apt_sources([cfg], params)
336
337 mockobj.assert_called_with(('apt-key', 'add', '-'), 'fakekey 4321')
338
339 self.assertTrue(os.path.isfile(filename))
340
341 contents = load_tfile_or_url(filename)
342 self.assertTrue(re.search(r"%s %s %s %s\n" %
343 ("deb",
344 ('http://ppa.launchpad.net/smoser/'
345 'cloud-init-test/ubuntu'),
346 "xenial", "main"),
347 contents, flags=re.IGNORECASE))
348
349 def test_apt_src_key(self):
350 """Test specification of a source + key with filename being set"""
351 cfg = {'source': ('deb '
352 'http://ppa.launchpad.net/'
353 'smoser/cloud-init-test/ubuntu'
354 ' xenial main'),
355 'key': "fakekey 4321",
356 'filename': self.aptlistfile}
357 self.apt_src_key(self.aptlistfile, cfg)
358
359 def test_apt_src_key_nofn(self):
360 """Test specification of a source + key without filename being set"""
361 cfg = {'source': ('deb '
362 'http://ppa.launchpad.net/'
363 'smoser/cloud-init-test/ubuntu'
364 ' xenial main'),
365 'key': "fakekey 4321"}
366 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
367 self.apt_src_key(self.fallbackfn, cfg)
368
369 def test_apt_src_keyonly(self):
370 """Test specifying key without source"""
371 params = self._get_default_params()
372 cfg = {'key': "fakekey 4242",
373 'filename': self.aptlistfile}
374
375 with mock.patch.object(util, 'subp') as mockobj:
376 cc_apt_configure.add_apt_sources([cfg], params)
377
378 mockobj.assert_called_once_with(('apt-key', 'add', '-'),
379 'fakekey 4242')
380
381 # filename should be ignored on key only
382 self.assertFalse(os.path.isfile(self.aptlistfile))
383
384 def test_apt_src_keyidonly(self):
385 """Test specification of a keyid without source"""
386 params = self._get_default_params()
387 cfg = {'keyid': "03683F77",
388 'filename': self.aptlistfile}
389
390 with mock.patch.object(util, 'subp',
391 return_value=('fakekey 1212', '')) as mockobj:
392 cc_apt_configure.add_apt_sources([cfg], params)
393
394 mockobj.assert_called_with(('apt-key', 'add', '-'), 'fakekey 1212')
395
396 # filename should be ignored on key only
397 self.assertFalse(os.path.isfile(self.aptlistfile))
398
399 def apt_src_keyid_real(self, cfg, expectedkey):
400 """apt_src_keyid_real
401 Test specification of a keyid without source including
402 up to addition of the key (add_apt_key_raw mocked to keep the
403 environment as is)
404 """
405 params = self._get_default_params()
406
407 with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
408 with mock.patch.object(gpg, 'get_key_by_id',
409 return_value=expectedkey) as mockgetkey:
410 cc_apt_configure.add_apt_sources([cfg], params)
411
412 mockgetkey.assert_called_with(cfg['keyid'],
413 cfg.get('keyserver',
414 'keyserver.ubuntu.com'))
415 mockkey.assert_called_with(expectedkey)
416
417 # filename should be ignored on key only
418 self.assertFalse(os.path.isfile(self.aptlistfile))
419
420 def test_apt_src_keyid_real(self):
421 """test_apt_src_keyid_real - Test keyid including key add"""
422 keyid = "03683F77"
423 cfg = {'keyid': keyid,
424 'filename': self.aptlistfile}
425
426 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
427
428 def test_apt_src_longkeyid_real(self):
429 """test_apt_src_longkeyid_real - Test long keyid including key add"""
430 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
431 cfg = {'keyid': keyid,
432 'filename': self.aptlistfile}
433
434 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
435
436 def test_apt_src_longkeyid_ks_real(self):
437 """test_apt_src_longkeyid_ks_real - Test long keyid from other ks"""
438 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
439 cfg = {'keyid': keyid,
440 'keyserver': 'keys.gnupg.net',
441 'filename': self.aptlistfile}
442
443 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
444
445 def test_apt_src_ppa(self):
446 """Test adding a ppa"""
447 params = self._get_default_params()
448 cfg = {'source': 'ppa:smoser/cloud-init-test',
449 'filename': self.aptlistfile}
450
451 # default matcher needed for ppa
452 matcher = re.compile(r'^[\w-]+:\w').search
453
454 with mock.patch.object(util, 'subp') as mockobj:
455 cc_apt_configure.add_apt_sources([cfg], params,
456 aa_repo_match=matcher)
457 mockobj.assert_called_once_with(['add-apt-repository',
458 'ppa:smoser/cloud-init-test'])
459
460 # adding ppa should ignore filename (uses add-apt-repository)
461 self.assertFalse(os.path.isfile(self.aptlistfile))
462
463 def test_apt_src_ppa_tri(self):
464 """Test adding three ppa's"""
465 params = self._get_default_params()
466 cfg1 = {'source': 'ppa:smoser/cloud-init-test',
467 'filename': self.aptlistfile}
468 cfg2 = {'source': 'ppa:smoser/cloud-init-test2',
469 'filename': self.aptlistfile2}
470 cfg3 = {'source': 'ppa:smoser/cloud-init-test3',
471 'filename': self.aptlistfile3}
472
473 # default matcher needed for ppa
474 matcher = re.compile(r'^[\w-]+:\w').search
475
476 with mock.patch.object(util, 'subp') as mockobj:
477 cc_apt_configure.add_apt_sources([cfg1, cfg2, cfg3], params,
478 aa_repo_match=matcher)
479 calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test']),
480 call(['add-apt-repository', 'ppa:smoser/cloud-init-test2']),
481 call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'])]
482 mockobj.assert_has_calls(calls, any_order=True)
483
484 # adding ppa should ignore all filenames (uses add-apt-repository)
485 self.assertFalse(os.path.isfile(self.aptlistfile))
486 self.assertFalse(os.path.isfile(self.aptlistfile2))
487 self.assertFalse(os.path.isfile(self.aptlistfile3))
488
489 def test_convert_to_new_format(self):
490 """Test the conversion of old to new format"""
491 cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
492 'filename': self.aptlistfile}
493 cfg2 = {'source': 'deb $MIRROR $RELEASE main',
494 'filename': self.aptlistfile2}
495 cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
496 'filename': self.aptlistfile3}
497 checkcfg = {self.aptlistfile: {'filename': self.aptlistfile,
498 'source': 'deb $MIRROR $RELEASE '
499 'multiverse'},
500 self.aptlistfile2: {'filename': self.aptlistfile2,
501 'source': 'deb $MIRROR $RELEASE main'},
502 self.aptlistfile3: {'filename': self.aptlistfile3,
503 'source': 'deb $MIRROR $RELEASE '
504 'universe'}}
505
506 newcfg = cc_apt_configure.convert_to_new_format([cfg1, cfg2, cfg3])
507 self.assertEqual(newcfg, checkcfg)
508
509 newcfg2 = cc_apt_configure.convert_to_new_format(newcfg)
510 self.assertEqual(newcfg2, checkcfg)
511
512 with self.assertRaises(ValueError):
513 cc_apt_configure.convert_to_new_format(5)
514
515
516# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_apt_source_v1.py b/tests/unittests/test_handler/test_handler_apt_source_v1.py
517new file mode 1006440new file mode 100644
index 0000000..d96779c
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_apt_source_v1.py
@@ -0,0 +1,551 @@
1""" test_handler_apt_source_v1
2Testing various config variations of the apt_source config
3This calls all things with v1 format to stress the conversion code on top of
4the actually tested code.
5"""
6import os
7import re
8import shutil
9import tempfile
10
11try:
12 from unittest import mock
13except ImportError:
14 import mock
15from mock import call
16
17from cloudinit.config import cc_apt_configure
18from cloudinit import gpg
19from cloudinit import util
20
21from ..helpers import TestCase
22
23EXPECTEDKEY = """-----BEGIN PGP PUBLIC KEY BLOCK-----
24Version: GnuPG v1
25
26mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
27NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
288PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
29HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
30CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
31OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
32FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
33S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
34=ACB2
35-----END PGP PUBLIC KEY BLOCK-----"""
36
37ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
38
39
40def load_tfile_or_url(*args, **kwargs):
41 """load_tfile_or_url
42 load file and return content after decoding
43 """
44 return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
45
46
47class FakeDistro(object):
48 """Fake Distro helper object"""
49 def update_package_sources(self):
50 """Fake update_package_sources helper method"""
51 return
52
53
54class FakeCloud(object):
55 """Fake Cloud helper object"""
56 def __init__(self):
57 self.distro = FakeDistro()
58
59
60class TestAptSourceConfig(TestCase):
61 """TestAptSourceConfig
62 Main Class to test apt_source configs
63 """
64 release = "fantastic"
65
66 def setUp(self):
67 super(TestAptSourceConfig, self).setUp()
68 self.tmp = tempfile.mkdtemp()
69 self.addCleanup(shutil.rmtree, self.tmp)
70 self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
71 self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
72 self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
73 self.join = os.path.join
74 self.matcher = re.compile(ADD_APT_REPO_MATCH).search
75 # mock fallback filename into writable tmp dir
76 self.fallbackfn = os.path.join(self.tmp, "etc/apt/sources.list.d/",
77 "cloud_config_sources.list")
78
79 self.fakecloud = FakeCloud()
80
81 rpatcher = mock.patch("cloudinit.util.lsb_release")
82 get_rel = rpatcher.start()
83 get_rel.return_value = {'codename': self.release}
84 self.addCleanup(rpatcher.stop)
85 apatcher = mock.patch("cloudinit.util.get_architecture")
86 get_arch = apatcher.start()
87 get_arch.return_value = 'amd64'
88 self.addCleanup(apatcher.stop)
89
90 def _get_default_params(self):
91 """get_default_params
92 Get the most basic default mrror and release info to be used in tests
93 """
94 params = {}
95 params['RELEASE'] = self.release
96 params['MIRROR'] = "http://archive.ubuntu.com/ubuntu"
97 return params
98
99 def wrapv1conf(self, cfg):
100 params = self._get_default_params()
101 # old v1 list format under old keys, but callabe to main handler
102 # disable source.list rendering and set mirror to avoid other code
103 return {'apt_preserve_sources_list': True,
104 'apt_mirror': params['MIRROR'],
105 'apt_sources': cfg}
106
107 def myjoin(self, *args, **kwargs):
108 """myjoin - redir into writable tmpdir"""
109 if (args[0] == "/etc/apt/sources.list.d/" and
110 args[1] == "cloud_config_sources.list" and
111 len(args) == 2):
112 return self.join(self.tmp, args[0].lstrip("/"), args[1])
113 else:
114 return self.join(*args, **kwargs)
115
116 def apt_src_basic(self, filename, cfg):
117 """apt_src_basic
118 Test Fix deb source string, has to overwrite mirror conf in params
119 """
120 cfg = self.wrapv1conf(cfg)
121
122 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
123
124 self.assertTrue(os.path.isfile(filename))
125
126 contents = load_tfile_or_url(filename)
127 self.assertTrue(re.search(r"%s %s %s %s\n" %
128 ("deb", "http://archive.ubuntu.com/ubuntu",
129 "karmic-backports",
130 "main universe multiverse restricted"),
131 contents, flags=re.IGNORECASE))
132
133 def test_apt_src_basic(self):
134 """Test deb source string, overwrite mirror and filename"""
135 cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
136 ' karmic-backports'
137 ' main universe multiverse restricted'),
138 'filename': self.aptlistfile}
139 self.apt_src_basic(self.aptlistfile, [cfg])
140
141 def test_apt_src_basic_dict(self):
142 """Test deb source string, overwrite mirror and filename (dict)"""
143 cfg = {self.aptlistfile: {'source':
144 ('deb http://archive.ubuntu.com/ubuntu'
145 ' karmic-backports'
146 ' main universe multiverse restricted')}}
147 self.apt_src_basic(self.aptlistfile, cfg)
148
149 def apt_src_basic_tri(self, cfg):
150 """apt_src_basic_tri
151 Test Fix three deb source string, has to overwrite mirror conf in
152 params. Test with filenames provided in config.
153 generic part to check three files with different content
154 """
155 self.apt_src_basic(self.aptlistfile, cfg)
156
157 # extra verify on two extra files of this test
158 contents = load_tfile_or_url(self.aptlistfile2)
159 self.assertTrue(re.search(r"%s %s %s %s\n" %
160 ("deb", "http://archive.ubuntu.com/ubuntu",
161 "precise-backports",
162 "main universe multiverse restricted"),
163 contents, flags=re.IGNORECASE))
164 contents = load_tfile_or_url(self.aptlistfile3)
165 self.assertTrue(re.search(r"%s %s %s %s\n" %
166 ("deb", "http://archive.ubuntu.com/ubuntu",
167 "lucid-backports",
168 "main universe multiverse restricted"),
169 contents, flags=re.IGNORECASE))
170
171 def test_apt_src_basic_tri(self):
172 """Test Fix three deb source string with filenames"""
173 cfg1 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
174 ' karmic-backports'
175 ' main universe multiverse restricted'),
176 'filename': self.aptlistfile}
177 cfg2 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
178 ' precise-backports'
179 ' main universe multiverse restricted'),
180 'filename': self.aptlistfile2}
181 cfg3 = {'source': ('deb http://archive.ubuntu.com/ubuntu'
182 ' lucid-backports'
183 ' main universe multiverse restricted'),
184 'filename': self.aptlistfile3}
185 self.apt_src_basic_tri([cfg1, cfg2, cfg3])
186
187 def test_apt_src_basic_dict_tri(self):
188 """Test Fix three deb source string with filenames (dict)"""
189 cfg = {self.aptlistfile: {'source':
190 ('deb http://archive.ubuntu.com/ubuntu'
191 ' karmic-backports'
192 ' main universe multiverse restricted')},
193 self.aptlistfile2: {'source':
194 ('deb http://archive.ubuntu.com/ubuntu'
195 ' precise-backports'
196 ' main universe multiverse restricted')},
197 self.aptlistfile3: {'source':
198 ('deb http://archive.ubuntu.com/ubuntu'
199 ' lucid-backports'
200 ' main universe multiverse restricted')}}
201 self.apt_src_basic_tri(cfg)
202
203 def test_apt_src_basic_nofn(self):
204 """Test Fix three deb source string without filenames (dict)"""
205 cfg = {'source': ('deb http://archive.ubuntu.com/ubuntu'
206 ' karmic-backports'
207 ' main universe multiverse restricted')}
208 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
209 self.apt_src_basic(self.fallbackfn, [cfg])
210
211 def apt_src_replacement(self, filename, cfg):
212 """apt_src_replace
213 Test Autoreplacement of MIRROR and RELEASE in source specs
214 """
215 cfg = self.wrapv1conf(cfg)
216 params = self._get_default_params()
217 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
218
219 self.assertTrue(os.path.isfile(filename))
220
221 contents = load_tfile_or_url(filename)
222 self.assertTrue(re.search(r"%s %s %s %s\n" %
223 ("deb", params['MIRROR'], params['RELEASE'],
224 "multiverse"),
225 contents, flags=re.IGNORECASE))
226
227 def test_apt_src_replace(self):
228 """Test Autoreplacement of MIRROR and RELEASE in source specs"""
229 cfg = {'source': 'deb $MIRROR $RELEASE multiverse',
230 'filename': self.aptlistfile}
231 self.apt_src_replacement(self.aptlistfile, [cfg])
232
233 def apt_src_replace_tri(self, cfg):
234 """apt_src_replace_tri
235 Test three autoreplacements of MIRROR and RELEASE in source specs with
236 generic part
237 """
238 self.apt_src_replacement(self.aptlistfile, cfg)
239
240 # extra verify on two extra files of this test
241 params = self._get_default_params()
242 contents = load_tfile_or_url(self.aptlistfile2)
243 self.assertTrue(re.search(r"%s %s %s %s\n" %
244 ("deb", params['MIRROR'], params['RELEASE'],
245 "main"),
246 contents, flags=re.IGNORECASE))
247 contents = load_tfile_or_url(self.aptlistfile3)
248 self.assertTrue(re.search(r"%s %s %s %s\n" %
249 ("deb", params['MIRROR'], params['RELEASE'],
250 "universe"),
251 contents, flags=re.IGNORECASE))
252
253 def test_apt_src_replace_tri(self):
254 """Test triple Autoreplacement of MIRROR and RELEASE in source specs"""
255 cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
256 'filename': self.aptlistfile}
257 cfg2 = {'source': 'deb $MIRROR $RELEASE main',
258 'filename': self.aptlistfile2}
259 cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
260 'filename': self.aptlistfile3}
261 self.apt_src_replace_tri([cfg1, cfg2, cfg3])
262
263 def test_apt_src_replace_dict_tri(self):
264 """Test triple Autoreplacement in source specs (dict)"""
265 cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
266 'notused': {'source': 'deb $MIRROR $RELEASE main',
267 'filename': self.aptlistfile2},
268 self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
269 self.apt_src_replace_tri(cfg)
270
271 def test_apt_src_replace_nofn(self):
272 """Test Autoreplacement of MIRROR and RELEASE in source specs nofile"""
273 cfg = {'source': 'deb $MIRROR $RELEASE multiverse'}
274 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
275 self.apt_src_replacement(self.fallbackfn, [cfg])
276
277 def apt_src_keyid(self, filename, cfg, keynum):
278 """apt_src_keyid
279 Test specification of a source + keyid
280 """
281 cfg = self.wrapv1conf(cfg)
282
283 with mock.patch.object(util, 'subp',
284 return_value=('fakekey 1234', '')) as mockobj:
285 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
286
287 # check if it added the right ammount of keys
288 calls = []
289 for _ in range(keynum):
290 calls.append(call(['apt-key', 'add', '-'],
291 data=b'fakekey 1234',
292 target=None))
293 mockobj.assert_has_calls(calls, any_order=True)
294
295 self.assertTrue(os.path.isfile(filename))
296
297 contents = load_tfile_or_url(filename)
298 self.assertTrue(re.search(r"%s %s %s %s\n" %
299 ("deb",
300 ('http://ppa.launchpad.net/smoser/'
301 'cloud-init-test/ubuntu'),
302 "xenial", "main"),
303 contents, flags=re.IGNORECASE))
304
305 def test_apt_src_keyid(self):
306 """Test specification of a source + keyid with filename being set"""
307 cfg = {'source': ('deb '
308 'http://ppa.launchpad.net/'
309 'smoser/cloud-init-test/ubuntu'
310 ' xenial main'),
311 'keyid': "03683F77",
312 'filename': self.aptlistfile}
313 self.apt_src_keyid(self.aptlistfile, [cfg], 1)
314
315 def test_apt_src_keyid_tri(self):
316 """Test 3x specification of a source + keyid with filename being set"""
317 cfg1 = {'source': ('deb '
318 'http://ppa.launchpad.net/'
319 'smoser/cloud-init-test/ubuntu'
320 ' xenial main'),
321 'keyid': "03683F77",
322 'filename': self.aptlistfile}
323 cfg2 = {'source': ('deb '
324 'http://ppa.launchpad.net/'
325 'smoser/cloud-init-test/ubuntu'
326 ' xenial universe'),
327 'keyid': "03683F77",
328 'filename': self.aptlistfile2}
329 cfg3 = {'source': ('deb '
330 'http://ppa.launchpad.net/'
331 'smoser/cloud-init-test/ubuntu'
332 ' xenial multiverse'),
333 'keyid': "03683F77",
334 'filename': self.aptlistfile3}
335
336 self.apt_src_keyid(self.aptlistfile, [cfg1, cfg2, cfg3], 3)
337 contents = load_tfile_or_url(self.aptlistfile2)
338 self.assertTrue(re.search(r"%s %s %s %s\n" %
339 ("deb",
340 ('http://ppa.launchpad.net/smoser/'
341 'cloud-init-test/ubuntu'),
342 "xenial", "universe"),
343 contents, flags=re.IGNORECASE))
344 contents = load_tfile_or_url(self.aptlistfile3)
345 self.assertTrue(re.search(r"%s %s %s %s\n" %
346 ("deb",
347 ('http://ppa.launchpad.net/smoser/'
348 'cloud-init-test/ubuntu'),
349 "xenial", "multiverse"),
350 contents, flags=re.IGNORECASE))
351
352 def test_apt_src_keyid_nofn(self):
353 """Test specification of a source + keyid without filename being set"""
354 cfg = {'source': ('deb '
355 'http://ppa.launchpad.net/'
356 'smoser/cloud-init-test/ubuntu'
357 ' xenial main'),
358 'keyid': "03683F77"}
359 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
360 self.apt_src_keyid(self.fallbackfn, [cfg], 1)
361
362 def apt_src_key(self, filename, cfg):
363 """apt_src_key
364 Test specification of a source + key
365 """
366 cfg = self.wrapv1conf([cfg])
367
368 with mock.patch.object(util, 'subp') as mockobj:
369 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
370
371 mockobj.assert_called_with(['apt-key', 'add', '-'],
372 data=b'fakekey 4321', target=None)
373
374 self.assertTrue(os.path.isfile(filename))
375
376 contents = load_tfile_or_url(filename)
377 self.assertTrue(re.search(r"%s %s %s %s\n" %
378 ("deb",
379 ('http://ppa.launchpad.net/smoser/'
380 'cloud-init-test/ubuntu'),
381 "xenial", "main"),
382 contents, flags=re.IGNORECASE))
383
384 def test_apt_src_key(self):
385 """Test specification of a source + key with filename being set"""
386 cfg = {'source': ('deb '
387 'http://ppa.launchpad.net/'
388 'smoser/cloud-init-test/ubuntu'
389 ' xenial main'),
390 'key': "fakekey 4321",
391 'filename': self.aptlistfile}
392 self.apt_src_key(self.aptlistfile, cfg)
393
394 def test_apt_src_key_nofn(self):
395 """Test specification of a source + key without filename being set"""
396 cfg = {'source': ('deb '
397 'http://ppa.launchpad.net/'
398 'smoser/cloud-init-test/ubuntu'
399 ' xenial main'),
400 'key': "fakekey 4321"}
401 with mock.patch.object(os.path, 'join', side_effect=self.myjoin):
402 self.apt_src_key(self.fallbackfn, cfg)
403
404 def test_apt_src_keyonly(self):
405 """Test specifying key without source"""
406 cfg = {'key': "fakekey 4242",
407 'filename': self.aptlistfile}
408 cfg = self.wrapv1conf([cfg])
409
410 with mock.patch.object(util, 'subp') as mockobj:
411 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
412
413 mockobj.assert_called_once_with(['apt-key', 'add', '-'],
414 data=b'fakekey 4242', target=None)
415
416 # filename should be ignored on key only
417 self.assertFalse(os.path.isfile(self.aptlistfile))
418
419 def test_apt_src_keyidonly(self):
420 """Test specification of a keyid without source"""
421 cfg = {'keyid': "03683F77",
422 'filename': self.aptlistfile}
423 cfg = self.wrapv1conf([cfg])
424
425 with mock.patch.object(util, 'subp',
426 return_value=('fakekey 1212', '')) as mockobj:
427 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
428
429 mockobj.assert_called_with(['apt-key', 'add', '-'],
430 data=b'fakekey 1212', target=None)
431
432 # filename should be ignored on key only
433 self.assertFalse(os.path.isfile(self.aptlistfile))
434
435 def apt_src_keyid_real(self, cfg, expectedkey):
436 """apt_src_keyid_real
437 Test specification of a keyid without source including
438 up to addition of the key (add_apt_key_raw mocked to keep the
439 environment as is)
440 """
441 key = cfg['keyid']
442 keyserver = cfg.get('keyserver', 'keyserver.ubuntu.com')
443 cfg = self.wrapv1conf([cfg])
444
445 with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
446 with mock.patch.object(gpg, 'getkeybyid',
447 return_value=expectedkey) as mockgetkey:
448 cc_apt_configure.handle("test", cfg, self.fakecloud,
449 None, None)
450
451 mockgetkey.assert_called_with(key, keyserver)
452 mockkey.assert_called_with(expectedkey, None)
453
454 # filename should be ignored on key only
455 self.assertFalse(os.path.isfile(self.aptlistfile))
456
457 def test_apt_src_keyid_real(self):
458 """test_apt_src_keyid_real - Test keyid including key add"""
459 keyid = "03683F77"
460 cfg = {'keyid': keyid,
461 'filename': self.aptlistfile}
462
463 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
464
465 def test_apt_src_longkeyid_real(self):
466 """test_apt_src_longkeyid_real - Test long keyid including key add"""
467 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
468 cfg = {'keyid': keyid,
469 'filename': self.aptlistfile}
470
471 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
472
473 def test_apt_src_longkeyid_ks_real(self):
474 """test_apt_src_longkeyid_ks_real - Test long keyid from other ks"""
475 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
476 cfg = {'keyid': keyid,
477 'keyserver': 'keys.gnupg.net',
478 'filename': self.aptlistfile}
479
480 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
481
482 def test_apt_src_ppa(self):
483 """Test adding a ppa"""
484 cfg = {'source': 'ppa:smoser/cloud-init-test',
485 'filename': self.aptlistfile}
486 cfg = self.wrapv1conf([cfg])
487
488 with mock.patch.object(util, 'subp') as mockobj:
489 cc_apt_configure.handle("test", cfg, self.fakecloud, None, None)
490 mockobj.assert_called_once_with(['add-apt-repository',
491 'ppa:smoser/cloud-init-test'],
492 target=None)
493
494 # adding ppa should ignore filename (uses add-apt-repository)
495 self.assertFalse(os.path.isfile(self.aptlistfile))
496
497 def test_apt_src_ppa_tri(self):
498 """Test adding three ppa's"""
499 cfg1 = {'source': 'ppa:smoser/cloud-init-test',
500 'filename': self.aptlistfile}
501 cfg2 = {'source': 'ppa:smoser/cloud-init-test2',
502 'filename': self.aptlistfile2}
503 cfg3 = {'source': 'ppa:smoser/cloud-init-test3',
504 'filename': self.aptlistfile3}
505 cfg = self.wrapv1conf([cfg1, cfg2, cfg3])
506
507 with mock.patch.object(util, 'subp') as mockobj:
508 cc_apt_configure.handle("test", cfg, self.fakecloud,
509 None, None)
510 calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test'],
511 target=None),
512 call(['add-apt-repository', 'ppa:smoser/cloud-init-test2'],
513 target=None),
514 call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'],
515 target=None)]
516 mockobj.assert_has_calls(calls, any_order=True)
517
518 # adding ppa should ignore all filenames (uses add-apt-repository)
519 self.assertFalse(os.path.isfile(self.aptlistfile))
520 self.assertFalse(os.path.isfile(self.aptlistfile2))
521 self.assertFalse(os.path.isfile(self.aptlistfile3))
522
523 def test_convert_to_new_format(self):
524 """Test the conversion of old to new format"""
525 cfg1 = {'source': 'deb $MIRROR $RELEASE multiverse',
526 'filename': self.aptlistfile}
527 cfg2 = {'source': 'deb $MIRROR $RELEASE main',
528 'filename': self.aptlistfile2}
529 cfg3 = {'source': 'deb $MIRROR $RELEASE universe',
530 'filename': self.aptlistfile3}
531 checkcfg = {self.aptlistfile: {'filename': self.aptlistfile,
532 'source': 'deb $MIRROR $RELEASE '
533 'multiverse'},
534 self.aptlistfile2: {'filename': self.aptlistfile2,
535 'source': 'deb $MIRROR $RELEASE main'},
536 self.aptlistfile3: {'filename': self.aptlistfile3,
537 'source': 'deb $MIRROR $RELEASE '
538 'universe'}}
539
540 newcfg = cc_apt_configure.convert_v1_to_v2_apt_format([cfg1, cfg2,
541 cfg3])
542 self.assertEqual(newcfg, checkcfg)
543
544 newcfg2 = cc_apt_configure.convert_v1_to_v2_apt_format(newcfg)
545 self.assertEqual(newcfg2, checkcfg)
546
547 with self.assertRaises(ValueError):
548 cc_apt_configure.convert_v1_to_v2_apt_format(5)
549
550
551# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_apt_source_v3.py b/tests/unittests/test_handler/test_handler_apt_source_v3.py
0new file mode 100644552new file mode 100644
index 0000000..75556b6
--- /dev/null
+++ b/tests/unittests/test_handler/test_handler_apt_source_v3.py
@@ -0,0 +1,1103 @@
1"""test_handler_apt_source_v3
2Testing various config variations of the apt_source custom config
3This tries to call all in the new v3 format and cares about new features
4"""
5import glob
6import os
7import re
8import shutil
9import socket
10import tempfile
11
12from unittest import TestCase
13
14try:
15 from unittest import mock
16except ImportError:
17 import mock
18from mock import call
19
20from cloudinit import cloud
21from cloudinit import distros
22from cloudinit import gpg
23from cloudinit import helpers
24from cloudinit import util
25
26from cloudinit.config import cc_apt_configure
27from cloudinit.sources import DataSourceNone
28
29from .. import helpers as t_help
30
31EXPECTEDKEY = u"""-----BEGIN PGP PUBLIC KEY BLOCK-----
32Version: GnuPG v1
33
34mI0ESuZLUgEEAKkqq3idtFP7g9hzOu1a8+v8ImawQN4TrvlygfScMU1TIS1eC7UQ
35NUA8Qqgr9iUaGnejb0VciqftLrU9D6WYHSKz+EITefgdyJ6SoQxjoJdsCpJ7o9Jy
368PQnpRttiFm4qHu6BVnKnBNxw/z3ST9YMqW5kbMQpfxbGe+obRox59NpABEBAAG0
37HUxhdW5jaHBhZCBQUEEgZm9yIFNjb3R0IE1vc2VyiLYEEwECACAFAkrmS1ICGwMG
38CwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAKCRAGILvPA2g/d3aEA/9tVjc10HOZwV29
39OatVuTeERjjrIbxflO586GLA8cp0C9RQCwgod/R+cKYdQcHjbqVcP0HqxveLg0RZ
40FJpWLmWKamwkABErwQLGlM/Hwhjfade8VvEQutH5/0JgKHmzRsoqfR+LMO6OS+Sm
41S0ORP6HXET3+jC8BMG4tBWCTK/XEZw==
42=ACB2
43-----END PGP PUBLIC KEY BLOCK-----"""
44
45ADD_APT_REPO_MATCH = r"^[\w-]+:\w"
46
47TARGET = None
48
49
50def load_tfile(*args, **kwargs):
51 """load_tfile_or_url
52 load file and return content after decoding
53 """
54 return util.decode_binary(util.read_file_or_url(*args, **kwargs).contents)
55
56
57class TestAptSourceConfig(t_help.FilesystemMockingTestCase):
58 """TestAptSourceConfig
59 Main Class to test apt configs
60 """
61 def setUp(self):
62 super(TestAptSourceConfig, self).setUp()
63 self.tmp = tempfile.mkdtemp()
64 self.new_root = tempfile.mkdtemp()
65 self.addCleanup(shutil.rmtree, self.tmp)
66 self.aptlistfile = os.path.join(self.tmp, "single-deb.list")
67 self.aptlistfile2 = os.path.join(self.tmp, "single-deb2.list")
68 self.aptlistfile3 = os.path.join(self.tmp, "single-deb3.list")
69 self.join = os.path.join
70 self.matcher = re.compile(ADD_APT_REPO_MATCH).search
71
72 @staticmethod
73 def _add_apt_sources(*args, **kwargs):
74 with mock.patch.object(cc_apt_configure, 'update_packages'):
75 cc_apt_configure.add_apt_sources(*args, **kwargs)
76
77 @staticmethod
78 def _get_default_params():
79 """get_default_params
80 Get the most basic default mrror and release info to be used in tests
81 """
82 params = {}
83 params['RELEASE'] = util.lsb_release()['codename']
84 arch = 'amd64'
85 params['MIRROR'] = cc_apt_configure.\
86 get_default_mirrors(arch)["PRIMARY"]
87 return params
88
89 def _myjoin(self, *args, **kwargs):
90 """_myjoin - redir into writable tmpdir"""
91 if (args[0] == "/etc/apt/sources.list.d/" and
92 args[1] == "cloud_config_sources.list" and
93 len(args) == 2):
94 return self.join(self.tmp, args[0].lstrip("/"), args[1])
95 else:
96 return self.join(*args, **kwargs)
97
98 def _get_cloud(self, distro, metadata=None):
99 self.patchUtils(self.new_root)
100 paths = helpers.Paths({})
101 cls = distros.fetch(distro)
102 mydist = cls(distro, {}, paths)
103 myds = DataSourceNone.DataSourceNone({}, mydist, paths)
104 if metadata:
105 myds.metadata.update(metadata)
106 return cloud.Cloud(myds, paths, {}, mydist, None)
107
108 def _apt_src_basic(self, filename, cfg):
109 """_apt_src_basic
110 Test Fix deb source string, has to overwrite mirror conf in params
111 """
112 params = self._get_default_params()
113
114 self._add_apt_sources(cfg, TARGET, template_params=params,
115 aa_repo_match=self.matcher)
116
117 self.assertTrue(os.path.isfile(filename))
118
119 contents = load_tfile(filename)
120 self.assertTrue(re.search(r"%s %s %s %s\n" %
121 ("deb", "http://test.ubuntu.com/ubuntu",
122 "karmic-backports",
123 "main universe multiverse restricted"),
124 contents, flags=re.IGNORECASE))
125
126 def test_apt_v3_src_basic(self):
127 """test_apt_v3_src_basic - Test fix deb source string"""
128 cfg = {self.aptlistfile: {'source':
129 ('deb http://test.ubuntu.com/ubuntu'
130 ' karmic-backports'
131 ' main universe multiverse restricted')}}
132 self._apt_src_basic(self.aptlistfile, cfg)
133
134 def test_apt_v3_src_basic_tri(self):
135 """test_apt_v3_src_basic_tri - Test multiple fix deb source strings"""
136 cfg = {self.aptlistfile: {'source':
137 ('deb http://test.ubuntu.com/ubuntu'
138 ' karmic-backports'
139 ' main universe multiverse restricted')},
140 self.aptlistfile2: {'source':
141 ('deb http://test.ubuntu.com/ubuntu'
142 ' precise-backports'
143 ' main universe multiverse restricted')},
144 self.aptlistfile3: {'source':
145 ('deb http://test.ubuntu.com/ubuntu'
146 ' lucid-backports'
147 ' main universe multiverse restricted')}}
148 self._apt_src_basic(self.aptlistfile, cfg)
149
150 # extra verify on two extra files of this test
151 contents = load_tfile(self.aptlistfile2)
152 self.assertTrue(re.search(r"%s %s %s %s\n" %
153 ("deb", "http://test.ubuntu.com/ubuntu",
154 "precise-backports",
155 "main universe multiverse restricted"),
156 contents, flags=re.IGNORECASE))
157 contents = load_tfile(self.aptlistfile3)
158 self.assertTrue(re.search(r"%s %s %s %s\n" %
159 ("deb", "http://test.ubuntu.com/ubuntu",
160 "lucid-backports",
161 "main universe multiverse restricted"),
162 contents, flags=re.IGNORECASE))
163
164 def _apt_src_replacement(self, filename, cfg):
165 """apt_src_replace
166 Test Autoreplacement of MIRROR and RELEASE in source specs
167 """
168 params = self._get_default_params()
169 self._add_apt_sources(cfg, TARGET, template_params=params,
170 aa_repo_match=self.matcher)
171
172 self.assertTrue(os.path.isfile(filename))
173
174 contents = load_tfile(filename)
175 self.assertTrue(re.search(r"%s %s %s %s\n" %
176 ("deb", params['MIRROR'], params['RELEASE'],
177 "multiverse"),
178 contents, flags=re.IGNORECASE))
179
180 def test_apt_v3_src_replace(self):
181 """test_apt_v3_src_replace - Test replacement of MIRROR & RELEASE"""
182 cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'}}
183 self._apt_src_replacement(self.aptlistfile, cfg)
184
185 def test_apt_v3_src_replace_fn(self):
186 """test_apt_v3_src_replace_fn - Test filename overwritten in dict"""
187 cfg = {'ignored': {'source': 'deb $MIRROR $RELEASE multiverse',
188 'filename': self.aptlistfile}}
189 # second file should overwrite the dict key
190 self._apt_src_replacement(self.aptlistfile, cfg)
191
192 def _apt_src_replace_tri(self, cfg):
193 """_apt_src_replace_tri
194 Test three autoreplacements of MIRROR and RELEASE in source specs with
195 generic part
196 """
197 self._apt_src_replacement(self.aptlistfile, cfg)
198
199 # extra verify on two extra files of this test
200 params = self._get_default_params()
201 contents = load_tfile(self.aptlistfile2)
202 self.assertTrue(re.search(r"%s %s %s %s\n" %
203 ("deb", params['MIRROR'], params['RELEASE'],
204 "main"),
205 contents, flags=re.IGNORECASE))
206 contents = load_tfile(self.aptlistfile3)
207 self.assertTrue(re.search(r"%s %s %s %s\n" %
208 ("deb", params['MIRROR'], params['RELEASE'],
209 "universe"),
210 contents, flags=re.IGNORECASE))
211
212 def test_apt_v3_src_replace_tri(self):
213 """test_apt_v3_src_replace_tri - Test multiple replace/overwrites"""
214 cfg = {self.aptlistfile: {'source': 'deb $MIRROR $RELEASE multiverse'},
215 'notused': {'source': 'deb $MIRROR $RELEASE main',
216 'filename': self.aptlistfile2},
217 self.aptlistfile3: {'source': 'deb $MIRROR $RELEASE universe'}}
218 self._apt_src_replace_tri(cfg)
219
220 def _apt_src_keyid(self, filename, cfg, keynum):
221 """_apt_src_keyid
222 Test specification of a source + keyid
223 """
224 params = self._get_default_params()
225
226 with mock.patch("cloudinit.util.subp",
227 return_value=('fakekey 1234', '')) as mockobj:
228 self._add_apt_sources(cfg, TARGET, template_params=params,
229 aa_repo_match=self.matcher)
230
231 # check if it added the right ammount of keys
232 calls = []
233 for _ in range(keynum):
234 calls.append(call(['apt-key', 'add', '-'], data=b'fakekey 1234',
235 target=TARGET))
236 mockobj.assert_has_calls(calls, any_order=True)
237
238 self.assertTrue(os.path.isfile(filename))
239
240 contents = load_tfile(filename)
241 self.assertTrue(re.search(r"%s %s %s %s\n" %
242 ("deb",
243 ('http://ppa.launchpad.net/smoser/'
244 'cloud-init-test/ubuntu'),
245 "xenial", "main"),
246 contents, flags=re.IGNORECASE))
247
248 def test_apt_v3_src_keyid(self):
249 """test_apt_v3_src_keyid - Test source + keyid with filename"""
250 cfg = {self.aptlistfile: {'source': ('deb '
251 'http://ppa.launchpad.net/'
252 'smoser/cloud-init-test/ubuntu'
253 ' xenial main'),
254 'keyid': "03683F77"}}
255 self._apt_src_keyid(self.aptlistfile, cfg, 1)
256
257 def test_apt_v3_src_keyid_tri(self):
258 """test_apt_v3_src_keyid_tri - Test multiple src+key+filen writes"""
259 cfg = {self.aptlistfile: {'source': ('deb '
260 'http://ppa.launchpad.net/'
261 'smoser/cloud-init-test/ubuntu'
262 ' xenial main'),
263 'keyid': "03683F77"},
264 'ignored': {'source': ('deb '
265 'http://ppa.launchpad.net/'
266 'smoser/cloud-init-test/ubuntu'
267 ' xenial universe'),
268 'keyid': "03683F77",
269 'filename': self.aptlistfile2},
270 self.aptlistfile3: {'source': ('deb '
271 'http://ppa.launchpad.net/'
272 'smoser/cloud-init-test/ubuntu'
273 ' xenial multiverse'),
274 'keyid': "03683F77"}}
275
276 self._apt_src_keyid(self.aptlistfile, cfg, 3)
277 contents = load_tfile(self.aptlistfile2)
278 self.assertTrue(re.search(r"%s %s %s %s\n" %
279 ("deb",
280 ('http://ppa.launchpad.net/smoser/'
281 'cloud-init-test/ubuntu'),
282 "xenial", "universe"),
283 contents, flags=re.IGNORECASE))
284 contents = load_tfile(self.aptlistfile3)
285 self.assertTrue(re.search(r"%s %s %s %s\n" %
286 ("deb",
287 ('http://ppa.launchpad.net/smoser/'
288 'cloud-init-test/ubuntu'),
289 "xenial", "multiverse"),
290 contents, flags=re.IGNORECASE))
291
292 def test_apt_v3_src_key(self):
293 """test_apt_v3_src_key - Test source + key"""
294 params = self._get_default_params()
295 cfg = {self.aptlistfile: {'source': ('deb '
296 'http://ppa.launchpad.net/'
297 'smoser/cloud-init-test/ubuntu'
298 ' xenial main'),
299 'key': "fakekey 4321"}}
300
301 with mock.patch.object(util, 'subp') as mockobj:
302 self._add_apt_sources(cfg, TARGET, template_params=params,
303 aa_repo_match=self.matcher)
304
305 mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 4321',
306 target=TARGET)
307
308 self.assertTrue(os.path.isfile(self.aptlistfile))
309
310 contents = load_tfile(self.aptlistfile)
311 self.assertTrue(re.search(r"%s %s %s %s\n" %
312 ("deb",
313 ('http://ppa.launchpad.net/smoser/'
314 'cloud-init-test/ubuntu'),
315 "xenial", "main"),
316 contents, flags=re.IGNORECASE))
317
318 def test_apt_v3_src_keyonly(self):
319 """test_apt_v3_src_keyonly - Test key without source"""
320 params = self._get_default_params()
321 cfg = {self.aptlistfile: {'key': "fakekey 4242"}}
322
323 with mock.patch.object(util, 'subp') as mockobj:
324 self._add_apt_sources(cfg, TARGET, template_params=params,
325 aa_repo_match=self.matcher)
326
327 mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 4242',
328 target=TARGET)
329
330 # filename should be ignored on key only
331 self.assertFalse(os.path.isfile(self.aptlistfile))
332
333 def test_apt_v3_src_keyidonly(self):
334 """test_apt_v3_src_keyidonly - Test keyid without source"""
335 params = self._get_default_params()
336 cfg = {self.aptlistfile: {'keyid': "03683F77"}}
337
338 with mock.patch.object(util, 'subp',
339 return_value=('fakekey 1212', '')) as mockobj:
340 self._add_apt_sources(cfg, TARGET, template_params=params,
341 aa_repo_match=self.matcher)
342
343 mockobj.assert_any_call(['apt-key', 'add', '-'], data=b'fakekey 1212',
344 target=TARGET)
345
346 # filename should be ignored on key only
347 self.assertFalse(os.path.isfile(self.aptlistfile))
348
349 def apt_src_keyid_real(self, cfg, expectedkey):
350 """apt_src_keyid_real
351 Test specification of a keyid without source including
352 up to addition of the key (add_apt_key_raw mocked to keep the
353 environment as is)
354 """
355 params = self._get_default_params()
356
357 with mock.patch.object(cc_apt_configure, 'add_apt_key_raw') as mockkey:
358 with mock.patch.object(gpg, 'getkeybyid',
359 return_value=expectedkey) as mockgetkey:
360 self._add_apt_sources(cfg, TARGET, template_params=params,
361 aa_repo_match=self.matcher)
362
363 keycfg = cfg[self.aptlistfile]
364 mockgetkey.assert_called_with(keycfg['keyid'],
365 keycfg.get('keyserver',
366 'keyserver.ubuntu.com'))
367 mockkey.assert_called_with(expectedkey, TARGET)
368
369 # filename should be ignored on key only
370 self.assertFalse(os.path.isfile(self.aptlistfile))
371
372 def test_apt_v3_src_keyid_real(self):
373 """test_apt_v3_src_keyid_real - Test keyid including key add"""
374 keyid = "03683F77"
375 cfg = {self.aptlistfile: {'keyid': keyid}}
376
377 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
378
379 def test_apt_v3_src_longkeyid_real(self):
380 """test_apt_v3_src_longkeyid_real Test long keyid including key add"""
381 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
382 cfg = {self.aptlistfile: {'keyid': keyid}}
383
384 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
385
386 def test_apt_v3_src_longkeyid_ks_real(self):
387 """test_apt_v3_src_longkeyid_ks_real Test long keyid from other ks"""
388 keyid = "B59D 5F15 97A5 04B7 E230 6DCA 0620 BBCF 0368 3F77"
389 cfg = {self.aptlistfile: {'keyid': keyid,
390 'keyserver': 'keys.gnupg.net'}}
391
392 self.apt_src_keyid_real(cfg, EXPECTEDKEY)
393
394 def test_apt_v3_src_keyid_keyserver(self):
395 """test_apt_v3_src_keyid_keyserver - Test custom keyserver"""
396 keyid = "03683F77"
397 params = self._get_default_params()
398 cfg = {self.aptlistfile: {'keyid': keyid,
399 'keyserver': 'test.random.com'}}
400
401 # in some test environments only *.ubuntu.com is reachable
402 # so mock the call and check if the config got there
403 with mock.patch.object(gpg, 'getkeybyid',
404 return_value="fakekey") as mockgetkey:
405 with mock.patch.object(cc_apt_configure,
406 'add_apt_key_raw') as mockadd:
407 self._add_apt_sources(cfg, TARGET, template_params=params,
408 aa_repo_match=self.matcher)
409
410 mockgetkey.assert_called_with('03683F77', 'test.random.com')
411 mockadd.assert_called_with('fakekey', TARGET)
412
413 # filename should be ignored on key only
414 self.assertFalse(os.path.isfile(self.aptlistfile))
415
416 def test_apt_v3_src_ppa(self):
417 """test_apt_v3_src_ppa - Test specification of a ppa"""
418 params = self._get_default_params()
419 cfg = {self.aptlistfile: {'source': 'ppa:smoser/cloud-init-test'}}
420
421 with mock.patch("cloudinit.util.subp") as mockobj:
422 self._add_apt_sources(cfg, TARGET, template_params=params,
423 aa_repo_match=self.matcher)
424 mockobj.assert_any_call(['add-apt-repository',
425 'ppa:smoser/cloud-init-test'], target=TARGET)
426
427 # adding ppa should ignore filename (uses add-apt-repository)
428 self.assertFalse(os.path.isfile(self.aptlistfile))
429
430 def test_apt_v3_src_ppa_tri(self):
431 """test_apt_v3_src_ppa_tri - Test specification of multiple ppa's"""
432 params = self._get_default_params()
433 cfg = {self.aptlistfile: {'source': 'ppa:smoser/cloud-init-test'},
434 self.aptlistfile2: {'source': 'ppa:smoser/cloud-init-test2'},
435 self.aptlistfile3: {'source': 'ppa:smoser/cloud-init-test3'}}
436
437 with mock.patch("cloudinit.util.subp") as mockobj:
438 self._add_apt_sources(cfg, TARGET, template_params=params,
439 aa_repo_match=self.matcher)
440 calls = [call(['add-apt-repository', 'ppa:smoser/cloud-init-test'],
441 target=TARGET),
442 call(['add-apt-repository', 'ppa:smoser/cloud-init-test2'],
443 target=TARGET),
444 call(['add-apt-repository', 'ppa:smoser/cloud-init-test3'],
445 target=TARGET)]
446 mockobj.assert_has_calls(calls, any_order=True)
447
448 # adding ppa should ignore all filenames (uses add-apt-repository)
449 self.assertFalse(os.path.isfile(self.aptlistfile))
450 self.assertFalse(os.path.isfile(self.aptlistfile2))
451 self.assertFalse(os.path.isfile(self.aptlistfile3))
452
453 @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
454 def test_apt_v3_list_rename(self, m_get_architecture):
455 """test_apt_v3_list_rename - Test find mirror and apt list renaming"""
456 pre = "/var/lib/apt/lists"
457 # filenames are archive dependent
458
459 arch = 's390x'
460 m_get_architecture.return_value = arch
461 component = "ubuntu-ports"
462 archive = "ports.ubuntu.com"
463
464 cfg = {'primary': [{'arches': ["default"],
465 'uri':
466 'http://test.ubuntu.com/%s/' % component}],
467 'security': [{'arches': ["default"],
468 'uri':
469 'http://testsec.ubuntu.com/%s/' % component}]}
470 post = ("%s_dists_%s-updates_InRelease" %
471 (component, util.lsb_release()['codename']))
472 fromfn = ("%s/%s_%s" % (pre, archive, post))
473 tofn = ("%s/test.ubuntu.com_%s" % (pre, post))
474
475 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
476
477 self.assertEqual(mirrors['MIRROR'],
478 "http://test.ubuntu.com/%s/" % component)
479 self.assertEqual(mirrors['PRIMARY'],
480 "http://test.ubuntu.com/%s/" % component)
481 self.assertEqual(mirrors['SECURITY'],
482 "http://testsec.ubuntu.com/%s/" % component)
483
484 with mock.patch.object(os, 'rename') as mockren:
485 with mock.patch.object(glob, 'glob',
486 return_value=[fromfn]):
487 cc_apt_configure.rename_apt_lists(mirrors, TARGET)
488
489 mockren.assert_any_call(fromfn, tofn)
490
491 @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
492 def test_apt_v3_list_rename_non_slash(self, m_get_architecture):
493 target = os.path.join(self.tmp, "rename_non_slash")
494 apt_lists_d = os.path.join(target, "./" + cc_apt_configure.APT_LISTS)
495
496 m_get_architecture.return_value = 'amd64'
497
498 mirror_path = "some/random/path/"
499 primary = "http://test.ubuntu.com/" + mirror_path
500 security = "http://test-security.ubuntu.com/" + mirror_path
501 mirrors = {'PRIMARY': primary, 'SECURITY': security}
502
503 # these match default archive prefixes
504 opri_pre = "archive.ubuntu.com_ubuntu_dists_xenial"
505 osec_pre = "security.ubuntu.com_ubuntu_dists_xenial"
506 # this one won't match and should not be renamed defaults.
507 other_pre = "dl.google.com_linux_chrome_deb_dists_stable"
508 # these are our new expected prefixes
509 npri_pre = "test.ubuntu.com_some_random_path_dists_xenial"
510 nsec_pre = "test-security.ubuntu.com_some_random_path_dists_xenial"
511
512 files = [
513 # orig prefix, new prefix, suffix
514 (opri_pre, npri_pre, "_main_binary-amd64_Packages"),
515 (opri_pre, npri_pre, "_main_binary-amd64_InRelease"),
516 (opri_pre, npri_pre, "-updates_main_binary-amd64_Packages"),
517 (opri_pre, npri_pre, "-updates_main_binary-amd64_InRelease"),
518 (other_pre, other_pre, "_main_binary-amd64_Packages"),
519 (other_pre, other_pre, "_Release"),
520 (other_pre, other_pre, "_Release.gpg"),
521 (osec_pre, nsec_pre, "_InRelease"),
522 (osec_pre, nsec_pre, "_main_binary-amd64_Packages"),
523 (osec_pre, nsec_pre, "_universe_binary-amd64_Packages"),
524 ]
525
526 expected = sorted([npre + suff for opre, npre, suff in files])
527 # create files
528 for (opre, npre, suff) in files:
529 fpath = os.path.join(apt_lists_d, opre + suff)
530 util.write_file(fpath, content=fpath)
531
532 cc_apt_configure.rename_apt_lists(mirrors, target)
533 found = sorted(os.listdir(apt_lists_d))
534 self.assertEqual(expected, found)
535
536 @staticmethod
537 def test_apt_v3_proxy():
538 """test_apt_v3_proxy - Test apt_*proxy configuration"""
539 cfg = {"proxy": "foobar1",
540 "http_proxy": "foobar2",
541 "ftp_proxy": "foobar3",
542 "https_proxy": "foobar4"}
543
544 with mock.patch.object(util, 'write_file') as mockobj:
545 cc_apt_configure.apply_apt_config(cfg, "proxyfn", "notused")
546
547 mockobj.assert_called_with('proxyfn',
548 ('Acquire::http::Proxy "foobar1";\n'
549 'Acquire::http::Proxy "foobar2";\n'
550 'Acquire::ftp::Proxy "foobar3";\n'
551 'Acquire::https::Proxy "foobar4";\n'))
552
553 def test_apt_v3_mirror(self):
554 """test_apt_v3_mirror - Test defining a mirror"""
555 pmir = "http://us.archive.ubuntu.com/ubuntu/"
556 smir = "http://security.ubuntu.com/ubuntu/"
557 cfg = {"primary": [{'arches': ["default"],
558 "uri": pmir}],
559 "security": [{'arches': ["default"],
560 "uri": smir}]}
561
562 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, 'amd64')
563
564 self.assertEqual(mirrors['MIRROR'],
565 pmir)
566 self.assertEqual(mirrors['PRIMARY'],
567 pmir)
568 self.assertEqual(mirrors['SECURITY'],
569 smir)
570
571 def test_apt_v3_mirror_default(self):
572 """test_apt_v3_mirror_default - Test without defining a mirror"""
573 arch = 'amd64'
574 default_mirrors = cc_apt_configure.get_default_mirrors(arch)
575 pmir = default_mirrors["PRIMARY"]
576 smir = default_mirrors["SECURITY"]
577 mycloud = self._get_cloud('ubuntu')
578 mirrors = cc_apt_configure.find_apt_mirror_info({}, mycloud, arch)
579
580 self.assertEqual(mirrors['MIRROR'],
581 pmir)
582 self.assertEqual(mirrors['PRIMARY'],
583 pmir)
584 self.assertEqual(mirrors['SECURITY'],
585 smir)
586
587 def test_apt_v3_mirror_arches(self):
588 """test_apt_v3_mirror_arches - Test arches selection of mirror"""
589 pmir = "http://my-primary.ubuntu.com/ubuntu/"
590 smir = "http://my-security.ubuntu.com/ubuntu/"
591 arch = 'ppc64el'
592 cfg = {"primary": [{'arches': ["default"], "uri": "notthis-primary"},
593 {'arches': [arch], "uri": pmir}],
594 "security": [{'arches': ["default"], "uri": "nothis-security"},
595 {'arches': [arch], "uri": smir}]}
596
597 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
598
599 self.assertEqual(mirrors['PRIMARY'], pmir)
600 self.assertEqual(mirrors['MIRROR'], pmir)
601 self.assertEqual(mirrors['SECURITY'], smir)
602
603 def test_apt_v3_mirror_arches_default(self):
604 """test_apt_v3_mirror_arches - Test falling back to default arch"""
605 pmir = "http://us.archive.ubuntu.com/ubuntu/"
606 smir = "http://security.ubuntu.com/ubuntu/"
607 cfg = {"primary": [{'arches': ["default"],
608 "uri": pmir},
609 {'arches': ["thisarchdoesntexist"],
610 "uri": "notthis"}],
611 "security": [{'arches': ["thisarchdoesntexist"],
612 "uri": "nothat"},
613 {'arches': ["default"],
614 "uri": smir}]}
615
616 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, 'amd64')
617
618 self.assertEqual(mirrors['MIRROR'],
619 pmir)
620 self.assertEqual(mirrors['PRIMARY'],
621 pmir)
622 self.assertEqual(mirrors['SECURITY'],
623 smir)
624
625 @mock.patch("cloudinit.config.cc_apt_configure.util.get_architecture")
626 def test_apt_v3_get_def_mir_non_intel_no_arch(self, m_get_architecture):
627 arch = 'ppc64el'
628 m_get_architecture.return_value = arch
629 expected = {'PRIMARY': 'http://ports.ubuntu.com/ubuntu-ports',
630 'SECURITY': 'http://ports.ubuntu.com/ubuntu-ports'}
631 self.assertEqual(expected, cc_apt_configure.get_default_mirrors())
632
633 def test_apt_v3_get_default_mirrors_non_intel_with_arch(self):
634 found = cc_apt_configure.get_default_mirrors('ppc64el')
635
636 expected = {'PRIMARY': 'http://ports.ubuntu.com/ubuntu-ports',
637 'SECURITY': 'http://ports.ubuntu.com/ubuntu-ports'}
638 self.assertEqual(expected, found)
639
640 def test_apt_v3_mirror_arches_sysdefault(self):
641 """test_apt_v3_mirror_arches - Test arches fallback to sys default"""
642 arch = 'amd64'
643 default_mirrors = cc_apt_configure.get_default_mirrors(arch)
644 pmir = default_mirrors["PRIMARY"]
645 smir = default_mirrors["SECURITY"]
646 mycloud = self._get_cloud('ubuntu')
647 cfg = {"primary": [{'arches': ["thisarchdoesntexist_64"],
648 "uri": "notthis"},
649 {'arches': ["thisarchdoesntexist"],
650 "uri": "notthiseither"}],
651 "security": [{'arches': ["thisarchdoesntexist"],
652 "uri": "nothat"},
653 {'arches': ["thisarchdoesntexist_64"],
654 "uri": "nothateither"}]}
655
656 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
657
658 self.assertEqual(mirrors['MIRROR'], pmir)
659 self.assertEqual(mirrors['PRIMARY'], pmir)
660 self.assertEqual(mirrors['SECURITY'], smir)
661
662 def test_apt_v3_mirror_search(self):
663 """test_apt_v3_mirror_search - Test searching mirrors in a list
664 mock checks to avoid relying on network connectivity"""
665 pmir = "http://us.archive.ubuntu.com/ubuntu/"
666 smir = "http://security.ubuntu.com/ubuntu/"
667 cfg = {"primary": [{'arches': ["default"],
668 "search": ["pfailme", pmir]}],
669 "security": [{'arches': ["default"],
670 "search": ["sfailme", smir]}]}
671
672 with mock.patch.object(cc_apt_configure, 'search_for_mirror',
673 side_effect=[pmir, smir]) as mocksearch:
674 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None,
675 'amd64')
676
677 calls = [call(["pfailme", pmir]),
678 call(["sfailme", smir])]
679 mocksearch.assert_has_calls(calls)
680
681 self.assertEqual(mirrors['MIRROR'],
682 pmir)
683 self.assertEqual(mirrors['PRIMARY'],
684 pmir)
685 self.assertEqual(mirrors['SECURITY'],
686 smir)
687
688 def test_apt_v3_mirror_search_many2(self):
689 """test_apt_v3_mirror_search_many3 - Test both mirrors specs at once"""
690 pmir = "http://us.archive.ubuntu.com/ubuntu/"
691 smir = "http://security.ubuntu.com/ubuntu/"
692 cfg = {"primary": [{'arches': ["default"],
693 "uri": pmir,
694 "search": ["pfailme", "foo"]}],
695 "security": [{'arches': ["default"],
696 "uri": smir,
697 "search": ["sfailme", "bar"]}]}
698
699 arch = 'amd64'
700
701 # should be called only once per type, despite two mirror configs
702 mycloud = None
703 with mock.patch.object(cc_apt_configure, 'get_mirror',
704 return_value="http://mocked/foo") as mockgm:
705 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
706 calls = [call(cfg, 'primary', arch, mycloud),
707 call(cfg, 'security', arch, mycloud)]
708 mockgm.assert_has_calls(calls)
709
710 # should not be called, since primary is specified
711 with mock.patch.object(cc_apt_configure,
712 'search_for_mirror') as mockse:
713 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, None, arch)
714 mockse.assert_not_called()
715
716 self.assertEqual(mirrors['MIRROR'],
717 pmir)
718 self.assertEqual(mirrors['PRIMARY'],
719 pmir)
720 self.assertEqual(mirrors['SECURITY'],
721 smir)
722
723 def test_apt_v3_url_resolvable(self):
724 """test_apt_v3_url_resolvable - Test resolving urls"""
725
726 with mock.patch.object(util, 'is_resolvable') as mockresolve:
727 util.is_resolvable_url("http://1.2.3.4/ubuntu")
728 mockresolve.assert_called_with("1.2.3.4")
729
730 with mock.patch.object(util, 'is_resolvable') as mockresolve:
731 util.is_resolvable_url("http://us.archive.ubuntu.com/ubuntu")
732 mockresolve.assert_called_with("us.archive.ubuntu.com")
733
734 # former tests can leave this set (or not if the test is ran directly)
735 # do a hard reset to ensure a stable result
736 util._DNS_REDIRECT_IP = None
737 bad = [(None, None, None, "badname", ["10.3.2.1"])]
738 good = [(None, None, None, "goodname", ["10.2.3.4"])]
739 with mock.patch.object(socket, 'getaddrinfo',
740 side_effect=[bad, bad, bad, good,
741 good]) as mocksock:
742 ret = util.is_resolvable_url("http://us.archive.ubuntu.com/ubuntu")
743 ret2 = util.is_resolvable_url("http://1.2.3.4/ubuntu")
744 mocksock.assert_any_call('does-not-exist.example.com.', None,
745 0, 0, 1, 2)
746 mocksock.assert_any_call('example.invalid.', None, 0, 0, 1, 2)
747 mocksock.assert_any_call('us.archive.ubuntu.com', None)
748 mocksock.assert_any_call('1.2.3.4', None)
749
750 self.assertTrue(ret)
751 self.assertTrue(ret2)
752
753 # side effect need only bad ret after initial call
754 with mock.patch.object(socket, 'getaddrinfo',
755 side_effect=[bad]) as mocksock:
756 ret3 = util.is_resolvable_url("http://failme.com/ubuntu")
757 calls = [call('failme.com', None)]
758 mocksock.assert_has_calls(calls)
759 self.assertFalse(ret3)
760
761 def test_apt_v3_disable_suites(self):
762 """test_disable_suites - disable_suites with many configurations"""
763 release = "xenial"
764 orig = """deb http://ubuntu.com//ubuntu xenial main
765deb http://ubuntu.com//ubuntu xenial-updates main
766deb http://ubuntu.com//ubuntu xenial-security main
767deb-src http://ubuntu.com//ubuntu universe multiverse
768deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
769
770 # disable nothing
771 disabled = []
772 expect = """deb http://ubuntu.com//ubuntu xenial main
773deb http://ubuntu.com//ubuntu xenial-updates main
774deb http://ubuntu.com//ubuntu xenial-security main
775deb-src http://ubuntu.com//ubuntu universe multiverse
776deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
777 result = cc_apt_configure.disable_suites(disabled, orig, release)
778 self.assertEqual(expect, result)
779
780 # single disable release suite
781 disabled = ["$RELEASE"]
782 expect = """\
783# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu xenial main
784deb http://ubuntu.com//ubuntu xenial-updates main
785deb http://ubuntu.com//ubuntu xenial-security main
786deb-src http://ubuntu.com//ubuntu universe multiverse
787deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
788 result = cc_apt_configure.disable_suites(disabled, orig, release)
789 self.assertEqual(expect, result)
790
791 # single disable other suite
792 disabled = ["$RELEASE-updates"]
793 expect = ("""deb http://ubuntu.com//ubuntu xenial main
794# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu"""
795 """ xenial-updates main
796deb http://ubuntu.com//ubuntu xenial-security main
797deb-src http://ubuntu.com//ubuntu universe multiverse
798deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
799 result = cc_apt_configure.disable_suites(disabled, orig, release)
800 self.assertEqual(expect, result)
801
802 # multi disable
803 disabled = ["$RELEASE-updates", "$RELEASE-security"]
804 expect = ("""deb http://ubuntu.com//ubuntu xenial main
805# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
806 """xenial-updates main
807# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
808 """xenial-security main
809deb-src http://ubuntu.com//ubuntu universe multiverse
810deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
811 result = cc_apt_configure.disable_suites(disabled, orig, release)
812 self.assertEqual(expect, result)
813
814 # multi line disable (same suite multiple times in input)
815 disabled = ["$RELEASE-updates", "$RELEASE-security"]
816 orig = """deb http://ubuntu.com//ubuntu xenial main
817deb http://ubuntu.com//ubuntu xenial-updates main
818deb http://ubuntu.com//ubuntu xenial-security main
819deb-src http://ubuntu.com//ubuntu universe multiverse
820deb http://UBUNTU.com//ubuntu xenial-updates main
821deb http://UBUNTU.COM//ubuntu xenial-updates main
822deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
823 expect = ("""deb http://ubuntu.com//ubuntu xenial main
824# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
825 """xenial-updates main
826# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
827 """xenial-security main
828deb-src http://ubuntu.com//ubuntu universe multiverse
829# suite disabled by cloud-init: deb http://UBUNTU.com//ubuntu """
830 """xenial-updates main
831# suite disabled by cloud-init: deb http://UBUNTU.COM//ubuntu """
832 """xenial-updates main
833deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
834 result = cc_apt_configure.disable_suites(disabled, orig, release)
835 self.assertEqual(expect, result)
836
837 # comment in input
838 disabled = ["$RELEASE-updates", "$RELEASE-security"]
839 orig = """deb http://ubuntu.com//ubuntu xenial main
840deb http://ubuntu.com//ubuntu xenial-updates main
841deb http://ubuntu.com//ubuntu xenial-security main
842deb-src http://ubuntu.com//ubuntu universe multiverse
843#foo
844#deb http://UBUNTU.com//ubuntu xenial-updates main
845deb http://UBUNTU.COM//ubuntu xenial-updates main
846deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
847 expect = ("""deb http://ubuntu.com//ubuntu xenial main
848# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
849 """xenial-updates main
850# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
851 """xenial-security main
852deb-src http://ubuntu.com//ubuntu universe multiverse
853#foo
854#deb http://UBUNTU.com//ubuntu xenial-updates main
855# suite disabled by cloud-init: deb http://UBUNTU.COM//ubuntu """
856 """xenial-updates main
857deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
858 result = cc_apt_configure.disable_suites(disabled, orig, release)
859 self.assertEqual(expect, result)
860
861 # single disable custom suite
862 disabled = ["foobar"]
863 orig = """deb http://ubuntu.com//ubuntu xenial main
864deb http://ubuntu.com//ubuntu xenial-updates main
865deb http://ubuntu.com//ubuntu xenial-security main
866deb http://ubuntu.com/ubuntu/ foobar main"""
867 expect = """deb http://ubuntu.com//ubuntu xenial main
868deb http://ubuntu.com//ubuntu xenial-updates main
869deb http://ubuntu.com//ubuntu xenial-security main
870# suite disabled by cloud-init: deb http://ubuntu.com/ubuntu/ foobar main"""
871 result = cc_apt_configure.disable_suites(disabled, orig, release)
872 self.assertEqual(expect, result)
873
874 # single disable non existing suite
875 disabled = ["foobar"]
876 orig = """deb http://ubuntu.com//ubuntu xenial main
877deb http://ubuntu.com//ubuntu xenial-updates main
878deb http://ubuntu.com//ubuntu xenial-security main
879deb http://ubuntu.com/ubuntu/ notfoobar main"""
880 expect = """deb http://ubuntu.com//ubuntu xenial main
881deb http://ubuntu.com//ubuntu xenial-updates main
882deb http://ubuntu.com//ubuntu xenial-security main
883deb http://ubuntu.com/ubuntu/ notfoobar main"""
884 result = cc_apt_configure.disable_suites(disabled, orig, release)
885 self.assertEqual(expect, result)
886
887 # single disable suite with option
888 disabled = ["$RELEASE-updates"]
889 orig = """deb http://ubuntu.com//ubuntu xenial main
890deb [a=b] http://ubu.com//ubu xenial-updates main
891deb http://ubuntu.com//ubuntu xenial-security main
892deb-src http://ubuntu.com//ubuntu universe multiverse
893deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
894 expect = ("""deb http://ubuntu.com//ubuntu xenial main
895# suite disabled by cloud-init: deb [a=b] http://ubu.com//ubu """
896 """xenial-updates main
897deb http://ubuntu.com//ubuntu xenial-security main
898deb-src http://ubuntu.com//ubuntu universe multiverse
899deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
900 result = cc_apt_configure.disable_suites(disabled, orig, release)
901 self.assertEqual(expect, result)
902
903 # single disable suite with more options and auto $RELEASE expansion
904 disabled = ["updates"]
905 orig = """deb http://ubuntu.com//ubuntu xenial main
906deb [a=b c=d] http://ubu.com//ubu xenial-updates main
907deb http://ubuntu.com//ubuntu xenial-security main
908deb-src http://ubuntu.com//ubuntu universe multiverse
909deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
910 expect = """deb http://ubuntu.com//ubuntu xenial main
911# suite disabled by cloud-init: deb [a=b c=d] \
912http://ubu.com//ubu xenial-updates main
913deb http://ubuntu.com//ubuntu xenial-security main
914deb-src http://ubuntu.com//ubuntu universe multiverse
915deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
916 result = cc_apt_configure.disable_suites(disabled, orig, release)
917 self.assertEqual(expect, result)
918
919 # single disable suite while options at others
920 disabled = ["$RELEASE-security"]
921 orig = """deb http://ubuntu.com//ubuntu xenial main
922deb [arch=foo] http://ubuntu.com//ubuntu xenial-updates main
923deb http://ubuntu.com//ubuntu xenial-security main
924deb-src http://ubuntu.com//ubuntu universe multiverse
925deb http://ubuntu.com/ubuntu/ xenial-proposed main"""
926 expect = ("""deb http://ubuntu.com//ubuntu xenial main
927deb [arch=foo] http://ubuntu.com//ubuntu xenial-updates main
928# suite disabled by cloud-init: deb http://ubuntu.com//ubuntu """
929 """xenial-security main
930deb-src http://ubuntu.com//ubuntu universe multiverse
931deb http://ubuntu.com/ubuntu/ xenial-proposed main""")
932 result = cc_apt_configure.disable_suites(disabled, orig, release)
933 self.assertEqual(expect, result)
934
935 def test_disable_suites_blank_lines(self):
936 """test_disable_suites_blank_lines - ensure blank lines allowed"""
937 lines = ["deb %(repo)s %(rel)s main universe",
938 "",
939 "deb %(repo)s %(rel)s-updates main universe",
940 " # random comment",
941 "#comment here",
942 ""]
943 rel = "trusty"
944 repo = 'http://example.com/mirrors/ubuntu'
945 orig = "\n".join(lines) % {'repo': repo, 'rel': rel}
946 self.assertEqual(
947 orig, cc_apt_configure.disable_suites(["proposed"], orig, rel))
948
949 def test_apt_v3_mirror_search_dns(self):
950 """test_apt_v3_mirror_search_dns - Test searching dns patterns"""
951 pmir = "phit"
952 smir = "shit"
953 arch = 'amd64'
954 mycloud = self._get_cloud('ubuntu')
955 cfg = {"primary": [{'arches': ["default"],
956 "search_dns": True}],
957 "security": [{'arches': ["default"],
958 "search_dns": True}]}
959
960 with mock.patch.object(cc_apt_configure, 'get_mirror',
961 return_value="http://mocked/foo") as mockgm:
962 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
963 calls = [call(cfg, 'primary', arch, mycloud),
964 call(cfg, 'security', arch, mycloud)]
965 mockgm.assert_has_calls(calls)
966
967 with mock.patch.object(cc_apt_configure, 'search_for_mirror_dns',
968 return_value="http://mocked/foo") as mocksdns:
969 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
970 calls = [call(True, 'primary', cfg, mycloud),
971 call(True, 'security', cfg, mycloud)]
972 mocksdns.assert_has_calls(calls)
973
974 # first return is for the non-dns call before
975 with mock.patch.object(cc_apt_configure, 'search_for_mirror',
976 side_effect=[None, pmir, None, smir]) as mockse:
977 mirrors = cc_apt_configure.find_apt_mirror_info(cfg, mycloud, arch)
978
979 calls = [call(None),
980 call(['http://ubuntu-mirror.localdomain/ubuntu',
981 'http://ubuntu-mirror/ubuntu']),
982 call(None),
983 call(['http://ubuntu-security-mirror.localdomain/ubuntu',
984 'http://ubuntu-security-mirror/ubuntu'])]
985 mockse.assert_has_calls(calls)
986
987 self.assertEqual(mirrors['MIRROR'],
988 pmir)
989 self.assertEqual(mirrors['PRIMARY'],
990 pmir)
991 self.assertEqual(mirrors['SECURITY'],
992 smir)
993
994
995class TestDebconfSelections(TestCase):
996
997 @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
998 def test_no_set_sel_if_none_to_set(self, m_set_sel):
999 cc_apt_configure.apply_debconf_selections({'foo': 'bar'})
1000 m_set_sel.assert_not_called()
1001
1002 @mock.patch("cloudinit.config.cc_apt_configure."
1003 "debconf_set_selections")
1004 @mock.patch("cloudinit.config.cc_apt_configure."
1005 "util.get_installed_packages")
1006 def test_set_sel_call_has_expected_input(self, m_get_inst, m_set_sel):
1007 data = {
1008 'set1': 'pkga pkga/q1 mybool false',
1009 'set2': ('pkgb\tpkgb/b1\tstr\tthis is a string\n'
1010 'pkgc\tpkgc/ip\tstring\t10.0.0.1')}
1011 lines = '\n'.join(data.values()).split('\n')
1012
1013 m_get_inst.return_value = ["adduser", "apparmor"]
1014 m_set_sel.return_value = None
1015
1016 cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
1017 self.assertTrue(m_get_inst.called)
1018 self.assertEqual(m_set_sel.call_count, 1)
1019
1020 # assumes called with *args value.
1021 selections = m_set_sel.call_args_list[0][0][0].decode()
1022
1023 missing = [l for l in lines if l not in selections.splitlines()]
1024 self.assertEqual([], missing)
1025
1026 @mock.patch("cloudinit.config.cc_apt_configure.dpkg_reconfigure")
1027 @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
1028 @mock.patch("cloudinit.config.cc_apt_configure."
1029 "util.get_installed_packages")
1030 def test_reconfigure_if_intersection(self, m_get_inst, m_set_sel,
1031 m_dpkg_r):
1032 data = {
1033 'set1': 'pkga pkga/q1 mybool false',
1034 'set2': ('pkgb\tpkgb/b1\tstr\tthis is a string\n'
1035 'pkgc\tpkgc/ip\tstring\t10.0.0.1'),
1036 'cloud-init': ('cloud-init cloud-init/datasources'
1037 'multiselect MAAS')}
1038
1039 m_set_sel.return_value = None
1040 m_get_inst.return_value = ["adduser", "apparmor", "pkgb",
1041 "cloud-init", 'zdog']
1042
1043 cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
1044
1045 # reconfigure should be called with the intersection
1046 # of (packages in config, packages installed)
1047 self.assertEqual(m_dpkg_r.call_count, 1)
1048 # assumes called with *args (dpkg_reconfigure([a,b,c], target=))
1049 packages = m_dpkg_r.call_args_list[0][0][0]
1050 self.assertEqual(set(['cloud-init', 'pkgb']), set(packages))
1051
1052 @mock.patch("cloudinit.config.cc_apt_configure.dpkg_reconfigure")
1053 @mock.patch("cloudinit.config.cc_apt_configure.debconf_set_selections")
1054 @mock.patch("cloudinit.config.cc_apt_configure."
1055 "util.get_installed_packages")
1056 def test_reconfigure_if_no_intersection(self, m_get_inst, m_set_sel,
1057 m_dpkg_r):
1058 data = {'set1': 'pkga pkga/q1 mybool false'}
1059
1060 m_get_inst.return_value = ["adduser", "apparmor", "pkgb",
1061 "cloud-init", 'zdog']
1062 m_set_sel.return_value = None
1063
1064 cc_apt_configure.apply_debconf_selections({'debconf_selections': data})
1065
1066 self.assertTrue(m_get_inst.called)
1067 self.assertEqual(m_dpkg_r.call_count, 0)
1068
1069 @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
1070 def test_dpkg_reconfigure_does_reconfigure(self, m_subp):
1071 target = "/foo-target"
1072
1073 # due to the way the cleaners are called (via dictionary reference)
1074 # mocking clean_cloud_init directly does not work. So we mock
1075 # the CONFIG_CLEANERS dictionary and assert our cleaner is called.
1076 ci_cleaner = mock.MagicMock()
1077 with mock.patch.dict(("cloudinit.config.cc_apt_configure."
1078 "CONFIG_CLEANERS"),
1079 values={'cloud-init': ci_cleaner}, clear=True):
1080 cc_apt_configure.dpkg_reconfigure(['pkga', 'cloud-init'],
1081 target=target)
1082 # cloud-init is actually the only package we have a cleaner for
1083 # so for now, its the only one that should reconfigured
1084 self.assertTrue(m_subp.called)
1085 ci_cleaner.assert_called_with(target)
1086 self.assertEqual(m_subp.call_count, 1)
1087 found = m_subp.call_args_list[0][0][0]
1088 expected = ['dpkg-reconfigure', '--frontend=noninteractive',
1089 'cloud-init']
1090 self.assertEqual(expected, found)
1091
1092 @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
1093 def test_dpkg_reconfigure_not_done_on_no_data(self, m_subp):
1094 cc_apt_configure.dpkg_reconfigure([])
1095 m_subp.assert_not_called()
1096
1097 @mock.patch("cloudinit.config.cc_apt_configure.util.subp")
1098 def test_dpkg_reconfigure_not_done_if_no_cleaners(self, m_subp):
1099 cc_apt_configure.dpkg_reconfigure(['pkgfoo', 'pkgbar'])
1100 m_subp.assert_not_called()
1101
1102#
1103# vi: ts=4 expandtab
diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py
index 73369cd..d2031f5 100644
--- a/tests/unittests/test_util.py
+++ b/tests/unittests/test_util.py
@@ -508,4 +508,73 @@ class TestReadSeeded(helpers.TestCase):
508 self.assertEqual(found_md, {'key1': 'val1'})508 self.assertEqual(found_md, {'key1': 'val1'})
509 self.assertEqual(found_ud, ud)509 self.assertEqual(found_ud, ud)
510510
511
512class TestSubp(helpers.TestCase):
513
514 stdin2err = ['bash', '-c', 'cat >&2']
515 stdin2out = ['cat']
516 utf8_invalid = b'ab\xaadef'
517 utf8_valid = b'start \xc3\xa9 end'
518 utf8_valid_2 = b'd\xc3\xa9j\xc8\xa7'
519
520 def printf_cmd(self, *args):
521 # bash's printf supports \xaa. So does /usr/bin/printf
522 # but by using bash, we remove dependency on another program.
523 return(['bash', '-c', 'printf "$@"', 'printf'] + list(args))
524
525 def test_subp_handles_utf8(self):
526 # The given bytes contain utf-8 accented characters as seen in e.g.
527 # the "deja dup" package in Ubuntu.
528 cmd = self.printf_cmd(self.utf8_valid_2)
529 (out, _err) = util.subp(cmd, capture=True)
530 self.assertEqual(out, self.utf8_valid_2.decode('utf-8'))
531
532 def test_subp_respects_decode_false(self):
533 (out, err) = util.subp(self.stdin2out, capture=True, decode=False,
534 data=self.utf8_valid)
535 self.assertTrue(isinstance(out, bytes))
536 self.assertTrue(isinstance(err, bytes))
537 self.assertEqual(out, self.utf8_valid)
538
539 def test_subp_decode_ignore(self):
540 # this executes a string that writes invalid utf-8 to stdout
541 (out, _err) = util.subp(self.printf_cmd('abc\\xaadef'),
542 capture=True, decode='ignore')
543 self.assertEqual(out, 'abcdef')
544
545 def test_subp_decode_strict_valid_utf8(self):
546 (out, _err) = util.subp(self.stdin2out, capture=True,
547 decode='strict', data=self.utf8_valid)
548 self.assertEqual(out, self.utf8_valid.decode('utf-8'))
549
550 def test_subp_decode_invalid_utf8_replaces(self):
551 (out, _err) = util.subp(self.stdin2out, capture=True,
552 data=self.utf8_invalid)
553 expected = self.utf8_invalid.decode('utf-8', errors='replace')
554 self.assertEqual(out, expected)
555
556 def test_subp_decode_strict_raises(self):
557 args = []
558 kwargs = {'args': self.stdin2out, 'capture': True,
559 'decode': 'strict', 'data': self.utf8_invalid}
560 self.assertRaises(UnicodeDecodeError, util.subp, *args, **kwargs)
561
562 def test_subp_capture_stderr(self):
563 data = b'hello world'
564 (out, err) = util.subp(self.stdin2err, capture=True,
565 decode=False, data=data)
566 self.assertEqual(err, data)
567 self.assertEqual(out, b'')
568
569 def test_returns_none_if_no_capture(self):
570 (out, err) = util.subp(self.stdin2out, data=b'', capture=False)
571 self.assertEqual(err, None)
572 self.assertEqual(out, None)
573
574 def test_bunch_of_slashes_in_path(self):
575 self.assertEqual("/target/my/path/",
576 util.target_path("/target/", "//my/path/"))
577 self.assertEqual("/target/my/path/",
578 util.target_path("/target/", "///my/path/"))
579
511# vi: ts=4 expandtab580# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches