4bcee33fec24ac0ad79dd00e6cb30363b21a3ac0
[ta/cm-plugins.git] / validators / src / CaasValidation.py
1 #!/usr/bin/python
2 # Copyright 2019 Nokia
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #    http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import json
17 import re
18 import base64
19 import logging
20 import ipaddr
21 from cmframework.apis import cmvalidator
22 from cmdatahandlers.api import validation
23 from cmdatahandlers.api import configerror
24
25
26 class CaasValidationError(configerror.ConfigError):
27     def __init__(self, description):
28         configerror.ConfigError.__init__(
29             self, 'Validation error in caas_validation: {}'.format(description))
30
31
32 class CaasValidationUtils(object):
33
34     def __init__(self):
35         pass
36
37     @staticmethod
38     def check_key_in_dict(key, dictionary):
39         if key not in dictionary:
40             raise CaasValidationError("{} cannot be found in {} ".format(key, dictionary))
41
42     def get_every_key_occurrence(self, var, key):
43         if hasattr(var, 'iteritems'):
44             for k, v in var.iteritems():
45                 if k == key:
46                     yield v
47                 if isinstance(v, dict):
48                     for result in self.get_every_key_occurrence(v, key):
49                         yield result
50                 elif isinstance(v, list):
51                     for d in v:
52                         for result in self.get_every_key_occurrence(d, key):
53                             yield result
54
55     @staticmethod
56     def is_optional_param_present(key, dictionary):
57         if key not in dictionary:
58             logging.info('{} key is not in the config dictionary, since this is an optional '
59                          'parameter, validation is skipped.'.format(key))
60             return False
61         if not dictionary[key]:
62             logging.info('Although {} key is in the config dictionary the correspondig value is '
63                          'empty, since this is an optional parametery, '
64                          'validation is skipped.'.format(key))
65             return False
66         return True
67
68
69 class CaasValidation(cmvalidator.CMValidator):
70     SUBSCRIPTION = r'^cloud\.caas|cloud\.hosts|cloud\.networking|cloud\.network_profiles$'
71     CAAS_DOMAIN = 'cloud.caas'
72     HOSTS_DOMAIN = 'cloud.hosts'
73     NETW_DOMAIN = 'cloud.networking'
74     NETPROF_DOMAIN = 'cloud.network_profiles'
75
76     SERV_PROF = 'service_profiles'
77     CAAS_PROFILE_PATTERN = 'caas_master|caas_worker'
78     CIDR = 'cidr'
79
80     DOCKER_SIZE_QUOTA = "docker_size_quota"
81     DOCKER_SIZE_QUOTA_PATTERN = r"^\d*[G,M,K]$"
82
83     HELM_OP_TIMEOUT = "helm_operation_timeout"
84
85     DOCKER0_CIDR = "docker0_cidr"
86
87     OAM_CIDR = "oam_cidr"
88
89     INSTANTIATION_TIMEOUT = "instantiation_timeout"
90
91     ENCRYPTED_CA = "encrypted_ca"
92     ENCRYPTED_CA_KEY = "encrypted_ca_key"
93
94     CLUSTER_NETS = 'cluster_networks'
95     TENANT_NETS = 'tenant_networks'
96
97     BLOG_FORWARDING = "infra_log_store"
98     LOG_FORWARDING = "log_forwarding"
99     URL_PORT_PATTERN = r"^(?:https?|udp|tcp):(?:\/\/)(?:((?:[\w\.-]+|" \
100                        r"\[(([1-9a-f][0-9a-f]{0,3}|\:)\:[1-9a-f][0-9a-f]{0,3}){0,7}\])\:[0-9]+))"
101     FLUENTD_PLUGINS = ['elasticsearch', 'remote_syslog']
102     INFRA_LOG_FLUENTD_PLUGINS = ['elasticsearch', 'remote_syslog']
103     LOG_FW_STREAM = ['stdout', 'stderr', 'both']
104
105     DOMAIN_NAME = "dns_domain"
106     DOMAIN_NAME_PATTERN = r"^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
107
108     def __init__(self):
109         cmvalidator.CMValidator.__init__(self)
110         self.validation_utils = validation.ValidationUtils()
111         self.conf = None
112         self.caas_conf = None
113         self.caas_utils = CaasValidationUtils()
114
115     def get_subscription_info(self):
116         return self.SUBSCRIPTION
117
118     def validate_set(self, props):
119         if not self.is_caas_mandatory(props):
120             logging.info("{} not found in {}, caas validation is not needed.".format(
121                 self.CAAS_PROFILE_PATTERN, self.HOSTS_DOMAIN))
122             return
123         self.props_pre_check(props)
124         self.validate_docker_size_quota()
125         self.validate_helm_operation_timeout()
126         self.validate_docker0_cidr(props)
127         self.validate_oam_cidr(props)
128         self.validate_instantiation_timeout()
129         self.validate_encrypted_ca(self.ENCRYPTED_CA)
130         self.validate_encrypted_ca(self.ENCRYPTED_CA_KEY)
131         self.validate_log_forwarding()
132         self.validate_networks(props)
133         self.validate_dns_domain()
134
135     def _get_conf(self, props, domain):
136         if props.get(domain):
137             conf_str = props[domain]
138         else:
139             conf_str = self.get_plugin_client().get_property(domain)
140         return json.loads(conf_str)
141
142     def is_caas_mandatory(self, props):
143         if not isinstance(props, dict):
144             raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
145         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
146         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
147         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
148         for profile in service_profiles:
149             if filter(pattern.match, profile):
150                 return True
151         return False
152
153     def props_pre_check(self, props):
154         self.caas_conf = self._get_conf(props, self.CAAS_DOMAIN)
155         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
156         if not self.caas_conf:
157             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
158
159     def validate_docker_size_quota(self):
160         if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QUOTA, self.caas_conf):
161             return
162         if not re.match(self.DOCKER_SIZE_QUOTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QUOTA]):
163             raise CaasValidationError(
164                 '{} is not a valid {}!'.format(self.caas_conf[self.DOCKER_SIZE_QUOTA],
165                                                self.DOCKER_SIZE_QUOTA))
166
167     def validate_helm_operation_timeout(self):
168         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
169             return
170         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
171             raise CaasValidationError(
172                 '{}:{} is not an integer'.format(self.HELM_OP_TIMEOUT,
173                                                  self.caas_conf[self.HELM_OP_TIMEOUT]))
174
175     def get_netw_obj(self, subnet, parameter):
176         try:
177             return ipaddr.IPNetwork(subnet)
178         except ValueError as exc:
179             raise CaasValidationError('{} is an invalid subnet address: {}'.format(
180                 parameter, exc))
181
182     def check_cidr_overlaps_with_netw_subnets(self, cidr_in, props, parameter):
183         netw_conf = self._get_conf(props, self.NETW_DOMAIN)
184         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
185         for cidr_in in cidrs:
186             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
187                 raise CaasValidationError(
188                     'CIDR configured for {} shall be an unused IP range, '
189                     'but it overlaps with {} from {}.'.format(parameter, cidr,
190                                                               self.NETW_DOMAIN))
191     def check_oam_cidr_prefix(self, cidr_obj):
192         if ipaddr.IPNetwork(cidr_obj).prefixlen != 16:
193             raise CaasValidationError('Wrong subnet size in caas.oam_cidr parameter. '
194                                       'The currently supported subnet size is 16')
195
196     def validate_docker0_cidr(self, props):
197         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
198             return
199         docker0_cidr_obj = self.get_netw_obj(self.caas_conf[self.DOCKER0_CIDR], self.DOCKER0_CIDR)
200         self.check_cidr_overlaps_with_netw_subnets(docker0_cidr_obj, props, self.DOCKER0_CIDR)
201
202     def validate_oam_cidr(self, props):
203         if not self.caas_utils.is_optional_param_present(self.OAM_CIDR, self.caas_conf):
204             return
205         oam_cidr_obj = self.get_netw_obj(self.caas_conf[self.OAM_CIDR], self.OAM_CIDR)
206         self.check_cidr_overlaps_with_netw_subnets(oam_cidr_obj, props, self.OAM_CIDR)
207         self.check_oam_cidr_prefix(oam_cidr_obj)
208
209     def validate_instantiation_timeout(self):
210         if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
211                                                          self.caas_conf):
212             return
213         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
214             raise CaasValidationError('{}:{} is not an integer'.format(
215                 self.INSTANTIATION_TIMEOUT, self.caas_conf[self.INSTANTIATION_TIMEOUT]))
216
217     def validate_encrypted_ca(self, enc_ca):
218         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
219         enc_ca_str = self.caas_conf[enc_ca][0]
220         if not enc_ca_str:
221             raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
222         try:
223             base64.b64decode(enc_ca_str)
224         except TypeError as exc:
225             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
226
227     def validate_log_forwarding(self):
228         # pylint: disable=too-many-branches
229         if self.caas_utils.is_optional_param_present(self.BLOG_FORWARDING, self.caas_conf):
230             if self.caas_conf[self.BLOG_FORWARDING] not in self.INFRA_LOG_FLUENTD_PLUGINS:
231                 raise CaasValidationError('"{}" property not valid! '
232                                           'Choose from {}!'.format(self.BLOG_FORWARDING,
233                                                                    self.INFRA_LOG_FLUENTD_PLUGINS))
234         if self.caas_utils.is_optional_param_present(self.LOG_FORWARDING, self.caas_conf):
235             log_fw_list = self.caas_conf[self.LOG_FORWARDING]
236             if log_fw_list:
237                 url_d = dict()
238                 url_s = set()
239                 for list_item in log_fw_list:
240                     self.caas_utils.check_key_in_dict('namespace', list_item)
241                     if list_item['namespace'] == 'kube-system':
242                         raise CaasValidationError(
243                             'You can\'t set "kube-system" as namespace in "{}"!'.format(
244                                 self.LOG_FORWARDING))
245                     self.caas_utils.check_key_in_dict('target_url', list_item)
246                     if not list_item['target_url'] or not re.match(self.URL_PORT_PATTERN,
247                                                                    list_item['target_url']):
248                         raise CaasValidationError(
249                             '"target_url" property {} not valid!'.format(list_item['target_url']))
250                     if not url_d:
251                         url_d[list_item['namespace']] = list_item['target_url']
252                     if list_item['namespace'] in url_d:
253                         if list_item['target_url'] in url_s:
254                             raise CaasValidationError('There can\'t be multiple rules for the same '
255                                                       'target_url for the same {} '
256                                                       'namespace!'.format(list_item['namespace']))
257                         else:
258                             url_s.add(list_item['target_url'])
259                         url_d[list_item['namespace']] = url_s
260                     else:
261                         url_d[list_item['namespace']] = list_item['target_url']
262                     if self.caas_utils.is_optional_param_present('plugin', list_item) and list_item[
263                         'plugin'] not in self.FLUENTD_PLUGINS:
264                         raise CaasValidationError(
265                             '"plugin" property not valid! Choose from {}'.format(
266                                 self.FLUENTD_PLUGINS))
267                     if self.caas_utils.is_optional_param_present('stream', list_item) and list_item[
268                         'stream'] not in self.LOG_FW_STREAM:
269                         raise CaasValidationError(
270                             '"stream" property not valid! Choose from {}'.format(
271                                 self.LOG_FW_STREAM))
272
273     def validate_networks(self, props):
274         caas_nets = []
275         for nets_key in [self.CLUSTER_NETS, self.TENANT_NETS]:
276             if self.caas_utils.is_optional_param_present(nets_key, self.caas_conf):
277                 if not isinstance(self.caas_conf[nets_key], list):
278                     raise CaasValidationError('{} is not a list'.format(nets_key))
279                 if len(set(self.caas_conf[nets_key])) != len(self.caas_conf[nets_key]):
280                     raise CaasValidationError('{} has duplicate entries'.format(nets_key))
281                 caas_nets.extend(self.caas_conf[nets_key])
282         if len(set(caas_nets)) != len(caas_nets):
283             raise CaasValidationError('{} and {} must be distinct, but same entries are '
284                                       'found from both lists'.format(self.CLUSTER_NETS,
285                                                                      self.TENANT_NETS))
286         self._validate_homogenous_net_setup(props, caas_nets)
287
288     def _validate_homogenous_net_setup(self, props, caas_nets):
289         # Validate homogenous CaaS provider network setup
290         # pylint: disable=too-many-locals,too-many-nested-blocks
291         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
292         netprof_conf = self._get_conf(props, self.NETPROF_DOMAIN)
293         net_iface_map = {}
294         for net in caas_nets:
295             net_iface_map[net] = None
296             for host, host_conf in hosts_conf.iteritems():
297                 # Validate only nodes that can host containerized workloads
298                 if ('caas_worker' in host_conf[self.SERV_PROF] or
299                         ('caas_master' in host_conf[self.SERV_PROF] and
300                          'compute' not in host_conf[self.SERV_PROF])):
301                     # Validating CaaS network 'net' mapping in 'host'
302                     profiles = host_conf.get('network_profiles')
303                     if isinstance(profiles, list) and profiles:
304                         net_prof = netprof_conf.get(profiles[0])
305                     if net_prof is not None:
306                         ifaces = net_prof.get('provider_network_interfaces', {})
307                         caas_provider_interfaces = self._filter_provider_networks_by_type(
308                             self._filter_provider_networkinterfaces_by_net(ifaces, net), 'caas')
309                         sriov_networks = net_prof.get('sriov_provider_networks', {})
310                         caas_sriov_networks_present = bool(
311                             net in sriov_networks and
312                             sriov_networks[net].get('type', "") == 'caas')
313                         if not caas_provider_interfaces and not caas_sriov_networks_present:
314                             raise CaasValidationError('CaaS network {} missing from host {}'
315                                                       .format(net, host))
316                         if caas_provider_interfaces:
317                             self._validate_homogenous_provider_net_setup(
318                                 net_iface_map, net, ifaces)
319                         if caas_sriov_networks_present:
320                             self._validate_homogenous_sriov_provider_net_setup(
321                                 net_iface_map, net, sriov_networks)
322
323     @staticmethod
324     def _filter_provider_networks_by_type(profile, net_type):
325         return {name: network for name, network in profile.iteritems()
326                 if network.get('type', "") == net_type}
327
328     @staticmethod
329     def _filter_provider_networkinterfaces_by_net(provider_interfaces, provider_net):
330         return {iface: data for iface, data in provider_interfaces.iteritems()
331                 if provider_net in data.get('provider_networks', [])}
332
333     @staticmethod
334     def _validate_homogenous_provider_net_setup(net_iface_map, net, ifaces):
335         is_caas_network_present = False
336         for iface, data in ifaces.iteritems():
337             net_type = data.get('type')
338             networks = data.get('provider_networks', [])
339             if net in networks and net_type == 'caas':
340                 is_caas_network_present = True
341                 if net_iface_map[net] is None:
342                     net_iface_map[net] = iface
343                 elif net_iface_map[net] != iface:
344                     msg = 'CaaS network {} mapped to interface {} in one host '
345                     msg += 'and interface {} in another host'
346                     raise CaasValidationError(msg.format(net, iface,
347                                                          net_iface_map[net]))
348                 break
349         return is_caas_network_present
350
351     @staticmethod
352     def _validate_homogenous_sriov_provider_net_setup(net_iface_map, net, sriov_networks):
353         is_caas_network_present = False
354         sriov_provider_net = sriov_networks.get(net, {})
355         if sriov_provider_net and sriov_provider_net.get('type') == 'caas':
356             interfaces = sriov_provider_net.get('interfaces', [])
357             tenant_interfaces = net_iface_map.get(net)
358             already_used_ifaces = [set(x).intersection(interfaces)
359                                    for x in net_iface_map.itervalues()
360                                    if x and isinstance(x, list)]
361             if tenant_interfaces is None and interfaces:
362                 net_iface_map[net] = interfaces
363                 is_caas_network_present = True
364             elif already_used_ifaces:
365                 msg = 'CaaS network {} mapped to sriov interfaces {} in one host '
366                 msg += 'and sriov interfaces {} in another host'
367                 raise CaasValidationError(msg.format(net, interfaces,
368                                                      tenant_interfaces))
369         return is_caas_network_present
370
371     def validate_dns_domain(self):
372         domain = self.caas_conf[self.DOMAIN_NAME]
373         if not self.caas_utils.is_optional_param_present(self.DOMAIN_NAME, self.caas_conf):
374             return
375         if not re.match(self.DOMAIN_NAME_PATTERN, domain):
376             raise CaasValidationError('{} is not a valid {} !'.format(
377                 domain,
378                 self.DOMAIN_NAME))