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