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
46 caas_service_profiles = (caas_master_profile, caas_worker_profile)
48 def get_subscription_info(self):
49 logging.debug('get_subscription info called')
50 hosts = r'cloud\.hosts'
51 net_profiles = r'cloud\.network_profiles'
52 storage_profiles = r'cloud\.storage_profiles'
53 perf_profiles = r'cloud\.performance_profiles'
54 net = r'cloud\.networking'
55 return '^%s|%s|%s|%s|%s$' % (hosts, net_profiles, storage_profiles, perf_profiles, net)
57 def validate_set(self, dict_key_value):
58 logging.debug('HostsValidation: validate_set called with %s', dict_key_value)
60 for key, value in dict_key_value.iteritems():
61 value_dict = {} if not value else json.loads(value)
63 if key != self.storage_profile_attr:
64 raise validation.ValidationError('No value for %s' % key)
66 if key == self.domain:
67 if not isinstance(value_dict, dict):
68 raise validation.ValidationError('%s value is not a dict' % self.domain)
70 net_profile_dict = self.get_domain_dict(dict_key_value,
71 self.network_profile_attr)
72 storage_profile_dict = self.get_domain_dict(dict_key_value,
73 self.storage_profile_attr)
74 perf_profile_dict = self.get_domain_dict(dict_key_value,
75 self.performance_profile_attr)
76 networking_dict = self.get_domain_dict(dict_key_value,
78 self.validate_hosts(value_dict,
84 self.validate_scale_in(dict_key_value)
86 elif key == self.network_profile_attr:
87 profile_list = [] if not value_dict else value_dict.keys()
89 host_dict = self.get_domain_dict(dict_key_value, self.domain)
90 perf_profile_config = self.get_domain_dict(dict_key_value,
91 self.performance_profile_attr)
92 storage_profile_config = self.get_domain_dict(dict_key_value,
93 self.storage_profile_attr)
94 net_profile_dict = self.get_domain_dict(dict_key_value,
95 self.network_profile_attr)
96 networking_dict = self.get_domain_dict(dict_key_value,
99 self.validate_network_ranges(host_dict, net_profile_dict, networking_dict)
101 is_caas_oam_mapped_on_any_hosts = False
103 for host_name, host_data in host_dict.iteritems():
104 attr = 'network_profiles'
105 profiles = host_data.get(attr)
106 profile_name = profiles[0]
107 self.validate_profile_list(profiles, profile_list, host_name, attr)
109 performance_profiles = host_data.get('performance_profiles')
111 if self.is_provider_type_ovs_dpdk(profile_name, value_dict):
112 if self.base_profile not in host_data['service_profiles']:
113 reason = 'Missing base service profile with ovs_dpdk'
114 reason += ' type provider network'
115 raise validation.ValidationError(reason)
116 if not performance_profiles:
118 'Missing performance profiles with ovs_dpdk type provider network'
119 raise validation.ValidationError(reason)
120 self.validate_performance_profile(perf_profile_config,
121 performance_profiles[0])
123 if self.is_provider_type_sriov(profile_name, value_dict):
124 if not self.is_sriov_allowed_for_host(host_data['service_profiles']):
125 reason = 'Missing base or caas_* service profile'
126 reason += ' with SR-IOV type provider network'
127 raise validation.ValidationError(reason)
129 subnet_name = 'infra_internal'
130 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name):
131 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name,
133 if self.management_profile in host_data['service_profiles']:
134 subnet_name = 'infra_external'
135 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name):
136 raise validation.ValidationError('%s is not mapped for %s' %
137 (subnet_name, host_name))
139 subnet_name = 'infra_external'
140 if self.network_is_mapped(value_dict.get(profile_name), subnet_name):
141 raise validation.ValidationError('%s is mapped for %s' %
142 (subnet_name, host_name))
144 if self.storage_profile in host_data['service_profiles']:
145 storage_profile_list = host_data.get('storage_profiles')
146 subnet_name = 'infra_storage_cluster'
147 if not self.network_is_mapped(value_dict.get(profile_name), subnet_name) \
148 and self.is_ceph_profile(storage_profile_config,
149 storage_profile_list):
150 raise validation.ValidationError('%s is not mapped for %s' %
151 (subnet_name, host_name))
153 if self.is_host_caas_node(host_data):
154 subnet_name = 'caas_oam'
155 if self.network_is_mapped(value_dict.get(profile_name), subnet_name):
156 is_caas_oam_mapped_on_any_hosts = True
157 elif is_caas_oam_mapped_on_any_hosts:
158 raise validation.ValidationError('%s is not mapped for %s' %
159 (subnet_name, host_name))
161 elif key == self.storage_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)
166 for host_name, host_data in host_dict.iteritems():
167 attr = 'storage_profiles'
168 profiles = host_data.get(attr)
170 self.validate_profile_list(profiles, profile_list, host_name, attr)
172 elif key == self.performance_profile_attr:
173 profile_list = [] if not value_dict else value_dict.keys()
175 host_dict = self.get_domain_dict(dict_key_value, self.domain)
176 network_profile_config = self.get_domain_dict(dict_key_value,
177 self.network_profile_attr)
179 for host_name, host_data in host_dict.iteritems():
180 attr = 'performance_profiles'
181 profiles = host_data.get(attr)
183 self.validate_profile_list(profiles, profile_list, host_name, attr)
184 self.validate_nonempty_performance_profile(value_dict, profiles[0],
187 network_profiles = host_data.get('network_profiles')
188 if self.is_provider_type_ovs_dpdk(network_profiles[0], network_profile_config):
191 'Missing performance profiles with ovs_dpdk type provider network'
192 raise validation.ValidationError(reason)
193 self.validate_performance_profile(value_dict,
195 elif key == self.networking_attr:
196 networking_dict = value_dict
198 hosts_dict = self.get_domain_dict(dict_key_value, self.domain)
199 profile_config = self.get_domain_dict(dict_key_value,
200 self.network_profile_attr)
202 self.validate_network_ranges(hosts_dict, profile_config, networking_dict)
205 raise validation.ValidationError('Unexpected configuration %s' % key)
207 def validate_delete(self, props):
208 logging.debug('validate_delete called with %s', props)
209 if self.domain in props:
210 raise validation.ValidationError('%s cannot be deleted' % self.domain)
212 raise validation.ValidationError('References in %s, cannot be deleted' % self.domain)
214 def validate_hosts(self, hosts_config, nw_profile_config,
215 storage_profile_config, perf_profile_config,
217 net_profile_list = [] if not nw_profile_config \
218 else nw_profile_config.keys()
219 storage_profile_list = [] if not storage_profile_config else storage_profile_config.keys()
220 performance_profile_list = [] if not perf_profile_config else perf_profile_config.keys()
222 service_profile_list = service_profiles.Profiles().get_service_profiles()
229 for key, value in hosts_config.iteritems():
231 if not re.match(r'^[\da-z][\da-z-]*$', key) or len(key) > 63:
232 raise validation.ValidationError('Invalid hostname %s' % key)
235 attr = 'network_domain'
236 network_domain = value.get(attr)
237 if not network_domain:
238 reason = 'Missing %s for %s' % (attr, key)
239 raise validation.ValidationError(reason)
242 attr = 'network_profiles'
243 profiles = value.get(attr)
244 self.validate_profile_list(profiles, net_profile_list, key, attr)
245 if len(profiles) != 1:
246 reason = 'More than one %s defined for %s' % (attr, key)
247 raise validation.ValidationError(reason)
249 nw_profile_name = profiles[0]
250 subnet_name = 'infra_internal'
251 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
252 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
254 # Performance profiles
255 attr = 'performance_profiles'
257 profiles = value.get(attr)
259 self.validate_profile_list(profiles, performance_profile_list,
261 if len(profiles) != 1:
262 reason = 'More than one %s defined for %s' % (attr, key)
263 raise validation.ValidationError(reason)
264 perf_profile = profiles[0]
265 self.validate_nonempty_performance_profile(perf_profile_config, perf_profile, key)
267 if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
269 reason = 'Missing performance profiles with ovs_dpdk type provider network'
270 raise validation.ValidationError(reason)
271 self.validate_performance_profile(perf_profile_config, perf_profile)
274 attr = 'service_profiles'
275 profiles = value.get(attr)
276 self.validate_profile_list(profiles, service_profile_list, key, attr)
277 if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
278 if self.base_profile not in profiles:
279 reason = 'Missing base service profile with ovs_dpdk type provider network'
280 raise validation.ValidationError(reason)
281 if self.is_provider_type_sriov(nw_profile_name, nw_profile_config):
282 if not self.is_sriov_allowed_for_host(profiles):
283 reason = 'Missing base or caas_* service profile'
284 reason += ' with SR-IOV type provider network'
285 raise validation.ValidationError(reason)
287 if not self.is_perf_allowed_for_host(profiles):
288 reason = 'Missing base or caas_* service profile'
289 reason += ' with performance profile host'
290 raise validation.ValidationError(reason)
291 if self.management_profile in profiles:
292 managements.append(key)
293 subnet_name = 'infra_external'
294 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
295 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
297 subnet_name = 'infra_external'
298 if self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name):
299 raise validation.ValidationError('%s is mapped for %s' % (subnet_name, key))
301 if self.base_profile in profiles:
303 if self.caas_master_profile in profiles:
304 caas_masters.append(key)
306 if self.storage_profile in profiles:
308 st_profiles = value.get('storage_profiles')
309 self.validate_profile_list(st_profiles, storage_profile_list,
310 key, 'storage_profiles')
311 subnet_name = 'infra_storage_cluster'
312 if not self.network_is_mapped(nw_profile_config.get(nw_profile_name), subnet_name) \
313 and self.is_ceph_profile(storage_profile_config, st_profiles):
314 raise validation.ValidationError('%s is not mapped for %s' % (subnet_name, key))
317 self.validate_hwmgmt(value.get('hwmgmt'), key)
320 self.validate_mac_list(value.get('mgmt_mac'))
322 # Preallocated IP validation
323 self.validate_preallocated_ips(value, nw_profile_config, networking_config)
325 # Check duplicated Preallocated IPs
326 self.search_for_duplicate_ips(hosts_config)
328 # There should be least one management node
329 if not managements and not caas_masters:
330 reason = 'No management node defined'
331 raise validation.ValidationError(reason)
333 # Number of caas_masters 1 or 3
335 if len(caas_masters) != 1 and len(caas_masters) != 3:
336 reason = 'Unexpected number of caas_master nodes %d' % len(caas_masters)
337 raise validation.ValidationError(reason)
339 # Number of management nodes 1 or 3
341 if len(managements) != 1 and len(managements) != 3:
342 reason = 'Unexpected number of controller nodes %d' % len(managements)
343 raise validation.ValidationError(reason)
345 # All managements must be in same network domain
346 management_network_domain = None
347 for management in managements:
348 if management_network_domain is None:
349 management_network_domain = hosts_config[management].get('network_domain')
351 if not management_network_domain == hosts_config[management].get('network_domain'):
352 reason = 'All management nodes must belong to the same networking domain'
353 raise validation.ValidationError(reason)
355 if len(managements) == 3 and len(storages) < 2:
356 raise validation.ValidationError('There are not enough storage nodes')
358 self.validate_network_ranges(hosts_config, nw_profile_config, networking_config)
360 def validate_network_ranges(self, hosts_config, nw_profile_config, networking_config):
361 host_counts = {} # (infra_network, network_domain) as a key, mapped host count as a value
362 for host_conf in hosts_config.itervalues():
363 if (isinstance(host_conf, dict) and
364 host_conf.get('network_profiles') and
365 isinstance(host_conf['network_profiles'], list) and
366 host_conf['network_profiles']):
367 domain = host_conf.get('network_domain')
368 profile = nw_profile_config.get(host_conf['network_profiles'][0])
369 if (isinstance(profile, dict) and
370 profile.get('interface_net_mapping') and
371 isinstance(profile['interface_net_mapping'], dict)):
372 for infras in profile['interface_net_mapping'].itervalues():
373 if isinstance(infras, list):
375 key = (infra, domain)
376 if key in host_counts:
377 host_counts[key] += 1
380 for (infra, domain), count in host_counts.iteritems():
381 self.validate_infra_network_range(infra, domain, networking_config, count)
383 def validate_infra_network_range(self, infra, network_domain, networking_config, host_count):
384 infra_conf = networking_config.get(infra)
385 if not isinstance(infra_conf, dict):
388 domains_conf = infra_conf.get('network_domains')
389 if not isinstance(domains_conf, dict) or network_domain not in domains_conf:
390 reason = '%s does not contain %s network domain configuration' % \
391 (infra, network_domain)
392 raise validation.ValidationError(reason)
393 cidr = domains_conf[network_domain].get('cidr')
394 start = domains_conf[network_domain].get('ip_range_start')
395 end = domains_conf[network_domain].get('ip_range_end')
397 if not start and cidr:
398 start = str(IPNetwork(cidr)[1])
400 end = str(IPNetwork(cidr)[-2])
401 required = host_count if infra != 'infra_external' else host_count + 1
402 if len(IPRange(start, end)) < required:
403 reason = 'IP range %s - %s does not contain %d addresses' % (start, end, required)
404 raise validation.ValidationError(reason)
406 def validate_profile_list(self, profile_list, profile_defs, host, attribute):
408 raise validation.ValidationError('Missing %s for %s' % (attribute, host))
409 if not isinstance(profile_list, list):
410 raise validation.ValidationError('%s %s value must be a list' % (host, attribute))
411 for profile in profile_list:
412 if profile not in profile_defs:
413 raise validation.ValidationError('Unknown %s %s for %s' %
414 (attribute, profile, host))
416 def validate_hwmgmt(self, hwmgmt, host):
417 # this list may not be comprehensive, but it matches ironic's idea
418 # of valid privileges. In practice, we'll likely only see OPERATOR
419 # and ADMINISTRATOR. Case seems to matter here.
420 valid_ipmi_priv = ['USER', 'CALLBACK', 'OPERATOR', 'ADMINISTRATOR']
423 raise validation.ValidationError('Missing hwmgmt configuration for %s' % host)
424 if not hwmgmt.get('user'):
425 raise validation.ValidationError('Missing hwmgmt username for %s' % host)
426 if not hwmgmt.get('password'):
427 raise validation.ValidationError('Missing hwmgmt password for %s' % host)
428 priv_level = hwmgmt.get('priv_level')
429 if priv_level and priv_level not in valid_ipmi_priv:
430 # priv_level is optional, but should be in the valid range.
431 raise validation.ValidationError('Invalid IPMI privilege level %s for %s' %
433 validationutils = validation.ValidationUtils()
434 validationutils.validate_ip_address(hwmgmt.get('address'))
436 def validate_nonempty_performance_profile(self, config, profile_name, host_name):
437 profile = config.get(profile_name)
438 if not isinstance(profile, dict) or not profile:
439 reason = 'Empty performance profile %s defined for %s' % (profile_name, host_name)
440 raise validation.ValidationError(reason)
442 def validate_performance_profile(self, config, profile_name):
443 attributes = ['default_hugepagesz', 'hugepagesz', 'hugepages',
445 profile = config.get(profile_name)
448 for attr in attributes:
449 if not profile.get(attr):
450 raise validation.ValidationError('Missing %s value for performance profile %s'
451 % (attr, profile_name))
453 def validate_mac_list(self, mac_list):
457 if not isinstance(mac_list, list):
458 raise validation.ValidationError('mgmt_mac value must be a list')
461 pattern = '[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$'
462 if not mac or not re.match(pattern, mac.lower()):
463 raise validation.ValidationError('Invalid mac address syntax %s' % mac)
465 def validate_preallocated_ips(self, host, nw_profile_config, networking_config):
466 if not self.host_has_preallocated_ip(host):
468 validationutils = validation.ValidationUtils()
469 for network_name, ip in host["pre_allocated_ips"].iteritems():
470 for net_profile_name in host["network_profiles"]:
471 if not self.is_network_in_net_profile(
472 network_name, nw_profile_config.get(net_profile_name)):
473 raise validation.ValidationError(
474 "Network %s is missing from network profile %s" %
475 (network_name, net_profile_name))
476 network_domains = networking_config.get(network_name).get("network_domains")
477 host_network_domain = host["network_domain"]
478 subnet = network_domains.get(host_network_domain)["cidr"]
479 validationutils.validate_ip_address(ip)
480 utils.validate_ip_in_network(ip, subnet)
482 def host_has_preallocated_ip(self, host):
483 ips_field = "pre_allocated_ips"
484 if ips_field in host and host.get(ips_field, {}) and all(host[ips_field]):
488 def is_network_in_net_profile(self, network_name, network_profile):
489 for networks in network_profile["interface_net_mapping"].itervalues():
490 if network_name in networks:
494 def search_for_duplicate_ips(self, hosts):
495 ips_field = "pre_allocated_ips"
496 hosts_with_preallocated_ip = {name: attributes
497 for name, attributes in hosts.iteritems()
498 if self.host_has_preallocated_ip(attributes)}
499 for host_name, host in hosts_with_preallocated_ip.iteritems():
500 other_hosts = {name: attributes
501 for name, attributes in hosts_with_preallocated_ip.iteritems()
502 if name != host_name}
503 for other_host_name, other_host in other_hosts.iteritems():
504 if self.host_has_preallocated_ip(other_host):
506 "Checking %s and %s for duplicated preallocated IPs",
507 host_name, other_host_name)
508 duplicated_ip = self.is_ip_duplicated(host[ips_field], other_host[ips_field])
510 raise validation.ValidationError(
511 "%s and %s has duplicated IP address: %s" %
512 (host_name, other_host_name, duplicated_ip))
514 def is_ip_duplicated(self, ips, other_host_ips):
515 logging.debug("Checking for IP duplication from %s to %s", ips, other_host_ips)
516 for network_name, ip in ips.iteritems():
517 if (network_name in other_host_ips and
518 ip == other_host_ips[network_name]):
522 def get_attribute_value(self, config, name_list):
524 for name in name_list:
525 value = None if not isinstance(value, dict) else value.get(name)
530 def get_domain_dict(self, config, domain_name):
531 client = self.get_plugin_client()
532 str_value = config.get(domain_name)
534 str_value = client.get_property(domain_name)
535 dict_value = {} if not str_value else json.loads(str_value)
538 def is_provider_type_ovs_dpdk(self, profile_name, profile_config):
539 path = [profile_name, 'provider_network_interfaces']
540 provider_ifs = self.get_attribute_value(profile_config, path)
542 for value in provider_ifs.values():
543 if value.get('type') == 'ovs-dpdk':
547 def is_provider_type_sriov(self, profile_name, profile_config):
548 path = [profile_name, 'sriov_provider_networks']
549 if self.get_attribute_value(profile_config, path):
553 def is_sriov_allowed_for_host(self, profiles):
554 return (self.base_profile in profiles or
555 self.caas_worker_profile in profiles or
556 self.caas_master_profile in profiles)
558 def is_perf_allowed_for_host(self, profiles):
559 return self.is_sriov_allowed_for_host(profiles)
561 def network_is_mapped(self, network_profile, name):
562 mapping = network_profile.get('interface_net_mapping')
563 if isinstance(mapping, dict):
564 for interface in mapping.values():
565 if name in interface:
569 def is_ceph_profile(self, storage_profiles, profile_list):
571 for profile in profile_list:
572 backend = storage_profiles[profile].get('backend')
577 def is_host_caas_node(self, host):
578 return bool(set(self.caas_service_profiles).intersection(host['service_profiles']))
580 def _get_type_of_nodes(self, nodetype, config):
581 nodes = [k for k, v in config.iteritems() if nodetype in v['service_profiles']]
584 def _get_storage_nodes(self, config):
585 return self._get_type_of_nodes(self.storage_profile, config)
587 def _get_changed_hosts_config(self, config, domain_name):
588 str_value = config.get(domain_name)
589 return {} if not str_value else json.loads(str_value)
591 def _get_running_hosts_config(self):
592 return self.get_domain_dict({}, self.domain)
594 def _get_number_of_changed_storage_hosts(self, changes):
595 conf = self._get_changed_hosts_config(changes, self.domain)
596 num = len(self._get_storage_nodes(conf))
598 'HostsValidator: number of changed storage hosts: %s', str(num))
601 def _get_number_of_old_storage_hosts(self):
602 conf = self._get_running_hosts_config()
604 num = len(self._get_storage_nodes(conf))
606 'HostsValidator: number of existing storage hosts: %s', str(num))
608 raise ConfigurationDoesNotExist(
609 "The running hosts configuration does not exist -> deployment ongoing.")
611 def _validate_only_one_storage_host_removed(self, changes):
612 num_existing_storage_hosts = self._get_number_of_old_storage_hosts()
613 if self._get_number_of_changed_storage_hosts(changes) < num_existing_storage_hosts-1:
614 raise validation.ValidationError(
615 "It is allowed to scale-in only 1 storage node at a time.")
617 def validate_scale_in(self, changes):
619 self._validate_only_one_storage_host_removed(changes)
620 except ConfigurationDoesNotExist as exc:
621 logging.debug(str(exc))