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