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