Merge ~akaris/cloud-init:bug1679817-c into cloud-init:master

Proposed by Andreas Karis
Status: Merged
Approved by: Scott Moser
Approved revision: 48bf21bcb6046f1a2d1d029d286dcbe9eaf4f62e
Merged at revision: f38fa41317602908139aa96e930b634f65e39555
Proposed branch: ~akaris/cloud-init:bug1679817-c
Merge into: cloud-init:master
Diff against target: 458 lines (+199/-132)
3 files modified
cloudinit/net/sysconfig.py (+174/-70)
tests/unittests/test_distros/test_netconfig.py (+3/-5)
tests/unittests/test_net.py (+22/-57)
Reviewer Review Type Date Requested Status
Scott Moser Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+324196@code.launchpad.net

This proposal supersedes a proposal from 2017-05-17.

Commit message

Fix dual stack IPv4/IPv6 configuration for RHEL

Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7.
This patch fixes several scenarios for IPv4/IPv6/dual stack with multiple IP assignment
Removes unpopular IPv4 alias files and invalid IPv6 alias files

Also fixes associated unit tests

LP: #1679817
LP: #1685534
LP: #1685532

To post a comment you must log in.
Revision history for this message
Andreas Karis (akaris) wrote : Posted in a previous version of this proposal

Fix dual stack IPv4/IPv6 configuration for RHEL

Dual stack IPv4/IPv6 configuration via config drive is broken for RHEL7.
This patch fixes several scenarios for IPv4/IPv6/dual stack with multiple IP assignment
Removes unpopular IPv4 alias files and invalid IPv6 alias files

Also fixes associated unit tests

LP: #1679817
LP: #1685534
LP: #1685532

Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal

FAILED: Continuous integration, rev:1a401c57978855b5d56ac23976252e2506d206fa
No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want a jenkins rebuild you need to trigger it yourself):
https://code.launchpad.net/~akaris/cloud-init/+git/cloud-init/+merge/324195/+edit-commit-message

https://jenkins.ubuntu.com/server/job/cloud-init-ci/345/
Executed test runs:
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-amd64/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-arm64/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=metal-ppc64el/345
    SUCCESS: https://jenkins.ubuntu.com/server/job/cloud-init-ci/nodes=vm-i386/345

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/345/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Hi,
I assume this will still work for Centos 6 (5?)

It looks very good, thanks for your work.

Will this work correctly for centos 5 and 6 ?

review: Needs Information
Revision history for this message
Server Team CI bot (server-team-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote :

Assuming this works with centos 5, 6, 7, I'm happy to pull this.

Revision history for this message
Andreas Karis (akaris) wrote :

Hi,

Let me test all of this out once more and give a final ack.

- Andreas

Revision history for this message
Andreas Karis (akaris) wrote :
Download full text (12.3 KiB)

Test procedure for RHEL 7:

### Enable config drive IPv4/IPv6 address injection ###
On the compute nodes and controllers, configure
~~~
crudini --set /etc/nova/nova.conf DEFAULT injected_network_template /usr/lib/python2.7/site-packages/nova/virt/interfaces.template
crudini --set /etc/nova/nova.conf DEFAULT flat_injected true
crudini --set /etc/nova/nova.conf DEFAULT force_config_drive true
crudini --set /etc/nova/nova.conf DEFAULT config_drive_cdrom True
crudini --set /etc/nova/nova.conf DEFAULT debug true
crudini --set /etc/nova/nova.conf DEFAULT use_ipv6 true
crudini --set /etc/nova/nova.conf os_vif_linux_bridge use_ipv6 true
crudini --set /etc/nova/nova.conf libvirt inject_partition -1
~~~

Restart all OpenStack services on computes and controllers:
~~~
systemctl list-units | grep nova | awk '{print $1}' | xargs -I {} systemctl restart {}
~~~

### Use a recent version of cloud-init within the instances ###
~~~
sudo yumdownloader cloud-init pyserial python-jinja2 python-babel python-markupsafe pytz
for i in *.rpm;do virt-customize -a rhel.qcow2 --upload $i:/root/$i ; done
virt-customize -a rhel.qcow2 -v --run-command 'yum -y localinstall /root/*.rpm'
source overcloudrc
~~~

Set password for console login
~~~
virt-customize -a rhel.qcow2 --root-password password:Redhat01
~~~

Create glance image
~~~
glance image-create --name rhel-cloud-init --file rhel.qcow2 --container-format bare --disk-format qcow2 --progress
~~~

### Open all security groups and add keypair ###
~~~
nova secgroup-add-rule default icmp -1 -1 0.0.0.0/0
nova secgroup-add-rule default tcp 1 65535 0.0.0.0/0
nova secgroup-add-rule default udp 1 65535 0.0.0.0/0
nova secgroup-add-rule default icmp -1 -1 ::/0
nova secgroup-add-rule default tcp 1 65535 ::/0
nova secgroup-add-rule default udp 1 65535 ::/0
~~~

~~~
nova keypair-add --pub-key ~/.ssh/id_rsa.pub id_rsa
~~~

### Configure networks without DHCP ###
Make sure that none of the subnets has DHCP enabled!
~~~
# access network
neutron net-create provider1 --provider:network_type vlan --provider:physical_network $PROVIDER_PHYSICAL_NETWORK --provider:segmentation_id $PROVIDER_SEGMENTATION_ID --shared --router:external
neutron subnet-create --gateway 10.0.0.1 --allocation-pool start=10.0.0.100,end=10.0.0.150 --dns-nameserver 8.8.8.8 --name provider1-subnet provider1 10.0.0.0/24
neutron subnet-update provider1-subnet --disable-dhcp

# test networks
neutron net-create private-no-dhcp-1
neutron net-delete private-no-dhcp-1
neutron net-create private-no-dhcp-1
neutron net-create private-no-dhcp-2
neutron net-create private-no-dhcp-3
neutron subnet-create --disable-dhcp private-no-dhcp-1 192.168.100.0/24
neutron subnet-create --disable-dhcp private-no-dhcp-1 192.168.101.0/24
neutron subnet-create --disable-dhcp private-no-dhcp-1 192.168.102.0/24
neutron subnet-create --disable-dhcp private-no-dhcp-2 192.168.200.0/24
neutron subnet-create --disable-dhcp --ip-version 6 private-no-dhcp-2 2000:192:168:200::/64
neutron subnet-create --disable-dhcp --ip-version 6 private-no-dhcp-2 2000:192:168:201::/64
neutron subnet-create --disable-dhcp --ip-version 6 --gateway 2000:192:168:202::1 private-no-dhcp-3 2000:192:168:202::/64
neutr...

Revision history for this message
Andreas Karis (akaris) wrote :

One issue that I have with this still is the dup default route generation for IPv6:

[root@rhel-cloud-init-patch-test network-scripts]# ip r
default via 192.168.200.1 dev eth2
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.106
169.254.0.0/16 dev eth0 scope link metric 1002
169.254.0.0/16 dev eth1 scope link metric 1003
169.254.0.0/16 dev eth2 scope link metric 1004
169.254.0.0/16 dev eth3 scope link metric 1005
192.168.101.0/24 dev eth1 proto kernel scope link src 192.168.101.7
192.168.200.0/24 dev eth2 proto kernel scope link src 192.168.200.2
[root@rhel-cloud-init-patch-test network-scripts]# ip -6 r
unreachable ::/96 dev lo metric 1024 error -113
unreachable ::ffff:0.0.0.0/96 dev lo metric 1024 error -113
2000:192:168:201::/64 dev eth2 proto kernel metric 256
2000:192:168:204::/64 dev eth3 proto kernel metric 256
unreachable 2002:a00::/24 dev lo metric 1024 error -113
unreachable 2002:7f00::/24 dev lo metric 1024 error -113
unreachable 2002:a9fe::/32 dev lo metric 1024 error -113
unreachable 2002:ac10::/28 dev lo metric 1024 error -113
unreachable 2002:c0a8::/32 dev lo metric 1024 error -113
unreachable 2002:e000::/19 dev lo metric 1024 error -113
unreachable 3ffe:ffff::/32 dev lo metric 1024 error -113
fe80::/64 dev eth0 proto kernel metric 256
fe80::/64 dev eth1 proto kernel metric 256
fe80::/64 dev eth2 proto kernel metric 256
fe80::/64 dev eth3 proto kernel metric 256
default via 2000:192:168:201::1 dev eth2 metric 1
default via 2000:192:168:201::1 dev eth2 metric 1024

This could be easy to work around. However, it was never taken into account before that default routes should not be added in the route- and route6- files before, so I'm wondering if this should go into a different fix instead of overloading this one.

Revision history for this message
Scott Moser (smoser) wrote :

Akaris, I'm fine to have another subsequent fix to pick up your issue in the last comment.
I'm interested in knowing though if this works for rhel 5 and rhel 6.
I dont want to regress those platforms.

Were you able to verify that ?
Scott

Revision history for this message
Andreas Karis (akaris) wrote :

RHEL 6:
~~~
# make sure to have rhel-guest-image-6.10-23.x86_64.qcow2
# make sure to have python-crypto-2.6.1-1.el6ost.x86_64.rpm
# make sure to have python-oauthlib-0.6.0-3.el6ost.noarch.rpm
virt-customize -a rhel-guest-image-6.10-23.x86_64.qcow2 --root-password password:Redhat01
for i in *el6ost*.rpm;do virt-customize -a rhel6-cloud-init.patch.qcow2 --upload $i:/root/$i ; done
virt-customize -a rhel6-cloud-init.patch.qcow2 -v --run-command 'yum -y localinstall /root/*.rpm'
for i in cloudinit.0.7.9.patch.tar.gz ; do virt-customize -a rhel6-cloud-init.patch.qcow2 --upload $i:/root/$i; done
virt-customize -a rhel6-cloud-init.patch.qcow2 -v --run-command 'rm -Rf /usr/lib/python2.6/site-packages/cloudinit ; tar -xzf /root/cloudinit.0.7.9.patch.tar.gz -C /usr/lib/python2.6/site-packages/'
glance image-create --name rhel6-cloud-init --file rhel6-cloud-init.patch.qcow2 --container-format bare --disk-format qcow2 --progress
nova boot --nic net-id=$NETID1 --nic net-id=$NETID2 --nic net-id=$NETID3 --nic net-id=$NETID4 --image rhel6-cloud-init --flavor m1.small --key-name id_rsa rhel6-cloud-init
~~~

Unfortunately, for RHEL 6, this is not working.

Revision history for this message
Andreas Karis (akaris) wrote :

* not working in the sense that: cloud-init is not running for me, my test is flawed, and so far I cannot test this. I'll check with lars about how we can test this.

Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

Just chiming in to say that this seems to work great under RHEL 7 for both plain ipv4 and mixed ipv4/ipv6 environments, with config drive or without. I haven't tested under EL6.

Revision history for this message
Scott Moser (smoser) wrote :

Thanks for the input.
We can't break rhel/centos 5 and 6 though.

On May 22, 2017 5:12:13 PM EDT, Lars Kellogg-Stedman <email address hidden> wrote:
>Just chiming in to say that this seems to work great under RHEL 7 for
>both plain ipv4 and mixed ipv4/ipv6 environments, with config drive or
>without. I haven't tested under EL6.
>--
>https://code.launchpad.net/~akaris/cloud-init/+git/cloud-init/+merge/324196
>You are reviewing the proposed merge of ~akaris/cloud-init:bug1679817-c
>into cloud-init:master.

Revision history for this message
Andreas Karis (akaris) wrote :

Hi,

I think we can at least forget about CentOS/RHEL 5:
https://wiki.centos.org/FAQ/General#head-fe8a0be91ee3e7dea812e8694491e1dde5b75e6d
https://access.redhat.com/support/policy/updates/errata

They went both end of production phase 3 at March 31, 2017

Remains testing for 6 to do

- Andreas

Revision history for this message
Scott Moser (smoser) wrote :

Andreas,
thanks for the RHEL 5 info, II agree.

Revision history for this message
Lars Kellogg-Stedman (larsks) wrote :

I have tested this under RHEL 6 and it seems to work just fine. It produces a more correct network configuration than does the current master, and it runs without producing any errors.

Setup for testing:

- booted with a recent rhel-guest-image
- removed cloud-init via 'yum -y remove cloud-init'
- cloned cloud-init repository from https://git.launchpad.net/~akaris/cloud-init and checked out the "bug1679817-c" branch
- installed via "python setup.py install --init-system sysvinit"

Testing:

- rm -rf /etc/sysconfig/network-scripts/ifcfg-eth* /etc/resolv.conf /var/lib/cloud/* /var/log/cloud*
- cloud-init init --local
- cloud-init init

At this point, /etc/resolv.conf and /etc/sysconfig/network-scripts/ifcfg-eth0 are generated and correct, and interfaces are up and configured.

As akaris indicated, I don't think it makes sense to worry about RHEL5 given its current lifecycle stage.

Revision history for this message
Scott Moser (smoser) wrote :

Thanks. I'll pull this.

review: Approve
Revision history for this message
Server Team CI bot (server-team-bot) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Scott Moser (smoser) wrote : Posted in a previous version of this proposal

Hi,
I'm marking this as 'merged' based on the fact that the new merge proposal *is* merged.
(https://code.launchpad.net/~akaris/cloud-init/+git/cloud-init/+merge/324196)
Please move back to 'Needs Review' (and explain) if you think otherwise.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
2index d981277..58c5713 100644
3--- a/cloudinit/net/sysconfig.py
4+++ b/cloudinit/net/sysconfig.py
5@@ -59,6 +59,9 @@ class ConfigMap(object):
6 def __setitem__(self, key, value):
7 self._conf[key] = value
8
9+ def __getitem__(self, key):
10+ return self._conf[key]
11+
12 def drop(self, key):
13 self._conf.pop(key, None)
14
15@@ -83,7 +86,8 @@ class ConfigMap(object):
16 class Route(ConfigMap):
17 """Represents a route configuration."""
18
19- route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
20+ route_fn_tpl_ipv4 = '%(base)s/network-scripts/route-%(name)s'
21+ route_fn_tpl_ipv6 = '%(base)s/network-scripts/route6-%(name)s'
22
23 def __init__(self, route_name, base_sysconf_dir):
24 super(Route, self).__init__()
25@@ -102,9 +106,58 @@ class Route(ConfigMap):
26 return r
27
28 @property
29- def path(self):
30- return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
31- 'name': self._route_name})
32+ def path_ipv4(self):
33+ return self.route_fn_tpl_ipv4 % ({'base': self._base_sysconf_dir,
34+ 'name': self._route_name})
35+
36+ @property
37+ def path_ipv6(self):
38+ return self.route_fn_tpl_ipv6 % ({'base': self._base_sysconf_dir,
39+ 'name': self._route_name})
40+
41+ def is_ipv6_route(self, address):
42+ return ':' in address
43+
44+ def to_string(self, proto="ipv4"):
45+ # only accept ipv4 and ipv6
46+ if proto not in ['ipv4', 'ipv6']:
47+ raise ValueError("Unknown protocol '%s'" % (str(proto)))
48+ buf = six.StringIO()
49+ buf.write(_make_header())
50+ if self._conf:
51+ buf.write("\n")
52+ # need to reindex IPv4 addresses
53+ # (because Route can contain a mix of IPv4 and IPv6)
54+ reindex = -1
55+ for key in sorted(self._conf.keys()):
56+ if 'ADDRESS' in key:
57+ index = key.replace('ADDRESS', '')
58+ address_value = str(self._conf[key])
59+ # only accept combinations:
60+ # if proto ipv6 only display ipv6 routes
61+ # if proto ipv4 only display ipv4 routes
62+ # do not add ipv6 routes if proto is ipv4
63+ # do not add ipv4 routes if proto is ipv6
64+ # (this array will contain a mix of ipv4 and ipv6)
65+ if proto == "ipv4" and not self.is_ipv6_route(address_value):
66+ netmask_value = str(self._conf['NETMASK' + index])
67+ gateway_value = str(self._conf['GATEWAY' + index])
68+ # increase IPv4 index
69+ reindex = reindex + 1
70+ buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
71+ _quote_value(address_value)))
72+ buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
73+ _quote_value(gateway_value)))
74+ buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
75+ _quote_value(netmask_value)))
76+ elif proto == "ipv6" and self.is_ipv6_route(address_value):
77+ netmask_value = str(self._conf['NETMASK' + index])
78+ gateway_value = str(self._conf['GATEWAY' + index])
79+ buf.write("%s/%s via %s\n" % (address_value,
80+ netmask_value,
81+ gateway_value))
82+
83+ return buf.getvalue()
84
85
86 class NetInterface(ConfigMap):
87@@ -211,65 +264,119 @@ class Renderer(renderer.Renderer):
88 iface_cfg[new_key] = old_value
89
90 @classmethod
91- def _render_subnet(cls, iface_cfg, route_cfg, subnet):
92- subnet_type = subnet.get('type')
93- if subnet_type == 'dhcp6':
94- iface_cfg['DHCPV6C'] = True
95- iface_cfg['IPV6INIT'] = True
96- iface_cfg['BOOTPROTO'] = 'dhcp'
97- elif subnet_type in ['dhcp4', 'dhcp']:
98- iface_cfg['BOOTPROTO'] = 'dhcp'
99- elif subnet_type == 'static':
100- iface_cfg['BOOTPROTO'] = 'static'
101- if subnet_is_ipv6(subnet):
102- iface_cfg['IPV6ADDR'] = subnet['address']
103+ def _render_subnets(cls, iface_cfg, subnets):
104+ # setting base values
105+ iface_cfg['BOOTPROTO'] = 'none'
106+
107+ # modifying base values according to subnets
108+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
109+ subnet_type = subnet.get('type')
110+ if subnet_type == 'dhcp6':
111 iface_cfg['IPV6INIT'] = True
112+ iface_cfg['DHCPV6C'] = True
113+ iface_cfg['BOOTPROTO'] = 'dhcp'
114+ elif subnet_type in ['dhcp4', 'dhcp']:
115+ iface_cfg['BOOTPROTO'] = 'dhcp'
116+ elif subnet_type == 'static':
117+ # grep BOOTPROTO sysconfig.txt -A2 | head -3
118+ # BOOTPROTO=none|bootp|dhcp
119+ # 'bootp' or 'dhcp' cause a DHCP client
120+ # to run on the device. Any other
121+ # value causes any static configuration
122+ # in the file to be applied.
123+ # ==> the following should not be set to 'static'
124+ # but should remain 'none'
125+ # if iface_cfg['BOOTPROTO'] == 'none':
126+ # iface_cfg['BOOTPROTO'] = 'static'
127+ if subnet_is_ipv6(subnet):
128+ iface_cfg['IPV6INIT'] = True
129 else:
130- iface_cfg['IPADDR'] = subnet['address']
131- else:
132- raise ValueError("Unknown subnet type '%s' found"
133- " for interface '%s'" % (subnet_type,
134- iface_cfg.name))
135- if 'netmask' in subnet:
136- iface_cfg['NETMASK'] = subnet['netmask']
137- is_ipv6 = subnet.get('ipv6')
138- for route in subnet.get('routes', []):
139- if _is_default_route(route):
140- if (
141- (subnet.get('ipv4') and
142- route_cfg.has_set_default_ipv4) or
143- (subnet.get('ipv6') and
144- route_cfg.has_set_default_ipv6)
145- ):
146- raise ValueError("Duplicate declaration of default "
147- "route found for interface '%s'"
148- % (iface_cfg.name))
149- # NOTE(harlowja): ipv6 and ipv4 default gateways
150- gw_key = 'GATEWAY0'
151- nm_key = 'NETMASK0'
152- addr_key = 'ADDRESS0'
153- # The owning interface provides the default route.
154- #
155- # TODO(harlowja): add validation that no other iface has
156- # also provided the default route?
157- iface_cfg['DEFROUTE'] = True
158- if 'gateway' in route:
159- if is_ipv6:
160- iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
161- route_cfg.has_set_default_ipv6 = True
162+ raise ValueError("Unknown subnet type '%s' found"
163+ " for interface '%s'" % (subnet_type,
164+ iface_cfg.name))
165+
166+ # set IPv4 and IPv6 static addresses
167+ ipv4_index = -1
168+ ipv6_index = -1
169+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
170+ subnet_type = subnet.get('type')
171+ if subnet_type == 'dhcp6':
172+ continue
173+ elif subnet_type in ['dhcp4', 'dhcp']:
174+ continue
175+ elif subnet_type == 'static':
176+ if subnet_is_ipv6(subnet):
177+ ipv6_index = ipv6_index + 1
178+ if 'netmask' in subnet and str(subnet['netmask']) != "":
179+ ipv6_cidr = (subnet['address'] +
180+ '/' +
181+ str(subnet['netmask']))
182 else:
183- iface_cfg['GATEWAY'] = route['gateway']
184- route_cfg.has_set_default_ipv4 = True
185- else:
186- gw_key = 'GATEWAY%s' % route_cfg.last_idx
187- nm_key = 'NETMASK%s' % route_cfg.last_idx
188- addr_key = 'ADDRESS%s' % route_cfg.last_idx
189- route_cfg.last_idx += 1
190- for (old_key, new_key) in [('gateway', gw_key),
191- ('netmask', nm_key),
192- ('network', addr_key)]:
193- if old_key in route:
194- route_cfg[new_key] = route[old_key]
195+ ipv6_cidr = subnet['address']
196+ if ipv6_index == 0:
197+ iface_cfg['IPV6ADDR'] = ipv6_cidr
198+ elif ipv6_index == 1:
199+ iface_cfg['IPV6ADDR_SECONDARIES'] = ipv6_cidr
200+ else:
201+ iface_cfg['IPV6ADDR_SECONDARIES'] = (
202+ iface_cfg['IPV6ADDR_SECONDARIES'] +
203+ " " + ipv6_cidr)
204+ else:
205+ ipv4_index = ipv4_index + 1
206+ if ipv4_index == 0:
207+ iface_cfg['IPADDR'] = subnet['address']
208+ if 'netmask' in subnet:
209+ iface_cfg['NETMASK'] = subnet['netmask']
210+ else:
211+ iface_cfg['IPADDR' + str(ipv4_index)] = \
212+ subnet['address']
213+ if 'netmask' in subnet:
214+ iface_cfg['NETMASK' + str(ipv4_index)] = \
215+ subnet['netmask']
216+
217+ @classmethod
218+ def _render_subnet_routes(cls, iface_cfg, route_cfg, subnets):
219+ for i, subnet in enumerate(subnets, start=len(iface_cfg.children)):
220+ for route in subnet.get('routes', []):
221+ is_ipv6 = subnet.get('ipv6')
222+
223+ if _is_default_route(route):
224+ if (
225+ (subnet.get('ipv4') and
226+ route_cfg.has_set_default_ipv4) or
227+ (subnet.get('ipv6') and
228+ route_cfg.has_set_default_ipv6)
229+ ):
230+ raise ValueError("Duplicate declaration of default "
231+ "route found for interface '%s'"
232+ % (iface_cfg.name))
233+ # NOTE(harlowja): ipv6 and ipv4 default gateways
234+ gw_key = 'GATEWAY0'
235+ nm_key = 'NETMASK0'
236+ addr_key = 'ADDRESS0'
237+ # The owning interface provides the default route.
238+ #
239+ # TODO(harlowja): add validation that no other iface has
240+ # also provided the default route?
241+ iface_cfg['DEFROUTE'] = True
242+ if 'gateway' in route:
243+ if is_ipv6:
244+ iface_cfg['IPV6_DEFAULTGW'] = route['gateway']
245+ route_cfg.has_set_default_ipv6 = True
246+ else:
247+ iface_cfg['GATEWAY'] = route['gateway']
248+ route_cfg.has_set_default_ipv4 = True
249+
250+ else:
251+ gw_key = 'GATEWAY%s' % route_cfg.last_idx
252+ nm_key = 'NETMASK%s' % route_cfg.last_idx
253+ addr_key = 'ADDRESS%s' % route_cfg.last_idx
254+ route_cfg.last_idx += 1
255+ for (old_key, new_key) in [('gateway', gw_key),
256+ ('netmask', nm_key),
257+ ('network', addr_key)]:
258+ if old_key in route:
259+ route_cfg[new_key] = route[old_key]
260
261 @classmethod
262 def _render_bonding_opts(cls, iface_cfg, iface):
263@@ -295,15 +402,9 @@ class Renderer(renderer.Renderer):
264 iface_subnets = iface.get("subnets", [])
265 iface_cfg = iface_contents[iface_name]
266 route_cfg = iface_cfg.routes
267- if len(iface_subnets) == 1:
268- cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
269- elif len(iface_subnets) > 1:
270- for i, isubnet in enumerate(iface_subnets,
271- start=len(iface_cfg.children)):
272- iface_sub_cfg = iface_cfg.copy()
273- iface_sub_cfg.name = "%s:%s" % (iface_name, i)
274- iface_cfg.children.append(iface_sub_cfg)
275- cls._render_subnet(iface_sub_cfg, route_cfg, isubnet)
276+
277+ cls._render_subnets(iface_cfg, iface_subnets)
278+ cls._render_subnet_routes(iface_cfg, route_cfg, iface_subnets)
279
280 @classmethod
281 def _render_bond_interfaces(cls, network_state, iface_contents):
282@@ -387,7 +488,10 @@ class Renderer(renderer.Renderer):
283 if iface_cfg:
284 contents[iface_cfg.path] = iface_cfg.to_string()
285 if iface_cfg.routes:
286- contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
287+ contents[iface_cfg.routes.path_ipv4] = \
288+ iface_cfg.routes.to_string("ipv4")
289+ contents[iface_cfg.routes.path_ipv6] = \
290+ iface_cfg.routes.to_string("ipv6")
291 return contents
292
293 def render_network_state(self, network_state, target=None):
294diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py
295index 1e10a33..fd7c051 100644
296--- a/tests/unittests/test_distros/test_netconfig.py
297+++ b/tests/unittests/test_distros/test_netconfig.py
298@@ -476,7 +476,7 @@ NETWORKING=yes
299 expected_buf = '''
300 # Created by cloud-init on instance boot automatically, do not edit.
301 #
302-BOOTPROTO=static
303+BOOTPROTO=none
304 DEVICE=eth0
305 IPADDR=192.168.1.5
306 NETMASK=255.255.255.0
307@@ -533,7 +533,6 @@ NETWORKING=yes
308 mock.patch.object(util, 'load_file', return_value=''))
309 mocks.enter_context(
310 mock.patch.object(os.path, 'isfile', return_value=False))
311-
312 rh_distro.apply_network(BASE_NET_CFG_IPV6, False)
313
314 self.assertEqual(len(write_bufs), 4)
315@@ -626,11 +625,10 @@ IPV6_AUTOCONF=no
316 expected_buf = '''
317 # Created by cloud-init on instance boot automatically, do not edit.
318 #
319-BOOTPROTO=static
320+BOOTPROTO=none
321 DEVICE=eth0
322-IPV6ADDR=2607:f0d0:1002:0011::2
323+IPV6ADDR=2607:f0d0:1002:0011::2/64
324 IPV6INIT=yes
325-NETMASK=64
326 NM_CONTROLLED=no
327 ONBOOT=yes
328 TYPE=Ethernet
329diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
330index d36d0e7..cf4cedc 100644
331--- a/tests/unittests/test_net.py
332+++ b/tests/unittests/test_net.py
333@@ -137,7 +137,7 @@ OS_SAMPLES = [
334 """
335 # Created by cloud-init on instance boot automatically, do not edit.
336 #
337-BOOTPROTO=static
338+BOOTPROTO=none
339 DEFROUTE=yes
340 DEVICE=eth0
341 GATEWAY=172.19.3.254
342@@ -205,38 +205,14 @@ nameserver 172.19.0.12
343 # Created by cloud-init on instance boot automatically, do not edit.
344 #
345 BOOTPROTO=none
346-DEVICE=eth0
347-HWADDR=fa:16:3e:ed:9a:59
348-NM_CONTROLLED=no
349-ONBOOT=yes
350-TYPE=Ethernet
351-USERCTL=no
352-""".lstrip()),
353- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
354- """
355-# Created by cloud-init on instance boot automatically, do not edit.
356-#
357-BOOTPROTO=static
358 DEFROUTE=yes
359-DEVICE=eth0:0
360+DEVICE=eth0
361 GATEWAY=172.19.3.254
362 HWADDR=fa:16:3e:ed:9a:59
363 IPADDR=172.19.1.34
364+IPADDR1=10.0.0.10
365 NETMASK=255.255.252.0
366-NM_CONTROLLED=no
367-ONBOOT=yes
368-TYPE=Ethernet
369-USERCTL=no
370-""".lstrip()),
371- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
372- """
373-# Created by cloud-init on instance boot automatically, do not edit.
374-#
375-BOOTPROTO=static
376-DEVICE=eth0:1
377-HWADDR=fa:16:3e:ed:9a:59
378-IPADDR=10.0.0.10
379-NETMASK=255.255.255.0
380+NETMASK1=255.255.255.0
381 NM_CONTROLLED=no
382 ONBOOT=yes
383 TYPE=Ethernet
384@@ -266,7 +242,7 @@ nameserver 172.19.0.12
385 }],
386 "ip_address": "172.19.1.34", "id": "network0"
387 }, {
388- "network_id": "public-ipv6",
389+ "network_id": "public-ipv6-a",
390 "type": "ipv6", "netmask": "",
391 "link": "tap1a81968a-79",
392 "routes": [
393@@ -277,6 +253,20 @@ nameserver 172.19.0.12
394 }
395 ],
396 "ip_address": "2001:DB8::10", "id": "network1"
397+ }, {
398+ "network_id": "public-ipv6-b",
399+ "type": "ipv6", "netmask": "64",
400+ "link": "tap1a81968a-79",
401+ "routes": [
402+ ],
403+ "ip_address": "2001:DB9::10", "id": "network2"
404+ }, {
405+ "network_id": "public-ipv6-c",
406+ "type": "ipv6", "netmask": "64",
407+ "link": "tap1a81968a-79",
408+ "routes": [
409+ ],
410+ "ip_address": "2001:DB10::10", "id": "network3"
411 }],
412 "links": [
413 {
414@@ -296,41 +286,16 @@ nameserver 172.19.0.12
415 # Created by cloud-init on instance boot automatically, do not edit.
416 #
417 BOOTPROTO=none
418-DEVICE=eth0
419-HWADDR=fa:16:3e:ed:9a:59
420-NM_CONTROLLED=no
421-ONBOOT=yes
422-TYPE=Ethernet
423-USERCTL=no
424-""".lstrip()),
425- ('etc/sysconfig/network-scripts/ifcfg-eth0:0',
426- """
427-# Created by cloud-init on instance boot automatically, do not edit.
428-#
429-BOOTPROTO=static
430 DEFROUTE=yes
431-DEVICE=eth0:0
432+DEVICE=eth0
433 GATEWAY=172.19.3.254
434 HWADDR=fa:16:3e:ed:9a:59
435 IPADDR=172.19.1.34
436-NETMASK=255.255.252.0
437-NM_CONTROLLED=no
438-ONBOOT=yes
439-TYPE=Ethernet
440-USERCTL=no
441-""".lstrip()),
442- ('etc/sysconfig/network-scripts/ifcfg-eth0:1',
443- """
444-# Created by cloud-init on instance boot automatically, do not edit.
445-#
446-BOOTPROTO=static
447-DEFROUTE=yes
448-DEVICE=eth0:1
449-HWADDR=fa:16:3e:ed:9a:59
450 IPV6ADDR=2001:DB8::10
451+IPV6ADDR_SECONDARIES="2001:DB9::10/64 2001:DB10::10/64"
452 IPV6INIT=yes
453 IPV6_DEFAULTGW=2001:DB8::1
454-NETMASK=
455+NETMASK=255.255.252.0
456 NM_CONTROLLED=no
457 ONBOOT=yes
458 TYPE=Ethernet

Subscribers

People subscribed via source and target branches