Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel
- Git
- lp:~chad.smith/cloud-init
- ubuntu/devel
- Merge into ubuntu/devel
Proposed by
Chad Smith
Status: | Merged |
---|---|
Approved by: | Scott Moser |
Approved revision: | 25198d2af0ece1941edfe1bd5e6476a5c34afe86 |
Merged at revision: | f3fd0af35de9298d716a440cc316c9ed4aff90cf |
Proposed branch: | ~chad.smith/cloud-init:ubuntu/devel |
Merge into: | cloud-init:ubuntu/devel |
Diff against target: |
1052 lines (+551/-128) 17 files modified
cloudinit/apport.py (+3/-3) cloudinit/net/network_state.py (+10/-0) cloudinit/settings.py (+1/-0) cloudinit/sources/DataSourceAliYun.py (+1/-1) cloudinit/sources/DataSourceCloudSigma.py (+1/-1) cloudinit/sources/DataSourceGCE.py (+1/-1) cloudinit/sources/DataSourceHetzner.py (+100/-0) cloudinit/sources/DataSourceOpenNebula.py (+75/-31) cloudinit/sources/DataSourceScaleway.py (+1/-1) cloudinit/sources/helpers/hetzner.py (+26/-0) cloudinit/sources/tests/test_init.py (+28/-0) debian/changelog (+11/-0) tests/unittests/test_datasource/test_common.py (+2/-0) tests/unittests/test_datasource/test_hetzner.py (+99/-0) tests/unittests/test_datasource/test_opennebula.py (+177/-89) tests/unittests/test_ds_identify.py (+9/-0) tools/ds-identify (+6/-1) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
Scott Moser | Pending | ||
Review via email: mp+341778@code.launchpad.net |
Commit message
Description of the change
sync upstream master to Bionic for release (include fix for GCE datasource LP: #1757176)
To post a comment you must log in.
Revision history for this message
Server Team CI bot (server-team-bot) wrote : | # |
review:
Approve
(continuous-integration)
There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/cloudinit/apport.py b/cloudinit/apport.py |
2 | index 221f341..618b016 100644 |
3 | --- a/cloudinit/apport.py |
4 | +++ b/cloudinit/apport.py |
5 | @@ -14,9 +14,9 @@ except ImportError: |
6 | |
7 | KNOWN_CLOUD_NAMES = [ |
8 | 'Amazon - Ec2', 'AliYun', 'AltCloud', 'Azure', 'Bigstep', 'CloudSigma', |
9 | - 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', 'MAAS', |
10 | - 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', 'Scaleway', 'SmartOS', |
11 | - 'VMware', 'Other'] |
12 | + 'CloudStack', 'DigitalOcean', 'GCE - Google Compute Engine', |
13 | + 'Hetzner Cloud', 'MAAS', 'NoCloud', 'OpenNebula', 'OpenStack', 'OVF', |
14 | + 'Scaleway', 'SmartOS', 'VMware', 'Other'] |
15 | |
16 | # Potentially clear text collected logs |
17 | CLOUDINIT_LOG = '/var/log/cloud-init.log' |
18 | diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py |
19 | index 1dd7ded..6d63e5c 100644 |
20 | --- a/cloudinit/net/network_state.py |
21 | +++ b/cloudinit/net/network_state.py |
22 | @@ -708,6 +708,7 @@ class NetworkStateInterpreter(object): |
23 | |
24 | gateway4 = None |
25 | gateway6 = None |
26 | + nameservers = {} |
27 | for address in cfg.get('addresses', []): |
28 | subnet = { |
29 | 'type': 'static', |
30 | @@ -723,6 +724,15 @@ class NetworkStateInterpreter(object): |
31 | gateway4 = cfg.get('gateway4') |
32 | subnet.update({'gateway': gateway4}) |
33 | |
34 | + if 'nameservers' in cfg and not nameservers: |
35 | + addresses = cfg.get('nameservers').get('addresses') |
36 | + if addresses: |
37 | + nameservers['dns_nameservers'] = addresses |
38 | + search = cfg.get('nameservers').get('search') |
39 | + if search: |
40 | + nameservers['dns_search'] = search |
41 | + subnet.update(nameservers) |
42 | + |
43 | subnets.append(subnet) |
44 | |
45 | routes = [] |
46 | diff --git a/cloudinit/settings.py b/cloudinit/settings.py |
47 | index c120498..5fe749d 100644 |
48 | --- a/cloudinit/settings.py |
49 | +++ b/cloudinit/settings.py |
50 | @@ -36,6 +36,7 @@ CFG_BUILTIN = { |
51 | 'SmartOS', |
52 | 'Bigstep', |
53 | 'Scaleway', |
54 | + 'Hetzner', |
55 | # At the end to act as a 'catch' when none of the above work... |
56 | 'None', |
57 | ], |
58 | diff --git a/cloudinit/sources/DataSourceAliYun.py b/cloudinit/sources/DataSourceAliYun.py |
59 | index 7ac8288..22279d0 100644 |
60 | --- a/cloudinit/sources/DataSourceAliYun.py |
61 | +++ b/cloudinit/sources/DataSourceAliYun.py |
62 | @@ -22,7 +22,7 @@ class DataSourceAliYun(EC2.DataSourceEc2): |
63 | super(DataSourceAliYun, self).__init__(sys_cfg, distro, paths) |
64 | self.seed_dir = os.path.join(paths.seed_dir, "AliYun") |
65 | |
66 | - def get_hostname(self, fqdn=False, _resolve_ip=False): |
67 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
68 | return self.metadata.get('hostname', 'localhost.localdomain') |
69 | |
70 | def get_public_ssh_keys(self): |
71 | diff --git a/cloudinit/sources/DataSourceCloudSigma.py b/cloudinit/sources/DataSourceCloudSigma.py |
72 | index 4eaad47..c816f34 100644 |
73 | --- a/cloudinit/sources/DataSourceCloudSigma.py |
74 | +++ b/cloudinit/sources/DataSourceCloudSigma.py |
75 | @@ -84,7 +84,7 @@ class DataSourceCloudSigma(sources.DataSource): |
76 | |
77 | return True |
78 | |
79 | - def get_hostname(self, fqdn=False, resolve_ip=False): |
80 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
81 | """ |
82 | Cleans up and uses the server's name if the latter is set. Otherwise |
83 | the first part from uuid is being used. |
84 | diff --git a/cloudinit/sources/DataSourceGCE.py b/cloudinit/sources/DataSourceGCE.py |
85 | index bebc991..d816262 100644 |
86 | --- a/cloudinit/sources/DataSourceGCE.py |
87 | +++ b/cloudinit/sources/DataSourceGCE.py |
88 | @@ -90,7 +90,7 @@ class DataSourceGCE(sources.DataSource): |
89 | public_keys_data = self.metadata['public-keys-data'] |
90 | return _parse_public_keys(public_keys_data, self.default_user) |
91 | |
92 | - def get_hostname(self, fqdn=False, resolve_ip=False): |
93 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
94 | # GCE has long FDQN's and has asked for short hostnames. |
95 | return self.metadata['local-hostname'].split('.')[0] |
96 | |
97 | diff --git a/cloudinit/sources/DataSourceHetzner.py b/cloudinit/sources/DataSourceHetzner.py |
98 | new file mode 100644 |
99 | index 0000000..769fe13 |
100 | --- /dev/null |
101 | +++ b/cloudinit/sources/DataSourceHetzner.py |
102 | @@ -0,0 +1,100 @@ |
103 | +# Author: Jonas Keidel <jonas.keidel@hetzner.com> |
104 | +# Author: Markus Schade <markus.schade@hetzner.com> |
105 | +# |
106 | +# This file is part of cloud-init. See LICENSE file for license information. |
107 | +# |
108 | +"""Hetzner Cloud API Documentation. |
109 | + https://docs.hetzner.cloud/""" |
110 | + |
111 | +from cloudinit import log as logging |
112 | +from cloudinit import net as cloudnet |
113 | +from cloudinit import sources |
114 | +from cloudinit import util |
115 | + |
116 | +import cloudinit.sources.helpers.hetzner as hc_helper |
117 | + |
118 | +LOG = logging.getLogger(__name__) |
119 | + |
120 | +BASE_URL_V1 = 'http://169.254.169.254/hetzner/v1' |
121 | + |
122 | +BUILTIN_DS_CONFIG = { |
123 | + 'metadata_url': BASE_URL_V1 + '/metadata', |
124 | + 'userdata_url': BASE_URL_V1 + '/userdata', |
125 | +} |
126 | + |
127 | +MD_RETRIES = 60 |
128 | +MD_TIMEOUT = 2 |
129 | +MD_WAIT_RETRY = 2 |
130 | + |
131 | + |
132 | +class DataSourceHetzner(sources.DataSource): |
133 | + def __init__(self, sys_cfg, distro, paths): |
134 | + sources.DataSource.__init__(self, sys_cfg, distro, paths) |
135 | + self.distro = distro |
136 | + self.metadata = dict() |
137 | + self.ds_cfg = util.mergemanydict([ |
138 | + util.get_cfg_by_path(sys_cfg, ["datasource", "Hetzner"], {}), |
139 | + BUILTIN_DS_CONFIG]) |
140 | + self.metadata_address = self.ds_cfg['metadata_url'] |
141 | + self.userdata_address = self.ds_cfg['userdata_url'] |
142 | + self.retries = self.ds_cfg.get('retries', MD_RETRIES) |
143 | + self.timeout = self.ds_cfg.get('timeout', MD_TIMEOUT) |
144 | + self.wait_retry = self.ds_cfg.get('wait_retry', MD_WAIT_RETRY) |
145 | + self._network_config = None |
146 | + self.dsmode = sources.DSMODE_NETWORK |
147 | + |
148 | + def get_data(self): |
149 | + nic = cloudnet.find_fallback_nic() |
150 | + with cloudnet.EphemeralIPv4Network(nic, "169.254.0.1", 16, |
151 | + "169.254.255.255"): |
152 | + md = hc_helper.read_metadata( |
153 | + self.metadata_address, timeout=self.timeout, |
154 | + sec_between=self.wait_retry, retries=self.retries) |
155 | + ud = hc_helper.read_userdata( |
156 | + self.userdata_address, timeout=self.timeout, |
157 | + sec_between=self.wait_retry, retries=self.retries) |
158 | + |
159 | + self.userdata_raw = ud |
160 | + self.metadata_full = md |
161 | + |
162 | + """hostname is name provided by user at launch. The API enforces |
163 | + it is a valid hostname, but it is not guaranteed to be resolvable |
164 | + in dns or fully qualified.""" |
165 | + self.metadata['instance-id'] = md['instance-id'] |
166 | + self.metadata['local-hostname'] = md['hostname'] |
167 | + self.metadata['network-config'] = md.get('network-config', None) |
168 | + self.metadata['public-keys'] = md.get('public-keys', None) |
169 | + self.vendordata_raw = md.get("vendor_data", None) |
170 | + |
171 | + return True |
172 | + |
173 | + @property |
174 | + def network_config(self): |
175 | + """Configure the networking. This needs to be done each boot, since |
176 | + the IP information may have changed due to snapshot and/or |
177 | + migration. |
178 | + """ |
179 | + |
180 | + if self._network_config: |
181 | + return self._network_config |
182 | + |
183 | + _net_config = self.metadata['network-config'] |
184 | + if not _net_config: |
185 | + raise Exception("Unable to get meta-data from server....") |
186 | + |
187 | + self._network_config = _net_config |
188 | + |
189 | + return self._network_config |
190 | + |
191 | + |
192 | +# Used to match classes to dependencies |
193 | +datasources = [ |
194 | + (DataSourceHetzner, (sources.DEP_FILESYSTEM, )), |
195 | +] |
196 | + |
197 | + |
198 | +# Return a list of data sources that match this set of dependencies |
199 | +def get_datasource_list(depends): |
200 | + return sources.list_from_depends(depends, datasources) |
201 | + |
202 | +# vi: ts=4 expandtab |
203 | diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py |
204 | index 9450835..d4a4111 100644 |
205 | --- a/cloudinit/sources/DataSourceOpenNebula.py |
206 | +++ b/cloudinit/sources/DataSourceOpenNebula.py |
207 | @@ -20,7 +20,6 @@ import string |
208 | |
209 | from cloudinit import log as logging |
210 | from cloudinit import net |
211 | -from cloudinit.net import eni |
212 | from cloudinit import sources |
213 | from cloudinit import util |
214 | |
215 | @@ -91,19 +90,19 @@ class DataSourceOpenNebula(sources.DataSource): |
216 | return False |
217 | |
218 | self.seed = seed |
219 | - self.network_eni = results.get('network-interfaces') |
220 | + self.network = results.get('network-interfaces') |
221 | self.metadata = md |
222 | self.userdata_raw = results.get('userdata') |
223 | return True |
224 | |
225 | @property |
226 | def network_config(self): |
227 | - if self.network_eni is not None: |
228 | - return eni.convert_eni_data(self.network_eni) |
229 | + if self.network is not None: |
230 | + return self.network |
231 | else: |
232 | return None |
233 | |
234 | - def get_hostname(self, fqdn=False, resolve_ip=None): |
235 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
236 | if resolve_ip is None: |
237 | if self.dsmode == sources.DSMODE_NETWORK: |
238 | resolve_ip = True |
239 | @@ -143,18 +142,42 @@ class OpenNebulaNetwork(object): |
240 | def mac2network(self, mac): |
241 | return self.mac2ip(mac).rpartition(".")[0] + ".0" |
242 | |
243 | - def get_dns(self, dev): |
244 | - return self.get_field(dev, "dns", "").split() |
245 | + def get_nameservers(self, dev): |
246 | + nameservers = {} |
247 | + dns = self.get_field(dev, "dns", "").split() |
248 | + dns.extend(self.context.get('DNS', "").split()) |
249 | + if dns: |
250 | + nameservers['addresses'] = dns |
251 | + search_domain = self.get_field(dev, "search_domain", "").split() |
252 | + if search_domain: |
253 | + nameservers['search'] = search_domain |
254 | + return nameservers |
255 | |
256 | - def get_domain(self, dev): |
257 | - return self.get_field(dev, "domain") |
258 | + def get_mtu(self, dev): |
259 | + return self.get_field(dev, "mtu") |
260 | |
261 | def get_ip(self, dev, mac): |
262 | return self.get_field(dev, "ip", self.mac2ip(mac)) |
263 | |
264 | + def get_ip6(self, dev): |
265 | + addresses6 = [] |
266 | + ip6 = self.get_field(dev, "ip6") |
267 | + if ip6: |
268 | + addresses6.append(ip6) |
269 | + ip6_ula = self.get_field(dev, "ip6_ula") |
270 | + if ip6_ula: |
271 | + addresses6.append(ip6_ula) |
272 | + return addresses6 |
273 | + |
274 | + def get_ip6_prefix(self, dev): |
275 | + return self.get_field(dev, "ip6_prefix_length", "64") |
276 | + |
277 | def get_gateway(self, dev): |
278 | return self.get_field(dev, "gateway") |
279 | |
280 | + def get_gateway6(self, dev): |
281 | + return self.get_field(dev, "gateway6") |
282 | + |
283 | def get_mask(self, dev): |
284 | return self.get_field(dev, "mask", "255.255.255.0") |
285 | |
286 | @@ -171,10 +194,11 @@ class OpenNebulaNetwork(object): |
287 | return default if val in (None, "") else val |
288 | |
289 | def gen_conf(self): |
290 | - global_dns = self.context.get('DNS', "").split() |
291 | - |
292 | - conf = ['auto lo', 'iface lo inet loopback', ''] |
293 | + netconf = {} |
294 | + netconf['version'] = 2 |
295 | + netconf['ethernets'] = {} |
296 | |
297 | + ethernets = {} |
298 | for mac, dev in self.ifaces.items(): |
299 | mac = mac.lower() |
300 | |
301 | @@ -182,29 +206,49 @@ class OpenNebulaNetwork(object): |
302 | # dev stores the current system name. |
303 | c_dev = self.context_devname.get(mac, dev) |
304 | |
305 | - conf.append('auto ' + dev) |
306 | - conf.append('iface ' + dev + ' inet static') |
307 | - conf.append(' #hwaddress %s' % mac) |
308 | - conf.append(' address ' + self.get_ip(c_dev, mac)) |
309 | - conf.append(' network ' + self.get_network(c_dev, mac)) |
310 | - conf.append(' netmask ' + self.get_mask(c_dev)) |
311 | + devconf = {} |
312 | + |
313 | + # Set MAC address |
314 | + devconf['match'] = {'macaddress': mac} |
315 | |
316 | + # Set IPv4 address |
317 | + devconf['addresses'] = [] |
318 | + mask = self.get_mask(c_dev) |
319 | + prefix = str(net.mask_to_net_prefix(mask)) |
320 | + devconf['addresses'].append( |
321 | + self.get_ip(c_dev, mac) + '/' + prefix) |
322 | + |
323 | + # Set IPv6 Global and ULA address |
324 | + addresses6 = self.get_ip6(c_dev) |
325 | + if addresses6: |
326 | + prefix6 = self.get_ip6_prefix(c_dev) |
327 | + devconf['addresses'].extend( |
328 | + [i + '/' + prefix6 for i in addresses6]) |
329 | + |
330 | + # Set IPv4 default gateway |
331 | gateway = self.get_gateway(c_dev) |
332 | if gateway: |
333 | - conf.append(' gateway ' + gateway) |
334 | + devconf['gateway4'] = gateway |
335 | + |
336 | + # Set IPv6 default gateway |
337 | + gateway6 = self.get_gateway6(c_dev) |
338 | + if gateway: |
339 | + devconf['gateway6'] = gateway6 |
340 | |
341 | - domain = self.get_domain(c_dev) |
342 | - if domain: |
343 | - conf.append(' dns-search ' + domain) |
344 | + # Set DNS servers and search domains |
345 | + nameservers = self.get_nameservers(c_dev) |
346 | + if nameservers: |
347 | + devconf['nameservers'] = nameservers |
348 | |
349 | - # add global DNS servers to all interfaces |
350 | - dns = self.get_dns(c_dev) |
351 | - if global_dns or dns: |
352 | - conf.append(' dns-nameservers ' + ' '.join(global_dns + dns)) |
353 | + # Set MTU size |
354 | + mtu = self.get_mtu(c_dev) |
355 | + if mtu: |
356 | + devconf['mtu'] = mtu |
357 | |
358 | - conf.append('') |
359 | + ethernets[dev] = devconf |
360 | |
361 | - return "\n".join(conf) |
362 | + netconf['ethernets'] = ethernets |
363 | + return(netconf) |
364 | |
365 | |
366 | def find_candidate_devs(): |
367 | @@ -390,10 +434,10 @@ def read_context_disk_dir(source_dir, asuser=None): |
368 | except TypeError: |
369 | LOG.warning("Failed base64 decoding of userdata") |
370 | |
371 | - # generate static /etc/network/interfaces |
372 | + # generate Network Configuration v2 |
373 | # only if there are any required context variables |
374 | - # http://opennebula.org/documentation:rel3.8:cong#network_configuration |
375 | - ipaddr_keys = [k for k in context if re.match(r'^ETH\d+_IP$', k)] |
376 | + # http://docs.opennebula.org/5.4/operation/references/template.html#context-section |
377 | + ipaddr_keys = [k for k in context if re.match(r'^ETH\d+_IP.*$', k)] |
378 | if ipaddr_keys: |
379 | onet = OpenNebulaNetwork(context) |
380 | results['network-interfaces'] = onet.gen_conf() |
381 | diff --git a/cloudinit/sources/DataSourceScaleway.py b/cloudinit/sources/DataSourceScaleway.py |
382 | index b0b19c9..9005624 100644 |
383 | --- a/cloudinit/sources/DataSourceScaleway.py |
384 | +++ b/cloudinit/sources/DataSourceScaleway.py |
385 | @@ -215,7 +215,7 @@ class DataSourceScaleway(sources.DataSource): |
386 | def get_public_ssh_keys(self): |
387 | return [key['key'] for key in self.metadata['ssh_public_keys']] |
388 | |
389 | - def get_hostname(self, fqdn=False, resolve_ip=False): |
390 | + def get_hostname(self, fqdn=False, resolve_ip=False, metadata_only=False): |
391 | return self.metadata['hostname'] |
392 | |
393 | @property |
394 | diff --git a/cloudinit/sources/helpers/hetzner.py b/cloudinit/sources/helpers/hetzner.py |
395 | new file mode 100644 |
396 | index 0000000..2554530 |
397 | --- /dev/null |
398 | +++ b/cloudinit/sources/helpers/hetzner.py |
399 | @@ -0,0 +1,26 @@ |
400 | +# Author: Jonas Keidel <jonas.keidel@hetzner.com> |
401 | +# Author: Markus Schade <markus.schade@hetzner.com> |
402 | +# |
403 | +# This file is part of cloud-init. See LICENSE file for license information. |
404 | + |
405 | +from cloudinit import log as logging |
406 | +from cloudinit import url_helper |
407 | +from cloudinit import util |
408 | + |
409 | +LOG = logging.getLogger(__name__) |
410 | + |
411 | + |
412 | +def read_metadata(url, timeout=2, sec_between=2, retries=30): |
413 | + response = url_helper.readurl(url, timeout=timeout, |
414 | + sec_between=sec_between, retries=retries) |
415 | + if not response.ok(): |
416 | + raise RuntimeError("unable to read metadata at %s" % url) |
417 | + return util.load_yaml(response.contents.decode()) |
418 | + |
419 | + |
420 | +def read_userdata(url, timeout=2, sec_between=2, retries=30): |
421 | + response = url_helper.readurl(url, timeout=timeout, |
422 | + sec_between=sec_between, retries=retries) |
423 | + if not response.ok(): |
424 | + raise RuntimeError("unable to read userdata at %s" % url) |
425 | + return response.contents |
426 | diff --git a/cloudinit/sources/tests/test_init.py b/cloudinit/sources/tests/test_init.py |
427 | index 5065083..e7fda22 100644 |
428 | --- a/cloudinit/sources/tests/test_init.py |
429 | +++ b/cloudinit/sources/tests/test_init.py |
430 | @@ -1,10 +1,12 @@ |
431 | # This file is part of cloud-init. See LICENSE file for license information. |
432 | |
433 | +import inspect |
434 | import os |
435 | import six |
436 | import stat |
437 | |
438 | from cloudinit.helpers import Paths |
439 | +from cloudinit import importer |
440 | from cloudinit.sources import ( |
441 | INSTANCE_JSON_FILE, DataSource) |
442 | from cloudinit.tests.helpers import CiTestCase, skipIf, mock |
443 | @@ -268,3 +270,29 @@ class TestDataSource(CiTestCase): |
444 | "WARNING: Error persisting instance-data.json: 'utf8' codec can't" |
445 | " decode byte 0xaa in position 2: invalid start byte", |
446 | self.logs.getvalue()) |
447 | + |
448 | + def test_get_hostname_subclass_support(self): |
449 | + """Validate get_hostname signature on all subclasses of DataSource.""" |
450 | + # Use inspect.getfullargspec when we drop py2.6 and py2.7 |
451 | + get_args = inspect.getargspec # pylint: disable=W1505 |
452 | + base_args = get_args(DataSource.get_hostname) # pylint: disable=W1505 |
453 | + # Import all DataSource subclasses so we can inspect them. |
454 | + modules = util.find_modules(os.path.dirname(os.path.dirname(__file__))) |
455 | + for loc, name in modules.items(): |
456 | + mod_locs, _ = importer.find_module(name, ['cloudinit.sources'], []) |
457 | + if mod_locs: |
458 | + importer.import_module(mod_locs[0]) |
459 | + for child in DataSource.__subclasses__(): |
460 | + if 'Test' in child.dsname: |
461 | + continue |
462 | + self.assertEqual( |
463 | + base_args, |
464 | + get_args(child.get_hostname), # pylint: disable=W1505 |
465 | + '%s does not implement DataSource.get_hostname params' |
466 | + % child) |
467 | + for grandchild in child.__subclasses__(): |
468 | + self.assertEqual( |
469 | + base_args, |
470 | + get_args(grandchild.get_hostname), # pylint: disable=W1505 |
471 | + '%s does not implement DataSource.get_hostname params' |
472 | + % grandchild) |
473 | diff --git a/debian/changelog b/debian/changelog |
474 | index f225d52..68191a2 100644 |
475 | --- a/debian/changelog |
476 | +++ b/debian/changelog |
477 | @@ -1,3 +1,14 @@ |
478 | +cloud-init (18.1-26-g685f9901-0ubuntu1) bionic; urgency=medium |
479 | + |
480 | + * New upstream snapshot. |
481 | + - datasources: fix DataSource subclass get_hostname method signature |
482 | + (LP: #1757176) |
483 | + - OpenNebula: Update network to return v2 config rather than ENI. |
484 | + [Akihiko Ota] |
485 | + - Add Hetzner Cloud DataSource [Markus Schade] |
486 | + |
487 | + -- Chad Smith <chad.smith@canonical.com> Tue, 20 Mar 2018 16:40:54 -0600 |
488 | + |
489 | cloud-init (18.1-23-gde34dc7c-0ubuntu1) bionic; urgency=medium |
490 | |
491 | * New upstream snapshot. |
492 | diff --git a/tests/unittests/test_datasource/test_common.py b/tests/unittests/test_datasource/test_common.py |
493 | index 80b9c65..6d2dc5b 100644 |
494 | --- a/tests/unittests/test_datasource/test_common.py |
495 | +++ b/tests/unittests/test_datasource/test_common.py |
496 | @@ -14,6 +14,7 @@ from cloudinit.sources import ( |
497 | DataSourceDigitalOcean as DigitalOcean, |
498 | DataSourceEc2 as Ec2, |
499 | DataSourceGCE as GCE, |
500 | + DataSourceHetzner as Hetzner, |
501 | DataSourceMAAS as MAAS, |
502 | DataSourceNoCloud as NoCloud, |
503 | DataSourceOpenNebula as OpenNebula, |
504 | @@ -31,6 +32,7 @@ DEFAULT_LOCAL = [ |
505 | CloudSigma.DataSourceCloudSigma, |
506 | ConfigDrive.DataSourceConfigDrive, |
507 | DigitalOcean.DataSourceDigitalOcean, |
508 | + Hetzner.DataSourceHetzner, |
509 | NoCloud.DataSourceNoCloud, |
510 | OpenNebula.DataSourceOpenNebula, |
511 | OVF.DataSourceOVF, |
512 | diff --git a/tests/unittests/test_datasource/test_hetzner.py b/tests/unittests/test_datasource/test_hetzner.py |
513 | new file mode 100644 |
514 | index 0000000..f1d1525 |
515 | --- /dev/null |
516 | +++ b/tests/unittests/test_datasource/test_hetzner.py |
517 | @@ -0,0 +1,99 @@ |
518 | +# Copyright (C) 2018 Jonas Keidel |
519 | +# |
520 | +# Author: Jonas Keidel <jonas.keidel@hetzner.com> |
521 | +# |
522 | +# This file is part of cloud-init. See LICENSE file for license information. |
523 | + |
524 | +from cloudinit.sources import DataSourceHetzner |
525 | +from cloudinit import util, settings, helpers |
526 | + |
527 | +from cloudinit.tests.helpers import mock, CiTestCase |
528 | + |
529 | +METADATA = util.load_yaml(""" |
530 | +hostname: cloudinit-test |
531 | +instance-id: 123456 |
532 | +local-ipv4: '' |
533 | +network-config: |
534 | + config: |
535 | + - mac_address: 96:00:00:08:19:da |
536 | + name: eth0 |
537 | + subnets: |
538 | + - dns_nameservers: |
539 | + - 213.133.99.99 |
540 | + - 213.133.100.100 |
541 | + - 213.133.98.98 |
542 | + ipv4: true |
543 | + type: dhcp |
544 | + type: physical |
545 | + - name: eth0:0 |
546 | + subnets: |
547 | + - address: 2a01:4f8:beef:beef::1/64 |
548 | + gateway: fe80::1 |
549 | + ipv6: true |
550 | + routes: |
551 | + - gateway: fe80::1%eth0 |
552 | + netmask: 0 |
553 | + network: '::' |
554 | + type: static |
555 | + type: physical |
556 | + version: 1 |
557 | +network-sysconfig: "DEVICE='eth0'\nTYPE=Ethernet\nBOOTPROTO=dhcp\n\ |
558 | + ONBOOT='yes'\nHWADDR=96:00:00:08:19:da\n\ |
559 | + IPV6INIT=yes\nIPV6ADDR=2a01:4f8:beef:beef::1/64\n\ |
560 | + IPV6_DEFAULTGW=fe80::1%eth0\nIPV6_AUTOCONF=no\n\ |
561 | + DNS1=213.133.99.99\nDNS2=213.133.100.100\n" |
562 | +public-ipv4: 192.168.0.1 |
563 | +public-keys: |
564 | +- ssh-ed25519 \ |
565 | + AAAAC3Nzac1lZdI1NTE5AaaAIaFrcac0yVITsmRrmueq6MD0qYNKlEvW8O1Ib4nkhmWh \ |
566 | + test-key@workstation |
567 | +vendor_data: "test" |
568 | +""") |
569 | + |
570 | +USERDATA = b"""#cloud-config |
571 | +runcmd: |
572 | +- [touch, /root/cloud-init-worked ] |
573 | +""" |
574 | + |
575 | + |
576 | +class TestDataSourceHetzner(CiTestCase): |
577 | + """ |
578 | + Test reading the meta-data |
579 | + """ |
580 | + def setUp(self): |
581 | + super(TestDataSourceHetzner, self).setUp() |
582 | + self.tmp = self.tmp_dir() |
583 | + |
584 | + def get_ds(self): |
585 | + ds = DataSourceHetzner.DataSourceHetzner( |
586 | + settings.CFG_BUILTIN, None, helpers.Paths({'run_dir': self.tmp})) |
587 | + return ds |
588 | + |
589 | + @mock.patch('cloudinit.net.EphemeralIPv4Network') |
590 | + @mock.patch('cloudinit.net.find_fallback_nic') |
591 | + @mock.patch('cloudinit.sources.helpers.hetzner.read_metadata') |
592 | + @mock.patch('cloudinit.sources.helpers.hetzner.read_userdata') |
593 | + def test_read_data(self, m_usermd, m_readmd, m_fallback_nic, m_net): |
594 | + m_readmd.return_value = METADATA.copy() |
595 | + m_usermd.return_value = USERDATA |
596 | + m_fallback_nic.return_value = 'eth0' |
597 | + |
598 | + ds = self.get_ds() |
599 | + ret = ds.get_data() |
600 | + self.assertTrue(ret) |
601 | + |
602 | + m_net.assert_called_once_with( |
603 | + 'eth0', '169.254.0.1', |
604 | + 16, '169.254.255.255' |
605 | + ) |
606 | + |
607 | + self.assertTrue(m_readmd.called) |
608 | + |
609 | + self.assertEqual(METADATA.get('hostname'), ds.get_hostname()) |
610 | + |
611 | + self.assertEqual(METADATA.get('public-keys'), |
612 | + ds.get_public_ssh_keys()) |
613 | + |
614 | + self.assertIsInstance(ds.get_public_ssh_keys(), list) |
615 | + self.assertEqual(ds.get_userdata_raw(), USERDATA) |
616 | + self.assertEqual(ds.get_vendordata_raw(), METADATA.get('vendor_data')) |
617 | diff --git a/tests/unittests/test_datasource/test_opennebula.py b/tests/unittests/test_datasource/test_opennebula.py |
618 | index 5c3ba01..ab42f34 100644 |
619 | --- a/tests/unittests/test_datasource/test_opennebula.py |
620 | +++ b/tests/unittests/test_datasource/test_opennebula.py |
621 | @@ -4,7 +4,6 @@ from cloudinit import helpers |
622 | from cloudinit.sources import DataSourceOpenNebula as ds |
623 | from cloudinit import util |
624 | from cloudinit.tests.helpers import mock, populate_dir, CiTestCase |
625 | -from textwrap import dedent |
626 | |
627 | import os |
628 | import pwd |
629 | @@ -33,6 +32,11 @@ HOSTNAME = 'foo.example.com' |
630 | PUBLIC_IP = '10.0.0.3' |
631 | MACADDR = '02:00:0a:12:01:01' |
632 | IP_BY_MACADDR = '10.18.1.1' |
633 | +IP4_PREFIX = '24' |
634 | +IP6_GLOBAL = '2001:db8:1:0:400:c0ff:fea8:1ba' |
635 | +IP6_ULA = 'fd01:dead:beaf:0:400:c0ff:fea8:1ba' |
636 | +IP6_GW = '2001:db8:1::ffff' |
637 | +IP6_PREFIX = '48' |
638 | |
639 | DS_PATH = "cloudinit.sources.DataSourceOpenNebula" |
640 | |
641 | @@ -221,7 +225,9 @@ class TestOpenNebulaDataSource(CiTestCase): |
642 | results = ds.read_context_disk_dir(self.seed_dir) |
643 | |
644 | self.assertTrue('network-interfaces' in results) |
645 | - self.assertTrue(IP_BY_MACADDR in results['network-interfaces']) |
646 | + self.assertTrue( |
647 | + IP_BY_MACADDR + '/' + IP4_PREFIX in |
648 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
649 | |
650 | # ETH0_IP and ETH0_MAC |
651 | populate_context_dir( |
652 | @@ -229,7 +235,9 @@ class TestOpenNebulaDataSource(CiTestCase): |
653 | results = ds.read_context_disk_dir(self.seed_dir) |
654 | |
655 | self.assertTrue('network-interfaces' in results) |
656 | - self.assertTrue(IP_BY_MACADDR in results['network-interfaces']) |
657 | + self.assertTrue( |
658 | + IP_BY_MACADDR + '/' + IP4_PREFIX in |
659 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
660 | |
661 | # ETH0_IP with empty string and ETH0_MAC |
662 | # in the case of using Virtual Network contains |
663 | @@ -239,55 +247,91 @@ class TestOpenNebulaDataSource(CiTestCase): |
664 | results = ds.read_context_disk_dir(self.seed_dir) |
665 | |
666 | self.assertTrue('network-interfaces' in results) |
667 | - self.assertTrue(IP_BY_MACADDR in results['network-interfaces']) |
668 | + self.assertTrue( |
669 | + IP_BY_MACADDR + '/' + IP4_PREFIX in |
670 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
671 | |
672 | - # ETH0_NETWORK |
673 | + # ETH0_MASK |
674 | populate_context_dir( |
675 | self.seed_dir, { |
676 | 'ETH0_IP': IP_BY_MACADDR, |
677 | 'ETH0_MAC': MACADDR, |
678 | - 'ETH0_NETWORK': '10.18.0.0' |
679 | + 'ETH0_MASK': '255.255.0.0' |
680 | }) |
681 | results = ds.read_context_disk_dir(self.seed_dir) |
682 | |
683 | self.assertTrue('network-interfaces' in results) |
684 | - self.assertTrue('10.18.0.0' in results['network-interfaces']) |
685 | + self.assertTrue( |
686 | + IP_BY_MACADDR + '/16' in |
687 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
688 | |
689 | - # ETH0_NETWORK with empty string |
690 | + # ETH0_MASK with empty string |
691 | populate_context_dir( |
692 | self.seed_dir, { |
693 | 'ETH0_IP': IP_BY_MACADDR, |
694 | 'ETH0_MAC': MACADDR, |
695 | - 'ETH0_NETWORK': '' |
696 | + 'ETH0_MASK': '' |
697 | }) |
698 | results = ds.read_context_disk_dir(self.seed_dir) |
699 | |
700 | self.assertTrue('network-interfaces' in results) |
701 | - self.assertTrue('10.18.1.0' in results['network-interfaces']) |
702 | + self.assertTrue( |
703 | + IP_BY_MACADDR + '/' + IP4_PREFIX in |
704 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
705 | |
706 | - # ETH0_MASK |
707 | + # ETH0_IP6 |
708 | populate_context_dir( |
709 | self.seed_dir, { |
710 | - 'ETH0_IP': IP_BY_MACADDR, |
711 | + 'ETH0_IP6': IP6_GLOBAL, |
712 | 'ETH0_MAC': MACADDR, |
713 | - 'ETH0_MASK': '255.255.0.0' |
714 | }) |
715 | results = ds.read_context_disk_dir(self.seed_dir) |
716 | |
717 | self.assertTrue('network-interfaces' in results) |
718 | - self.assertTrue('255.255.0.0' in results['network-interfaces']) |
719 | + self.assertTrue( |
720 | + IP6_GLOBAL + '/64' in |
721 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
722 | |
723 | - # ETH0_MASK with empty string |
724 | + # ETH0_IP6_ULA |
725 | populate_context_dir( |
726 | self.seed_dir, { |
727 | - 'ETH0_IP': IP_BY_MACADDR, |
728 | + 'ETH0_IP6_ULA': IP6_ULA, |
729 | + 'ETH0_MAC': MACADDR, |
730 | + }) |
731 | + results = ds.read_context_disk_dir(self.seed_dir) |
732 | + |
733 | + self.assertTrue('network-interfaces' in results) |
734 | + self.assertTrue( |
735 | + IP6_ULA + '/64' in |
736 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
737 | + |
738 | + # ETH0_IP6 and ETH0_IP6_PREFIX_LENGTH |
739 | + populate_context_dir( |
740 | + self.seed_dir, { |
741 | + 'ETH0_IP6': IP6_GLOBAL, |
742 | + 'ETH0_IP6_PREFIX_LENGTH': IP6_PREFIX, |
743 | + 'ETH0_MAC': MACADDR, |
744 | + }) |
745 | + results = ds.read_context_disk_dir(self.seed_dir) |
746 | + |
747 | + self.assertTrue('network-interfaces' in results) |
748 | + self.assertTrue( |
749 | + IP6_GLOBAL + '/' + IP6_PREFIX in |
750 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
751 | + |
752 | + # ETH0_IP6 and ETH0_IP6_PREFIX_LENGTH with empty string |
753 | + populate_context_dir( |
754 | + self.seed_dir, { |
755 | + 'ETH0_IP6': IP6_GLOBAL, |
756 | + 'ETH0_IP6_PREFIX_LENGTH': '', |
757 | 'ETH0_MAC': MACADDR, |
758 | - 'ETH0_MASK': '' |
759 | }) |
760 | results = ds.read_context_disk_dir(self.seed_dir) |
761 | |
762 | self.assertTrue('network-interfaces' in results) |
763 | - self.assertTrue('255.255.255.0' in results['network-interfaces']) |
764 | + self.assertTrue( |
765 | + IP6_GLOBAL + '/64' in |
766 | + results['network-interfaces']['ethernets'][dev]['addresses']) |
767 | |
768 | def test_find_candidates(self): |
769 | def my_devs_with(criteria): |
770 | @@ -310,108 +354,152 @@ class TestOpenNebulaNetwork(unittest.TestCase): |
771 | |
772 | system_nics = ('eth0', 'ens3') |
773 | |
774 | - def test_lo(self): |
775 | - net = ds.OpenNebulaNetwork(context={}, system_nics_by_mac={}) |
776 | - self.assertEqual(net.gen_conf(), u'''\ |
777 | -auto lo |
778 | -iface lo inet loopback |
779 | -''') |
780 | - |
781 | @mock.patch(DS_PATH + ".get_physical_nics_by_mac") |
782 | def test_eth0(self, m_get_phys_by_mac): |
783 | for nic in self.system_nics: |
784 | m_get_phys_by_mac.return_value = {MACADDR: nic} |
785 | net = ds.OpenNebulaNetwork({}) |
786 | - self.assertEqual(net.gen_conf(), dedent("""\ |
787 | - auto lo |
788 | - iface lo inet loopback |
789 | - |
790 | - auto {dev} |
791 | - iface {dev} inet static |
792 | - #hwaddress {macaddr} |
793 | - address 10.18.1.1 |
794 | - network 10.18.1.0 |
795 | - netmask 255.255.255.0 |
796 | - """.format(dev=nic, macaddr=MACADDR))) |
797 | + expected = { |
798 | + 'version': 2, |
799 | + 'ethernets': { |
800 | + nic: { |
801 | + 'match': {'macaddress': MACADDR}, |
802 | + 'addresses': [IP_BY_MACADDR + '/' + IP4_PREFIX]}}} |
803 | + |
804 | + self.assertEqual(net.gen_conf(), expected) |
805 | |
806 | def test_eth0_override(self): |
807 | + self.maxDiff = None |
808 | context = { |
809 | 'DNS': '1.2.3.8', |
810 | - 'ETH0_IP': '10.18.1.1', |
811 | - 'ETH0_NETWORK': '10.18.0.0', |
812 | - 'ETH0_MASK': '255.255.0.0', |
813 | + 'ETH0_DNS': '1.2.3.6 1.2.3.7', |
814 | 'ETH0_GATEWAY': '1.2.3.5', |
815 | - 'ETH0_DOMAIN': 'example.com', |
816 | + 'ETH0_GATEWAY6': '', |
817 | + 'ETH0_IP': IP_BY_MACADDR, |
818 | + 'ETH0_IP6': '', |
819 | + 'ETH0_IP6_PREFIX_LENGTH': '', |
820 | + 'ETH0_IP6_ULA': '', |
821 | + 'ETH0_MAC': '02:00:0a:12:01:01', |
822 | + 'ETH0_MASK': '255.255.0.0', |
823 | + 'ETH0_MTU': '', |
824 | + 'ETH0_NETWORK': '10.18.0.0', |
825 | + 'ETH0_SEARCH_DOMAIN': '', |
826 | + } |
827 | + for nic in self.system_nics: |
828 | + net = ds.OpenNebulaNetwork(context, |
829 | + system_nics_by_mac={MACADDR: nic}) |
830 | + expected = { |
831 | + 'version': 2, |
832 | + 'ethernets': { |
833 | + nic: { |
834 | + 'match': {'macaddress': MACADDR}, |
835 | + 'addresses': [IP_BY_MACADDR + '/16'], |
836 | + 'gateway4': '1.2.3.5', |
837 | + 'gateway6': None, |
838 | + 'nameservers': { |
839 | + 'addresses': ['1.2.3.6', '1.2.3.7', '1.2.3.8']}}}} |
840 | + |
841 | + self.assertEqual(expected, net.gen_conf()) |
842 | + |
843 | + def test_eth0_v4v6_override(self): |
844 | + self.maxDiff = None |
845 | + context = { |
846 | + 'DNS': '1.2.3.8', |
847 | 'ETH0_DNS': '1.2.3.6 1.2.3.7', |
848 | - 'ETH0_MAC': '02:00:0a:12:01:01' |
849 | + 'ETH0_GATEWAY': '1.2.3.5', |
850 | + 'ETH0_GATEWAY6': IP6_GW, |
851 | + 'ETH0_IP': IP_BY_MACADDR, |
852 | + 'ETH0_IP6': IP6_GLOBAL, |
853 | + 'ETH0_IP6_PREFIX_LENGTH': IP6_PREFIX, |
854 | + 'ETH0_IP6_ULA': IP6_ULA, |
855 | + 'ETH0_MAC': '02:00:0a:12:01:01', |
856 | + 'ETH0_MASK': '255.255.0.0', |
857 | + 'ETH0_MTU': '1280', |
858 | + 'ETH0_NETWORK': '10.18.0.0', |
859 | + 'ETH0_SEARCH_DOMAIN': 'example.com example.org', |
860 | } |
861 | for nic in self.system_nics: |
862 | - expected = dedent("""\ |
863 | - auto lo |
864 | - iface lo inet loopback |
865 | - |
866 | - auto {dev} |
867 | - iface {dev} inet static |
868 | - #hwaddress {macaddr} |
869 | - address 10.18.1.1 |
870 | - network 10.18.0.0 |
871 | - netmask 255.255.0.0 |
872 | - gateway 1.2.3.5 |
873 | - dns-search example.com |
874 | - dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7 |
875 | - """).format(dev=nic, macaddr=MACADDR) |
876 | net = ds.OpenNebulaNetwork(context, |
877 | system_nics_by_mac={MACADDR: nic}) |
878 | + |
879 | + expected = { |
880 | + 'version': 2, |
881 | + 'ethernets': { |
882 | + nic: { |
883 | + 'match': {'macaddress': MACADDR}, |
884 | + 'addresses': [ |
885 | + IP_BY_MACADDR + '/16', |
886 | + IP6_GLOBAL + '/' + IP6_PREFIX, |
887 | + IP6_ULA + '/' + IP6_PREFIX], |
888 | + 'gateway4': '1.2.3.5', |
889 | + 'gateway6': IP6_GW, |
890 | + 'nameservers': { |
891 | + 'addresses': ['1.2.3.6', '1.2.3.7', '1.2.3.8'], |
892 | + 'search': ['example.com', 'example.org']}, |
893 | + 'mtu': '1280'}}} |
894 | + |
895 | self.assertEqual(expected, net.gen_conf()) |
896 | |
897 | def test_multiple_nics(self): |
898 | """Test rendering multiple nics with names that differ from context.""" |
899 | + self.maxDiff = None |
900 | MAC_1 = "02:00:0a:12:01:01" |
901 | MAC_2 = "02:00:0a:12:01:02" |
902 | context = { |
903 | 'DNS': '1.2.3.8', |
904 | - 'ETH0_IP': '10.18.1.1', |
905 | - 'ETH0_NETWORK': '10.18.0.0', |
906 | - 'ETH0_MASK': '255.255.0.0', |
907 | - 'ETH0_GATEWAY': '1.2.3.5', |
908 | - 'ETH0_DOMAIN': 'example.com', |
909 | 'ETH0_DNS': '1.2.3.6 1.2.3.7', |
910 | + 'ETH0_GATEWAY': '1.2.3.5', |
911 | + 'ETH0_GATEWAY6': IP6_GW, |
912 | + 'ETH0_IP': '10.18.1.1', |
913 | + 'ETH0_IP6': IP6_GLOBAL, |
914 | + 'ETH0_IP6_PREFIX_LENGTH': '', |
915 | + 'ETH0_IP6_ULA': IP6_ULA, |
916 | 'ETH0_MAC': MAC_2, |
917 | - 'ETH3_IP': '10.3.1.3', |
918 | - 'ETH3_NETWORK': '10.3.0.0', |
919 | - 'ETH3_MASK': '255.255.0.0', |
920 | - 'ETH3_GATEWAY': '10.3.0.1', |
921 | - 'ETH3_DOMAIN': 'third.example.com', |
922 | + 'ETH0_MASK': '255.255.0.0', |
923 | + 'ETH0_MTU': '1280', |
924 | + 'ETH0_NETWORK': '10.18.0.0', |
925 | + 'ETH0_SEARCH_DOMAIN': 'example.com', |
926 | 'ETH3_DNS': '10.3.1.2', |
927 | + 'ETH3_GATEWAY': '10.3.0.1', |
928 | + 'ETH3_GATEWAY6': '', |
929 | + 'ETH3_IP': '10.3.1.3', |
930 | + 'ETH3_IP6': '', |
931 | + 'ETH3_IP6_PREFIX_LENGTH': '', |
932 | + 'ETH3_IP6_ULA': '', |
933 | 'ETH3_MAC': MAC_1, |
934 | + 'ETH3_MASK': '255.255.0.0', |
935 | + 'ETH3_MTU': '', |
936 | + 'ETH3_NETWORK': '10.3.0.0', |
937 | + 'ETH3_SEARCH_DOMAIN': 'third.example.com third.example.org', |
938 | } |
939 | net = ds.OpenNebulaNetwork( |
940 | context, system_nics_by_mac={MAC_1: 'enp0s25', MAC_2: 'enp1s2'}) |
941 | |
942 | - expected = dedent("""\ |
943 | - auto lo |
944 | - iface lo inet loopback |
945 | - |
946 | - auto enp0s25 |
947 | - iface enp0s25 inet static |
948 | - #hwaddress 02:00:0a:12:01:01 |
949 | - address 10.3.1.3 |
950 | - network 10.3.0.0 |
951 | - netmask 255.255.0.0 |
952 | - gateway 10.3.0.1 |
953 | - dns-search third.example.com |
954 | - dns-nameservers 1.2.3.8 10.3.1.2 |
955 | - |
956 | - auto enp1s2 |
957 | - iface enp1s2 inet static |
958 | - #hwaddress 02:00:0a:12:01:02 |
959 | - address 10.18.1.1 |
960 | - network 10.18.0.0 |
961 | - netmask 255.255.0.0 |
962 | - gateway 1.2.3.5 |
963 | - dns-search example.com |
964 | - dns-nameservers 1.2.3.8 1.2.3.6 1.2.3.7 |
965 | - """) |
966 | + expected = { |
967 | + 'version': 2, |
968 | + 'ethernets': { |
969 | + 'enp1s2': { |
970 | + 'match': {'macaddress': MAC_2}, |
971 | + 'addresses': [ |
972 | + '10.18.1.1/16', |
973 | + IP6_GLOBAL + '/64', |
974 | + IP6_ULA + '/64'], |
975 | + 'gateway4': '1.2.3.5', |
976 | + 'gateway6': IP6_GW, |
977 | + 'nameservers': { |
978 | + 'addresses': ['1.2.3.6', '1.2.3.7', '1.2.3.8'], |
979 | + 'search': ['example.com']}, |
980 | + 'mtu': '1280'}, |
981 | + 'enp0s25': { |
982 | + 'match': {'macaddress': MAC_1}, |
983 | + 'addresses': ['10.3.1.3/16'], |
984 | + 'gateway4': '10.3.0.1', |
985 | + 'gateway6': None, |
986 | + 'nameservers': { |
987 | + 'addresses': ['10.3.1.2', '1.2.3.8'], |
988 | + 'search': [ |
989 | + 'third.example.com', |
990 | + 'third.example.org']}}}} |
991 | |
992 | self.assertEqual(expected, net.gen_conf()) |
993 | |
994 | diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py |
995 | index 9be3f96..9c5628e 100644 |
996 | --- a/tests/unittests/test_ds_identify.py |
997 | +++ b/tests/unittests/test_ds_identify.py |
998 | @@ -60,6 +60,7 @@ P_CHASSIS_ASSET_TAG = "sys/class/dmi/id/chassis_asset_tag" |
999 | P_PRODUCT_NAME = "sys/class/dmi/id/product_name" |
1000 | P_PRODUCT_SERIAL = "sys/class/dmi/id/product_serial" |
1001 | P_PRODUCT_UUID = "sys/class/dmi/id/product_uuid" |
1002 | +P_SYS_VENDOR = "sys/class/dmi/id/sys_vendor" |
1003 | P_SEED_DIR = "var/lib/cloud/seed" |
1004 | P_DSID_CFG = "etc/cloud/ds-identify.cfg" |
1005 | |
1006 | @@ -379,6 +380,10 @@ class TestDsIdentify(CiTestCase): |
1007 | """Nocloud seed directory ubuntu core writable""" |
1008 | self._test_ds_found('NoCloud-seed-ubuntu-core') |
1009 | |
1010 | + def test_hetzner_found(self): |
1011 | + """Hetzner cloud is identified in sys_vendor.""" |
1012 | + self._test_ds_found('Hetzner') |
1013 | + |
1014 | |
1015 | def blkid_out(disks=None): |
1016 | """Convert a list of disk dictionaries into blkid content.""" |
1017 | @@ -559,6 +564,10 @@ VALID_CFG = { |
1018 | }, |
1019 | ], |
1020 | }, |
1021 | + 'Hetzner': { |
1022 | + 'ds': 'Hetzner', |
1023 | + 'files': {P_SYS_VENDOR: 'Hetzner\n'}, |
1024 | + }, |
1025 | } |
1026 | |
1027 | # vi: ts=4 expandtab |
1028 | diff --git a/tools/ds-identify b/tools/ds-identify |
1029 | index ec368d5..e3f93c9 100755 |
1030 | --- a/tools/ds-identify |
1031 | +++ b/tools/ds-identify |
1032 | @@ -114,7 +114,7 @@ DI_DSNAME="" |
1033 | # be searched if there is no setting found in config. |
1034 | DI_DSLIST_DEFAULT="MAAS ConfigDrive NoCloud AltCloud Azure Bigstep \ |
1035 | CloudSigma CloudStack DigitalOcean AliYun Ec2 GCE OpenNebula OpenStack \ |
1036 | -OVF SmartOS Scaleway" |
1037 | +OVF SmartOS Scaleway Hetzner" |
1038 | DI_DSLIST="" |
1039 | DI_MODE="" |
1040 | DI_ON_FOUND="" |
1041 | @@ -979,6 +979,11 @@ dscheck_Scaleway() { |
1042 | return ${DS_NOT_FOUND} |
1043 | } |
1044 | |
1045 | +dscheck_Hetzner() { |
1046 | + dmi_sys_vendor_is Hetzner && return ${DS_FOUND} |
1047 | + return ${DS_NOT_FOUND} |
1048 | +} |
1049 | + |
1050 | collect_info() { |
1051 | read_virt |
1052 | read_pid1_product_name |
PASSED: Continuous integration, rev:25198d2af0e ce1941edfe1bd5e 6476a5c34afe86 /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 894/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
SUCCESS: MAAS Compatability Testing
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 894/rebuild
https:/