Merge ~raharper/cloud-init:ubuntu/devel/newupstream-20181214 into cloud-init:ubuntu/devel
- Git
- lp:~raharper/cloud-init
- ubuntu/devel/newupstream-20181214
- Merge into ubuntu/devel
Proposed by
Ryan Harper
Status: | Merged | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Merged at revision: | 47a4d44a244aefb55e2a192bd069ae830c6d176e | ||||||||||||
Proposed branch: | ~raharper/cloud-init:ubuntu/devel/newupstream-20181214 | ||||||||||||
Merge into: | cloud-init:ubuntu/devel | ||||||||||||
Diff against target: |
1245 lines (+556/-145) 23 files modified
ChangeLog (+54/-0) bash_completion/cloud-init (+4/-1) cloudinit/cmd/devel/net_convert.py (+10/-5) cloudinit/cmd/main.py (+4/-16) cloudinit/config/cc_write_files.py (+6/-1) cloudinit/dhclient_hook.py (+72/-38) cloudinit/net/eni.py (+15/-14) cloudinit/net/netplan.py (+3/-3) cloudinit/net/sysconfig.py (+21/-4) cloudinit/sources/DataSourceAzure.py (+2/-2) cloudinit/sources/DataSourceNoCloud.py (+31/-1) cloudinit/sources/helpers/vmware/imc/config_nic.py (+2/-3) cloudinit/tests/test_dhclient_hook.py (+105/-0) cloudinit/version.py (+1/-1) config/cloud.cfg.tmpl (+11/-1) debian/changelog (+16/-0) tests/cloud_tests/releases.yaml (+16/-0) tests/unittests/test_cli.py (+8/-8) tests/unittests/test_datasource/test_nocloud.py (+66/-34) tests/unittests/test_handler/test_handler_write_files.py (+12/-0) tests/unittests/test_net.py (+44/-6) tests/unittests/test_vmware_config_file.py (+52/-6) tox.ini (+1/-1) |
||||||||||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Server Team CI bot | continuous-integration | Approve | |
cloud-init Commiters | Pending | ||
Review via email: mp+360957@code.launchpad.net |
Commit message
cloud-init (18.5-1-
* New upstream snapshot.
- Update to pylint 2.2.2.
- Release 18.5 (LP: #1808380)
- tests: add Disco release [Joshua Powers]
- net: render 'metric' values in per-subnet routes (LP: #1805871)
- write_files: add support for appending to files. [James Baxter]
- config: On ubuntu select cloud archive mirrors for armel, armhf, arm64.
(LP: #1805854)
- dhclient-hook: cleanups, tests and fix a bug on 'down' event.
- NoCloud: Allow top level 'network' key in network-config. (LP: #1798117)
- ovf: Fix ovf network config generation gateway/routes (LP: #1806103)
-- Ryan Harper <email address hidden> Fri, 14 Dec 2018 14:45:46 -0600
Description of the change
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)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/ChangeLog b/ChangeLog | |||
2 | index 9c043b0..8fa6fdd 100644 | |||
3 | --- a/ChangeLog | |||
4 | +++ b/ChangeLog | |||
5 | @@ -1,3 +1,57 @@ | |||
6 | 1 | 18.5: | ||
7 | 2 | - tests: add Disco release [Joshua Powers] | ||
8 | 3 | - net: render 'metric' values in per-subnet routes (LP: #1805871) | ||
9 | 4 | - write_files: add support for appending to files. [James Baxter] | ||
10 | 5 | - config: On ubuntu select cloud archive mirrors for armel, armhf, arm64. | ||
11 | 6 | (LP: #1805854) | ||
12 | 7 | - dhclient-hook: cleanups, tests and fix a bug on 'down' event. | ||
13 | 8 | - NoCloud: Allow top level 'network' key in network-config. (LP: #1798117) | ||
14 | 9 | - ovf: Fix ovf network config generation gateway/routes (LP: #1806103) | ||
15 | 10 | - azure: detect vnet migration via netlink media change event | ||
16 | 11 | [Tamilmani Manoharan] | ||
17 | 12 | - Azure: fix copy/paste error in error handling when reading azure ovf. | ||
18 | 13 | [Adam DePue] | ||
19 | 14 | - tests: fix incorrect order of mocks in test_handle_zfs_root. | ||
20 | 15 | - doc: Change dns_nameserver property to dns_nameservers. [Tomer Cohen] | ||
21 | 16 | - OVF: identify label iso9660 filesystems with label 'OVF ENV'. | ||
22 | 17 | - logs: collect-logs ignore instance-data-sensitive.json on non-root user | ||
23 | 18 | (LP: #1805201) | ||
24 | 19 | - net: Ephemeral*Network: add connectivity check via URL | ||
25 | 20 | - azure: _poll_imds only retry on 404. Fail on Timeout (LP: #1803598) | ||
26 | 21 | - resizefs: Prefix discovered devpath with '/dev/' when path does not | ||
27 | 22 | exist [Igor Galić] | ||
28 | 23 | - azure: retry imds polling on requests.Timeout (LP: #1800223) | ||
29 | 24 | - azure: Accept variation in error msg from mount for ntfs volumes | ||
30 | 25 | [Jason Zions] (LP: #1799338) | ||
31 | 26 | - azure: fix regression introduced when persisting ephemeral dhcp lease | ||
32 | 27 | [asakkurr] | ||
33 | 28 | - azure: add udev rules to create cloud-init Gen2 disk name symlinks | ||
34 | 29 | (LP: #1797480) | ||
35 | 30 | - tests: ec2 mock missing httpretty user-data and instance-identity routes | ||
36 | 31 | - azure: remove /etc/netplan/90-hotplug-azure.yaml when net from IMDS | ||
37 | 32 | - azure: report ready to fabric after reprovision and reduce logging | ||
38 | 33 | [asakkurr] (LP: #1799594) | ||
39 | 34 | - query: better error when missing read permission on instance-data | ||
40 | 35 | - instance-data: fallback to instance-data.json if sensitive is absent. | ||
41 | 36 | (LP: #1798189) | ||
42 | 37 | - docs: remove colon from network v1 config example. [Tomer Cohen] | ||
43 | 38 | - Add cloud-id binary to packages for SUSE [Jason Zions] | ||
44 | 39 | - systemd: On SUSE ensure cloud-init.service runs before wicked | ||
45 | 40 | [Robert Schweikert] (LP: #1799709) | ||
46 | 41 | - update detection of openSUSE variants [Robert Schweikert] | ||
47 | 42 | - azure: Add apply_network_config option to disable network from IMDS | ||
48 | 43 | (LP: #1798424) | ||
49 | 44 | - Correct spelling in an error message (udevadm). [Katie McLaughlin] | ||
50 | 45 | - tests: meta_data key changed to meta-data in ec2 instance-data.json | ||
51 | 46 | (LP: #1797231) | ||
52 | 47 | - tests: fix kvm integration test to assert flexible config-disk path | ||
53 | 48 | (LP: #1797199) | ||
54 | 49 | - tools: Add cloud-id command line utility | ||
55 | 50 | - instance-data: Add standard keys platform and subplatform. Refactor ec2. | ||
56 | 51 | - net: ignore nics that have "zero" mac address. (LP: #1796917) | ||
57 | 52 | - tests: fix apt_configure_primary to be more flexible | ||
58 | 53 | - Ubuntu: update sources.list to comment out deb-src entries. (LP: #74747) | ||
59 | 54 | |||
60 | 1 | 18.4: | 55 | 18.4: |
61 | 2 | - add rtd example docs about new standardized keys | 56 | - add rtd example docs about new standardized keys |
62 | 3 | - use ds._crawled_metadata instance attribute if set when writing | 57 | - use ds._crawled_metadata instance attribute if set when writing |
63 | diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init | |||
64 | index 8c25032..a9577e9 100644 | |||
65 | --- a/bash_completion/cloud-init | |||
66 | +++ b/bash_completion/cloud-init | |||
67 | @@ -30,7 +30,10 @@ _cloudinit_complete() | |||
68 | 30 | devel) | 30 | devel) |
69 | 31 | COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word)) | 31 | COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word)) |
70 | 32 | ;; | 32 | ;; |
72 | 33 | dhclient-hook|features) | 33 | dhclient-hook) |
73 | 34 | COMPREPLY=($(compgen -W "--help up down" -- $cur_word)) | ||
74 | 35 | ;; | ||
75 | 36 | features) | ||
76 | 34 | COMPREPLY=($(compgen -W "--help" -- $cur_word)) | 37 | COMPREPLY=($(compgen -W "--help" -- $cur_word)) |
77 | 35 | ;; | 38 | ;; |
78 | 36 | init) | 39 | init) |
79 | diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py | |||
80 | index a0f58a0..1ad7e0b 100755 | |||
81 | --- a/cloudinit/cmd/devel/net_convert.py | |||
82 | +++ b/cloudinit/cmd/devel/net_convert.py | |||
83 | @@ -9,6 +9,7 @@ import yaml | |||
84 | 9 | 9 | ||
85 | 10 | from cloudinit.sources.helpers import openstack | 10 | from cloudinit.sources.helpers import openstack |
86 | 11 | from cloudinit.sources import DataSourceAzure as azure | 11 | from cloudinit.sources import DataSourceAzure as azure |
87 | 12 | from cloudinit.sources import DataSourceOVF as ovf | ||
88 | 12 | 13 | ||
89 | 13 | from cloudinit import distros | 14 | from cloudinit import distros |
90 | 14 | from cloudinit.net import eni, netplan, network_state, sysconfig | 15 | from cloudinit.net import eni, netplan, network_state, sysconfig |
91 | @@ -31,7 +32,7 @@ def get_parser(parser=None): | |||
92 | 31 | metavar="PATH", required=True) | 32 | metavar="PATH", required=True) |
93 | 32 | parser.add_argument("-k", "--kind", | 33 | parser.add_argument("-k", "--kind", |
94 | 33 | choices=['eni', 'network_data.json', 'yaml', | 34 | choices=['eni', 'network_data.json', 'yaml', |
96 | 34 | 'azure-imds'], | 35 | 'azure-imds', 'vmware-imc'], |
97 | 35 | required=True) | 36 | required=True) |
98 | 36 | parser.add_argument("-d", "--directory", | 37 | parser.add_argument("-d", "--directory", |
99 | 37 | metavar="PATH", | 38 | metavar="PATH", |
100 | @@ -76,7 +77,6 @@ def handle_args(name, args): | |||
101 | 76 | net_data = args.network_data.read() | 77 | net_data = args.network_data.read() |
102 | 77 | if args.kind == "eni": | 78 | if args.kind == "eni": |
103 | 78 | pre_ns = eni.convert_eni_data(net_data) | 79 | pre_ns = eni.convert_eni_data(net_data) |
104 | 79 | ns = network_state.parse_net_config_data(pre_ns) | ||
105 | 80 | elif args.kind == "yaml": | 80 | elif args.kind == "yaml": |
106 | 81 | pre_ns = yaml.load(net_data) | 81 | pre_ns = yaml.load(net_data) |
107 | 82 | if 'network' in pre_ns: | 82 | if 'network' in pre_ns: |
108 | @@ -85,15 +85,16 @@ def handle_args(name, args): | |||
109 | 85 | sys.stderr.write('\n'.join( | 85 | sys.stderr.write('\n'.join( |
110 | 86 | ["Input YAML", | 86 | ["Input YAML", |
111 | 87 | yaml.dump(pre_ns, default_flow_style=False, indent=4), ""])) | 87 | yaml.dump(pre_ns, default_flow_style=False, indent=4), ""])) |
112 | 88 | ns = network_state.parse_net_config_data(pre_ns) | ||
113 | 89 | elif args.kind == 'network_data.json': | 88 | elif args.kind == 'network_data.json': |
114 | 90 | pre_ns = openstack.convert_net_json( | 89 | pre_ns = openstack.convert_net_json( |
115 | 91 | json.loads(net_data), known_macs=known_macs) | 90 | json.loads(net_data), known_macs=known_macs) |
116 | 92 | ns = network_state.parse_net_config_data(pre_ns) | ||
117 | 93 | elif args.kind == 'azure-imds': | 91 | elif args.kind == 'azure-imds': |
118 | 94 | pre_ns = azure.parse_network_config(json.loads(net_data)) | 92 | pre_ns = azure.parse_network_config(json.loads(net_data)) |
120 | 95 | ns = network_state.parse_net_config_data(pre_ns) | 93 | elif args.kind == 'vmware-imc': |
121 | 94 | config = ovf.Config(ovf.ConfigFile(args.network_data.name)) | ||
122 | 95 | pre_ns = ovf.get_network_config_from_conf(config, False) | ||
123 | 96 | 96 | ||
124 | 97 | ns = network_state.parse_net_config_data(pre_ns) | ||
125 | 97 | if not ns: | 98 | if not ns: |
126 | 98 | raise RuntimeError("No valid network_state object created from" | 99 | raise RuntimeError("No valid network_state object created from" |
127 | 99 | "input data") | 100 | "input data") |
128 | @@ -111,6 +112,10 @@ def handle_args(name, args): | |||
129 | 111 | elif args.output_kind == "netplan": | 112 | elif args.output_kind == "netplan": |
130 | 112 | r_cls = netplan.Renderer | 113 | r_cls = netplan.Renderer |
131 | 113 | config = distro.renderer_configs.get('netplan') | 114 | config = distro.renderer_configs.get('netplan') |
132 | 115 | # don't run netplan generate/apply | ||
133 | 116 | config['postcmds'] = False | ||
134 | 117 | # trim leading slash | ||
135 | 118 | config['netplan_path'] = config['netplan_path'][1:] | ||
136 | 114 | else: | 119 | else: |
137 | 115 | r_cls = sysconfig.Renderer | 120 | r_cls = sysconfig.Renderer |
138 | 116 | config = distro.renderer_configs.get('sysconfig') | 121 | config = distro.renderer_configs.get('sysconfig') |
139 | diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py | |||
140 | index 5a43702..933c019 100644 | |||
141 | --- a/cloudinit/cmd/main.py | |||
142 | +++ b/cloudinit/cmd/main.py | |||
143 | @@ -41,7 +41,7 @@ from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE, | |||
144 | 41 | from cloudinit import atomic_helper | 41 | from cloudinit import atomic_helper |
145 | 42 | 42 | ||
146 | 43 | from cloudinit.config import cc_set_hostname | 43 | from cloudinit.config import cc_set_hostname |
148 | 44 | from cloudinit.dhclient_hook import LogDhclient | 44 | from cloudinit import dhclient_hook |
149 | 45 | 45 | ||
150 | 46 | 46 | ||
151 | 47 | # Welcome message template | 47 | # Welcome message template |
152 | @@ -586,12 +586,6 @@ def main_single(name, args): | |||
153 | 586 | return 0 | 586 | return 0 |
154 | 587 | 587 | ||
155 | 588 | 588 | ||
156 | 589 | def dhclient_hook(name, args): | ||
157 | 590 | record = LogDhclient(args) | ||
158 | 591 | record.check_hooks_dir() | ||
159 | 592 | record.record() | ||
160 | 593 | |||
161 | 594 | |||
162 | 595 | def status_wrapper(name, args, data_d=None, link_d=None): | 589 | def status_wrapper(name, args, data_d=None, link_d=None): |
163 | 596 | if data_d is None: | 590 | if data_d is None: |
164 | 597 | data_d = os.path.normpath("/var/lib/cloud/data") | 591 | data_d = os.path.normpath("/var/lib/cloud/data") |
165 | @@ -795,15 +789,9 @@ def main(sysv_args=None): | |||
166 | 795 | 'query', | 789 | 'query', |
167 | 796 | help='Query standardized instance metadata from the command line.') | 790 | help='Query standardized instance metadata from the command line.') |
168 | 797 | 791 | ||
178 | 798 | parser_dhclient = subparsers.add_parser('dhclient-hook', | 792 | parser_dhclient = subparsers.add_parser( |
179 | 799 | help=('run the dhclient hook' | 793 | dhclient_hook.NAME, help=dhclient_hook.__doc__) |
180 | 800 | 'to record network info')) | 794 | dhclient_hook.get_parser(parser_dhclient) |
172 | 801 | parser_dhclient.add_argument("net_action", | ||
173 | 802 | help=('action taken on the interface')) | ||
174 | 803 | parser_dhclient.add_argument("net_interface", | ||
175 | 804 | help=('the network interface being acted' | ||
176 | 805 | ' upon')) | ||
177 | 806 | parser_dhclient.set_defaults(action=('dhclient_hook', dhclient_hook)) | ||
181 | 807 | 795 | ||
182 | 808 | parser_features = subparsers.add_parser('features', | 796 | parser_features = subparsers.add_parser('features', |
183 | 809 | help=('list defined features')) | 797 | help=('list defined features')) |
184 | diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py | |||
185 | index 31d1db6..0b6546e 100644 | |||
186 | --- a/cloudinit/config/cc_write_files.py | |||
187 | +++ b/cloudinit/config/cc_write_files.py | |||
188 | @@ -49,6 +49,10 @@ binary gzip data can be specified and will be decoded before being written. | |||
189 | 49 | ... | 49 | ... |
190 | 50 | path: /bin/arch | 50 | path: /bin/arch |
191 | 51 | permissions: '0555' | 51 | permissions: '0555' |
192 | 52 | - content: | | ||
193 | 53 | 15 * * * * root ship_logs | ||
194 | 54 | path: /etc/crontab | ||
195 | 55 | append: true | ||
196 | 52 | """ | 56 | """ |
197 | 53 | 57 | ||
198 | 54 | import base64 | 58 | import base64 |
199 | @@ -113,7 +117,8 @@ def write_files(name, files): | |||
200 | 113 | contents = extract_contents(f_info.get('content', ''), extractions) | 117 | contents = extract_contents(f_info.get('content', ''), extractions) |
201 | 114 | (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER)) | 118 | (u, g) = util.extract_usergroup(f_info.get('owner', DEFAULT_OWNER)) |
202 | 115 | perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS) | 119 | perms = decode_perms(f_info.get('permissions'), DEFAULT_PERMS) |
204 | 116 | util.write_file(path, contents, mode=perms) | 120 | omode = 'ab' if util.get_cfg_option_bool(f_info, 'append') else 'wb' |
205 | 121 | util.write_file(path, contents, omode=omode, mode=perms) | ||
206 | 117 | util.chownbyname(path, u, g) | 122 | util.chownbyname(path, u, g) |
207 | 118 | 123 | ||
208 | 119 | 124 | ||
209 | diff --git a/cloudinit/dhclient_hook.py b/cloudinit/dhclient_hook.py | |||
210 | index 7f02d7f..72b51b6 100644 | |||
211 | --- a/cloudinit/dhclient_hook.py | |||
212 | +++ b/cloudinit/dhclient_hook.py | |||
213 | @@ -1,5 +1,8 @@ | |||
214 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | 1 | # This file is part of cloud-init. See LICENSE file for license information. |
215 | 2 | 2 | ||
216 | 3 | """Run the dhclient hook to record network info.""" | ||
217 | 4 | |||
218 | 5 | import argparse | ||
219 | 3 | import os | 6 | import os |
220 | 4 | 7 | ||
221 | 5 | from cloudinit import atomic_helper | 8 | from cloudinit import atomic_helper |
222 | @@ -8,44 +11,75 @@ from cloudinit import stages | |||
223 | 8 | 11 | ||
224 | 9 | LOG = logging.getLogger(__name__) | 12 | LOG = logging.getLogger(__name__) |
225 | 10 | 13 | ||
226 | 14 | NAME = "dhclient-hook" | ||
227 | 15 | UP = "up" | ||
228 | 16 | DOWN = "down" | ||
229 | 17 | EVENTS = (UP, DOWN) | ||
230 | 18 | |||
231 | 19 | |||
232 | 20 | def _get_hooks_dir(): | ||
233 | 21 | i = stages.Init() | ||
234 | 22 | return os.path.join(i.paths.get_runpath(), 'dhclient.hooks') | ||
235 | 23 | |||
236 | 24 | |||
237 | 25 | def _filter_env_vals(info): | ||
238 | 26 | """Given info (os.environ), return a dictionary with | ||
239 | 27 | lower case keys for each entry starting with DHCP4_ or new_.""" | ||
240 | 28 | new_info = {} | ||
241 | 29 | for k, v in info.items(): | ||
242 | 30 | if k.startswith("DHCP4_") or k.startswith("new_"): | ||
243 | 31 | key = (k.replace('DHCP4_', '').replace('new_', '')).lower() | ||
244 | 32 | new_info[key] = v | ||
245 | 33 | return new_info | ||
246 | 34 | |||
247 | 35 | |||
248 | 36 | def run_hook(interface, event, data_d=None, env=None): | ||
249 | 37 | if event not in EVENTS: | ||
250 | 38 | raise ValueError("Unexpected event '%s'. Expected one of: %s" % | ||
251 | 39 | (event, EVENTS)) | ||
252 | 40 | if data_d is None: | ||
253 | 41 | data_d = _get_hooks_dir() | ||
254 | 42 | if env is None: | ||
255 | 43 | env = os.environ | ||
256 | 44 | hook_file = os.path.join(data_d, interface + ".json") | ||
257 | 45 | |||
258 | 46 | if event == UP: | ||
259 | 47 | if not os.path.exists(data_d): | ||
260 | 48 | os.makedirs(data_d) | ||
261 | 49 | atomic_helper.write_json(hook_file, _filter_env_vals(env)) | ||
262 | 50 | LOG.debug("Wrote dhclient options in %s", hook_file) | ||
263 | 51 | elif event == DOWN: | ||
264 | 52 | if os.path.exists(hook_file): | ||
265 | 53 | os.remove(hook_file) | ||
266 | 54 | LOG.debug("Removed dhclient options file %s", hook_file) | ||
267 | 55 | |||
268 | 56 | |||
269 | 57 | def get_parser(parser=None): | ||
270 | 58 | if parser is None: | ||
271 | 59 | parser = argparse.ArgumentParser(prog=NAME, description=__doc__) | ||
272 | 60 | parser.add_argument( | ||
273 | 61 | "event", help='event taken on the interface', choices=EVENTS) | ||
274 | 62 | parser.add_argument( | ||
275 | 63 | "interface", help='the network interface being acted upon') | ||
276 | 64 | # cloud-init main uses 'action' | ||
277 | 65 | parser.set_defaults(action=(NAME, handle_args)) | ||
278 | 66 | return parser | ||
279 | 67 | |||
280 | 68 | |||
281 | 69 | def handle_args(name, args, data_d=None): | ||
282 | 70 | """Handle the Namespace args. | ||
283 | 71 | Takes 'name' as passed by cloud-init main. not used here.""" | ||
284 | 72 | return run_hook(interface=args.interface, event=args.event, data_d=data_d) | ||
285 | 73 | |||
286 | 74 | |||
287 | 75 | if __name__ == '__main__': | ||
288 | 76 | import sys | ||
289 | 77 | parser = get_parser() | ||
290 | 78 | args = parser.parse_args(args=sys.argv[1:]) | ||
291 | 79 | return_value = handle_args( | ||
292 | 80 | NAME, args, data_d=os.environ.get('_CI_DHCP_HOOK_DATA_D')) | ||
293 | 81 | if return_value: | ||
294 | 82 | sys.exit(return_value) | ||
295 | 11 | 83 | ||
296 | 12 | class LogDhclient(object): | ||
297 | 13 | |||
298 | 14 | def __init__(self, cli_args): | ||
299 | 15 | self.hooks_dir = self._get_hooks_dir() | ||
300 | 16 | self.net_interface = cli_args.net_interface | ||
301 | 17 | self.net_action = cli_args.net_action | ||
302 | 18 | self.hook_file = os.path.join(self.hooks_dir, | ||
303 | 19 | self.net_interface + ".json") | ||
304 | 20 | |||
305 | 21 | @staticmethod | ||
306 | 22 | def _get_hooks_dir(): | ||
307 | 23 | i = stages.Init() | ||
308 | 24 | return os.path.join(i.paths.get_runpath(), 'dhclient.hooks') | ||
309 | 25 | |||
310 | 26 | def check_hooks_dir(self): | ||
311 | 27 | if not os.path.exists(self.hooks_dir): | ||
312 | 28 | os.makedirs(self.hooks_dir) | ||
313 | 29 | else: | ||
314 | 30 | # If the action is down and the json file exists, we need to | ||
315 | 31 | # delete the file | ||
316 | 32 | if self.net_action is 'down' and os.path.exists(self.hook_file): | ||
317 | 33 | os.remove(self.hook_file) | ||
318 | 34 | |||
319 | 35 | @staticmethod | ||
320 | 36 | def get_vals(info): | ||
321 | 37 | new_info = {} | ||
322 | 38 | for k, v in info.items(): | ||
323 | 39 | if k.startswith("DHCP4_") or k.startswith("new_"): | ||
324 | 40 | key = (k.replace('DHCP4_', '').replace('new_', '')).lower() | ||
325 | 41 | new_info[key] = v | ||
326 | 42 | return new_info | ||
327 | 43 | |||
328 | 44 | def record(self): | ||
329 | 45 | envs = os.environ | ||
330 | 46 | if self.hook_file is None: | ||
331 | 47 | return | ||
332 | 48 | atomic_helper.write_json(self.hook_file, self.get_vals(envs)) | ||
333 | 49 | LOG.debug("Wrote dhclient options in %s", self.hook_file) | ||
334 | 50 | 84 | ||
335 | 51 | # vi: ts=4 expandtab | 85 | # vi: ts=4 expandtab |
336 | diff --git a/cloudinit/net/eni.py b/cloudinit/net/eni.py | |||
337 | index c6f631a..6423632 100644 | |||
338 | --- a/cloudinit/net/eni.py | |||
339 | +++ b/cloudinit/net/eni.py | |||
340 | @@ -371,22 +371,23 @@ class Renderer(renderer.Renderer): | |||
341 | 371 | 'gateway': 'gw', | 371 | 'gateway': 'gw', |
342 | 372 | 'metric': 'metric', | 372 | 'metric': 'metric', |
343 | 373 | } | 373 | } |
344 | 374 | |||
345 | 375 | default_gw = '' | ||
346 | 374 | if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': | 376 | if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0': |
350 | 375 | default_gw = " default gw %s" % route['gateway'] | 377 | default_gw = ' default' |
348 | 376 | content.append(up + default_gw + or_true) | ||
349 | 377 | content.append(down + default_gw + or_true) | ||
351 | 378 | elif route['network'] == '::' and route['prefix'] == 0: | 378 | elif route['network'] == '::' and route['prefix'] == 0: |
363 | 379 | # ipv6! | 379 | default_gw = ' -A inet6 default' |
364 | 380 | default_gw = " -A inet6 default gw %s" % route['gateway'] | 380 | |
365 | 381 | content.append(up + default_gw + or_true) | 381 | route_line = '' |
366 | 382 | content.append(down + default_gw + or_true) | 382 | for k in ['network', 'netmask', 'gateway', 'metric']: |
367 | 383 | else: | 383 | if default_gw and k in ['network', 'netmask']: |
368 | 384 | route_line = "" | 384 | continue |
369 | 385 | for k in ['network', 'netmask', 'gateway', 'metric']: | 385 | if k == 'gateway': |
370 | 386 | if k in route: | 386 | route_line += '%s %s %s' % (default_gw, mapping[k], route[k]) |
371 | 387 | route_line += " %s %s" % (mapping[k], route[k]) | 387 | elif k in route: |
372 | 388 | content.append(up + route_line + or_true) | 388 | route_line += ' %s %s' % (mapping[k], route[k]) |
373 | 389 | content.append(down + route_line + or_true) | 389 | content.append(up + route_line + or_true) |
374 | 390 | content.append(down + route_line + or_true) | ||
375 | 390 | return content | 391 | return content |
376 | 391 | 392 | ||
377 | 392 | def _render_iface(self, iface, render_hwaddress=False): | 393 | def _render_iface(self, iface, render_hwaddress=False): |
378 | diff --git a/cloudinit/net/netplan.py b/cloudinit/net/netplan.py | |||
379 | index bc1087f..21517fd 100644 | |||
380 | --- a/cloudinit/net/netplan.py | |||
381 | +++ b/cloudinit/net/netplan.py | |||
382 | @@ -114,13 +114,13 @@ def _extract_addresses(config, entry, ifname): | |||
383 | 114 | for route in subnet.get('routes', []): | 114 | for route in subnet.get('routes', []): |
384 | 115 | to_net = "%s/%s" % (route.get('network'), | 115 | to_net = "%s/%s" % (route.get('network'), |
385 | 116 | route.get('prefix')) | 116 | route.get('prefix')) |
387 | 117 | route = { | 117 | new_route = { |
388 | 118 | 'via': route.get('gateway'), | 118 | 'via': route.get('gateway'), |
389 | 119 | 'to': to_net, | 119 | 'to': to_net, |
390 | 120 | } | 120 | } |
391 | 121 | if 'metric' in route: | 121 | if 'metric' in route: |
394 | 122 | route.update({'metric': route.get('metric', 100)}) | 122 | new_route.update({'metric': route.get('metric', 100)}) |
395 | 123 | routes.append(route) | 123 | routes.append(new_route) |
396 | 124 | 124 | ||
397 | 125 | addresses.append(addr) | 125 | addresses.append(addr) |
398 | 126 | 126 | ||
399 | diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py | |||
400 | index 9c16d3a..17293e1 100644 | |||
401 | --- a/cloudinit/net/sysconfig.py | |||
402 | +++ b/cloudinit/net/sysconfig.py | |||
403 | @@ -156,13 +156,23 @@ class Route(ConfigMap): | |||
404 | 156 | _quote_value(gateway_value))) | 156 | _quote_value(gateway_value))) |
405 | 157 | buf.write("%s=%s\n" % ('NETMASK' + str(reindex), | 157 | buf.write("%s=%s\n" % ('NETMASK' + str(reindex), |
406 | 158 | _quote_value(netmask_value))) | 158 | _quote_value(netmask_value))) |
407 | 159 | metric_key = 'METRIC' + index | ||
408 | 160 | if metric_key in self._conf: | ||
409 | 161 | metric_value = str(self._conf['METRIC' + index]) | ||
410 | 162 | buf.write("%s=%s\n" % ('METRIC' + str(reindex), | ||
411 | 163 | _quote_value(metric_value))) | ||
412 | 159 | elif proto == "ipv6" and self.is_ipv6_route(address_value): | 164 | elif proto == "ipv6" and self.is_ipv6_route(address_value): |
413 | 160 | netmask_value = str(self._conf['NETMASK' + index]) | 165 | netmask_value = str(self._conf['NETMASK' + index]) |
414 | 161 | gateway_value = str(self._conf['GATEWAY' + index]) | 166 | gateway_value = str(self._conf['GATEWAY' + index]) |
419 | 162 | buf.write("%s/%s via %s dev %s\n" % (address_value, | 167 | metric_value = ( |
420 | 163 | netmask_value, | 168 | 'metric ' + str(self._conf['METRIC' + index]) |
421 | 164 | gateway_value, | 169 | if 'METRIC' + index in self._conf else '') |
422 | 165 | self._route_name)) | 170 | buf.write( |
423 | 171 | "%s/%s via %s %s dev %s\n" % (address_value, | ||
424 | 172 | netmask_value, | ||
425 | 173 | gateway_value, | ||
426 | 174 | metric_value, | ||
427 | 175 | self._route_name)) | ||
428 | 166 | 176 | ||
429 | 167 | return buf.getvalue() | 177 | return buf.getvalue() |
430 | 168 | 178 | ||
431 | @@ -370,6 +380,9 @@ class Renderer(renderer.Renderer): | |||
432 | 370 | else: | 380 | else: |
433 | 371 | iface_cfg['GATEWAY'] = subnet['gateway'] | 381 | iface_cfg['GATEWAY'] = subnet['gateway'] |
434 | 372 | 382 | ||
435 | 383 | if 'metric' in subnet: | ||
436 | 384 | iface_cfg['METRIC'] = subnet['metric'] | ||
437 | 385 | |||
438 | 373 | if 'dns_search' in subnet: | 386 | if 'dns_search' in subnet: |
439 | 374 | iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search']) | 387 | iface_cfg['DOMAIN'] = ' '.join(subnet['dns_search']) |
440 | 375 | 388 | ||
441 | @@ -414,15 +427,19 @@ class Renderer(renderer.Renderer): | |||
442 | 414 | else: | 427 | else: |
443 | 415 | iface_cfg['GATEWAY'] = route['gateway'] | 428 | iface_cfg['GATEWAY'] = route['gateway'] |
444 | 416 | route_cfg.has_set_default_ipv4 = True | 429 | route_cfg.has_set_default_ipv4 = True |
445 | 430 | if 'metric' in route: | ||
446 | 431 | iface_cfg['METRIC'] = route['metric'] | ||
447 | 417 | 432 | ||
448 | 418 | else: | 433 | else: |
449 | 419 | gw_key = 'GATEWAY%s' % route_cfg.last_idx | 434 | gw_key = 'GATEWAY%s' % route_cfg.last_idx |
450 | 420 | nm_key = 'NETMASK%s' % route_cfg.last_idx | 435 | nm_key = 'NETMASK%s' % route_cfg.last_idx |
451 | 421 | addr_key = 'ADDRESS%s' % route_cfg.last_idx | 436 | addr_key = 'ADDRESS%s' % route_cfg.last_idx |
452 | 437 | metric_key = 'METRIC%s' % route_cfg.last_idx | ||
453 | 422 | route_cfg.last_idx += 1 | 438 | route_cfg.last_idx += 1 |
454 | 423 | # add default routes only to ifcfg files, not | 439 | # add default routes only to ifcfg files, not |
455 | 424 | # to route-* or route6-* | 440 | # to route-* or route6-* |
456 | 425 | for (old_key, new_key) in [('gateway', gw_key), | 441 | for (old_key, new_key) in [('gateway', gw_key), |
457 | 442 | ('metric', metric_key), | ||
458 | 426 | ('netmask', nm_key), | 443 | ('netmask', nm_key), |
459 | 427 | ('network', addr_key)]: | 444 | ('network', addr_key)]: |
460 | 428 | if old_key in route: | 445 | if old_key in route: |
461 | diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py | |||
462 | index e076d5d..46efca4 100644 | |||
463 | --- a/cloudinit/sources/DataSourceAzure.py | |||
464 | +++ b/cloudinit/sources/DataSourceAzure.py | |||
465 | @@ -980,8 +980,8 @@ def read_azure_ovf(contents): | |||
466 | 980 | raise NonAzureDataSource("No LinuxProvisioningConfigurationSet") | 980 | raise NonAzureDataSource("No LinuxProvisioningConfigurationSet") |
467 | 981 | if len(lpcs_nodes) > 1: | 981 | if len(lpcs_nodes) > 1: |
468 | 982 | raise BrokenAzureDataSource("found '%d' %ss" % | 982 | raise BrokenAzureDataSource("found '%d' %ss" % |
471 | 983 | ("LinuxProvisioningConfigurationSet", | 983 | (len(lpcs_nodes), |
472 | 984 | len(lpcs_nodes))) | 984 | "LinuxProvisioningConfigurationSet")) |
473 | 985 | lpcs = lpcs_nodes[0] | 985 | lpcs = lpcs_nodes[0] |
474 | 986 | 986 | ||
475 | 987 | if not lpcs.hasChildNodes(): | 987 | if not lpcs.hasChildNodes(): |
476 | diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py | |||
477 | index 9010f06..6860f0c 100644 | |||
478 | --- a/cloudinit/sources/DataSourceNoCloud.py | |||
479 | +++ b/cloudinit/sources/DataSourceNoCloud.py | |||
480 | @@ -311,6 +311,35 @@ def parse_cmdline_data(ds_id, fill, cmdline=None): | |||
481 | 311 | return True | 311 | return True |
482 | 312 | 312 | ||
483 | 313 | 313 | ||
484 | 314 | def _maybe_remove_top_network(cfg): | ||
485 | 315 | """If network-config contains top level 'network' key, then remove it. | ||
486 | 316 | |||
487 | 317 | Some providers of network configuration may provide a top level | ||
488 | 318 | 'network' key (LP: #1798117) even though it is not necessary. | ||
489 | 319 | |||
490 | 320 | Be friendly and remove it if it really seems so. | ||
491 | 321 | |||
492 | 322 | Return the original value if no change or the updated value if changed.""" | ||
493 | 323 | nullval = object() | ||
494 | 324 | network_val = cfg.get('network', nullval) | ||
495 | 325 | if network_val is nullval: | ||
496 | 326 | return cfg | ||
497 | 327 | bmsg = 'Top level network key in network-config %s: %s' | ||
498 | 328 | if not isinstance(network_val, dict): | ||
499 | 329 | LOG.debug(bmsg, "was not a dict", cfg) | ||
500 | 330 | return cfg | ||
501 | 331 | if len(list(cfg.keys())) != 1: | ||
502 | 332 | LOG.debug(bmsg, "had multiple top level keys", cfg) | ||
503 | 333 | return cfg | ||
504 | 334 | if network_val.get('config') == "disabled": | ||
505 | 335 | LOG.debug(bmsg, "was config/disabled", cfg) | ||
506 | 336 | elif not all(('config' in network_val, 'version' in network_val)): | ||
507 | 337 | LOG.debug(bmsg, "but missing 'config' or 'version'", cfg) | ||
508 | 338 | return cfg | ||
509 | 339 | LOG.debug(bmsg, "fixed by removing shifting network.", cfg) | ||
510 | 340 | return network_val | ||
511 | 341 | |||
512 | 342 | |||
513 | 314 | def _merge_new_seed(cur, seeded): | 343 | def _merge_new_seed(cur, seeded): |
514 | 315 | ret = cur.copy() | 344 | ret = cur.copy() |
515 | 316 | 345 | ||
516 | @@ -320,7 +349,8 @@ def _merge_new_seed(cur, seeded): | |||
517 | 320 | ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd]) | 349 | ret['meta-data'] = util.mergemanydict([cur['meta-data'], newmd]) |
518 | 321 | 350 | ||
519 | 322 | if seeded.get('network-config'): | 351 | if seeded.get('network-config'): |
521 | 323 | ret['network-config'] = util.load_yaml(seeded['network-config']) | 352 | ret['network-config'] = _maybe_remove_top_network( |
522 | 353 | util.load_yaml(seeded.get('network-config'))) | ||
523 | 324 | 354 | ||
524 | 325 | if 'user-data' in seeded: | 355 | if 'user-data' in seeded: |
525 | 326 | ret['user-data'] = seeded['user-data'] | 356 | ret['user-data'] = seeded['user-data'] |
526 | diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py | |||
527 | index e1890e2..77cbf3b 100644 | |||
528 | --- a/cloudinit/sources/helpers/vmware/imc/config_nic.py | |||
529 | +++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py | |||
530 | @@ -165,9 +165,8 @@ class NicConfigurator(object): | |||
531 | 165 | 165 | ||
532 | 166 | # Add routes if there is no primary nic | 166 | # Add routes if there is no primary nic |
533 | 167 | if not self._primaryNic and v4.gateways: | 167 | if not self._primaryNic and v4.gateways: |
537 | 168 | route_list.extend(self.gen_ipv4_route(nic, | 168 | subnet.update( |
538 | 169 | v4.gateways, | 169 | {'routes': self.gen_ipv4_route(nic, v4.gateways, v4.netmask)}) |
536 | 170 | v4.netmask)) | ||
539 | 171 | 170 | ||
540 | 172 | return ([subnet], route_list) | 171 | return ([subnet], route_list) |
541 | 173 | 172 | ||
542 | diff --git a/cloudinit/tests/test_dhclient_hook.py b/cloudinit/tests/test_dhclient_hook.py | |||
543 | 174 | new file mode 100644 | 173 | new file mode 100644 |
544 | index 0000000..7aab8dd | |||
545 | --- /dev/null | |||
546 | +++ b/cloudinit/tests/test_dhclient_hook.py | |||
547 | @@ -0,0 +1,105 @@ | |||
548 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | ||
549 | 2 | |||
550 | 3 | """Tests for cloudinit.dhclient_hook.""" | ||
551 | 4 | |||
552 | 5 | from cloudinit import dhclient_hook as dhc | ||
553 | 6 | from cloudinit.tests.helpers import CiTestCase, dir2dict, populate_dir | ||
554 | 7 | |||
555 | 8 | import argparse | ||
556 | 9 | import json | ||
557 | 10 | import mock | ||
558 | 11 | import os | ||
559 | 12 | |||
560 | 13 | |||
561 | 14 | class TestDhclientHook(CiTestCase): | ||
562 | 15 | |||
563 | 16 | ex_env = { | ||
564 | 17 | 'interface': 'eth0', | ||
565 | 18 | 'new_dhcp_lease_time': '3600', | ||
566 | 19 | 'new_host_name': 'x1', | ||
567 | 20 | 'new_ip_address': '10.145.210.163', | ||
568 | 21 | 'new_subnet_mask': '255.255.255.0', | ||
569 | 22 | 'old_host_name': 'x1', | ||
570 | 23 | 'PATH': '/usr/sbin:/usr/bin:/sbin:/bin', | ||
571 | 24 | 'pid': '614', | ||
572 | 25 | 'reason': 'BOUND', | ||
573 | 26 | } | ||
574 | 27 | |||
575 | 28 | # some older versions of dhclient put the same content, | ||
576 | 29 | # but in upper case with DHCP4_ instead of new_ | ||
577 | 30 | ex_env_dhcp4 = { | ||
578 | 31 | 'REASON': 'BOUND', | ||
579 | 32 | 'DHCP4_dhcp_lease_time': '3600', | ||
580 | 33 | 'DHCP4_host_name': 'x1', | ||
581 | 34 | 'DHCP4_ip_address': '10.145.210.163', | ||
582 | 35 | 'DHCP4_subnet_mask': '255.255.255.0', | ||
583 | 36 | 'INTERFACE': 'eth0', | ||
584 | 37 | 'PATH': '/usr/sbin:/usr/bin:/sbin:/bin', | ||
585 | 38 | 'pid': '614', | ||
586 | 39 | } | ||
587 | 40 | |||
588 | 41 | expected = { | ||
589 | 42 | 'dhcp_lease_time': '3600', | ||
590 | 43 | 'host_name': 'x1', | ||
591 | 44 | 'ip_address': '10.145.210.163', | ||
592 | 45 | 'subnet_mask': '255.255.255.0'} | ||
593 | 46 | |||
594 | 47 | def setUp(self): | ||
595 | 48 | super(TestDhclientHook, self).setUp() | ||
596 | 49 | self.tmp = self.tmp_dir() | ||
597 | 50 | |||
598 | 51 | def test_handle_args(self): | ||
599 | 52 | """quick test of call to handle_args.""" | ||
600 | 53 | nic = 'eth0' | ||
601 | 54 | args = argparse.Namespace(event=dhc.UP, interface=nic) | ||
602 | 55 | with mock.patch.dict("os.environ", clear=True, values=self.ex_env): | ||
603 | 56 | dhc.handle_args(dhc.NAME, args, data_d=self.tmp) | ||
604 | 57 | found = dir2dict(self.tmp + os.path.sep) | ||
605 | 58 | self.assertEqual([nic + ".json"], list(found.keys())) | ||
606 | 59 | self.assertEqual(self.expected, json.loads(found[nic + ".json"])) | ||
607 | 60 | |||
608 | 61 | def test_run_hook_up_creates_dir(self): | ||
609 | 62 | """If dir does not exist, run_hook should create it.""" | ||
610 | 63 | subd = self.tmp_path("subdir", self.tmp) | ||
611 | 64 | nic = 'eth1' | ||
612 | 65 | dhc.run_hook(nic, 'up', data_d=subd, env=self.ex_env) | ||
613 | 66 | self.assertEqual( | ||
614 | 67 | set([nic + ".json"]), set(dir2dict(subd + os.path.sep))) | ||
615 | 68 | |||
616 | 69 | def test_run_hook_up(self): | ||
617 | 70 | """Test expected use of run_hook_up.""" | ||
618 | 71 | nic = 'eth0' | ||
619 | 72 | dhc.run_hook(nic, 'up', data_d=self.tmp, env=self.ex_env) | ||
620 | 73 | found = dir2dict(self.tmp + os.path.sep) | ||
621 | 74 | self.assertEqual([nic + ".json"], list(found.keys())) | ||
622 | 75 | self.assertEqual(self.expected, json.loads(found[nic + ".json"])) | ||
623 | 76 | |||
624 | 77 | def test_run_hook_up_dhcp4_prefix(self): | ||
625 | 78 | """Test run_hook filters correctly with older DHCP4_ data.""" | ||
626 | 79 | nic = 'eth0' | ||
627 | 80 | dhc.run_hook(nic, 'up', data_d=self.tmp, env=self.ex_env_dhcp4) | ||
628 | 81 | found = dir2dict(self.tmp + os.path.sep) | ||
629 | 82 | self.assertEqual([nic + ".json"], list(found.keys())) | ||
630 | 83 | self.assertEqual(self.expected, json.loads(found[nic + ".json"])) | ||
631 | 84 | |||
632 | 85 | def test_run_hook_down_deletes(self): | ||
633 | 86 | """down should delete the created json file.""" | ||
634 | 87 | nic = 'eth1' | ||
635 | 88 | populate_dir( | ||
636 | 89 | self.tmp, {nic + ".json": "{'abcd'}", 'myfile.txt': 'text'}) | ||
637 | 90 | dhc.run_hook(nic, 'down', data_d=self.tmp, env={'old_host_name': 'x1'}) | ||
638 | 91 | self.assertEqual( | ||
639 | 92 | set(['myfile.txt']), | ||
640 | 93 | set(dir2dict(self.tmp + os.path.sep))) | ||
641 | 94 | |||
642 | 95 | def test_get_parser(self): | ||
643 | 96 | """Smoke test creation of get_parser.""" | ||
644 | 97 | # cloud-init main uses 'action'. | ||
645 | 98 | event, interface = (dhc.UP, 'mynic0') | ||
646 | 99 | self.assertEqual( | ||
647 | 100 | argparse.Namespace(event=event, interface=interface, | ||
648 | 101 | action=(dhc.NAME, dhc.handle_args)), | ||
649 | 102 | dhc.get_parser().parse_args([event, interface])) | ||
650 | 103 | |||
651 | 104 | |||
652 | 105 | # vi: ts=4 expandtab | ||
653 | diff --git a/cloudinit/version.py b/cloudinit/version.py | |||
654 | index 844a02e..a2c5d43 100644 | |||
655 | --- a/cloudinit/version.py | |||
656 | +++ b/cloudinit/version.py | |||
657 | @@ -4,7 +4,7 @@ | |||
658 | 4 | # | 4 | # |
659 | 5 | # This file is part of cloud-init. See LICENSE file for license information. | 5 | # This file is part of cloud-init. See LICENSE file for license information. |
660 | 6 | 6 | ||
662 | 7 | __VERSION__ = "18.4" | 7 | __VERSION__ = "18.5" |
663 | 8 | _PACKAGED_VERSION = '@@PACKAGED_VERSION@@' | 8 | _PACKAGED_VERSION = '@@PACKAGED_VERSION@@' |
664 | 9 | 9 | ||
665 | 10 | FEATURES = [ | 10 | FEATURES = [ |
666 | diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl | |||
667 | index 1fef133..7513176 100644 | |||
668 | --- a/config/cloud.cfg.tmpl | |||
669 | +++ b/config/cloud.cfg.tmpl | |||
670 | @@ -167,7 +167,17 @@ system_info: | |||
671 | 167 | - http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/ | 167 | - http://%(availability_zone)s.clouds.archive.ubuntu.com/ubuntu/ |
672 | 168 | - http://%(region)s.clouds.archive.ubuntu.com/ubuntu/ | 168 | - http://%(region)s.clouds.archive.ubuntu.com/ubuntu/ |
673 | 169 | security: [] | 169 | security: [] |
675 | 170 | - arches: [armhf, armel, default] | 170 | - arches: [arm64, armel, armhf] |
676 | 171 | failsafe: | ||
677 | 172 | primary: http://ports.ubuntu.com/ubuntu-ports | ||
678 | 173 | security: http://ports.ubuntu.com/ubuntu-ports | ||
679 | 174 | search: | ||
680 | 175 | primary: | ||
681 | 176 | - http://%(ec2_region)s.ec2.ports.ubuntu.com/ubuntu-ports/ | ||
682 | 177 | - http://%(availability_zone)s.clouds.ports.ubuntu.com/ubuntu-ports/ | ||
683 | 178 | - http://%(region)s.clouds.ports.ubuntu.com/ubuntu-ports/ | ||
684 | 179 | security: [] | ||
685 | 180 | - arches: [default] | ||
686 | 171 | failsafe: | 181 | failsafe: |
687 | 172 | primary: http://ports.ubuntu.com/ubuntu-ports | 182 | primary: http://ports.ubuntu.com/ubuntu-ports |
688 | 173 | security: http://ports.ubuntu.com/ubuntu-ports | 183 | security: http://ports.ubuntu.com/ubuntu-ports |
689 | diff --git a/debian/changelog b/debian/changelog | |||
690 | index 283bcd8..09a0034 100644 | |||
691 | --- a/debian/changelog | |||
692 | +++ b/debian/changelog | |||
693 | @@ -1,3 +1,19 @@ | |||
694 | 1 | cloud-init (18.5-1-g5b065316-0ubuntu1) disco; urgency=medium | ||
695 | 2 | |||
696 | 3 | * New upstream snapshot. | ||
697 | 4 | - Update to pylint 2.2.2. | ||
698 | 5 | - Release 18.5 (LP: #1808380) | ||
699 | 6 | - tests: add Disco release [Joshua Powers] | ||
700 | 7 | - net: render 'metric' values in per-subnet routes (LP: #1805871) | ||
701 | 8 | - write_files: add support for appending to files. [James Baxter] | ||
702 | 9 | - config: On ubuntu select cloud archive mirrors for armel, armhf, arm64. | ||
703 | 10 | (LP: #1805854) | ||
704 | 11 | - dhclient-hook: cleanups, tests and fix a bug on 'down' event. | ||
705 | 12 | - NoCloud: Allow top level 'network' key in network-config. (LP: #1798117) | ||
706 | 13 | - ovf: Fix ovf network config generation gateway/routes (LP: #1806103) | ||
707 | 14 | |||
708 | 15 | -- Ryan Harper <ryan.harper@canonical.com> Fri, 14 Dec 2018 14:45:46 -0600 | ||
709 | 16 | |||
710 | 1 | cloud-init (18.4-31-gbf791715-0ubuntu1) disco; urgency=medium | 17 | cloud-init (18.4-31-gbf791715-0ubuntu1) disco; urgency=medium |
711 | 2 | 18 | ||
712 | 3 | * New upstream snapshot. | 19 | * New upstream snapshot. |
713 | diff --git a/tests/cloud_tests/releases.yaml b/tests/cloud_tests/releases.yaml | |||
714 | index defae02..ec5da72 100644 | |||
715 | --- a/tests/cloud_tests/releases.yaml | |||
716 | +++ b/tests/cloud_tests/releases.yaml | |||
717 | @@ -129,6 +129,22 @@ features: | |||
718 | 129 | 129 | ||
719 | 130 | releases: | 130 | releases: |
720 | 131 | # UBUNTU ================================================================= | 131 | # UBUNTU ================================================================= |
721 | 132 | disco: | ||
722 | 133 | # EOL: Jan 2020 | ||
723 | 134 | default: | ||
724 | 135 | enabled: true | ||
725 | 136 | release: disco | ||
726 | 137 | version: 19.04 | ||
727 | 138 | os: ubuntu | ||
728 | 139 | feature_groups: | ||
729 | 140 | - base | ||
730 | 141 | - debian_base | ||
731 | 142 | - ubuntu_specific | ||
732 | 143 | lxd: | ||
733 | 144 | sstreams_server: https://cloud-images.ubuntu.com/daily | ||
734 | 145 | alias: disco | ||
735 | 146 | setup_overrides: null | ||
736 | 147 | override_templates: false | ||
737 | 132 | cosmic: | 148 | cosmic: |
738 | 133 | # EOL: Jul 2019 | 149 | # EOL: Jul 2019 |
739 | 134 | default: | 150 | default: |
740 | diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py | |||
741 | index 199d69b..d283f13 100644 | |||
742 | --- a/tests/unittests/test_cli.py | |||
743 | +++ b/tests/unittests/test_cli.py | |||
744 | @@ -246,18 +246,18 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): | |||
745 | 246 | self.assertEqual('cc_ntp', parseargs.name) | 246 | self.assertEqual('cc_ntp', parseargs.name) |
746 | 247 | self.assertFalse(parseargs.report) | 247 | self.assertFalse(parseargs.report) |
747 | 248 | 248 | ||
750 | 249 | @mock.patch('cloudinit.cmd.main.dhclient_hook') | 249 | @mock.patch('cloudinit.cmd.main.dhclient_hook.handle_args') |
751 | 250 | def test_dhclient_hook_subcommand(self, m_dhclient_hook): | 250 | def test_dhclient_hook_subcommand(self, m_handle_args): |
752 | 251 | """The subcommand 'dhclient-hook' calls dhclient_hook with args.""" | 251 | """The subcommand 'dhclient-hook' calls dhclient_hook with args.""" |
756 | 252 | self._call_main(['cloud-init', 'dhclient-hook', 'net_action', 'eth0']) | 252 | self._call_main(['cloud-init', 'dhclient-hook', 'up', 'eth0']) |
757 | 253 | (name, parseargs) = m_dhclient_hook.call_args_list[0][0] | 253 | (name, parseargs) = m_handle_args.call_args_list[0][0] |
758 | 254 | self.assertEqual('dhclient_hook', name) | 254 | self.assertEqual('dhclient-hook', name) |
759 | 255 | self.assertEqual('dhclient-hook', parseargs.subcommand) | 255 | self.assertEqual('dhclient-hook', parseargs.subcommand) |
761 | 256 | self.assertEqual('dhclient_hook', parseargs.action[0]) | 256 | self.assertEqual('dhclient-hook', parseargs.action[0]) |
762 | 257 | self.assertFalse(parseargs.debug) | 257 | self.assertFalse(parseargs.debug) |
763 | 258 | self.assertFalse(parseargs.force) | 258 | self.assertFalse(parseargs.force) |
766 | 259 | self.assertEqual('net_action', parseargs.net_action) | 259 | self.assertEqual('up', parseargs.event) |
767 | 260 | self.assertEqual('eth0', parseargs.net_interface) | 260 | self.assertEqual('eth0', parseargs.interface) |
768 | 261 | 261 | ||
769 | 262 | @mock.patch('cloudinit.cmd.main.main_features') | 262 | @mock.patch('cloudinit.cmd.main.main_features') |
770 | 263 | def test_features_hook_subcommand(self, m_features): | 263 | def test_features_hook_subcommand(self, m_features): |
771 | diff --git a/tests/unittests/test_datasource/test_nocloud.py b/tests/unittests/test_datasource/test_nocloud.py | |||
772 | index b6468b6..3429272 100644 | |||
773 | --- a/tests/unittests/test_datasource/test_nocloud.py | |||
774 | +++ b/tests/unittests/test_datasource/test_nocloud.py | |||
775 | @@ -1,7 +1,10 @@ | |||
776 | 1 | # This file is part of cloud-init. See LICENSE file for license information. | 1 | # This file is part of cloud-init. See LICENSE file for license information. |
777 | 2 | 2 | ||
778 | 3 | from cloudinit import helpers | 3 | from cloudinit import helpers |
780 | 4 | from cloudinit.sources import DataSourceNoCloud | 4 | from cloudinit.sources.DataSourceNoCloud import ( |
781 | 5 | DataSourceNoCloud as dsNoCloud, | ||
782 | 6 | _maybe_remove_top_network, | ||
783 | 7 | parse_cmdline_data) | ||
784 | 5 | from cloudinit import util | 8 | from cloudinit import util |
785 | 6 | from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack | 9 | from cloudinit.tests.helpers import CiTestCase, populate_dir, mock, ExitStack |
786 | 7 | 10 | ||
787 | @@ -40,9 +43,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
788 | 40 | 'datasource': {'NoCloud': {'fs_label': None}} | 43 | 'datasource': {'NoCloud': {'fs_label': None}} |
789 | 41 | } | 44 | } |
790 | 42 | 45 | ||
794 | 43 | ds = DataSourceNoCloud.DataSourceNoCloud | 46 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
792 | 44 | |||
793 | 45 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
795 | 46 | ret = dsrc.get_data() | 47 | ret = dsrc.get_data() |
796 | 47 | self.assertEqual(dsrc.userdata_raw, ud) | 48 | self.assertEqual(dsrc.userdata_raw, ud) |
797 | 48 | self.assertEqual(dsrc.metadata, md) | 49 | self.assertEqual(dsrc.metadata, md) |
798 | @@ -63,9 +64,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
799 | 63 | 'datasource': {'NoCloud': {'fs_label': None}} | 64 | 'datasource': {'NoCloud': {'fs_label': None}} |
800 | 64 | } | 65 | } |
801 | 65 | 66 | ||
805 | 66 | ds = DataSourceNoCloud.DataSourceNoCloud | 67 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
803 | 67 | |||
804 | 68 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
806 | 69 | self.assertTrue(dsrc.get_data()) | 68 | self.assertTrue(dsrc.get_data()) |
807 | 70 | self.assertEqual(dsrc.platform_type, 'nocloud') | 69 | self.assertEqual(dsrc.platform_type, 'nocloud') |
808 | 71 | self.assertEqual( | 70 | self.assertEqual( |
809 | @@ -73,8 +72,6 @@ class TestNoCloudDataSource(CiTestCase): | |||
810 | 73 | 72 | ||
811 | 74 | def test_fs_label(self, m_is_lxd): | 73 | def test_fs_label(self, m_is_lxd): |
812 | 75 | # find_devs_with should not be called ff fs_label is None | 74 | # find_devs_with should not be called ff fs_label is None |
813 | 76 | ds = DataSourceNoCloud.DataSourceNoCloud | ||
814 | 77 | |||
815 | 78 | class PsuedoException(Exception): | 75 | class PsuedoException(Exception): |
816 | 79 | pass | 76 | pass |
817 | 80 | 77 | ||
818 | @@ -84,12 +81,12 @@ class TestNoCloudDataSource(CiTestCase): | |||
819 | 84 | 81 | ||
820 | 85 | # by default, NoCloud should search for filesystems by label | 82 | # by default, NoCloud should search for filesystems by label |
821 | 86 | sys_cfg = {'datasource': {'NoCloud': {}}} | 83 | sys_cfg = {'datasource': {'NoCloud': {}}} |
823 | 87 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | 84 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
824 | 88 | self.assertRaises(PsuedoException, dsrc.get_data) | 85 | self.assertRaises(PsuedoException, dsrc.get_data) |
825 | 89 | 86 | ||
826 | 90 | # but disabling searching should just end up with None found | 87 | # but disabling searching should just end up with None found |
827 | 91 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 88 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
829 | 92 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | 89 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
830 | 93 | ret = dsrc.get_data() | 90 | ret = dsrc.get_data() |
831 | 94 | self.assertFalse(ret) | 91 | self.assertFalse(ret) |
832 | 95 | 92 | ||
833 | @@ -97,13 +94,10 @@ class TestNoCloudDataSource(CiTestCase): | |||
834 | 97 | # no source should be found if no cmdline, config, and fs_label=None | 94 | # no source should be found if no cmdline, config, and fs_label=None |
835 | 98 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 95 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
836 | 99 | 96 | ||
839 | 100 | ds = DataSourceNoCloud.DataSourceNoCloud | 97 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
838 | 101 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
840 | 102 | self.assertFalse(dsrc.get_data()) | 98 | self.assertFalse(dsrc.get_data()) |
841 | 103 | 99 | ||
842 | 104 | def test_seed_in_config(self, m_is_lxd): | 100 | def test_seed_in_config(self, m_is_lxd): |
843 | 105 | ds = DataSourceNoCloud.DataSourceNoCloud | ||
844 | 106 | |||
845 | 107 | data = { | 101 | data = { |
846 | 108 | 'fs_label': None, | 102 | 'fs_label': None, |
847 | 109 | 'meta-data': yaml.safe_dump({'instance-id': 'IID'}), | 103 | 'meta-data': yaml.safe_dump({'instance-id': 'IID'}), |
848 | @@ -111,7 +105,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
849 | 111 | } | 105 | } |
850 | 112 | 106 | ||
851 | 113 | sys_cfg = {'datasource': {'NoCloud': data}} | 107 | sys_cfg = {'datasource': {'NoCloud': data}} |
853 | 114 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | 108 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
854 | 115 | ret = dsrc.get_data() | 109 | ret = dsrc.get_data() |
855 | 116 | self.assertEqual(dsrc.userdata_raw, b"USER_DATA_RAW") | 110 | self.assertEqual(dsrc.userdata_raw, b"USER_DATA_RAW") |
856 | 117 | self.assertEqual(dsrc.metadata.get('instance-id'), 'IID') | 111 | self.assertEqual(dsrc.metadata.get('instance-id'), 'IID') |
857 | @@ -130,9 +124,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
858 | 130 | 'datasource': {'NoCloud': {'fs_label': None}} | 124 | 'datasource': {'NoCloud': {'fs_label': None}} |
859 | 131 | } | 125 | } |
860 | 132 | 126 | ||
864 | 133 | ds = DataSourceNoCloud.DataSourceNoCloud | 127 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
862 | 134 | |||
863 | 135 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
865 | 136 | ret = dsrc.get_data() | 128 | ret = dsrc.get_data() |
866 | 137 | self.assertEqual(dsrc.userdata_raw, ud) | 129 | self.assertEqual(dsrc.userdata_raw, ud) |
867 | 138 | self.assertEqual(dsrc.metadata, md) | 130 | self.assertEqual(dsrc.metadata, md) |
868 | @@ -145,9 +137,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
869 | 145 | 137 | ||
870 | 146 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 138 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
871 | 147 | 139 | ||
875 | 148 | ds = DataSourceNoCloud.DataSourceNoCloud | 140 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
873 | 149 | |||
874 | 150 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
876 | 151 | ret = dsrc.get_data() | 141 | ret = dsrc.get_data() |
877 | 152 | self.assertEqual(dsrc.userdata_raw, b"ud") | 142 | self.assertEqual(dsrc.userdata_raw, b"ud") |
878 | 153 | self.assertFalse(dsrc.vendordata) | 143 | self.assertFalse(dsrc.vendordata) |
879 | @@ -174,9 +164,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
880 | 174 | 164 | ||
881 | 175 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 165 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
882 | 176 | 166 | ||
886 | 177 | ds = DataSourceNoCloud.DataSourceNoCloud | 167 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
884 | 178 | |||
885 | 179 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
887 | 180 | ret = dsrc.get_data() | 168 | ret = dsrc.get_data() |
888 | 181 | self.assertTrue(ret) | 169 | self.assertTrue(ret) |
889 | 182 | # very simple check just for the strings above | 170 | # very simple check just for the strings above |
890 | @@ -195,9 +183,23 @@ class TestNoCloudDataSource(CiTestCase): | |||
891 | 195 | 183 | ||
892 | 196 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 184 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
893 | 197 | 185 | ||
895 | 198 | ds = DataSourceNoCloud.DataSourceNoCloud | 186 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
896 | 187 | ret = dsrc.get_data() | ||
897 | 188 | self.assertTrue(ret) | ||
898 | 189 | self.assertEqual(netconf, dsrc.network_config) | ||
899 | 190 | |||
900 | 191 | def test_metadata_network_config_with_toplevel_network(self, m_is_lxd): | ||
901 | 192 | """network-config may have 'network' top level key.""" | ||
902 | 193 | netconf = {'config': 'disabled'} | ||
903 | 194 | populate_dir( | ||
904 | 195 | os.path.join(self.paths.seed_dir, "nocloud"), | ||
905 | 196 | {'user-data': b"ud", | ||
906 | 197 | 'meta-data': "instance-id: IID\n", | ||
907 | 198 | 'network-config': yaml.dump({'network': netconf}) + "\n"}) | ||
908 | 199 | |||
909 | 200 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | ||
910 | 199 | 201 | ||
912 | 200 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | 202 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
913 | 201 | ret = dsrc.get_data() | 203 | ret = dsrc.get_data() |
914 | 202 | self.assertTrue(ret) | 204 | self.assertTrue(ret) |
915 | 203 | self.assertEqual(netconf, dsrc.network_config) | 205 | self.assertEqual(netconf, dsrc.network_config) |
916 | @@ -228,9 +230,7 @@ class TestNoCloudDataSource(CiTestCase): | |||
917 | 228 | 230 | ||
918 | 229 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} | 231 | sys_cfg = {'datasource': {'NoCloud': {'fs_label': None}}} |
919 | 230 | 232 | ||
923 | 231 | ds = DataSourceNoCloud.DataSourceNoCloud | 233 | dsrc = dsNoCloud(sys_cfg=sys_cfg, distro=None, paths=self.paths) |
921 | 232 | |||
922 | 233 | dsrc = ds(sys_cfg=sys_cfg, distro=None, paths=self.paths) | ||
924 | 234 | ret = dsrc.get_data() | 234 | ret = dsrc.get_data() |
925 | 235 | self.assertTrue(ret) | 235 | self.assertTrue(ret) |
926 | 236 | self.assertEqual(netconf, dsrc.network_config) | 236 | self.assertEqual(netconf, dsrc.network_config) |
927 | @@ -258,8 +258,7 @@ class TestParseCommandLineData(CiTestCase): | |||
928 | 258 | for (fmt, expected) in pairs: | 258 | for (fmt, expected) in pairs: |
929 | 259 | fill = {} | 259 | fill = {} |
930 | 260 | cmdline = fmt % {'ds_id': ds_id} | 260 | cmdline = fmt % {'ds_id': ds_id} |
933 | 261 | ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill, | 261 | ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline) |
932 | 262 | cmdline=cmdline) | ||
934 | 263 | self.assertEqual(expected, fill) | 262 | self.assertEqual(expected, fill) |
935 | 264 | self.assertTrue(ret) | 263 | self.assertTrue(ret) |
936 | 265 | 264 | ||
937 | @@ -276,10 +275,43 @@ class TestParseCommandLineData(CiTestCase): | |||
938 | 276 | 275 | ||
939 | 277 | for cmdline in cmdlines: | 276 | for cmdline in cmdlines: |
940 | 278 | fill = {} | 277 | fill = {} |
943 | 279 | ret = DataSourceNoCloud.parse_cmdline_data(ds_id=ds_id, fill=fill, | 278 | ret = parse_cmdline_data(ds_id=ds_id, fill=fill, cmdline=cmdline) |
942 | 280 | cmdline=cmdline) | ||
944 | 281 | self.assertEqual(fill, {}) | 279 | self.assertEqual(fill, {}) |
945 | 282 | self.assertFalse(ret) | 280 | self.assertFalse(ret) |
946 | 283 | 281 | ||
947 | 284 | 282 | ||
948 | 283 | class TestMaybeRemoveToplevelNetwork(CiTestCase): | ||
949 | 284 | """test _maybe_remove_top_network function.""" | ||
950 | 285 | basecfg = [{'type': 'physical', 'name': 'interface0', | ||
951 | 286 | 'subnets': [{'type': 'dhcp'}]}] | ||
952 | 287 | |||
953 | 288 | def test_should_remove_safely(self): | ||
954 | 289 | mcfg = {'config': self.basecfg, 'version': 1} | ||
955 | 290 | self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg})) | ||
956 | 291 | |||
957 | 292 | def test_no_remove_if_other_keys(self): | ||
958 | 293 | """should not shift if other keys at top level.""" | ||
959 | 294 | mcfg = {'network': {'config': self.basecfg, 'version': 1}, | ||
960 | 295 | 'unknown_keyname': 'keyval'} | ||
961 | 296 | self.assertEqual(mcfg, _maybe_remove_top_network(mcfg)) | ||
962 | 297 | |||
963 | 298 | def test_no_remove_if_non_dict(self): | ||
964 | 299 | """should not shift if not a dict.""" | ||
965 | 300 | mcfg = {'network': '"content here'} | ||
966 | 301 | self.assertEqual(mcfg, _maybe_remove_top_network(mcfg)) | ||
967 | 302 | |||
968 | 303 | def test_no_remove_if_missing_config_or_version(self): | ||
969 | 304 | """should not shift unless network entry has config and version.""" | ||
970 | 305 | mcfg = {'network': {'config': self.basecfg}} | ||
971 | 306 | self.assertEqual(mcfg, _maybe_remove_top_network(mcfg)) | ||
972 | 307 | |||
973 | 308 | mcfg = {'network': {'version': 1}} | ||
974 | 309 | self.assertEqual(mcfg, _maybe_remove_top_network(mcfg)) | ||
975 | 310 | |||
976 | 311 | def test_remove_with_config_disabled(self): | ||
977 | 312 | """network/config=disabled should be shifted.""" | ||
978 | 313 | mcfg = {'config': 'disabled'} | ||
979 | 314 | self.assertEqual(mcfg, _maybe_remove_top_network({'network': mcfg})) | ||
980 | 315 | |||
981 | 316 | |||
982 | 285 | # vi: ts=4 expandtab | 317 | # vi: ts=4 expandtab |
983 | diff --git a/tests/unittests/test_handler/test_handler_write_files.py b/tests/unittests/test_handler/test_handler_write_files.py | |||
984 | index 7fa8fd2..bc8756c 100644 | |||
985 | --- a/tests/unittests/test_handler/test_handler_write_files.py | |||
986 | +++ b/tests/unittests/test_handler/test_handler_write_files.py | |||
987 | @@ -52,6 +52,18 @@ class TestWriteFiles(FilesystemMockingTestCase): | |||
988 | 52 | "test_simple", [{"content": expected, "path": filename}]) | 52 | "test_simple", [{"content": expected, "path": filename}]) |
989 | 53 | self.assertEqual(util.load_file(filename), expected) | 53 | self.assertEqual(util.load_file(filename), expected) |
990 | 54 | 54 | ||
991 | 55 | def test_append(self): | ||
992 | 56 | self.patchUtils(self.tmp) | ||
993 | 57 | existing = "hello " | ||
994 | 58 | added = "world\n" | ||
995 | 59 | expected = existing + added | ||
996 | 60 | filename = "/tmp/append.file" | ||
997 | 61 | util.write_file(filename, existing) | ||
998 | 62 | write_files( | ||
999 | 63 | "test_append", | ||
1000 | 64 | [{"content": added, "path": filename, "append": "true"}]) | ||
1001 | 65 | self.assertEqual(util.load_file(filename), expected) | ||
1002 | 66 | |||
1003 | 55 | def test_yaml_binary(self): | 67 | def test_yaml_binary(self): |
1004 | 56 | self.patchUtils(self.tmp) | 68 | self.patchUtils(self.tmp) |
1005 | 57 | data = util.load_yaml(YAML_TEXT) | 69 | data = util.load_yaml(YAML_TEXT) |
1006 | diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py | |||
1007 | index 8e38373..195f261 100644 | |||
1008 | --- a/tests/unittests/test_net.py | |||
1009 | +++ b/tests/unittests/test_net.py | |||
1010 | @@ -488,8 +488,8 @@ NETWORK_CONFIGS = { | |||
1011 | 488 | address 192.168.21.3/24 | 488 | address 192.168.21.3/24 |
1012 | 489 | dns-nameservers 8.8.8.8 8.8.4.4 | 489 | dns-nameservers 8.8.8.8 8.8.4.4 |
1013 | 490 | dns-search barley.maas sach.maas | 490 | dns-search barley.maas sach.maas |
1016 | 491 | post-up route add default gw 65.61.151.37 || true | 491 | post-up route add default gw 65.61.151.37 metric 10000 || true |
1017 | 492 | pre-down route del default gw 65.61.151.37 || true | 492 | pre-down route del default gw 65.61.151.37 metric 10000 || true |
1018 | 493 | """).rstrip(' '), | 493 | """).rstrip(' '), |
1019 | 494 | 'expected_netplan': textwrap.dedent(""" | 494 | 'expected_netplan': textwrap.dedent(""" |
1020 | 495 | network: | 495 | network: |
1021 | @@ -513,7 +513,8 @@ NETWORK_CONFIGS = { | |||
1022 | 513 | - barley.maas | 513 | - barley.maas |
1023 | 514 | - sach.maas | 514 | - sach.maas |
1024 | 515 | routes: | 515 | routes: |
1026 | 516 | - to: 0.0.0.0/0 | 516 | - metric: 10000 |
1027 | 517 | to: 0.0.0.0/0 | ||
1028 | 517 | via: 65.61.151.37 | 518 | via: 65.61.151.37 |
1029 | 518 | set-name: eth99 | 519 | set-name: eth99 |
1030 | 519 | """).rstrip(' '), | 520 | """).rstrip(' '), |
1031 | @@ -537,6 +538,7 @@ NETWORK_CONFIGS = { | |||
1032 | 537 | HWADDR=c0:d6:9f:2c:e8:80 | 538 | HWADDR=c0:d6:9f:2c:e8:80 |
1033 | 538 | IPADDR=192.168.21.3 | 539 | IPADDR=192.168.21.3 |
1034 | 539 | NETMASK=255.255.255.0 | 540 | NETMASK=255.255.255.0 |
1035 | 541 | METRIC=10000 | ||
1036 | 540 | NM_CONTROLLED=no | 542 | NM_CONTROLLED=no |
1037 | 541 | ONBOOT=yes | 543 | ONBOOT=yes |
1038 | 542 | TYPE=Ethernet | 544 | TYPE=Ethernet |
1039 | @@ -561,7 +563,7 @@ NETWORK_CONFIGS = { | |||
1040 | 561 | - gateway: 65.61.151.37 | 563 | - gateway: 65.61.151.37 |
1041 | 562 | netmask: 0.0.0.0 | 564 | netmask: 0.0.0.0 |
1042 | 563 | network: 0.0.0.0 | 565 | network: 0.0.0.0 |
1044 | 564 | metric: 2 | 566 | metric: 10000 |
1045 | 565 | - type: physical | 567 | - type: physical |
1046 | 566 | name: eth1 | 568 | name: eth1 |
1047 | 567 | mac_address: "cf:d6:af:48:e8:80" | 569 | mac_address: "cf:d6:af:48:e8:80" |
1048 | @@ -1161,6 +1163,13 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | |||
1049 | 1161 | - gateway: 192.168.0.3 | 1163 | - gateway: 192.168.0.3 |
1050 | 1162 | netmask: 255.255.255.0 | 1164 | netmask: 255.255.255.0 |
1051 | 1163 | network: 10.1.3.0 | 1165 | network: 10.1.3.0 |
1052 | 1166 | - gateway: 2001:67c:1562:1 | ||
1053 | 1167 | network: 2001:67c:1 | ||
1054 | 1168 | netmask: ffff:ffff:0 | ||
1055 | 1169 | - gateway: 3001:67c:1562:1 | ||
1056 | 1170 | network: 3001:67c:1 | ||
1057 | 1171 | netmask: ffff:ffff:0 | ||
1058 | 1172 | metric: 10000 | ||
1059 | 1164 | - type: static | 1173 | - type: static |
1060 | 1165 | address: 192.168.1.2/24 | 1174 | address: 192.168.1.2/24 |
1061 | 1166 | - type: static | 1175 | - type: static |
1062 | @@ -1197,6 +1206,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | |||
1063 | 1197 | routes: | 1206 | routes: |
1064 | 1198 | - to: 10.1.3.0/24 | 1207 | - to: 10.1.3.0/24 |
1065 | 1199 | via: 192.168.0.3 | 1208 | via: 192.168.0.3 |
1066 | 1209 | - to: 2001:67c:1/32 | ||
1067 | 1210 | via: 2001:67c:1562:1 | ||
1068 | 1211 | - metric: 10000 | ||
1069 | 1212 | to: 3001:67c:1/32 | ||
1070 | 1213 | via: 3001:67c:1562:1 | ||
1071 | 1200 | """), | 1214 | """), |
1072 | 1201 | 'yaml-v2': textwrap.dedent(""" | 1215 | 'yaml-v2': textwrap.dedent(""" |
1073 | 1202 | version: 2 | 1216 | version: 2 |
1074 | @@ -1228,6 +1242,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | |||
1075 | 1228 | routes: | 1242 | routes: |
1076 | 1229 | - to: 10.1.3.0/24 | 1243 | - to: 10.1.3.0/24 |
1077 | 1230 | via: 192.168.0.3 | 1244 | via: 192.168.0.3 |
1078 | 1245 | - to: 2001:67c:1562:8007::1/64 | ||
1079 | 1246 | via: 2001:67c:1562:8007::aac:40b2 | ||
1080 | 1247 | - metric: 10000 | ||
1081 | 1248 | to: 3001:67c:1562:8007::1/64 | ||
1082 | 1249 | via: 3001:67c:1562:8007::aac:40b2 | ||
1083 | 1231 | """), | 1250 | """), |
1084 | 1232 | 'expected_netplan-v2': textwrap.dedent(""" | 1251 | 'expected_netplan-v2': textwrap.dedent(""" |
1085 | 1233 | network: | 1252 | network: |
1086 | @@ -1249,6 +1268,11 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | |||
1087 | 1249 | routes: | 1268 | routes: |
1088 | 1250 | - to: 10.1.3.0/24 | 1269 | - to: 10.1.3.0/24 |
1089 | 1251 | via: 192.168.0.3 | 1270 | via: 192.168.0.3 |
1090 | 1271 | - to: 2001:67c:1562:8007::1/64 | ||
1091 | 1272 | via: 2001:67c:1562:8007::aac:40b2 | ||
1092 | 1273 | - metric: 10000 | ||
1093 | 1274 | to: 3001:67c:1562:8007::1/64 | ||
1094 | 1275 | via: 3001:67c:1562:8007::aac:40b2 | ||
1095 | 1252 | ethernets: | 1276 | ethernets: |
1096 | 1253 | eth0: | 1277 | eth0: |
1097 | 1254 | match: | 1278 | match: |
1098 | @@ -1349,6 +1373,10 @@ pre-down route del -net 10.0.0.0 netmask 255.0.0.0 gw 11.0.0.1 metric 3 || true | |||
1099 | 1349 | USERCTL=no | 1373 | USERCTL=no |
1100 | 1350 | """), | 1374 | """), |
1101 | 1351 | 'route6-bond0': textwrap.dedent("""\ | 1375 | 'route6-bond0': textwrap.dedent("""\ |
1102 | 1376 | # Created by cloud-init on instance boot automatically, do not edit. | ||
1103 | 1377 | # | ||
1104 | 1378 | 2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1 dev bond0 | ||
1105 | 1379 | 3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0 | ||
1106 | 1352 | """), | 1380 | """), |
1107 | 1353 | 'route-bond0': textwrap.dedent("""\ | 1381 | 'route-bond0': textwrap.dedent("""\ |
1108 | 1354 | ADDRESS0=10.1.3.0 | 1382 | ADDRESS0=10.1.3.0 |
1109 | @@ -1879,14 +1907,24 @@ class TestRhelSysConfigRendering(CiTestCase): | |||
1110 | 1879 | return dir2dict(dir) | 1907 | return dir2dict(dir) |
1111 | 1880 | 1908 | ||
1112 | 1881 | def _compare_files_to_expected(self, expected, found): | 1909 | def _compare_files_to_expected(self, expected, found): |
1113 | 1910 | |||
1114 | 1911 | def _try_load(f): | ||
1115 | 1912 | ''' Attempt to load shell content, otherwise return as-is ''' | ||
1116 | 1913 | try: | ||
1117 | 1914 | return util.load_shell_content(f) | ||
1118 | 1915 | except ValueError: | ||
1119 | 1916 | pass | ||
1120 | 1917 | # route6- * files aren't shell content, but iproute2 params | ||
1121 | 1918 | return f | ||
1122 | 1919 | |||
1123 | 1882 | orig_maxdiff = self.maxDiff | 1920 | orig_maxdiff = self.maxDiff |
1124 | 1883 | expected_d = dict( | 1921 | expected_d = dict( |
1126 | 1884 | (os.path.join(self.scripts_dir, k), util.load_shell_content(v)) | 1922 | (os.path.join(self.scripts_dir, k), _try_load(v)) |
1127 | 1885 | for k, v in expected.items()) | 1923 | for k, v in expected.items()) |
1128 | 1886 | 1924 | ||
1129 | 1887 | # only compare the files in scripts_dir | 1925 | # only compare the files in scripts_dir |
1130 | 1888 | scripts_found = dict( | 1926 | scripts_found = dict( |
1132 | 1889 | (k, util.load_shell_content(v)) for k, v in found.items() | 1927 | (k, _try_load(v)) for k, v in found.items() |
1133 | 1890 | if k.startswith(self.scripts_dir)) | 1928 | if k.startswith(self.scripts_dir)) |
1134 | 1891 | try: | 1929 | try: |
1135 | 1892 | self.maxDiff = None | 1930 | self.maxDiff = None |
1136 | diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/test_vmware_config_file.py | |||
1137 | index 602dedb..f47335e 100644 | |||
1138 | --- a/tests/unittests/test_vmware_config_file.py | |||
1139 | +++ b/tests/unittests/test_vmware_config_file.py | |||
1140 | @@ -263,7 +263,7 @@ class TestVmwareConfigFile(CiTestCase): | |||
1141 | 263 | nicConfigurator = NicConfigurator(config.nics, False) | 263 | nicConfigurator = NicConfigurator(config.nics, False) |
1142 | 264 | nics_cfg_list = nicConfigurator.generate() | 264 | nics_cfg_list = nicConfigurator.generate() |
1143 | 265 | 265 | ||
1145 | 266 | self.assertEqual(5, len(nics_cfg_list), "number of elements") | 266 | self.assertEqual(2, len(nics_cfg_list), "number of elements") |
1146 | 267 | 267 | ||
1147 | 268 | nic1 = {'name': 'NIC1'} | 268 | nic1 = {'name': 'NIC1'} |
1148 | 269 | nic2 = {'name': 'NIC2'} | 269 | nic2 = {'name': 'NIC2'} |
1149 | @@ -275,8 +275,6 @@ class TestVmwareConfigFile(CiTestCase): | |||
1150 | 275 | nic1.update(cfg) | 275 | nic1.update(cfg) |
1151 | 276 | elif cfg.get('name') == nic2.get('name'): | 276 | elif cfg.get('name') == nic2.get('name'): |
1152 | 277 | nic2.update(cfg) | 277 | nic2.update(cfg) |
1153 | 278 | elif cfg_type == 'route': | ||
1154 | 279 | route_list.append(cfg) | ||
1155 | 280 | 278 | ||
1156 | 281 | self.assertEqual('physical', nic1.get('type'), 'type of NIC1') | 279 | self.assertEqual('physical', nic1.get('type'), 'type of NIC1') |
1157 | 282 | self.assertEqual('NIC1', nic1.get('name'), 'name of NIC1') | 280 | self.assertEqual('NIC1', nic1.get('name'), 'name of NIC1') |
1158 | @@ -297,6 +295,9 @@ class TestVmwareConfigFile(CiTestCase): | |||
1159 | 297 | static6_subnet.append(subnet) | 295 | static6_subnet.append(subnet) |
1160 | 298 | else: | 296 | else: |
1161 | 299 | self.assertEqual(True, False, 'Unknown type') | 297 | self.assertEqual(True, False, 'Unknown type') |
1162 | 298 | if 'route' in subnet: | ||
1163 | 299 | for route in subnet.get('routes'): | ||
1164 | 300 | route_list.append(route) | ||
1165 | 300 | 301 | ||
1166 | 301 | self.assertEqual(1, len(static_subnet), 'Number of static subnet') | 302 | self.assertEqual(1, len(static_subnet), 'Number of static subnet') |
1167 | 302 | self.assertEqual(1, len(static6_subnet), 'Number of static6 subnet') | 303 | self.assertEqual(1, len(static6_subnet), 'Number of static6 subnet') |
1168 | @@ -351,6 +352,8 @@ class TestVmwareConfigFile(CiTestCase): | |||
1169 | 351 | class TestVmwareNetConfig(CiTestCase): | 352 | class TestVmwareNetConfig(CiTestCase): |
1170 | 352 | """Test conversion of vmware config to cloud-init config.""" | 353 | """Test conversion of vmware config to cloud-init config.""" |
1171 | 353 | 354 | ||
1172 | 355 | maxDiff = None | ||
1173 | 356 | |||
1174 | 354 | def _get_NicConfigurator(self, text): | 357 | def _get_NicConfigurator(self, text): |
1175 | 355 | fp = None | 358 | fp = None |
1176 | 356 | try: | 359 | try: |
1177 | @@ -420,9 +423,52 @@ class TestVmwareNetConfig(CiTestCase): | |||
1178 | 420 | 'mac_address': '00:50:56:a6:8c:08', | 423 | 'mac_address': '00:50:56:a6:8c:08', |
1179 | 421 | 'subnets': [ | 424 | 'subnets': [ |
1180 | 422 | {'control': 'auto', 'type': 'static', | 425 | {'control': 'auto', 'type': 'static', |
1184 | 423 | 'address': '10.20.87.154', 'netmask': '255.255.252.0'}]}, | 426 | 'address': '10.20.87.154', 'netmask': '255.255.252.0', |
1185 | 424 | {'type': 'route', 'destination': '10.20.84.0/22', | 427 | 'routes': |
1186 | 425 | 'gateway': '10.20.87.253', 'metric': 10000}], | 428 | [{'type': 'route', 'destination': '10.20.84.0/22', |
1187 | 429 | 'gateway': '10.20.87.253', 'metric': 10000}]}]}], | ||
1188 | 430 | nc.generate()) | ||
1189 | 431 | |||
1190 | 432 | def test_cust_non_primary_nic_with_gateway_(self): | ||
1191 | 433 | """A customer non primary nic set can have a gateway.""" | ||
1192 | 434 | config = textwrap.dedent("""\ | ||
1193 | 435 | [NETWORK] | ||
1194 | 436 | NETWORKING = yes | ||
1195 | 437 | BOOTPROTO = dhcp | ||
1196 | 438 | HOSTNAME = static-debug-vm | ||
1197 | 439 | DOMAINNAME = cluster.local | ||
1198 | 440 | |||
1199 | 441 | [NIC-CONFIG] | ||
1200 | 442 | NICS = NIC1 | ||
1201 | 443 | |||
1202 | 444 | [NIC1] | ||
1203 | 445 | MACADDR = 00:50:56:ac:d1:8a | ||
1204 | 446 | ONBOOT = yes | ||
1205 | 447 | IPv4_MODE = BACKWARDS_COMPATIBLE | ||
1206 | 448 | BOOTPROTO = static | ||
1207 | 449 | IPADDR = 100.115.223.75 | ||
1208 | 450 | NETMASK = 255.255.255.0 | ||
1209 | 451 | GATEWAY = 100.115.223.254 | ||
1210 | 452 | |||
1211 | 453 | |||
1212 | 454 | [DNS] | ||
1213 | 455 | DNSFROMDHCP=no | ||
1214 | 456 | |||
1215 | 457 | NAMESERVER|1 = 8.8.8.8 | ||
1216 | 458 | |||
1217 | 459 | [DATETIME] | ||
1218 | 460 | UTC = yes | ||
1219 | 461 | """) | ||
1220 | 462 | nc = self._get_NicConfigurator(config) | ||
1221 | 463 | self.assertEqual( | ||
1222 | 464 | [{'type': 'physical', 'name': 'NIC1', | ||
1223 | 465 | 'mac_address': '00:50:56:ac:d1:8a', | ||
1224 | 466 | 'subnets': [ | ||
1225 | 467 | {'control': 'auto', 'type': 'static', | ||
1226 | 468 | 'address': '100.115.223.75', 'netmask': '255.255.255.0', | ||
1227 | 469 | 'routes': | ||
1228 | 470 | [{'type': 'route', 'destination': '100.115.223.0/24', | ||
1229 | 471 | 'gateway': '100.115.223.254', 'metric': 10000}]}]}], | ||
1230 | 426 | nc.generate()) | 472 | nc.generate()) |
1231 | 427 | 473 | ||
1232 | 428 | def test_a_primary_nic_with_gateway(self): | 474 | def test_a_primary_nic_with_gateway(self): |
1233 | diff --git a/tox.ini b/tox.ini | |||
1234 | index 2fb3209..d983348 100644 | |||
1235 | --- a/tox.ini | |||
1236 | +++ b/tox.ini | |||
1237 | @@ -21,7 +21,7 @@ setenv = | |||
1238 | 21 | basepython = python3 | 21 | basepython = python3 |
1239 | 22 | deps = | 22 | deps = |
1240 | 23 | # requirements | 23 | # requirements |
1242 | 24 | pylint==1.8.1 | 24 | pylint==2.2.2 |
1243 | 25 | # test-requirements because unit tests are now present in cloudinit tree | 25 | # test-requirements because unit tests are now present in cloudinit tree |
1244 | 26 | -r{toxinidir}/test-requirements.txt | 26 | -r{toxinidir}/test-requirements.txt |
1245 | 27 | commands = {envpython} -m pylint {posargs:cloudinit tests tools} | 27 | commands = {envpython} -m pylint {posargs:cloudinit tests tools} |
PASSED: Continuous integration, rev:47a4d44a244 aefb55e2a192bd0 69ae830c6d176e /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 490/
https:/
Executed test runs:
SUCCESS: Checkout
SUCCESS: Unit & Style Tests
SUCCESS: Ubuntu LTS: Build
SUCCESS: Ubuntu LTS: Integration
IN_PROGRESS: Declarative: Post Actions
Click here to trigger a rebuild: /jenkins. ubuntu. com/server/ job/cloud- init-ci/ 490/rebuild
https:/