1500 lines
59 KiB
Python
1500 lines
59 KiB
Python
#!/usr/bin/python
|
|
# Copyright (c) 2015 Monty Taylor
|
|
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import argparse
|
|
import contextlib
|
|
import copy
|
|
import errno
|
|
import json
|
|
import logging
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
|
|
from glean import systemlock
|
|
from glean import utils
|
|
from glean._vendor import distro
|
|
|
|
try:
|
|
import configparser
|
|
except ImportError:
|
|
import ConfigParser as configparser
|
|
|
|
try:
|
|
from StringIO import StringIO
|
|
except ImportError:
|
|
from io import StringIO
|
|
|
|
log = logging.getLogger("glean")
|
|
|
|
|
|
# Type value for permanent mac addrs as defined by the linux kernel.
|
|
PERMANENT_ADDR_TYPE = '0'
|
|
|
|
# Global flag for selinux restore.
|
|
SELINUX_RESTORECON = '/usr/sbin/restorecon'
|
|
HAVE_SELINUX = os.path.exists(SELINUX_RESTORECON)
|
|
|
|
|
|
# Wrap open calls in this to make sure that any created or modified
|
|
# files retain their selinux context.
|
|
@contextlib.contextmanager
|
|
def safe_open(*args, **kwargs):
|
|
f = open(*args, **kwargs)
|
|
yield f
|
|
f.close()
|
|
path = os.path.abspath(f.name)
|
|
if HAVE_SELINUX:
|
|
logging.debug("Restoring selinux context for %s" % path)
|
|
subprocess.call([SELINUX_RESTORECON, path])
|
|
|
|
|
|
def _exists_rh_interface(name, distro):
|
|
file_to_check = _network_files(distro)['ifcfg'] + '-{name}'.format(
|
|
name=name
|
|
)
|
|
return os.path.exists(file_to_check)
|
|
|
|
|
|
def _is_suse(distro):
|
|
# 'distro could be any of suse, opensuse,
|
|
# opensuse-leap, opensuse-tumbleweed
|
|
return 'suse' in distro
|
|
|
|
|
|
def _network_files(distro):
|
|
network_files = {}
|
|
if _is_suse(distro):
|
|
network_files = {
|
|
"ifcfg": "/etc/sysconfig/network/ifcfg",
|
|
"route": "/etc/sysconfig/network/ifroute",
|
|
}
|
|
else:
|
|
network_files = {
|
|
"ifcfg": "/etc/sysconfig/network-scripts/ifcfg",
|
|
"route": "/etc/sysconfig/network-scripts/route",
|
|
}
|
|
|
|
return network_files
|
|
|
|
|
|
def _network_config(args):
|
|
distro = args.distro
|
|
network_config = {}
|
|
if _is_suse(distro):
|
|
header = "\n".join(["# Automatically generated, do not edit",
|
|
"BOOTPROTO={bootproto}",
|
|
"LLADDR={hwaddr}"])
|
|
footer = "STARTMODE=auto" + "\n"
|
|
|
|
network_config = {
|
|
"static": "\n".join([header,
|
|
"IPADDR={ip_address}",
|
|
"NETMASK={netmask}",
|
|
footer])
|
|
}
|
|
else:
|
|
header = "\n".join(["# Automatically generated, do not edit",
|
|
"DEVICE={name}",
|
|
"BOOTPROTO={bootproto}",
|
|
"HWADDR={hwaddr}"])
|
|
footer = "\n".join(["ONBOOT=yes",
|
|
"NM_CONTROLLED=%s" %
|
|
("yes" if args.use_nm else "no"),
|
|
"TYPE=Ethernet"]) + "\n"
|
|
|
|
network_config = {
|
|
# RedHat does not use TYPE=Ethernet in the static configurations
|
|
"static": "\n".join([header,
|
|
"IPADDR={ip_address}",
|
|
"NETMASK={netmask}",
|
|
footer.replace("TYPE=Ethernet\n", "")])
|
|
}
|
|
|
|
# RedHat does not use TYPE=Ethernet in the dhcp configurations
|
|
network_config["dhcp"] = "\n".join([header, footer])
|
|
network_config["none"] = "\n".join([header, footer])
|
|
|
|
return network_config
|
|
|
|
|
|
def _set_rh_bonding(name, interface, distro, results):
|
|
if not any(bond in ['bond_slaves', 'bond_master'] for bond in interface):
|
|
return results
|
|
|
|
# Careful, we are operating on the live 'results' variable
|
|
# so we need to always append our data
|
|
if _is_suse(distro):
|
|
# SUSE configures the slave interfaces on the master ifcfg file.
|
|
# The master interface contains a 'bond_slaves' key containing a list
|
|
# of the slave interfaces
|
|
if 'bond_slaves' in interface:
|
|
results += "BONDING_MASTER=yes\n"
|
|
slave_cnt = 0
|
|
for slave in interface['bond_slaves']:
|
|
results += "BONDING_SLAVE_{id}={name}\n".format(
|
|
id=slave_cnt, name=slave)
|
|
slave_cnt += 1
|
|
else:
|
|
# Slave interfaces do not know they are part of a bonded
|
|
# interface. All we need to do is to set the STARTMODE
|
|
# to hotplug
|
|
results = results.replace("=auto", "=hotplug")
|
|
|
|
else:
|
|
# RedHat does not add any specific configuration to the master
|
|
# interface. All configuration is done in the slave ifcfg files.
|
|
if 'bond_slaves' in interface:
|
|
return results
|
|
|
|
results += "SLAVE=yes\n"
|
|
results += "MASTER={0}\n".format(interface['bond_master'])
|
|
|
|
return results
|
|
|
|
|
|
def _set_rh_vlan(name, interface, distro):
|
|
results = ""
|
|
|
|
if 'vlan_id' not in interface:
|
|
return results
|
|
|
|
if _is_suse(distro):
|
|
results += "VLAN_ID={vlan_id}\n".format(vlan_id=interface['vlan_id'])
|
|
results += "ETHERDEVICE={etherdevice}\n".format(
|
|
etherdevice=name.split('.')[0])
|
|
else:
|
|
results += "VLAN=yes\n"
|
|
|
|
return results
|
|
|
|
|
|
def _write_rh_interface(name, interface, args):
|
|
distro = args.distro
|
|
files_to_write = dict()
|
|
results = _network_config(args)["static"].format(
|
|
bootproto="static",
|
|
name=name,
|
|
hwaddr=interface['mac_address'],
|
|
ip_address=interface['ip_address'],
|
|
netmask=interface['netmask'],
|
|
)
|
|
results += _set_rh_vlan(name, interface, distro)
|
|
# set_rh_bonding takes results as argument so we need to assign
|
|
# the return value, not append it
|
|
results = _set_rh_bonding(name, interface, distro, results)
|
|
routes = []
|
|
for route in interface['routes']:
|
|
if route['network'] == '0.0.0.0' and route['netmask'] == '0.0.0.0':
|
|
if not _is_suse(distro):
|
|
results += "DEFROUTE=yes\n"
|
|
results += "GATEWAY={gw}\n".format(gw=route['gateway'])
|
|
else:
|
|
# Special notation for default route on SUSE/wicked
|
|
routes.append(dict(
|
|
net='default', mask='', gw=route['gateway']))
|
|
else:
|
|
routes.append(dict(
|
|
net=route['network'], mask=route['netmask'],
|
|
gw=route['gateway']))
|
|
|
|
if routes:
|
|
route_content = ""
|
|
for x in range(0, len(routes)):
|
|
if not _is_suse(distro):
|
|
route_content += "ADDRESS{x}={net}\n".format(x=x, **routes[x])
|
|
route_content += "NETMASK{x}={mask}\n".format(x=x, **routes[x])
|
|
route_content += "GATEWAY{x}={gw}\n".format(x=x, **routes[x])
|
|
else:
|
|
# Avoid the extra trailing whitespace for the default route
|
|
# because mask is empty in that case.
|
|
route_content += "{net} {gw} {mask}\n".format(
|
|
**routes[x]).replace(' \n', '\n')
|
|
files_to_write[_network_files(distro)["route"] + '-{name}'
|
|
.format(name=name)] = route_content
|
|
files_to_write[_network_files(distro)["ifcfg"] + '-{name}'.format(
|
|
name=name)] = results
|
|
|
|
return files_to_write
|
|
|
|
|
|
def _write_rh_dhcp(name, interface, args):
|
|
distro = args.distro
|
|
filename = _network_files(distro)["ifcfg"] + '-{name}'.format(name=name)
|
|
results = _network_config(args)["dhcp"].format(
|
|
bootproto="dhcp", name=name, hwaddr=interface['mac_address'])
|
|
results += _set_rh_vlan(name, interface, distro)
|
|
# set_rh_bonding takes results as argument so we need to assign
|
|
# the return value, not append it
|
|
results = _set_rh_bonding(name, interface, distro, results)
|
|
|
|
return {filename: results}
|
|
|
|
|
|
def _write_rh_manual(name, interface, args):
|
|
distro = args.distro
|
|
filename = _network_files(distro)["ifcfg"] + '-{name}'.format(name=name)
|
|
results = _network_config(args)["none"].format(
|
|
bootproto="none", name=name, hwaddr=interface['mac_address'])
|
|
results += _set_rh_vlan(name, interface, distro)
|
|
# set_rh_bonding takes results as argument so we need to assign
|
|
# the return value, not append it
|
|
results = _set_rh_bonding(name, interface, distro, results)
|
|
|
|
return {filename: results}
|
|
|
|
|
|
def write_redhat_interfaces(interfaces, sys_interfaces, args):
|
|
files_to_write = dict()
|
|
# Sort the interfaces by id so that we'll have consistent output order
|
|
for iname, interface in sorted(
|
|
interfaces.items(), key=lambda x: x[1]['id']):
|
|
if interface['type'] == 'ipv6':
|
|
continue
|
|
# sys_interfaces is pruned by --interface; if one of the
|
|
# raw_macs (or, *the* MAC for single interfaces) does not
|
|
# match as one of the interfaces we want configured, skip
|
|
raw_macs = interface.get('raw_macs', [interface['mac_address']])
|
|
if not set(sys_interfaces).intersection(set(raw_macs)):
|
|
continue
|
|
|
|
if 'vlan_id' in interface:
|
|
# raw_macs will have a single entry if the vlan device is a
|
|
# phsical device and >1 when it is a bond device.
|
|
if len(raw_macs) == 1:
|
|
vlan_raw_device = sys_interfaces.get(raw_macs[0])
|
|
else:
|
|
vlan_raw_device = interface['vlan_link']
|
|
interface_name = "{0}.{1}".format(
|
|
vlan_raw_device, interface['vlan_id'])
|
|
elif 'bond_mode' in interface:
|
|
# It is possible our interface does not have a link, so fall back
|
|
# to iname which is the link id.
|
|
interface_name = interface.get('link', iname)
|
|
else:
|
|
interface_name = sys_interfaces[interface['mac_address']]
|
|
|
|
if 'bond_links' in interface:
|
|
# We need to keep track of the slave interfaces because
|
|
# SUSE configures the slaves on the master ifcfg file
|
|
bond_slaves = []
|
|
for phy in interface['raw_macs']:
|
|
bond_slaves.append(sys_interfaces[phy])
|
|
interface['bond_slaves'] = bond_slaves
|
|
# Remove the 'bond_links' key
|
|
interface.pop('bond_links')
|
|
|
|
if interface['type'] == 'ipv4':
|
|
files_to_write.update(
|
|
_write_rh_interface(interface_name, interface, args))
|
|
if interface['type'] == 'ipv4_dhcp':
|
|
files_to_write.update(
|
|
_write_rh_dhcp(interface_name, interface, args))
|
|
if interface['type'] == 'manual':
|
|
files_to_write.update(
|
|
_write_rh_manual(interface_name, interface, args))
|
|
for mac, iname in sorted(
|
|
sys_interfaces.items(), key=lambda x: x[1]):
|
|
if _exists_rh_interface(iname, args.distro):
|
|
# This interface already has a config file, move on
|
|
log.debug("%s already has config file, skipping" % iname)
|
|
continue
|
|
inter_macs = [intf['mac_address'] for intf in interfaces.values()]
|
|
link_macs = [intf.get('link_mac') for intf in interfaces.values()
|
|
if 'vlan_id' in interface]
|
|
if mac in inter_macs or mac in link_macs:
|
|
# We have a config drive config, move on
|
|
log.debug("%s configured via config-drive" % mac)
|
|
continue
|
|
files_to_write.update(_write_rh_dhcp(iname, {'mac_address': mac},
|
|
args))
|
|
return files_to_write
|
|
|
|
|
|
def _write_networkd_interface(name, interfaces, args, files_struct=dict()):
|
|
vlans = []
|
|
for interface in interfaces:
|
|
iname = name
|
|
# if vlan set interface name to vlan format
|
|
if 'vlan_id' in interface:
|
|
iname = name + '-vlan' + str(interface['vlan_id'])
|
|
vlans.append(iname)
|
|
network_file = '/etc/systemd/network/{name}.network'.format(name=iname)
|
|
if network_file not in files_struct:
|
|
files_struct[network_file] = dict()
|
|
if '[Match]' not in files_struct[network_file]:
|
|
files_struct[network_file]['[Match]'] = list()
|
|
files_struct[network_file]['[Match]'].append(
|
|
'MACAddress={mac_address}'.format(
|
|
mac_address=interface['mac_address']
|
|
)
|
|
)
|
|
files_struct[network_file]['[Match]'].append(
|
|
'Name={name}'.format(name=iname)
|
|
)
|
|
# define network if needed (basically always)
|
|
if ((interface['type'] in ['ipv4_dhcp', 'ipv6_slaac',
|
|
'ipv6_dhcpv6_stateful', 'manual', 'ipv4', 'ipv6']) or
|
|
('vlan_id' in interface) or
|
|
('bond_mode' in interface)):
|
|
if '[Network]' not in files_struct[network_file]:
|
|
files_struct[network_file]['[Network]'] = list()
|
|
if 'services' in interface:
|
|
for service in interface['services']:
|
|
if service['type'] == 'dns':
|
|
if not args.skip_dns:
|
|
files_struct[network_file]['[Network]'].append(
|
|
'DNS={address}'.format(
|
|
address=service['address']
|
|
)
|
|
)
|
|
# dhcp network, set to yes if both dhcp6 and dhcp4 are set
|
|
if interface['type'] == 'ipv4_dhcp':
|
|
if 'DHCP=ipv6' in files_struct[network_file]['[Network]']:
|
|
files_struct[network_file]['[Network]'].append('DHCP=yes')
|
|
else:
|
|
files_struct[network_file]['[Network]'].append('DHCP=ipv4')
|
|
if interface['type'] == 'ipv6_dhcpv6_stateful':
|
|
if 'DHCP=ipv4' in files_struct[network_file]['[Network]']:
|
|
files_struct[network_file]['[Network]'].append('DHCP=yes')
|
|
else:
|
|
files_struct[network_file]['[Network]'].append('DHCP=ipv6')
|
|
# slaac can start dhcp6 if the associated RA option is sent to server
|
|
if interface['type'] == 'ipv6_slaac':
|
|
# we are accepting slaac now, remove the disabling of slaac
|
|
if 'IPv6AcceptRA=no' in files_struct[network_file]['[Network]']:
|
|
files_struct[network_file]['[Network]'].remove(
|
|
'IPv6AcceptRA=no'
|
|
)
|
|
files_struct[network_file]['[Network]'].append('IPv6AcceptRA=yes')
|
|
else:
|
|
# only disbale slaac if slac is not already enabled
|
|
if 'IPv6AcceptRA=yes' not in \
|
|
files_struct[network_file]['[Network]']:
|
|
files_struct[network_file]['[Network]'].append(
|
|
'IPv6AcceptRA=no'
|
|
)
|
|
# vlan network
|
|
|
|
# static network
|
|
if interface['type'] in ['ipv4', 'ipv6']:
|
|
if 'addresses' not in files_struct[network_file]:
|
|
files_struct[network_file]['addresses'] = list()
|
|
if interface['type'] == 'ipv4':
|
|
files_struct[network_file]['addresses'].append(
|
|
'Address={address}/{cidr}'.format(
|
|
address=interface['ip_address'],
|
|
cidr=utils.ipv4_netmask_length(interface['netmask'])
|
|
)
|
|
)
|
|
if interface['type'] == 'ipv6':
|
|
files_struct[network_file]['addresses'].append(
|
|
'Address={address}/{cidr}'.format(
|
|
address=interface['ip_address'],
|
|
cidr=utils.ipv6_netmask_length(interface['netmask'])
|
|
)
|
|
)
|
|
# routes
|
|
if 'routes' in interface:
|
|
if 'routes' not in files_struct[network_file]:
|
|
files_struct[network_file]['routes'] = list()
|
|
for route in interface['routes']:
|
|
route_destination = None
|
|
route_gateway = None
|
|
if 'network' in route:
|
|
if 'v6' in interface['type']:
|
|
cidr = utils.ipv6_netmask_length(route['netmask'])
|
|
else:
|
|
cidr = utils.ipv4_netmask_length(route['netmask'])
|
|
route_destination = 'Destination={network}/{cidr}'.format(
|
|
network=route['network'], cidr=cidr
|
|
)
|
|
if 'gateway' in route:
|
|
route_gateway = 'Gateway={gateway}'.format(
|
|
gateway=route['gateway']
|
|
)
|
|
# add route as a dictionary to the routes list
|
|
files_struct[network_file]['routes'].append({
|
|
'route': route_destination,
|
|
'gw': route_gateway
|
|
})
|
|
|
|
# create netdev files
|
|
if 'bond_mode' or 'vlan_id' in interface:
|
|
netdev_file = \
|
|
'/etc/systemd/network/{name}.netdev'.format(name=iname)
|
|
if netdev_file not in files_struct:
|
|
files_struct[netdev_file] = dict()
|
|
if '[NetDev]' not in files_struct[netdev_file]:
|
|
files_struct[netdev_file]['[NetDev]'] = list()
|
|
files_struct[netdev_file]['[NetDev]'].append(
|
|
'Name={name}'.format(name=iname)
|
|
)
|
|
if 'mac_address' in interface:
|
|
files_struct[netdev_file]['[NetDev]'].append(
|
|
'MACAddress={mac_address}'.format(
|
|
mac_address=interface['mac_address']
|
|
)
|
|
)
|
|
if 'vlan_id' in interface:
|
|
files_struct[netdev_file]['[NetDev]'].append('Kind=vlan')
|
|
files_struct[netdev_file]['[VLAN]'] = list()
|
|
files_struct[netdev_file]['[VLAN]'].append(
|
|
'Id={id}'.format(id=interface['vlan_id'])
|
|
)
|
|
if 'bond_mode' in interface:
|
|
files_struct[netdev_file]['[NetDev]'].append('Kind=bond')
|
|
files_struct[netdev_file]['[Bond]'] = list()
|
|
files_struct[netdev_file]['[Bond]'].append(
|
|
'Mode={bond_mode}'.format(bond_mode=interface['bond_mode'])
|
|
)
|
|
files_struct[netdev_file]['[Bond]'].append(
|
|
'LACPTransmitRate=fast'
|
|
)
|
|
if 'slaves' in interface:
|
|
for slave in interface['slaves']:
|
|
slave_net_file = \
|
|
'/etc/systemd/network/{name}.network'.format(
|
|
name=slave
|
|
)
|
|
if slave_net_file not in files_struct:
|
|
files_struct[slave_net_file] = dict()
|
|
if '[Network]' not in files_struct[slave_net_file]:
|
|
files_struct[slave_net_file]['[Network]'] = list()
|
|
files_struct[slave_net_file]['[Network]'].append(
|
|
'Bond={name}'.format(name=iname)
|
|
)
|
|
if 'bond_xmit_hash_policy' in interface:
|
|
files_struct[netdev_file]['[Bond]'].append(
|
|
'TransmitHashPolicy={bond_xmit_hash_policy}'.format(
|
|
bond_xmit_hash_policy=interface[
|
|
'bond_xmit_hash_policy'
|
|
]
|
|
)
|
|
)
|
|
if 'bond_miimon' in interface:
|
|
files_struct[netdev_file]['[Bond]'].append(
|
|
'MIIMonitorSec={milliseconds}'.format(
|
|
milliseconds=interface['bond_miimon']
|
|
)
|
|
)
|
|
|
|
# vlan mapping sucks (forward and reverse)
|
|
if vlans:
|
|
netdev = vlans[0].split('-')[0]
|
|
vlan_master_file = \
|
|
'/etc/systemd/network/{name}.network'.format(name=netdev)
|
|
if vlan_master_file not in files_struct:
|
|
files_struct[vlan_master_file] = dict()
|
|
if '[Network]' not in files_struct[vlan_master_file]:
|
|
files_struct[vlan_master_file]['[Network]'] = list()
|
|
for vlan in vlans:
|
|
files_struct[vlan_master_file]['[Network]'].append('VLAN=' + vlan)
|
|
vlan_file = '/etc/systemd/network/{name}.network'.format(name=vlan)
|
|
if vlan_file not in files_struct:
|
|
files_struct[vlan_file] = dict()
|
|
if '[Network]' not in files_struct[vlan_file]:
|
|
files_struct[vlan_file]['[Network]'] = list()
|
|
files_struct[vlan_file]['[Network]'].append('VLAN=' + vlan)
|
|
|
|
return files_struct
|
|
|
|
|
|
def write_networkd_interfaces(interfaces, sys_interfaces, args):
|
|
files_to_write = dict()
|
|
gen_intfs = {}
|
|
files_struct = dict()
|
|
# Sort the interfaces by id so that we'll have consistent output order
|
|
for iname, interface in sorted(
|
|
interfaces.items(), key=lambda x: x[1]['id']):
|
|
# sys_interfaces is pruned by --interface; if one of the
|
|
# raw_macs (or, *the* MAC for single interfaces) does not
|
|
# match as one of the interfaces we want configured, skip
|
|
raw_macs = interface.get('raw_macs', [interface['mac_address']])
|
|
if not set(sys_interfaces).intersection(set(raw_macs)):
|
|
continue
|
|
|
|
if 'bond_mode' in interface:
|
|
interface['slaves'] = [
|
|
sys_interfaces[mac] for mac in interface['raw_macs']]
|
|
|
|
if 'raw_macs' in interface:
|
|
key = tuple(interface['raw_macs'])
|
|
if key not in gen_intfs:
|
|
gen_intfs[key] = []
|
|
gen_intfs[key].append(interface)
|
|
else:
|
|
key = (interface['mac_address'],)
|
|
if key not in gen_intfs:
|
|
gen_intfs[key] = []
|
|
gen_intfs[key].append(interface)
|
|
|
|
for raw_macs, interfs in gen_intfs.items():
|
|
if len(raw_macs) == 1:
|
|
interface_name = sys_interfaces[raw_macs[0]]
|
|
else:
|
|
# It is possible our interface does not have a link, so
|
|
# fall back to interface id.
|
|
interface_name = next(
|
|
intf.get('link', intf['id']) for intf in interfs
|
|
if 'bond_mode' in intf)
|
|
files_struct = _write_networkd_interface(
|
|
interface_name, interfs, args, files_struct)
|
|
|
|
for mac, iname in sorted(
|
|
sys_interfaces.items(), key=lambda x: x[1]):
|
|
if _exists_networkd_interface(iname):
|
|
# This interface already has a config file, move on
|
|
log.debug("%s already has config file, skipping" % iname)
|
|
continue
|
|
if (mac,) in gen_intfs:
|
|
# We have a config drive config, move on
|
|
log.debug("%s configured via config-drive" % mac)
|
|
continue
|
|
interface = {'type': 'ipv4_dhcp', 'mac_address': mac}
|
|
files_struct = _write_networkd_interface(
|
|
iname, [interface], args, files_struct)
|
|
|
|
for networkd_file in files_struct:
|
|
file_contents = '# Automatically generated, do not edit\n'
|
|
if '[Match]' in files_struct[networkd_file]:
|
|
file_contents += '[Match]\n'
|
|
for line in sorted(set(files_struct[networkd_file]['[Match]'])):
|
|
file_contents += line
|
|
file_contents += '\n'
|
|
file_contents += '\n'
|
|
if '[Network]' in files_struct[networkd_file]:
|
|
file_contents += '[Network]\n'
|
|
for line in sorted(set(files_struct[networkd_file]['[Network]'])):
|
|
file_contents += line
|
|
file_contents += '\n'
|
|
file_contents += '\n'
|
|
if 'addresses' in files_struct[networkd_file]:
|
|
for address in files_struct[networkd_file]['addresses']:
|
|
file_contents += '[Address]\n'
|
|
file_contents += address + '\n\n'
|
|
if 'routes' in files_struct[networkd_file]:
|
|
for route in files_struct[networkd_file]['routes']:
|
|
file_contents += '[Route]\n'
|
|
if route['route'] is not None:
|
|
file_contents += route['route'] + '\n'
|
|
if route['gw'] is not None:
|
|
file_contents += route['gw'] + '\n'
|
|
file_contents += '\n'
|
|
if '[NetDev]' in files_struct[networkd_file]:
|
|
file_contents += '[NetDev]\n'
|
|
for line in sorted(set(files_struct[networkd_file]['[NetDev]'])):
|
|
file_contents += line
|
|
file_contents += '\n'
|
|
file_contents += '\n'
|
|
if '[VLAN]' in files_struct[networkd_file]:
|
|
file_contents += '[VLAN]\n'
|
|
for line in sorted(set(files_struct[networkd_file]['[VLAN]'])):
|
|
file_contents += line
|
|
file_contents += '\n'
|
|
file_contents += '\n'
|
|
if '[Bond]' in files_struct[networkd_file]:
|
|
file_contents += '[Bond]\n'
|
|
for line in sorted(set(files_struct[networkd_file]['[Bond]'])):
|
|
file_contents += line
|
|
file_contents += '\n'
|
|
file_contents += '\n'
|
|
files_to_write['{path}'.format(path=networkd_file)] = file_contents
|
|
return files_to_write
|
|
|
|
|
|
def _exists_networkd_interface(name):
|
|
network_file = '/etc/systemd/network/{name}.network'.format(name=name)
|
|
netdev_file = '/etc/systemd/network/{name}.netdev'.format(name=name)
|
|
return (os.path.exists(network_file) or os.path.exists(netdev_file))
|
|
|
|
|
|
def _exists_gentoo_interface(name):
|
|
file_to_check = '/etc/conf.d/net.{name}'.format(name=name)
|
|
return os.path.exists(file_to_check)
|
|
|
|
|
|
def _enable_gentoo_interface(name):
|
|
log.debug('rc-update add {name} default'.format(name=name))
|
|
subprocess.call(['rc-update', 'add',
|
|
'net.{name}'.format(name=name), 'default'])
|
|
|
|
|
|
def _write_gentoo_interface(name, interfaces):
|
|
files_to_write = dict()
|
|
results = ""
|
|
vlans = []
|
|
for interface in interfaces:
|
|
iname = name
|
|
if 'vlan_id' in interface:
|
|
vlans.append(interface['vlan_id'])
|
|
iname = "%s_%s" % (iname, interface['vlan_id'])
|
|
if interface['type'] == 'ipv4':
|
|
results += """config_{name}="{ip_address} netmask {netmask}"
|
|
mac_{name}="{hwaddr}\"\n""".format(
|
|
name=iname,
|
|
ip_address=interface['ip_address'],
|
|
netmask=interface['netmask'],
|
|
hwaddr=interface['mac_address']
|
|
)
|
|
routes = list()
|
|
for route in interface['routes']:
|
|
if (route['network'] == '0.0.0.0' and
|
|
route['netmask'] == '0.0.0.0'):
|
|
# add default route if it exists
|
|
routes.append('default via {gw}'.format(
|
|
name=name,
|
|
gw=route['gateway']
|
|
))
|
|
else:
|
|
# add remaining static routes
|
|
routes.append('{net} netmask {mask} via {gw}'.format(
|
|
net=route['network'],
|
|
mask=route['netmask'],
|
|
gw=route['gateway']
|
|
))
|
|
if routes:
|
|
routes_string = '\n'.join(route for route in routes)
|
|
results += 'routes_{name}="{routes}"'.format(
|
|
name=name,
|
|
routes=routes_string
|
|
# routes='\n'.join(str(route) for route in routes)
|
|
)
|
|
results += '\n'
|
|
elif interface['type'] == 'manual':
|
|
results += """config_{name}="null"
|
|
mac_{name}="{hwaddr}"
|
|
""".format(name=iname, hwaddr=interface['mac_address'])
|
|
_enable_gentoo_interface(iname)
|
|
else:
|
|
results += """config_{name}="dhcp"
|
|
mac_{name}="{hwaddr}"
|
|
""".format(name=iname, hwaddr=interface['mac_address'])
|
|
_enable_gentoo_interface(iname)
|
|
if 'bond_mode' in interface:
|
|
slaves = ' '.join(interface['slaves'])
|
|
results += """slaves_{name}="{slaves}"
|
|
mode_{name}="{mode}"
|
|
""".format(name=iname, slaves=slaves, mode=interface['bond_mode'])
|
|
|
|
full_results = "# Automatically generated, do not edit\n"
|
|
if vlans:
|
|
full_results += 'vlans_{name}="{vlans}"\n'.format(
|
|
name=name,
|
|
vlans=' '.join(str(vlan) for vlan in vlans))
|
|
full_results += results
|
|
|
|
files_to_write['/etc/conf.d/net.{name}'.format(name=name)] = full_results
|
|
return files_to_write
|
|
|
|
|
|
def _setup_gentoo_network_init(sys_interface, interfaces):
|
|
for interface in interfaces:
|
|
interface_name = '{name}'.format(name=sys_interface)
|
|
if 'vlan_id' in interface:
|
|
interface_name += ".{vlan}".format(
|
|
vlan=interface['vlan_id'])
|
|
log.debug('vlan {vlan} found, interface named {name}'.
|
|
format(vlan=interface['vlan_id'], name=interface_name))
|
|
if 'bond_master' in interface:
|
|
continue
|
|
_create_gentoo_net_symlink_and_enable(interface_name)
|
|
if not interfaces:
|
|
_create_gentoo_net_symlink_and_enable(sys_interface)
|
|
|
|
|
|
def _create_gentoo_net_symlink_and_enable(interface_name):
|
|
file_path = '/etc/init.d/net.{name}'.format(name=interface_name)
|
|
if not os.path.islink(file_path):
|
|
log.debug('ln -s /etc/init.d/net.lo {file_path}'.
|
|
format(file_path=file_path))
|
|
os.symlink('/etc/init.d/net.lo',
|
|
'{file_path}'.format(file_path=file_path))
|
|
_enable_gentoo_interface(interface_name)
|
|
|
|
|
|
def write_gentoo_interfaces(interfaces, sys_interfaces):
|
|
files_to_write = dict()
|
|
gen_intfs = {}
|
|
# Sort the interfaces by id so that we'll have consistent output order
|
|
for iname, interface in sorted(
|
|
interfaces.items(), key=lambda x: x[1]['id']):
|
|
if interface['type'] == 'ipv6':
|
|
continue
|
|
# sys_interfaces is pruned by --interface; if one of the
|
|
# raw_macs (or, *the* MAC for single interfaces) does not
|
|
# match as one of the interfaces we want configured, skip
|
|
raw_macs = interface.get('raw_macs', [interface['mac_address']])
|
|
if not set(sys_interfaces).intersection(set(raw_macs)):
|
|
continue
|
|
|
|
if 'bond_mode' in interface:
|
|
interface['slaves'] = [
|
|
sys_interfaces[mac] for mac in interface['raw_macs']]
|
|
|
|
if 'raw_macs' in interface:
|
|
key = tuple(interface['raw_macs'])
|
|
if key not in gen_intfs:
|
|
gen_intfs[key] = []
|
|
gen_intfs[key].append(interface)
|
|
else:
|
|
key = (interface['mac_address'],)
|
|
if key not in gen_intfs:
|
|
gen_intfs[key] = []
|
|
gen_intfs[key].append(interface)
|
|
|
|
for raw_macs, interfs in gen_intfs.items():
|
|
if len(raw_macs) == 1:
|
|
interface_name = sys_interfaces[raw_macs[0]]
|
|
else:
|
|
# It is possible our interface does not have a link, so
|
|
# fall back to interface id.
|
|
interface_name = next(
|
|
intf.get('link', intf['id']) for intf in interfs
|
|
if 'bond_mode' in intf)
|
|
files_to_write.update(
|
|
_write_gentoo_interface(interface_name, interfs))
|
|
_setup_gentoo_network_init(interface_name, interfs)
|
|
|
|
for mac, iname in sorted(
|
|
sys_interfaces.items(), key=lambda x: x[1]):
|
|
if _exists_gentoo_interface(iname):
|
|
# This interface already has a config file, move on
|
|
log.debug("%s already has config file, skipping" % iname)
|
|
continue
|
|
if (mac,) in gen_intfs:
|
|
# We have a config drive config, move on
|
|
log.debug("%s configured via config-drive" % mac)
|
|
continue
|
|
interface = {'type': 'ipv4_dhcp', 'mac_address': mac}
|
|
files_to_write.update(_write_gentoo_interface(iname, [interface]))
|
|
_setup_gentoo_network_init(iname, [])
|
|
return files_to_write
|
|
|
|
|
|
def _write_debian_bond_conf(interface_name, interface, sys_interfaces):
|
|
result = ""
|
|
if interface['mac_address']:
|
|
result += " hwaddress {0}\n".format(
|
|
interface['mac_address'])
|
|
result += " bond-mode {0}\n".format(interface['bond_mode'])
|
|
result += " bond-miimon {0}\n".format(
|
|
interface.get('bond_miimon', 0))
|
|
result += " bond-lacp-rate {0}\n".format(
|
|
interface.get('bond_lacp_rate', 'slow'))
|
|
result += " bond-xmit_hash_policy {0}\n".format(
|
|
interface.get('bond_xmit_hash_policy', 'layer2'))
|
|
slave_devices = [sys_interfaces[mac]
|
|
for mac in interface['raw_macs']]
|
|
slaves = ' '.join(slave_devices)
|
|
result += " bond-slaves none\n"
|
|
result += " post-up ifenslave {0} {1}\n".format(interface_name, slaves)
|
|
result += " pre-down ifenslave -d {0} {1}\n".format(
|
|
interface_name, slaves)
|
|
return result
|
|
|
|
|
|
def write_debian_interfaces(interfaces, sys_interfaces):
|
|
eni_path = '/etc/network/interfaces'
|
|
eni_d_path = eni_path + '.d'
|
|
files_to_write = dict()
|
|
files_to_write[eni_path] = "auto lo\niface lo inet loopback\n"
|
|
files_to_write[eni_path] += "source /etc/network/interfaces.d/*.cfg\n"
|
|
# Sort the interfaces by id so that we'll have consistent output order
|
|
for iname, interface in sorted(
|
|
interfaces.items(), key=lambda x: x[1]['id']):
|
|
# sys_interfaces is pruned by --interface; if one of the
|
|
# raw_macs (or, *the* MAC for single interfaces) does not
|
|
# match as one of the interfaces we want configured, skip
|
|
raw_macs = interface.get('raw_macs', [interface['mac_address']])
|
|
if not set(sys_interfaces).intersection(set(raw_macs)):
|
|
continue
|
|
|
|
# Determine the debian interface name and skip configuration for
|
|
# this interface if config already exists for it.
|
|
vlan_raw_device = None
|
|
if 'vlan_id' in interface:
|
|
# raw_macs will have a single entry if the vlan device is a
|
|
# phsical device and >1 when it is a bond device.
|
|
if len(raw_macs) == 1:
|
|
vlan_raw_device = sys_interfaces.get(raw_macs[0])
|
|
else:
|
|
vlan_raw_device = interface['vlan_link']
|
|
interface_name = "{0}.{1}".format(vlan_raw_device,
|
|
interface['vlan_id'])
|
|
elif 'bond_mode' in interface:
|
|
# It is possible our interface does not have a link, so fall back
|
|
# to iname which is the link id.
|
|
interface_name = interface.get('link', iname)
|
|
else:
|
|
interface_name = sys_interfaces[interface['mac_address']]
|
|
|
|
iface_path = os.path.join(eni_d_path, '%s.cfg' % interface_name)
|
|
if os.path.exists(iface_path):
|
|
continue
|
|
|
|
if iface_path not in files_to_write:
|
|
# Write the header if this is the first time editing
|
|
# this interface.
|
|
result = "auto {0}\n".format(interface_name)
|
|
else:
|
|
# Append to existing config
|
|
result = files_to_write[iface_path]
|
|
|
|
if interface['type'] == 'ipv4_dhcp':
|
|
result += "iface {0} inet dhcp\n".format(interface_name)
|
|
if vlan_raw_device is not None:
|
|
result += " vlan-raw-device {0}\n".format(vlan_raw_device)
|
|
result += " hw-mac-address {0}\n".format(
|
|
interface['mac_address'])
|
|
elif interface['type'] == 'manual':
|
|
result += "iface {0} inet manual\n".format(interface_name)
|
|
else:
|
|
# Static ipv4 and ipv6
|
|
if interface['type'] == 'ipv6':
|
|
link_type = "inet6"
|
|
elif interface['type'] == 'ipv4':
|
|
link_type = "inet"
|
|
else:
|
|
# We do not know this type of entry
|
|
continue
|
|
|
|
result += "iface {name} {link_type} static\n".format(
|
|
name=interface_name, link_type=link_type)
|
|
if vlan_raw_device:
|
|
result += " vlan-raw-device {0}\n".format(vlan_raw_device)
|
|
result += " address {0}\n".format(interface['ip_address'])
|
|
|
|
if interface['type'] == 'ipv4':
|
|
result += " netmask {0}\n".format(interface['netmask'])
|
|
else:
|
|
result += " netmask {0}\n".format(
|
|
utils.ipv6_netmask_length(interface['netmask']))
|
|
|
|
for route in interface['routes']:
|
|
if ((route['network'] == '0.0.0.0' and
|
|
route['netmask'] == '0.0.0.0') or
|
|
(route['network'] == '::' and
|
|
route['netmask'] == '::')):
|
|
result += " gateway {0}\n".format(route['gateway'])
|
|
else:
|
|
if interface['type'] == 'ipv4':
|
|
route_add = (" up route add -net {net} netmask "
|
|
"{mask} gw {gw} || true\n")
|
|
route_del = (" down route del -net {net} netmask "
|
|
"{mask} gw {gw} || true\n")
|
|
_netmask = route['netmask']
|
|
else:
|
|
route_add = (" up ip -6 route add {net}/{mask} "
|
|
"via {gw} dev {interface} || true\n")
|
|
route_del = (" down ip -6 route del {net}/{mask} "
|
|
"via {gw} dev {interface} || true\n")
|
|
_netmask = utils.ipv6_netmask_length(route['netmask'])
|
|
|
|
result += route_add.format(
|
|
net=route['network'], mask=_netmask,
|
|
gw=route['gateway'], interface=interface_name)
|
|
result += route_del.format(
|
|
net=route['network'], mask=_netmask,
|
|
gw=route['gateway'], interface=interface_name)
|
|
if 'bond_master' in interface:
|
|
result += " bond-master {0}\n".format(
|
|
interface['bond_master'])
|
|
if 'bond_mode' in interface:
|
|
result += _write_debian_bond_conf(interface_name,
|
|
interface,
|
|
sys_interfaces)
|
|
files_to_write[iface_path] = result
|
|
|
|
# Configure any interfaces not mentioned in the config drive data for DHCP.
|
|
for mac, iname in sorted(
|
|
sys_interfaces.items(), key=lambda x: x[1]):
|
|
iface_path = os.path.join(eni_d_path, '%s.cfg' % iname)
|
|
if os.path.exists(iface_path):
|
|
# This interface already has a config file, move on
|
|
continue
|
|
inter_macs = [intf['mac_address'] for intf in interfaces.values()]
|
|
link_macs = [intf.get('link_mac') for intf in interfaces.values()
|
|
if 'vlan_id' in interface]
|
|
if mac in inter_macs or mac in link_macs:
|
|
# We have a config drive config, move on
|
|
continue
|
|
result = "auto {0}\n".format(iname)
|
|
result += "iface {0} inet dhcp\n".format(iname)
|
|
files_to_write[iface_path] = result
|
|
return files_to_write
|
|
|
|
|
|
def write_dns_info(dns_servers):
|
|
resolve_confs = {}
|
|
resolv_nameservers = ""
|
|
for server in dns_servers:
|
|
resolv_nameservers += "nameserver {0}\n".format(server)
|
|
resolve_confs['/etc/resolv.conf'] = resolv_nameservers
|
|
# set up resolved if available
|
|
if os.path.isfile('/etc/systemd/resolved.conf'):
|
|
# read the existing config so we only overwrite what's needed
|
|
resolved_conf = configparser.ConfigParser()
|
|
resolved_conf.read('/etc/systemd/resolved.conf')
|
|
# create config section if not created
|
|
if not resolved_conf.has_section('Resolve'):
|
|
resolved_conf.add_section('Resolve')
|
|
# write space separated dns servers
|
|
resolved_conf.set('Resolve', 'DNS', " ".join(dns_servers))
|
|
# use stringio to output the resulting config to string
|
|
# configparser only outputs to file descriptors
|
|
resolved_conf_fd = StringIO("")
|
|
resolved_conf.write(resolved_conf_fd)
|
|
resolved_conf_output = resolved_conf_fd.getvalue()
|
|
resolved_conf_fd.close()
|
|
# add the config to files to be written
|
|
resolve_confs['/etc/systemd/resolved.conf'] = resolved_conf_output
|
|
return resolve_confs
|
|
|
|
|
|
def get_config_drive_interfaces(net):
|
|
interfaces = {}
|
|
|
|
if 'networks' not in net or 'links' not in net:
|
|
log.debug("No config-drive interfaces defined")
|
|
return interfaces
|
|
|
|
networks = {}
|
|
for network in net['networks']:
|
|
networks[network['link']] = network
|
|
|
|
vlans = {}
|
|
phys = {}
|
|
bonds = {}
|
|
for link in net['links']:
|
|
if link['type'] == 'vlan':
|
|
vlans[link['id']] = link
|
|
elif link['type'] == 'bond':
|
|
bonds[link['id']] = link
|
|
else:
|
|
phys[link['id']] = link
|
|
|
|
for link in vlans.values():
|
|
if link['vlan_link'] in phys:
|
|
vlan_link = phys[link['vlan_link']]
|
|
link['raw_macs'] = [vlan_link['ethernet_mac_address'].lower()]
|
|
elif link['vlan_link'] in bonds:
|
|
vlan_link = bonds[link['vlan_link']]
|
|
link['raw_macs'] = []
|
|
for phy in vlan_link['bond_links']:
|
|
link['raw_macs'].append(
|
|
phys[phy]['ethernet_mac_address'].lower())
|
|
link['mac_address'] = link.pop(
|
|
'vlan_mac_address', vlan_link['ethernet_mac_address']).lower()
|
|
|
|
for link in bonds.values():
|
|
phy_macs = []
|
|
for phy in link['bond_links']:
|
|
phy_link = phys[phy]
|
|
phy_link['bond_master'] = link['id']
|
|
if phy in phys:
|
|
phy_macs.append(phy_link['ethernet_mac_address'].lower())
|
|
link['raw_macs'] = phy_macs
|
|
link['mac_address'] = link.pop('ethernet_mac_address').lower()
|
|
if link['id'] not in networks:
|
|
link['type'] = 'manual'
|
|
interfaces[link['id']] = link
|
|
|
|
for link in phys.values():
|
|
link['mac_address'] = link.pop('ethernet_mac_address').lower()
|
|
if link['id'] not in networks:
|
|
link['type'] = 'manual'
|
|
interfaces[link['id']] = link
|
|
|
|
for network in net['networks']:
|
|
link = vlans.get(
|
|
network['link'],
|
|
phys.get(network['link'], bonds.get(network['link'])))
|
|
if not link:
|
|
continue
|
|
|
|
link.update(network)
|
|
# NOTE(pabelanger): Make sure we index by the existing network id,
|
|
# rather then creating out own.
|
|
interfaces[network['id']] = copy.deepcopy(link)
|
|
|
|
return interfaces
|
|
|
|
|
|
def get_dns_from_config_drive(net):
|
|
if 'services' not in net:
|
|
log.debug("No DNS info available from config-drive")
|
|
return []
|
|
return [
|
|
f['address'] for f in net['services'] if f['type'] == 'dns'
|
|
]
|
|
|
|
|
|
def write_static_network_info(
|
|
interfaces, sys_interfaces, files_to_write, args):
|
|
|
|
if args.distro in ('debian', 'ubuntu'):
|
|
files_to_write.update(
|
|
write_debian_interfaces(interfaces, sys_interfaces))
|
|
elif args.distro in ('redhat', 'centos', 'fedora') or \
|
|
_is_suse(args.distro):
|
|
files_to_write.update(
|
|
write_redhat_interfaces(interfaces, sys_interfaces, args))
|
|
elif args.distro in 'gentoo':
|
|
files_to_write.update(
|
|
write_gentoo_interfaces(interfaces, sys_interfaces)
|
|
)
|
|
elif args.distro in 'networkd':
|
|
files_to_write.update(
|
|
write_networkd_interfaces(interfaces, sys_interfaces, args)
|
|
)
|
|
else:
|
|
return False
|
|
|
|
finish_files(files_to_write, args)
|
|
|
|
|
|
def finish_files(files_to_write, args):
|
|
files = sorted(files_to_write.keys())
|
|
log.debug("Writing output files")
|
|
for k in files:
|
|
if not files_to_write[k]:
|
|
# Don't write empty files
|
|
log.debug("%s is blank, skipped" % k)
|
|
continue
|
|
|
|
if args.noop:
|
|
sys.stdout.write("### Write {0}\n{1}".format(k, files_to_write[k]))
|
|
continue
|
|
|
|
retries = 0
|
|
while True:
|
|
try:
|
|
log.debug("Writing output file : %s" % k)
|
|
with safe_open(k, 'w') as outfile:
|
|
outfile.write(files_to_write[k])
|
|
log.debug(" ... done")
|
|
break
|
|
except IOError as e:
|
|
# if we got ELOOP the file was a dangling or bad
|
|
# symlink. We're taking ownership of this, so
|
|
# overwrite it.
|
|
if e.errno == errno.ELOOP and retries < 1:
|
|
log.debug("Dangling symlink <%s>; "
|
|
"unlinking and trying again" % k)
|
|
os.unlink(k)
|
|
retries = 1
|
|
continue
|
|
elif e.errno == errno.EACCESS:
|
|
log.debug(" ... is read only, skipped")
|
|
break
|
|
else:
|
|
raise
|
|
|
|
|
|
def is_interface_live(interface, sys_root):
|
|
try:
|
|
if open('{root}/{iface}/carrier'.format(
|
|
root=sys_root, iface=interface)).read().strip() == '1':
|
|
return True
|
|
except IOError as e:
|
|
# We get this error if the link is not up
|
|
if e.errno != 22:
|
|
raise
|
|
return False
|
|
|
|
|
|
def interface_live(iface, sys_root, args):
|
|
log.debug("Checking if interface %s has an active link carrier." % iface)
|
|
if is_interface_live(iface, sys_root):
|
|
return True
|
|
|
|
if args.noop:
|
|
return False
|
|
|
|
subprocess.check_call(['ip', 'link', 'set', 'dev', iface, 'up'])
|
|
|
|
return True
|
|
|
|
|
|
def is_interface_vlan(iface, distro):
|
|
if distro in ('debian', 'ubuntu'):
|
|
file_name = '/etc/network/interfaces.d/%s.cfg' % iface
|
|
if os.path.exists(file_name):
|
|
return 'vlan-raw-device' in open(file_name).read()
|
|
elif distro in ('redhat', 'centos', 'fedora'):
|
|
file_name = '/etc/sysconfig/network-scripts/ifcfg-%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'VLAN=YES' in open(file_name).read()
|
|
elif _is_suse(distro):
|
|
file_name = '/etc/sysconfig/network/ifcfg-%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'ETHERDEVICE' in open(file_name).read()
|
|
elif distro in ('gentoo'):
|
|
file_name = '/etc/conf.d/net.%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'vlan_id' in open(file_name).read()
|
|
|
|
return False
|
|
|
|
|
|
def is_interface_bridge(iface, distro):
|
|
if distro in ('debian', 'ubuntu'):
|
|
file_name = '/etc/network/interfaces.d/%s.cfg' % iface
|
|
if os.path.exists(file_name):
|
|
return 'bridge_ports' in open(file_name).read().lower()
|
|
elif distro in ('redhat', 'centos', 'fedora'):
|
|
file_name = '/etc/sysconfig/network-scripts/ifcfg-%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'type=bridge' in open(file_name).read().lower()
|
|
elif _is_suse(distro):
|
|
file_name = '/etc/sysconfig/network/ifcfg-%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'bridge=yes' in open(file_name).read().lower()
|
|
elif distro in ('gentoo'):
|
|
file_name = '/etc/conf.d/net.%s' % iface
|
|
if os.path.exists(file_name):
|
|
return 'bridge' in open(file_name).read().lower()
|
|
|
|
return False
|
|
|
|
|
|
def get_sys_interfaces(interface, args):
|
|
log.debug("Probing system interfaces")
|
|
sys_root = os.path.join(args.root, 'sys/class/net')
|
|
|
|
ignored_interfaces = ('sit', 'tunl', 'bonding_master', 'teql',
|
|
'ip6gre', 'ip6_vti', 'ip6tnl', 'bond', 'lo')
|
|
sys_interfaces = {}
|
|
if interface is not None:
|
|
log.debug("Only considering interface %s from arguments" % interface)
|
|
interfaces = [interface]
|
|
else:
|
|
interfaces = [f for f in os.listdir(sys_root)
|
|
if not f.startswith(ignored_interfaces)]
|
|
# build interface dict. so we can enumerate through later
|
|
if_dict = {}
|
|
for iface in interfaces:
|
|
# if interface is for an already configured vlan, skip it
|
|
if is_interface_vlan(iface, args.distro):
|
|
log.debug("Skipping vlan %s" % iface)
|
|
continue
|
|
|
|
# if interface is for an already configured bridge, skip it
|
|
if is_interface_bridge(iface, args.distro):
|
|
log.debug("Skipping bridge %s" % iface)
|
|
continue
|
|
|
|
mac_addr_type = open(
|
|
'%s/%s/addr_assign_type' % (sys_root, iface), 'r').read().strip()
|
|
# Interfaces without a permanent address are likely created by some
|
|
# other system on the host like a running neutron agent. In these cases
|
|
# that system should be responsible for configuring the interface not
|
|
# glean.
|
|
if mac_addr_type != PERMANENT_ADDR_TYPE:
|
|
continue
|
|
# check if interface is up if not try and bring it up
|
|
if interface_live(iface, sys_root, args):
|
|
mac = open('%s/%s/address' % (sys_root, iface), 'r').read().strip()
|
|
if_dict[iface] = mac
|
|
|
|
# wait up to 9 seconds all interfaces to reach up
|
|
log.debug("Waiting for interfaces to become active.")
|
|
if_up_list = []
|
|
for x in range(0, 90):
|
|
for iface in if_dict:
|
|
mac = if_dict[iface]
|
|
if iface in if_up_list:
|
|
continue
|
|
if is_interface_live(iface, sys_root):
|
|
# Add system interface
|
|
sys_interfaces[mac] = iface
|
|
log.debug("Added system interface %s (%s)" % (iface, mac))
|
|
if_up_list.append(iface)
|
|
|
|
if sorted(if_up_list) == sorted(if_dict.keys()):
|
|
# all interfaces are up no need to continue looping
|
|
break
|
|
time.sleep(.1)
|
|
|
|
if sorted(if_up_list) != sorted(if_dict.keys()):
|
|
# not all interfaces became active with in the time limit
|
|
for iface in if_dict:
|
|
if iface in if_up_list:
|
|
continue
|
|
msg = "Skipping system interface %s (%s)" % (iface,
|
|
if_dict[iface])
|
|
log.warn(msg)
|
|
|
|
return sys_interfaces
|
|
|
|
|
|
def get_network_info(args):
|
|
"""Retrieves network info from config-drive.
|
|
|
|
If there is no meta_data.json in config-drive, it means that there
|
|
is no config drive mounted- which means we know nothing.
|
|
"""
|
|
config_drive = os.path.join(args.root, 'mnt/config')
|
|
network_info_file = '%s/openstack/latest/network_info.json' % config_drive
|
|
network_data_file = '%s/openstack/latest/network_data.json' % config_drive
|
|
vendor_data_file = '%s/openstack/latest/vendor_data.json' % config_drive
|
|
|
|
network_info = {}
|
|
if os.path.exists(network_info_file):
|
|
log.debug("Found network_info file %s" % network_info_file)
|
|
network_info = json.load(open(network_info_file))
|
|
# network_data.json is the file written by nova that should be there.
|
|
# Other cloud deployments may use the above network_info.json or
|
|
# vendor_data.json but the canonical location is this one.
|
|
if os.path.exists(network_data_file):
|
|
log.debug("Found network_info file %s" % network_data_file)
|
|
network_info = json.load(open(network_data_file))
|
|
elif os.path.exists(vendor_data_file):
|
|
log.debug("Found vendor_data_file file %s" % vendor_data_file)
|
|
vendor_data = json.load(open(vendor_data_file))
|
|
if 'network_info' in vendor_data:
|
|
log.debug("Found network_info in vendor_data_file")
|
|
network_info = vendor_data['network_info']
|
|
else:
|
|
log.debug("Did not find vendor_data or network_info in config-drive")
|
|
|
|
if not network_info:
|
|
log.debug("Found no network_info in config-drive! "
|
|
"Asusming DHCP interfaces")
|
|
|
|
return network_info
|
|
|
|
|
|
def write_network_info_from_config_drive(args):
|
|
"""Write network info from config-drive.
|
|
|
|
If there is no meta_data.json in config-drive, it means that there
|
|
is no config drive mounted- which means we know nothing.
|
|
|
|
Can set 'glean_ignore_interfaces' in nova metadata to ignore the
|
|
interface configuration specified by the config drive. This will
|
|
cause it to fallback to using dhcp configuration.
|
|
|
|
Returns False on any issue, which will cause the writing of
|
|
DHCP network files.
|
|
"""
|
|
|
|
config_drive = os.path.join(args.root, 'mnt/config')
|
|
meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive
|
|
meta_data = {}
|
|
if os.path.exists(meta_data_path):
|
|
meta_data = json.load(open(meta_data_path))
|
|
|
|
network_info = get_network_info(args)
|
|
|
|
dns = {}
|
|
if not args.skip_dns:
|
|
dns = write_dns_info(get_dns_from_config_drive(network_info))
|
|
interfaces = get_config_drive_interfaces(network_info)
|
|
if 'meta' in meta_data and 'glean_ignore_interfaces' in meta_data['meta']:
|
|
# Force DHCP to be used ignoring the interface information.
|
|
# Some clouds have neutron configured in such a way that we get
|
|
# interface config drive data that is at odds with the networking
|
|
# in the cloud. Note we set interfaces to {} so that fallback dhcp
|
|
# configuration can happen in write_static_network_info().
|
|
interfaces = {}
|
|
sys_interfaces = get_sys_interfaces(args.interface, args)
|
|
|
|
write_static_network_info(interfaces, sys_interfaces, dns, args)
|
|
|
|
|
|
def write_ssh_keys(args):
|
|
"""Write ssh-keys from config-drive.
|
|
|
|
If there is no meta_data.json in config-drive, it means that there
|
|
is no config drive mounted- which means we do nothing.
|
|
"""
|
|
|
|
config_drive = os.path.join(args.root, 'mnt/config')
|
|
ssh_path = os.path.join(args.root, 'root/.ssh')
|
|
meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive
|
|
if not os.path.exists(meta_data_path):
|
|
return 0
|
|
|
|
meta_data = json.load(open(meta_data_path))
|
|
if 'public_keys' not in meta_data:
|
|
return 0
|
|
|
|
keys_to_write = []
|
|
|
|
# if we have keys already there, we want to preserve them
|
|
if os.path.exists('/root/.ssh/authorized_keys'):
|
|
with open('/root/.ssh/authorized_keys', 'r') as fk:
|
|
for line in fk:
|
|
keys_to_write.append(line.strip())
|
|
for (name, key) in meta_data['public_keys'].items():
|
|
key_title = "# Injected key {name} by keypair extension".format(
|
|
name=name)
|
|
if key_title not in keys_to_write:
|
|
keys_to_write.append(key_title)
|
|
|
|
if key not in keys_to_write:
|
|
keys_to_write.append(key)
|
|
|
|
files_to_write = {
|
|
'/root/.ssh/authorized_keys': '\n'.join(keys_to_write) + '\n',
|
|
}
|
|
try:
|
|
os.mkdir(ssh_path, 0o700)
|
|
except OSError as e:
|
|
if e.errno != 17: # not File Exists
|
|
raise
|
|
finish_files(files_to_write, args)
|
|
|
|
|
|
def set_hostname_from_config_drive(args):
|
|
if args.noop:
|
|
return
|
|
|
|
config_drive = os.path.join(args.root, 'mnt/config')
|
|
meta_data_path = '%s/openstack/latest/meta_data.json' % config_drive
|
|
if not os.path.exists(meta_data_path):
|
|
return
|
|
|
|
meta_data = json.load(open(meta_data_path))
|
|
if 'name' not in meta_data:
|
|
return
|
|
|
|
hostname = meta_data['name']
|
|
log.debug("Got hostname from meta_data.json : %s" % hostname)
|
|
# underscore is not a valid hostname, but it's easy to name your
|
|
# host with that on the command-line. be helpful...
|
|
if '_' in hostname:
|
|
hostname = hostname.replace('_', '-')
|
|
log.debug("Fixed up hostname to %s" % hostname)
|
|
|
|
ret = subprocess.call(['hostname', hostname])
|
|
if ret != 0:
|
|
raise RuntimeError('Error setting hostname')
|
|
else:
|
|
# gentoo's hostname file is in a different location
|
|
if args.distro is 'gentoo':
|
|
with open('/etc/conf.d/hostname', 'w') as fh:
|
|
fh.write("hostname=\"{host}\"\n".format(host=hostname))
|
|
else:
|
|
with safe_open('/etc/hostname', 'w') as fh:
|
|
fh.write(hostname)
|
|
fh.write('\n')
|
|
|
|
# generate the lists of hosts and ips
|
|
hosts_to_add = {'localhost': '127.0.0.1'}
|
|
|
|
# get information on the network
|
|
hostname_ip = '127.0.1.1'
|
|
network_info = get_network_info(args)
|
|
if network_info:
|
|
interfaces = get_config_drive_interfaces(network_info)
|
|
keys = sorted(interfaces.keys())
|
|
|
|
for key in keys:
|
|
interface = interfaces[key]
|
|
if interface and 'ip_address' in interface:
|
|
hostname_ip = interface['ip_address']
|
|
break
|
|
|
|
# check short hostname and generate list for hosts
|
|
hosts_to_add[hostname] = hostname_ip
|
|
short_hostname = hostname.split('.')[0]
|
|
if short_hostname != hostname:
|
|
hosts_to_add[short_hostname] = hostname_ip
|
|
|
|
for host in hosts_to_add:
|
|
host_value = hosts_to_add[host]
|
|
# See if we already have a hosts entry for hostname
|
|
prog = re.compile('^%s .*%s\n' % (host_value, host))
|
|
match = None
|
|
if os.path.isfile('/etc/hosts'):
|
|
with open('/etc/hosts') as fh:
|
|
match = prog.match(fh.read())
|
|
|
|
# Write out a hosts entry for hostname
|
|
if match is None:
|
|
with safe_open('/etc/hosts', 'a+') as fh:
|
|
fh.write(u'%s %s\n' % (host_value, host))
|
|
|
|
|
|
def main(argv=None):
|
|
|
|
if argv is None:
|
|
args = sys.argv[1:]
|
|
|
|
parser = argparse.ArgumentParser(description="Static network config")
|
|
parser.add_argument(
|
|
'-n', '--noop', action='store_true', help='Do not write files')
|
|
_distro = distro.linux_distribution(
|
|
full_distribution_name=False)[0].lower()
|
|
parser.add_argument(
|
|
'--distro', dest='distro', default=_distro,
|
|
help='Override distro (detected "%s")' % _distro)
|
|
parser.add_argument(
|
|
'--root', dest='root', default='/',
|
|
help='Mounted root for config drive info, defaults to /')
|
|
parser.add_argument(
|
|
'-i', '--interface', dest='interface',
|
|
default=None, help="Interface to process")
|
|
parser.add_argument(
|
|
'--ssh', dest='ssh', action='store_true', help="Write ssh key")
|
|
parser.add_argument(
|
|
'--hostname', dest='hostname', action='store_true',
|
|
help="Set the hostname if name is available in config drive.")
|
|
parser.add_argument(
|
|
'--skip-network', dest='skip', action='store_true',
|
|
help="Do not write network info")
|
|
parser.add_argument(
|
|
'--use-nm', dest='use_nm', action='store_true',
|
|
help=('Use NetworkManager instead of legacy'
|
|
'configuration scripts to manage interfaces'))
|
|
parser.add_argument(
|
|
'--skip-dns', dest='skip_dns', action='store_true',
|
|
help='Do not write dns info')
|
|
parser.add_argument(
|
|
'--debug', dest='debug', action='store_true',
|
|
help="Enable debugging output")
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.debug:
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
else:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
log.debug("Starting glean")
|
|
log.debug("Detected distro : %s" % args.distro)
|
|
log.debug("Configuring %s NetworkManager" %
|
|
"with" if args.use_nm else "without")
|
|
|
|
with systemlock.Lock('/tmp/glean.lock'):
|
|
if args.ssh:
|
|
write_ssh_keys(args)
|
|
if args.hostname:
|
|
set_hostname_from_config_drive(args)
|
|
if args.interface != 'lo' and not args.skip:
|
|
write_network_info_from_config_drive(args)
|
|
log.debug("Done!")
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|