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