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.
19 from netaddr import IPRange
20 from netaddr import IPNetwork
22 from cmframework.apis import cmvalidator
23 from cmdatahandlers.api import validation
24 from cmdatahandlers.api import utils
25 from serviceprofiles import profiles as service_profiles
28 class ConfigurationDoesNotExist(Exception):
32 class HostsValidation(cmvalidator.CMValidator):
33 domain = 'cloud.hosts'
34 management_profile = 'management'
35 controller_profile = 'controller'
36 caas_master_profile = 'caas_master'
37 caas_worker_profile = 'caas_worker'
39 storage_profile = 'storage'
41 storage_profile_attr = 'cloud.storage_profiles'
42 network_profile_attr = 'cloud.network_profiles'
43 performance_profile_attr = 'cloud.performance_profiles'
44 networking_attr = 'cloud.networking'
45 MIN_PASSWORD_LENGTH = 8
47 def get_subscription_info(self):
48 logging.debug('get_subscription info called')
49 hosts = r'cloud\.hosts'
50 net_profiles = r'cloud\.network_profiles'
51 storage_profiles = r'cloud\.storage_profiles'
52 perf_profiles = r'cloud\.performance_profiles'
53 net = r'cloud\.networking'
54 return '^%s|%s|%s|%s|%s$' % (hosts, net_profiles, storage_profiles, perf_profiles, net)
56 def validate_set(self, dict_key_value):
57 logging.debug('HostsValidation: validate_set called with %s', dict_key_value)
59 for key, value in dict_key_value.iteritems():
60 value_dict = {} if not value else json.loads(value)
62 if key != self.storage_profile_attr:
63 raise validation.ValidationError('No value for %s' % key)
65 if key == self.domain:
66 if not isinstance(value_dict, dict):
67 raise validation.ValidationError('%s value is not a dict' % self.domain)
69 net_profile_dict = self.get_domain_dict(dict_key_value,
70 self.network_profile_attr)
71 storage_profile_dict = self.get_domain_dict(dict_key_value,
72 self.storage_profile_attr)
73 perf_profile_dict = self.get_domain_dict(dict_key_value,
74 self.performance_profile_attr)
75 networking_dict = self.get_domain_dict(dict_key_value,
77 self.validate_hosts(value_dict,
83 self.validate_scale_in(dict_key_value)
85 elif key == self.network_profile_attr:
86 profile_list = [] if not value_dict else value_dict.keys()
88 host_dict = self.get_domain_dict(dict_key_value, self.domain)
89 perf_profile_config = self.get_domain_dict(dict_key_value,
90 self.performance_profile_attr)
91 storage_profile_config = self.get_domain_dict(dict_key_value,
92 self.storage_profile_attr)
93 net_profile_dict = self.get_domain_dict(dict_key_value,
94 self.network_profile_attr)
95 networking_dict = self.get_domain_dict(dict_key_value,
98 self.validate_network_ranges(host_dict, net_profile_dict, networking_dict)
100 for host_name, host_data in host_dict.iteritems():
101 attr = 'network_profiles'
102 profiles = host_data.get(attr)
103 profile_name = profiles[0]
104 self.validate_profile_list(profiles, profile_list, host_name, attr)
106 performance_profiles = host_data.get('performance_profiles')
108 if self.is_provider_type_ovs_dpdk(profile_name, value_dict):
109 if self.base_profile not in host_data['service_profiles']:
110 reason = 'Missing base service profile with ovs_dpdk'
111 reason += ' type provider network'
112 raise validation.ValidationError(reason)
113 if not performance_profiles:
115 'Missing performance profiles with ovs_dpdk type provider network'
116 raise validation.ValidationError(reason)
117 self.validate_performance_profile(perf_profile_config,
118 performance_profiles[0])
120 if self.is_provider_type_sriov(profile_name, value_dict):
121 if not self.is_sriov_allowed_for_host(host_data['service_profiles']):
122 reason = 'Missing base or caas_* service profile'
123 reason += ' with SR-IOV type provider network'
124 raise validation.ValidationError(reason)
126 subnet_name = 'infra_internal'
127 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name):
128 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name,
130 if self.management_profile in host_data['service_profiles']:
131 subnet_name = 'infra_external'
132 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name):
133 raise validation.ValidationError('%s is not mapped for %s' %
134 (subnet_name, host_name))
136 subnet_name = 'infra_external'
137 if self.network_is_mapped(value_dict.get(profile_name), subnet_name):
138 raise validation.ValidationError('%s is mapped for %s' %
139 (subnet_name, host_name))
141 if self.storage_profile in host_data['service_profiles']:
142 storage_profile_list = host_data.get('storage_profiles')
143 subnet_name = 'infra_storage_cluster'
144 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name) \
145 and self.is_ceph_profile(storage_profile_config,
146 storage_profile_list):
147 raise validation.ValidationError('%s is not mapped for %s' %
148 (subnet_name, host_name))
150 elif key == self.storage_profile_attr:
151 profile_list = [] if not value_dict else value_dict.keys()
153 host_dict = self.get_domain_dict(dict_key_value, self.domain)
155 for host_name, host_data in host_dict.iteritems():
156 attr = 'storage_profiles'
157 profiles = host_data.get(attr)
159 self.validate_profile_list(profiles, profile_list, host_name, attr)
161 elif key == self.performance_profile_attr:
162 profile_list = [] if not value_dict else value_dict.keys()
164 host_dict = self.get_domain_dict(dict_key_value, self.domain)
165 network_profile_config = self.get_domain_dict(dict_key_value,
166 self.network_profile_attr)
168 for host_name, host_data in host_dict.iteritems():
169 attr = 'performance_profiles'
170 profiles = host_data.get(attr)
172 self.validate_profile_list(profiles, profile_list, host_name, attr)
173 self.validate_nonempty_performance_profile(value_dict, profiles[0],
176 network_profiles = host_data.get('network_profiles')
177 if self.is_provider_type_ovs_dpdk(network_profiles[0], network_profile_config):
180 'Missing performance profiles with ovs_dpdk type provider network'
181 raise validation.ValidationError(reason)
182 self.validate_performance_profile(value_dict,
184 elif key == self.networking_attr:
185 networking_dict = value_dict
187 hosts_dict = self.get_domain_dict(dict_key_value, self.domain)
188 profile_config = self.get_domain_dict(dict_key_value,
189 self.network_profile_attr)
191 self.validate_network_ranges(hosts_dict, profile_config, networking_dict)
194 raise validation.ValidationError('Unexpected configuration %s' % key)
196 def validate_delete(self, props):
197 logging.debug('validate_delete called with %s', props)
198 if self.domain in props:
199 raise validation.ValidationError('%s cannot be deleted' % self.domain)
201 raise validation.ValidationError('References in %s, cannot be deleted' % self.domain)
203 def validate_hosts(self, hosts_config, nw_profile_config,
204 storage_profile_config, perf_profile_config,
206 net_profile_list = [] if not nw_profile_config \
207 else nw_profile_config.keys()
208 storage_profile_list = [] if not storage_profile_config else storage_profile_config.keys()
209 performance_profile_list = [] if not perf_profile_config else perf_profile_config.keys()
211 service_profile_list = service_profiles.Profiles().get_service_profiles()
218 for key, value in hosts_config.iteritems():
220 if not re.match(r'^[\da-z][\da-z-]*$', key) or len(key) > 63:
221 raise validation.ValidationError('Invalid hostname %s' % key)
224 attr = 'network_domain'
225 network_domain = value.get(attr)
226 if not network_domain:
227 reason = 'Missing %s for %s' % (attr, key)
228 raise validation.ValidationError(reason)
231 attr = 'network_profiles'
232 profiles = value.get(attr)
233 self.validate_profile_list(profiles, net_profile_list, key, attr)
234 if len(profiles) != 1:
235 reason = 'More than one %s defined for %s' % (attr, key)
236 raise validation.ValidationError(reason)
238 nw_profile_name = profiles[0]
239 subnet_name = 'infra_internal'
240 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
241 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
243 # Performance profiles
244 attr = 'performance_profiles'
246 profiles = value.get(attr)
248 self.validate_profile_list(profiles, performance_profile_list,
250 if len(profiles) != 1:
251 reason = 'More than one %s defined for %s' % (attr, key)
252 raise validation.ValidationError(reason)
253 perf_profile = profiles[0]
254 self.validate_nonempty_performance_profile(perf_profile_config, perf_profile, key)
256 if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
258 reason = 'Missing performance profiles with ovs_dpdk type provider network'
259 raise validation.ValidationError(reason)
260 self.validate_performance_profile(perf_profile_config, perf_profile)
263 attr = 'service_profiles'
264 profiles = value.get(attr)
265 self.validate_profile_list(profiles, service_profile_list, key, attr)
266 if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
267 if self.base_profile not in profiles:
268 reason = 'Missing base service profile with ovs_dpdk type provider network'
269 raise validation.ValidationError(reason)
270 if self.is_provider_type_sriov(nw_profile_name, nw_profile_config):
271 if not self.is_sriov_allowed_for_host(profiles):
272 reason = 'Missing base or caas_* service profile'
273 reason += ' with SR-IOV type provider network'
274 raise validation.ValidationError(reason)
276 if not self.is_perf_allowed_for_host(profiles):
277 reason = 'Missing base or caas_* service profile'
278 reason += ' with performance profile host'
279 raise validation.ValidationError(reason)
280 if self.management_profile in profiles:
281 managements.append(key)
282 subnet_name = 'infra_external'
283 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
284 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
286 subnet_name = 'infra_external'
287 if self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
288 raise validation.ValidationError('%s is mapped for %s' % (subnet_name, key))
290 if self.base_profile in profiles:
292 if self.caas_master_profile in profiles:
293 caas_masters.append(key)
295 if self.storage_profile in profiles:
297 st_profiles = value.get('storage_profiles')
298 self.validate_profile_list(st_profiles, storage_profile_list,
299 key, 'storage_profiles')
300 subnet_name = 'infra_storage_cluster'
301 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name) \
302 and self.is_ceph_profile(storage_profile_config, st_profiles):
303 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
306 self.validate_hwmgmt(value.get('hwmgmt'), key)
309 self.validate_mac_list(value.get('mgmt_mac'))
311 # Preallocated IP validation
312 self.validate_preallocated_ips(value, nw_profile_config, networking_config)
314 # Check duplicated Preallocated IPs
315 self.search_for_duplicate_ips(hosts_config)
317 # There should be least one management node
318 if not managements and not caas_masters:
319 reason = 'No management node defined'
320 raise validation.ValidationError(reason)
322 # Number of caas_masters 1 or 3
324 if len(caas_masters) != 1 and len(caas_masters) != 3:
325 reason = 'Unexpected number of caas_master nodes %d' % len(caas_masters)
326 raise validation.ValidationError(reason)
328 # Number of management nodes 1 or 3
330 if len(managements) != 1 and len(managements) != 3:
331 reason = 'Unexpected number of controller nodes %d' % len(managements)
332 raise validation.ValidationError(reason)
334 # All managements must be in same network domain
335 management_network_domain = None
336 for management in managements:
337 if management_network_domain is None:
338 management_network_domain = hosts_config[management].get('network_domain')
340 if not management_network_domain == hosts_config[management].get('network_domain'):
341 reason = 'All management nodes must belong to the same networking domain'
342 raise validation.ValidationError(reason)
344 if len(managements) == 3 and len(storages) < 2:
345 raise validation.ValidationError('There are not enough storage nodes')
347 self.validate_network_ranges(hosts_config, nw_profile_config, networking_config)
349 def validate_network_ranges(self, hosts_config, nw_profile_config, networking_config):
350 host_counts = {} # (infra_network, network_domain) as a key, mapped host count as a value
351 for host_conf in hosts_config.itervalues():
352 if (isinstance(host_conf, dict) and
353 host_conf.get('network_profiles') and
354 isinstance(host_conf['network_profiles'], list) and
355 host_conf['network_profiles']):
356 domain = host_conf.get('network_domain')
357 profile = nw_profile_config.get(host_conf['network_profiles'][0])
358 if (isinstance(profile, dict) and
359 profile.get('interface_net_mapping') and
360 isinstance(profile['interface_net_mapping'], dict)):
361 for infras in profile['interface_net_mapping'].itervalues():
362 if isinstance(infras, list):
364 key = (infra, domain)
365 if key in host_counts:
366 host_counts[key] += 1
369 for (infra, domain), count in host_counts.iteritems():
370 self.validate_infra_network_range(infra, domain, networking_config, count)
372 def validate_infra_network_range(self, infra, network_domain, networking_config, host_count):
373 infra_conf = networking_config.get(infra)
374 if not isinstance(infra_conf, dict):
377 domains_conf = infra_conf.get('network_domains')
378 if not isinstance(domains_conf, dict) or network_domain not in domains_conf:
379 reason = '%s does not contain %s network domain configuration' % \
380 (infra, network_domain)
381 raise validation.ValidationError(reason)
382 cidr = domains_conf[network_domain].get('cidr')
383 start = domains_conf[network_domain].get('ip_range_start')
384 end = domains_conf[network_domain].get('ip_range_end')
386 if not start and cidr:
387 start = str(IPNetwork(cidr)[1])
389 end = str(IPNetwork(cidr)[-2])
390 required = host_count if infra != 'infra_external' else host_count + 1
391 if len(IPRange(start, end)) < required:
392 reason = 'IP range %s - %s does not contain %d addresses' % (start, end, required)
393 raise validation.ValidationError(reason)
395 def validate_profile_list(self, profile_list, profile_defs, host, attribute):
397 raise validation.ValidationError('Missing %s for %s' % (attribute, host))
398 if not isinstance(profile_list, list):
399 raise validation.ValidationError('%s %s value must be a list' % (host, attribute))
400 for profile in profile_list:
401 if profile not in profile_defs:
402 raise validation.ValidationError('Unknown %s %s for %s' %
403 (attribute, profile, host))
405 def validate_hwmgmt(self, hwmgmt, host):
407 raise validation.ValidationError('Missing hwmgmt configuration for %s' % host)
408 if not hwmgmt.get('user'):
409 raise validation.ValidationError('Missing hwmgmt username for %s' % host)
410 if not hwmgmt.get('password'):
411 raise validation.ValidationError('Missing hwmgmt password for %s' % host)
412 validationutils = validation.ValidationUtils()
413 validationutils.validate_ip_address(hwmgmt.get('address'))
415 def validate_nonempty_performance_profile(self, config, profile_name, host_name):
416 profile = config.get(profile_name)
417 if not isinstance(profile, dict) or not profile:
418 reason = 'Empty performance profile %s defined for %s' % (profile_name, host_name)
419 raise validation.ValidationError(reason)
421 def validate_performance_profile(self, config, profile_name):
422 attributes = ['default_hugepagesz', 'hugepagesz', 'hugepages',
424 profile = config.get(profile_name)
427 for attr in attributes:
428 if not profile.get(attr):
429 raise validation.ValidationError('Missing %s value for performance profile %s'
430 % (attr, profile_name))
432 def validate_mac_list(self, mac_list):
436 if not isinstance(mac_list, list):
437 raise validation.ValidationError('mgmt_mac value must be a list')
440 pattern = '[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$'
441 if not mac or not re.match(pattern, mac.lower()):
442 raise validation.ValidationError('Invalid mac address syntax %s' % mac)
444 def validate_preallocated_ips(self, host, nw_profile_config, networking_config):
445 if not self.host_has_preallocated_ip(host):
447 validationutils = validation.ValidationUtils()
448 for network_name, ip in host["pre_allocated_ips"].iteritems():
449 for net_profile_name in host["network_profiles"]:
450 if not self.is_network_in_net_profile(
451 network_name, nw_profile_config.get(net_profile_name)):
452 raise validation.ValidationError(
453 "Network %s is missing from network profile %s" %
454 (network_name, net_profile_name))
455 network_domains = networking_config.get(network_name).get("network_domains")
456 host_network_domain = host["network_domain"]
457 subnet = network_domains.get(host_network_domain)["cidr"]
458 validationutils.validate_ip_address(ip)
459 utils.validate_ip_in_network(ip, subnet)
461 def host_has_preallocated_ip(self, host):
462 ips_field = "pre_allocated_ips"
463 if ips_field in host and host.get(ips_field, {}) and all(host[ips_field]):
467 def is_network_in_net_profile(self, network_name, network_profile):
468 for networks in network_profile["interface_net_mapping"].itervalues():
469 if network_name in networks:
473 def search_for_duplicate_ips(self, hosts):
474 ips_field = "pre_allocated_ips"
475 hosts_with_preallocated_ip = {name: attributes
476 for name, attributes in hosts.iteritems()
477 if self.host_has_preallocated_ip(attributes)}
478 for host_name, host in hosts_with_preallocated_ip.iteritems():
479 other_hosts = {name: attributes
480 for name, attributes in hosts_with_preallocated_ip.iteritems()
481 if name != host_name}
482 for other_host_name, other_host in other_hosts.iteritems():
483 if self.host_has_preallocated_ip(other_host):
485 "Checking %s and %s for duplicated preallocated IPs",
486 host_name, other_host_name)
487 duplicated_ip = self.is_ip_duplicated(host[ips_field], other_host[ips_field])
489 raise validation.ValidationError(
490 "%s and %s has duplicated IP address: %s" %
491 (host_name, other_host_name, duplicated_ip))
493 def is_ip_duplicated(self, ips, other_host_ips):
494 logging.debug("Checking for IP duplication from %s to %s", ips, other_host_ips)
495 for network_name, ip in ips.iteritems():
496 if (network_name in other_host_ips and
497 ip == other_host_ips[network_name]):
501 def get_attribute_value(self, config, name_list):
503 for name in name_list:
504 value = None if not isinstance(value, dict) else value.get(name)
509 def get_domain_dict(self, config, domain_name):
510 client = self.get_plugin_client()
511 str_value = config.get(domain_name)
513 str_value = client.get_property(domain_name)
514 dict_value = {} if not str_value else json.loads(str_value)
517 def is_provider_type_ovs_dpdk(self, profile_name, profile_config):
518 path = [profile_name, 'provider_network_interfaces']
519 provider_ifs = self.get_attribute_value(profile_config, path)
521 for value in provider_ifs.values():
522 if value.get('type') == 'ovs-dpdk':
526 def is_provider_type_sriov(self, profile_name, profile_config):
527 path = [profile_name, 'sriov_provider_networks']
528 if self.get_attribute_value(profile_config, path):
532 def is_sriov_allowed_for_host(self, profiles):
533 return (self.base_profile in profiles or
534 self.caas_worker_profile in profiles or
535 self.caas_master_profile in profiles)
537 def is_perf_allowed_for_host(self, profiles):
538 return self.is_sriov_allowed_for_host(profiles)
540 def network_is_mapped(self, network_profile, name):
541 mapping = network_profile.get('interface_net_mapping')
542 if isinstance(mapping, dict):
543 for interface in mapping.values():
544 if name in interface:
548 def is_ceph_profile(self, storage_profiles, profile_list):
550 for profile in profile_list:
551 backend = storage_profiles[profile].get('backend')
556 def _get_type_of_nodes(self, nodetype, config):
557 nodes = [k for k, v in config.iteritems() if nodetype in v['service_profiles']]
560 def _get_storage_nodes(self, config):
561 return self._get_type_of_nodes(self.storage_profile, config)
563 def _get_changed_hosts_config(self, config, domain_name):
564 str_value = config.get(domain_name)
565 return {} if not str_value else json.loads(str_value)
567 def _get_running_hosts_config(self):
568 return self.get_domain_dict({}, self.domain)
570 def _get_number_of_changed_storage_hosts(self, changes):
571 conf = self._get_changed_hosts_config(changes, self.domain)
572 num = len(self._get_storage_nodes(conf))
574 'HostsValidator: number of changed storage hosts: %s', str(num))
577 def _get_number_of_old_storage_hosts(self):
578 conf = self._get_running_hosts_config()
580 num = len(self._get_storage_nodes(conf))
582 'HostsValidator: number of existing storage hosts: %s', str(num))
584 raise ConfigurationDoesNotExist(
585 "The running hosts configuration does not exist -> deployment ongoing.")
587 def _validate_only_one_storage_host_removed(self, changes):
588 num_existing_storage_hosts = self._get_number_of_old_storage_hosts()
589 if self._get_number_of_changed_storage_hosts(changes) < num_existing_storage_hosts-1:
590 raise validation.ValidationError(
591 "It is allowed to scale-in only 1 storage node at a time.")
593 def validate_scale_in(self, changes):
595 self._validate_only_one_storage_host_removed(changes)
596 except ConfigurationDoesNotExist as exc:
597 logging.debug(str(exc))