9bb0b8c26278442b045d14d405ed2b52a949a955
[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
71     SUBSCRIPTION = r'^cloud\.caas|cloud\.hosts|cloud\.networking$'
72     CAAS_DOMAIN = 'cloud.caas'
73     NETW_DOMAIN = 'cloud.networking'
74     HOSTS_DOMAIN = 'cloud.hosts'
75
76     SERV_PROF = 'service_profiles'
77     CAAS_PROFILE_PATTERN = 'caas_master|caas_worker'
78     CIDR = 'cidr'
79
80     DOCKER_SIZE_QOUTA = "docker_size_quota"
81     DOCKER_SIZE_QOUTA_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     DOMAIN_NAME = "dns_domain"
93     DOMAIN_NAME_PATTERN = "^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
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_dns_domain()
118
119     def is_caas_mandatory(self, props):
120         hosts_conf = json.loads(props[self.HOSTS_DOMAIN])
121         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
122         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
123         for profile in service_profiles:
124             if filter(pattern.match, profile):
125                 return True
126         return False
127
128     def props_pre_check(self, props):
129         if not isinstance(props, dict):
130             raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
131         if self.CAAS_DOMAIN not in props:
132             raise CaasValidationError(
133                 '{} configuration is missing from {}!'.format(self.CAAS_DOMAIN, props))
134         self.caas_conf = json.loads(props[self.CAAS_DOMAIN])
135         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
136         if not self.caas_conf:
137             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
138
139     def validate_docker_size_quota(self):
140         if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QOUTA, self.caas_conf):
141             return
142         if not re.match(self.DOCKER_SIZE_QOUTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QOUTA]):
143             raise CaasValidationError('{} is not a valid {}!'.format(
144                 self.caas_conf[self.DOCKER_SIZE_QOUTA],
145                 self.DOCKER_SIZE_QOUTA))
146
147     def validate_helm_operation_timeout(self):
148         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
149             return
150         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
151             raise CaasValidationError('{}:{} is not an integer'.format(
152                 self.HELM_OP_TIMEOUT,
153                 self.caas_conf[self.HELM_OP_TIMEOUT]))
154
155     def get_docker0_cidr_netw_obj(self, subnet):
156         try:
157             return ipaddr.IPNetwork(subnet)
158         except ValueError as exc:
159             raise CaasValidationError('{} is an invalid subnet address: {}'.format(
160                 self.DOCKER0_CIDR, exc))
161
162     def check_docker0_cidr_overlaps_with_netw_subnets(self, docker0_cidr, props):
163         netw_conf = json.loads(props[self.NETW_DOMAIN])
164         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
165         for cidr in cidrs:
166             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
167                 raise CaasValidationError(
168                     'CIDR configured for {} shall be an unused IP range, '
169                     'but it overlaps with {} from {}.'.format(
170                         self.DOCKER0_CIDR, cidr, self.NETW_DOMAIN))
171
172     def validate_docker0_cidr(self, props):
173         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
174             return
175         docker0_cidr_obj = self.get_docker0_cidr_netw_obj(self.caas_conf[self.DOCKER0_CIDR])
176         self.check_docker0_cidr_overlaps_with_netw_subnets(docker0_cidr_obj, props)
177
178     def validate_instantiation_timeout(self):
179         if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
180                                                          self.caas_conf):
181             return
182         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
183             raise CaasValidationError('{}:{} is not an integer'.format(
184                 self.INSTANTIATION_TIMEOUT,
185                 self.caas_conf[self.INSTANTIATION_TIMEOUT]))
186
187     def validate_encrypted_ca(self, enc_ca):
188         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
189         enc_ca_str = self.caas_conf[enc_ca][0]
190         if not enc_ca_str:
191             raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
192         try:
193             base64.b64decode(enc_ca_str)
194         except TypeError as exc:
195             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
196
197     def validate_dns_domain(self):
198         domain = self.caas_conf[self.DOMAIN_NAME]
199         if not self.caas_utils.is_optional_param_present(self.DOMAIN_NAME, self.caas_conf):
200             return
201         if not re.match(self.DOMAIN_NAME_PATTERN, domain):
202             raise CaasValidationError('{} is not a valid {} !'.format(
203                 domain,
204                 self.DOMAIN_NAME))