Fuel UI
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

cluster.py 23KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2013 Mirantis, Inc.
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # 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, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. from distutils import version
  16. import six
  17. import sqlalchemy as sa
  18. from nailgun.api.v1.validators import base
  19. from nailgun.api.v1.validators.json_schema import cluster as cluster_schema
  20. from nailgun.api.v1.validators.node import ProvisionSelectedNodesValidator
  21. from nailgun import consts
  22. from nailgun.db import db
  23. from nailgun.db.sqlalchemy.models import Node
  24. from nailgun import errors
  25. from nailgun import objects
  26. from nailgun.plugins.manager import PluginManager
  27. from nailgun.utils.restrictions import ComponentsRestrictions
  28. class ClusterValidator(base.BasicValidator):
  29. single_schema = cluster_schema.single_schema
  30. collection_schema = cluster_schema.collection_schema
  31. _blocked_for_update = (
  32. 'net_provider',
  33. )
  34. @classmethod
  35. def _validate_common(cls, data, instance=None):
  36. d = cls.validate_json(data)
  37. release_id = d.get("release", d.get("release_id"))
  38. if release_id:
  39. release = objects.Release.get_by_uid(release_id)
  40. if not release:
  41. raise errors.InvalidData(
  42. "Invalid release ID", log_message=True)
  43. if not objects.Release.is_deployable(release):
  44. raise errors.NotAllowed(
  45. "Release with ID '{0}' is not deployable.".format(
  46. release_id), log_message=True)
  47. cls._validate_mode(d, release)
  48. return d
  49. @classmethod
  50. def _validate_components(cls, release_id, components_list):
  51. release = objects.Release.get_by_uid(release_id)
  52. release_components = objects.Release.get_all_components(release)
  53. ComponentsRestrictions.validate_components(
  54. components_list,
  55. release_components,
  56. release.required_component_types)
  57. @classmethod
  58. def validate(cls, data):
  59. d = cls._validate_common(data)
  60. # TODO(ikalnitsky): move it to _validate_common when
  61. # PATCH method will be implemented
  62. release_id = d.get("release", d.get("release_id", None))
  63. if not release_id:
  64. raise errors.InvalidData(
  65. u"Release ID is required", log_message=True)
  66. if "name" in d:
  67. if objects.ClusterCollection.filter_by(
  68. None, name=d["name"]).first():
  69. raise errors.AlreadyExists(
  70. "Environment with this name already exists",
  71. log_message=True
  72. )
  73. if "components" in d:
  74. cls._validate_components(release_id, d['components'])
  75. return d
  76. @classmethod
  77. def validate_update(cls, data, instance):
  78. d = cls._validate_common(data, instance=instance)
  79. if "name" in d:
  80. query = objects.ClusterCollection.filter_by_not(
  81. None, id=instance.id)
  82. if objects.ClusterCollection.filter_by(
  83. query, name=d["name"]).first():
  84. raise errors.AlreadyExists(
  85. "Environment with this name already exists",
  86. log_message=True
  87. )
  88. for k in cls._blocked_for_update:
  89. if k in d and getattr(instance, k) != d[k]:
  90. raise errors.InvalidData(
  91. u"Changing '{0}' for environment is prohibited".format(k),
  92. log_message=True
  93. )
  94. cls._validate_mode(d, instance.release)
  95. if 'nodes' in d:
  96. # Here d['nodes'] is list of node IDs
  97. # to be assigned to the cluster.
  98. cls._validate_nodes(d['nodes'], instance)
  99. return d
  100. @classmethod
  101. def _validate_mode(cls, data, release):
  102. mode = data.get("mode")
  103. if mode and mode not in release.modes:
  104. modes_list = ', '.join(release.modes)
  105. raise errors.InvalidData(
  106. "Cannot deploy in {0} mode in current release."
  107. " Need to be one of: {1}".format(
  108. mode, modes_list),
  109. log_message=True
  110. )
  111. @classmethod
  112. def _validate_nodes(cls, new_node_ids, instance):
  113. set_new_node_ids = set(new_node_ids)
  114. set_old_node_ids = set(objects.Cluster.get_nodes_ids(instance))
  115. nodes_to_add = set_new_node_ids - set_old_node_ids
  116. nodes_to_remove = set_old_node_ids - set_new_node_ids
  117. hostnames_to_add = [x[0] for x in db.query(Node.hostname)
  118. .filter(Node.id.in_(nodes_to_add)).all()]
  119. duplicated = [x[0] for x in db.query(Node.hostname).filter(
  120. sa.and_(
  121. Node.hostname.in_(hostnames_to_add),
  122. Node.cluster_id == instance.id,
  123. Node.id.notin_(nodes_to_remove)
  124. )
  125. ).all()]
  126. if duplicated:
  127. raise errors.AlreadyExists(
  128. "Nodes with hostnames [{0}] already exist in cluster {1}."
  129. .format(",".join(duplicated), instance.id)
  130. )
  131. class ClusterAttributesValidator(base.BasicAttributesValidator):
  132. @classmethod
  133. def validate(cls, data, cluster=None, force=False):
  134. d = cls.validate_json(data)
  135. if "generated" in d:
  136. raise errors.InvalidData(
  137. "It is not allowed to update generated attributes",
  138. log_message=True
  139. )
  140. if "editable" in d and not isinstance(d["editable"], dict):
  141. raise errors.InvalidData(
  142. "Editable attributes should be a dictionary",
  143. log_message=True
  144. )
  145. attrs = d
  146. models = None
  147. if cluster is not None:
  148. attrs = objects.Cluster.get_updated_editable_attributes(cluster, d)
  149. cls.validate_provision(cluster, attrs)
  150. cls.validate_allowed_attributes(cluster, d, force)
  151. models = objects.Cluster.get_restrictions_models(
  152. cluster, attrs=attrs.get('editable', {}))
  153. cls.validate_attributes(attrs.get('editable', {}), models, force=force)
  154. return d
  155. @classmethod
  156. def validate_provision(cls, cluster, attrs):
  157. # NOTE(agordeev): disable classic provisioning for 7.0 or higher
  158. if version.StrictVersion(cluster.release.environment_version) >= \
  159. version.StrictVersion(consts.FUEL_IMAGE_BASED_ONLY):
  160. provision_data = attrs['editable'].get('provision')
  161. if provision_data:
  162. if provision_data['method']['value'] != \
  163. consts.PROVISION_METHODS.image:
  164. raise errors.InvalidData(
  165. u"Cannot use classic provisioning for adding "
  166. u"nodes to environment",
  167. log_message=True)
  168. else:
  169. raise errors.InvalidData(
  170. u"Provisioning method is not set. Unable to continue",
  171. log_message=True)
  172. @classmethod
  173. def validate_allowed_attributes(cls, cluster, data, force):
  174. """Validates if attributes are hot pluggable or not.
  175. :param cluster: A cluster instance
  176. :type cluster: nailgun.db.sqlalchemy.models.cluster.Cluster
  177. :param data: Changed attributes of cluster
  178. :type data: dict
  179. :param force: Allow forcefully update cluster attributes
  180. :type force: bool
  181. :raises: errors.NotAllowed
  182. """
  183. # TODO(need to enable restrictions check for cluster attributes[1])
  184. # [1] https://bugs.launchpad.net/fuel/+bug/1519904
  185. # Validates only that plugin can be installed on deployed env.
  186. # If cluster is locked we have to check which attributes
  187. # we want to change and block an entire operation if there
  188. # one with always_editable=False.
  189. if not cluster.is_locked or force:
  190. return
  191. editable_cluster = objects.Cluster.get_editable_attributes(
  192. cluster, all_plugins_versions=True)
  193. editable_request = data.get('editable', {})
  194. for attr_name, attr_request in six.iteritems(editable_request):
  195. attr_cluster = editable_cluster.get(attr_name, {})
  196. meta_cluster = attr_cluster.get('metadata', {})
  197. meta_request = attr_request.get('metadata', {})
  198. if PluginManager.is_plugin_data(attr_cluster):
  199. if meta_request['enabled']:
  200. changed_ids = [meta_request['chosen_id']]
  201. if meta_cluster['enabled']:
  202. changed_ids.append(meta_cluster['chosen_id'])
  203. changed_ids = set(changed_ids)
  204. elif meta_cluster['enabled']:
  205. changed_ids = [meta_cluster['chosen_id']]
  206. else:
  207. continue
  208. for plugin in meta_cluster['versions']:
  209. plugin_id = plugin['metadata']['plugin_id']
  210. always_editable = plugin['metadata']\
  211. .get('always_editable', False)
  212. if plugin_id in changed_ids and not always_editable:
  213. raise errors.NotAllowed(
  214. "Plugin '{0}' version '{1}' couldn't be changed "
  215. "after or during deployment."
  216. .format(attr_name,
  217. plugin['metadata']['plugin_version']),
  218. log_message=True
  219. )
  220. elif not meta_cluster.get('always_editable', False):
  221. raise errors.NotAllowed(
  222. "Environment attribute '{0}' couldn't be changed "
  223. "after or during deployment.".format(attr_name),
  224. log_message=True
  225. )
  226. class ClusterChangesValidator(base.BaseDefferedTaskValidator):
  227. @classmethod
  228. def validate(cls, cluster, graph_type=None):
  229. cls.validate_release(cluster=cluster, graph_type=graph_type)
  230. ProvisionSelectedNodesValidator.validate_provision(None, cluster)
  231. class ClusterStopDeploymentValidator(base.BaseDefferedTaskValidator):
  232. @classmethod
  233. def validate(cls, cluster):
  234. super(ClusterStopDeploymentValidator, cls).validate(cluster)
  235. # NOTE(aroma): the check must regard the case when stop deployment
  236. # is called for cluster that was created before master node upgrade
  237. # to versions >= 8.0 and so having 'deployed_before' flag absent
  238. # in their attributes.
  239. # NOTE(vsharshov): task based deployment (>=9.0) implements
  240. # safe way to stop deployment action, so we can enable
  241. # stop deployment for such cluster without restrictions.
  242. # But it is still need to be disabled for old env < 9.0
  243. # which was already deployed once[1]
  244. # [1]: https://bugs.launchpad.net/fuel/+bug/1529691
  245. generated = cluster.attributes.generated
  246. if generated.get('deployed_before', {}).get('value') and\
  247. not objects.Release.is_lcm_supported(cluster.release):
  248. raise errors.CannotBeStopped('Current deployment process is '
  249. 'running on a pre-deployed cluster '
  250. 'that does not support LCM.')
  251. class VmwareAttributesValidator(base.BasicValidator):
  252. single_schema = cluster_schema.vmware_attributes_schema
  253. @staticmethod
  254. def _get_target_node_id(nova_compute_data):
  255. return nova_compute_data['target_node']['current']['id']
  256. @classmethod
  257. def _validate_updated_attributes(cls, attributes, instance):
  258. """Validate that attributes contains changes only for allowed fields.
  259. :param attributes: new vmware attribute settings for db instance
  260. :param instance: nailgun.db.sqlalchemy.models.VmwareAttributes instance
  261. """
  262. metadata = instance.editable.get('metadata', {})
  263. db_editable_attributes = instance.editable.get('value', {})
  264. new_editable_attributes = attributes.get('editable', {}).get('value')
  265. for attribute_metadata in metadata:
  266. if attribute_metadata.get('type') == 'array':
  267. attribute_name = attribute_metadata['name']
  268. cls._check_attribute(
  269. attribute_metadata,
  270. db_editable_attributes.get(attribute_name),
  271. new_editable_attributes.get(attribute_name)
  272. )
  273. else:
  274. cls._check_attribute(
  275. attribute_metadata,
  276. db_editable_attributes,
  277. new_editable_attributes
  278. )
  279. @classmethod
  280. def _check_attribute(cls, metadata, attributes, new_attributes):
  281. """Check new_attributes is equal with attributes except editable fields
  282. :param metadata: dict describes structure and properties of attributes
  283. :param attributes: attributes which is the basis for comparison
  284. :param new_attributes: attributes with modifications to check
  285. """
  286. if type(attributes) != type(new_attributes):
  287. raise errors.InvalidData(
  288. "Value type of '{0}' attribute couldn't be changed.".
  289. format(metadata.get('label') or metadata.get('name')),
  290. log_message=True
  291. )
  292. # if metadata field contains editable_for_deployed = True, attribute
  293. # and all its childs may be changed too. No need to check it.
  294. if metadata.get('editable_for_deployed'):
  295. return
  296. # no 'fields' in metadata means that attribute has no any childs(leaf)
  297. if 'fields' not in metadata:
  298. if attributes != new_attributes:
  299. raise errors.InvalidData(
  300. "Value of '{0}' attribute couldn't be changed.".
  301. format(metadata.get('label') or metadata.get('name')),
  302. log_message=True
  303. )
  304. return
  305. fields_sort_functions = {
  306. 'availability_zones': lambda x: x['az_name'],
  307. 'nova_computes': lambda x: x['vsphere_cluster']
  308. }
  309. field_name = metadata['name']
  310. if isinstance(attributes, (list, tuple)):
  311. if len(attributes) != len(new_attributes):
  312. raise errors.InvalidData(
  313. "Value of '{0}' attribute couldn't be changed.".
  314. format(metadata.get('label') or metadata.get('name')),
  315. log_message=True
  316. )
  317. attributes = sorted(
  318. attributes, key=fields_sort_functions.get(field_name))
  319. new_attributes = sorted(
  320. new_attributes, key=fields_sort_functions.get(field_name))
  321. for item, new_item in six.moves.zip(attributes, new_attributes):
  322. for field_metadata in metadata['fields']:
  323. cls._check_attribute(field_metadata,
  324. item.get(field_metadata['name']),
  325. new_item.get(field_metadata['name']))
  326. elif isinstance(attributes, dict):
  327. for field_metadata in metadata['fields']:
  328. cls._check_attribute(field_metadata,
  329. attributes.get(field_name),
  330. new_attributes.get(field_name))
  331. @classmethod
  332. def _validate_nova_computes(cls, attributes, instance):
  333. """Validates a 'nova_computes' attributes from vmware_attributes
  334. Raise InvalidData exception if new attributes is not valid.
  335. :param instance: nailgun.db.sqlalchemy.models.VmwareAttributes instance
  336. :param attributes: new attributes for db instance for validation
  337. """
  338. input_nova_computes = objects.VmwareAttributes.get_nova_computes_attrs(
  339. attributes.get('editable'))
  340. cls.check_nova_compute_duplicate_and_empty_values(input_nova_computes)
  341. db_nova_computes = objects.VmwareAttributes.get_nova_computes_attrs(
  342. instance.editable)
  343. if instance.cluster.is_locked:
  344. cls.check_operational_controllers_settings(input_nova_computes,
  345. db_nova_computes)
  346. operational_compute_nodes = objects.Cluster.\
  347. get_operational_vmware_compute_nodes(instance.cluster)
  348. cls.check_operational_node_settings(
  349. input_nova_computes, db_nova_computes, operational_compute_nodes)
  350. @classmethod
  351. def check_nova_compute_duplicate_and_empty_values(cls, attributes):
  352. """Check 'nova_computes' attributes for empty and duplicate values."""
  353. nova_compute_attributes_sets = {
  354. 'vsphere_cluster': set(),
  355. 'service_name': set(),
  356. 'target_node': set()
  357. }
  358. for nova_compute_data in attributes:
  359. for attr, values in six.iteritems(nova_compute_attributes_sets):
  360. if attr == 'target_node':
  361. settings_value = cls._get_target_node_id(nova_compute_data)
  362. if settings_value == 'controllers':
  363. continue
  364. else:
  365. settings_value = nova_compute_data.get(attr)
  366. if not settings_value:
  367. raise errors.InvalidData(
  368. "Empty value for attribute '{0}' is not allowed".
  369. format(attr),
  370. log_message=True
  371. )
  372. if settings_value in values:
  373. raise errors.InvalidData(
  374. "Duplicate value '{0}' for attribute '{1}' is "
  375. "not allowed".format(settings_value, attr),
  376. log_message=True
  377. )
  378. values.add(settings_value)
  379. @classmethod
  380. def check_operational_node_settings(cls, input_nova_computes,
  381. db_nova_computes, operational_nodes):
  382. """Validates a 'nova_computes' attributes for operational compute nodes
  383. Raise InvalidData exception if nova_compute settings will be changed or
  384. deleted for deployed nodes with role 'compute-vmware' that wasn't
  385. marked for deletion
  386. :param input_nova_computes: new nova_compute attributes
  387. :type input_nova_computes: list of dicts
  388. :param db_nova_computes: nova_computes attributes stored in db
  389. :type db_nova_computes: list of dicts
  390. :param operational_nodes: list of operational vmware-compute nodes
  391. :type operational_nodes: list of nailgun.db.sqlalchemy.models.Node
  392. """
  393. input_computes_by_node_name = dict(
  394. (cls._get_target_node_id(nc), nc) for nc in input_nova_computes)
  395. db_computes_by_node_name = dict(
  396. (cls._get_target_node_id(nc), nc) for nc in db_nova_computes)
  397. for node in operational_nodes:
  398. node_hostname = node.hostname
  399. input_nova_compute = input_computes_by_node_name.get(node_hostname)
  400. if not input_nova_compute:
  401. raise errors.InvalidData(
  402. "The following compute-vmware node couldn't be "
  403. "deleted from vSphere cluster: {0}".format(node.name),
  404. log_message=True
  405. )
  406. db_nova_compute = db_computes_by_node_name.get(node_hostname)
  407. for attr, db_value in six.iteritems(db_nova_compute):
  408. if attr != 'target_node' and \
  409. db_value != input_nova_compute.get(attr):
  410. raise errors.InvalidData(
  411. "Parameter '{0}' of nova compute instance with target "
  412. "node '{1}' couldn't be changed".format(
  413. attr, node.name),
  414. log_message=True
  415. )
  416. @classmethod
  417. def check_operational_controllers_settings(cls, input_nova_computes,
  418. db_nova_computes):
  419. """Check deployed nova computes settings with target = controllers.
  420. Raise InvalidData exception if any deployed nova computes clusters with
  421. target 'controllers' were added, removed or modified.
  422. :param input_nova_computes: new nova_compute settings
  423. :type input_nova_computes: list of dicts
  424. :param db_nova_computes: nova_computes settings stored in db
  425. :type db_nova_computes: list of dicts
  426. """
  427. input_computes_by_vsphere_name = dict(
  428. (nc['vsphere_cluster'], nc) for nc in input_nova_computes if
  429. cls._get_target_node_id(nc) == 'controllers'
  430. )
  431. db_clusters_names = set()
  432. for db_nova_compute in db_nova_computes:
  433. target_name = cls._get_target_node_id(db_nova_compute)
  434. if target_name == 'controllers':
  435. vsphere_name = db_nova_compute['vsphere_cluster']
  436. input_nova_compute = \
  437. input_computes_by_vsphere_name.get(vsphere_name)
  438. if not input_nova_compute:
  439. raise errors.InvalidData(
  440. "Nova compute instance with target 'controllers' and "
  441. "vSphere cluster {0} couldn't be deleted from "
  442. "operational environment.".format(vsphere_name),
  443. log_message=True
  444. )
  445. for attr, db_value in six.iteritems(db_nova_compute):
  446. input_value = input_nova_compute.get(attr)
  447. if attr == 'target_node':
  448. db_value = cls._get_target_node_id(db_nova_compute)
  449. input_value = cls._get_target_node_id(
  450. input_nova_compute)
  451. if db_value != input_value:
  452. raise errors.InvalidData(
  453. "Parameter '{0}' of nova compute instance with "
  454. "vSphere cluster name '{1}' couldn't be changed".
  455. format(attr, vsphere_name),
  456. log_message=True
  457. )
  458. db_clusters_names.add(vsphere_name)
  459. input_clusters_names = set(input_computes_by_vsphere_name)
  460. if input_clusters_names - db_clusters_names:
  461. raise errors.InvalidData(
  462. "Nova compute instances with target 'controllers' couldn't be "
  463. "added to operational environment. Check nova compute "
  464. "instances with the following vSphere cluster names: {0}".
  465. format(', '.join(
  466. sorted(input_clusters_names - db_clusters_names))),
  467. log_message=True
  468. )
  469. @classmethod
  470. def validate(cls, data, instance):
  471. d = cls.validate_json(data)
  472. if 'metadata' in d.get('editable'):
  473. db_metadata = instance.editable.get('metadata')
  474. input_metadata = d.get('editable').get('metadata')
  475. if db_metadata != input_metadata:
  476. raise errors.InvalidData(
  477. 'Metadata shouldn\'t change',
  478. log_message=True
  479. )
  480. if instance.cluster.is_locked:
  481. cls._validate_updated_attributes(d, instance)
  482. cls._validate_nova_computes(d, instance)
  483. # TODO(apopovych): write validation processing from
  484. # openstack.yaml for vmware
  485. return d