FIX: Set default IPMI privilege level
[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
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)
55
56     def validate_set(self, dict_key_value):
57         logging.debug('HostsValidation: validate_set called with %s', dict_key_value)
58
59         for key, value in dict_key_value.iteritems():
60             value_dict = {} if not value else json.loads(value)
61             if not value_dict:
62                 if key != self.storage_profile_attr:
63                     raise validation.ValidationError('No value for %s' % key)
64
65             if key == self.domain:
66                 if not isinstance(value_dict, dict):
67                     raise validation.ValidationError('%s value is not a dict' % self.domain)
68
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,
76                                                        self.networking_attr)
77                 self.validate_hosts(value_dict,
78                                     net_profile_dict,
79                                     storage_profile_dict,
80                                     perf_profile_dict,
81                                     networking_dict)
82
83                 self.validate_scale_in(dict_key_value)
84
85             elif key == self.network_profile_attr:
86                 profile_list = [] if not value_dict else value_dict.keys()
87
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,
96                                                        self.networking_attr)
97
98                 self.validate_network_ranges(host_dict, net_profile_dict, networking_dict)
99
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)
105
106                     performance_profiles = host_data.get('performance_profiles')
107
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:
114                             reason = \
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])
119
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)
125
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,
129                                                                                       host_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))
135                     else:
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))
140
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))
149
150             elif key == self.storage_profile_attr:
151                 profile_list = [] if not value_dict else value_dict.keys()
152
153                 host_dict = self.get_domain_dict(dict_key_value, self.domain)
154
155                 for host_name, host_data in host_dict.iteritems():
156                     attr = 'storage_profiles'
157                     profiles = host_data.get(attr)
158                     if profiles:
159                         self.validate_profile_list(profiles, profile_list, host_name, attr)
160
161             elif key == self.performance_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                 network_profile_config = self.get_domain_dict(dict_key_value,
166                                                               self.network_profile_attr)
167
168                 for host_name, host_data in host_dict.iteritems():
169                     attr = 'performance_profiles'
170                     profiles = host_data.get(attr)
171                     if profiles:
172                         self.validate_profile_list(profiles, profile_list, host_name, attr)
173                         self.validate_nonempty_performance_profile(value_dict, profiles[0],
174                                                                    host_name)
175
176                     network_profiles = host_data.get('network_profiles')
177                     if self.is_provider_type_ovs_dpdk(network_profiles[0], network_profile_config):
178                         if not profiles:
179                             reason = \
180                                 'Missing performance profiles with ovs_dpdk type provider network'
181                             raise validation.ValidationError(reason)
182                         self.validate_performance_profile(value_dict,
183                                                           profiles[0])
184             elif key == self.networking_attr:
185                 networking_dict = value_dict
186
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)
190
191                 self.validate_network_ranges(hosts_dict, profile_config, networking_dict)
192
193             else:
194                 raise validation.ValidationError('Unexpected configuration %s' % key)
195
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)
200         else:
201             raise validation.ValidationError('References in %s, cannot be deleted' % self.domain)
202
203     def validate_hosts(self, hosts_config, nw_profile_config,
204                        storage_profile_config, perf_profile_config,
205                        networking_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()
210
211         service_profile_list = service_profiles.Profiles().get_service_profiles()
212
213         bases = []
214         storages = []
215         caas_masters = []
216         managements = []
217
218         for key, value in hosts_config.iteritems():
219             # Hostname
220             if not re.match(r'^[\da-z][\da-z-]*$', key) or len(key) > 63:
221                 raise validation.ValidationError('Invalid hostname %s' % key)
222
223             # Network domain
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)
229
230             # Network profiles
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)
237
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))
242
243             # Performance profiles
244             attr = 'performance_profiles'
245             perf_profile = None
246             profiles = value.get(attr)
247             if profiles:
248                 self.validate_profile_list(profiles, performance_profile_list,
249                                            key, attr)
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)
255
256             if self.is_provider_type_ovs_dpdk(nw_profile_name, nw_profile_config):
257                 if not profiles:
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)
261
262             # Service profiles
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)
275             if perf_profile:
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))
285             else:
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))
289
290             if self.base_profile in profiles:
291                 bases.append(key)
292             if self.caas_master_profile in profiles:
293                 caas_masters.append(key)
294
295             if self.storage_profile in profiles:
296                 storages.append(key)
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))
304
305             # HW management
306             self.validate_hwmgmt(value.get('hwmgmt'), key)
307
308             # MAC address
309             self.validate_mac_list(value.get('mgmt_mac'))
310
311             # Preallocated IP validation
312             self.validate_preallocated_ips(value, nw_profile_config, networking_config)
313
314         # Check duplicated Preallocated IPs
315         self.search_for_duplicate_ips(hosts_config)
316
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)
321
322         # Number of caas_masters 1 or 3
323         if caas_masters:
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)
327
328         # Number of management nodes 1 or 3
329         if managements:
330             if len(managements) != 1 and len(managements) != 3:
331                 reason = 'Unexpected number of controller nodes %d' % len(managements)
332                 raise validation.ValidationError(reason)
333
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')
339             else:
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)
343
344         if len(managements) == 3 and len(storages) < 2:
345             raise validation.ValidationError('There are not enough storage nodes')
346
347         self.validate_network_ranges(hosts_config, nw_profile_config, networking_config)
348
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):
363                             for infra in infras:
364                                 key = (infra, domain)
365                                 if key in host_counts:
366                                     host_counts[key] += 1
367                                 else:
368                                     host_counts[key] = 1
369         for (infra, domain), count in host_counts.iteritems():
370             self.validate_infra_network_range(infra, domain, networking_config, count)
371
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):
375             return
376
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')
385
386         if not start and cidr:
387             start = str(IPNetwork(cidr)[1])
388         if not end and cidr:
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)
394
395     def validate_profile_list(self, profile_list, profile_defs, host, attribute):
396         if not profile_list:
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))
404
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']
410
411         if not hwmgmt:
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' %
421                                              (priv_level, host))
422         validationutils = validation.ValidationUtils()
423         validationutils.validate_ip_address(hwmgmt.get('address'))
424
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)
430
431     def validate_performance_profile(self, config, profile_name):
432         attributes = ['default_hugepagesz', 'hugepagesz', 'hugepages',
433                       'ovs_dpdk_cpus']
434         profile = config.get(profile_name)
435         if not profile:
436             profile = {}
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))
441
442     def validate_mac_list(self, mac_list):
443         if not mac_list:
444             return
445
446         if not isinstance(mac_list, list):
447             raise validation.ValidationError('mgmt_mac value must be a list')
448
449         for mac in mac_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)
453
454     def validate_preallocated_ips(self, host, nw_profile_config, networking_config):
455         if not self.host_has_preallocated_ip(host):
456             return
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)
470
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]):
474             return True
475         return False
476
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:
480                 return True
481         return False
482
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):
494                     logging.debug(
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])
498                     if duplicated_ip:
499                         raise validation.ValidationError(
500                             "%s and %s has duplicated IP address: %s" %
501                             (host_name, other_host_name, duplicated_ip))
502
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]):
508                 return ip
509         return False
510
511     def get_attribute_value(self, config, name_list):
512         value = config
513         for name in name_list:
514             value = None if not isinstance(value, dict) else value.get(name)
515             if not value:
516                 break
517         return value
518
519     def get_domain_dict(self, config, domain_name):
520         client = self.get_plugin_client()
521         str_value = config.get(domain_name)
522         if not str_value:
523             str_value = client.get_property(domain_name)
524         dict_value = {} if not str_value else json.loads(str_value)
525         return dict_value
526
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)
530         if provider_ifs:
531             for value in provider_ifs.values():
532                 if value.get('type') == 'ovs-dpdk':
533                     return True
534         return False
535
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):
539             return True
540         return False
541
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)
546
547     def is_perf_allowed_for_host(self, profiles):
548         return self.is_sriov_allowed_for_host(profiles)
549
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:
555                     return True
556         return False
557
558     def is_ceph_profile(self, storage_profiles, profile_list):
559         ceph = 'ceph'
560         for profile in profile_list:
561             backend = storage_profiles[profile].get('backend')
562             if backend == ceph:
563                 return True
564         return False
565
566     def _get_type_of_nodes(self, nodetype, config):
567         nodes = [k for k, v in config.iteritems() if nodetype in v['service_profiles']]
568         return nodes
569
570     def _get_storage_nodes(self, config):
571         return self._get_type_of_nodes(self.storage_profile, config)
572
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)
576
577     def _get_running_hosts_config(self):
578         return self.get_domain_dict({}, self.domain)
579
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))
583         logging.debug(
584             'HostsValidator: number of changed storage hosts: %s', str(num))
585         return num
586
587     def _get_number_of_old_storage_hosts(self):
588         conf = self._get_running_hosts_config()
589         if conf:
590             num = len(self._get_storage_nodes(conf))
591             logging.debug(
592                 'HostsValidator: number of existing storage hosts: %s', str(num))
593             return num
594         raise ConfigurationDoesNotExist(
595             "The running hosts configuration does not exist -> deployment ongoing.")
596
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.")
602
603     def validate_scale_in(self, changes):
604         try:
605             self._validate_only_one_storage_host_removed(changes)
606         except ConfigurationDoesNotExist as exc:
607             logging.debug(str(exc))
608             return