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_restriction.py 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 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 six
  16. import yaml
  17. from nailgun import errors
  18. from nailgun import objects
  19. from nailgun.test import base
  20. from nailgun.utils.restrictions import AttributesRestriction
  21. from nailgun.utils.restrictions import ComponentsRestrictions
  22. from nailgun.utils.restrictions import LimitsMixin
  23. from nailgun.utils.restrictions import RestrictionBase
  24. DATA = """
  25. attributes:
  26. group:
  27. attribute_1:
  28. name: attribute_1
  29. value: true
  30. restrictions:
  31. - condition: 'settings:group.attribute_2.value == true'
  32. message: 'Only one of attributes 1 and 2 allowed'
  33. - condition: 'settings:group.attribute_3.value == "spam"'
  34. message: 'Only one of attributes 1 and 3 allowed'
  35. attribute_2:
  36. name: attribute_2
  37. value: true
  38. attribute_3:
  39. name: attribute_3
  40. value: spam
  41. restrictions:
  42. - condition: 'settings:group.attribute_3.value ==
  43. settings:group.attribute_4.value'
  44. message: 'Only one of attributes 3 and 4 allowed'
  45. action: enable
  46. attribute_4:
  47. name: attribute_4
  48. value: spam
  49. attribute_5:
  50. name: attribute_5
  51. value: 4
  52. roles_meta:
  53. cinder:
  54. limits:
  55. min: 1
  56. overrides:
  57. - condition: 'settings:group.attribute_2.value == true'
  58. message: 'At most one role_1 node can be added'
  59. max: 1
  60. controller:
  61. limits:
  62. recommended: 'settings:group.attribute_5.value'
  63. mongo:
  64. limits:
  65. max: 12
  66. message: 'At most 12 MongoDB node should be added'
  67. overrides:
  68. - condition: 'settings:group.attribute_3.value == "spam"'
  69. min: 4
  70. message: 'At least 4 MongoDB node can be added if spam'
  71. - condition: 'settings:group.attribute_3.value == "egg"'
  72. recommended: 3
  73. message: "At least 3 MongoDB nodes are recommended"
  74. """
  75. class TestRestriction(base.BaseTestCase):
  76. def setUp(self):
  77. super(TestRestriction, self).setUp()
  78. self.data = yaml.load(DATA)
  79. def test_check_restrictions(self):
  80. attributes = self.data.get('attributes')
  81. for gkey, gvalue in six.iteritems(attributes):
  82. for key, value in six.iteritems(gvalue):
  83. result = RestrictionBase.check_restrictions(
  84. models={'settings': attributes},
  85. restrictions=value.get('restrictions', []))
  86. # check when couple restrictions true for some item
  87. if key == 'attribute_1':
  88. self.assertTrue(result.get('result'))
  89. self.assertEqual(
  90. result.get('message'),
  91. 'Only one of attributes 1 and 2 allowed. ' +
  92. 'Only one of attributes 1 and 3 allowed')
  93. # check when different values uses in restriction
  94. if key == 'attribute_3':
  95. self.assertTrue(result.get('result'))
  96. self.assertEqual(
  97. result.get('message'),
  98. 'Only one of attributes 3 and 4 allowed')
  99. def test_expand_restriction_format(self):
  100. string_restriction = 'settings.some_attribute.value != true'
  101. dict_restriction_long_format = {
  102. 'condition': 'settings.some_attribute.value != true',
  103. 'message': 'Another attribute required'
  104. }
  105. dict_restriction_short_format = {
  106. 'settings.some_attribute.value != true':
  107. 'Another attribute required'
  108. }
  109. result = {
  110. 'action': 'disable',
  111. 'condition': 'settings.some_attribute.value != true',
  112. }
  113. invalid_format = ['some_condition']
  114. # check string format
  115. self.assertDictEqual(
  116. RestrictionBase._expand_restriction(
  117. string_restriction), result)
  118. result['message'] = 'Another attribute required'
  119. # check long format
  120. self.assertDictEqual(
  121. RestrictionBase._expand_restriction(
  122. dict_restriction_long_format), result)
  123. # check short format
  124. self.assertDictEqual(
  125. RestrictionBase._expand_restriction(
  126. dict_restriction_short_format), result)
  127. # check invalid format
  128. self.assertRaises(
  129. errors.InvalidData,
  130. RestrictionBase._expand_restriction,
  131. invalid_format)
  132. class TestLimits(base.BaseTestCase):
  133. def setUp(self):
  134. super(TestLimits, self).setUp()
  135. self.data = yaml.load(DATA)
  136. self.env.create(
  137. nodes_kwargs=[
  138. {"status": "ready", "roles": ["cinder"]},
  139. {"status": "ready", "roles": ["controller"]},
  140. {"status": "ready", "roles": ["mongo"]},
  141. {"status": "ready", "roles": ["mongo"]},
  142. ]
  143. )
  144. def test_check_node_limits(self):
  145. roles = self.data.get('roles_meta')
  146. attributes = self.data.get('attributes')
  147. for role, data in six.iteritems(roles):
  148. result = LimitsMixin().check_node_limits(
  149. models={'settings': attributes},
  150. nodes=self.env.nodes,
  151. role=role,
  152. limits=data.get('limits'))
  153. if role == 'cinder':
  154. self.assertTrue(result.get('valid'))
  155. if role == 'controller':
  156. self.assertFalse(result.get('valid'))
  157. self.assertEqual(
  158. result.get('messages'),
  159. 'Default message')
  160. if role == 'mongo':
  161. self.assertFalse(result.get('valid'))
  162. self.assertEqual(
  163. result.get('messages'),
  164. 'At least 4 MongoDB node can be added if spam')
  165. def test_check_override(self):
  166. roles = self.data.get('roles_meta')
  167. attributes = self.data.get('attributes')
  168. limits = LimitsMixin()
  169. # Set nodes count to 4
  170. limits.count = 4
  171. limits.limit_reached = True
  172. limits.models = {'settings': attributes}
  173. limits.nodes = self.env.nodes
  174. # Set "cinder" role to working on
  175. limits.role = 'cinder'
  176. limits.limit_types = ['max']
  177. limits.checked_limit_types = {}
  178. limits.limit_values = {'max': None}
  179. override_data = roles['cinder']['limits']['overrides'][0]
  180. result = limits._check_override(override_data)
  181. self.assertEqual(
  182. result[0]['message'], 'At most one role_1 node can be added')
  183. def test_get_proper_message(self):
  184. limits = LimitsMixin()
  185. limits.messages = [
  186. {'type': 'min', 'value': '1', 'message': 'Message for min_1'},
  187. {'type': 'min', 'value': '2', 'message': 'Message for min_2'},
  188. {'type': 'max', 'value': '5', 'message': 'Message for max_5'},
  189. {'type': 'max', 'value': '8', 'message': 'Message for max_8'}
  190. ]
  191. self.assertEqual(
  192. limits._get_message('min'), 'Message for min_2')
  193. self.assertEqual(
  194. limits._get_message('max'), 'Message for max_5')
  195. class TestAttributesRestriction(base.BaseTestCase):
  196. def setUp(self):
  197. super(TestAttributesRestriction, self).setUp()
  198. self.cluster = self.env.create(
  199. cluster_kwargs={
  200. 'api': False
  201. }
  202. )
  203. attributes_metadata = """
  204. editable:
  205. access:
  206. user:
  207. value: ""
  208. type: "text"
  209. regex:
  210. source: '\S'
  211. error: "Invalid username"
  212. email:
  213. value: "admin@localhost"
  214. type: "text"
  215. regex:
  216. source: '\S'
  217. error: "Invalid email"
  218. tenant:
  219. value: [""]
  220. type: "text_list"
  221. regex:
  222. source: '\S'
  223. error: "Invalid tenant name"
  224. another_tenant:
  225. value: ["test"]
  226. type: "text_list"
  227. min: 2
  228. max: 2
  229. regex:
  230. source: '\S'
  231. error: "Invalid tenant name"
  232. another_tenant_2:
  233. value: ["test1", "test2", "test3"]
  234. type: "text_list"
  235. min: 2
  236. max: 2
  237. regex:
  238. source: '\S'
  239. error: "Invalid tenant name"
  240. password:
  241. value: "secret"
  242. type: "password"
  243. regex:
  244. source: '\S'
  245. error: "Empty password"
  246. nullable_text:
  247. label: "Nullable text"
  248. value: null
  249. nullable: True
  250. type: "text"
  251. regex:
  252. source: '\S'
  253. error: "Empty value"
  254. not_nullable_text:
  255. label: "Not nullable text"
  256. value: null
  257. type: "text"
  258. nullable_number:
  259. label: "Nullable number"
  260. value: null
  261. nullable: True
  262. type: "number"
  263. not_nullable_number:
  264. label: "Not nullable number"
  265. value: null
  266. type: "number"
  267. """
  268. self.attributes_data = yaml.load(attributes_metadata)
  269. def test_check_with_invalid_values(self):
  270. objects.Cluster.update_attributes(
  271. self.cluster, self.attributes_data)
  272. attributes = objects.Cluster.get_editable_attributes(self.cluster)
  273. models = {
  274. 'settings': attributes,
  275. 'default': attributes,
  276. }
  277. errs = AttributesRestriction.check_data(models, attributes)
  278. self.assertItemsEqual(
  279. errs, ['Invalid username', ['Invalid tenant name'],
  280. "Value ['test'] should have at least 2 items",
  281. "Value ['test1', 'test2', 'test3'] "
  282. "should not have more than 2 items",
  283. "Null value is forbidden for 'Not nullable text'",
  284. "Null value is forbidden for 'Not nullable number'"])
  285. def test_check_with_valid_values(self):
  286. access = self.attributes_data['editable']['access']
  287. access['user']['value'] = 'admin'
  288. access['tenant']['value'] = ['test']
  289. access['another_tenant']['value'] = ['test1', 'test2']
  290. access['another_tenant_2']['value'] = ['test1', 'test2']
  291. access['not_nullable_text']['value'] = 'test'
  292. access['not_nullable_number']['value'] = 123
  293. objects.Cluster.update_attributes(
  294. self.cluster, self.attributes_data)
  295. attributes = objects.Cluster.get_editable_attributes(self.cluster)
  296. models = {
  297. 'settings': attributes,
  298. 'default': attributes,
  299. }
  300. errs = AttributesRestriction.check_data(models, attributes)
  301. self.assertListEqual(errs, [])
  302. class TestComponentsRestrictions(base.BaseTestCase):
  303. def setUp(self):
  304. super(TestComponentsRestrictions, self).setUp()
  305. self.required_components_types = ['hypervisor', 'network', 'storage']
  306. self.components_metadata = [
  307. {
  308. 'name': 'hypervisor:test_hypervisor'
  309. },
  310. {
  311. 'name': 'network:core:test_network_1',
  312. 'incompatible': [
  313. {'name': 'hypervisor:test_hypervisor'}
  314. ]
  315. },
  316. {
  317. 'name': 'network:core:test_network_2'
  318. },
  319. {
  320. 'name': 'network:ml2:test_network_3'
  321. },
  322. {
  323. 'name': 'storage:test_storage',
  324. 'compatible': [
  325. {'name': 'hypervisor:test_hypervisor'}
  326. ],
  327. 'requires': [
  328. {'name': 'hypervisor:test_hypervisor'}
  329. ]
  330. },
  331. {
  332. 'name': 'storage:test_storage_2'
  333. }
  334. ]
  335. def test_components_not_in_available_components(self):
  336. self._validate_with_expected_errors(
  337. ['storage:not_existing_component'],
  338. "['storage:not_existing_component'] components are not related to "
  339. "used release."
  340. )
  341. def test_not_all_required_types_components(self):
  342. selected_components_list = [
  343. 'hypervisor:test_hypervisor',
  344. 'network:core:test_network_2',
  345. 'storage:test_storage_2'
  346. ]
  347. ComponentsRestrictions.validate_components(
  348. selected_components_list, self.components_metadata,
  349. self.required_components_types)
  350. while selected_components_list:
  351. selected_components_list.pop()
  352. self._validate_with_expected_errors(
  353. selected_components_list,
  354. "Components with {0} types are required but wasn't found "
  355. "in data.".format(sorted(
  356. set(self.required_components_types) - set(
  357. [x.split(':')[0] for x in selected_components_list])
  358. ))
  359. )
  360. def test_incompatible_components_found(self):
  361. self._validate_with_expected_errors(
  362. ['hypervisor:test_hypervisor', 'network:core:test_network_1'],
  363. "Incompatible components were found: 'network:core:test_network_1'"
  364. " incompatible with ['hypervisor:test_hypervisor']."
  365. )
  366. def test_requires_components_not_found(self):
  367. self._validate_with_expected_errors(
  368. ['storage:test_storage'],
  369. "Component 'storage:test_storage' requires any of components from "
  370. "['hypervisor:test_hypervisor'] set."
  371. )
  372. def test_requires_mixed_format(self):
  373. self.components_metadata.append({
  374. 'name': 'storage:wrong_storage',
  375. 'requires': [
  376. {'any_of': {
  377. 'items': ['network:core:*']
  378. }},
  379. {'name': 'hypervisor:test_hypervisor'}
  380. ]
  381. })
  382. self._validate_with_expected_errors(
  383. ['storage:wrong_storage'],
  384. "Component 'storage:wrong_storage' has mixed format of requires."
  385. )
  386. def test_requires_any_of_predicate(self):
  387. self.components_metadata.append({
  388. 'name': 'additional_service:test_service',
  389. 'requires': [
  390. {'any_of': {
  391. 'items': ['network:core:*']
  392. }},
  393. {'any_of': {
  394. 'items': [
  395. 'storage:test_storage_2', 'hypervisor:test_hypervisor'
  396. ],
  397. }}
  398. ]
  399. })
  400. self._validate_with_expected_errors(
  401. ['additional_service:test_service', 'network:ml2:test_network_3'],
  402. "Requirements was not satisfied for component "
  403. "'additional_service:test_service': any_of(['network:core:*'])"
  404. )
  405. self._validate_with_expected_errors(
  406. ['additional_service:test_service', 'network:core:test_network_2'],
  407. "Requirements was not satisfied for component "
  408. "'additional_service:test_service': "
  409. "any_of(['hypervisor:test_hypervisor', 'storage:test_storage_2'])"
  410. )
  411. ComponentsRestrictions.validate_components(
  412. ['additional_service:test_service', 'network:core:test_network_2',
  413. 'hypervisor:test_hypervisor', 'storage:test_storage_2'],
  414. self.components_metadata,
  415. self.required_components_types
  416. )
  417. def test_requires_one_of_predicate(self):
  418. self.components_metadata.append({
  419. 'name': 'additional_service:test_service',
  420. 'requires': [
  421. {'one_of': {
  422. 'items': ['network:core:*']
  423. }},
  424. {'one_of': {
  425. 'items': [
  426. 'storage:test_storage_2', 'hypervisor:test_hypervisor'
  427. ]
  428. }}
  429. ]
  430. })
  431. selected_components_list = ['additional_service:test_service',
  432. 'network:core:test_network_1',
  433. 'network:core:test_network_2',
  434. 'storage:test_storage_2']
  435. self._validate_with_expected_errors(
  436. selected_components_list,
  437. "Requirements was not satisfied for component "
  438. "'additional_service:test_service': one_of(['network:core:*'])"
  439. )
  440. self._validate_with_expected_errors(
  441. ['additional_service:test_service', 'network:core:test_network_1'],
  442. "Requirements was not satisfied for component "
  443. "'additional_service:test_service': "
  444. "one_of(['hypervisor:test_hypervisor', 'storage:test_storage_2'])"
  445. )
  446. ComponentsRestrictions.validate_components(
  447. ['additional_service:test_service', 'network:core:test_network_2',
  448. 'hypervisor:test_hypervisor', 'storage:test_storage'],
  449. self.components_metadata,
  450. self.required_components_types
  451. )
  452. def test_requires_none_of_predicate(self):
  453. self.components_metadata.append({
  454. 'name': 'additional_service:test_service',
  455. 'requires': [{
  456. 'none_of': {
  457. 'items': ['network:core:*', 'storage:test_storage']
  458. }
  459. }]
  460. })
  461. selected_components_list = ['additional_service:test_service',
  462. 'network:core:test_network_1']
  463. self._validate_with_expected_errors(
  464. selected_components_list,
  465. "Requirements was not satisfied for component "
  466. "'additional_service:test_service': "
  467. "none_of(['network:core:*', 'storage:test_storage'])"
  468. )
  469. ComponentsRestrictions.validate_components(
  470. ['additional_service:test_service', 'network:ml2:test_network_3',
  471. 'storage:test_storage_2', 'hypervisor:test_hypervisor'],
  472. self.components_metadata,
  473. self.required_components_types
  474. )
  475. def test_requires_all_of_predicate(self):
  476. self.components_metadata.append({
  477. 'name': 'additional_service:test_service',
  478. 'requires': [{
  479. 'all_of': {
  480. 'items': [
  481. 'network:core:test_network_2',
  482. 'storage:*',
  483. 'hypervisor:test_hypervisor'
  484. ]
  485. }
  486. }]
  487. })
  488. selected_components_list = ['additional_service:test_service',
  489. 'network:core:test_network_2',
  490. 'storage:test_storage_2',
  491. 'hypervisor:test_hypervisor']
  492. self._validate_with_expected_errors(
  493. selected_components_list,
  494. "Requirements was not satisfied for component "
  495. "'additional_service:test_service': all_of(["
  496. "'hypervisor:test_hypervisor', 'network:core:test_network_2', "
  497. "'storage:*'])"
  498. )
  499. selected_components_list.append('storage:test_storage')
  500. ComponentsRestrictions.validate_components(
  501. selected_components_list, self.components_metadata,
  502. self.required_components_types)
  503. def _validate_with_expected_errors(self, components_list, error_msg):
  504. with self.assertRaises(errors.InvalidData) as exc_cm:
  505. ComponentsRestrictions.validate_components(
  506. components_list, self.components_metadata,
  507. self.required_components_types)
  508. self.assertEqual(exc_cm.exception.message, error_msg)