Add validation for CaaS DNS domain
[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     CHART_NAME = "chart_name"
84     CHART_NAME_PATTERN = r"[A-Za-z0-9\.-_]+"
85
86     CHART_VERSION = "chart_version"
87     CHART_VERSION_PATTERN = r"^\d+\.\d+\.\d+$"
88
89     HELM_OP_TIMEOUT = "helm_operation_timeout"
90
91     DOCKER0_CIDR = "docker0_cidr"
92
93     INSTANTIATION_TIMEOUT = "instantiation_timeout"
94
95     HELM_PARAMETERS = "helm_parameters"
96
97     ENCRYPTED_CA = "encrypted_ca"
98     ENCRYPTED_CA_KEY = "encrypted_ca_key"
99
100     DOMAIN_NAME = "dns_domain"
101     DOMAIN_NAME_PATTERN = "^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
102
103     def __init__(self):
104         cmvalidator.CMValidator.__init__(self)
105         self.validation_utils = validation.ValidationUtils()
106         self.conf = None
107         self.caas_conf = None
108         self.caas_utils = CaasValidationUtils()
109
110     def get_subscription_info(self):
111         return self.SUBSCRIPTION
112
113     def validate_set(self, props):
114         if not self.is_caas_mandatory(props):
115             logging.info("{} not found in {}, caas validation is not needed.".format(
116                 self.CAAS_PROFILE_PATTERN, self.HOSTS_DOMAIN))
117             return
118         self.props_pre_check(props)
119         self.validate_docker_size_quota()
120         self.validate_chart_name()
121         self.validate_chart_version()
122         self.validate_helm_operation_timeout()
123         self.validate_docker0_cidr(props)
124         self.validate_instantiation_timeout()
125         self.validate_helm_parameters()
126         self.validate_encrypted_ca(self.ENCRYPTED_CA)
127         self.validate_encrypted_ca(self.ENCRYPTED_CA_KEY)
128         self.validate_dns_domain()
129
130     def is_caas_mandatory(self, props):
131         hosts_conf = json.loads(props[self.HOSTS_DOMAIN])
132         service_profiles = self.caas_utils.get_every_key_occurrence(hosts_conf, self.SERV_PROF)
133         pattern = re.compile(self.CAAS_PROFILE_PATTERN)
134         for profile in service_profiles:
135             if filter(pattern.match, profile):
136                 return True
137         return False
138
139     def props_pre_check(self, props):
140         if not isinstance(props, dict):
141             raise CaasValidationError('The given input: {} is not a dictionary!'.format(props))
142         if self.CAAS_DOMAIN not in props:
143             raise CaasValidationError(
144                 '{} configuration is missing from {}!'.format(self.CAAS_DOMAIN, props))
145         self.caas_conf = json.loads(props[self.CAAS_DOMAIN])
146         self.conf = {self.CAAS_DOMAIN: self.caas_conf}
147         if not self.caas_conf:
148             raise CaasValidationError('{} is an empty dictionary!'.format(self.conf))
149
150     def validate_docker_size_quota(self):
151         if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QOUTA, self.caas_conf):
152             return
153         if not re.match(self.DOCKER_SIZE_QOUTA_PATTERN, self.caas_conf[self.DOCKER_SIZE_QOUTA]):
154             raise CaasValidationError('{} is not a valid {}!'.format(
155                 self.caas_conf[self.DOCKER_SIZE_QOUTA],
156                 self.DOCKER_SIZE_QOUTA))
157
158     def validate_chart_name(self):
159         if not self.caas_utils.is_optional_param_present(self.CHART_NAME, self.caas_conf):
160             return
161         if not re.match(self.CHART_NAME_PATTERN, self.caas_conf[self.CHART_NAME]):
162             raise CaasValidationError('{} is not a valid {} !'.format(
163                 self.caas_conf[self.CHART_NAME],
164                 self.CHART_NAME))
165
166     def validate_chart_version(self):
167         if not self.caas_utils.is_optional_param_present(self.CHART_VERSION, self.caas_conf):
168             return
169         if not self.caas_conf[self.CHART_NAME]:
170             logging.warn('{} shall be set only, when {} is set.'.format(
171                 self.CHART_VERSION, self.CHART_NAME))
172         if not re.match(self.CHART_VERSION_PATTERN, self.caas_conf[self.CHART_VERSION]):
173             raise CaasValidationError('{} is not a valid {} !'.format(
174                 self.caas_conf[self.CHART_VERSION],
175                 self.CHART_VERSION))
176
177     def validate_helm_operation_timeout(self):
178         if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
179             return
180         if not isinstance(self.caas_conf[self.HELM_OP_TIMEOUT], int):
181             raise CaasValidationError('{}:{} is not an integer'.format(
182                 self.HELM_OP_TIMEOUT,
183                 self.caas_conf[self.HELM_OP_TIMEOUT]))
184
185     def get_docker0_cidr_netw_obj(self, subnet):
186         try:
187             return ipaddr.IPNetwork(subnet)
188         except ValueError as exc:
189             raise CaasValidationError('{} is an invalid subnet address: {}'.format(
190                 self.DOCKER0_CIDR, exc))
191
192     def check_docker0_cidr_overlaps_with_netw_subnets(self, docker0_cidr, props):
193         netw_conf = json.loads(props[self.NETW_DOMAIN])
194         cidrs = self.caas_utils.get_every_key_occurrence(netw_conf, self.CIDR)
195         for cidr in cidrs:
196             if docker0_cidr.overlaps(ipaddr.IPNetwork(cidr)):
197                 raise CaasValidationError(
198                     'CIDR configured for {} shall be an unused IP range, '
199                     'but it overlaps with {} from {}.'.format(
200                         self.DOCKER0_CIDR, cidr, self.NETW_DOMAIN))
201
202     def validate_docker0_cidr(self, props):
203         if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
204             return
205         docker0_cidr_obj = self.get_docker0_cidr_netw_obj(self.caas_conf[self.DOCKER0_CIDR])
206         self.check_docker0_cidr_overlaps_with_netw_subnets(docker0_cidr_obj, props)
207
208     def validate_instantiation_timeout(self):
209         if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
210                                                          self.caas_conf):
211             return
212         if not isinstance(self.caas_conf[self.INSTANTIATION_TIMEOUT], int):
213             raise CaasValidationError('{}:{} is not an integer'.format(
214                 self.INSTANTIATION_TIMEOUT,
215                 self.caas_conf[self.INSTANTIATION_TIMEOUT]))
216
217     def validate_helm_parameters(self):
218         if not self.caas_utils.is_optional_param_present(self.HELM_PARAMETERS, self.caas_conf):
219             return
220         if not isinstance(self.caas_conf[self.HELM_PARAMETERS], dict):
221             raise CaasValidationError('The given input: {} is not a dictionary!'.format(
222                 self.caas_conf[self.HELM_PARAMETERS]))
223
224     def validate_encrypted_ca(self, enc_ca):
225         self.caas_utils.check_key_in_dict(enc_ca, self.caas_conf)
226         enc_ca_str = self.caas_conf[enc_ca][0]
227         if not enc_ca_str:
228             raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
229         try:
230             base64.b64decode(enc_ca_str)
231         except TypeError as exc:
232             raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
233
234     def validate_dns_domain(self):
235         domain = self.caas_conf[self.DOMAIN_NAME]
236         if not self.caas_utils.is_optional_param_present(self.DOMAIN_NAME, self.caas_conf):
237             return
238         if not re.match(self.DOMAIN_NAME_PATTERN, domain):
239             raise CaasValidationError('{} is not a valid {} !'.format(
240                 domain,
241                 self.DOMAIN_NAME))