Add validation for "caas_oam" networking type
[ta/cm-plugins.git] / validators / src / HostsValidation.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 logging
17 import json
18 import re
19 from netaddr import IPRange
20 from netaddr import IPNetwork
21
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
26
27
28 class ConfigurationDoesNotExist(Exception):
29     pass
30
31
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'
38     base_profile = 'base'
39     storage_profile = 'storage'
40
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)
47
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)
56
57     def validate_set(self, dict_key_value):
58         logging.debug('HostsValidation: validate_set called with %s', dict_key_value)
59
60         for key, value in dict_key_value.iteritems():
61             value_dict = {} if not value else json.loads(value)
62             if not value_dict:
63                 if key != self.storage_profile_attr:
64                     raise validation.ValidationError('No value for %s' % key)
65
66             if key == self.domain:
67                 if not isinstance(value_dict, dict):
68                     raise validation.ValidationError('%s value is not a dict' % self.domain)
69
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,
77                                                        self.networking_attr)
78                 self.validate_hosts(value_dict,
79                                     net_profile_dict,
80                                     storage_profile_dict,
81                                     perf_profile_dict,
82                                     networking_dict)
83
84                 self.validate_scale_in(dict_key_value)
85
86             elif key == self.network_profile_attr:
87                 profile_list = [] if not value_dict else value_dict.keys()
88
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,
97                                                        self.networking_attr)
98
99                 self.validate_network_ranges(host_dict, net_profile_dict, networking_dict)
100
101                 is_caas_oam_mapped_on_any_hosts = False
102
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)
108
109                     performance_profiles = host_data.get('performance_profiles')
110
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:
117                             reason = \
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])
122
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)
128
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,
132                                                                                       host_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))
138                     else:
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))
143
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))
152
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))
160
161             elif key == self.storage_profile_attr:
162                 profile_list = [] if not value_dict else value_dict.keys()
163
164                 host_dict = self.get_domain_dict(dict_key_value, self.domain)
165
166                 for host_name, host_data in host_dict.iteritems():
167                     attr = 'storage_profiles'
168                     profiles = host_data.get(attr)
169                     if profiles:
170                         self.validate_profile_list(profiles, profile_list, host_name, attr)
171
172             elif key == self.performance_profile_attr:
173                 profile_list = [] if not value_dict else value_dict.keys()
174
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)
178
179                 for host_name, host_data in host_dict.iteritems():
180                     attr = 'performance_profiles'
181                     profiles = host_data.get(attr)
182                     if profiles:
183                         self.validate_profile_list(profiles, profile_list, host_name, attr)
184                         self.validate_nonempty_performance_profile(value_dict, profiles[0],
185                                                                    host_name)
186
187                     network_profiles = host_data.get('network_profiles')
188                     if self.is_provider_type_ovs_dpdk(network_profiles[0], network_profile_config):
189                         if not profiles:
190                             reason = \
191                                 'Missing performance profiles with ovs_dpdk type provider network'
192                             raise validation.ValidationError(reason)
193                         self.validate_performance_profile(value_dict,
194                                                           profiles[0])
195             elif key == self.networking_attr:
196                 networking_dict = value_dict
197
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)
201
202                 self.validate_network_ranges(hosts_dict, profile_config, networking_dict)
203
204             else:
205                 raise validation.ValidationError('Unexpected configuration %s' % key)
206
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)
211         else:
212             raise validation.ValidationError('References in %s, cannot be deleted' % self.domain)
213
214     def validate_hosts(self, hosts_config, nw_profile_config,
215                        storage_profile_config, perf_profile_config,
216                        networking_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()
221
222         service_profile_list = service_profiles.Profiles().get_service_profiles()
223
224         bases = []
225         storages = []
226         caas_masters = []
227         managements = []
228
229         for key, value in hosts_config.iteritems():
230             # Hostname
231             if not re.match(r'^[\da-z][\da-z-]*$', key) or len(key) > 63:
232                 raise validation.ValidationError('Invalid hostname %s' % key)
233
234             # Network domain
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)
240
241             # Network profiles
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)
248
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))
253
254             # Performance profiles
255             attr = 'performance_profiles'
256             perf_profile = None
257             profiles = value.get(attr)
258             if profiles:
259                 self.validate_profile_list(profiles, performance_profile_list,
260                                            key, attr)
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)
266
267             if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
268                 if not profiles:
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)
272
273             # Service profiles
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)
286             if perf_profile:
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))
296             else:
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))
300
301             if self.base_profile in profiles:
302                 bases.append(key)
303             if self.caas_master_profile in profiles:
304                 caas_masters.append(key)
305
306             if self.storage_profile in profiles:
307                 storages.append(key)
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))
315
316             # HW management
317             self.validate_hwmgmt(value.get('hwmgmt'), key)
318
319             # MAC address
320             self.validate_mac_list(value.get('mgmt_mac'))
321
322             # Preallocated IP validation
323             self.validate_preallocated_ips(value, nw_profile_config, networking_config)
324
325         # Check duplicated Preallocated IPs
326         self.search_for_duplicate_ips(hosts_config)
327
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)
332
333         # Number of caas_masters 1 or 3
334         if caas_masters:
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)
338
339         # Number of management nodes 1 or 3
340         if managements:
341             if len(managements) != 1 and len(managements) != 3:
342                 reason = 'Unexpected number of controller nodes %d' % len(managements)
343                 raise validation.ValidationError(reason)
344
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')
350             else:
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)
354
355         if len(managements) == 3 and len(storages) < 2:
356             raise validation.ValidationError('There are not enough storage nodes')
357
358         self.validate_network_ranges(hosts_config, nw_profile_config, networking_config)
359
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):
374                             for infra in infras:
375                                 key = (infra, domain)
376                                 if key in host_counts:
377                                     host_counts[key] += 1
378                                 else:
379                                     host_counts[key] = 1
380         for (infra, domain), count in host_counts.iteritems():
381             self.validate_infra_network_range(infra, domain, networking_config, count)
382
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):
386             return
387
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')
396
397         if not start and cidr:
398             start = str(IPNetwork(cidr)[1])
399         if not end and cidr:
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)
405
406     def validate_profile_list(self, profile_list, profile_defs, host, attribute):
407         if not profile_list:
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))
415
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']
421
422         if not hwmgmt:
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' %
432                                              (priv_level, host))
433         validationutils = validation.ValidationUtils()
434         validationutils.validate_ip_address(hwmgmt.get('address'))
435
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)
441
442     def validate_performance_profile(self, config, profile_name):
443         attributes = ['default_hugepagesz', 'hugepagesz', 'hugepages',
444                       'ovs_dpdk_cpus']
445         profile = config.get(profile_name)
446         if not profile:
447             profile = {}
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))
452
453     def validate_mac_list(self, mac_list):
454         if not mac_list:
455             return
456
457         if not isinstance(mac_list, list):
458             raise validation.ValidationError('mgmt_mac value must be a list')
459
460         for mac in mac_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)
464
465     def validate_preallocated_ips(self, host, nw_profile_config, networking_config):
466         if not self.host_has_preallocated_ip(host):
467             return
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)
481
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]):
485             return True
486         return False
487
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:
491                 return True
492         return False
493
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):
505                     logging.debug(
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])
509                     if duplicated_ip:
510                         raise validation.ValidationError(
511                             "%s and %s has duplicated IP address: %s" %
512                             (host_name, other_host_name, duplicated_ip))
513
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]):
519                 return ip
520         return False
521
522     def get_attribute_value(self, config, name_list):
523         value = config
524         for name in name_list:
525             value = None if not isinstance(value, dict) else value.get(name)
526             if not value:
527                 break
528         return value
529
530     def get_domain_dict(self, config, domain_name):
531         client = self.get_plugin_client()
532         str_value = config.get(domain_name)
533         if not str_value:
534             str_value = client.get_property(domain_name)
535         dict_value = {} if not str_value else json.loads(str_value)
536         return dict_value
537
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)
541         if provider_ifs:
542             for value in provider_ifs.values():
543                 if value.get('type') == 'ovs-dpdk':
544                     return True
545         return False
546
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):
550             return True
551         return False
552
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)
557
558     def is_perf_allowed_for_host(self, profiles):
559         return self.is_sriov_allowed_for_host(profiles)
560
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:
566                     return True
567         return False
568
569     def is_ceph_profile(self, storage_profiles, profile_list):
570         ceph = 'ceph'
571         for profile in profile_list:
572             backend = storage_profiles[profile].get('backend')
573             if backend == ceph:
574                 return True
575         return False
576
577     def is_host_caas_node(self, host):
578         return bool(set(self.caas_service_profiles).intersection(host['service_profiles']))
579
580     def _get_type_of_nodes(self, nodetype, config):
581         nodes = [k for k, v in config.iteritems() if nodetype in v['service_profiles']]
582         return nodes
583
584     def _get_storage_nodes(self, config):
585         return self._get_type_of_nodes(self.storage_profile, config)
586
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)
590
591     def _get_running_hosts_config(self):
592         return self.get_domain_dict({}, self.domain)
593
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))
597         logging.debug(
598             'HostsValidator: number of changed storage hosts: %s', str(num))
599         return num
600
601     def _get_number_of_old_storage_hosts(self):
602         conf = self._get_running_hosts_config()
603         if conf:
604             num = len(self._get_storage_nodes(conf))
605             logging.debug(
606                 'HostsValidator: number of existing storage hosts: %s', str(num))
607             return num
608         raise ConfigurationDoesNotExist(
609             "The running hosts configuration does not exist -> deployment ongoing.")
610
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.")
616
617     def validate_scale_in(self, changes):
618         try:
619             self._validate_only_one_storage_host_removed(changes)
620         except ConfigurationDoesNotExist as exc:
621             logging.debug(str(exc))
622             return