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