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):
406 # this list may not be comprehensive, but it matches ironic's idea
407 # of valid privileges. In practice, we'll likely only see OPERATOR
408 # and ADMINISTRATOR. Case seems to matter here.
409 valid_ipmi_priv = ['USER', 'CALLBACK', 'OPERATOR', 'ADMINISTRATOR']
412 raise validation.ValidationError('Missing hwmgmt configuration for %s' % host)
413 if not hwmgmt.get('user'):
414 raise validation.ValidationError('Missing hwmgmt username for %s' % host)
415 if not hwmgmt.get('password'):
416 raise validation.ValidationError('Missing hwmgmt password for %s' % host)
417 priv_level = hwmgmt.get('priv_level')
418 if priv_level and priv_level not in valid_ipmi_priv:
419 # priv_level is optional, but should be in the valid range.
420 raise validation.ValidationError('Invalid IPMI privilege level %s for %s' %
422 validationutils = validation.ValidationUtils()
423 validationutils.validate_ip_address(hwmgmt.get('address'))
425 def validate_nonempty_performance_profile(self, config, profile_name, host_name):
426 profile = config.get(profile_name)
427 if not isinstance(profile, dict) or not profile:
428 reason = 'Empty performance profile %s defined for %s' % (profile_name, host_name)
429 raise validation.ValidationError(reason)
431 def validate_performance_profile(self, config, profile_name):
432 attributes = ['default_hugepagesz', 'hugepagesz', 'hugepages',
434 profile = config.get(profile_name)
437 for attr in attributes:
438 if not profile.get(attr):
439 raise validation.ValidationError('Missing %s value for performance profile %s'
440 % (attr, profile_name))
442 def validate_mac_list(self, mac_list):
446 if not isinstance(mac_list, list):
447 raise validation.ValidationError('mgmt_mac value must be a list')
450 pattern = '[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$'
451 if not mac or not re.match(pattern, mac.lower()):
452 raise validation.ValidationError('Invalid mac address syntax %s' % mac)
454 def validate_preallocated_ips(self, host, nw_profile_config, networking_config):
455 if not self.host_has_preallocated_ip(host):
457 validationutils = validation.ValidationUtils()
458 for network_name, ip in host["pre_allocated_ips"].iteritems():
459 for net_profile_name in host["network_profiles"]:
460 if not self.is_network_in_net_profile(
461 network_name, nw_profile_config.get(net_profile_name)):
462 raise validation.ValidationError(
463 "Network %s is missing from network profile %s" %
464 (network_name, net_profile_name))
465 network_domains = networking_config.get(network_name).get("network_domains")
466 host_network_domain = host["network_domain"]
467 subnet = network_domains.get(host_network_domain)["cidr"]
468 validationutils.validate_ip_address(ip)
469 utils.validate_ip_in_network(ip, subnet)
471 def host_has_preallocated_ip(self, host):
472 ips_field = "pre_allocated_ips"
473 if ips_field in host and host.get(ips_field, {}) and all(host[ips_field]):
477 def is_network_in_net_profile(self, network_name, network_profile):
478 for networks in network_profile["interface_net_mapping"].itervalues():
479 if network_name in networks:
483 def search_for_duplicate_ips(self, hosts):
484 ips_field = "pre_allocated_ips"
485 hosts_with_preallocated_ip = {name: attributes
486 for name, attributes in hosts.iteritems()
487 if self.host_has_preallocated_ip(attributes)}
488 for host_name, host in hosts_with_preallocated_ip.iteritems():
489 other_hosts = {name: attributes
490 for name, attributes in hosts_with_preallocated_ip.iteritems()
491 if name != host_name}
492 for other_host_name, other_host in other_hosts.iteritems():
493 if self.host_has_preallocated_ip(other_host):
495 "Checking %s and %s for duplicated preallocated IPs",
496 host_name, other_host_name)
497 duplicated_ip = self.is_ip_duplicated(host[ips_field], other_host[ips_field])
499 raise validation.ValidationError(
500 "%s and %s has duplicated IP address: %s" %
501 (host_name, other_host_name, duplicated_ip))
503 def is_ip_duplicated(self, ips, other_host_ips):
504 logging.debug("Checking for IP duplication from %s to %s", ips, other_host_ips)
505 for network_name, ip in ips.iteritems():
506 if (network_name in other_host_ips and
507 ip == other_host_ips[network_name]):
511 def get_attribute_value(self, config, name_list):
513 for name in name_list:
514 value = None if not isinstance(value, dict) else value.get(name)
519 def get_domain_dict(self, config, domain_name):
520 client = self.get_plugin_client()
521 str_value = config.get(domain_name)
523 str_value = client.get_property(domain_name)
524 dict_value = {} if not str_value else json.loads(str_value)
527 def is_provider_type_ovs_dpdk(self, profile_name, profile_config):
528 path = [profile_name, 'provider_network_interfaces']
529 provider_ifs = self.get_attribute_value(profile_config, path)
531 for value in provider_ifs.values():
532 if value.get('type') == 'ovs-dpdk':
536 def is_provider_type_sriov(self, profile_name, profile_config):
537 path = [profile_name, 'sriov_provider_networks']
538 if self.get_attribute_value(profile_config, path):
542 def is_sriov_allowed_for_host(self, profiles):
543 return (self.base_profile in profiles or
544 self.caas_worker_profile in profiles or
545 self.caas_master_profile in profiles)
547 def is_perf_allowed_for_host(self, profiles):
548 return self.is_sriov_allowed_for_host(profiles)
550 def network_is_mapped(self, network_profile, name):
551 mapping = network_profile.get('interface_net_mapping')
552 if isinstance(mapping, dict):
553 for interface in mapping.values():
554 if name in interface:
558 def is_ceph_profile(self, storage_profiles, profile_list):
560 for profile in profile_list:
561 backend = storage_profiles[profile].get('backend')
566 def _get_type_of_nodes(self, nodetype, config):
567 nodes = [k for k, v in config.iteritems() if nodetype in v['service_profiles']]
570 def _get_storage_nodes(self, config):
571 return self._get_type_of_nodes(self.storage_profile, config)
573 def _get_changed_hosts_config(self, config, domain_name):
574 str_value = config.get(domain_name)
575 return {} if not str_value else json.loads(str_value)
577 def _get_running_hosts_config(self):
578 return self.get_domain_dict({}, self.domain)
580 def _get_number_of_changed_storage_hosts(self, changes):
581 conf = self._get_changed_hosts_config(changes, self.domain)
582 num = len(self._get_storage_nodes(conf))
584 'HostsValidator: number of changed storage hosts: %s', str(num))
587 def _get_number_of_old_storage_hosts(self):
588 conf = self._get_running_hosts_config()
590 num = len(self._get_storage_nodes(conf))
592 'HostsValidator: number of existing storage hosts: %s', str(num))
594 raise ConfigurationDoesNotExist(
595 "The running hosts configuration does not exist -> deployment ongoing.")
597 def _validate_only_one_storage_host_removed(self, changes):
598 num_existing_storage_hosts = self._get_number_of_old_storage_hosts()
599 if self._get_number_of_changed_storage_hosts(changes) < num_existing_storage_hosts-1:
600 raise validation.ValidationError(
601 "It is allowed to scale-in only 1 storage node at a time.")
603 def validate_scale_in(self, changes):
605 self._validate_only_one_storage_host_removed(changes)
606 except ConfigurationDoesNotExist as exc:
607 logging.debug(str(exc))