3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License version 3, as
5
# published by the Free Software Foundation.
7
# This program is distributed in the hope that it will be useful,
8
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10
# GNU General Public License for more details.
12
# You should have received a copy of the GNU General Public License
13
# along with this program. If not, see <http://www.gnu.org/licenses/>.
20
from cloudinit.distros.parsers import resolv_conf
21
from cloudinit import util
23
from . import renderer
26
def _make_header(sep='#'):
28
"Created by cloud-init on instance boot automatically, do not edit.",
31
for i in range(0, len(lines)):
33
lines[i] = sep + " " + lines[i]
36
return "\n".join(lines)
39
def _is_default_route(route):
40
if route['network'] == '::' and route['netmask'] == 0:
42
if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
47
def _quote_value(value):
48
if re.search(r"\s", value):
49
# This doesn't handle complex cases...
50
if value.startswith('"') and value.endswith('"'):
58
class ConfigMap(object):
59
"""Sysconfig like dictionary object."""
61
# Why does redhat prefer yes/no to true/false??
70
def __setitem__(self, key, value):
71
self._conf[key] = value
74
self._conf.pop(key, None)
77
return len(self._conf)
81
buf.write(_make_header())
84
for key in sorted(self._conf.keys()):
85
value = self._conf[key]
86
if isinstance(value, bool):
87
value = self._bool_map[value]
88
if not isinstance(value, six.string_types):
90
buf.write("%s=%s\n" % (key, _quote_value(value)))
94
class Route(ConfigMap):
95
"""Represents a route configuration."""
97
route_fn_tpl = '%(base)s/network-scripts/route-%(name)s'
99
def __init__(self, route_name, base_sysconf_dir):
100
super(Route, self).__init__()
102
self.has_set_default = False
103
self._route_name = route_name
104
self._base_sysconf_dir = base_sysconf_dir
107
r = Route(self._route_name, self._base_sysconf_dir)
108
r._conf = self._conf.copy()
109
r.last_idx = self.last_idx
110
r.has_set_default = self.has_set_default
115
return self.route_fn_tpl % ({'base': self._base_sysconf_dir,
116
'name': self._route_name})
119
class NetInterface(ConfigMap):
120
"""Represents a sysconfig/networking-script (and its config + children)."""
122
iface_fn_tpl = '%(base)s/network-scripts/ifcfg-%(name)s'
125
'ethernet': 'Ethernet',
130
def __init__(self, iface_name, base_sysconf_dir, kind='ethernet'):
131
super(NetInterface, self).__init__()
133
self.routes = Route(iface_name, base_sysconf_dir)
135
self._iface_name = iface_name
136
self._conf['DEVICE'] = iface_name
137
self._conf['TYPE'] = self.iface_types[kind]
138
self._base_sysconf_dir = base_sysconf_dir
142
return self._iface_name
145
def name(self, iface_name):
146
self._iface_name = iface_name
147
self._conf['DEVICE'] = iface_name
154
def kind(self, kind):
156
self._conf['TYPE'] = self.iface_types[kind]
160
return self.iface_fn_tpl % ({'base': self._base_sysconf_dir,
163
def copy(self, copy_children=False, copy_routes=False):
164
c = NetInterface(self.name, self._base_sysconf_dir, kind=self._kind)
165
c._conf = self._conf.copy()
167
c.children = list(self.children)
169
c.routes = self.routes.copy()
173
class Renderer(renderer.Renderer):
174
"""Renders network information in a /etc/sysconfig format."""
176
# See: https://access.redhat.com/documentation/en-US/\
177
# Red_Hat_Enterprise_Linux/6/html/Deployment_Guide/\
178
# s1-networkscripts-interfaces.html (or other docs for
179
# details about this)
181
iface_defaults = tuple([
184
('NM_CONTROLLED', False),
185
('BOOTPROTO', 'none'),
188
# If these keys exist, then there values will be used to form
189
# a BONDING_OPTS grouping; otherwise no grouping will be set.
190
bond_tpl_opts = tuple([
191
('bond_mode', "mode=%s"),
192
('bond_xmit_hash_policy', "xmit_hash_policy=%s"),
193
('bond_miimon', "miimon=%s"),
196
bridge_opts_keys = tuple([
197
('bridge_stp', 'STP'),
198
('bridge_ageing', 'AGEING'),
199
('bridge_bridgeprio', 'PRIO'),
202
def __init__(self, config=None):
205
self.sysconf_dir = config.get('sysconf_dir', 'etc/sysconfig/')
206
self.netrules_path = config.get(
207
'netrules_path', 'etc/udev/rules.d/70-persistent-net.rules')
208
self.dns_path = config.get('dns_path', 'etc/resolv.conf')
211
def _render_iface_shared(cls, iface, iface_cfg):
212
for k, v in cls.iface_defaults:
214
for (old_key, new_key) in [('mac_address', 'HWADDR'), ('mtu', 'MTU')]:
215
old_value = iface.get(old_key)
216
if old_value is not None:
217
iface_cfg[new_key] = old_value
220
def _render_subnet(cls, iface_cfg, route_cfg, subnet):
221
subnet_type = subnet.get('type')
222
if subnet_type == 'dhcp6':
223
iface_cfg['DHCPV6C'] = True
224
iface_cfg['IPV6INIT'] = True
225
iface_cfg['BOOTPROTO'] = 'dhcp'
226
elif subnet_type in ['dhcp4', 'dhcp']:
227
iface_cfg['BOOTPROTO'] = 'dhcp'
228
elif subnet_type == 'static':
229
iface_cfg['BOOTPROTO'] = 'static'
230
if subnet.get('ipv6'):
231
iface_cfg['IPV6ADDR'] = subnet['address']
232
iface_cfg['IPV6INIT'] = True
234
iface_cfg['IPADDR'] = subnet['address']
236
raise ValueError("Unknown subnet type '%s' found"
237
" for interface '%s'" % (subnet_type,
239
if 'netmask' in subnet:
240
iface_cfg['NETMASK'] = subnet['netmask']
241
for route in subnet.get('routes', []):
242
if _is_default_route(route):
243
if route_cfg.has_set_default:
244
raise ValueError("Duplicate declaration of default"
245
" route found for interface '%s'"
247
# NOTE(harlowja): ipv6 and ipv4 default gateways
250
addr_key = 'ADDRESS0'
251
# The owning interface provides the default route.
253
# TODO(harlowja): add validation that no other iface has
254
# also provided the default route?
255
iface_cfg['DEFROUTE'] = True
256
if 'gateway' in route:
257
iface_cfg['GATEWAY'] = route['gateway']
258
route_cfg.has_set_default = True
260
gw_key = 'GATEWAY%s' % route_cfg.last_idx
261
nm_key = 'NETMASK%s' % route_cfg.last_idx
262
addr_key = 'ADDRESS%s' % route_cfg.last_idx
263
route_cfg.last_idx += 1
264
for (old_key, new_key) in [('gateway', gw_key),
266
('network', addr_key)]:
268
route_cfg[new_key] = route[old_key]
271
def _render_bonding_opts(cls, iface_cfg, iface):
273
for (bond_key, value_tpl) in cls.bond_tpl_opts:
274
# Seems like either dash or underscore is possible?
275
bond_keys = [bond_key, bond_key.replace("_", "-")]
276
for bond_key in bond_keys:
277
if bond_key in iface:
278
bond_value = iface[bond_key]
279
if isinstance(bond_value, (tuple, list)):
280
bond_value = " ".join(bond_value)
281
bond_opts.append(value_tpl % (bond_value))
284
iface_cfg['BONDING_OPTS'] = " ".join(bond_opts)
287
def _render_physical_interfaces(cls, network_state, iface_contents):
288
physical_filter = renderer.filter_by_physical
289
for iface in network_state.iter_interfaces(physical_filter):
290
iface_name = iface['name']
291
iface_subnets = iface.get("subnets", [])
292
iface_cfg = iface_contents[iface_name]
293
route_cfg = iface_cfg.routes
294
if len(iface_subnets) == 1:
295
cls._render_subnet(iface_cfg, route_cfg, iface_subnets[0])
296
elif len(iface_subnets) > 1:
297
for i, iface_subnet in enumerate(iface_subnets,
298
start=len(iface.children)):
299
iface_sub_cfg = iface_cfg.copy()
300
iface_sub_cfg.name = "%s:%s" % (iface_name, i)
301
iface.children.append(iface_sub_cfg)
302
cls._render_subnet(iface_sub_cfg, route_cfg, iface_subnet)
305
def _render_bond_interfaces(cls, network_state, iface_contents):
306
bond_filter = renderer.filter_by_type('bond')
307
for iface in network_state.iter_interfaces(bond_filter):
308
iface_name = iface['name']
309
iface_cfg = iface_contents[iface_name]
310
cls._render_bonding_opts(iface_cfg, iface)
311
iface_master_name = iface['bond-master']
312
iface_cfg['MASTER'] = iface_master_name
313
iface_cfg['SLAVE'] = True
314
# Ensure that the master interface (and any of its children)
315
# are actually marked as being bond types...
316
master_cfg = iface_contents[iface_master_name]
317
master_cfgs = [master_cfg]
318
master_cfgs.extend(master_cfg.children)
319
for master_cfg in master_cfgs:
320
master_cfg['BONDING_MASTER'] = True
321
master_cfg.kind = 'bond'
324
def _render_vlan_interfaces(network_state, iface_contents):
325
vlan_filter = renderer.filter_by_type('vlan')
326
for iface in network_state.iter_interfaces(vlan_filter):
327
iface_name = iface['name']
328
iface_cfg = iface_contents[iface_name]
329
iface_cfg['VLAN'] = True
330
iface_cfg['PHYSDEV'] = iface_name[:iface_name.rfind('.')]
333
def _render_dns(network_state, existing_dns_path=None):
334
content = resolv_conf.ResolvConf("")
335
if existing_dns_path and os.path.isfile(existing_dns_path):
336
content = resolv_conf.ResolvConf(util.load_file(existing_dns_path))
337
for nameserver in network_state.dns_nameservers:
338
content.add_nameserver(nameserver)
339
for searchdomain in network_state.dns_searchdomains:
340
content.add_search_domain(searchdomain)
341
return "\n".join([_make_header(';'), str(content)])
344
def _render_bridge_interfaces(cls, network_state, iface_contents):
345
bridge_filter = renderer.filter_by_type('bridge')
346
for iface in network_state.iter_interfaces(bridge_filter):
347
iface_name = iface['name']
348
iface_cfg = iface_contents[iface_name]
349
iface_cfg.kind = 'bridge'
350
for old_key, new_key in cls.bridge_opts_keys:
352
iface_cfg[new_key] = iface[old_key]
353
# Is this the right key to get all the connected interfaces?
354
for bridged_iface_name in iface.get('bridge_ports', []):
355
# Ensure all bridged interfaces are correctly tagged
356
# as being bridged to this interface.
357
bridged_cfg = iface_contents[bridged_iface_name]
358
bridged_cfgs = [bridged_cfg]
359
bridged_cfgs.extend(bridged_cfg.children)
360
for bridge_cfg in bridged_cfgs:
361
bridge_cfg['BRIDGE'] = iface_name
364
def _render_sysconfig(cls, base_sysconf_dir, network_state):
365
'''Given state, return /etc/sysconfig files + contents'''
367
for iface in network_state.iter_interfaces():
368
iface_name = iface['name']
369
iface_cfg = NetInterface(iface_name, base_sysconf_dir)
370
cls._render_iface_shared(iface, iface_cfg)
371
iface_contents[iface_name] = iface_cfg
372
cls._render_physical_interfaces(network_state, iface_contents)
373
cls._render_bond_interfaces(network_state, iface_contents)
374
cls._render_vlan_interfaces(network_state, iface_contents)
375
cls._render_bridge_interfaces(network_state, iface_contents)
377
for iface_name, iface_cfg in iface_contents.items():
378
if iface_cfg or iface_cfg.children:
379
contents[iface_cfg.path] = iface_cfg.to_string()
380
for iface_cfg in iface_cfg.children:
382
contents[iface_cfg.path] = iface_cfg.to_string()
384
contents[iface_cfg.routes.path] = iface_cfg.routes.to_string()
387
def render_network_state(self, target, network_state):
388
base_sysconf_dir = os.path.join(target, self.sysconf_dir)
389
for path, data in self._render_sysconfig(base_sysconf_dir,
390
network_state).items():
391
util.write_file(path, data)
393
dns_path = os.path.join(target, self.dns_path)
394
resolv_content = self._render_dns(network_state,
395
existing_dns_path=dns_path)
396
util.write_file(dns_path, resolv_content)
397
if self.netrules_path:
398
netrules_content = self._render_persistent_net(network_state)
399
netrules_path = os.path.join(target, self.netrules_path)
400
util.write_file(netrules_path, netrules_content)