Bumps OVS version to 2.8 for OVN
[apex.git] / apex / settings / network_settings.py
1 ##############################################################################
2 # Copyright (c) 2016 Feng Pan (fpan@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 ipaddress
11 import logging
12 from copy import copy
13
14 import yaml
15
16 from apex.common import utils
17 from apex.common.constants import (
18     CONTROLLER,
19     COMPUTE,
20     ROLES,
21     DOMAIN_NAME,
22     DNS_SERVERS,
23     NTP_SERVER,
24     ADMIN_NETWORK,
25     EXTERNAL_NETWORK,
26     OPNFV_NETWORK_TYPES,
27 )
28 from apex.network import ip_utils
29
30
31 class NetworkSettings(dict):
32     """
33     This class parses APEX network settings yaml file into an object. It
34     generates or detects all missing fields for deployment.
35
36     The resulting object will be used later to generate network environment
37     file as well as configuring post deployment networks.
38
39     Currently the parsed object is dumped into a bash global definition file
40     for deploy.sh consumption. This object will later be used directly as
41     deployment script move to python.
42     """
43     def __init__(self, filename):
44         init_dict = {}
45         if isinstance(filename, str):
46             with open(filename, 'r') as network_settings_file:
47                 init_dict = yaml.safe_load(network_settings_file)
48         else:
49             # assume input is a dict to build from
50             init_dict = filename
51         super().__init__(init_dict)
52
53         if 'apex' in self:
54             # merge two dicts Non-destructively
55             def merge(pri, sec):
56                 for key, val in sec.items():
57                     if key in pri:
58                         if isinstance(val, dict):
59                             merge(pri[key], val)
60                         # else
61                         # do not overwrite what's already there
62                     else:
63                         pri[key] = val
64             # merge the apex specific config into the first class settings
65             merge(self, copy(self['apex']))
66
67         self.enabled_network_list = []
68         self.nics = {COMPUTE: {}, CONTROLLER: {}}
69         self.nics_specified = {COMPUTE: False, CONTROLLER: False}
70         self._validate_input()
71
72     def get_network(self, network):
73         if network == EXTERNAL_NETWORK and self['networks'][network]:
74             for net in self['networks'][network]:
75                 if 'public' in net:
76                     return net
77
78             raise NetworkSettingsException("The external network, "
79                                            "'public', should be defined "
80                                            "when external networks are "
81                                            "enabled")
82         else:
83             return self['networks'][network]
84
85     def _validate_input(self):
86         """
87         Validates the network settings file and populates all fields.
88
89         NetworkSettingsException will be raised if validation fails.
90         """
91         if not self['networks'].get(ADMIN_NETWORK, {}).get('enabled', False):
92             raise NetworkSettingsException("You must enable admin network "
93                                            "and configure it explicitly or "
94                                            "use auto-detection")
95
96         for network in OPNFV_NETWORK_TYPES:
97             if network in self['networks']:
98                 _network = self.get_network(network)
99                 if _network.get('enabled', True):
100                     logging.info("{} enabled".format(network))
101                     self._config_required_settings(network)
102                     nicmap = _network['nic_mapping']
103                     self._validate_overcloud_nic_order(network)
104                     iface = nicmap[CONTROLLER]['members'][0]
105                     self._config_ip_range(network=network,
106                                           interface=iface,
107                                           ip_range='overcloud_ip_range',
108                                           start_offset=21, end_offset=21)
109                     self.enabled_network_list.append(network)
110                     # TODO self._config_optional_settings(network)
111                 else:
112                     logging.info("{} disabled, will collapse with "
113                                  "admin network".format(network))
114             else:
115                 logging.info("{} is not in specified, will collapse with "
116                              "admin network".format(network))
117
118         if 'dns-domain' not in self:
119             self['domain_name'] = DOMAIN_NAME
120         else:
121             self['domain_name'] = self['dns-domain']
122         self['dns_servers'] = self.get('dns_nameservers', DNS_SERVERS)
123         self['ntp_servers'] = self.get('ntp', NTP_SERVER)
124
125     def _validate_overcloud_nic_order(self, network):
126         """
127         Detects if nic order is specified per profile (compute/controller)
128         for network
129
130         If nic order is specified in a network for a profile, it should be
131         specified for every network with that profile other than admin network
132
133         Duplicate nic names are also not allowed across different networks
134
135         :param network: network to detect if nic order present
136         :return: None
137         """
138         for role in ROLES:
139             _network = self.get_network(network)
140             _nicmap = _network.get('nic_mapping', {})
141             _role = _nicmap.get(role, {})
142             interfaces = _role.get('members', [])
143
144             if interfaces:
145                 interface = interfaces[0]
146                 if not isinstance(_role.get('vlan', 'native'), int) and \
147                    any(y == interface for x, y in self.nics[role].items()):
148                     raise NetworkSettingsException(
149                         "Duplicate {} already specified for "
150                         "another network".format(interface))
151                 self.nics[role][network] = interface
152                 self.nics_specified[role] = True
153                 logging.info("{} nic order specified for network {"
154                              "}".format(role, network))
155             else:
156                 raise NetworkSettingsException(
157                     "Interface members are not supplied for {} network "
158                     "for the {} role. Please add nic assignments"
159                     "".format(network, role))
160
161     def _config_required_settings(self, network):
162         """
163         Configures either CIDR or bridged_interface setting
164
165         cidr takes precedence if both cidr and bridged_interface are specified
166         for a given network.
167
168         When using bridged_interface, we will detect network setting on the
169         given NIC in the system. The resulting config in settings object will
170         be an ipaddress.network object, replacing the NIC name.
171         """
172         _network = self.get_network(network)
173         # if vlan not defined then default it to native
174         if network is not ADMIN_NETWORK:
175             for role in ROLES:
176                 if 'vlan' not in _network['nic_mapping'][role]:
177                     _network['nic_mapping'][role]['vlan'] = 'native'
178
179         cidr = _network.get('cidr')
180
181         if cidr:
182             cidr = ipaddress.ip_network(_network['cidr'])
183             _network['cidr'] = cidr
184             logging.info("{}_cidr: {}".format(network, cidr))
185         elif 'installer_vm' in _network:
186             ucloud_if_list = _network['installer_vm']['members']
187             # If cidr is not specified, we need to know if we should find
188             # IPv6 or IPv4 address on the interface
189             ip = ipaddress.ip_address(_network['installer_vm']['ip'])
190             nic_if = ip_utils.get_interface(ucloud_if_list[0], ip.version)
191             if nic_if:
192                 logging.info("{}_bridged_interface: {}".
193                              format(network, nic_if))
194             else:
195                 raise NetworkSettingsException(
196                     "Auto detection failed for {}: Unable to find valid "
197                     "ip for interface {}".format(network, ucloud_if_list[0]))
198
199         else:
200             raise NetworkSettingsException(
201                 "Auto detection failed for {}: either installer_vm "
202                 "members or cidr must be specified".format(network))
203
204         # undercloud settings
205         if network == ADMIN_NETWORK:
206             provisioner_ip = _network['installer_vm']['ip']
207             iface = _network['installer_vm']['members'][0]
208             if not provisioner_ip:
209                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
210             self._config_ip_range(network=network, interface=iface,
211                                   ip_range='dhcp_range',
212                                   start_offset=2, count=9)
213             self._config_ip_range(network=network, interface=iface,
214                                   ip_range='introspection_range',
215                                   start_offset=11, count=9)
216         elif network == EXTERNAL_NETWORK:
217             provisioner_ip = _network['installer_vm']['ip']
218             iface = _network['installer_vm']['members'][0]
219             if not provisioner_ip:
220                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
221             self._config_ip_range(network=network, interface=iface,
222                                   ip_range='floating_ip_range',
223                                   end_offset=2, count=20)
224
225             gateway = _network['gateway']
226             interface = _network['installer_vm']['ip']
227             self._config_gateway(network, gateway, interface)
228
229     def _config_ip_range(self, network, ip_range, interface=None,
230                          start_offset=None, end_offset=None, count=None):
231         """
232         Configures IP range for a given setting.
233         If the setting is already specified, no change will be made.
234         The spec for start_offset, end_offset and count are identical to
235         ip_utils.get_ip_range.
236         """
237         _network = self.get_network(network)
238         if ip_range not in _network:
239             cidr = _network.get('cidr')
240             _ip_range = ip_utils.get_ip_range(start_offset=start_offset,
241                                               end_offset=end_offset,
242                                               count=count,
243                                               cidr=cidr,
244                                               interface=interface)
245             _network[ip_range] = _ip_range.split(',')
246
247         logging.info("Config IP Range: {} {}".format(network, ip_range))
248
249     def _gen_ip(self, network, offset):
250         """
251         Generate and ip offset within the given network
252         """
253         _network = self.get_network(network)
254         cidr = _network.get('cidr')
255         ip = ip_utils.get_ip(offset, cidr)
256         logging.info("Config IP: {} {}".format(network, ip))
257         return ip
258
259     def _config_optional_settings(self, network):
260         """
261         Configures optional settings:
262         - admin_network:
263             - provisioner_ip
264             - dhcp_range
265             - introspection_range
266         - public_network:
267             - provisioner_ip
268             - floating_ip_range
269             - gateway
270         """
271         if network == ADMIN_NETWORK:
272             # FIXME: _config_ip  function does not exist!
273             self._config_ip(network, None, 'provisioner_ip', 1)
274             self._config_ip_range(network=network,
275                                   ip_range='dhcp_range',
276                                   start_offset=2, count=9)
277             self._config_ip_range(network=network,
278                                   ip_range='introspection_range',
279                                   start_offset=11, count=9)
280         elif network == EXTERNAL_NETWORK:
281             # FIXME: _config_ip  function does not exist!
282             self._config_ip(network, None, 'provisioner_ip', 1)
283             self._config_ip_range(network=network,
284                                   ip_range='floating_ip_range',
285                                   end_offset=2, count=20)
286             self._config_gateway(network)
287
288     def _config_gateway(self, network, gateway, interface):
289         """
290         Configures gateway setting for a given network.
291
292         If cidr is specified, we always use the first address in the address
293         space for gateway. Otherwise, we detect the system gateway.
294         """
295         _network = self.get_network(network)
296         if not gateway:
297             cidr = _network.get('cidr')
298             if cidr:
299                 _gateway = ip_utils.get_ip(1, cidr)
300             else:
301                 _gateway = ip_utils.find_gateway(interface)
302
303             if _gateway:
304                 _network['gateway'] = _gateway
305             else:
306                 raise NetworkSettingsException("Failed to set gateway")
307
308         logging.info("Config Gateway: {} {}".format(network, gateway))
309
310     def get_ip_addr_family(self,):
311         """
312         Returns IP address family for current deployment.
313
314         If any enabled network has IPv6 CIDR, the deployment is classified as
315         IPv6.
316         """
317         return max([
318             ipaddress.ip_network(self.get_network(n)['cidr']).version
319             for n in self.enabled_network_list])
320
321
322 class NetworkSettingsException(Exception):
323     def __init__(self, value):
324         self.value = value
325
326     def __str__(self):
327             return self.value