Completed
Pull Request — master (#2988)
by Edward
07:10
created

PackAPI.to_model()   A

Complexity

Conditions 1

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
c 3
b 0
f 0
dl 0
loc 20
rs 9.4285
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  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 os
17
18
import jsonschema
19
from oslo_config import cfg
20
21
from st2common import log as logging
22
from st2common.util import schema as util_schema
23
from st2common.constants.keyvalue import SYSTEM_SCOPE
24
from st2common.constants.keyvalue import USER_SCOPE
25
from st2common.constants.pack import PACK_REF_WHITELIST_REGEX
26
from st2common.constants.pack import PACK_VERSION_REGEX
27
from st2common.constants.pack import ST2_VERSION_REGEX
28
from st2common.persistence.pack import ConfigSchema
29
from st2common.models.api.base import BaseAPI
30
from st2common.models.db.pack import PackDB
31
from st2common.models.db.pack import ConfigSchemaDB
32
from st2common.models.db.pack import ConfigDB
33
from st2common.exceptions.db import StackStormDBObjectNotFoundError
34
35
__all__ = [
36
    'PackAPI',
37
    'ConfigSchemaAPI',
38
    'ConfigAPI',
39
40
    'ConfigItemSetAPI',
41
42
    'PackInstallRequestAPI',
43
    'PackRegisterRequestAPI',
44
    'PackSearchRequestAPI',
45
    'PackAsyncAPI'
46
]
47
48
LOG = logging.getLogger(__name__)
49
50
51
class PackAPI(BaseAPI):
52
    model = PackDB
53
    schema = {
54
        'type': 'object',
55
        'description': 'Content pack schema.',
56
        'properties': {
57
            'id': {
58
                'type': 'string',
59
                'description': 'Unique identifier for the pack.',
60
                'default': None
61
            },
62
            'name': {
63
                'type': 'string',
64
                'description': 'Display name of the pack. If the name only contains lowercase'
65
                               'letters, digits and underscores, the "ref" field is not required.',
66
                'required': True
67
            },
68
            'ref': {
69
                'type': 'string',
70
                'description': 'Reference for the pack, used as an internal id.',
71
                'default': None,
72
                'pattern': PACK_REF_WHITELIST_REGEX
73
            },
74
            'uid': {
75
                'type': 'string'
76
            },
77
            'description': {
78
                'type': 'string',
79
                'description': 'Brief description of the pack and the service it integrates with.',
80
                'required': True
81
            },
82
            'keywords': {
83
                'type': 'array',
84
                'description': 'Keywords describing the pack.',
85
                'items': {'type': 'string'},
86
                'default': []
87
            },
88
            'version': {
89
                'type': 'string',
90
                'description': 'Pack version. Must follow the semver format '
91
                               '(for instance, "0.1.0").',
92
                'pattern': PACK_VERSION_REGEX,
93
                'required': True
94
            },
95
            'stackstorm_version': {
96
                'type': 'string',
97
                'description': 'Required StackStorm version. Examples: ">1.6.0", '
98
                               '">=1.8.0, <2.2.0"',
99
                'pattern': ST2_VERSION_REGEX,
100
            },
101
            'author': {
102
                'type': 'string',
103
                'description': 'Pack author or authors.',
104
                'required': True
105
            },
106
            'email': {
107
                'type': 'string',
108
                'description': 'E-mail of the pack author.',
109
                'format': 'email'
110
            },
111
            'files': {
112
                'type': 'array',
113
                'description': 'A list of files inside the pack.',
114
                'items': {'type': 'string'},
115
                'default': []
116
            },
117
            'dependencies': {
118
                'type': 'array',
119
                'description': 'A list of other StackStorm packs this pack depends upon. '
120
                               'The same format as in "st2 pack install" is used: '
121
                               '"<name or full URL>[=<version or git ref>]".',
122
                'items': {'type': 'string'},
123
                'default': []
124
            },
125
            'system': {
126
                'type': 'object',
127
                'description': 'Specification for the system components and packages '
128
                               'required for the pack.',
129
                'default': {}
130
            }
131
        }
132
    }
133
134
    def __init__(self, **values):
135
        # Note: If some version values are not explicitly surrounded by quotes they are recognized
136
        # as numbers so we cast them to string
137
        if values.get('version', None):
138
            values['version'] = str(values['version'])
139
140
        super(PackAPI, self).__init__(**values)
141
142
    def validate(self):
143
        # We wrap default validate() implementation and throw a more user-friendly exception in
144
        # case pack version doesn't follow a valid semver format
145
        try:
146
            super(PackAPI, self).validate()
147
        except jsonschema.ValidationError as e:
148
            msg = str(e)
149
150
            if "Failed validating 'pattern' in schema['properties']['version']" in msg:
151
                new_msg = ('Pack version "%s" doesn\'t follow a valid semver format. Valid '
152
                           'versions and formats include: 0.1.0, 0.2.1, 1.1.0, etc.' %
153
                           (self.version))
154
                new_msg += '\n\n' + msg
155
                raise jsonschema.ValidationError(new_msg)
156
157
            raise e
158
159
    @classmethod
160
    def to_model(cls, pack):
161
        ref = pack.ref
162
        name = pack.name
163
        description = pack.description
164
        keywords = getattr(pack, 'keywords', [])
165
        version = str(pack.version)
166
167
        stackstorm_version = getattr(pack, 'stackstorm_version', None)
168
        author = pack.author
169
        email = pack.email
170
        files = getattr(pack, 'files', [])
171
        dependencies = getattr(pack, 'dependencies', [])
172
        system = getattr(pack, 'system', {})
173
174
        model = cls.model(ref=ref, name=name, description=description, keywords=keywords,
175
                          version=version, author=author, email=email, files=files,
176
                          dependencies=dependencies, system=system,
177
                          stackstorm_version=stackstorm_version)
178
        return model
179
180
181
class ConfigSchemaAPI(BaseAPI):
182
    model = ConfigSchemaDB
183
    schema = {
184
        "title": "ConfigSchema",
185
        "description": "Pack config schema.",
186
        "type": "object",
187
        "properties": {
188
            "id": {
189
                "description": "The unique identifier for the config schema.",
190
                "type": "string"
191
            },
192
            "pack": {
193
                "description": "The content pack this config schema belongs to.",
194
                "type": "string"
195
            },
196
            "attributes": {
197
                "description": "Config schema attributes.",
198
                "type": "object",
199
                "patternProperties": {
200
                    "^\w+$": util_schema.get_action_parameters_schema()
0 ignored issues
show
Bug introduced by
A suspicious escape sequence \w was found. Did you maybe forget to add an r prefix?

Escape sequences in Python are generally interpreted according to rules similar to standard C. Only if strings are prefixed with r or R are they interpreted as regular expressions.

The escape sequence that was used indicates that you might have intended to write a regular expression.

Learn more about the available escape sequences. in the Python documentation.

Loading history...
201
                },
202
                'additionalProperties': False,
203
                "default": {}
204
            }
205
        },
206
        "additionalProperties": False
207
    }
208
209
    @classmethod
210
    def to_model(cls, config_schema):
211
        pack = config_schema.pack
212
        attributes = config_schema.attributes
213
214
        model = cls.model(pack=pack, attributes=attributes)
215
        return model
216
217
218
class ConfigAPI(BaseAPI):
219
    model = ConfigDB
220
    schema = {
221
        "title": "Config",
222
        "description": "Pack config.",
223
        "type": "object",
224
        "properties": {
225
            "id": {
226
                "description": "The unique identifier for the config.",
227
                "type": "string"
228
            },
229
            "pack": {
230
                "description": "The content pack this config belongs to.",
231
                "type": "string"
232
            },
233
            "values": {
234
                "description": "Config values.",
235
                "type": "object",
236
                "default": {}
237
            }
238
        },
239
        "additionalProperties": False
240
    }
241
242
    def validate(self, validate_against_schema=False):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'validate' method
Loading history...
243
        # Perform base API model validation against json schema
244
        result = super(ConfigAPI, self).validate()
245
246
        # Perform config values validation against the config values schema
247
        if validate_against_schema:
248
            cleaned_values = self._validate_config_values_against_schema()
249
            result.values = cleaned_values
250
251
        return result
252
253
    def _validate_config_values_against_schema(self):
254
        try:
255
            config_schema_db = ConfigSchema.get_by_pack(value=self.pack)
256
        except StackStormDBObjectNotFoundError:
257
            # Config schema is optional
258
            return
259
260
        # Note: We are doing optional validation so for now, we do allow additional properties
261
        instance = self.values or {}
262
        schema = config_schema_db.attributes
263
        schema = util_schema.get_schema_for_resource_parameters(parameters_schema=schema,
264
                                                                allow_additional_properties=True)
265
266
        try:
267
            cleaned = util_schema.validate(instance=instance, schema=schema,
268
                                           cls=util_schema.CustomValidator, use_default=True,
269
                                           allow_default_none=True)
270
        except jsonschema.ValidationError as e:
271
            attribute = getattr(e, 'path', [])
272
            attribute = '.'.join(attribute)
273
            configs_path = os.path.join(cfg.CONF.system.base_path, 'configs/')
274
            config_path = os.path.join(configs_path, '%s.yaml' % (self.pack))
275
276
            msg = ('Failed validating attribute "%s" in config for pack "%s" (%s): %s' %
277
                   (attribute, self.pack, config_path, str(e)))
278
            raise jsonschema.ValidationError(msg)
279
280
        return cleaned
281
282
    @classmethod
283
    def to_model(cls, config):
284
        pack = config.pack
285
        values = config.values
286
287
        model = cls.model(pack=pack, values=values)
288
        return model
289
290
291
class ConfigUpdateRequestAPI(BaseAPI):
292
    schema = {
293
        "type": "object"
294
    }
295
296
297
class ConfigItemSetAPI(BaseAPI):
298
    """
299
    API class used with the config set API endpoint.
300
    """
301
    model = None
302
    schema = {
303
        "title": "",
304
        "description": "",
305
        "type": "object",
306
        "properties": {
307
            "name": {
308
                "description": "Config item name (key)",
309
                "type": "string",
310
                "required": True
311
            },
312
            "value": {
313
                "description": "Config item value.",
314
                "type": ["string", "number", "boolean", "array", "object"],
315
                "required": True
316
            },
317
            "scope": {
318
                "description": "Config item scope (system / user)",
319
                "type": "string",
320
                "default": SYSTEM_SCOPE,
321
                "enum": [
322
                    SYSTEM_SCOPE,
323
                    USER_SCOPE
324
                ]
325
            },
326
            "user": {
327
                "description": "User for user-scoped items (only available to admins).",
328
                "type": "string",
329
                "required": False,
330
                "default": None
331
            }
332
        },
333
        "additionalProperties": False
334
    }
335
336
337
class PackInstallRequestAPI(BaseAPI):
338
    schema = {
339
        "type": "object",
340
        "properties": {
341
            "packs": {
342
                "type": "array"
343
            }
344
        }
345
    }
346
347
348
class PackRegisterRequestAPI(BaseAPI):
349
    schema = {
350
        "type": "object",
351
        "properties": {
352
            "types": {
353
                "type": "array",
354
                "items": {
355
                    "type": "string"
356
                }
357
            },
358
            "packs": {
359
                "type": "array",
360
                "items": {
361
                    "type": "string"
362
                }
363
            }
364
        }
365
    }
366
367
368
class PackSearchRequestAPI(BaseAPI):
369
    schema = {
370
        "type": "object",
371
        "oneOf": [
372
            {
373
                "properties": {
374
                    "query": {
375
                        "type": "string",
376
                        "required": True,
377
                    },
378
                },
379
                "additionalProperties": False,
380
            },
381
            {
382
                "properties": {
383
                    "pack": {
384
                        "type": "string",
385
                        "required": True,
386
                    },
387
                },
388
                "additionalProperties": False,
389
            },
390
        ]
391
    }
392
393
394
class PackAsyncAPI(BaseAPI):
395
    schema = {
396
        "type": "object",
397
        "properties": {
398
            "execution_id": {
399
                "type": "string",
400
                "required": True
401
            }
402
        },
403
        "additionalProperties": False
404
    }
405