Merge "Build Congress RPM package"
[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         self.settings_obj['domain_name'] = self.settings_obj.get(
79             'domain_name', constants.DOMAIN_NAME)
80
81     def _validate_overcloud_nic_order(self, network):
82         """
83         Detects if nic order is specified per profile (compute/controller)
84         for network
85
86         If nic order is specified in a network for a profile, it should be
87         specified for every network with that profile other than admin_network
88
89         Duplicate nic names are also not allowed across different networks
90
91         :param network: network to detect if nic order present
92         :return: None
93         """
94
95         for role in constants.ROLES:
96             interface = role+'_interface'
97             nic_index = self.get_enabled_networks().index(network) + 1
98             if interface in self.settings_obj[network]:
99                 if any(y == self.settings_obj[network][interface] for x, y in
100                        self.nics[role].items()):
101                     raise NetworkSettingsException("Duplicate {} already "
102                                                    "specified for "
103                                                    "another network"
104                                                    .format(self.settings_obj
105                                                            [network]
106                                                            [interface]))
107                 self.nics[role][network] = self.settings_obj[network][
108                     interface]
109                 self.nics_specified[role] = True
110                 logging.info("{} nic order specified for network {"
111                              "}".format(role, network))
112             elif self.nics_specified[role]:
113                 logging.error("{} nic order not specified for network {"
114                               "}".format(role, network))
115                 raise NetworkSettingsException("Must specify {} for all "
116                                                "enabled networks (other than "
117                                                " admin) or not specify it for "
118                                                "any".format(interface))
119             else:
120                 logging.info("{} nic order not specified for network {"
121                              "}. Will use logical default "
122                              "nic{}".format(interface, network, nic_index))
123                 self.nics[role][network] = 'nic' + str(nic_index)
124                 nic_index += 1
125
126     def _config_required_settings(self, network):
127         """
128         Configures either CIDR or bridged_interface setting
129
130         cidr takes precedence if both cidr and bridged_interface are specified
131         for a given network.
132
133         When using bridged_interface, we will detect network setting on the
134         given NIC in the system. The resulting config in settings object will
135         be an ipaddress.network object, replacing the NIC name.
136         """
137         # if vlan not defined then default it to native
138         if network is not constants.ADMIN_NETWORK:
139             if 'vlan' not in self.settings_obj[network]:
140                 self.settings_obj[network]['vlan'] = 'native'
141
142         cidr = self.settings_obj[network].get('cidr')
143         nic_name = self.settings_obj[network].get('bridged_interface')
144
145         if cidr:
146             cidr = ipaddress.ip_network(self.settings_obj[network]['cidr'])
147             self.settings_obj[network]['cidr'] = cidr
148             logging.info("{}_cidr: {}".format(network, cidr))
149             return 0
150         elif nic_name:
151             # If cidr is not specified, we need to know if we should find
152             # IPv6 or IPv4 address on the interface
153             if utils.str2bool(self.settings_obj[network].get('ipv6')):
154                 address_family = 6
155             else:
156                 address_family = 4
157             nic_interface = ip_utils.get_interface(nic_name, address_family)
158             if nic_interface:
159                 self.settings_obj[network]['bridged_interface'] = nic_interface
160                 logging.info("{}_bridged_interface: {}".
161                              format(network, nic_interface))
162                 return 0
163             else:
164                 raise NetworkSettingsException("Auto detection failed for {}: "
165                                                "Unable to find valid ip for "
166                                                "interface {}"
167                                                .format(network, nic_name))
168
169         else:
170             raise NetworkSettingsException("Auto detection failed for {}: "
171                                            "either bridge_interface or cidr "
172                                            "must be specified"
173                                            .format(network))
174
175     def _config_ip_range(self, network, setting, start_offset=None,
176                          end_offset=None, count=None):
177         """
178         Configures IP range for a given setting.
179
180         If the setting is already specified, no change will be made.
181
182         The spec for start_offset, end_offset and count are identical to
183         ip_utils.get_ip_range.
184         """
185         ip_range = self.settings_obj[network].get(setting)
186         interface = self.settings_obj[network].get('bridged_interface')
187
188         if not ip_range:
189             cidr = self.settings_obj[network].get('cidr')
190             ip_range = ip_utils.get_ip_range(start_offset=start_offset,
191                                              end_offset=end_offset,
192                                              count=count,
193                                              cidr=cidr,
194                                              interface=interface)
195             self.settings_obj[network][setting] = ip_range
196
197         logging.info("{}_{}: {}".format(network, setting, ip_range))
198
199     def _config_ip(self, network, setting, offset):
200         """
201         Configures IP for a given setting.
202
203         If the setting is already specified, no change will be made.
204
205         The spec for offset is identical to ip_utils.get_ip
206         """
207         ip = self.settings_obj[network].get(setting)
208         interface = self.settings_obj[network].get('bridged_interface')
209
210         if not ip:
211             cidr = self.settings_obj[network].get('cidr')
212             ip = ip_utils.get_ip(offset, cidr, interface)
213             self.settings_obj[network][setting] = ip
214
215         logging.info("{}_{}: {}".format(network, setting, ip))
216
217     def _config_optional_settings(self, network):
218         """
219         Configures optional settings:
220         - admin_network:
221             - provisioner_ip
222             - dhcp_range
223             - introspection_range
224         - public_network:
225             - provisioner_ip
226             - floating_ip_range
227             - gateway
228         """
229         if network == constants.ADMIN_NETWORK:
230             self._config_ip(network, 'provisioner_ip', 1)
231             self._config_ip_range(network=network, setting='dhcp_range',
232                                   start_offset=2, count=9)
233             self._config_ip_range(network=network,
234                                   setting='introspection_range',
235                                   start_offset=11, count=9)
236         elif network == constants.PUBLIC_NETWORK:
237             self._config_ip(network, 'provisioner_ip', 1)
238             self._config_ip_range(network=network,
239                                   setting='floating_ip_range',
240                                   end_offset=2, count=20)
241             self._config_gateway(network)
242
243     def _config_gateway(self, network):
244         """
245         Configures gateway setting for a given network.
246
247         If cidr is specified, we always use the first address in the address
248         space for gateway. Otherwise, we detect the system gateway.
249         """
250         gateway = self.settings_obj[network].get('gateway')
251         interface = self.settings_obj[network].get('bridged_interface')
252
253         if not gateway:
254             cidr = self.settings_obj[network].get('cidr')
255             if cidr:
256                 gateway = ip_utils.get_ip(1, cidr)
257             else:
258                 gateway = ip_utils.find_gateway(interface)
259
260             if gateway:
261                 self.settings_obj[network]['gateway'] = gateway
262             else:
263                 raise NetworkSettingsException("Failed to set gateway")
264
265         logging.info("{}_gateway: {}".format(network, gateway))
266
267     def dump_bash(self, path=None):
268         """
269         Prints settings for bash consumption.
270
271         If optional path is provided, bash string will be written to the file
272         instead of stdout.
273         """
274         bash_str = ''
275         for network in self.enabled_network_list:
276             for key, value in self.settings_obj[network].items():
277                 bash_str += "{}_{}={}\n".format(network, key, value)
278         bash_str += "enabled_network_list='{}'\n" \
279             .format(' '.join(self.enabled_network_list))
280         bash_str += "ip_addr_family={}\n".format(self.get_ip_addr_family())
281         dns_list = ""
282         for dns_server in self.settings_obj['dns_servers']:
283             dns_list = dns_list + "{} ".format(dns_server)
284         dns_list = dns_list.strip()
285         bash_str += "dns_servers=\'{}\'\n".format(dns_list)
286         bash_str += "domain_name=\'{}\'\n".format(self.settings_obj[
287                                                   'domain_name'])
288         if path:
289             with open(path, 'w') as file:
290                 file.write(bash_str)
291         else:
292             print(bash_str)
293
294     def get_ip_addr_family(self):
295         """
296         Returns IP address family for current deployment.
297
298         If any enabled network has IPv6 CIDR, the deployment is classified as
299         IPv6.
300         """
301         for network in self.enabled_network_list:
302             cidr = ipaddress.ip_network(self.settings_obj[network]['cidr'])
303             if cidr.version == 6:
304                 return 6
305
306         return 4
307
308     def get_network_settings(self):
309         """
310         Getter for network settings
311         :return: network settings dictionary
312         """
313         return self.settings_obj
314
315     def get_enabled_networks(self):
316         """
317         Getter for enabled network list
318         :return: list of enabled networks
319         """
320         return self.enabled_network_list
321
322
323 class NetworkSettingsException(Exception):
324     def __init__(self, value):
325         self.value = value
326
327     def __str__(self):
328             return self.value