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.

test_cluster_handler.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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. import mock
  16. from oslo_serialization import jsonutils
  17. from nailgun import consts
  18. from nailgun.db.sqlalchemy.models import Cluster
  19. from nailgun.db.sqlalchemy.models import DeploymentGraph
  20. from nailgun.db.sqlalchemy.models import NetworkGroup
  21. from nailgun.db.sqlalchemy.models import Node
  22. from nailgun import errors
  23. from nailgun.test.base import BaseIntegrationTest
  24. from nailgun.test.base import fake_tasks
  25. from nailgun.test.utils import make_mock_extensions
  26. from nailgun.utils import reverse
  27. class TestHandlers(BaseIntegrationTest):
  28. def delete(self, cluster_id):
  29. return self.app.delete(
  30. reverse('ClusterHandler', kwargs={'obj_id': cluster_id}),
  31. headers=self.default_headers
  32. )
  33. def test_cluster_get(self):
  34. cluster = self.env.create_cluster(api=False)
  35. resp = self.app.get(
  36. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  37. headers=self.default_headers
  38. )
  39. self.assertEqual(200, resp.status_code)
  40. self.assertEqual(cluster.id, resp.json_body['id'])
  41. self.assertEqual(cluster.name, resp.json_body['name'])
  42. self.assertEqual(cluster.release.id, resp.json_body['release_id'])
  43. def test_cluster_creation(self):
  44. release = self.env.create_release(api=False)
  45. yet_another_cluster_name = 'Yet another cluster'
  46. resp = self.app.post(
  47. reverse('ClusterCollectionHandler'),
  48. params=jsonutils.dumps({
  49. 'name': yet_another_cluster_name,
  50. 'release': release.id
  51. }),
  52. headers=self.default_headers
  53. )
  54. self.assertEqual(201, resp.status_code)
  55. self.assertEqual(yet_another_cluster_name, resp.json_body['name'])
  56. self.assertEqual(release.id, resp.json_body['release_id'])
  57. def test_cluster_update(self):
  58. updated_name = u'Updated cluster'
  59. cluster = self.env.create_cluster(api=False)
  60. clusters_before = len(self.db.query(Cluster).all())
  61. resp = self.app.put(
  62. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  63. jsonutils.dumps({'name': updated_name}),
  64. headers=self.default_headers
  65. )
  66. self.db.refresh(cluster)
  67. self.assertEqual(resp.status_code, 200)
  68. clusters = self.db.query(Cluster).filter(
  69. Cluster.name == updated_name
  70. ).all()
  71. self.assertEqual(len(clusters), 1)
  72. self.assertEqual(clusters[0].name, updated_name)
  73. clusters_after = len(self.db.query(Cluster).all())
  74. self.assertEqual(clusters_before, clusters_after)
  75. def test_cluster_update_fails_on_net_provider_change(self):
  76. cluster = self.env.create_cluster(
  77. api=False,
  78. net_provider=consts.CLUSTER_NET_PROVIDERS.nova_network)
  79. resp = self.app.put(
  80. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  81. jsonutils.dumps({'net_provider': 'neutron'}),
  82. headers=self.default_headers,
  83. expect_errors=True
  84. )
  85. self.assertEqual(resp.status_code, 400)
  86. self.assertEqual(
  87. resp.json_body["message"],
  88. "Changing 'net_provider' for environment is prohibited"
  89. )
  90. def test_cluster_node_list_update(self):
  91. node1 = self.env.create_node(api=False, hostname='name1')
  92. cluster = self.env.create_cluster(api=False)
  93. resp = self.app.put(
  94. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  95. jsonutils.dumps({'nodes': [node1.id]}),
  96. headers=self.default_headers,
  97. expect_errors=True
  98. )
  99. self.assertEqual(resp.status_code, 200)
  100. node2 = self.env.create_node(api=False, hostname='name1')
  101. nodes = self.db.query(Node).filter(Node.cluster == cluster).all()
  102. self.assertEqual(1, len(nodes))
  103. self.assertEqual(nodes[0].id, node1.id)
  104. resp = self.app.put(
  105. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  106. jsonutils.dumps({'nodes': [node2.id]}),
  107. headers=self.default_headers
  108. )
  109. self.assertEqual(resp.status_code, 200)
  110. self.assertEqual('node-{0}'.format(node1.id), node1.hostname)
  111. nodes = self.db.query(Node).filter(Node.cluster == cluster)
  112. self.assertEqual(1, nodes.count())
  113. def test_cluster_node_list_update_error(self):
  114. node1 = self.env.create_node(api=False, hostname='name1')
  115. cluster = self.env.create_cluster(api=False)
  116. self.app.put(
  117. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  118. jsonutils.dumps({'nodes': [node1.id]}),
  119. headers=self.default_headers,
  120. expect_errors=True
  121. )
  122. node2 = self.env.create_node(api=False, hostname='name1')
  123. # try to add to cluster one more node with the same hostname
  124. resp = self.app.put(
  125. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  126. jsonutils.dumps({'nodes': [node1.id, node2.id]}),
  127. headers=self.default_headers,
  128. expect_errors=True
  129. )
  130. self.assertEqual(resp.status_code, 409)
  131. def test_empty_cluster_deletion(self):
  132. cluster = self.env.create_cluster(api=True)
  133. resp = self.delete(cluster['id'])
  134. self.assertEqual(resp.status_code, 202)
  135. self.assertEqual(self.db.query(Node).count(), 0)
  136. self.assertEqual(self.db.query(Cluster).count(), 0)
  137. @fake_tasks()
  138. def test_cluster_deletion(self):
  139. cluster = self.env.create(
  140. cluster_kwargs={},
  141. nodes_kwargs=[
  142. {"pending_addition": True},
  143. {"status": "ready"}])
  144. graphs_before_deletion = self.db.query(DeploymentGraph).count()
  145. cluster_id = self.env.clusters[0].id
  146. resp = self.delete(cluster_id)
  147. self.assertEqual(resp.status_code, 202)
  148. self.assertIsNone(self.db.query(Cluster).get(cluster.id))
  149. graphs_after_deletion = self.db.query(DeploymentGraph).count()
  150. self.assertEqual(1, graphs_before_deletion - graphs_after_deletion)
  151. # Nodes should be in discover status
  152. self.assertEqual(self.db.query(Node).count(), 2)
  153. for node in self.db.query(Node):
  154. self.assertEqual(node.status, 'discover')
  155. self.assertIsNone(node.cluster_id)
  156. self.assertIsNone(node.group_id)
  157. self.assertEqual(node.roles, [])
  158. self.assertFalse(node.pending_deletion)
  159. self.assertFalse(node.pending_addition)
  160. @fake_tasks(recover_offline_nodes=False)
  161. def test_cluster_deletion_with_offline_nodes(self):
  162. cluster = self.env.create(
  163. cluster_kwargs={},
  164. nodes_kwargs=[
  165. {'pending_addition': True},
  166. {'online': False, 'status': 'ready'}])
  167. resp = self.delete(cluster.id)
  168. self.assertEqual(resp.status_code, 202)
  169. self.assertIsNone(self.db.query(Cluster).get(cluster.id))
  170. self.assertEqual(self.db.query(Node).count(), 1)
  171. node = self.db.query(Node).first()
  172. self.assertEqual(node.status, 'discover')
  173. self.assertIsNone(node.cluster_id)
  174. def test_cluster_deletion_delete_networks(self):
  175. cluster = self.env.create_cluster(api=True)
  176. cluster_db = self.db.query(Cluster).get(cluster['id'])
  177. ngroups = [n.id for n in cluster_db.network_groups]
  178. self.db.delete(cluster_db)
  179. self.db.commit()
  180. ngs = self.db.query(NetworkGroup).filter(
  181. NetworkGroup.id.in_(ngroups)
  182. ).all()
  183. self.assertEqual(ngs, [])
  184. def test_cluster_generated_data_handler(self):
  185. cluster = self.env.create(
  186. nodes_kwargs=[
  187. {'pending_addition': True},
  188. {'online': False, 'status': 'ready'}])
  189. get_resp = self.app.get(
  190. reverse('ClusterGeneratedData',
  191. kwargs={'cluster_id': cluster.id}),
  192. headers=self.default_headers
  193. )
  194. self.assertEqual(get_resp.status_code, 200)
  195. self.datadiff(get_resp.json_body, cluster.attributes.generated)
  196. def test_cluster_name_length(self):
  197. long_name = u'ю' * 2048
  198. cluster = self.env.create_cluster(api=False)
  199. resp = self.app.put(
  200. reverse('ClusterHandler', kwargs={'obj_id': cluster.id}),
  201. jsonutils.dumps({'name': long_name}),
  202. headers=self.default_headers
  203. )
  204. self.assertEqual(resp.status_code, 200)
  205. self.db.refresh(cluster)
  206. self.assertEqual(long_name, cluster.name)
  207. class TestClusterModes(BaseIntegrationTest):
  208. def test_fail_to_create_cluster_with_multinode_mode(self):
  209. release = self.env.create_release(
  210. version='2015-7.0',
  211. modes=[consts.CLUSTER_MODES.ha_compact],
  212. )
  213. cluster_data = {
  214. 'name': 'CrazyFrog',
  215. 'release_id': release.id,
  216. 'mode': consts.CLUSTER_MODES.multinode,
  217. }
  218. resp = self.app.post(
  219. reverse('ClusterCollectionHandler'),
  220. jsonutils.dumps(cluster_data),
  221. headers=self.default_headers,
  222. expect_errors=True
  223. )
  224. self.check_wrong_response(resp)
  225. def check_wrong_response(self, resp):
  226. self.assertEqual(resp.status_code, 400)
  227. self.assertIn(
  228. 'Cannot deploy in multinode mode in current release. '
  229. 'Need to be one of',
  230. resp.json_body['message']
  231. )
  232. def test_update_cluster_to_wrong_mode(self):
  233. update_resp = self._try_cluster_update(
  234. name='SadCrazyFrog',
  235. mode=consts.CLUSTER_MODES.multinode,
  236. )
  237. self.check_wrong_response(update_resp)
  238. def test_update_cluster_but_not_mode(self):
  239. update_resp = self._try_cluster_update(
  240. name='HappyCrazyFrog',
  241. )
  242. self.assertEqual(update_resp.status_code, 200)
  243. def _try_cluster_update(self, **attrs_to_update):
  244. release = self.env.create_release(
  245. version='2015-7.0',
  246. modes=[consts.CLUSTER_MODES.ha_compact],
  247. )
  248. create_resp = self.env.create_cluster(
  249. release_id=release.id,
  250. mode=consts.CLUSTER_MODES.ha_compact,
  251. api=True,
  252. )
  253. cluster_id = create_resp['id']
  254. return self.app.put(
  255. reverse('ClusterHandler', kwargs={'obj_id': cluster_id}),
  256. jsonutils.dumps(attrs_to_update),
  257. headers=self.default_headers,
  258. expect_errors=True
  259. )
  260. class TestClusterComponents(BaseIntegrationTest):
  261. def setUp(self):
  262. super(TestClusterComponents, self).setUp()
  263. self.release = self.env.create_release(
  264. version='2015.1-8.0',
  265. operating_system='Ubuntu',
  266. modes=[consts.CLUSTER_MODES.ha_compact],
  267. components_metadata=[
  268. {
  269. 'name': 'hypervisor:test_hypervisor'
  270. },
  271. {
  272. 'name': 'network:core:test_network_1',
  273. 'incompatible': [
  274. {'name': 'hypervisor:test_hypervisor'}
  275. ]
  276. },
  277. {
  278. 'name': 'network:core:test_network_2'
  279. },
  280. {
  281. 'name': 'storage:test_storage',
  282. 'compatible': [
  283. {'name': 'hypervisors:test_hypervisor'}
  284. ],
  285. 'requires': [
  286. {'name': 'hypervisors:test_hypervisor'}
  287. ]
  288. }
  289. ])
  290. self.cluster_data = {
  291. 'name': 'TestCluster',
  292. 'release_id': self.release.id,
  293. 'mode': consts.CLUSTER_MODES.ha_compact
  294. }
  295. def test_component_validation_failed(self):
  296. error_msg = "Component validation error"
  297. self.cluster_data.update(
  298. {'components': ['hypervisor:test_hypervisor']})
  299. with mock.patch('nailgun.utils.restrictions.ComponentsRestrictions.'
  300. 'validate_components') as validate_mock:
  301. validate_mock.side_effect = errors.InvalidData(error_msg)
  302. resp = self._create_cluster_with_expected_errors(self.cluster_data)
  303. self.assertEqual(resp.status_code, 400)
  304. self.assertEqual(error_msg, resp.json_body['message'])
  305. def test_components_not_in_release(self):
  306. self.cluster_data.update(
  307. {'components': ['storage:not_existing_component']})
  308. resp = self._create_cluster_with_expected_errors(self.cluster_data)
  309. self.assertEqual(resp.status_code, 400)
  310. self.assertEqual(
  311. u"[u'storage:not_existing_component'] components are not "
  312. "related to used release.",
  313. resp.json_body['message']
  314. )
  315. def test_incompatible_components_found(self):
  316. self.cluster_data.update(
  317. {'components': [
  318. 'hypervisor:test_hypervisor',
  319. 'network:core:test_network_1']})
  320. resp = self._create_cluster_with_expected_errors(self.cluster_data)
  321. self.assertEqual(resp.status_code, 400)
  322. self.assertEqual(
  323. u"Incompatible components were found: "
  324. "'hypervisor:test_hypervisor' incompatible with "
  325. "[u'network:core:test_network_1'].",
  326. resp.json_body['message']
  327. )
  328. def test_requires_components_not_found(self):
  329. self.cluster_data.update(
  330. {'components': ['storage:test_storage']})
  331. resp = self._create_cluster_with_expected_errors(self.cluster_data)
  332. self.assertEqual(resp.status_code, 400)
  333. self.assertEqual(
  334. u"Component 'storage:test_storage' requires any of components "
  335. "from [u'hypervisors:test_hypervisor'] set.",
  336. resp.json_body['message']
  337. )
  338. def _create_cluster_with_expected_errors(self, cluster_data):
  339. return self.app.post(
  340. reverse('ClusterCollectionHandler'),
  341. jsonutils.dumps(cluster_data),
  342. headers=self.default_headers,
  343. expect_errors=True
  344. )
  345. class TestClusterExtension(BaseIntegrationTest):
  346. def setUp(self):
  347. super(TestClusterExtension, self).setUp()
  348. self.env.create_cluster()
  349. self.cluster = self.env.clusters[0]
  350. def test_get_enabled_extensions(self):
  351. enabled_extensions = 'volume_manager', 'bareon'
  352. self.cluster.extensions = enabled_extensions
  353. self.db.commit()
  354. resp = self.app.get(
  355. reverse(
  356. 'ClusterExtensionsHandler',
  357. kwargs={'cluster_id': self.cluster.id}),
  358. headers=self.default_headers,
  359. )
  360. self.assertEqual(resp.status_code, 200)
  361. self.assertItemsEqual(resp.json_body, enabled_extensions)
  362. def test_enabling_duplicated_extensions(self):
  363. extensions = 'bareon', 'volume_manager'
  364. requested_extensions = 2 * extensions
  365. with mock.patch(
  366. 'nailgun.api.v1.validators.extension.get_all_extensions',
  367. return_value=make_mock_extensions(extensions)):
  368. resp = self.app.put(
  369. reverse(
  370. 'ClusterExtensionsHandler',
  371. kwargs={'cluster_id': self.cluster.id}),
  372. jsonutils.dumps(requested_extensions),
  373. headers=self.default_headers,
  374. )
  375. self.assertEqual(resp.status_code, 200)
  376. self.db.refresh(self.cluster)
  377. for ext in extensions:
  378. self.assertIn(ext, self.cluster.extensions)
  379. def test_enabling_extensions(self):
  380. extensions = 'bareon', 'volume_manager'
  381. with mock.patch(
  382. 'nailgun.api.v1.validators.extension.get_all_extensions',
  383. return_value=make_mock_extensions(extensions)):
  384. resp = self.app.put(
  385. reverse(
  386. 'ClusterExtensionsHandler',
  387. kwargs={'cluster_id': self.cluster.id}),
  388. jsonutils.dumps(extensions),
  389. headers=self.default_headers,
  390. )
  391. self.assertEqual(resp.status_code, 200)
  392. self.db.refresh(self.cluster)
  393. for ext in extensions:
  394. self.assertIn(ext, self.cluster.extensions)
  395. def test_enabling_invalid_extensions(self):
  396. existed_extensions = 'bareon', 'volume_manager'
  397. requested_extensions = 'network_manager', 'volume_manager'
  398. with mock.patch(
  399. 'nailgun.api.v1.validators.extension.get_all_extensions',
  400. return_value=make_mock_extensions(existed_extensions)):
  401. resp = self.app.put(
  402. reverse(
  403. 'ClusterExtensionsHandler',
  404. kwargs={'cluster_id': self.cluster.id}),
  405. jsonutils.dumps(requested_extensions),
  406. headers=self.default_headers,
  407. expect_errors=True,
  408. )
  409. self.assertEqual(resp.status_code, 400)
  410. self.assertIn(u"No such extensions:", resp.json_body['message'])
  411. self.assertIn(requested_extensions[0], resp.json_body['message'])
  412. self.assertNotIn(requested_extensions[1], resp.json_body['message'])
  413. def test_disabling_invalid_extensions(self):
  414. requested_extensions = 'network_manager', 'bareon'
  415. self.cluster.extensions = 'volume_manager', 'bareon'
  416. self.db.commit()
  417. url = reverse('ClusterExtensionsHandler',
  418. kwargs={'cluster_id': self.cluster.id})
  419. query_str = 'extension_names={0}'.format(
  420. ','.join(requested_extensions))
  421. resp = self.app.delete(
  422. '{0}?{1}'.format(url, query_str),
  423. headers=self.default_headers,
  424. expect_errors=True,
  425. )
  426. self.assertEqual(resp.status_code, 400)
  427. self.assertIn(u"No such extensions to disable:",
  428. resp.json_body['message'])
  429. self.assertIn(requested_extensions[0], resp.json_body['message'])
  430. self.assertNotIn(requested_extensions[1], resp.json_body['message'])
  431. def test_disabling_extensions(self):
  432. existed_extensions = 'network_manager', 'volume_manager', 'bareon'
  433. requested_extensions = 'network_manager', 'bareon'
  434. self.cluster.extensions = existed_extensions
  435. self.db.commit()
  436. url = reverse('ClusterExtensionsHandler',
  437. kwargs={'cluster_id': self.cluster.id})
  438. query_str = 'extension_names={0}'.format(
  439. ','.join(requested_extensions))
  440. self.app.delete(
  441. '{0}?{1}'.format(url, query_str),
  442. headers=self.default_headers,
  443. )
  444. self.db.refresh(self.cluster)
  445. for ext in requested_extensions:
  446. self.assertNotIn(ext, self.cluster.extensions)
  447. def test_disabling_dublicated_extensions(self):
  448. existed_extensions = 'network_manager', 'volume_manager', 'bareon'
  449. requested_extensions = 'network_manager', 'bareon'
  450. self.cluster.extensions = existed_extensions
  451. self.db.commit()
  452. url = reverse('ClusterExtensionsHandler',
  453. kwargs={'cluster_id': self.cluster.id})
  454. query_str = 'extension_names={0}'.format(
  455. ','.join(2 * requested_extensions))
  456. self.app.delete(
  457. '{0}?{1}'.format(url, query_str),
  458. headers=self.default_headers,
  459. )
  460. self.db.refresh(self.cluster)
  461. for ext in requested_extensions:
  462. self.assertNotIn(ext, self.cluster.extensions)