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
8 # http://www.apache.org/licenses/LICENSE-2.0
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.
21 from cmframework.apis import cmvalidator
22 from cmdatahandlers.api import validation
23 from cmdatahandlers.api import configerror
26 class CaasValidationError(configerror.ConfigError):
27 def __init__(self, description):
28 configerror.ConfigError.__init__(
29 self, 'Validation error in caas_validation: {}'.format(description))
32 class CaasValidationUtils(object):
38 def check_key_in_dict(key, dictionary):
39 if key not in dictionary:
40 raise CaasValidationError("{} cannot be found in {} ".format(key, dictionary))
42 def get_every_key_occurrence(self, var, key):
43 if hasattr(var, 'iteritems'):
44 for k, v in var.iteritems():
47 if isinstance(v, dict):
48 for result in self.get_every_key_occurrence(v, key):
50 elif isinstance(v, list):
52 for result in self.get_every_key_occurrence(d, key):
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))
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))
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'
76 SERV_PROF = 'service_profiles'
77 CAAS_PROFILE_PATTERN = 'caas_master|caas_worker'
80 DOCKER_SIZE_QUOTA = "docker_size_quota"
81 DOCKER_SIZE_QUOTA_PATTERN = r"^\d*[G,M,K]$"
83 HELM_OP_TIMEOUT = "helm_operation_timeout"
85 DOCKER0_CIDR = "docker0_cidr"
89 INSTANTIATION_TIMEOUT = "instantiation_timeout"
91 ENCRYPTED_CA = "encrypted_ca"
92 ENCRYPTED_CA_KEY = "encrypted_ca_key"
94 CLUSTER_NETS = 'cluster_networks'
95 TENANT_NETS = 'tenant_networks'
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']
105 DOMAIN_NAME = "dns_domain"
106 DOMAIN_NAME_PATTERN = r"^[a-z0-9]([a-z0-9-\.]{0,253}[a-z0-9])?$"
109 cmvalidator.CMValidator.__init__(self)
110 self.validation_utils = validation.ValidationUtils()
112 self.caas_conf = None
113 self.caas_utils = CaasValidationUtils()
115 def get_subscription_info(self):
116 return self.SUBSCRIPTION
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))
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()
135 def _get_conf(self, props, domain):
136 if props.get(domain):
137 conf_str = props[domain]
139 conf_str = self.get_plugin_client().get_property(domain)
140 return json.loads(conf_str)
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):
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))
159 def validate_docker_size_quota(self):
160 if not self.caas_utils.is_optional_param_present(self.DOCKER_SIZE_QUOTA, self.caas_conf):
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))
167 def validate_helm_operation_timeout(self):
168 if not self.caas_utils.is_optional_param_present(self.HELM_OP_TIMEOUT, self.caas_conf):
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]))
175 def get_netw_obj(self, subnet, parameter):
177 return ipaddr.IPNetwork(subnet)
178 except ValueError as exc:
179 raise CaasValidationError('{} is an invalid subnet address: {}'.format(
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)
186 if cidr_in.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,
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')
196 def validate_docker0_cidr(self, props):
197 if not self.caas_utils.is_optional_param_present(self.DOCKER0_CIDR, self.caas_conf):
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)
202 def validate_oam_cidr(self, props):
203 if not self.caas_utils.is_optional_param_present(self.OAM_CIDR, self.caas_conf):
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)
209 def validate_instantiation_timeout(self):
210 if not self.caas_utils.is_optional_param_present(self.INSTANTIATION_TIMEOUT,
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]))
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]
221 raise CaasValidationError('{} shall not be empty !'.format(enc_ca))
223 base64.b64decode(enc_ca_str)
224 except TypeError as exc:
225 raise CaasValidationError('Invalid {}: {}'.format(enc_ca, exc))
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]
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']))
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']))
258 url_s.add(list_item['target_url'])
259 url_d[list_item['namespace']] = url_s
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(
273 def validate_networks(self, props):
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,
286 self._validate_homogenous_net_setup(props, caas_nets)
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)
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 {}'
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)
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}
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', [])}
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,
349 return is_caas_network_present
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,
369 return is_caas_network_present
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):
375 if not re.match(self.DOMAIN_NAME_PATTERN, domain):
376 raise CaasValidationError('{} is not a valid {} !'.format(