c28c105eef1d9e359ba1778d7ebcaa9a05d276fd
[apex.git] / apex / network / jumphost.py
1 ##############################################################################
2 # Copyright (c) 2017 Tim Rozet (trozet@redhat.com) and others.
3 #
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
9
10 import logging
11 import os
12 import shutil
13 import subprocess
14
15 from apex.common.exceptions import JumpHostNetworkException
16 from apex.common import parsers
17 from apex.network import ip_utils
18
19 NET_MAP = {
20     'admin': 'br-admin',
21     'tenant': 'br-tenant',
22     'external': 'br-external',
23     'storage': 'br-storage',
24     'api': 'br-api'
25 }
26
27 NET_CFG_PATH = '/etc/sysconfig/network-scripts'
28
29
30 def configure_bridges(ns):
31     """
32     Configures IP on jumphost bridges
33     :param ns: network_settings
34     :return: None
35     """
36     bridge_networks = ['admin']
37     if 'external' in ns.enabled_network_list:
38         bridge_networks.append('external')
39     for network in bridge_networks:
40         if network == 'external':
41             net_config = ns['networks'][network][0]
42         else:
43             net_config = ns['networks'][network]
44         cidr = net_config['cidr']
45         interface = ip_utils.get_interface(NET_MAP[network], cidr.version)
46
47         if interface:
48             logging.info("Bridge {} already configured with IP: {}".format(
49                 NET_MAP[network], interface.ip))
50         else:
51             logging.info("Will configure IP for {}".format(NET_MAP[network]))
52             ovs_ip = net_config['overcloud_ip_range'][1]
53             if cidr.version == 6:
54                 ipv6_br_path = "/proc/sys/net/ipv6/conf/{}/disable_" \
55                                "ipv6".format(NET_MAP[network])
56                 try:
57                     subprocess.check_call('echo', 0, '>', ipv6_br_path)
58                 except subprocess.CalledProcessError:
59                     logging.error("Unable to enable ipv6 on "
60                                   "bridge {}".format(NET_MAP[network]))
61                     raise
62             try:
63                 ip_prefix = "{}/{}".format(ovs_ip, cidr.prefixlen)
64                 subprocess.check_call(['ip', 'addr', 'add', ip_prefix, 'dev',
65                                       NET_MAP[network]])
66                 subprocess.check_call(['ip', 'link', 'set', 'up', NET_MAP[
67                     network]])
68                 logging.info("IP configured: {} on bridge {}".format(ovs_ip,
69                              NET_MAP[network]))
70             except subprocess.CalledProcessError:
71                 logging.error("Unable to configure IP address on "
72                               "bridge {}".format(NET_MAP[network]))
73                 raise
74
75
76 def generate_ifcfg_params(if_file, network):
77     """
78     Generates and validates ifcfg parameters required for a network
79     :param if_file: ifcfg file to parse
80     :param network: Apex network
81     :return: dictionary of generated/validated ifcfg params
82     """
83     ifcfg_params = parsers.parse_ifcfg_file(if_file)
84     if not ifcfg_params['IPADDR']:
85         logging.error("IPADDR missing in {}".format(if_file))
86         raise JumpHostNetworkException("IPADDR missing in {}".format(if_file))
87     if not (ifcfg_params['NETMASK'] or ifcfg_params['PREFIX']):
88         logging.error("NETMASK/PREFIX missing in {}".format(if_file))
89         raise JumpHostNetworkException("NETMASK/PREFIX missing in {}".format(
90             if_file))
91     if network == 'external' and not ifcfg_params['GATEWAY']:
92         logging.error("GATEWAY is required to be in {} for external "
93                       "network".format(if_file))
94         raise JumpHostNetworkException("GATEWAY is required to be in {} for "
95                                        "external network".format(if_file))
96
97     if ifcfg_params['DNS1'] or ifcfg_params['DNS2']:
98         ifcfg_params['PEERDNS'] = 'yes'
99     else:
100         ifcfg_params['PEERDNS'] = 'no'
101     return ifcfg_params
102
103
104 def is_ovs_bridge(bridge):
105     """
106     Finds an OVS bridge
107     :param bridge: OVS bridge to find
108     :return: boolean if OVS bridge exists
109     """
110     try:
111         output = subprocess.check_output(['ovs-vsctl', 'show'],
112                                          stderr=subprocess.STDOUT)
113         if bridge not in output.decode('utf-8'):
114             logging.debug("Bridge {} not found".format(bridge))
115             return False
116         else:
117             logging.debug("Bridge {} found".format(bridge))
118             return True
119     except subprocess.CalledProcessError:
120         logging.error("Unable to validate OVS bridge {}".format(bridge))
121         raise
122
123
124 def dump_ovs_ports(bridge):
125     """
126     Returns
127     :param bridge: OVS bridge to list ports
128     :return: list of ports
129     """
130     try:
131         output = subprocess.check_output(['ovs-vsctl', 'list-ports', bridge],
132                                          stderr=subprocess.STDOUT)
133     except subprocess.CalledProcessError:
134         logging.error("Unable to show ports for {}".format(bridge))
135         raise
136     return output.decode('utf-8').strip().split('\n')
137
138
139 def attach_interface_to_ovs(bridge, interface, network):
140     """
141     Attaches jumphost interface to OVS for baremetal deployments
142     :param bridge: bridge to attach to
143     :param interface: interface to attach to bridge
144     :param network: Apex network type for these interfaces
145     :return: None
146     """
147
148     if_file = os.path.join(NET_CFG_PATH, "ifcfg-{}".format(interface))
149     ovs_file = os.path.join(NET_CFG_PATH, "ifcfg-{}".format(bridge))
150
151     logging.info("Attaching interface: {} to bridge: {} on network {}".format(
152         bridge, interface, network
153     ))
154
155     if not is_ovs_bridge(bridge):
156         subprocess.check_call(['ovs-vsctl', 'add-br', bridge])
157     elif interface in dump_ovs_ports(bridge):
158         logging.debug("Interface already attached to bridge")
159         return
160
161     if not os.path.isfile(if_file):
162         logging.error("Interface ifcfg not found: {}".format(if_file))
163         raise FileNotFoundError("Interface file missing: {}".format(if_file))
164     ifcfg_params = generate_ifcfg_params(if_file, network)
165     shutil.move(if_file, "{}.orig".format(if_file))
166     if_content = """DEVICE={}
167 DEVICETYPE=ovs
168 TYPE=OVSPort
169 PEERDNS=no
170 BOOTPROTO=static
171 NM_CONTROLLED=no
172 ONBOOT=yes
173 OVS_BRIDGE={}
174 PROMISC=yes""".format(interface, bridge)
175
176     bridge_content = """DEVICE={}
177 DEVICETYPE=ovs
178 BOOTPROTO=static
179 ONBOOT=yes
180 TYPE=OVSBridge
181 PROMISC=yes""".format(bridge)
182     for param, value in ifcfg_params.items():
183         if value:
184             bridge_content += "\n{}={}".format(param, value)
185
186     logging.debug("New interface file content:\n{}".format(if_content))
187     logging.debug("New bridge file content:\n{}".format(bridge_content))
188     with open(if_file, 'w') as fh:
189         fh.write(if_content)
190     with open(ovs_file, 'w') as fh:
191         fh.write(bridge_content)
192     logging.info("New network ifcfg files written")
193     logging.info("Restarting Linux networking")
194     try:
195         subprocess.check_call(['systemctl', 'restart', 'network'])
196     except subprocess.CalledProcessError:
197         logging.error("Failed to restart Linux networking")
198         raise
199
200
201 def detach_interface_from_ovs(network):
202     """
203     Detach interface from OVS for baremetal deployments
204     :param network: Apex network to detach single interface from
205     :return: None
206     """
207
208     bridge = NET_MAP[network]
209     logging.debug("Detaching interfaces from bridge on network: {}".format(
210         network))
211     # ensure bridge exists
212     if not is_ovs_bridge(bridge):
213         return
214
215     # check if real port is on bridge
216     for interface in dump_ovs_ports(bridge):
217         if interface and not interface.startswith('vnet'):
218             logging.debug("Interface found: {}".format(interface))
219             real_interface = interface
220             break
221     else:
222         logging.info("No jumphost interface exists on bridge {}".format(
223             bridge))
224         return
225
226     # check if original backup ifcfg file exists or create
227     orig_ifcfg_file = os.path.join(NET_CFG_PATH,
228                                    "ifcfg-{}.orig".format(real_interface))
229     ifcfg_file = orig_ifcfg_file[:-len('.orig')]
230     bridge_ifcfg_file = os.path.join(NET_CFG_PATH,
231                                      "ifcfg-{}".format(bridge))
232     if os.path.isfile(orig_ifcfg_file):
233         logging.debug("Original interface file found: "
234                       "{}".format(orig_ifcfg_file))
235     else:
236         logging.info("No original ifcfg file found...will attempt to use "
237                      "bridge ifcfg file and re-create")
238         if os.path.isfile(bridge_ifcfg_file):
239             ifcfg_params = generate_ifcfg_params(bridge_ifcfg_file, network)
240             if_content = """DEVICE={}
241 BOOTPROTO=static
242 ONBOOT=yes
243 TYPE=Ethernet
244 NM_CONTROLLED=no""".format(real_interface)
245             for param, value in ifcfg_params.items():
246                 if value:
247                     if_content += "\n{}={}".format(param, value)
248             logging.debug("Interface file content:\n{}".format(if_content))
249             # write original backup
250             with open(orig_ifcfg_file, 'w') as fh:
251                 fh.write(if_content)
252             logging.debug("Original interface file created: "
253                           "{}".format(orig_ifcfg_file))
254         else:
255             logging.error("Unable to find original interface config file: {} "
256                           "or bridge config file:{}".format(orig_ifcfg_file,
257                                                             bridge_ifcfg_file))
258             raise FileNotFoundError("Unable to locate bridge or original "
259                                     "interface ifcfg file")
260
261     # move original file back and rewrite bridge ifcfg
262     shutil.move(orig_ifcfg_file, ifcfg_file)
263     bridge_content = """DEVICE={}
264 DEVICETYPE=ovs
265 BOOTPROTO=static
266 ONBOOT=yes
267 TYPE=OVSBridge
268 PROMISC=yes""".format(bridge)
269     with open(bridge_ifcfg_file, 'w') as fh:
270         fh.write(bridge_content)
271     # restart linux networking
272     logging.info("Restarting Linux networking")
273     try:
274         subprocess.check_call(['systemctl', 'restart', 'network'])
275     except subprocess.CalledProcessError:
276         logging.error("Failed to restart Linux networking")
277         raise
278
279
280 def remove_ovs_bridge(network):
281     """
282     Unconfigure and remove an OVS bridge
283     :param network: Apex network to remove OVS bridge for
284     :return:
285     """
286     bridge = NET_MAP[network]
287     if is_ovs_bridge(bridge):
288         logging.info("Removing bridge: {}".format(bridge))
289         try:
290             subprocess.check_call(['ovs-vsctl', 'del-br', bridge])
291         except subprocess.CalledProcessError:
292             logging.error('Unable to destroy OVS bridge')
293             raise
294
295         logging.debug('Bridge destroyed')
296         bridge_ifcfg_file = os.path.join(NET_CFG_PATH,
297                                          "ifcfg-{}".format(bridge))
298         if os.path.isfile(bridge_ifcfg_file):
299             os.remove(bridge_ifcfg_file)
300             logging.debug("Bridge ifcfg file removed: {}".format(
301                 bridge_ifcfg_file))
302         else:
303             logging.debug('Bridge ifcfg file not found')