Browse Source

Lift too strict restrictions on cross-deps role name

  Allow a user to specify any arbitrary string
  as role name for cross-deps that could be
  a regexp or a TASK_ROLE_PATTERN string

  Log a warning when task is not assigned
  to roles/groups/fields

  Set default logging level to INFO

Change-Id: I42c2490cf22f53892a189165698d1acd56ee4c74
Closes-bug: #1557997
tags/4.1.0
Vladimir Kuklin 3 years ago
parent
commit
ca8240ff61

+ 4
- 0
fuel_plugin_builder/errors.py View File

@@ -33,6 +33,10 @@ class ValidationError(FuelPluginException):
33 33
     pass
34 34
 
35 35
 
36
+class TaskFieldError(ValidationError):
37
+    pass
38
+
39
+
36 40
 class FileIsEmpty(ValidationError):
37 41
     def __init__(self, file_path):
38 42
         super(FileIsEmpty, self).__init__(

+ 1
- 1
fuel_plugin_builder/logger.py View File

@@ -20,7 +20,7 @@ import logging
20 20
 def configure_logger(debug=False):
21 21
     logger = logging.getLogger('fuel_plugin_builder')
22 22
 
23
-    logger.setLevel(logging.CRITICAL)
23
+    logger.setLevel(logging.INFO)
24 24
     if debug:
25 25
         logger.setLevel(logging.DEBUG)
26 26
 

+ 3
- 2
fuel_plugin_builder/tests/test_base_validator.py View File

@@ -13,7 +13,7 @@
13 13
 #    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14 14
 #    License for the specific language governing permissions and limitations
15 15
 #    under the License.
16
-
16
+import jsonschema
17 17
 import mock
18 18
 
19 19
 from fuel_plugin_builder import errors
@@ -37,6 +37,7 @@ class TestBaseValidator(BaseTestCase):
37 37
         self.validator = NewValidator(self.plugin_path)
38 38
         self.data = {'data': 'data1'}
39 39
         self.schema = self.make_schema(['data'], {'data': {'type': 'string'}})
40
+        self.format_checker = jsonschema.FormatChecker
40 41
 
41 42
     @classmethod
42 43
     def make_schema(cls, required, properties):
@@ -54,7 +55,7 @@ class TestBaseValidator(BaseTestCase):
54 55
             'file_path')
55 56
         schema_mock.validate.assert_called_once_with(
56 57
             self.data,
57
-            self.schema)
58
+            self.schema, format_checker=self.format_checker)
58 59
 
59 60
     def test_validate_schema_raises_error(self):
60 61
         schema = self.make_schema(['key'], {'key': {'type': 'string'}})

+ 82
- 14
fuel_plugin_builder/tests/test_validator_v4.py View File

@@ -290,7 +290,7 @@ class TestValidatorV4(TestValidatorV3):
290 290
         mock_data = [{
291 291
             'id': 'plugin_task',
292 292
             'type': 'puppet',
293
-            'group': ['controller'],
293
+            'groups': ['controller'],
294 294
             'reexecute_on': ['bla']}]
295 295
         err_msg = "File '/tmp/plugin_path/deployment_tasks.yaml', " \
296 296
                   "'bla' is not one of"
@@ -299,8 +299,9 @@ class TestValidatorV4(TestValidatorV3):
299 299
             err_msg, self.validator.check_deployment_tasks_schema)
300 300
 
301 301
     @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
302
+    @mock.patch('fuel_plugin_builder.validators.validator_v4.logger')
302 303
     def test_role_attribute_is_required_for_deployment_task_types(
303
-            self, utils_mock, *args):
304
+            self, logger_mock, utils_mock, *args):
304 305
         deployment_tasks_data = [
305 306
             {
306 307
                 'id': 'plugin_name',
@@ -332,31 +333,98 @@ class TestValidatorV4(TestValidatorV3):
332 333
             }
333 334
         ]
334 335
 
335
-        err_msg = "File '/tmp/plugin_path/deployment_tasks.yaml', " \
336
-                  "'role' is a required property, value path '0'"
337 336
         for task in deployment_tasks_data:
338
-            self.check_raised_exception(
339
-                utils_mock, [task],
340
-                err_msg, self.validator.check_deployment_tasks)
337
+            utils_mock.parse_yaml.return_value = [task]
338
+            logger_mock.warn.reset_mock()
339
+            self.validator.check_deployment_tasks()
340
+            self.assertEqual(logger_mock.warn.call_count, 1)
341 341
 
342 342
     # This is the section of tests inherited from the v3 validator
343 343
     # where decorators is re-defined for module v4
344 344
 
345
-    @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
346
-    def test_check_deployment_task_role(self, utils_mock, *args):
347
-        super(TestValidatorV4, self).test_check_deployment_task_role(
348
-            utils_mock)
349
-
350 345
     @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
351 346
     @mock.patch('fuel_plugin_builder.validators.base.utils.exists')
352 347
     def test_check_tasks_no_file(self, exists_mock, utils_mock, *args):
353 348
         super(TestValidatorV4, self).test_check_deployment_task_role(
354 349
             exists_mock, utils_mock)
355 350
 
351
+    @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
352
+    def test_check_deployment_task_role(self, utils_mock, *args):
353
+        utils_mock.parse_yaml.return_value = [
354
+            {'id': 'plugin_name', 'type': 'group', 'groups': ['a', 'b']},
355
+            {'id': 'plugin_name', 'type': 'group', 'groups': '*'},
356
+            {'id': 'plugin_name', 'type': 'puppet', 'role': ['a', 'b']},
357
+            {'id': 'plugin_name', 'type': 'puppet', 'role': '*'},
358
+            {'id': 'plugin_name', 'type': 'shell', 'roles': ['a', 'b']},
359
+            {'id': 'plugin_name', 'type': 'shell', 'roles': '*'},
360
+            {'id': 'plugin_name', 'type': 'skipped', 'role': '/test/'},
361
+            {'id': 'plugin_name', 'type': 'stage'},
362
+            {'id': 'plugin_name', 'type': 'reboot', 'groups': 'contrail'},
363
+            {
364
+                'id': 'plugin_name',
365
+                'type': 'copy_files',
366
+                'role': '*',
367
+                'parameters': {
368
+                    'files': [
369
+                        {'src': 'some_source', 'dst': 'some_destination'}]}
370
+            },
371
+            {
372
+                'id': 'plugin_name',
373
+                'type': 'sync',
374
+                'role': 'plugin_name',
375
+                'parameters': {
376
+                    'src': 'some_source', 'dst': 'some_destination'}
377
+            },
378
+            {
379
+                'id': 'plugin_name',
380
+                'type': 'upload_file',
381
+                'role': '/^.*plugin\w+name$/',
382
+                'parameters': {
383
+                    'path': 'some_path', 'data': 'some_data'}
384
+            },
385
+        ]
386
+
387
+        self.validator.check_deployment_tasks()
388
+
356 389
     @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
357 390
     def test_check_deployment_task_role_failed(self, utils_mock, *args):
358
-        super(TestValidatorV4, self).test_check_deployment_task_role_failed(
359
-            utils_mock)
391
+        mock_data = [{
392
+            'id': 'plugin_name',
393
+            'type': 'group',
394
+            'role': ['plugin_n@me']}]
395
+        err_msg = "field should"
396
+        self.check_raised_exception(
397
+            utils_mock, mock_data,
398
+            err_msg, self.validator.check_deployment_tasks)
399
+
400
+    @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
401
+    def test_check_deployment_task_required_missing(self, utils_mock, *args):
402
+        mock_data = [{
403
+            'groups': 'plugin_name',
404
+            'type': 'puppet'}]
405
+        err_msg = 'required'
406
+        self.check_raised_exception(
407
+            utils_mock, mock_data,
408
+            err_msg, self.validator.check_deployment_tasks)
409
+
410
+    @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
411
+    def test_check_deployment_task_required_roles_missing_is_ok(
412
+            self, utils_mock, *args):
413
+        utils_mock.parse_yaml.return_value = [{
414
+            'id': 'plugin_name',
415
+            'type': 'stage'}]
416
+        self.validator.check_deployment_tasks()
417
+
418
+    @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
419
+    def test_check_deployment_task_role_regexp_failed(self, utils_mock, *args):
420
+        mock_data = [{
421
+            'id': 'plugin_name',
422
+            'type': 'group',
423
+            'role': '/[0-9]++/'}]
424
+        err_msg = "field should.*multiple repeat"
425
+        self.check_raised_exception(
426
+            utils_mock, mock_data,
427
+            err_msg, self.validator.check_deployment_tasks)
360 428
 
361 429
     @mock.patch('fuel_plugin_builder.validators.validator_v4.utils')
362 430
     def test_check_group_type_deployment_task_does_not_contain_manifests(

+ 7
- 3
fuel_plugin_builder/validators/base.py View File

@@ -35,15 +35,16 @@ class BaseValidator(object):
35 35
     def basic_version(self):
36 36
         pass
37 37
 
38
-    def __init__(self, plugin_path):
38
+    def __init__(self, plugin_path, format_checker=jsonschema.FormatChecker):
39 39
         self.plugin_path = plugin_path
40
+        self.format_checker = format_checker
40 41
 
41 42
     def validate_schema(self, data, schema, file_path, value_path=None):
42 43
         logger.debug(
43 44
             'Start schema validation for %s file, %s', file_path, schema)
44
-
45 45
         try:
46
-            jsonschema.validate(data, schema)
46
+            jsonschema.validate(data, schema,
47
+                                format_checker=self.format_checker)
47 48
         except jsonschema.exceptions.ValidationError as exc:
48 49
             raise errors.ValidationError(
49 50
                 self._make_error_message(exc, file_path, value_path))
@@ -104,6 +105,7 @@ class BaseValidator(object):
104 105
     @abc.abstractmethod
105 106
     def validate(self):
106 107
         """Performs validation
108
+
107 109
         """
108 110
 
109 111
     def check_schemas(self):
@@ -169,9 +171,11 @@ class BaseValidator(object):
169 171
 
170 172
     def check_compatibility(self):
171 173
         """Json schema doesn't have any conditions, so we have
174
+
172 175
         to make sure here, that this validation schema can be used
173 176
         for described fuel releases
174 177
         """
178
+
175 179
         meta = utils.parse_yaml(self.meta_path)
176 180
         for fuel_release in meta['fuel_version']:
177 181
             if StrictVersion(fuel_release) < StrictVersion(self.basic_version):

+ 53
- 0
fuel_plugin_builder/validators/formatchecker.py View File

@@ -0,0 +1,53 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+#    Copyright 2016 Mirantis, Inc.
4
+#
5
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
6
+#    not use this file except in compliance with the License. You may obtain
7
+#    a copy of the License at
8
+#
9
+#         http://www.apache.org/licenses/LICENSE-2.0
10
+#
11
+#    Unless required by applicable law or agreed to in writing, software
12
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
+#    License for the specific language governing permissions and limitations
15
+#    under the License.
16
+
17
+import re
18
+from sre_constants import error as sre_error
19
+
20
+import jsonschema
21
+import six
22
+
23
+from fuel_plugin_builder import errors
24
+
25
+
26
+class FormatChecker(jsonschema.FormatChecker):
27
+
28
+    def __init__(self, role_patterns=(), *args, **kwargs):
29
+        super(FormatChecker, self).__init__()
30
+
31
+        @self.checks('fuel_task_role_format')
32
+        def _validate_role(instance):
33
+            sre_msg = None
34
+            if isinstance(instance, six.string_types):
35
+                if instance.startswith('/') and instance.endswith('/'):
36
+                    try:
37
+                        re.compile(instance[1:-1])
38
+                        return True
39
+                    except sre_error as e:
40
+                        sre_msg = str(e)
41
+                else:
42
+                    for role_pattern in role_patterns:
43
+                        if re.match(role_pattern, instance):
44
+                            return True
45
+                err_msg = "Role field should be either a valid " \
46
+                          "regexp enclosed by " \
47
+                          "slashes or a string of '{0}' or an " \
48
+                          "array of those. " \
49
+                          "Got '{1}' instead.".format(", ".join(role_patterns),
50
+                                                      instance)
51
+                if sre_msg:
52
+                    err_msg += "SRE error: \"{0}\"".format(sre_msg)
53
+                raise errors.TaskFieldError(err_msg)

+ 26
- 12
fuel_plugin_builder/validators/schemas/v4.py View File

@@ -27,16 +27,26 @@ COMPATIBLE_COMPONENT_NAME_PATTERN = \
27 27
     '^({0}):([0-9a-z_-]+:)*([0-9a-z_-]+|(\*)?)$'.format(COMPONENTS_TYPES_STR)
28 28
 
29 29
 
30
-TASK_NAME_PATTERN = TASK_ROLE_PATTERN = '^[0-9a-zA-Z_-]+$'
30
+TASK_NAME_PATTERN = TASK_ROLE_PATTERN = '^[0-9a-zA-Z_-]+$|^\*$'
31 31
 NETWORK_ROLE_PATTERN = '^[0-9a-z_-]+$'
32 32
 FILE_PERMISSIONS_PATTERN = '^[0-7]{4}$'
33 33
 TASK_VERSION_PATTERN = '^\d+.\d+.\d+$'
34 34
 STAGE_PATTERN = '^(post_deployment|pre_deployment)' \
35 35
                 '(/[-+]?([0-9]*\.[0-9]+|[0-9]+))?$'
36 36
 
37
+ROLE_ALIASES = ('roles', 'groups', 'role')
38
+TASK_OBLIGATORY_FIELDS = ['id', 'type']
39
+ROLELESS_TASKS = ('stage')
40
+
37 41
 
38 42
 class SchemaV4(SchemaV3):
39 43
 
44
+    def __init__(self):
45
+        super(SchemaV4, self).__init__()
46
+        self.role_pattern = TASK_ROLE_PATTERN
47
+        self.roleless_tasks = ROLELESS_TASKS
48
+        self.role_aliases = ROLE_ALIASES
49
+
40 50
     @property
41 51
     def _task_relation(self):
42 52
         return {
@@ -58,13 +68,13 @@ class SchemaV4(SchemaV3):
58 68
             'oneOf': [
59 69
                 {
60 70
                     'type': 'string',
61
-                    'enum': ['*', 'master', 'self']
71
+                    'format': 'fuel_task_role_format'
62 72
                 },
63 73
                 {
64 74
                     'type': 'array',
65 75
                     'items': {
66 76
                         'type': 'string',
67
-                        'pattern': TASK_ROLE_PATTERN
77
+                        'format': 'fuel_task_role_format'
68 78
                     }
69 79
                 }
70 80
             ]
@@ -95,7 +105,8 @@ class SchemaV4(SchemaV3):
95 105
             }
96 106
         }
97 107
 
98
-    def _gen_task_schema(self, task_types, required=None, parameters=None):
108
+    def _gen_task_schema(self, task_types, required=None,
109
+                         parameters=None):
99 110
         """Generate deployment task schema using prototype.
100 111
 
101 112
         :param task_types: task types
@@ -119,11 +130,12 @@ class SchemaV4(SchemaV3):
119 130
         }
120 131
         parameters.setdefault("properties", {})
121 132
         parameters["properties"].setdefault("strategy", self._task_strategy)
122
-
133
+        task_specific_req_fields = list(set(TASK_OBLIGATORY_FIELDS +
134
+                                            (required or [])))
123 135
         return {
124 136
             '$schema': 'http://json-schema.org/draft-04/schema#',
125 137
             'type': 'object',
126
-            'required': list(set(['id', 'type'] + (required or []))),
138
+            'required': task_specific_req_fields,
127 139
             'properties': {
128 140
                 'type': {'enum': task_types},
129 141
                 'id': {
@@ -132,6 +144,8 @@ class SchemaV4(SchemaV3):
132 144
                 'version': {
133 145
                     'type': 'string', "pattern": TASK_VERSION_PATTERN},
134 146
                 'role': self._task_role,
147
+                'groups': self._task_role,
148
+                'roles': self._task_role,
135 149
                 'required_for': self.task_group,
136 150
                 'requires': self.task_group,
137 151
                 'cross-depends': {
@@ -148,7 +162,7 @@ class SchemaV4(SchemaV3):
148 162
                         'pattern': TASK_ROLE_PATTERN}},
149 163
                 'reexecute_on': self._task_reexecute,
150 164
                 'parameters': parameters or {},
151
-            }
165
+            },
152 166
         }
153 167
 
154 168
     @property
@@ -180,7 +194,7 @@ class SchemaV4(SchemaV3):
180 194
     def copy_files_task(self):
181 195
         return self._gen_task_schema(
182 196
             "copy_files",
183
-            ['role', 'parameters'],
197
+            ['parameters'],
184 198
             {
185 199
                 'type': 'object',
186 200
                 'required': ['files'],
@@ -203,7 +217,7 @@ class SchemaV4(SchemaV3):
203 217
 
204 218
     @property
205 219
     def group_task(self):
206
-        return self._gen_task_schema("group", ['role'])
220
+        return self._gen_task_schema("group", [])
207 221
 
208 222
     @property
209 223
     def puppet_task(self):
@@ -242,7 +256,7 @@ class SchemaV4(SchemaV3):
242 256
     def shell_task(self):
243 257
         return self._gen_task_schema(
244 258
             "shell",
245
-            ['role'],
259
+            [],
246 260
             {
247 261
                 'type': 'object',
248 262
                 'required': ['cmd'],
@@ -271,7 +285,7 @@ class SchemaV4(SchemaV3):
271 285
     def sync_task(self):
272 286
         return self._gen_task_schema(
273 287
             "sync",
274
-            ['role', 'parameters'],
288
+            ['parameters'],
275 289
             {
276 290
                 'type': 'object',
277 291
                 'required': ['src', 'dst'],
@@ -287,7 +301,7 @@ class SchemaV4(SchemaV3):
287 301
     def upload_file_task(self):
288 302
         return self._gen_task_schema(
289 303
             "upload_file",
290
-            ['role', 'parameters'],
304
+            ['parameters'],
291 305
             {
292 306
                 'type': 'object',
293 307
                 'required': ['path', 'data'],

+ 16
- 1
fuel_plugin_builder/validators/validator_v4.py View File

@@ -19,9 +19,11 @@ from os.path import join as join_path
19 19
 
20 20
 from fuel_plugin_builder import errors
21 21
 from fuel_plugin_builder import utils
22
+from fuel_plugin_builder.validators.formatchecker import FormatChecker
22 23
 from fuel_plugin_builder.validators.schemas import SchemaV4
23 24
 from fuel_plugin_builder.validators import ValidatorV3
24 25
 
26
+
25 27
 logger = logging.getLogger(__name__)
26 28
 
27 29
 
@@ -30,7 +32,8 @@ class ValidatorV4(ValidatorV3):
30 32
     schema = SchemaV4()
31 33
 
32 34
     def __init__(self, *args, **kwargs):
33
-        super(ValidatorV4, self).__init__(*args, **kwargs)
35
+        super(ValidatorV4, self).__init__(format_checker=FormatChecker(
36
+            role_patterns=[self.schema.role_pattern]), *args, **kwargs)
34 37
         self.components_path = join_path(self.plugin_path, 'components.yaml')
35 38
 
36 39
     @property
@@ -89,6 +92,18 @@ class ValidatorV4(ValidatorV3):
89 92
                 error_msg = 'There is no such task type:' \
90 93
                             '{0}'.format(deployment_task['type'])
91 94
                 raise errors.ValidationError(error_msg)
95
+            if deployment_task['type'] not in self.schema.roleless_tasks:
96
+                for role_alias in self.schema.role_aliases:
97
+                    deployment_role = deployment_task.get(role_alias)
98
+                    if deployment_role:
99
+                        break
100
+                else:
101
+                    logger.warn(
102
+                        'Task {0} does not contain {1} fields. That '
103
+                        'may lead to tasks being unassigned to nodes.'.
104
+                        format(deployment_task['id'], '/'.
105
+                               join(self.schema.role_aliases)))
106
+
92 107
             self.validate_schema(
93 108
                 deployment_task,
94 109
                 schemas[deployment_task['type']],

Loading…
Cancel
Save