f656683451087ddf75aacf5191c5fbf48a66daa1
[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         if network is not ADMIN_NETWORK:
171             for role in ROLES:
172                 if 'vlan' not in _network['nic_mapping'][role]:
173                     _network['nic_mapping'][role]['vlan'] = 'native'
174
175         cidr = _network.get('cidr')
176
177         if cidr:
178             cidr = ipaddress.ip_network(_network['cidr'])
179             _network['cidr'] = cidr
180             logging.info("{}_cidr: {}".format(network, cidr))
181         elif 'installer_vm' in _network:
182             ucloud_if_list = _network['installer_vm']['members']
183             # If cidr is not specified, we need to know if we should find
184             # IPv6 or IPv4 address on the interface
185             ip = ipaddress.ip_address(_network['installer_vm']['ip'])
186             nic_if = ip_utils.get_interface(ucloud_if_list[0], ip.version)
187             if nic_if:
188                 logging.info("{}_bridged_interface: {}".
189                              format(network, nic_if))
190             else:
191                 raise NetworkSettingsException(
192                     "Auto detection failed for {}: Unable to find valid "
193                     "ip for interface {}".format(network, ucloud_if_list[0]))
194
195         else:
196             raise NetworkSettingsException(
197                 "Auto detection failed for {}: either installer_vm "
198                 "members or cidr must be specified".format(network))
199
200         # undercloud settings
201         if network == ADMIN_NETWORK:
202             provisioner_ip = _network['installer_vm']['ip']
203             iface = _network['installer_vm']['members'][0]
204             if not provisioner_ip:
205                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
206             self._config_ip_range(network=network, interface=iface,
207                                   ip_range='dhcp_range',
208                                   start_offset=2, count=9)
209             self._config_ip_range(network=network, interface=iface,
210                                   ip_range='introspection_range',
211                                   start_offset=11, count=9)
212         elif network == EXTERNAL_NETWORK:
213             provisioner_ip = _network['installer_vm']['ip']
214             iface = _network['installer_vm']['members'][0]
215             if not provisioner_ip:
216                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
217             self._config_ip_range(network=network, interface=iface,
218                                   ip_range='floating_ip_range',
219                                   end_offset=2, count=20)
220
221             gateway = _network['gateway']
222             interface = _network['installer_vm']['ip']
223             self._config_gateway(network, gateway, interface)
224
225     def _config_ip_range(self, network, ip_range, interface=None,
226                          start_offset=None, end_offset=None, count=None):
227         """
228         Configures IP range for a given setting.
229         If the setting is already specified, no change will be made.
230         The spec for start_offset, end_offset and count are identical to
231         ip_utils.get_ip_range.
232         """
233         _network = self.get_network(network)
234         if ip_range not in _network:
235             cidr = _network.get('cidr')
236             _ip_range = ip_utils.get_ip_range(start_offset=start_offset,
237                                               end_offset=end_offset,
238                                               count=count,
239                                               cidr=cidr,
240                                               interface=interface)
241             _network[ip_range] = _ip_range.split(',')
242
243         logging.info("Config IP Range: {} {}".format(network, ip_range))
244
245     def _gen_ip(self, network, offset):
246         """
247         Generate and ip offset within the given network
248         """
249         _network = self.get_network(network)
250         cidr = _network.get('cidr')
251         ip = ip_utils.get_ip(offset, cidr)
252         logging.info("Config IP: {} {}".format(network, ip))
253         return ip
254
255     def _config_optional_settings(self, network):
256         """
257         Configures optional settings:
258         - admin_network:
259             - provisioner_ip
260             - dhcp_range
261             - introspection_range
262         - public_network:
263             - provisioner_ip
264             - floating_ip_range
265             - gateway
266         """
267         if network == ADMIN_NETWORK:
268             # FIXME: _config_ip  function does not exist!
269             self._config_ip(network, None, 'provisioner_ip', 1)
270             self._config_ip_range(network=network,
271                                   ip_range='dhcp_range',
272                                   start_offset=2, count=9)
273             self._config_ip_range(network=network,
274                                   ip_range='introspection_range',
275                                   start_offset=11, count=9)
276         elif network == EXTERNAL_NETWORK:
277             # FIXME: _config_ip  function does not exist!
278             self._config_ip(network, None, 'provisioner_ip', 1)
279             self._config_ip_range(network=network,
280                                   ip_range='floating_ip_range',
281                                   end_offset=2, count=20)
282             self._config_gateway(network)
283
284     def _config_gateway(self, network, gateway, interface):
285         """
286         Configures gateway setting for a given network.
287
288         If cidr is specified, we always use the first address in the address
289         space for gateway. Otherwise, we detect the system gateway.
290         """
291         _network = self.get_network(network)
292         if not gateway:
293             cidr = _network.get('cidr')
294             if cidr:
295                 _gateway = ip_utils.get_ip(1, cidr)
296             else:
297                 _gateway = ip_utils.find_gateway(interface)
298
299             if _gateway:
300                 _network['gateway'] = _gateway
301             else:
302                 raise NetworkSettingsException("Failed to set gateway")
303
304         logging.info("Config Gateway: {} {}".format(network, gateway))
305
306     def get_ip_addr_family(self,):
307         """
308         Returns IP address family for current deployment.
309
310         If any enabled network has IPv6 CIDR, the deployment is classified as
311         IPv6.
312         """
313         return max([
314             ipaddress.ip_network(self.get_network(n)['cidr']).version
315             for n in self.enabled_network_list])
316
317
318 class NetworkSettingsException(Exception):
319     def __init__(self, value):
320         self.value = value
321
322     def __str__(self):
323             return self.value