Add CaaS networks related validations
[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     def __init__(self):
96         cmvalidator.CMValidator.__init__(self)
97         self.validation_utils = validation.ValidationUtils()
98         self.conf = None
99         self.caas_conf = None
100         self.caas_utils = CaasValidationUtils()
101
102     def get_subscription_info(self):
103         return self.SUBSCRIPTION
104
105     def validate_set(self, props):
106         if not self.is_caas_mandatory(props):
107             logging.info("{} not found in {}, caas validation is not needed.".format(
108                 self.CAAS_PROFILE_PATTERN, self.HOSTS_DOMAIN))
109             return
110         self.props_pre_check(props)
111         self.validate_docker_size_quota()
112         self.validate_helm_operation_timeout()
113         self.validate_docker0_cidr(props)
114         self.validate_instantiation_timeout()
115         self.validate_encrypted_ca(self.ENCRYPTED_CA)
116         self.validate_encrypted_ca(self.ENCRYPTED_CA_KEY)
117         self.validate_networks(props)
118
119     def _get_conf(self, props, domain):
120         if props.get(domain):
121             conf_str = props[domain]
122         else:
123             conf_str = self.get_plugin_client().get_property(domain)
124         return json.loads(conf_str)
125
126     def is_caas_mandatory(self, props):
127         if not isinstance(props, dict):
128             raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
129         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
130         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
131         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
132         for profile in service_profiles:
133             if filter(pattern.match, profile):
134                 return True
135         return False
136
137     def props_pre_check(self, props):
138         self.caas_conf = self._get_conf(props, self.CAAS_DOMAIN)
139         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
140         if not self.caas_conf:
141             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
142
143     def validate_docker_size_quota(self):
144         if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QUOTA, self.caas_conf):
145             return
146         if not re.match(self.DOCKER_SIZE_QUOTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QUOTA]):
147             raise CaasValidationError(
148                 '{} is not a valid {}!'.format(self.caas_conf[self.DOCKER_SIZE_QUOTA],
149                                                self.DOCKER_SIZE_QUOTA))
150
151     def validate_helm_operation_timeout(self):
152         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
153             return
154         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
155             raise CaasValidationError(
156                 '{}:{} is not an integer'.format(self.HELM_OP_TIMEOUT,
157                                                  self.caas_conf[self.HELM_OP_TIMEOUT]))
158
159     def get_docker0_cidr_netw_obj(self, subnet):
160         try:
161             return ipaddr.IPNetwork(subnet)
162         except ValueError as exc:
163             raise CaasValidationError('{} is an invalid subnet address: {}'.format(
164                 self.DOCKER0_CIDR, exc))
165
166     def check_docker0_cidr_overlaps_with_netw_subnets(self, docker0_cidr, props):
167         netw_conf = self._get_conf(props, self.NETW_DOMAIN)
168         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
169         for cidr in cidrs:
170             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
171                 raise CaasValidationError(
172                     'CIDR configured for {} shall be an unused IP range, '
173                     'but it overlaps with {} from {}.'.format(self.DOCKER0_CIDR, cidr,
174                                                               self.NETW_DOMAIN))
175
176     def validate_docker0_cidr(self, props):
177         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
178             return
179         docker0_cidr_obj = self.get_docker0_cidr_netw_obj(self.caas_conf[self.DOCKER0_CIDR])
180         self.check_docker0_cidr_overlaps_with_netw_subnets(docker0_cidr_obj, props)
181
182     def validate_instantiation_timeout(self):
183         if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
184                                                          self.caas_conf):
185             return
186         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
187             raise CaasValidationError('{}:{} is not an integer'.format(
188                 self.INSTANTIATION_TIMEOUT, self.caas_conf[self.INSTANTIATION_TIMEOUT]))
189
190     def validate_encrypted_ca(self, enc_ca):
191         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
192         enc_ca_str = self.caas_conf[enc_ca][0]
193         if not enc_ca_str:
194             raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
195         try:
196             base64.b64decode(enc_ca_str)
197         except TypeError as exc:
198             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
199
200     def validate_networks(self, props):
201         caas_nets = []
202         for nets_key in [self.CLUSTER_NETS, self.TENANT_NETS]:
203             if self.caas_utils.is_optional_param_present(nets_key, self.caas_conf):
204                 if not isinstance(self.caas_conf[nets_key], list):
205                     raise CaasValidationError('{} is not a list'.format(nets_key))
206                 if len(set(self.caas_conf[nets_key])) != len(self.caas_conf[nets_key]):
207                     raise CaasValidationError('{} has duplicate entries'.format(nets_key))
208                 caas_nets.extend(self.caas_conf[nets_key])
209         if len(set(caas_nets)) != len(caas_nets):
210             raise CaasValidationError('{} and {} must be distinct, but same entries are '
211                                       'found from both lists'.format(self.CLUSTER_NETS,
212                                                                      self.TENANT_NETS))
213         self._validate_homogenous_net_setup(props, caas_nets)
214
215     def _validate_homogenous_net_setup(self, props, caas_nets):
216         # Validate homogenous CaaS provider network setup
217         # pylint: disable=too-many-locals,too-many-nested-blocks
218         hosts_conf = self._get_conf(props, self.HOSTS_DOMAIN)
219         netprof_conf = self._get_conf(props, self.NETPROF_DOMAIN)
220         net_iface_map = {}
221         for net in caas_nets:
222             net_iface_map[net] = None
223             for host, host_conf in hosts_conf.iteritems():
224                 # Validate only nodes that can host containerized workloads
225                 if ('caas_worker' in host_conf[self.SERV_PROF] or
226                         ('caas_master' in host_conf[self.SERV_PROF] and
227                          'compute' not in host_conf[self.SERV_PROF])):
228                     # Validating CaaS network 'net' mapping in 'host'
229                     is_caas_network_present = False
230                     profiles = host_conf.get('network_profiles')
231                     if isinstance(profiles, list) and profiles:
232                         net_prof = netprof_conf.get(profiles[0])
233                     if net_prof is not None:
234                         ifaces = net_prof.get('provider_network_interfaces', {})
235                         for iface, data in ifaces.iteritems():
236                             net_type = data.get('type')
237                             networks = data.get('provider_networks', [])
238                             if net in networks and net_type == 'caas':
239                                 is_caas_network_present = True
240                                 if net_iface_map[net] is None:
241                                     net_iface_map[net] = iface
242                                 elif net_iface_map[net] != iface:
243                                     msg = 'CaaS network {} mapped to interface {} in one host '
244                                     msg += 'and interface {} in another host'
245                                     raise CaasValidationError(msg.format(net, iface,
246                                                                          net_iface_map[net]))
247                                 break
248                     if not is_caas_network_present:
249                         raise CaasValidationError('CaaS network {} missing from host {}'
250                                                   .format(net, host))