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