8dc6f6a387d8541c0b7ab1c467cdf760e46082f9
[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     INSTANTIATION_TIMEOUT = "instantiation_timeout"
88
89     ENCRYPTED_CA = "encrypted_ca"
90     ENCRYPTED_CA_KEY = "encrypted_ca_key"
91
92     CLUSTER_NETS = 'cluster_networks'
93     TENANT_NETS = 'tenant_networks'
94
95     BLOG_FORWARDING = "infra_log_store"
96     LOG_FORWARDING = "log_forwarding"
97     URL_PORT_PATTERN = r"^(?:https?|udp|tcp):(?:\/\/)(?:((?:[\w\.-]+|" \
98                        r"\[(([1-9a-f][0-9a-f]{0,3}|\:)\:[1-9a-f][0-9a-f]{0,3}){0,7}\])\:[0-9]+))"
99     FLUENTD_PLUGINS = ['elasticsearch', 'remote_syslog']
100     INFRA_LOG_FLUENTD_PLUGINS = ['elasticsearch', 'remote_syslog']
101     LOG_FW_STREAM = ['stdout', 'stderr', 'both']
102
103     DOMAIN_NAME = "dns_domain"
104     DOMAIN_NAME_PATTERN = r"^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
105
106     def __init__(self):
107         cmvalidator.CMValidator.__init__(self)
108         self.validation_utils = validation.ValidationUtils()
109         self.conf = None
110         self.caas_conf = None
111         self.caas_utils = CaasValidationUtils()
112
113     def get_subscription_info(self):
114         return self.SUBSCRIPTION
115
116     def validate_set(self, props):
117         if not self.is_caas_mandatory(props):
118             logging.info("{} not found in {}, caas validation is not needed.".format(
119                 self.CAAS_PROFILE_PATTERN, self.HOSTS_DOMAIN))
120             return
121         self.props_pre_check(props)
122         self.validate_docker_size_quota()
123         self.validate_helm_operation_timeout()
124         self.validate_docker0_cidr(props)
125         self.validate_instantiation_timeout()
126         self.validate_encrypted_ca(self.ENCRYPTED_CA)
127         self.validate_encrypted_ca(self.ENCRYPTED_CA_KEY)
128         self.validate_log_forwarding()
129         self.validate_networks(props)
130         self.validate_dns_domain()
131
132     def _get_conf(self, props, domain):
133         if props.get(domain):
134             conf_str = props[domain]
135         else:
136             conf_str = self.get_plugin_client().get_property(domain)
137         return json.loads(conf_str)
138
139     def is_caas_mandatory(self, props):
140         if not isinstance(props, dict):
141             raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
142         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
143         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
144         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
145         for profile in service_profiles:
146             if filter(pattern.match, profile):
147                 return True
148         return False
149
150     def props_pre_check(self, props):
151         self.caas_conf = self._get_conf(props, self.CAAS_DOMAIN)
152         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
153         if not self.caas_conf:
154             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
155
156     def validate_docker_size_quota(self):
157         if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QUOTA, self.caas_conf):
158             return
159         if not re.match(self.DOCKER_SIZE_QUOTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QUOTA]):
160             raise CaasValidationError(
161                 '{} is not a valid {}!'.format(self.caas_conf[self.DOCKER_SIZE_QUOTA],
162                                                self.DOCKER_SIZE_QUOTA))
163
164     def validate_helm_operation_timeout(self):
165         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
166             return
167         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
168             raise CaasValidationError(
169                 '{}:{} is not an integer'.format(self.HELM_OP_TIMEOUT,
170                                                  self.caas_conf[self.HELM_OP_TIMEOUT]))
171
172     def get_docker0_cidr_netw_obj(self, subnet):
173         try:
174             return ipaddr.IPNetwork(subnet)
175         except ValueError as exc:
176             raise CaasValidationError('{} is an invalid subnet address: {}'.format(
177                 self.DOCKER0_CIDR, exc))
178
179     def check_docker0_cidr_overlaps_with_netw_subnets(self, docker0_cidr, props):
180         netw_conf = self._get_conf(props, self.NETW_DOMAIN)
181         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
182         for cidr in cidrs:
183             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
184                 raise CaasValidationError(
185                     'CIDR configured for {} shall be an unused IP range, '
186                     'but it overlaps with {} from {}.'.format(self.DOCKER0_CIDR, cidr,
187                                                               self.NETW_DOMAIN))
188
189     def validate_docker0_cidr(self, props):
190         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
191             return
192         docker0_cidr_obj = self.get_docker0_cidr_netw_obj(self.caas_conf[self.DOCKER0_CIDR])
193         self.check_docker0_cidr_overlaps_with_netw_subnets(docker0_cidr_obj, props)
194
195     def validate_instantiation_timeout(self):
196         if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
197                                                          self.caas_conf):
198             return
199         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
200             raise CaasValidationError('{}:{} is not an integer'.format(
201                 self.INSTANTIATION_TIMEOUT, self.caas_conf[self.INSTANTIATION_TIMEOUT]))
202
203     def validate_encrypted_ca(self, enc_ca):
204         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
205         enc_ca_str = self.caas_conf[enc_ca][0]
206         if not enc_ca_str:
207             raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
208         try:
209             base64.b64decode(enc_ca_str)
210         except TypeError as exc:
211             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
212
213     def validate_log_forwarding(self):
214         # pylint: disable=too-many-branches
215         if self.caas_utils.is_optional_param_present(self.BLOG_FORWARDING, self.caas_conf):
216             if self.caas_conf[self.BLOG_FORWARDING] not in self.INFRA_LOG_FLUENTD_PLUGINS:
217                 raise CaasValidationError('"{}" property not valid! '
218                                           'Choose from {}!'.format(self.BLOG_FORWARDING,
219                                                                    self.INFRA_LOG_FLUENTD_PLUGINS))
220         if self.caas_utils.is_optional_param_present(self.LOG_FORWARDING, self.caas_conf):
221             log_fw_list = self.caas_conf[self.LOG_FORWARDING]
222             if log_fw_list:
223                 url_d = dict()
224                 url_s = set()
225                 for list_item in log_fw_list:
226                     self.caas_utils.check_key_in_dict('namespace', list_item)
227                     if list_item['namespace'] == 'kube-system':
228                         raise CaasValidationError(
229                             'You can\'t set "kube-system" as namespace in "{}"!'.format(
230                                 self.LOG_FORWARDING))
231                     self.caas_utils.check_key_in_dict('target_url', list_item)
232                     if not list_item['target_url'] or not re.match(self.URL_PORT_PATTERN,
233                                                                    list_item['target_url']):
234                         raise CaasValidationError(
235                             '"target_url" property {} not valid!'.format(list_item['target_url']))
236                     if not url_d:
237                         url_d[list_item['namespace']] = list_item['target_url']
238                     if list_item['namespace'] in url_d:
239                         if list_item['target_url'] in url_s:
240                             raise CaasValidationError('There can\'t be multiple rules for the same '
241                                                       'target_url for the same {} '
242                                                       'namespace!'.format(list_item['namespace']))
243                         else:
244                             url_s.add(list_item['target_url'])
245                         url_d[list_item['namespace']] = url_s
246                     else:
247                         url_d[list_item['namespace']] = list_item['target_url']
248                     if self.caas_utils.is_optional_param_present('plugin', list_item) and list_item[
249                         'plugin'] not in self.FLUENTD_PLUGINS:
250                         raise CaasValidationError(
251                             '"plugin" property not valid! Choose from {}'.format(
252                                 self.FLUENTD_PLUGINS))
253                     if self.caas_utils.is_optional_param_present('stream', list_item) and list_item[
254                         'stream'] not in self.LOG_FW_STREAM:
255                         raise CaasValidationError(
256                             '"stream" property not valid! Choose from {}'.format(
257                                 self.LOG_FW_STREAM))
258
259     def validate_networks(self, props):
260         caas_nets = []
261         for nets_key in [self.CLUSTER_NETS, self.TENANT_NETS]:
262             if self.caas_utils.is_optional_param_present(nets_key, self.caas_conf):
263                 if not isinstance(self.caas_conf[nets_key], list):
264                     raise CaasValidationError('{} is not a list'.format(nets_key))
265                 if len(set(self.caas_conf[nets_key])) != len(self.caas_conf[nets_key]):
266                     raise CaasValidationError('{} has duplicate entries'.format(nets_key))
267                 caas_nets.extend(self.caas_conf[nets_key])
268         if len(set(caas_nets)) != len(caas_nets):
269             raise CaasValidationError('{} and {} must be distinct, but same entries are '
270                                       'found from both lists'.format(self.CLUSTER_NETS,
271                                                                      self.TENANT_NETS))
272         self._validate_homogenous_net_setup(props, caas_nets)
273
274     def _validate_homogenous_net_setup(self, props, caas_nets):
275         # Validate homogenous CaaS provider network setup
276         # pylint: disable=too-many-locals,too-many-nested-blocks
277         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
278         netprof_conf = self._get_conf(props, self.NETPROF_DOMAIN)
279         net_iface_map = {}
280         for net in caas_nets:
281             net_iface_map[net] = None
282             for host, host_conf in hosts_conf.iteritems():
283                 # Validate only nodes that can host containerized workloads
284                 if ('caas_worker' in host_conf[self.SERV_PROF] or
285                         ('caas_master' in host_conf[self.SERV_PROF] and
286                          'compute' not in host_conf[self.SERV_PROF])):
287                     # Validating CaaS network 'net' mapping in 'host'
288                     profiles = host_conf.get('network_profiles')
289                     if isinstance(profiles, list) and profiles:
290                         net_prof = netprof_conf.get(profiles[0])
291                     if net_prof is not None:
292                         ifaces = net_prof.get('provider_network_interfaces', {})
293                         caas_provider_interfaces = self._filter_provider_networks_by_type(
294                             self._filter_provider_networkinterfaces_by_net(ifaces, net), 'caas')
295                         sriov_networks = net_prof.get('sriov_provider_networks', {})
296                         caas_sriov_networks_present = bool(
297                             net in sriov_networks and
298                             sriov_networks[net].get('type', "") == 'caas')
299                         if not caas_provider_interfaces and not caas_sriov_networks_present:
300                             raise CaasValidationError('CaaS network {} missing from host {}'
301                                                       .format(net, host))
302                         if caas_provider_interfaces:
303                             self._validate_homogenous_provider_net_setup(
304                                 net_iface_map, net, ifaces)
305                         if caas_sriov_networks_present:
306                             self._validate_homogenous_sriov_provider_net_setup(
307                                 net_iface_map, net, sriov_networks)
308
309     @staticmethod
310     def _filter_provider_networks_by_type(profile, net_type):
311         return {name: network for name, network in profile.iteritems()
312                 if network.get('type', "") == net_type}
313
314     @staticmethod
315     def _filter_provider_networkinterfaces_by_net(provider_interfaces, provider_net):
316         return {iface: data for iface, data in provider_interfaces.iteritems()
317                 if provider_net in data.get('provider_networks', [])}
318
319     @staticmethod
320     def _validate_homogenous_provider_net_setup(net_iface_map, net, ifaces):
321         is_caas_network_present = False
322         for iface, data in ifaces.iteritems():
323             net_type = data.get('type')
324             networks = data.get('provider_networks', [])
325             if net in networks and net_type == 'caas':
326                 is_caas_network_present = True
327                 if net_iface_map[net] is None:
328                     net_iface_map[net] = iface
329                 elif net_iface_map[net] != iface:
330                     msg = 'CaaS network {} mapped to interface {} in one host '
331                     msg += 'and interface {} in another host'
332                     raise CaasValidationError(msg.format(net, iface,
333                                                          net_iface_map[net]))
334                 break
335         return is_caas_network_present
336
337     @staticmethod
338     def _validate_homogenous_sriov_provider_net_setup(net_iface_map, net, sriov_networks):
339         is_caas_network_present = False
340         sriov_provider_net = sriov_networks.get(net, {})
341         if sriov_provider_net and sriov_provider_net.get('type') == 'caas':
342             interfaces = sriov_provider_net.get('interfaces', [])
343             tenant_interfaces = net_iface_map.get(net)
344             already_used_ifaces = [set(x).intersection(interfaces)
345                                    for x in net_iface_map.itervalues()
346                                    if x and isinstance(x, list)]
347             if tenant_interfaces is None and interfaces:
348                 net_iface_map[net] = interfaces
349                 is_caas_network_present = True
350             elif already_used_ifaces:
351                 msg = 'CaaS network {} mapped to sriov interfaces {} in one host '
352                 msg += 'and sriov interfaces {} in another host'
353                 raise CaasValidationError(msg.format(net, interfaces,
354                                                      tenant_interfaces))
355         return is_caas_network_present
356
357     def validate_dns_domain(self):
358         domain = self.caas_conf[self.DOMAIN_NAME]
359         if not self.caas_utils.is_optional_param_present(self.DOMAIN_NAME, self.caas_conf):
360             return
361         if not re.match(self.DOMAIN_NAME_PATTERN, domain):
362             raise CaasValidationError('{} is not a valid {} !'.format(
363                 domain,
364                 self.DOMAIN_NAME))