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