migrating to proposed common network settings file
[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
16 from . import ip_utils
17 from .common import utils
18 from .common.constants import (
19     CONTROLLER,
20     COMPUTE,
21     ROLES,
22     DOMAIN_NAME,
23     DNS_SERVERS,
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, network_isolation):
43         init_dict = {}
44         if type(filename) is 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 type(val) is 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.network_isolation = network_isolation
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             return self['networks'][network][0]
75         else:
76             return self['networks'][network]
77
78     def _validate_input(self):
79         """
80         Validates the network settings file and populates all fields.
81
82         NetworkSettingsException will be raised if validation fails.
83         """
84         if not self['networks'].get(ADMIN_NETWORK, {}).get('enabled', False):
85             raise NetworkSettingsException("You must enable admin network "
86                                            "and configure it explicitly or "
87                                            "use auto-detection")
88
89         for network in OPNFV_NETWORK_TYPES:
90             if network in self['networks']:
91                 _network = self.get_network(network)
92                 if _network.get('enabled', True):
93                     logging.info("{} enabled".format(network))
94                     self._config_required_settings(network)
95                     if network == EXTERNAL_NETWORK:
96                         nicmap = _network['nic_mapping']
97                     else:
98                         nicmap = _network['nic_mapping']
99                     iface = nicmap[CONTROLLER]['members'][0]
100                     self._config_ip_range(network=network,
101                                           interface=iface,
102                                           ip_range='usable_ip_range',
103                                           start_offset=21, end_offset=21)
104                     self.enabled_network_list.append(network)
105                     self._validate_overcloud_nic_order(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         self['dns_servers'] = self.get('dns_servers', DNS_SERVERS)
117
118     def _validate_overcloud_nic_order(self, network):
119         """
120         Detects if nic order is specified per profile (compute/controller)
121         for network
122
123         If nic order is specified in a network for a profile, it should be
124         specified for every network with that profile other than admin network
125
126         Duplicate nic names are also not allowed across different networks
127
128         :param network: network to detect if nic order present
129         :return: None
130         """
131         for role in ROLES:
132             _network = self.get_network(network)
133             _nicmap = _network.get('nic_mapping', {})
134             _role = _nicmap.get(role, {})
135             interfaces = _role.get('members', [])
136
137             if interfaces:
138                 interface = interfaces[0]
139                 if type(_role.get('vlan', 'native')) is not int and \
140                    any(y == interface for x, y in self.nics[role].items()):
141                     raise NetworkSettingsException(
142                         "Duplicate {} already specified for "
143                         "another network".format(interface))
144                 self.nics[role][network] = interface
145                 self.nics_specified[role] = True
146                 logging.info("{} nic order specified for network {"
147                              "}".format(role, network))
148             else:
149                 raise NetworkSettingsException(
150                     "Interface members are not supplied for {} network "
151                     "for the {} role. Please add nic assignments"
152                     "".format(network, role))
153
154     def _config_required_settings(self, network):
155         """
156         Configures either CIDR or bridged_interface setting
157
158         cidr takes precedence if both cidr and bridged_interface are specified
159         for a given network.
160
161         When using bridged_interface, we will detect network setting on the
162         given NIC in the system. The resulting config in settings object will
163         be an ipaddress.network object, replacing the NIC name.
164         """
165         _network = self.get_network(network)
166         # if vlan not defined then default it to native
167         if network is not ADMIN_NETWORK:
168             for role in ROLES:
169                 if 'vlan' not in _network['nic_mapping'][role]:
170                     _network['nic_mapping'][role]['vlan'] = 'native'
171
172         cidr = _network.get('cidr')
173
174         if cidr:
175             cidr = ipaddress.ip_network(_network['cidr'])
176             _network['cidr'] = cidr
177             logging.info("{}_cidr: {}".format(network, cidr))
178         elif 'installer_vm' in _network:
179             ucloud_if_list = _network['installer_vm']['members']
180             # If cidr is not specified, we need to know if we should find
181             # IPv6 or IPv4 address on the interface
182             ip = ipaddress.ip_address(_network['installer_vm']['ip'])
183             nic_if = ip_utils.get_interface(ucloud_if_list[0], ip.version)
184             if nic_if:
185                 ucloud_if_list = [nic_if]
186                 logging.info("{}_bridged_interface: {}".
187                              format(network, nic_if))
188             else:
189                 raise NetworkSettingsException(
190                     "Auto detection failed for {}: Unable to find valid "
191                     "ip for interface {}".format(network, ucloud_if_list[0]))
192
193         else:
194             raise NetworkSettingsException(
195                 "Auto detection failed for {}: either installer_vm "
196                 "members or cidr must be specified".format(network))
197
198         # undercloud settings
199         if network == ADMIN_NETWORK:
200             provisioner_ip = _network['installer_vm']['ip']
201             iface = _network['installer_vm']['members'][0]
202             if not provisioner_ip:
203                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
204             self._config_ip_range(network=network, interface=iface,
205                                   ip_range='dhcp_range',
206                                   start_offset=2, count=9)
207             self._config_ip_range(network=network, interface=iface,
208                                   ip_range='introspection_range',
209                                   start_offset=11, count=9)
210         elif network == EXTERNAL_NETWORK:
211             provisioner_ip = _network['installer_vm']['ip']
212             iface = _network['installer_vm']['members'][0]
213             if not provisioner_ip:
214                 _network['installer_vm']['ip'] = self._gen_ip(network, 1)
215             self._config_ip_range(network=network, interface=iface,
216                                   ip_range='floating_ip_range',
217                                   end_offset=2, count=20)
218
219             gateway = _network['gateway']
220             interface = _network['installer_vm']['ip']
221             self._config_gateway(network, gateway, interface)
222
223     def _config_ip_range(self, network, ip_range, interface=None,
224                          start_offset=None, end_offset=None, count=None):
225         """
226         Configures IP range for a given setting.
227         If the setting is already specified, no change will be made.
228         The spec for start_offset, end_offset and count are identical to
229         ip_utils.get_ip_range.
230         """
231         _network = self.get_network(network)
232         if ip_range not in _network:
233             cidr = _network.get('cidr')
234             _ip_range = ip_utils.get_ip_range(start_offset=start_offset,
235                                               end_offset=end_offset,
236                                               count=count,
237                                               cidr=cidr,
238                                               interface=interface)
239             _network[ip_range] = _ip_range.split(',')
240
241         logging.info("Config IP Range: {} {}".format(network, ip_range))
242
243     def _gen_ip(self, network, offset):
244         """
245         Generate and ip offset within the given network
246         """
247         _network = self.get_network(network)
248         cidr = _network.get('cidr')
249         ip = ip_utils.get_ip(offset, cidr)
250         logging.info("Config IP: {} {}".format(network, ip))
251         return ip
252
253     def _config_optional_settings(self, network):
254         """
255         Configures optional settings:
256         - admin_network:
257             - provisioner_ip
258             - dhcp_range
259             - introspection_range
260         - public_network:
261             - provisioner_ip
262             - floating_ip_range
263             - gateway
264         """
265         if network == ADMIN_NETWORK:
266             self._config_ip(network, None, 'provisioner_ip', 1)
267             self._config_ip_range(network=network,
268                                   ip_range='dhcp_range',
269                                   start_offset=2, count=9)
270             self._config_ip_range(network=network,
271                                   ip_range='introspection_range',
272                                   start_offset=11, count=9)
273         elif network == EXTERNAL_NETWORK:
274             self._config_ip(network, None, 'provisioner_ip', 1)
275             self._config_ip_range(network=network,
276                                   ip_range='floating_ip_range',
277                                   end_offset=2, count=20)
278             self._config_gateway(network)
279
280     def _config_gateway(self, network, gateway, interface):
281         """
282         Configures gateway setting for a given network.
283
284         If cidr is specified, we always use the first address in the address
285         space for gateway. Otherwise, we detect the system gateway.
286         """
287         _network = self.get_network(network)
288         if not gateway:
289             cidr = _network.get('cidr')
290             if cidr:
291                 _gateway = ip_utils.get_ip(1, cidr)
292             else:
293                 _gateway = ip_utils.find_gateway(interface)
294
295             if _gateway:
296                 _network['gateway'] = _gateway
297             else:
298                 raise NetworkSettingsException("Failed to set gateway")
299
300         logging.info("Config Gateway: {} {}".format(network, gateway))
301
302     def dump_bash(self, path=None):
303         """
304         Prints settings for bash consumption.
305
306         If optional path is provided, bash string will be written to the file
307         instead of stdout.
308         """
309         def flatten(name, obj, delim=','):
310             """
311             flatten lists to delim separated strings
312             flatten dics to underscored key names and string values
313             """
314             if type(obj) is list:
315                 return "{}=\'{}\'\n".format(name,
316                                             delim.join(map(lambda x: str(x),
317                                                            obj)))
318             elif type(obj) is dict:
319                 flat_str = ''
320                 for k in obj:
321                     flat_str += flatten("{}_{}".format(name, k), obj[k])
322                 return flat_str
323             elif type(obj) is str:
324                 return "{}='{}'\n".format(name, obj)
325             else:
326                 return "{}={}\n".format(name, str(obj))
327
328         bash_str = ''
329         for network in self.enabled_network_list:
330             _network = self.get_network(network)
331             bash_str += flatten(network, _network)
332         bash_str += flatten('enabled_network_list',
333                             self.enabled_network_list, ' ')
334         bash_str += flatten('ip_addr_family', self.get_ip_addr_family())
335         bash_str += flatten('dns_servers', self['dns_servers'], ' ')
336         bash_str += flatten('domain_name', self['dns-domain'], ' ')
337         if path:
338             with open(path, 'w') as file:
339                 file.write(bash_str)
340         else:
341             print(bash_str)
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