Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel
- Git
- lp:~chad.smith/cloud-init
- ubuntu/devel
- Merge into ubuntu/devel
Proposed by
Chad Smith
Status: | Merged |
---|---|
Merged at revision: | b602a10c0620f77882113e14c145b0877a455b02 |
Proposed branch: | ~chad.smith/cloud-init:ubuntu/devel |
Merge into: | cloud-init:ubuntu/devel |
Diff against target: |
2392 lines (+1470/-394) 27 files modified
cloudinit/apport.py (+23/-4) cloudinit/config/cc_ntp.py (+407/-78) cloudinit/distros/__init__.py (+12/-0) cloudinit/distros/opensuse.py (+24/-0) cloudinit/distros/ubuntu.py (+19/-0) cloudinit/templater.py (+9/-1) config/cloud.cfg.tmpl (+2/-0) debian/changelog (+12/-0) templates/chrony.conf.debian.tmpl (+39/-0) templates/chrony.conf.fedora.tmpl (+48/-0) templates/chrony.conf.opensuse.tmpl (+38/-0) templates/chrony.conf.rhel.tmpl (+45/-0) templates/chrony.conf.sles.tmpl (+38/-0) templates/chrony.conf.ubuntu.tmpl (+42/-0) tests/cloud_tests/testcases/base.py (+5/-4) tests/cloud_tests/testcases/modules/ntp.yaml (+1/-0) tests/cloud_tests/testcases/modules/ntp_chrony.py (+15/-0) tests/cloud_tests/testcases/modules/ntp_chrony.yaml (+17/-0) tests/cloud_tests/testcases/modules/ntp_pools.yaml (+1/-0) tests/cloud_tests/testcases/modules/ntp_servers.yaml (+1/-0) tests/cloud_tests/testcases/modules/ntp_timesyncd.py (+15/-0) tests/cloud_tests/testcases/modules/ntp_timesyncd.yaml (+15/-0) tests/unittests/test_distros/test_netconfig.py (+6/-0) tests/unittests/test_distros/test_user_data_normalize.py (+6/-0) tests/unittests/test_handler/test_handler_ntp.py (+578/-303) tests/unittests/test_templating.py (+40/-1) tools/make-tarball (+12/-3) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Scott Moser | Pending | ||
Review via email:
|
Commit message
Sync bug fixes from tip into ubuntu/devel for Bionic FFe release.
Description of the change
To post a comment you must log in.
Revision history for this message
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/apport.py b/cloudinit/apport.py |
2 | index 618b016..130ff26 100644 |
3 | --- a/cloudinit/apport.py |
4 | +++ b/cloudinit/apport.py |
5 | @@ -13,10 +13,29 @@ except ImportError: |
6 | |
7 | |
8 | KNOWN_CLOUD_NAMES = [ |
9 | - 'Amazon - Ec2', 'AliYun', 'AltCloud', 'Azure', 'Bigstep', 'CloudSigma', |
10 | - 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', |
11 | - 'Hetzner Cloud', 'MAAS', 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', |
12 | - 'Scaleway', 'SmartOS', 'VMware', 'Other'] |
13 | + 'AliYun', |
14 | + 'AltCloud', |
15 | + 'Amazon - Ec2', |
16 | + 'Azure', |
17 | + 'Bigstep', |
18 | + 'Brightbox', |
19 | + 'CloudSigma', |
20 | + 'CloudStack', |
21 | + 'DigitalOcean', |
22 | + 'GCE - Google Compute Engine', |
23 | + 'Hetzner Cloud', |
24 | + 'IBM - (aka SoftLayer or BlueMix)', |
25 | + 'LXD', |
26 | + 'MAAS', |
27 | + 'NoCloud', |
28 | + 'OpenNebula', |
29 | + 'OpenStack', |
30 | + 'OVF', |
31 | + 'OpenTelekomCloud', |
32 | + 'Scaleway', |
33 | + 'SmartOS', |
34 | + 'VMware', |
35 | + 'Other'] |
36 | |
37 | # Potentially clear text collected logs |
38 | CLOUDINIT_LOG = '/var/log/cloud-init.log' |
39 | diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py |
40 | index cbd0237..9e074bd 100644 |
41 | --- a/cloudinit/config/cc_ntp.py |
42 | +++ b/cloudinit/config/cc_ntp.py |
43 | @@ -10,20 +10,95 @@ from cloudinit.config.schema import ( |
44 | get_schema_doc, validate_cloudconfig_schema) |
45 | from cloudinit import log as logging |
46 | from cloudinit.settings import PER_INSTANCE |
47 | +from cloudinit import temp_utils |
48 | from cloudinit import templater |
49 | from cloudinit import type_utils |
50 | from cloudinit import util |
51 | |
52 | +import copy |
53 | import os |
54 | +import six |
55 | from textwrap import dedent |
56 | |
57 | LOG = logging.getLogger(__name__) |
58 | |
59 | frequency = PER_INSTANCE |
60 | NTP_CONF = '/etc/ntp.conf' |
61 | -TIMESYNCD_CONF = '/etc/systemd/timesyncd.conf.d/cloud-init.conf' |
62 | NR_POOL_SERVERS = 4 |
63 | -distros = ['centos', 'debian', 'fedora', 'opensuse', 'sles', 'ubuntu'] |
64 | +distros = ['centos', 'debian', 'fedora', 'opensuse', 'rhel', 'sles', 'ubuntu'] |
65 | + |
66 | +NTP_CLIENT_CONFIG = { |
67 | + 'chrony': { |
68 | + 'check_exe': 'chronyd', |
69 | + 'confpath': '/etc/chrony.conf', |
70 | + 'packages': ['chrony'], |
71 | + 'service_name': 'chrony', |
72 | + 'template_name': 'chrony.conf.{distro}', |
73 | + 'template': None, |
74 | + }, |
75 | + 'ntp': { |
76 | + 'check_exe': 'ntpd', |
77 | + 'confpath': NTP_CONF, |
78 | + 'packages': ['ntp'], |
79 | + 'service_name': 'ntp', |
80 | + 'template_name': 'ntp.conf.{distro}', |
81 | + 'template': None, |
82 | + }, |
83 | + 'ntpdate': { |
84 | + 'check_exe': 'ntpdate', |
85 | + 'confpath': NTP_CONF, |
86 | + 'packages': ['ntpdate'], |
87 | + 'service_name': 'ntpdate', |
88 | + 'template_name': 'ntp.conf.{distro}', |
89 | + 'template': None, |
90 | + }, |
91 | + 'systemd-timesyncd': { |
92 | + 'check_exe': '/lib/systemd/systemd-timesyncd', |
93 | + 'confpath': '/etc/systemd/timesyncd.conf.d/cloud-init.conf', |
94 | + 'packages': [], |
95 | + 'service_name': 'systemd-timesyncd', |
96 | + 'template_name': 'timesyncd.conf', |
97 | + 'template': None, |
98 | + }, |
99 | +} |
100 | + |
101 | +# This is Distro-specific configuration overrides of the base config |
102 | +DISTRO_CLIENT_CONFIG = { |
103 | + 'debian': { |
104 | + 'chrony': { |
105 | + 'confpath': '/etc/chrony/chrony.conf', |
106 | + }, |
107 | + }, |
108 | + 'opensuse': { |
109 | + 'chrony': { |
110 | + 'service_name': 'chronyd', |
111 | + }, |
112 | + 'ntp': { |
113 | + 'confpath': '/etc/ntp.conf', |
114 | + 'service_name': 'ntpd', |
115 | + }, |
116 | + 'systemd-timesyncd': { |
117 | + 'check_exe': '/usr/lib/systemd/systemd-timesyncd', |
118 | + }, |
119 | + }, |
120 | + 'sles': { |
121 | + 'chrony': { |
122 | + 'service_name': 'chronyd', |
123 | + }, |
124 | + 'ntp': { |
125 | + 'confpath': '/etc/ntp.conf', |
126 | + 'service_name': 'ntpd', |
127 | + }, |
128 | + 'systemd-timesyncd': { |
129 | + 'check_exe': '/usr/lib/systemd/systemd-timesyncd', |
130 | + }, |
131 | + }, |
132 | + 'ubuntu': { |
133 | + 'chrony': { |
134 | + 'confpath': '/etc/chrony/chrony.conf', |
135 | + }, |
136 | + }, |
137 | +} |
138 | |
139 | |
140 | # The schema definition for each cloud-config module is a strict contract for |
141 | @@ -48,7 +123,34 @@ schema = { |
142 | 'distros': distros, |
143 | 'examples': [ |
144 | dedent("""\ |
145 | + # Override ntp with chrony configuration on Ubuntu |
146 | + ntp: |
147 | + enabled: true |
148 | + ntp_client: chrony # Uses cloud-init default chrony configuration |
149 | + """), |
150 | + dedent("""\ |
151 | + # Provide a custom ntp client configuration |
152 | ntp: |
153 | + enabled: true |
154 | + ntp_client: myntpclient |
155 | + config: |
156 | + confpath: /etc/myntpclient/myntpclient.conf |
157 | + check_exe: myntpclientd |
158 | + packages: |
159 | + - myntpclient |
160 | + service_name: myntpclient |
161 | + template: | |
162 | + ## template:jinja |
163 | + # My NTP Client config |
164 | + {% if pools -%}# pools{% endif %} |
165 | + {% for pool in pools -%} |
166 | + pool {{pool}} iburst |
167 | + {% endfor %} |
168 | + {%- if servers %}# servers |
169 | + {% endif %} |
170 | + {% for server in servers -%} |
171 | + server {{server}} iburst |
172 | + {% endfor %} |
173 | pools: [0.int.pool.ntp.org, 1.int.pool.ntp.org, ntp.myorg.org] |
174 | servers: |
175 | - ntp.server.local |
176 | @@ -83,79 +185,159 @@ schema = { |
177 | List of ntp servers. If both pools and servers are |
178 | empty, 4 default pool servers will be provided with |
179 | the format ``{0-3}.{distro}.pool.ntp.org``.""") |
180 | - } |
181 | + }, |
182 | + 'ntp_client': { |
183 | + 'type': 'string', |
184 | + 'default': 'auto', |
185 | + 'description': dedent("""\ |
186 | + Name of an NTP client to use to configure system NTP. |
187 | + When unprovided or 'auto' the default client preferred |
188 | + by the distribution will be used. The following |
189 | + built-in client names can be used to override existing |
190 | + configuration defaults: chrony, ntp, ntpdate, |
191 | + systemd-timesyncd."""), |
192 | + }, |
193 | + 'enabled': { |
194 | + 'type': 'boolean', |
195 | + 'default': True, |
196 | + 'description': dedent("""\ |
197 | + Attempt to enable ntp clients if set to True. If set |
198 | + to False, ntp client will not be configured or |
199 | + installed"""), |
200 | + }, |
201 | + 'config': { |
202 | + 'description': dedent("""\ |
203 | + Configuration settings or overrides for the |
204 | + ``ntp_client`` specified."""), |
205 | + 'type': ['object'], |
206 | + 'properties': { |
207 | + 'confpath': { |
208 | + 'type': 'string', |
209 | + 'description': dedent("""\ |
210 | + The path to where the ``ntp_client`` |
211 | + configuration is written."""), |
212 | + }, |
213 | + 'check_exe': { |
214 | + 'type': 'string', |
215 | + 'description': dedent("""\ |
216 | + The executable name for the ``ntp_client``. |
217 | + For example, ntp service ``check_exe`` is |
218 | + 'ntpd' because it runs the ntpd binary."""), |
219 | + }, |
220 | + 'packages': { |
221 | + 'type': 'array', |
222 | + 'items': { |
223 | + 'type': 'string', |
224 | + }, |
225 | + 'uniqueItems': True, |
226 | + 'description': dedent("""\ |
227 | + List of packages needed to be installed for the |
228 | + selected ``ntp_client``."""), |
229 | + }, |
230 | + 'service_name': { |
231 | + 'type': 'string', |
232 | + 'description': dedent("""\ |
233 | + The systemd or sysvinit service name used to |
234 | + start and stop the ``ntp_client`` |
235 | + service."""), |
236 | + }, |
237 | + 'template': { |
238 | + 'type': 'string', |
239 | + 'description': dedent("""\ |
240 | + Inline template allowing users to define their |
241 | + own ``ntp_client`` configuration template. |
242 | + The value must start with '## template:jinja' |
243 | + to enable use of templating support. |
244 | + """), |
245 | + }, |
246 | + }, |
247 | + # Don't use REQUIRED_NTP_CONFIG_KEYS to allow for override |
248 | + # of builtin client values. |
249 | + 'required': [], |
250 | + 'minProperties': 1, # If we have config, define something |
251 | + 'additionalProperties': False |
252 | + }, |
253 | }, |
254 | 'required': [], |
255 | 'additionalProperties': False |
256 | } |
257 | } |
258 | } |
259 | - |
260 | -__doc__ = get_schema_doc(schema) # Supplement python help() |
261 | +REQUIRED_NTP_CONFIG_KEYS = frozenset([ |
262 | + 'check_exe', 'confpath', 'packages', 'service_name']) |
263 | |
264 | |
265 | -def handle(name, cfg, cloud, log, _args): |
266 | - """Enable and configure ntp.""" |
267 | - if 'ntp' not in cfg: |
268 | - LOG.debug( |
269 | - "Skipping module named %s, not present or disabled by cfg", name) |
270 | - return |
271 | - ntp_cfg = cfg['ntp'] |
272 | - if ntp_cfg is None: |
273 | - ntp_cfg = {} # Allow empty config which will install the package |
274 | +__doc__ = get_schema_doc(schema) # Supplement python help() |
275 | |
276 | - # TODO drop this when validate_cloudconfig_schema is strict=True |
277 | - if not isinstance(ntp_cfg, (dict)): |
278 | - raise RuntimeError( |
279 | - "'ntp' key existed in config, but not a dictionary type," |
280 | - " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg))) |
281 | |
282 | - validate_cloudconfig_schema(cfg, schema) |
283 | - if ntp_installable(): |
284 | - service_name = 'ntp' |
285 | - confpath = NTP_CONF |
286 | - template_name = None |
287 | - packages = ['ntp'] |
288 | - check_exe = 'ntpd' |
289 | - else: |
290 | - service_name = 'systemd-timesyncd' |
291 | - confpath = TIMESYNCD_CONF |
292 | - template_name = 'timesyncd.conf' |
293 | - packages = [] |
294 | - check_exe = '/lib/systemd/systemd-timesyncd' |
295 | - |
296 | - rename_ntp_conf() |
297 | - # ensure when ntp is installed it has a configuration file |
298 | - # to use instead of starting up with packaged defaults |
299 | - write_ntp_config_template(ntp_cfg, cloud, confpath, template=template_name) |
300 | - install_ntp(cloud.distro.install_packages, packages=packages, |
301 | - check_exe=check_exe) |
302 | +def distro_ntp_client_configs(distro): |
303 | + """Construct a distro-specific ntp client config dictionary by merging |
304 | + distro specific changes into base config. |
305 | |
306 | - try: |
307 | - reload_ntp(service_name, systemd=cloud.distro.uses_systemd()) |
308 | - except util.ProcessExecutionError as e: |
309 | - LOG.exception("Failed to reload/start ntp service: %s", e) |
310 | - raise |
311 | + @param distro: String providing the distro class name. |
312 | + @returns: Dict of distro configurations for ntp clients. |
313 | + """ |
314 | + dcfg = DISTRO_CLIENT_CONFIG |
315 | + cfg = copy.copy(NTP_CLIENT_CONFIG) |
316 | + if distro in dcfg: |
317 | + cfg = util.mergemanydict([cfg, dcfg[distro]], reverse=True) |
318 | + return cfg |
319 | |
320 | |
321 | -def ntp_installable(): |
322 | - """Check if we can install ntp package |
323 | +def select_ntp_client(ntp_client, distro): |
324 | + """Determine which ntp client is to be used, consulting the distro |
325 | + for its preference. |
326 | |
327 | - Ubuntu-Core systems do not have an ntp package available, so |
328 | - we always return False. Other systems require package managers to install |
329 | - the ntp package If we fail to find one of the package managers, then we |
330 | - cannot install ntp. |
331 | + @param ntp_client: String name of the ntp client to use. |
332 | + @param distro: Distro class instance. |
333 | + @returns: Dict of the selected ntp client or {} if none selected. |
334 | """ |
335 | - if util.system_is_snappy(): |
336 | - return False |
337 | |
338 | - if any(map(util.which, ['apt-get', 'dnf', 'yum', 'zypper'])): |
339 | - return True |
340 | + # construct distro-specific ntp_client_config dict |
341 | + distro_cfg = distro_ntp_client_configs(distro.name) |
342 | + |
343 | + # user specified client, return its config |
344 | + if ntp_client and ntp_client != 'auto': |
345 | + LOG.debug('Selected NTP client "%s" via user-data configuration', |
346 | + ntp_client) |
347 | + return distro_cfg.get(ntp_client, {}) |
348 | + |
349 | + # default to auto if unset in distro |
350 | + distro_ntp_client = distro.get_option('ntp_client', 'auto') |
351 | + |
352 | + clientcfg = {} |
353 | + if distro_ntp_client == "auto": |
354 | + for client in distro.preferred_ntp_clients: |
355 | + cfg = distro_cfg.get(client) |
356 | + if util.which(cfg.get('check_exe')): |
357 | + LOG.debug('Selected NTP client "%s", already installed', |
358 | + client) |
359 | + clientcfg = cfg |
360 | + break |
361 | + |
362 | + if not clientcfg: |
363 | + client = distro.preferred_ntp_clients[0] |
364 | + LOG.debug( |
365 | + 'Selected distro preferred NTP client "%s", not yet installed', |
366 | + client) |
367 | + clientcfg = distro_cfg.get(client) |
368 | + else: |
369 | + LOG.debug('Selected NTP client "%s" via distro system config', |
370 | + distro_ntp_client) |
371 | + clientcfg = distro_cfg.get(distro_ntp_client, {}) |
372 | + |
373 | + return clientcfg |
374 | |
375 | - return False |
376 | |
377 | +def install_ntp_client(install_func, packages=None, check_exe="ntpd"): |
378 | + """Install ntp client package if not already installed. |
379 | |
380 | -def install_ntp(install_func, packages=None, check_exe="ntpd"): |
381 | + @param install_func: function. This parameter is invoked with the contents |
382 | + of the packages parameter. |
383 | + @param packages: list. This parameter defaults to ['ntp']. |
384 | + @param check_exe: string. The name of a binary that indicates the package |
385 | + the specified package is already installed. |
386 | + """ |
387 | if util.which(check_exe): |
388 | return |
389 | if packages is None: |
390 | @@ -164,15 +346,23 @@ def install_ntp(install_func, packages=None, check_exe="ntpd"): |
391 | install_func(packages) |
392 | |
393 | |
394 | -def rename_ntp_conf(config=None): |
395 | - """Rename any existing ntp.conf file""" |
396 | - if config is None: # For testing |
397 | - config = NTP_CONF |
398 | - if os.path.exists(config): |
399 | - util.rename(config, config + ".dist") |
400 | +def rename_ntp_conf(confpath=None): |
401 | + """Rename any existing ntp client config file |
402 | + |
403 | + @param confpath: string. Specify a path to an existing ntp client |
404 | + configuration file. |
405 | + """ |
406 | + if os.path.exists(confpath): |
407 | + util.rename(confpath, confpath + ".dist") |
408 | |
409 | |
410 | def generate_server_names(distro): |
411 | + """Generate a list of server names to populate an ntp client configuration |
412 | + file. |
413 | + |
414 | + @param distro: string. Specify the distro name |
415 | + @returns: list: A list of strings representing ntp servers for this distro. |
416 | + """ |
417 | names = [] |
418 | pool_distro = distro |
419 | # For legal reasons x.pool.sles.ntp.org does not exist, |
420 | @@ -185,34 +375,60 @@ def generate_server_names(distro): |
421 | return names |
422 | |
423 | |
424 | -def write_ntp_config_template(cfg, cloud, path, template=None): |
425 | - servers = cfg.get('servers', []) |
426 | - pools = cfg.get('pools', []) |
427 | +def write_ntp_config_template(distro_name, servers=None, pools=None, |
428 | + path=None, template_fn=None, template=None): |
429 | + """Render a ntp client configuration for the specified client. |
430 | + |
431 | + @param distro_name: string. The distro class name. |
432 | + @param servers: A list of strings specifying ntp servers. Defaults to empty |
433 | + list. |
434 | + @param pools: A list of strings specifying ntp pools. Defaults to empty |
435 | + list. |
436 | + @param path: A string to specify where to write the rendered template. |
437 | + @param template_fn: A string to specify the template source file. |
438 | + @param template: A string specifying the contents of the template. This |
439 | + content will be written to a temporary file before being used to render |
440 | + the configuration file. |
441 | + |
442 | + @raises: ValueError when path is None. |
443 | + @raises: ValueError when template_fn is None and template is None. |
444 | + """ |
445 | + if not servers: |
446 | + servers = [] |
447 | + if not pools: |
448 | + pools = [] |
449 | |
450 | if len(servers) == 0 and len(pools) == 0: |
451 | - pools = generate_server_names(cloud.distro.name) |
452 | + pools = generate_server_names(distro_name) |
453 | LOG.debug( |
454 | 'Adding distro default ntp pool servers: %s', ','.join(pools)) |
455 | |
456 | - params = { |
457 | - 'servers': servers, |
458 | - 'pools': pools, |
459 | - } |
460 | + if not path: |
461 | + raise ValueError('Invalid value for path parameter') |
462 | |
463 | - if template is None: |
464 | - template = 'ntp.conf.%s' % cloud.distro.name |
465 | + if not template_fn and not template: |
466 | + raise ValueError('Not template_fn or template provided') |
467 | |
468 | - template_fn = cloud.get_template_filename(template) |
469 | - if not template_fn: |
470 | - template_fn = cloud.get_template_filename('ntp.conf') |
471 | - if not template_fn: |
472 | - raise RuntimeError( |
473 | - 'No template found, not rendering {path}'.format(path=path)) |
474 | + params = {'servers': servers, 'pools': pools} |
475 | + if template: |
476 | + tfile = temp_utils.mkstemp(prefix='template_name-', suffix=".tmpl") |
477 | + template_fn = tfile[1] # filepath is second item in tuple |
478 | + util.write_file(template_fn, content=template) |
479 | |
480 | templater.render_to_file(template_fn, path, params) |
481 | + # clean up temporary template |
482 | + if template: |
483 | + util.del_file(template_fn) |
484 | |
485 | |
486 | def reload_ntp(service, systemd=False): |
487 | + """Restart or reload an ntp system service. |
488 | + |
489 | + @param service: A string specifying the name of the service to be affected. |
490 | + @param systemd: A boolean indicating if the distro uses systemd, defaults |
491 | + to False. |
492 | + @returns: A tuple of stdout, stderr results from executing the action. |
493 | + """ |
494 | if systemd: |
495 | cmd = ['systemctl', 'reload-or-restart', service] |
496 | else: |
497 | @@ -220,4 +436,117 @@ def reload_ntp(service, systemd=False): |
498 | util.subp(cmd, capture=True) |
499 | |
500 | |
501 | +def supplemental_schema_validation(ntp_config): |
502 | + """Validate user-provided ntp:config option values. |
503 | + |
504 | + This function supplements flexible jsonschema validation with specific |
505 | + value checks to aid in triage of invalid user-provided configuration. |
506 | + |
507 | + @param ntp_config: Dictionary of configuration value under 'ntp'. |
508 | + |
509 | + @raises: ValueError describing invalid values provided. |
510 | + """ |
511 | + errors = [] |
512 | + missing = REQUIRED_NTP_CONFIG_KEYS.difference(set(ntp_config.keys())) |
513 | + if missing: |
514 | + keys = ', '.join(sorted(missing)) |
515 | + errors.append( |
516 | + 'Missing required ntp:config keys: {keys}'.format(keys=keys)) |
517 | + elif not any([ntp_config.get('template'), |
518 | + ntp_config.get('template_name')]): |
519 | + errors.append( |
520 | + 'Either ntp:config:template or ntp:config:template_name values' |
521 | + ' are required') |
522 | + for key, value in sorted(ntp_config.items()): |
523 | + keypath = 'ntp:config:' + key |
524 | + if key == 'confpath': |
525 | + if not all([value, isinstance(value, six.string_types)]): |
526 | + errors.append( |
527 | + 'Expected a config file path {keypath}.' |
528 | + ' Found ({value})'.format(keypath=keypath, value=value)) |
529 | + elif key == 'packages': |
530 | + if not isinstance(value, list): |
531 | + errors.append( |
532 | + 'Expected a list of required package names for {keypath}.' |
533 | + ' Found ({value})'.format(keypath=keypath, value=value)) |
534 | + elif key in ('template', 'template_name'): |
535 | + if value is None: # Either template or template_name can be none |
536 | + continue |
537 | + if not isinstance(value, six.string_types): |
538 | + errors.append( |
539 | + 'Expected a string type for {keypath}.' |
540 | + ' Found ({value})'.format(keypath=keypath, value=value)) |
541 | + elif not isinstance(value, six.string_types): |
542 | + errors.append( |
543 | + 'Expected a string type for {keypath}.' |
544 | + ' Found ({value})'.format(keypath=keypath, value=value)) |
545 | + |
546 | + if errors: |
547 | + raise ValueError(r'Invalid ntp configuration:\n{errors}'.format( |
548 | + errors='\n'.join(errors))) |
549 | + |
550 | + |
551 | +def handle(name, cfg, cloud, log, _args): |
552 | + """Enable and configure ntp.""" |
553 | + if 'ntp' not in cfg: |
554 | + LOG.debug( |
555 | + "Skipping module named %s, not present or disabled by cfg", name) |
556 | + return |
557 | + ntp_cfg = cfg['ntp'] |
558 | + if ntp_cfg is None: |
559 | + ntp_cfg = {} # Allow empty config which will install the package |
560 | + |
561 | + # TODO drop this when validate_cloudconfig_schema is strict=True |
562 | + if not isinstance(ntp_cfg, (dict)): |
563 | + raise RuntimeError( |
564 | + "'ntp' key existed in config, but not a dictionary type," |
565 | + " is a {_type} instead".format(_type=type_utils.obj_name(ntp_cfg))) |
566 | + |
567 | + validate_cloudconfig_schema(cfg, schema) |
568 | + |
569 | + # Allow users to explicitly enable/disable |
570 | + enabled = ntp_cfg.get('enabled', True) |
571 | + if util.is_false(enabled): |
572 | + LOG.debug("Skipping module named %s, disabled by cfg", name) |
573 | + return |
574 | + |
575 | + # Select which client is going to be used and get the configuration |
576 | + ntp_client_config = select_ntp_client(ntp_cfg.get('ntp_client'), |
577 | + cloud.distro) |
578 | + |
579 | + # Allow user ntp config to override distro configurations |
580 | + ntp_client_config = util.mergemanydict( |
581 | + [ntp_client_config, ntp_cfg.get('config', {})], reverse=True) |
582 | + |
583 | + supplemental_schema_validation(ntp_client_config) |
584 | + rename_ntp_conf(confpath=ntp_client_config.get('confpath')) |
585 | + |
586 | + template_fn = None |
587 | + if not ntp_client_config.get('template'): |
588 | + template_name = ( |
589 | + ntp_client_config.get('template_name').replace('{distro}', |
590 | + cloud.distro.name)) |
591 | + template_fn = cloud.get_template_filename(template_name) |
592 | + if not template_fn: |
593 | + msg = ('No template found, not rendering %s' % |
594 | + ntp_client_config.get('template_name')) |
595 | + raise RuntimeError(msg) |
596 | + |
597 | + write_ntp_config_template(cloud.distro.name, |
598 | + servers=ntp_cfg.get('servers', []), |
599 | + pools=ntp_cfg.get('pools', []), |
600 | + path=ntp_client_config.get('confpath'), |
601 | + template_fn=template_fn, |
602 | + template=ntp_client_config.get('template')) |
603 | + |
604 | + install_ntp_client(cloud.distro.install_packages, |
605 | + packages=ntp_client_config['packages'], |
606 | + check_exe=ntp_client_config['check_exe']) |
607 | + try: |
608 | + reload_ntp(ntp_client_config['service_name'], |
609 | + systemd=cloud.distro.uses_systemd()) |
610 | + except util.ProcessExecutionError as e: |
611 | + LOG.exception("Failed to reload/start ntp service: %s", e) |
612 | + raise |
613 | + |
614 | # vi: ts=4 expandtab |
615 | diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py |
616 | index 55260ea..6c22b07 100755 |
617 | --- a/cloudinit/distros/__init__.py |
618 | +++ b/cloudinit/distros/__init__.py |
619 | @@ -49,6 +49,9 @@ LOG = logging.getLogger(__name__) |
620 | # It could break when Amazon adds new regions and new AZs. |
621 | _EC2_AZ_RE = re.compile('^[a-z][a-z]-(?:[a-z]+-)+[0-9][a-z]$') |
622 | |
623 | +# Default NTP Client Configurations |
624 | +PREFERRED_NTP_CLIENTS = ['chrony', 'systemd-timesyncd', 'ntp', 'ntpdate'] |
625 | + |
626 | |
627 | @six.add_metaclass(abc.ABCMeta) |
628 | class Distro(object): |
629 | @@ -60,6 +63,7 @@ class Distro(object): |
630 | tz_zone_dir = "/usr/share/zoneinfo" |
631 | init_cmd = ['service'] # systemctl, service etc |
632 | renderer_configs = {} |
633 | + _preferred_ntp_clients = None |
634 | |
635 | def __init__(self, name, cfg, paths): |
636 | self._paths = paths |
637 | @@ -339,6 +343,14 @@ class Distro(object): |
638 | contents.write("%s\n" % (eh)) |
639 | util.write_file(self.hosts_fn, contents.getvalue(), mode=0o644) |
640 | |
641 | + @property |
642 | + def preferred_ntp_clients(self): |
643 | + """Allow distro to determine the preferred ntp client list""" |
644 | + if not self._preferred_ntp_clients: |
645 | + self._preferred_ntp_clients = list(PREFERRED_NTP_CLIENTS) |
646 | + |
647 | + return self._preferred_ntp_clients |
648 | + |
649 | def _bring_up_interface(self, device_name): |
650 | cmd = ['ifup', device_name] |
651 | LOG.debug("Attempting to run bring up interface %s using command %s", |
652 | diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py |
653 | index 162dfa0..9f90e95 100644 |
654 | --- a/cloudinit/distros/opensuse.py |
655 | +++ b/cloudinit/distros/opensuse.py |
656 | @@ -208,4 +208,28 @@ class Distro(distros.Distro): |
657 | nameservers, searchservers) |
658 | return dev_names |
659 | |
660 | + @property |
661 | + def preferred_ntp_clients(self): |
662 | + """The preferred ntp client is dependent on the version.""" |
663 | + |
664 | + """Allow distro to determine the preferred ntp client list""" |
665 | + if not self._preferred_ntp_clients: |
666 | + distro_info = util.system_info()['dist'] |
667 | + name = distro_info[0] |
668 | + major_ver = int(distro_info[1].split('.')[0]) |
669 | + |
670 | + # This is horribly complicated because of a case of |
671 | + # "we do not care if versions should be increasing syndrome" |
672 | + if ( |
673 | + (major_ver >= 15 and 'openSUSE' not in name) or |
674 | + (major_ver >= 15 and 'openSUSE' in name and major_ver != 42) |
675 | + ): |
676 | + self._preferred_ntp_clients = ['chrony', |
677 | + 'systemd-timesyncd', 'ntp'] |
678 | + else: |
679 | + self._preferred_ntp_clients = ['ntp', |
680 | + 'systemd-timesyncd', 'chrony'] |
681 | + |
682 | + return self._preferred_ntp_clients |
683 | + |
684 | # vi: ts=4 expandtab |
685 | diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py |
686 | index 82ca34f..fdc1f62 100644 |
687 | --- a/cloudinit/distros/ubuntu.py |
688 | +++ b/cloudinit/distros/ubuntu.py |
689 | @@ -10,12 +10,31 @@ |
690 | # This file is part of cloud-init. See LICENSE file for license information. |
691 | |
692 | from cloudinit.distros import debian |
693 | +from cloudinit.distros import PREFERRED_NTP_CLIENTS |
694 | from cloudinit import log as logging |
695 | +from cloudinit import util |
696 | + |
697 | +import copy |
698 | |
699 | LOG = logging.getLogger(__name__) |
700 | |
701 | |
702 | class Distro(debian.Distro): |
703 | + |
704 | + @property |
705 | + def preferred_ntp_clients(self): |
706 | + """The preferred ntp client is dependent on the version.""" |
707 | + if not self._preferred_ntp_clients: |
708 | + (name, version, codename) = util.system_info()['dist'] |
709 | + # Xenial cloud-init only installed ntp, UbuntuCore has timesyncd. |
710 | + if codename == "xenial" and not util.system_is_snappy(): |
711 | + self._preferred_ntp_clients = ['ntp'] |
712 | + else: |
713 | + self._preferred_ntp_clients = ( |
714 | + copy.deepcopy(PREFERRED_NTP_CLIENTS)) |
715 | + return self._preferred_ntp_clients |
716 | + |
717 | pass |
718 | |
719 | + |
720 | # vi: ts=4 expandtab |
721 | diff --git a/cloudinit/templater.py b/cloudinit/templater.py |
722 | index b3ea64e..9a087e1 100644 |
723 | --- a/cloudinit/templater.py |
724 | +++ b/cloudinit/templater.py |
725 | @@ -121,7 +121,11 @@ def detect_template(text): |
726 | def render_from_file(fn, params): |
727 | if not params: |
728 | params = {} |
729 | - template_type, renderer, content = detect_template(util.load_file(fn)) |
730 | + # jinja in python2 uses unicode internally. All py2 str will be decoded. |
731 | + # If it is given a str that has non-ascii then it will raise a |
732 | + # UnicodeDecodeError. So we explicitly convert to unicode type here. |
733 | + template_type, renderer, content = detect_template( |
734 | + util.load_file(fn, decode=False).decode('utf-8')) |
735 | LOG.debug("Rendering content of '%s' using renderer %s", fn, template_type) |
736 | return renderer(content, params) |
737 | |
738 | @@ -132,11 +136,15 @@ def render_to_file(fn, outfn, params, mode=0o644): |
739 | |
740 | |
741 | def render_string_to_file(content, outfn, params, mode=0o644): |
742 | + """Render string (or py2 unicode) to file. |
743 | + Warning: py2 str with non-ascii chars will cause UnicodeDecodeError.""" |
744 | contents = render_string(content, params) |
745 | util.write_file(outfn, contents, mode=mode) |
746 | |
747 | |
748 | def render_string(content, params): |
749 | + """Render string (or py2 unicode). |
750 | + Warning: py2 str with non-ascii chars will cause UnicodeDecodeError.""" |
751 | if not params: |
752 | params = {} |
753 | template_type, renderer, content = detect_template(content) |
754 | diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl |
755 | index 3129d4e..5619de3 100644 |
756 | --- a/config/cloud.cfg.tmpl |
757 | +++ b/config/cloud.cfg.tmpl |
758 | @@ -151,6 +151,8 @@ system_info: |
759 | groups: [adm, audio, cdrom, dialout, dip, floppy, lxd, netdev, plugdev, sudo, video] |
760 | sudo: ["ALL=(ALL) NOPASSWD:ALL"] |
761 | shell: /bin/bash |
762 | + # Automatically discover the best ntp_client |
763 | + ntp_client: auto |
764 | # Other config here will be given to the distro class and/or path classes |
765 | paths: |
766 | cloud_dir: /var/lib/cloud/ |
767 | diff --git a/debian/changelog b/debian/changelog |
768 | index 390f345..d3a4234 100644 |
769 | --- a/debian/changelog |
770 | +++ b/debian/changelog |
771 | @@ -1,3 +1,15 @@ |
772 | +cloud-init (18.2-9-g49b562c9-0ubuntu1) bionic; urgency=medium |
773 | + |
774 | + * New upstream snapshot. |
775 | + - tools: Fix make-tarball cli tool usage for development |
776 | + - renderer: support unicode in render_from_file. |
777 | + - Implement ntp client spec with auto support for distro selection |
778 | + (LP: #1749722) |
779 | + - Apport: add Brightbox, IBM, LXD, and OpenTelekomCloud to list of clouds. |
780 | + - tests: fix ec2 integration network metadata validation |
781 | + |
782 | + -- Chad Smith <chad.smith@canonical.com> Thu, 12 Apr 2018 16:06:24 -0600 |
783 | + |
784 | cloud-init (18.2-4-g05926e48-0ubuntu2) bionic; urgency=medium |
785 | |
786 | * debian/cloud-init.templates: enable IBMCloud by default (LP: #1762773). |
787 | diff --git a/templates/chrony.conf.debian.tmpl b/templates/chrony.conf.debian.tmpl |
788 | new file mode 100644 |
789 | index 0000000..661bf04 |
790 | --- /dev/null |
791 | +++ b/templates/chrony.conf.debian.tmpl |
792 | @@ -0,0 +1,39 @@ |
793 | +## template:jinja |
794 | +# Welcome to the chrony configuration file. See chrony.conf(5) for more |
795 | +# information about usuable directives. |
796 | +{% if pools %}# pools |
797 | +{% endif %} |
798 | +{% for pool in pools -%} |
799 | +pool {{pool}} iburst |
800 | +{% endfor %} |
801 | +{%- if servers %}# servers |
802 | +{% endif %} |
803 | +{% for server in servers -%} |
804 | +server {{server}} iburst |
805 | +{% endfor %} |
806 | + |
807 | +# This directive specify the location of the file containing ID/key pairs for |
808 | +# NTP authentication. |
809 | +keyfile /etc/chrony/chrony.keys |
810 | + |
811 | +# This directive specify the file into which chronyd will store the rate |
812 | +# information. |
813 | +driftfile /var/lib/chrony/chrony.drift |
814 | + |
815 | +# Uncomment the following line to turn logging on. |
816 | +#log tracking measurements statistics |
817 | + |
818 | +# Log files location. |
819 | +logdir /var/log/chrony |
820 | + |
821 | +# Stop bad estimates upsetting machine clock. |
822 | +maxupdateskew 100.0 |
823 | + |
824 | +# This directive enables kernel synchronisation (every 11 minutes) of the |
825 | +# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. |
826 | +rtcsync |
827 | + |
828 | +# Step the system clock instead of slewing it if the adjustment is larger than |
829 | +# one second, but only in the first three clock updates. |
830 | +makestep 1 3 |
831 | + |
832 | diff --git a/templates/chrony.conf.fedora.tmpl b/templates/chrony.conf.fedora.tmpl |
833 | new file mode 100644 |
834 | index 0000000..8551f79 |
835 | --- /dev/null |
836 | +++ b/templates/chrony.conf.fedora.tmpl |
837 | @@ -0,0 +1,48 @@ |
838 | +## template:jinja |
839 | +# Use public servers from the pool.ntp.org project. |
840 | +# Please consider joining the pool (http://www.pool.ntp.org/join.html). |
841 | +{% if pools %}# pools |
842 | +{% endif %} |
843 | +{% for pool in pools -%} |
844 | +pool {{pool}} iburst |
845 | +{% endfor %} |
846 | +{%- if servers %}# servers |
847 | +{% endif %} |
848 | +{% for server in servers -%} |
849 | +server {{server}} iburst |
850 | +{% endfor %} |
851 | + |
852 | +# Record the rate at which the system clock gains/losses time. |
853 | +driftfile /var/lib/chrony/drift |
854 | + |
855 | +# Allow the system clock to be stepped in the first three updates |
856 | +# if its offset is larger than 1 second. |
857 | +makestep 1.0 3 |
858 | + |
859 | +# Enable kernel synchronization of the real-time clock (RTC). |
860 | +rtcsync |
861 | + |
862 | +# Enable hardware timestamping on all interfaces that support it. |
863 | +#hwtimestamp * |
864 | + |
865 | +# Increase the minimum number of selectable sources required to adjust |
866 | +# the system clock. |
867 | +#minsources 2 |
868 | + |
869 | +# Allow NTP client access from local network. |
870 | +#allow 192.168.0.0/16 |
871 | + |
872 | +# Serve time even if not synchronized to a time source. |
873 | +#local stratum 10 |
874 | + |
875 | +# Specify file containing keys for NTP authentication. |
876 | +#keyfile /etc/chrony.keys |
877 | + |
878 | +# Get TAI-UTC offset and leap seconds from the system tz database. |
879 | +leapsectz right/UTC |
880 | + |
881 | +# Specify directory for log files. |
882 | +logdir /var/log/chrony |
883 | + |
884 | +# Select which information is logged. |
885 | +#log measurements statistics tracking |
886 | diff --git a/templates/chrony.conf.opensuse.tmpl b/templates/chrony.conf.opensuse.tmpl |
887 | new file mode 100644 |
888 | index 0000000..a3d3e0e |
889 | --- /dev/null |
890 | +++ b/templates/chrony.conf.opensuse.tmpl |
891 | @@ -0,0 +1,38 @@ |
892 | +## template:jinja |
893 | +# Use public servers from the pool.ntp.org project. |
894 | +# Please consider joining the pool (http://www.pool.ntp.org/join.html). |
895 | +{% if pools %}# pools |
896 | +{% endif %} |
897 | +{% for pool in pools -%} |
898 | +pool {{pool}} iburst |
899 | +{% endfor %} |
900 | +{%- if servers %}# servers |
901 | +{% endif %} |
902 | +{% for server in servers -%} |
903 | +server {{server}} iburst |
904 | +{% endfor %} |
905 | + |
906 | +# Record the rate at which the system clock gains/losses time. |
907 | +driftfile /var/lib/chrony/drift |
908 | + |
909 | +# In first three updates step the system clock instead of slew |
910 | +# if the adjustment is larger than 1 second. |
911 | +makestep 1.0 3 |
912 | + |
913 | +# Enable kernel synchronization of the real-time clock (RTC). |
914 | +rtcsync |
915 | + |
916 | +# Allow NTP client access from local network. |
917 | +#allow 192.168/16 |
918 | + |
919 | +# Serve time even if not synchronized to any NTP server. |
920 | +#local stratum 10 |
921 | + |
922 | +# Specify file containing keys for NTP authentication. |
923 | +#keyfile /etc/chrony.keys |
924 | + |
925 | +# Specify directory for log files. |
926 | +logdir /var/log/chrony |
927 | + |
928 | +# Select which information is logged. |
929 | +#log measurements statistics tracking |
930 | diff --git a/templates/chrony.conf.rhel.tmpl b/templates/chrony.conf.rhel.tmpl |
931 | new file mode 100644 |
932 | index 0000000..5b3542e |
933 | --- /dev/null |
934 | +++ b/templates/chrony.conf.rhel.tmpl |
935 | @@ -0,0 +1,45 @@ |
936 | +## template:jinja |
937 | +# Use public servers from the pool.ntp.org project. |
938 | +# Please consider joining the pool (http://www.pool.ntp.org/join.html). |
939 | +{% if pools %}# pools |
940 | +{% endif %} |
941 | +{% for pool in pools -%} |
942 | +pool {{pool}} iburst |
943 | +{% endfor %} |
944 | +{%- if servers %}# servers |
945 | +{% endif %} |
946 | +{% for server in servers -%} |
947 | +server {{server}} iburst |
948 | +{% endfor %} |
949 | + |
950 | +# Record the rate at which the system clock gains/losses time. |
951 | +driftfile /var/lib/chrony/drift |
952 | + |
953 | +# Allow the system clock to be stepped in the first three updates |
954 | +# if its offset is larger than 1 second. |
955 | +makestep 1.0 3 |
956 | + |
957 | +# Enable kernel synchronization of the real-time clock (RTC). |
958 | +rtcsync |
959 | + |
960 | +# Enable hardware timestamping on all interfaces that support it. |
961 | +#hwtimestamp * |
962 | + |
963 | +# Increase the minimum number of selectable sources required to adjust |
964 | +# the system clock. |
965 | +#minsources 2 |
966 | + |
967 | +# Allow NTP client access from local network. |
968 | +#allow 192.168.0.0/16 |
969 | + |
970 | +# Serve time even if not synchronized to a time source. |
971 | +#local stratum 10 |
972 | + |
973 | +# Specify file containing keys for NTP authentication. |
974 | +#keyfile /etc/chrony.keys |
975 | + |
976 | +# Specify directory for log files. |
977 | +logdir /var/log/chrony |
978 | + |
979 | +# Select which information is logged. |
980 | +#log measurements statistics tracking |
981 | diff --git a/templates/chrony.conf.sles.tmpl b/templates/chrony.conf.sles.tmpl |
982 | new file mode 100644 |
983 | index 0000000..a3d3e0e |
984 | --- /dev/null |
985 | +++ b/templates/chrony.conf.sles.tmpl |
986 | @@ -0,0 +1,38 @@ |
987 | +## template:jinja |
988 | +# Use public servers from the pool.ntp.org project. |
989 | +# Please consider joining the pool (http://www.pool.ntp.org/join.html). |
990 | +{% if pools %}# pools |
991 | +{% endif %} |
992 | +{% for pool in pools -%} |
993 | +pool {{pool}} iburst |
994 | +{% endfor %} |
995 | +{%- if servers %}# servers |
996 | +{% endif %} |
997 | +{% for server in servers -%} |
998 | +server {{server}} iburst |
999 | +{% endfor %} |
1000 | + |
1001 | +# Record the rate at which the system clock gains/losses time. |
1002 | +driftfile /var/lib/chrony/drift |
1003 | + |
1004 | +# In first three updates step the system clock instead of slew |
1005 | +# if the adjustment is larger than 1 second. |
1006 | +makestep 1.0 3 |
1007 | + |
1008 | +# Enable kernel synchronization of the real-time clock (RTC). |
1009 | +rtcsync |
1010 | + |
1011 | +# Allow NTP client access from local network. |
1012 | +#allow 192.168/16 |
1013 | + |
1014 | +# Serve time even if not synchronized to any NTP server. |
1015 | +#local stratum 10 |
1016 | + |
1017 | +# Specify file containing keys for NTP authentication. |
1018 | +#keyfile /etc/chrony.keys |
1019 | + |
1020 | +# Specify directory for log files. |
1021 | +logdir /var/log/chrony |
1022 | + |
1023 | +# Select which information is logged. |
1024 | +#log measurements statistics tracking |
1025 | diff --git a/templates/chrony.conf.ubuntu.tmpl b/templates/chrony.conf.ubuntu.tmpl |
1026 | new file mode 100644 |
1027 | index 0000000..50a6f51 |
1028 | --- /dev/null |
1029 | +++ b/templates/chrony.conf.ubuntu.tmpl |
1030 | @@ -0,0 +1,42 @@ |
1031 | +## template:jinja |
1032 | +# Welcome to the chrony configuration file. See chrony.conf(5) for more |
1033 | +# information about usuable directives. |
1034 | + |
1035 | +# Use servers from the NTP Pool Project. Approved by Ubuntu Technical Board |
1036 | +# on 2011-02-08 (LP: #104525). See http://www.pool.ntp.org/join.html for |
1037 | +# more information. |
1038 | +{% if pools %}# pools |
1039 | +{% endif %} |
1040 | +{% for pool in pools -%} |
1041 | +pool {{pool}} iburst |
1042 | +{% endfor %} |
1043 | +{%- if servers %}# servers |
1044 | +{% endif %} |
1045 | +{% for server in servers -%} |
1046 | +server {{server}} iburst |
1047 | +{% endfor %} |
1048 | + |
1049 | +# This directive specify the location of the file containing ID/key pairs for |
1050 | +# NTP authentication. |
1051 | +keyfile /etc/chrony/chrony.keys |
1052 | + |
1053 | +# This directive specify the file into which chronyd will store the rate |
1054 | +# information. |
1055 | +driftfile /var/lib/chrony/chrony.drift |
1056 | + |
1057 | +# Uncomment the following line to turn logging on. |
1058 | +#log tracking measurements statistics |
1059 | + |
1060 | +# Log files location. |
1061 | +logdir /var/log/chrony |
1062 | + |
1063 | +# Stop bad estimates upsetting machine clock. |
1064 | +maxupdateskew 100.0 |
1065 | + |
1066 | +# This directive enables kernel synchronisation (every 11 minutes) of the |
1067 | +# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. |
1068 | +rtcsync |
1069 | + |
1070 | +# Step the system clock instead of slewing it if the adjustment is larger than |
1071 | +# one second, but only in the first three clock updates. |
1072 | +makestep 1 3 |
1073 | diff --git a/tests/cloud_tests/testcases/base.py b/tests/cloud_tests/testcases/base.py |
1074 | index 324c7c9..7598d46 100644 |
1075 | --- a/tests/cloud_tests/testcases/base.py |
1076 | +++ b/tests/cloud_tests/testcases/base.py |
1077 | @@ -149,7 +149,10 @@ class CloudTestCase(unittest.TestCase): |
1078 | self.assertEqual( |
1079 | ['ds/user-data'], instance_data['base64-encoded-keys']) |
1080 | ds = instance_data.get('ds', {}) |
1081 | - macs = ds.get('network', {}).get('interfaces', {}).get('macs', {}) |
1082 | + v1_data = instance_data.get('v1', {}) |
1083 | + metadata = ds.get('meta-data', {}) |
1084 | + macs = metadata.get( |
1085 | + 'network', {}).get('interfaces', {}).get('macs', {}) |
1086 | if not macs: |
1087 | raise AssertionError('No network data from EC2 meta-data') |
1088 | # Check meta-data items we depend on |
1089 | @@ -160,10 +163,8 @@ class CloudTestCase(unittest.TestCase): |
1090 | for key in expected_net_keys: |
1091 | self.assertIn(key, mac_data) |
1092 | self.assertIsNotNone( |
1093 | - ds.get('placement', {}).get('availability-zone'), |
1094 | + metadata.get('placement', {}).get('availability-zone'), |
1095 | 'Could not determine EC2 Availability zone placement') |
1096 | - ds = instance_data.get('ds', {}) |
1097 | - v1_data = instance_data.get('v1', {}) |
1098 | self.assertIsNotNone( |
1099 | v1_data['availability-zone'], 'expected ec2 availability-zone') |
1100 | self.assertEqual('aws', v1_data['cloud-name']) |
1101 | diff --git a/tests/cloud_tests/testcases/modules/ntp.yaml b/tests/cloud_tests/testcases/modules/ntp.yaml |
1102 | index 2530d72..7ea0707 100644 |
1103 | --- a/tests/cloud_tests/testcases/modules/ntp.yaml |
1104 | +++ b/tests/cloud_tests/testcases/modules/ntp.yaml |
1105 | @@ -4,6 +4,7 @@ |
1106 | cloud_config: | |
1107 | #cloud-config |
1108 | ntp: |
1109 | + ntp_client: ntp |
1110 | pools: [] |
1111 | servers: [] |
1112 | collect_scripts: |
1113 | diff --git a/tests/cloud_tests/testcases/modules/ntp_chrony.py b/tests/cloud_tests/testcases/modules/ntp_chrony.py |
1114 | new file mode 100644 |
1115 | index 0000000..461630a |
1116 | --- /dev/null |
1117 | +++ b/tests/cloud_tests/testcases/modules/ntp_chrony.py |
1118 | @@ -0,0 +1,15 @@ |
1119 | +# This file is part of cloud-init. See LICENSE file for license information. |
1120 | + |
1121 | +"""cloud-init Integration Test Verify Script.""" |
1122 | +from tests.cloud_tests.testcases import base |
1123 | + |
1124 | + |
1125 | +class TestNtpChrony(base.CloudTestCase): |
1126 | + """Test ntp module with chrony client""" |
1127 | + |
1128 | + def test_chrony_entires(self): |
1129 | + """Test chrony config entries""" |
1130 | + out = self.get_data_file('chrony_conf') |
1131 | + self.assertIn('.pool.ntp.org', out) |
1132 | + |
1133 | +# vi: ts=4 expandtab |
1134 | diff --git a/tests/cloud_tests/testcases/modules/ntp_chrony.yaml b/tests/cloud_tests/testcases/modules/ntp_chrony.yaml |
1135 | new file mode 100644 |
1136 | index 0000000..120735e |
1137 | --- /dev/null |
1138 | +++ b/tests/cloud_tests/testcases/modules/ntp_chrony.yaml |
1139 | @@ -0,0 +1,17 @@ |
1140 | +# |
1141 | +# ntp enabled, chrony selected, check conf file |
1142 | +# as chrony won't start in a container |
1143 | +# |
1144 | +cloud_config: | |
1145 | + #cloud-config |
1146 | + ntp: |
1147 | + enabled: true |
1148 | + ntp_client: chrony |
1149 | +collect_scripts: |
1150 | + chrony_conf: | |
1151 | + #!/bin/sh |
1152 | + set -- /etc/chrony.conf /etc/chrony/chrony.conf |
1153 | + for p in "$@"; do |
1154 | + [ -e "$p" ] && { cat "$p"; exit; } |
1155 | + done |
1156 | +# vi: ts=4 expandtab |
1157 | diff --git a/tests/cloud_tests/testcases/modules/ntp_pools.yaml b/tests/cloud_tests/testcases/modules/ntp_pools.yaml |
1158 | index d490b22..60fa0fd 100644 |
1159 | --- a/tests/cloud_tests/testcases/modules/ntp_pools.yaml |
1160 | +++ b/tests/cloud_tests/testcases/modules/ntp_pools.yaml |
1161 | @@ -9,6 +9,7 @@ required_features: |
1162 | cloud_config: | |
1163 | #cloud-config |
1164 | ntp: |
1165 | + ntp_client: ntp |
1166 | pools: |
1167 | - 0.cloud-init.mypool |
1168 | - 1.cloud-init.mypool |
1169 | diff --git a/tests/cloud_tests/testcases/modules/ntp_servers.yaml b/tests/cloud_tests/testcases/modules/ntp_servers.yaml |
1170 | index 6b13b70..ee63667 100644 |
1171 | --- a/tests/cloud_tests/testcases/modules/ntp_servers.yaml |
1172 | +++ b/tests/cloud_tests/testcases/modules/ntp_servers.yaml |
1173 | @@ -6,6 +6,7 @@ required_features: |
1174 | cloud_config: | |
1175 | #cloud-config |
1176 | ntp: |
1177 | + ntp_client: ntp |
1178 | servers: |
1179 | - 172.16.15.14 |
1180 | - 172.16.17.18 |
1181 | diff --git a/tests/cloud_tests/testcases/modules/ntp_timesyncd.py b/tests/cloud_tests/testcases/modules/ntp_timesyncd.py |
1182 | new file mode 100644 |
1183 | index 0000000..eca750b |
1184 | --- /dev/null |
1185 | +++ b/tests/cloud_tests/testcases/modules/ntp_timesyncd.py |
1186 | @@ -0,0 +1,15 @@ |
1187 | +# This file is part of cloud-init. See LICENSE file for license information. |
1188 | + |
1189 | +"""cloud-init Integration Test Verify Script.""" |
1190 | +from tests.cloud_tests.testcases import base |
1191 | + |
1192 | + |
1193 | +class TestNtpTimesyncd(base.CloudTestCase): |
1194 | + """Test ntp module with systemd-timesyncd client""" |
1195 | + |
1196 | + def test_timesyncd_entries(self): |
1197 | + """Test timesyncd config entries""" |
1198 | + out = self.get_data_file('timesyncd_conf') |
1199 | + self.assertIn('.pool.ntp.org', out) |
1200 | + |
1201 | +# vi: ts=4 expandtab |
1202 | diff --git a/tests/cloud_tests/testcases/modules/ntp_timesyncd.yaml b/tests/cloud_tests/testcases/modules/ntp_timesyncd.yaml |
1203 | new file mode 100644 |
1204 | index 0000000..ee47a74 |
1205 | --- /dev/null |
1206 | +++ b/tests/cloud_tests/testcases/modules/ntp_timesyncd.yaml |
1207 | @@ -0,0 +1,15 @@ |
1208 | +# |
1209 | +# ntp enabled, systemd-timesyncd selected, check conf file |
1210 | +# as systemd-timesyncd won't start in a container |
1211 | +# |
1212 | +cloud_config: | |
1213 | + #cloud-config |
1214 | + ntp: |
1215 | + enabled: true |
1216 | + ntp_client: systemd-timesyncd |
1217 | +collect_scripts: |
1218 | + timesyncd_conf: | |
1219 | + #!/bin/sh |
1220 | + cat /etc/systemd/timesyncd.conf.d/cloud-init.conf |
1221 | + |
1222 | +# vi: ts=4 expandtab |
1223 | diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py |
1224 | index 1c2e45f..7765e40 100644 |
1225 | --- a/tests/unittests/test_distros/test_netconfig.py |
1226 | +++ b/tests/unittests/test_distros/test_netconfig.py |
1227 | @@ -189,6 +189,12 @@ hn0: flags=8843<UP,BROADCAST,RUNNING,SIMPLEX,MULTICAST> metric 0 mtu 1500 |
1228 | status: active |
1229 | """ |
1230 | |
1231 | + def setUp(self): |
1232 | + super(TestNetCfgDistro, self).setUp() |
1233 | + self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') |
1234 | + self.add_patch('cloudinit.util.system_info', 'm_sysinfo') |
1235 | + self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} |
1236 | + |
1237 | def _get_distro(self, dname, renderers=None): |
1238 | cls = distros.fetch(dname) |
1239 | cfg = settings.CFG_BUILTIN |
1240 | diff --git a/tests/unittests/test_distros/test_user_data_normalize.py b/tests/unittests/test_distros/test_user_data_normalize.py |
1241 | index 0fa9cdb..fa4b6cf 100644 |
1242 | --- a/tests/unittests/test_distros/test_user_data_normalize.py |
1243 | +++ b/tests/unittests/test_distros/test_user_data_normalize.py |
1244 | @@ -22,6 +22,12 @@ bcfg = { |
1245 | |
1246 | class TestUGNormalize(TestCase): |
1247 | |
1248 | + def setUp(self): |
1249 | + super(TestUGNormalize, self).setUp() |
1250 | + self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') |
1251 | + self.add_patch('cloudinit.util.system_info', 'm_sysinfo') |
1252 | + self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} |
1253 | + |
1254 | def _make_distro(self, dtype, def_user=None): |
1255 | cfg = dict(settings.CFG_BUILTIN) |
1256 | cfg['system_info']['distro'] = dtype |
1257 | diff --git a/tests/unittests/test_handler/test_handler_ntp.py b/tests/unittests/test_handler/test_handler_ntp.py |
1258 | index 695897c..02676aa 100644 |
1259 | --- a/tests/unittests/test_handler/test_handler_ntp.py |
1260 | +++ b/tests/unittests/test_handler/test_handler_ntp.py |
1261 | @@ -4,20 +4,21 @@ from cloudinit.config import cc_ntp |
1262 | from cloudinit.sources import DataSourceNone |
1263 | from cloudinit import (distros, helpers, cloud, util) |
1264 | from cloudinit.tests.helpers import ( |
1265 | - FilesystemMockingTestCase, mock, skipUnlessJsonSchema) |
1266 | + CiTestCase, FilesystemMockingTestCase, mock, skipUnlessJsonSchema) |
1267 | |
1268 | |
1269 | +import copy |
1270 | import os |
1271 | from os.path import dirname |
1272 | import shutil |
1273 | |
1274 | -NTP_TEMPLATE = b"""\ |
1275 | +NTP_TEMPLATE = """\ |
1276 | ## template: jinja |
1277 | servers {{servers}} |
1278 | pools {{pools}} |
1279 | """ |
1280 | |
1281 | -TIMESYNCD_TEMPLATE = b"""\ |
1282 | +TIMESYNCD_TEMPLATE = """\ |
1283 | ## template:jinja |
1284 | [Time] |
1285 | {% if servers or pools -%} |
1286 | @@ -32,56 +33,88 @@ class TestNtp(FilesystemMockingTestCase): |
1287 | |
1288 | def setUp(self): |
1289 | super(TestNtp, self).setUp() |
1290 | - self.subp = util.subp |
1291 | self.new_root = self.tmp_dir() |
1292 | + self.add_patch('cloudinit.util.system_is_snappy', 'm_snappy') |
1293 | + self.m_snappy.return_value = False |
1294 | + self.add_patch('cloudinit.util.system_info', 'm_sysinfo') |
1295 | + self.m_sysinfo.return_value = {'dist': ('Distro', '99.1', 'Codename')} |
1296 | |
1297 | - def _get_cloud(self, distro): |
1298 | - self.patchUtils(self.new_root) |
1299 | + def _get_cloud(self, distro, sys_cfg=None): |
1300 | + self.new_root = self.reRoot(root=self.new_root) |
1301 | paths = helpers.Paths({'templates_dir': self.new_root}) |
1302 | cls = distros.fetch(distro) |
1303 | - mydist = cls(distro, {}, paths) |
1304 | - myds = DataSourceNone.DataSourceNone({}, mydist, paths) |
1305 | - return cloud.Cloud(myds, paths, {}, mydist, None) |
1306 | + if not sys_cfg: |
1307 | + sys_cfg = {} |
1308 | + mydist = cls(distro, sys_cfg, paths) |
1309 | + myds = DataSourceNone.DataSourceNone(sys_cfg, mydist, paths) |
1310 | + return cloud.Cloud(myds, paths, sys_cfg, mydist, None) |
1311 | + |
1312 | + def _get_template_path(self, template_name, distro, basepath=None): |
1313 | + # ntp.conf.{distro} -> ntp.conf.debian.tmpl |
1314 | + template_fn = '{0}.tmpl'.format( |
1315 | + template_name.replace('{distro}', distro)) |
1316 | + if not basepath: |
1317 | + basepath = self.new_root |
1318 | + path = os.path.join(basepath, template_fn) |
1319 | + return path |
1320 | + |
1321 | + def _generate_template(self, template=None): |
1322 | + if not template: |
1323 | + template = NTP_TEMPLATE |
1324 | + confpath = os.path.join(self.new_root, 'client.conf') |
1325 | + template_fn = os.path.join(self.new_root, 'client.conf.tmpl') |
1326 | + util.write_file(template_fn, content=template) |
1327 | + return (confpath, template_fn) |
1328 | + |
1329 | + def _mock_ntp_client_config(self, client=None, distro=None): |
1330 | + if not client: |
1331 | + client = 'ntp' |
1332 | + if not distro: |
1333 | + distro = 'ubuntu' |
1334 | + dcfg = cc_ntp.distro_ntp_client_configs(distro) |
1335 | + if client == 'systemd-timesyncd': |
1336 | + template = TIMESYNCD_TEMPLATE |
1337 | + else: |
1338 | + template = NTP_TEMPLATE |
1339 | + (confpath, template_fn) = self._generate_template(template=template) |
1340 | + ntpconfig = copy.deepcopy(dcfg[client]) |
1341 | + ntpconfig['confpath'] = confpath |
1342 | + ntpconfig['template_name'] = os.path.basename(confpath) |
1343 | + return ntpconfig |
1344 | |
1345 | @mock.patch("cloudinit.config.cc_ntp.util") |
1346 | def test_ntp_install(self, mock_util): |
1347 | - """ntp_install installs via install_func when check_exe is absent.""" |
1348 | + """ntp_install_client runs install_func when check_exe is absent.""" |
1349 | mock_util.which.return_value = None # check_exe not found. |
1350 | install_func = mock.MagicMock() |
1351 | - cc_ntp.install_ntp(install_func, packages=['ntpx'], check_exe='ntpdx') |
1352 | - |
1353 | + cc_ntp.install_ntp_client(install_func, |
1354 | + packages=['ntpx'], check_exe='ntpdx') |
1355 | mock_util.which.assert_called_with('ntpdx') |
1356 | install_func.assert_called_once_with(['ntpx']) |
1357 | |
1358 | @mock.patch("cloudinit.config.cc_ntp.util") |
1359 | def test_ntp_install_not_needed(self, mock_util): |
1360 | - """ntp_install doesn't attempt install when check_exe is found.""" |
1361 | - mock_util.which.return_value = ["/usr/sbin/ntpd"] # check_exe found. |
1362 | + """ntp_install_client doesn't install when check_exe is found.""" |
1363 | + client = 'chrony' |
1364 | + mock_util.which.return_value = [client] # check_exe found. |
1365 | install_func = mock.MagicMock() |
1366 | - cc_ntp.install_ntp(install_func, packages=['ntp'], check_exe='ntpd') |
1367 | + cc_ntp.install_ntp_client(install_func, packages=[client], |
1368 | + check_exe=client) |
1369 | install_func.assert_not_called() |
1370 | |
1371 | @mock.patch("cloudinit.config.cc_ntp.util") |
1372 | def test_ntp_install_no_op_with_empty_pkg_list(self, mock_util): |
1373 | - """ntp_install calls install_func with empty list""" |
1374 | + """ntp_install_client runs install_func with empty list""" |
1375 | mock_util.which.return_value = None # check_exe not found |
1376 | install_func = mock.MagicMock() |
1377 | - cc_ntp.install_ntp(install_func, packages=[], check_exe='timesyncd') |
1378 | + cc_ntp.install_ntp_client(install_func, packages=[], |
1379 | + check_exe='timesyncd') |
1380 | install_func.assert_called_once_with([]) |
1381 | |
1382 | - def test_ntp_rename_ntp_conf(self): |
1383 | - """When NTP_CONF exists, rename_ntp moves it.""" |
1384 | - ntpconf = self.tmp_path("ntp.conf", self.new_root) |
1385 | - util.write_file(ntpconf, "") |
1386 | - with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf): |
1387 | - cc_ntp.rename_ntp_conf() |
1388 | - self.assertFalse(os.path.exists(ntpconf)) |
1389 | - self.assertTrue(os.path.exists("{0}.dist".format(ntpconf))) |
1390 | - |
1391 | @mock.patch("cloudinit.config.cc_ntp.util") |
1392 | def test_reload_ntp_defaults(self, mock_util): |
1393 | """Test service is restarted/reloaded (defaults)""" |
1394 | - service = 'ntp' |
1395 | + service = 'ntp_service_name' |
1396 | cmd = ['service', service, 'restart'] |
1397 | cc_ntp.reload_ntp(service) |
1398 | mock_util.subp.assert_called_with(cmd, capture=True) |
1399 | @@ -89,193 +122,171 @@ class TestNtp(FilesystemMockingTestCase): |
1400 | @mock.patch("cloudinit.config.cc_ntp.util") |
1401 | def test_reload_ntp_systemd(self, mock_util): |
1402 | """Test service is restarted/reloaded (systemd)""" |
1403 | - service = 'ntp' |
1404 | - cmd = ['systemctl', 'reload-or-restart', service] |
1405 | + service = 'ntp_service_name' |
1406 | cc_ntp.reload_ntp(service, systemd=True) |
1407 | - mock_util.subp.assert_called_with(cmd, capture=True) |
1408 | - |
1409 | - @mock.patch("cloudinit.config.cc_ntp.util") |
1410 | - def test_reload_ntp_systemd_timesycnd(self, mock_util): |
1411 | - """Test service is restarted/reloaded (systemd/timesyncd)""" |
1412 | - service = 'systemd-timesycnd' |
1413 | cmd = ['systemctl', 'reload-or-restart', service] |
1414 | - cc_ntp.reload_ntp(service, systemd=True) |
1415 | mock_util.subp.assert_called_with(cmd, capture=True) |
1416 | |
1417 | + def test_ntp_rename_ntp_conf(self): |
1418 | + """When NTP_CONF exists, rename_ntp moves it.""" |
1419 | + ntpconf = self.tmp_path("ntp.conf", self.new_root) |
1420 | + util.write_file(ntpconf, "") |
1421 | + cc_ntp.rename_ntp_conf(confpath=ntpconf) |
1422 | + self.assertFalse(os.path.exists(ntpconf)) |
1423 | + self.assertTrue(os.path.exists("{0}.dist".format(ntpconf))) |
1424 | + |
1425 | def test_ntp_rename_ntp_conf_skip_missing(self): |
1426 | """When NTP_CONF doesn't exist rename_ntp doesn't create a file.""" |
1427 | ntpconf = self.tmp_path("ntp.conf", self.new_root) |
1428 | self.assertFalse(os.path.exists(ntpconf)) |
1429 | - with mock.patch("cloudinit.config.cc_ntp.NTP_CONF", ntpconf): |
1430 | - cc_ntp.rename_ntp_conf() |
1431 | + cc_ntp.rename_ntp_conf(confpath=ntpconf) |
1432 | self.assertFalse(os.path.exists("{0}.dist".format(ntpconf))) |
1433 | self.assertFalse(os.path.exists(ntpconf)) |
1434 | |
1435 | - def test_write_ntp_config_template_from_ntp_conf_tmpl_with_servers(self): |
1436 | - """write_ntp_config_template reads content from ntp.conf.tmpl. |
1437 | - |
1438 | - It reads ntp.conf.tmpl if present and renders the value from servers |
1439 | - key. When no pools key is defined, template is rendered using an empty |
1440 | - list for pools. |
1441 | - """ |
1442 | - distro = 'ubuntu' |
1443 | - cfg = { |
1444 | - 'servers': ['192.168.2.1', '192.168.2.2'] |
1445 | - } |
1446 | - mycloud = self._get_cloud(distro) |
1447 | - ntp_conf = self.tmp_path("ntp.conf", self.new_root) # Doesn't exist |
1448 | - # Create ntp.conf.tmpl |
1449 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1450 | - stream.write(NTP_TEMPLATE) |
1451 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1452 | - cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf) |
1453 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
1454 | + def test_write_ntp_config_template_uses_ntp_conf_distro_no_servers(self): |
1455 | + """write_ntp_config_template reads from $client.conf.distro.tmpl""" |
1456 | + servers = [] |
1457 | + pools = ['10.0.0.1', '10.0.0.2'] |
1458 | + (confpath, template_fn) = self._generate_template() |
1459 | + mock_path = 'cloudinit.config.cc_ntp.temp_utils._TMPDIR' |
1460 | + with mock.patch(mock_path, self.new_root): |
1461 | + cc_ntp.write_ntp_config_template('ubuntu', |
1462 | + servers=servers, pools=pools, |
1463 | + path=confpath, |
1464 | + template_fn=template_fn, |
1465 | + template=None) |
1466 | + content = util.read_file_or_url('file://' + confpath).contents |
1467 | self.assertEqual( |
1468 | - "servers ['192.168.2.1', '192.168.2.2']\npools []\n", |
1469 | - content.decode()) |
1470 | + "servers []\npools ['10.0.0.1', '10.0.0.2']\n", content.decode()) |
1471 | |
1472 | - def test_write_ntp_config_template_uses_ntp_conf_distro_no_servers(self): |
1473 | - """write_ntp_config_template reads content from ntp.conf.distro.tmpl. |
1474 | + def test_write_ntp_config_template_defaults_pools_w_empty_lists(self): |
1475 | + """write_ntp_config_template defaults pools servers upon empty config. |
1476 | |
1477 | - It reads ntp.conf.<distro>.tmpl before attempting ntp.conf.tmpl. It |
1478 | - renders the value from the keys servers and pools. When no |
1479 | - servers value is present, template is rendered using an empty list. |
1480 | + When both pools and servers are empty, default NR_POOL_SERVERS get |
1481 | + configured. |
1482 | """ |
1483 | distro = 'ubuntu' |
1484 | - cfg = { |
1485 | - 'pools': ['10.0.0.1', '10.0.0.2'] |
1486 | - } |
1487 | - mycloud = self._get_cloud(distro) |
1488 | - ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
1489 | - # Create ntp.conf.tmpl which isn't read |
1490 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1491 | - stream.write(b'NOT READ: ntp.conf.<distro>.tmpl is primary') |
1492 | - # Create ntp.conf.tmpl.<distro> |
1493 | - with open('{0}.{1}.tmpl'.format(ntp_conf, distro), 'wb') as stream: |
1494 | - stream.write(NTP_TEMPLATE) |
1495 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1496 | - cc_ntp.write_ntp_config_template(cfg, mycloud, ntp_conf) |
1497 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
1498 | + pools = cc_ntp.generate_server_names(distro) |
1499 | + servers = [] |
1500 | + (confpath, template_fn) = self._generate_template() |
1501 | + mock_path = 'cloudinit.config.cc_ntp.temp_utils._TMPDIR' |
1502 | + with mock.patch(mock_path, self.new_root): |
1503 | + cc_ntp.write_ntp_config_template(distro, |
1504 | + servers=servers, pools=pools, |
1505 | + path=confpath, |
1506 | + template_fn=template_fn, |
1507 | + template=None) |
1508 | + content = util.read_file_or_url('file://' + confpath).contents |
1509 | self.assertEqual( |
1510 | - "servers []\npools ['10.0.0.1', '10.0.0.2']\n", |
1511 | + "servers []\npools {0}\n".format(pools), |
1512 | content.decode()) |
1513 | |
1514 | - def test_write_ntp_config_template_defaults_pools_when_empty_lists(self): |
1515 | - """write_ntp_config_template defaults pools servers upon empty config. |
1516 | + def test_defaults_pools_empty_lists_sles(self): |
1517 | + """write_ntp_config_template defaults opensuse pools upon empty config. |
1518 | |
1519 | When both pools and servers are empty, default NR_POOL_SERVERS get |
1520 | configured. |
1521 | """ |
1522 | - distro = 'ubuntu' |
1523 | - mycloud = self._get_cloud(distro) |
1524 | - ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
1525 | - # Create ntp.conf.tmpl |
1526 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1527 | - stream.write(NTP_TEMPLATE) |
1528 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1529 | - cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) |
1530 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
1531 | - default_pools = [ |
1532 | - "{0}.{1}.pool.ntp.org".format(x, distro) |
1533 | - for x in range(0, cc_ntp.NR_POOL_SERVERS)] |
1534 | + distro = 'sles' |
1535 | + default_pools = cc_ntp.generate_server_names(distro) |
1536 | + (confpath, template_fn) = self._generate_template() |
1537 | + |
1538 | + cc_ntp.write_ntp_config_template(distro, |
1539 | + servers=[], pools=[], |
1540 | + path=confpath, |
1541 | + template_fn=template_fn, |
1542 | + template=None) |
1543 | + content = util.read_file_or_url('file://' + confpath).contents |
1544 | + for pool in default_pools: |
1545 | + self.assertIn('opensuse', pool) |
1546 | self.assertEqual( |
1547 | - "servers []\npools {0}\n".format(default_pools), |
1548 | - content.decode()) |
1549 | + "servers []\npools {0}\n".format(default_pools), content.decode()) |
1550 | self.assertIn( |
1551 | "Adding distro default ntp pool servers: {0}".format( |
1552 | ",".join(default_pools)), |
1553 | self.logs.getvalue()) |
1554 | |
1555 | - @mock.patch("cloudinit.config.cc_ntp.ntp_installable") |
1556 | - def test_ntp_handler_mocked_template(self, m_ntp_install): |
1557 | - """Test ntp handler renders ubuntu ntp.conf template.""" |
1558 | - pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org'] |
1559 | - servers = ['192.168.23.3', '192.168.23.4'] |
1560 | - cfg = { |
1561 | - 'ntp': { |
1562 | - 'pools': pools, |
1563 | - 'servers': servers |
1564 | - } |
1565 | - } |
1566 | - mycloud = self._get_cloud('ubuntu') |
1567 | - ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
1568 | - m_ntp_install.return_value = True |
1569 | - |
1570 | - # Create ntp.conf.tmpl |
1571 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1572 | - stream.write(NTP_TEMPLATE) |
1573 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1574 | - with mock.patch.object(util, 'which', return_value=None): |
1575 | - cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
1576 | - |
1577 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
1578 | - self.assertEqual( |
1579 | - 'servers {0}\npools {1}\n'.format(servers, pools), |
1580 | - content.decode()) |
1581 | - |
1582 | - @mock.patch("cloudinit.config.cc_ntp.util") |
1583 | - def test_ntp_handler_mocked_template_snappy(self, m_util): |
1584 | - """Test ntp handler renders timesycnd.conf template on snappy.""" |
1585 | + def test_timesyncd_template(self): |
1586 | + """Test timesycnd template is correct""" |
1587 | pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org'] |
1588 | servers = ['192.168.23.3', '192.168.23.4'] |
1589 | - cfg = { |
1590 | - 'ntp': { |
1591 | - 'pools': pools, |
1592 | - 'servers': servers |
1593 | - } |
1594 | - } |
1595 | - mycloud = self._get_cloud('ubuntu') |
1596 | - m_util.system_is_snappy.return_value = True |
1597 | - |
1598 | - # Create timesyncd.conf.tmpl |
1599 | - tsyncd_conf = self.tmp_path("timesyncd.conf", self.new_root) |
1600 | - template = '{0}.tmpl'.format(tsyncd_conf) |
1601 | - with open(template, 'wb') as stream: |
1602 | - stream.write(TIMESYNCD_TEMPLATE) |
1603 | - |
1604 | - with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf): |
1605 | - cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
1606 | - |
1607 | - content = util.read_file_or_url('file://' + tsyncd_conf).contents |
1608 | + (confpath, template_fn) = self._generate_template( |
1609 | + template=TIMESYNCD_TEMPLATE) |
1610 | + cc_ntp.write_ntp_config_template('ubuntu', |
1611 | + servers=servers, pools=pools, |
1612 | + path=confpath, |
1613 | + template_fn=template_fn, |
1614 | + template=None) |
1615 | + content = util.read_file_or_url('file://' + confpath).contents |
1616 | self.assertEqual( |
1617 | "[Time]\nNTP=%s %s \n" % (" ".join(servers), " ".join(pools)), |
1618 | content.decode()) |
1619 | |
1620 | - def test_ntp_handler_real_distro_templates(self): |
1621 | - """Test ntp handler renders the shipped distro ntp.conf templates.""" |
1622 | + def test_distro_ntp_client_configs(self): |
1623 | + """Test we have updated ntp client configs on different distros""" |
1624 | + delta = copy.deepcopy(cc_ntp.DISTRO_CLIENT_CONFIG) |
1625 | + base = copy.deepcopy(cc_ntp.NTP_CLIENT_CONFIG) |
1626 | + # confirm no-delta distros match the base config |
1627 | + for distro in cc_ntp.distros: |
1628 | + if distro not in delta: |
1629 | + result = cc_ntp.distro_ntp_client_configs(distro) |
1630 | + self.assertEqual(base, result) |
1631 | + # for distros with delta, ensure the merged config values match |
1632 | + # what is set in the delta |
1633 | + for distro in delta.keys(): |
1634 | + result = cc_ntp.distro_ntp_client_configs(distro) |
1635 | + for client in delta[distro].keys(): |
1636 | + for key in delta[distro][client].keys(): |
1637 | + self.assertEqual(delta[distro][client][key], |
1638 | + result[client][key]) |
1639 | + |
1640 | + def test_ntp_handler_real_distro_ntp_templates(self): |
1641 | + """Test ntp handler renders the shipped distro ntp client templates.""" |
1642 | pools = ['0.mycompany.pool.ntp.org', '3.mycompany.pool.ntp.org'] |
1643 | servers = ['192.168.23.3', '192.168.23.4'] |
1644 | - cfg = { |
1645 | - 'ntp': { |
1646 | - 'pools': pools, |
1647 | - 'servers': servers |
1648 | - } |
1649 | - } |
1650 | - ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
1651 | - for distro in ('debian', 'ubuntu', 'fedora', 'rhel', 'sles'): |
1652 | - mycloud = self._get_cloud(distro) |
1653 | - root_dir = dirname(dirname(os.path.realpath(util.__file__))) |
1654 | - tmpl_file = os.path.join( |
1655 | - '{0}/templates/ntp.conf.{1}.tmpl'.format(root_dir, distro)) |
1656 | - # Create a copy in our tmp_dir |
1657 | - shutil.copy( |
1658 | - tmpl_file, |
1659 | - os.path.join(self.new_root, 'ntp.conf.%s.tmpl' % distro)) |
1660 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1661 | - with mock.patch.object(util, 'which', return_value=[True]): |
1662 | - cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
1663 | - |
1664 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
1665 | - expected_servers = '\n'.join([ |
1666 | - 'server {0} iburst'.format(server) for server in servers]) |
1667 | - self.assertIn( |
1668 | - expected_servers, content.decode(), |
1669 | - 'failed to render ntp.conf for distro:{0}'.format(distro)) |
1670 | - expected_pools = '\n'.join([ |
1671 | - 'pool {0} iburst'.format(pool) for pool in pools]) |
1672 | - self.assertIn( |
1673 | - expected_pools, content.decode(), |
1674 | - 'failed to render ntp.conf for distro:{0}'.format(distro)) |
1675 | + for client in ['ntp', 'systemd-timesyncd', 'chrony']: |
1676 | + for distro in cc_ntp.distros: |
1677 | + distro_cfg = cc_ntp.distro_ntp_client_configs(distro) |
1678 | + ntpclient = distro_cfg[client] |
1679 | + confpath = ( |
1680 | + os.path.join(self.new_root, ntpclient.get('confpath')[1:])) |
1681 | + template = ntpclient.get('template_name') |
1682 | + # find sourcetree template file |
1683 | + root_dir = ( |
1684 | + dirname(dirname(os.path.realpath(util.__file__))) + |
1685 | + '/templates') |
1686 | + source_fn = self._get_template_path(template, distro, |
1687 | + basepath=root_dir) |
1688 | + template_fn = self._get_template_path(template, distro) |
1689 | + # don't fail if cloud-init doesn't have a template for |
1690 | + # a distro,client pair |
1691 | + if not os.path.exists(source_fn): |
1692 | + continue |
1693 | + # Create a copy in our tmp_dir |
1694 | + shutil.copy(source_fn, template_fn) |
1695 | + cc_ntp.write_ntp_config_template(distro, servers=servers, |
1696 | + pools=pools, path=confpath, |
1697 | + template_fn=template_fn) |
1698 | + content = util.read_file_or_url('file://' + confpath).contents |
1699 | + if client in ['ntp', 'chrony']: |
1700 | + expected_servers = '\n'.join([ |
1701 | + 'server {0} iburst'.format(srv) for srv in servers]) |
1702 | + print('distro=%s client=%s' % (distro, client)) |
1703 | + self.assertIn(expected_servers, content.decode('utf-8'), |
1704 | + ('failed to render {0} conf' |
1705 | + ' for distro:{1}'.format(client, distro))) |
1706 | + expected_pools = '\n'.join([ |
1707 | + 'pool {0} iburst'.format(pool) for pool in pools]) |
1708 | + self.assertIn(expected_pools, content.decode('utf-8'), |
1709 | + ('failed to render {0} conf' |
1710 | + ' for distro:{1}'.format(client, distro))) |
1711 | + elif client == 'systemd-timesyncd': |
1712 | + expected_content = ( |
1713 | + "# cloud-init generated file\n" + |
1714 | + "# See timesyncd.conf(5) for details.\n\n" + |
1715 | + "[Time]\nNTP=%s %s \n" % (" ".join(servers), |
1716 | + " ".join(pools))) |
1717 | + self.assertEqual(expected_content, content.decode()) |
1718 | |
1719 | def test_no_ntpcfg_does_nothing(self): |
1720 | """When no ntp section is defined handler logs a warning and noops.""" |
1721 | @@ -285,95 +296,99 @@ class TestNtp(FilesystemMockingTestCase): |
1722 | 'not present or disabled by cfg\n', |
1723 | self.logs.getvalue()) |
1724 | |
1725 | - def test_ntp_handler_schema_validation_allows_empty_ntp_config(self): |
1726 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1727 | + def test_ntp_handler_schema_validation_allows_empty_ntp_config(self, |
1728 | + m_select): |
1729 | """Ntp schema validation allows for an empty ntp: configuration.""" |
1730 | valid_empty_configs = [{'ntp': {}}, {'ntp': None}] |
1731 | - distro = 'ubuntu' |
1732 | - cc = self._get_cloud(distro) |
1733 | - ntp_conf = os.path.join(self.new_root, 'ntp.conf') |
1734 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1735 | - stream.write(NTP_TEMPLATE) |
1736 | for valid_empty_config in valid_empty_configs: |
1737 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1738 | - cc_ntp.handle('cc_ntp', valid_empty_config, cc, None, []) |
1739 | - with open(ntp_conf) as stream: |
1740 | - content = stream.read() |
1741 | - default_pools = [ |
1742 | - "{0}.{1}.pool.ntp.org".format(x, distro) |
1743 | - for x in range(0, cc_ntp.NR_POOL_SERVERS)] |
1744 | - self.assertEqual( |
1745 | - "servers []\npools {0}\n".format(default_pools), |
1746 | - content) |
1747 | - self.assertNotIn('Invalid config:', self.logs.getvalue()) |
1748 | + for distro in cc_ntp.distros: |
1749 | + mycloud = self._get_cloud(distro) |
1750 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1751 | + confpath = ntpconfig['confpath'] |
1752 | + m_select.return_value = ntpconfig |
1753 | + cc_ntp.handle('cc_ntp', valid_empty_config, mycloud, None, []) |
1754 | + content = util.read_file_or_url('file://' + confpath).contents |
1755 | + pools = cc_ntp.generate_server_names(mycloud.distro.name) |
1756 | + self.assertEqual( |
1757 | + "servers []\npools {0}\n".format(pools), content.decode()) |
1758 | + self.assertNotIn('Invalid config:', self.logs.getvalue()) |
1759 | |
1760 | @skipUnlessJsonSchema() |
1761 | - def test_ntp_handler_schema_validation_warns_non_string_item_type(self): |
1762 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1763 | + def test_ntp_handler_schema_validation_warns_non_string_item_type(self, |
1764 | + m_sel): |
1765 | """Ntp schema validation warns of non-strings in pools or servers. |
1766 | |
1767 | Schema validation is not strict, so ntp config is still be rendered. |
1768 | """ |
1769 | invalid_config = {'ntp': {'pools': [123], 'servers': ['valid', None]}} |
1770 | - cc = self._get_cloud('ubuntu') |
1771 | - ntp_conf = os.path.join(self.new_root, 'ntp.conf') |
1772 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1773 | - stream.write(NTP_TEMPLATE) |
1774 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1775 | - cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) |
1776 | - self.assertIn( |
1777 | - "Invalid config:\nntp.pools.0: 123 is not of type 'string'\n" |
1778 | - "ntp.servers.1: None is not of type 'string'", |
1779 | - self.logs.getvalue()) |
1780 | - with open(ntp_conf) as stream: |
1781 | - content = stream.read() |
1782 | - self.assertEqual("servers ['valid', None]\npools [123]\n", content) |
1783 | + for distro in cc_ntp.distros: |
1784 | + mycloud = self._get_cloud(distro) |
1785 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1786 | + confpath = ntpconfig['confpath'] |
1787 | + m_sel.return_value = ntpconfig |
1788 | + cc_ntp.handle('cc_ntp', invalid_config, mycloud, None, []) |
1789 | + self.assertIn( |
1790 | + "Invalid config:\nntp.pools.0: 123 is not of type 'string'\n" |
1791 | + "ntp.servers.1: None is not of type 'string'", |
1792 | + self.logs.getvalue()) |
1793 | + content = util.read_file_or_url('file://' + confpath).contents |
1794 | + self.assertEqual("servers ['valid', None]\npools [123]\n", |
1795 | + content.decode()) |
1796 | |
1797 | @skipUnlessJsonSchema() |
1798 | - def test_ntp_handler_schema_validation_warns_of_non_array_type(self): |
1799 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1800 | + def test_ntp_handler_schema_validation_warns_of_non_array_type(self, |
1801 | + m_select): |
1802 | """Ntp schema validation warns of non-array pools or servers types. |
1803 | |
1804 | Schema validation is not strict, so ntp config is still be rendered. |
1805 | """ |
1806 | invalid_config = {'ntp': {'pools': 123, 'servers': 'non-array'}} |
1807 | - cc = self._get_cloud('ubuntu') |
1808 | - ntp_conf = os.path.join(self.new_root, 'ntp.conf') |
1809 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1810 | - stream.write(NTP_TEMPLATE) |
1811 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1812 | - cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) |
1813 | - self.assertIn( |
1814 | - "Invalid config:\nntp.pools: 123 is not of type 'array'\n" |
1815 | - "ntp.servers: 'non-array' is not of type 'array'", |
1816 | - self.logs.getvalue()) |
1817 | - with open(ntp_conf) as stream: |
1818 | - content = stream.read() |
1819 | - self.assertEqual("servers non-array\npools 123\n", content) |
1820 | + |
1821 | + for distro in cc_ntp.distros: |
1822 | + mycloud = self._get_cloud(distro) |
1823 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1824 | + confpath = ntpconfig['confpath'] |
1825 | + m_select.return_value = ntpconfig |
1826 | + cc_ntp.handle('cc_ntp', invalid_config, mycloud, None, []) |
1827 | + self.assertIn( |
1828 | + "Invalid config:\nntp.pools: 123 is not of type 'array'\n" |
1829 | + "ntp.servers: 'non-array' is not of type 'array'", |
1830 | + self.logs.getvalue()) |
1831 | + content = util.read_file_or_url('file://' + confpath).contents |
1832 | + self.assertEqual("servers non-array\npools 123\n", |
1833 | + content.decode()) |
1834 | |
1835 | @skipUnlessJsonSchema() |
1836 | - def test_ntp_handler_schema_validation_warns_invalid_key_present(self): |
1837 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1838 | + def test_ntp_handler_schema_validation_warns_invalid_key_present(self, |
1839 | + m_select): |
1840 | """Ntp schema validation warns of invalid keys present in ntp config. |
1841 | |
1842 | Schema validation is not strict, so ntp config is still be rendered. |
1843 | """ |
1844 | invalid_config = { |
1845 | 'ntp': {'invalidkey': 1, 'pools': ['0.mycompany.pool.ntp.org']}} |
1846 | - cc = self._get_cloud('ubuntu') |
1847 | - ntp_conf = os.path.join(self.new_root, 'ntp.conf') |
1848 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1849 | - stream.write(NTP_TEMPLATE) |
1850 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1851 | - cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) |
1852 | - self.assertIn( |
1853 | - "Invalid config:\nntp: Additional properties are not allowed " |
1854 | - "('invalidkey' was unexpected)", |
1855 | - self.logs.getvalue()) |
1856 | - with open(ntp_conf) as stream: |
1857 | - content = stream.read() |
1858 | - self.assertEqual( |
1859 | - "servers []\npools ['0.mycompany.pool.ntp.org']\n", |
1860 | - content) |
1861 | + for distro in cc_ntp.distros: |
1862 | + mycloud = self._get_cloud(distro) |
1863 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1864 | + confpath = ntpconfig['confpath'] |
1865 | + m_select.return_value = ntpconfig |
1866 | + cc_ntp.handle('cc_ntp', invalid_config, mycloud, None, []) |
1867 | + self.assertIn( |
1868 | + "Invalid config:\nntp: Additional properties are not allowed " |
1869 | + "('invalidkey' was unexpected)", |
1870 | + self.logs.getvalue()) |
1871 | + content = util.read_file_or_url('file://' + confpath).contents |
1872 | + self.assertEqual( |
1873 | + "servers []\npools ['0.mycompany.pool.ntp.org']\n", |
1874 | + content.decode()) |
1875 | |
1876 | @skipUnlessJsonSchema() |
1877 | - def test_ntp_handler_schema_validation_warns_of_duplicates(self): |
1878 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1879 | + def test_ntp_handler_schema_validation_warns_of_duplicates(self, m_select): |
1880 | """Ntp schema validation warns of duplicates in servers or pools. |
1881 | |
1882 | Schema validation is not strict, so ntp config is still be rendered. |
1883 | @@ -381,74 +396,334 @@ class TestNtp(FilesystemMockingTestCase): |
1884 | invalid_config = { |
1885 | 'ntp': {'pools': ['0.mypool.org', '0.mypool.org'], |
1886 | 'servers': ['10.0.0.1', '10.0.0.1']}} |
1887 | - cc = self._get_cloud('ubuntu') |
1888 | - ntp_conf = os.path.join(self.new_root, 'ntp.conf') |
1889 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
1890 | - stream.write(NTP_TEMPLATE) |
1891 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
1892 | - cc_ntp.handle('cc_ntp', invalid_config, cc, None, []) |
1893 | - self.assertIn( |
1894 | - "Invalid config:\nntp.pools: ['0.mypool.org', '0.mypool.org'] has " |
1895 | - "non-unique elements\nntp.servers: ['10.0.0.1', '10.0.0.1'] has " |
1896 | - "non-unique elements", |
1897 | - self.logs.getvalue()) |
1898 | - with open(ntp_conf) as stream: |
1899 | - content = stream.read() |
1900 | - self.assertEqual( |
1901 | - "servers ['10.0.0.1', '10.0.0.1']\n" |
1902 | - "pools ['0.mypool.org', '0.mypool.org']\n", |
1903 | - content) |
1904 | + for distro in cc_ntp.distros: |
1905 | + mycloud = self._get_cloud(distro) |
1906 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1907 | + confpath = ntpconfig['confpath'] |
1908 | + m_select.return_value = ntpconfig |
1909 | + cc_ntp.handle('cc_ntp', invalid_config, mycloud, None, []) |
1910 | + self.assertIn( |
1911 | + "Invalid config:\nntp.pools: ['0.mypool.org', '0.mypool.org']" |
1912 | + " has non-unique elements\nntp.servers: " |
1913 | + "['10.0.0.1', '10.0.0.1'] has non-unique elements", |
1914 | + self.logs.getvalue()) |
1915 | + content = util.read_file_or_url('file://' + confpath).contents |
1916 | + self.assertEqual( |
1917 | + "servers ['10.0.0.1', '10.0.0.1']\n" |
1918 | + "pools ['0.mypool.org', '0.mypool.org']\n", content.decode()) |
1919 | |
1920 | - @mock.patch("cloudinit.config.cc_ntp.ntp_installable") |
1921 | - def test_ntp_handler_timesyncd(self, m_ntp_install): |
1922 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1923 | + def test_ntp_handler_timesyncd(self, m_select): |
1924 | """Test ntp handler configures timesyncd""" |
1925 | - m_ntp_install.return_value = False |
1926 | - distro = 'ubuntu' |
1927 | - cfg = { |
1928 | - 'servers': ['192.168.2.1', '192.168.2.2'], |
1929 | - 'pools': ['0.mypool.org'], |
1930 | - } |
1931 | - mycloud = self._get_cloud(distro) |
1932 | - tsyncd_conf = self.tmp_path("timesyncd.conf", self.new_root) |
1933 | - # Create timesyncd.conf.tmpl |
1934 | - template = '{0}.tmpl'.format(tsyncd_conf) |
1935 | - print(template) |
1936 | - with open(template, 'wb') as stream: |
1937 | - stream.write(TIMESYNCD_TEMPLATE) |
1938 | - with mock.patch('cloudinit.config.cc_ntp.TIMESYNCD_CONF', tsyncd_conf): |
1939 | - cc_ntp.write_ntp_config_template(cfg, mycloud, tsyncd_conf, |
1940 | - template='timesyncd.conf') |
1941 | - |
1942 | - content = util.read_file_or_url('file://' + tsyncd_conf).contents |
1943 | - self.assertEqual( |
1944 | - "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n", |
1945 | - content.decode()) |
1946 | + servers = ['192.168.2.1', '192.168.2.2'] |
1947 | + pools = ['0.mypool.org'] |
1948 | + cfg = {'ntp': {'servers': servers, 'pools': pools}} |
1949 | + client = 'systemd-timesyncd' |
1950 | + for distro in cc_ntp.distros: |
1951 | + mycloud = self._get_cloud(distro) |
1952 | + ntpconfig = self._mock_ntp_client_config(distro=distro, |
1953 | + client=client) |
1954 | + confpath = ntpconfig['confpath'] |
1955 | + m_select.return_value = ntpconfig |
1956 | + cc_ntp.handle('cc_ntp', cfg, mycloud, None, []) |
1957 | + content = util.read_file_or_url('file://' + confpath).contents |
1958 | + self.assertEqual( |
1959 | + "[Time]\nNTP=192.168.2.1 192.168.2.2 0.mypool.org \n", |
1960 | + content.decode()) |
1961 | + |
1962 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1963 | + def test_ntp_handler_enabled_false(self, m_select): |
1964 | + """Test ntp handler does not run if enabled: false """ |
1965 | + cfg = {'ntp': {'enabled': False}} |
1966 | + for distro in cc_ntp.distros: |
1967 | + mycloud = self._get_cloud(distro) |
1968 | + cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
1969 | + self.assertEqual(0, m_select.call_count) |
1970 | + |
1971 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
1972 | + @mock.patch("cloudinit.distros.Distro.uses_systemd") |
1973 | + def test_ntp_the_whole_package(self, m_sysd, m_select): |
1974 | + """Test enabled config renders template, and restarts service """ |
1975 | + cfg = {'ntp': {'enabled': True}} |
1976 | + for distro in cc_ntp.distros: |
1977 | + mycloud = self._get_cloud(distro) |
1978 | + ntpconfig = self._mock_ntp_client_config(distro=distro) |
1979 | + confpath = ntpconfig['confpath'] |
1980 | + service_name = ntpconfig['service_name'] |
1981 | + m_select.return_value = ntpconfig |
1982 | + pools = cc_ntp.generate_server_names(mycloud.distro.name) |
1983 | + # force uses systemd path |
1984 | + m_sysd.return_value = True |
1985 | + with mock.patch('cloudinit.config.cc_ntp.util') as m_util: |
1986 | + # allow use of util.mergemanydict |
1987 | + m_util.mergemanydict.side_effect = util.mergemanydict |
1988 | + # default client is present |
1989 | + m_util.which.return_value = True |
1990 | + # use the config 'enabled' value |
1991 | + m_util.is_false.return_value = util.is_false( |
1992 | + cfg['ntp']['enabled']) |
1993 | + cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
1994 | + m_util.subp.assert_called_with( |
1995 | + ['systemctl', 'reload-or-restart', |
1996 | + service_name], capture=True) |
1997 | + content = util.read_file_or_url('file://' + confpath).contents |
1998 | + self.assertEqual( |
1999 | + "servers []\npools {0}\n".format(pools), |
2000 | + content.decode()) |
2001 | + |
2002 | + def test_opensuse_picks_chrony(self): |
2003 | + """Test opensuse picks chrony or ntp on certain distro versions""" |
2004 | + # < 15.0 => ntp |
2005 | + self.m_sysinfo.return_value = {'dist': |
2006 | + ('openSUSE', '13.2', 'Harlequin')} |
2007 | + mycloud = self._get_cloud('opensuse') |
2008 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2009 | + self.assertEqual('ntp', expected_client) |
2010 | + |
2011 | + # >= 15.0 and not openSUSE => chrony |
2012 | + self.m_sysinfo.return_value = {'dist': |
2013 | + ('SLES', '15.0', |
2014 | + 'SUSE Linux Enterprise Server 15')} |
2015 | + mycloud = self._get_cloud('sles') |
2016 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2017 | + self.assertEqual('chrony', expected_client) |
2018 | + |
2019 | + # >= 15.0 and openSUSE and ver != 42 => chrony |
2020 | + self.m_sysinfo.return_value = {'dist': ('openSUSE Tumbleweed', |
2021 | + '20180326', |
2022 | + 'timbleweed')} |
2023 | + mycloud = self._get_cloud('opensuse') |
2024 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2025 | + self.assertEqual('chrony', expected_client) |
2026 | + |
2027 | + def test_ubuntu_xenial_picks_ntp(self): |
2028 | + """Test Ubuntu picks ntp on xenial release""" |
2029 | + |
2030 | + self.m_sysinfo.return_value = {'dist': ('Ubuntu', '16.04', 'xenial')} |
2031 | + mycloud = self._get_cloud('ubuntu') |
2032 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2033 | + self.assertEqual('ntp', expected_client) |
2034 | |
2035 | - def test_write_ntp_config_template_defaults_pools_empty_lists_sles(self): |
2036 | - """write_ntp_config_template defaults pools servers upon empty config. |
2037 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2038 | + def test_snappy_system_picks_timesyncd(self, m_which): |
2039 | + """Test snappy systems prefer installed clients""" |
2040 | |
2041 | - When both pools and servers are empty, default NR_POOL_SERVERS get |
2042 | - configured. |
2043 | - """ |
2044 | - distro = 'sles' |
2045 | - mycloud = self._get_cloud(distro) |
2046 | - ntp_conf = self.tmp_path('ntp.conf', self.new_root) # Doesn't exist |
2047 | - # Create ntp.conf.tmpl |
2048 | - with open('{0}.tmpl'.format(ntp_conf), 'wb') as stream: |
2049 | - stream.write(NTP_TEMPLATE) |
2050 | - with mock.patch('cloudinit.config.cc_ntp.NTP_CONF', ntp_conf): |
2051 | - cc_ntp.write_ntp_config_template({}, mycloud, ntp_conf) |
2052 | - content = util.read_file_or_url('file://' + ntp_conf).contents |
2053 | - default_pools = [ |
2054 | - "{0}.opensuse.pool.ntp.org".format(x) |
2055 | - for x in range(0, cc_ntp.NR_POOL_SERVERS)] |
2056 | - self.assertEqual( |
2057 | - "servers []\npools {0}\n".format(default_pools), |
2058 | - content.decode()) |
2059 | - self.assertIn( |
2060 | - "Adding distro default ntp pool servers: {0}".format( |
2061 | - ",".join(default_pools)), |
2062 | - self.logs.getvalue()) |
2063 | + # we are on ubuntu-core here |
2064 | + self.m_snappy.return_value = True |
2065 | |
2066 | + # ubuntu core systems will have timesyncd installed |
2067 | + m_which.side_effect = iter([None, '/lib/systemd/systemd-timesyncd', |
2068 | + None, None, None]) |
2069 | + distro = 'ubuntu' |
2070 | + mycloud = self._get_cloud(distro) |
2071 | + distro_configs = cc_ntp.distro_ntp_client_configs(distro) |
2072 | + expected_client = 'systemd-timesyncd' |
2073 | + expected_cfg = distro_configs[expected_client] |
2074 | + expected_calls = [] |
2075 | + # we only get to timesyncd |
2076 | + for client in mycloud.distro.preferred_ntp_clients[0:2]: |
2077 | + cfg = distro_configs[client] |
2078 | + expected_calls.append(mock.call(cfg['check_exe'])) |
2079 | + result = cc_ntp.select_ntp_client(None, mycloud.distro) |
2080 | + m_which.assert_has_calls(expected_calls) |
2081 | + self.assertEqual(sorted(expected_cfg), sorted(cfg)) |
2082 | + self.assertEqual(sorted(expected_cfg), sorted(result)) |
2083 | + |
2084 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2085 | + def test_ntp_distro_searches_all_preferred_clients(self, m_which): |
2086 | + """Test select_ntp_client search all distro perferred clients """ |
2087 | + # nothing is installed |
2088 | + m_which.return_value = None |
2089 | + for distro in cc_ntp.distros: |
2090 | + mycloud = self._get_cloud(distro) |
2091 | + distro_configs = cc_ntp.distro_ntp_client_configs(distro) |
2092 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2093 | + expected_cfg = distro_configs[expected_client] |
2094 | + expected_calls = [] |
2095 | + for client in mycloud.distro.preferred_ntp_clients: |
2096 | + cfg = distro_configs[client] |
2097 | + expected_calls.append(mock.call(cfg['check_exe'])) |
2098 | + cc_ntp.select_ntp_client({}, mycloud.distro) |
2099 | + m_which.assert_has_calls(expected_calls) |
2100 | + self.assertEqual(sorted(expected_cfg), sorted(cfg)) |
2101 | + |
2102 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2103 | + def test_user_cfg_ntp_client_auto_uses_distro_clients(self, m_which): |
2104 | + """Test user_cfg.ntp_client='auto' defaults to distro search""" |
2105 | + # nothing is installed |
2106 | + m_which.return_value = None |
2107 | + for distro in cc_ntp.distros: |
2108 | + mycloud = self._get_cloud(distro) |
2109 | + distro_configs = cc_ntp.distro_ntp_client_configs(distro) |
2110 | + expected_client = mycloud.distro.preferred_ntp_clients[0] |
2111 | + expected_cfg = distro_configs[expected_client] |
2112 | + expected_calls = [] |
2113 | + for client in mycloud.distro.preferred_ntp_clients: |
2114 | + cfg = distro_configs[client] |
2115 | + expected_calls.append(mock.call(cfg['check_exe'])) |
2116 | + cc_ntp.select_ntp_client('auto', mycloud.distro) |
2117 | + m_which.assert_has_calls(expected_calls) |
2118 | + self.assertEqual(sorted(expected_cfg), sorted(cfg)) |
2119 | + |
2120 | + @mock.patch('cloudinit.config.cc_ntp.write_ntp_config_template') |
2121 | + @mock.patch('cloudinit.cloud.Cloud.get_template_filename') |
2122 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2123 | + def test_ntp_custom_client_overrides_installed_clients(self, m_which, |
2124 | + m_tmpfn, m_write): |
2125 | + """Test user client is installed despite other clients present """ |
2126 | + client = 'ntpdate' |
2127 | + cfg = {'ntp': {'ntp_client': client}} |
2128 | + for distro in cc_ntp.distros: |
2129 | + # client is not installed |
2130 | + m_which.side_effect = iter([None]) |
2131 | + mycloud = self._get_cloud(distro) |
2132 | + with mock.patch.object(mycloud.distro, |
2133 | + 'install_packages') as m_install: |
2134 | + cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
2135 | + m_install.assert_called_with([client]) |
2136 | + m_which.assert_called_with(client) |
2137 | + |
2138 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2139 | + def test_ntp_system_config_overrides_distro_builtin_clients(self, m_which): |
2140 | + """Test distro system_config overrides builtin preferred ntp clients""" |
2141 | + system_client = 'chrony' |
2142 | + sys_cfg = {'ntp_client': system_client} |
2143 | + # no clients installed |
2144 | + m_which.return_value = None |
2145 | + for distro in cc_ntp.distros: |
2146 | + mycloud = self._get_cloud(distro, sys_cfg=sys_cfg) |
2147 | + distro_configs = cc_ntp.distro_ntp_client_configs(distro) |
2148 | + expected_cfg = distro_configs[system_client] |
2149 | + result = cc_ntp.select_ntp_client(None, mycloud.distro) |
2150 | + self.assertEqual(sorted(expected_cfg), sorted(result)) |
2151 | + m_which.assert_has_calls([]) |
2152 | + |
2153 | + @mock.patch('cloudinit.config.cc_ntp.util.which') |
2154 | + def test_ntp_user_config_overrides_system_cfg(self, m_which): |
2155 | + """Test user-data overrides system_config ntp_client""" |
2156 | + system_client = 'chrony' |
2157 | + sys_cfg = {'ntp_client': system_client} |
2158 | + user_client = 'systemd-timesyncd' |
2159 | + # no clients installed |
2160 | + m_which.return_value = None |
2161 | + for distro in cc_ntp.distros: |
2162 | + mycloud = self._get_cloud(distro, sys_cfg=sys_cfg) |
2163 | + distro_configs = cc_ntp.distro_ntp_client_configs(distro) |
2164 | + expected_cfg = distro_configs[user_client] |
2165 | + result = cc_ntp.select_ntp_client(user_client, mycloud.distro) |
2166 | + self.assertEqual(sorted(expected_cfg), sorted(result)) |
2167 | + m_which.assert_has_calls([]) |
2168 | + |
2169 | + @mock.patch('cloudinit.config.cc_ntp.reload_ntp') |
2170 | + @mock.patch('cloudinit.config.cc_ntp.install_ntp_client') |
2171 | + def test_ntp_user_provided_config_with_template(self, m_install, m_reload): |
2172 | + custom = r'\n#MyCustomTemplate' |
2173 | + user_template = NTP_TEMPLATE + custom |
2174 | + confpath = os.path.join(self.new_root, 'etc/myntp/myntp.conf') |
2175 | + cfg = { |
2176 | + 'ntp': { |
2177 | + 'pools': ['mypool.org'], |
2178 | + 'ntp_client': 'myntpd', |
2179 | + 'config': { |
2180 | + 'check_exe': 'myntpd', |
2181 | + 'confpath': confpath, |
2182 | + 'packages': ['myntp'], |
2183 | + 'service_name': 'myntp', |
2184 | + 'template': user_template, |
2185 | + } |
2186 | + } |
2187 | + } |
2188 | + for distro in cc_ntp.distros: |
2189 | + mycloud = self._get_cloud(distro) |
2190 | + mock_path = 'cloudinit.config.cc_ntp.temp_utils._TMPDIR' |
2191 | + with mock.patch(mock_path, self.new_root): |
2192 | + cc_ntp.handle('notimportant', cfg, mycloud, None, None) |
2193 | + content = util.read_file_or_url('file://' + confpath).contents |
2194 | + self.assertEqual( |
2195 | + "servers []\npools ['mypool.org']\n%s" % custom, |
2196 | + content.decode()) |
2197 | + |
2198 | + @mock.patch('cloudinit.config.cc_ntp.supplemental_schema_validation') |
2199 | + @mock.patch('cloudinit.config.cc_ntp.reload_ntp') |
2200 | + @mock.patch('cloudinit.config.cc_ntp.install_ntp_client') |
2201 | + @mock.patch('cloudinit.config.cc_ntp.select_ntp_client') |
2202 | + def test_ntp_user_provided_config_template_only(self, m_select, m_install, |
2203 | + m_reload, m_schema): |
2204 | + """Test custom template for default client""" |
2205 | + custom = r'\n#MyCustomTemplate' |
2206 | + user_template = NTP_TEMPLATE + custom |
2207 | + client = 'chrony' |
2208 | + cfg = { |
2209 | + 'pools': ['mypool.org'], |
2210 | + 'ntp_client': client, |
2211 | + 'config': { |
2212 | + 'template': user_template, |
2213 | + } |
2214 | + } |
2215 | + expected_merged_cfg = { |
2216 | + 'check_exe': 'chronyd', |
2217 | + 'confpath': '{tmpdir}/client.conf'.format(tmpdir=self.new_root), |
2218 | + 'template_name': 'client.conf', 'template': user_template, |
2219 | + 'service_name': 'chrony', 'packages': ['chrony']} |
2220 | + for distro in cc_ntp.distros: |
2221 | + mycloud = self._get_cloud(distro) |
2222 | + ntpconfig = self._mock_ntp_client_config(client=client, |
2223 | + distro=distro) |
2224 | + confpath = ntpconfig['confpath'] |
2225 | + m_select.return_value = ntpconfig |
2226 | + mock_path = 'cloudinit.config.cc_ntp.temp_utils._TMPDIR' |
2227 | + with mock.patch(mock_path, self.new_root): |
2228 | + cc_ntp.handle('notimportant', |
2229 | + {'ntp': cfg}, mycloud, None, None) |
2230 | + content = util.read_file_or_url('file://' + confpath).contents |
2231 | + self.assertEqual( |
2232 | + "servers []\npools ['mypool.org']\n%s" % custom, |
2233 | + content.decode()) |
2234 | + m_schema.assert_called_with(expected_merged_cfg) |
2235 | + |
2236 | + |
2237 | +class TestSupplementalSchemaValidation(CiTestCase): |
2238 | + |
2239 | + def test_error_on_missing_keys(self): |
2240 | + """ValueError raised reporting any missing required ntp:config keys""" |
2241 | + cfg = {} |
2242 | + match = (r'Invalid ntp configuration:\\nMissing required ntp:config' |
2243 | + ' keys: check_exe, confpath, packages, service_name') |
2244 | + with self.assertRaisesRegex(ValueError, match): |
2245 | + cc_ntp.supplemental_schema_validation(cfg) |
2246 | + |
2247 | + def test_error_requiring_either_template_or_template_name(self): |
2248 | + """ValueError raised if both template not template_name are None.""" |
2249 | + cfg = {'confpath': 'someconf', 'check_exe': '', 'service_name': '', |
2250 | + 'template': None, 'template_name': None, 'packages': []} |
2251 | + match = (r'Invalid ntp configuration:\\nEither ntp:config:template' |
2252 | + ' or ntp:config:template_name values are required') |
2253 | + with self.assertRaisesRegex(ValueError, match): |
2254 | + cc_ntp.supplemental_schema_validation(cfg) |
2255 | + |
2256 | + def test_error_on_non_list_values(self): |
2257 | + """ValueError raised when packages is not of type list.""" |
2258 | + cfg = {'confpath': 'someconf', 'check_exe': '', 'service_name': '', |
2259 | + 'template': 'asdf', 'template_name': None, 'packages': 'NOPE'} |
2260 | + match = (r'Invalid ntp configuration:\\nExpected a list of required' |
2261 | + ' package names for ntp:config:packages. Found \(NOPE\)') |
2262 | + with self.assertRaisesRegex(ValueError, match): |
2263 | + cc_ntp.supplemental_schema_validation(cfg) |
2264 | + |
2265 | + def test_error_on_non_string_values(self): |
2266 | + """ValueError raised for any values expected as string type.""" |
2267 | + cfg = {'confpath': 1, 'check_exe': 2, 'service_name': 3, |
2268 | + 'template': 4, 'template_name': 5, 'packages': []} |
2269 | + errors = [ |
2270 | + 'Expected a config file path ntp:config:confpath. Found (1)', |
2271 | + 'Expected a string type for ntp:config:check_exe. Found (2)', |
2272 | + 'Expected a string type for ntp:config:service_name. Found (3)', |
2273 | + 'Expected a string type for ntp:config:template. Found (4)', |
2274 | + 'Expected a string type for ntp:config:template_name. Found (5)'] |
2275 | + with self.assertRaises(ValueError) as context_mgr: |
2276 | + cc_ntp.supplemental_schema_validation(cfg) |
2277 | + error_msg = str(context_mgr.exception) |
2278 | + for error in errors: |
2279 | + self.assertIn(error, error_msg) |
2280 | |
2281 | # vi: ts=4 expandtab |
2282 | diff --git a/tests/unittests/test_templating.py b/tests/unittests/test_templating.py |
2283 | index 53154d3..1080e13 100644 |
2284 | --- a/tests/unittests/test_templating.py |
2285 | +++ b/tests/unittests/test_templating.py |
2286 | @@ -10,6 +10,7 @@ from cloudinit.tests import helpers as test_helpers |
2287 | import textwrap |
2288 | |
2289 | from cloudinit import templater |
2290 | +from cloudinit.util import load_file, write_file |
2291 | |
2292 | try: |
2293 | import Cheetah |
2294 | @@ -19,7 +20,17 @@ except ImportError: |
2295 | HAS_CHEETAH = False |
2296 | |
2297 | |
2298 | -class TestTemplates(test_helpers.TestCase): |
2299 | +class TestTemplates(test_helpers.CiTestCase): |
2300 | + jinja_utf8 = b'It\xe2\x80\x99s not ascii, {{name}}\n' |
2301 | + jinja_utf8_rbob = b'It\xe2\x80\x99s not ascii, bob\n'.decode('utf-8') |
2302 | + |
2303 | + @staticmethod |
2304 | + def add_header(renderer, data): |
2305 | + """Return text (py2 unicode/py3 str) with template header.""" |
2306 | + if isinstance(data, bytes): |
2307 | + data = data.decode('utf-8') |
2308 | + return "## template: %s\n" % renderer + data |
2309 | + |
2310 | def test_render_basic(self): |
2311 | in_data = textwrap.dedent(""" |
2312 | ${b} |
2313 | @@ -106,4 +117,32 @@ $a,$b''' |
2314 | 'codename': codename}) |
2315 | self.assertEqual(ex_data, out_data) |
2316 | |
2317 | + def test_jinja_nonascii_render_to_string(self): |
2318 | + """Test jinja render_to_string with non-ascii content.""" |
2319 | + self.assertEqual( |
2320 | + templater.render_string( |
2321 | + self.add_header("jinja", self.jinja_utf8), {"name": "bob"}), |
2322 | + self.jinja_utf8_rbob) |
2323 | + |
2324 | + def test_jinja_nonascii_render_to_file(self): |
2325 | + """Test jinja render_to_file of a filename with non-ascii content.""" |
2326 | + tmpl_fn = self.tmp_path("j-render-to-file.template") |
2327 | + out_fn = self.tmp_path("j-render-to-file.out") |
2328 | + write_file(filename=tmpl_fn, omode="wb", |
2329 | + content=self.add_header( |
2330 | + "jinja", self.jinja_utf8).encode('utf-8')) |
2331 | + templater.render_to_file(tmpl_fn, out_fn, {"name": "bob"}) |
2332 | + result = load_file(out_fn, decode=False).decode('utf-8') |
2333 | + self.assertEqual(result, self.jinja_utf8_rbob) |
2334 | + |
2335 | + def test_jinja_nonascii_render_from_file(self): |
2336 | + """Test jinja render_from_file with non-ascii content.""" |
2337 | + tmpl_fn = self.tmp_path("j-render-from-file.template") |
2338 | + write_file(tmpl_fn, omode="wb", |
2339 | + content=self.add_header( |
2340 | + "jinja", self.jinja_utf8).encode('utf-8')) |
2341 | + result = templater.render_from_file(tmpl_fn, {"name": "bob"}) |
2342 | + self.assertEqual(result, self.jinja_utf8_rbob) |
2343 | + |
2344 | + |
2345 | # vi: ts=4 expandtab |
2346 | diff --git a/tools/make-tarball b/tools/make-tarball |
2347 | index 3197689..8d54013 100755 |
2348 | --- a/tools/make-tarball |
2349 | +++ b/tools/make-tarball |
2350 | @@ -13,22 +13,28 @@ Usage: ${0##*/} [revision] |
2351 | create a tarball of revision (default HEAD) |
2352 | |
2353 | options: |
2354 | - -o | --output FILE write to file |
2355 | + -h | --help print usage |
2356 | + -o | --output FILE write to file |
2357 | + --orig-tarball Write file cloud-init_<version>.orig.tar.gz |
2358 | + --long Use git describe --long for versioning |
2359 | EOF |
2360 | } |
2361 | |
2362 | short_opts="ho:v" |
2363 | -long_opts="help,output:,long,verbose" |
2364 | +long_opts="help,output:,orig-tarball,long" |
2365 | getopt_out=$(getopt --name "${0##*/}" \ |
2366 | --options "${short_opts}" --long "${long_opts}" -- "$@") && |
2367 | eval set -- "${getopt_out}" || { Usage 1>&2; exit 1; } |
2368 | |
2369 | long_opt="" |
2370 | +orig_opt="" |
2371 | while [ $# -ne 0 ]; do |
2372 | cur=$1; next=$2 |
2373 | case "$cur" in |
2374 | + -h|--help) Usage; exit 0;; |
2375 | -o|--output) output=$next; shift;; |
2376 | --long) long_opt="--long";; |
2377 | + --orig-tarball) orig_opt=".orig";; |
2378 | --) shift; break;; |
2379 | esac |
2380 | shift; |
2381 | @@ -39,7 +45,10 @@ version=$(git describe --abbrev=8 "--match=[0-9]*" ${long_opt} $rev) |
2382 | |
2383 | archive_base="cloud-init-$version" |
2384 | if [ -z "$output" ]; then |
2385 | - output="$archive_base.tar.gz" |
2386 | + if [ ! -z "$orig_opt" ]; then |
2387 | + archive_base="cloud-init_$version" |
2388 | + fi |
2389 | + output="$archive_base$orig_opt.tar.gz" |
2390 | fi |
2391 | |
2392 | # when building an archiving from HEAD, ensure that there aren't any |
PASSED: Continuous integration, rev:b602a10c062 0f77882113e14c1 45b0877a455b02 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1004/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 1004/rebuild
https:/