Passed
Pull Request — master (#3462)
by Lakshmi
11:43 queued 05:50
created

PackAPI.__init__()   A

Complexity

Conditions 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
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
from st2common.util.pack import validate_config_against_schema
35
36
__all__ = [
37
    'PackAPI',
38
    'ConfigSchemaAPI',
39
    'ConfigAPI',
40
41
    'ConfigItemSetAPI',
42
43
    'PackInstallRequestAPI',
44
    'PackRegisterRequestAPI',
45
    'PackSearchRequestAPI',
46
    'PackAsyncAPI'
47
]
48
49
LOG = logging.getLogger(__name__)
50
51
52
class PackAPI(BaseAPI):
53
    model = PackDB
54
    schema = {
55
        'type': 'object',
56
        'description': 'Content pack schema.',
57
        'properties': {
58
            'id': {
59
                'type': 'string',
60
                'description': 'Unique identifier for the pack.',
61
                'default': None
62
            },
63
            'name': {
64
                'type': 'string',
65
                'description': 'Display name of the pack. If the name only contains lowercase'
66
                               'letters, digits and underscores, the "ref" field is not required.',
67
                'required': True
68
            },
69
            'ref': {
70
                'type': 'string',
71
                'description': 'Reference for the pack, used as an internal id.',
72
                'default': None,
73
                'pattern': PACK_REF_WHITELIST_REGEX
74
            },
75
            'uid': {
76
                'type': 'string'
77
            },
78
            'description': {
79
                'type': 'string',
80
                'description': 'Brief description of the pack and the service it integrates with.',
81
                'required': True
82
            },
83
            'keywords': {
84
                'type': 'array',
85
                'description': 'Keywords describing the pack.',
86
                'items': {'type': 'string'},
87
                'default': []
88
            },
89
            'version': {
90
                'type': 'string',
91
                'description': 'Pack version. Must follow the semver format '
92
                               '(for instance, "0.1.0").',
93
                'pattern': PACK_VERSION_REGEX,
94
                'required': True
95
            },
96
            'stackstorm_version': {
97
                'type': 'string',
98
                'description': 'Required StackStorm version. Examples: ">1.6.0", '
99
                               '">=1.8.0, <2.2.0"',
100
                'pattern': ST2_VERSION_REGEX,
101
            },
102
            'author': {
103
                'type': 'string',
104
                'description': 'Pack author or authors.',
105
                'required': True
106
            },
107
            'email': {
108
                'type': 'string',
109
                'description': 'E-mail of the pack author.',
110
                'format': 'email'
111
            },
112
            'contributors': {
113
                'type': 'array',
114
                'items': {
115
                    'type': 'string',
116
                    'maxLength': 100
117
                },
118
                'description': ('A list of people who have contributed to the pack. Format is: '
119
                                'Name <email address> e.g. Tomaz Muraus <[email protected]>.')
120
            },
121
            'files': {
122
                'type': 'array',
123
                'description': 'A list of files inside the pack.',
124
                'items': {'type': 'string'},
125
                'default': []
126
            },
127
            'dependencies': {
128
                'type': 'array',
129
                'description': 'A list of other StackStorm packs this pack depends upon. '
130
                               'The same format as in "st2 pack install" is used: '
131
                               '"<name or full URL>[=<version or git ref>]".',
132
                'items': {'type': 'string'},
133
                'default': []
134
            },
135
            'system': {
136
                'type': 'object',
137
                'description': 'Specification for the system components and packages '
138
                               'required for the pack.',
139
                'default': {}
140
            }
141
        }
142
    }
143
144
    def __init__(self, **values):
145
        # Note: If some version values are not explicitly surrounded by quotes they are recognized
146
        # as numbers so we cast them to string
147
        if values.get('version', None):
148
            values['version'] = str(values['version'])
149
150
        super(PackAPI, self).__init__(**values)
151
152
    def validate(self):
153
        # We wrap default validate() implementation and throw a more user-friendly exception in
154
        # case pack version doesn't follow a valid semver format and other errors
155
        try:
156
            super(PackAPI, self).validate()
157
        except jsonschema.ValidationError as e:
158
            msg = str(e)
159
160
            # Invalid version
161
            if "Failed validating 'pattern' in schema['properties']['version']" in msg:
162
                new_msg = ('Pack version "%s" doesn\'t follow a valid semver format. Valid '
163
                           'versions and formats include: 0.1.0, 0.2.1, 1.1.0, etc.' %
164
                           (self.version))
165
                new_msg += '\n\n' + msg
166
                raise jsonschema.ValidationError(new_msg)
167
168
            # Invalid ref / name
169
            if "Failed validating 'pattern' in schema['properties']['ref']" in msg:
170
                new_msg = ('Pack ref / name can only contain valid word characters (a-z, 0-9 and '
171
                           '_), dashes are not allowed.')
172
                new_msg += '\n\n' + msg
173
                raise jsonschema.ValidationError(new_msg)
174
175
            raise e
176
177
    @classmethod
178
    def to_model(cls, pack):
179
        ref = pack.ref
180
        name = pack.name
181
        description = pack.description
182
        keywords = getattr(pack, 'keywords', [])
183
        version = str(pack.version)
184
185
        stackstorm_version = getattr(pack, 'stackstorm_version', None)
186
        author = pack.author
187
        email = pack.email
188
        contributors = getattr(pack, 'contributors', [])
189
        files = getattr(pack, 'files', [])
190
        dependencies = getattr(pack, 'dependencies', [])
191
        system = getattr(pack, 'system', {})
192
193
        model = cls.model(ref=ref, name=name, description=description, keywords=keywords,
194
                          version=version, author=author, email=email, contributors=contributors,
195
                          files=files, dependencies=dependencies, system=system,
196
                          stackstorm_version=stackstorm_version)
197
        return model
198
199
200
class ConfigSchemaAPI(BaseAPI):
201
    model = ConfigSchemaDB
202
    schema = {
203
        "title": "ConfigSchema",
204
        "description": "Pack config schema.",
205
        "type": "object",
206
        "properties": {
207
            "id": {
208
                "description": "The unique identifier for the config schema.",
209
                "type": "string"
210
            },
211
            "pack": {
212
                "description": "The content pack this config schema belongs to.",
213
                "type": "string"
214
            },
215
            "attributes": {
216
                "description": "Config schema attributes.",
217
                "type": "object",
218
                "patternProperties": {
219
                    "^\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...
220
                },
221
                'additionalProperties': False,
222
                "default": {}
223
            }
224
        },
225
        "additionalProperties": False
226
    }
227
228
    @classmethod
229
    def to_model(cls, config_schema):
230
        pack = config_schema.pack
231
        attributes = config_schema.attributes
232
233
        model = cls.model(pack=pack, attributes=attributes)
234
        return model
235
236
237
class ConfigAPI(BaseAPI):
238
    model = ConfigDB
239
    schema = {
240
        "title": "Config",
241
        "description": "Pack config.",
242
        "type": "object",
243
        "properties": {
244
            "id": {
245
                "description": "The unique identifier for the config.",
246
                "type": "string"
247
            },
248
            "pack": {
249
                "description": "The content pack this config belongs to.",
250
                "type": "string"
251
            },
252
            "values": {
253
                "description": "Config values.",
254
                "type": "object",
255
                "default": {}
256
            }
257
        },
258
        "additionalProperties": False
259
    }
260
261
    def validate(self, validate_against_schema=False):
0 ignored issues
show
Bug introduced by
Arguments number differs from overridden 'validate' method
Loading history...
262
        # Perform base API model validation against json schema
263
        result = super(ConfigAPI, self).validate()
264
265
        # Perform config values validation against the config values schema
266
        if validate_against_schema:
267
            cleaned_values = self._validate_config_values_against_schema()
268
            result.values = cleaned_values
269
270
        return result
271
272
    def _validate_config_values_against_schema(self):
273
        try:
274
            config_schema_db = ConfigSchema.get_by_pack(value=self.pack)
275
        except StackStormDBObjectNotFoundError:
276
            # Config schema is optional
277
            return
278
279
        # Note: We are doing optional validation so for now, we do allow additional properties
280
        instance = self.values or {}
281
        schema = config_schema_db.attributes or {}
282
283
        configs_path = os.path.join(cfg.CONF.system.base_path, 'configs/')
284
        config_path = os.path.join(configs_path, '%s.yaml' % (self.pack))
285
286
        cleaned = validate_config_against_schema(config_schema=schema,
287
                                                 config_object=instance,
288
                                                 config_path=config_path,
289
                                                 pack_name=self.pack)
290
291
        return cleaned
292
293
    @classmethod
294
    def to_model(cls, config):
295
        pack = config.pack
296
        values = config.values
297
298
        model = cls.model(pack=pack, values=values)
299
        return model
300
301
302
class ConfigUpdateRequestAPI(BaseAPI):
303
    schema = {
304
        "type": "object"
305
    }
306
307
308
class ConfigItemSetAPI(BaseAPI):
309
    """
310
    API class used with the config set API endpoint.
311
    """
312
    model = None
313
    schema = {
314
        "title": "",
315
        "description": "",
316
        "type": "object",
317
        "properties": {
318
            "name": {
319
                "description": "Config item name (key)",
320
                "type": "string",
321
                "required": True
322
            },
323
            "value": {
324
                "description": "Config item value.",
325
                "type": ["string", "number", "boolean", "array", "object"],
326
                "required": True
327
            },
328
            "scope": {
329
                "description": "Config item scope (system / user)",
330
                "type": "string",
331
                "default": SYSTEM_SCOPE,
332
                "enum": [
333
                    SYSTEM_SCOPE,
334
                    USER_SCOPE
335
                ]
336
            },
337
            "user": {
338
                "description": "User for user-scoped items (only available to admins).",
339
                "type": "string",
340
                "required": False,
341
                "default": None
342
            }
343
        },
344
        "additionalProperties": False
345
    }
346
347
348
class PackInstallRequestAPI(BaseAPI):
349
    schema = {
350
        "type": "object",
351
        "properties": {
352
            "packs": {
353
                "type": "array"
354
            },
355
            "force": {
356
                "type": "boolean",
357
                "description": "Force pack installation",
358
                "default": False
359
            }
360
        }
361
    }
362
363
364
class PackRegisterRequestAPI(BaseAPI):
365
    schema = {
366
        "type": "object",
367
        "properties": {
368
            "types": {
369
                "type": "array",
370
                "items": {
371
                    "type": "string"
372
                }
373
            },
374
            "packs": {
375
                "type": "array",
376
                "items": {
377
                    "type": "string"
378
                }
379
            },
380
            "fail_on_failure": {
381
                "type": "boolean",
382
                "description": "True to fail on failure",
383
                "default": True
384
            }
385
        }
386
    }
387
388
389
class PackSearchRequestAPI(BaseAPI):
390
    schema = {
391
        "type": "object",
392
        "oneOf": [
393
            {
394
                "properties": {
395
                    "query": {
396
                        "type": "string",
397
                        "required": True,
398
                    },
399
                },
400
                "additionalProperties": False,
401
            },
402
            {
403
                "properties": {
404
                    "pack": {
405
                        "type": "string",
406
                        "required": True,
407
                    },
408
                },
409
                "additionalProperties": False,
410
            },
411
        ]
412
    }
413
414
415
class PackAsyncAPI(BaseAPI):
416
    schema = {
417
        "type": "object",
418
        "properties": {
419
            "execution_id": {
420
                "type": "string",
421
                "required": True
422
            }
423
        },
424
        "additionalProperties": False
425
    }
426