Allows specifying nic order for overcloud nodes in network settings
[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 from . import ip_utils
14 from .common import constants, utils
15
16
17 class NetworkSettings:
18     """
19     This class parses APEX network settings yaml file into an object. It
20     generates or detects all missing fields for deployment.
21
22     The resulting object will be used later to generate network environment
23     file as well as configuring post deployment networks.
24
25     Currently the parsed object is dumped into a bash global definition file
26     for deploy.sh consumption. This object will later be used directly as
27     deployment script move to python.
28     """
29     def __init__(self, filename, network_isolation):
30         with open(filename, 'r') as network_settings_file:
31             self.settings_obj = yaml.load(network_settings_file)
32             self.network_isolation = network_isolation
33             self.enabled_network_list = []
34             self.nics = {'compute': dict(), 'controller': dict()}
35             self.nics_specified = {'compute': False, 'controller': False}
36             self._validate_input()
37
38     def _validate_input(self):
39         """
40         Validates the network settings file and populates all fields.
41
42         NetworkSettingsException will be raised if validation fails.
43         """
44         if constants.ADMIN_NETWORK not in self.settings_obj or \
45             not utils.str2bool(self.settings_obj[constants.ADMIN_NETWORK].get(
46                 'enabled')):
47             raise NetworkSettingsException("You must enable admin_network "
48                                            "and configure it explicitly or "
49                                            "use auto-detection")
50         if self.network_isolation and \
51             (constants.PUBLIC_NETWORK not in self.settings_obj or not
52                 utils.str2bool(self.settings_obj[constants.PUBLIC_NETWORK].get(
53                     'enabled'))):
54             raise NetworkSettingsException("You must enable public_network "
55                                            "and configure it explicitly or "
56                                            "use auto-detection")
57
58         for network in constants.OPNFV_NETWORK_TYPES:
59             if network in self.settings_obj:
60                 if utils.str2bool(self.settings_obj[network].get('enabled')):
61                     logging.info("{} enabled".format(network))
62                     self._config_required_settings(network)
63                     self._config_ip_range(network=network,
64                                           setting='usable_ip_range',
65                                           start_offset=21, end_offset=21)
66                     self._config_optional_settings(network)
67                     self.enabled_network_list.append(network)
68                     self._validate_overcloud_nic_order(network)
69                 else:
70                     logging.info("{} disabled, will collapse with "
71                                  "admin_network".format(network))
72             else:
73                 logging.info("{} is not in specified, will collapse with "
74                              "admin_network".format(network))
75
76         self.settings_obj['dns_servers'] = self.settings_obj.get(
77             'dns_servers', constants.DNS_SERVERS)
78
79     def _validate_overcloud_nic_order(self, network):
80         """
81         Detects if nic order is specified per profile (compute/controller)
82         for network
83
84         If nic order is specified in a network for a profile, it should be
85         specified for every network with that profile other than admin_network
86
87         Duplicate nic names are also not allowed across different networks
88
89         :param network: network to detect if nic order present
90         :return: None
91         """
92
93         for role in constants.ROLES:
94             interface = role+'_interface'
95             nic_index = self.get_enabled_networks().index(network) + 1
96             if interface in self.settings_obj[network]:
97                 if any(y == self.settings_obj[network][interface] for x, y in
98                        self.nics[role].items()):
99                     raise NetworkSettingsException("Duplicate {} already "
100                                                    "specified for "
101                                                    "another network"
102                                                    .format(self.settings_obj
103                                                            [network]
104                                                            [interface]))
105                 self.nics[role][network] = self.settings_obj[network][
106                     interface]
107                 self.nics_specified[role] = True
108                 logging.info("{} nic order specified for network {"
109                              "}".format(role, network))
110             elif self.nics_specified[role]:
111                 logging.error("{} nic order not specified for network {"
112                               "}".format(role, network))
113                 raise NetworkSettingsException("Must specify {} for all "
114                                                "enabled networks (other than "
115                                                " admin) or not specify it for "
116                                                "any".format(interface))
117             else:
118                 logging.info("{} nic order not specified for network {"
119                              "}. Will use logical default "
120                              "nic{}".format(interface, network, nic_index))
121                 self.nics[role][network] = 'nic' + str(nic_index)
122                 nic_index += 1
123
124     def _config_required_settings(self, network):
125         """
126         Configures either CIDR or bridged_interface setting
127
128         cidr takes precedence if both cidr and bridged_interface are specified
129         for a given network.
130
131         When using bridged_interface, we will detect network setting on the
132         given NIC in the system. The resulting config in settings object will
133         be an ipaddress.network object, replacing the NIC name.
134         """
135         # if vlan not defined then default it to native
136         if network is not constants.ADMIN_NETWORK:
137             if 'vlan' not in self.settings_obj[network]:
138                 self.settings_obj[network]['vlan'] = 'native'
139
140         cidr = self.settings_obj[network].get('cidr')
141         nic_name = self.settings_obj[network].get('bridged_interface')
142
143         if cidr:
144             cidr = ipaddress.ip_network(self.settings_obj[network]['cidr'])
145             self.settings_obj[network]['cidr'] = cidr
146             logging.info("{}_cidr: {}".format(network, cidr))
147             return 0
148         elif nic_name:
149             # If cidr is not specified, we need to know if we should find
150             # IPv6 or IPv4 address on the interface
151             if utils.str2bool(self.settings_obj[network].get('ipv6')):
152                 address_family = 6
153             else:
154                 address_family = 4
155             nic_interface = ip_utils.get_interface(nic_name, address_family)
156             if nic_interface:
157                 self.settings_obj[network]['bridged_interface'] = nic_interface
158                 logging.info("{}_bridged_interface: {}".
159                              format(network, nic_interface))
160                 return 0
161             else:
162                 raise NetworkSettingsException("Auto detection failed for {}: "
163                                                "Unable to find valid ip for "
164                                                "interface {}"
165                                                .format(network, nic_name))
166
167         else:
168             raise NetworkSettingsException("Auto detection failed for {}: "
169                                            "either bridge_interface or cidr "
170                                            "must be specified"
171                                            .format(network))
172
173     def _config_ip_range(self, network, setting, start_offset=None,
174                          end_offset=None, count=None):
175         """
176         Configures IP range for a given setting.
177
178         If the setting is already specified, no change will be made.
179
180         The spec for start_offset, end_offset and count are identical to
181         ip_utils.get_ip_range.
182         """
183         ip_range = self.settings_obj[network].get(setting)
184         interface = self.settings_obj[network].get('bridged_interface')
185
186         if not ip_range:
187             cidr = self.settings_obj[network].get('cidr')
188             ip_range = ip_utils.get_ip_range(start_offset=start_offset,
189                                              end_offset=end_offset,
190                                              count=count,
191                                              cidr=cidr,
192                                              interface=interface)
193             self.settings_obj[network][setting] = ip_range
194
195         logging.info("{}_{}: {}".format(network, setting, ip_range))
196
197     def _config_ip(self, network, setting, offset):
198         """
199         Configures IP for a given setting.
200
201         If the setting is already specified, no change will be made.
202
203         The spec for offset is identical to ip_utils.get_ip
204         """
205         ip = self.settings_obj[network].get(setting)
206         interface = self.settings_obj[network].get('bridged_interface')
207
208         if not ip:
209             cidr = self.settings_obj[network].get('cidr')
210             ip = ip_utils.get_ip(offset, cidr, interface)
211             self.settings_obj[network][setting] = ip
212
213         logging.info("{}_{}: {}".format(network, setting, ip))
214
215     def _config_optional_settings(self, network):
216         """
217         Configures optional settings:
218         - admin_network:
219             - provisioner_ip
220             - dhcp_range
221             - introspection_range
222         - public_network:
223             - provisioner_ip
224             - floating_ip
225             - gateway
226         """
227         if network == constants.ADMIN_NETWORK:
228             self._config_ip(network, 'provisioner_ip', 1)
229             self._config_ip_range(network=network, setting='dhcp_range',
230                                   start_offset=2, count=9)
231             self._config_ip_range(network=network,
232                                   setting='introspection_range',
233                                   start_offset=11, count=9)
234         elif network == constants.PUBLIC_NETWORK:
235             self._config_ip(network, 'provisioner_ip', 1)
236             self._config_ip_range(network=network,
237                                   setting='floating_ip',
238                                   end_offset=2, count=20)
239             self._config_gateway(network)
240
241     def _config_gateway(self, network):
242         """
243         Configures gateway setting for a given network.
244
245         If cidr is specified, we always use the first address in the address
246         space for gateway. Otherwise, we detect the system gateway.
247         """
248         gateway = self.settings_obj[network].get('gateway')
249         interface = self.settings_obj[network].get('bridged_interface')
250
251         if not gateway:
252             cidr = self.settings_obj[network].get('cidr')
253             if cidr:
254                 gateway = ip_utils.get_ip(1, cidr)
255             else:
256                 gateway = ip_utils.find_gateway(interface)
257
258             if gateway:
259                 self.settings_obj[network]['gateway'] = gateway
260             else:
261                 raise NetworkSettingsException("Failed to set gateway")
262
263         logging.info("{}_gateway: {}".format(network, gateway))
264
265     def dump_bash(self, path=None):
266         """
267         Prints settings for bash consumption.
268
269         If optional path is provided, bash string will be written to the file
270         instead of stdout.
271         """
272         bash_str = ''
273         for network in self.enabled_network_list:
274             for key, value in self.settings_obj[network].items():
275                 bash_str += "{}_{}={}\n".format(network, key, value)
276         bash_str += "enabled_network_list='{}'\n" \
277             .format(' '.join(self.enabled_network_list))
278         bash_str += "ip_addr_family={}\n".format(self.get_ip_addr_family())
279         dns_list = ""
280         for dns_server in self.settings_obj['dns_servers']:
281             dns_list = dns_list + "{} ".format(dns_server)
282         dns_list = dns_list.strip()
283         bash_str += "dns_servers=\'{}\'\n".format(dns_list)
284         if path:
285             with open(path, 'w') as file:
286                 file.write(bash_str)
287         else:
288             print(bash_str)
289
290     def get_ip_addr_family(self):
291         """
292         Returns IP address family for current deployment.
293
294         If any enabled network has IPv6 CIDR, the deployment is classified as
295         IPv6.
296         """
297         for network in self.enabled_network_list:
298             cidr = ipaddress.ip_network(self.settings_obj[network]['cidr'])
299             if cidr.version == 6:
300                 return 6
301
302         return 4
303
304     def get_network_settings(self):
305         """
306         Getter for network settings
307         :return: network settings dictionary
308         """
309         return self.settings_obj
310
311     def get_enabled_networks(self):
312         """
313         Getter for enabled network list
314         :return: list of enabled networks
315         """
316         return self.enabled_network_list
317
318
319 class NetworkSettingsException(Exception):
320     def __init__(self, value):
321         self.value = value
322
323     def __str__(self):
324             return self.value