a85d11fb224b7ebc265250d4af013d450de8a923
[dcaegen2/platform/cli.git] / dcae-cli / dcae_cli / catalog / mock / schema.py
1 # ============LICENSE_START=======================================================
2 # org.onap.dcae
3 # ================================================================================
4 # Copyright (c) 2017 AT&T Intellectual Property. All rights reserved.
5 # ================================================================================
6 # Licensed under the Apache License, Version 2.0 (the "License");
7 # you may not use this file except in compliance with the License.
8 # You may obtain a copy of the License at
9 #
10 #      http://www.apache.org/licenses/LICENSE-2.0
11 #
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS,
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 # See the License for the specific language governing permissions and
16 # limitations under the License.
17 # ============LICENSE_END=========================================================
18 #
19 # ECOMP is a trademark and service mark of AT&T Intellectual Property.
20
21 # -*- coding: utf-8 -*-
22 """
23 Provides jsonschema
24 """
25 import json
26 from functools import partial, reduce
27
28 import six
29 from jsonschema import validate, ValidationError
30 import requests
31
32 from dcae_cli.util import reraise_with_msg
33 from dcae_cli.util import config as cli_config
34 from dcae_cli.util.exc import DcaeException
35 from dcae_cli.util.logger import get_logger
36
37
38 log = get_logger('Schema')
39
40 # UPDATE: This message applies to the component spec which has been moved on a
41 # remote server.
42 #
43 # WARNING: The below has a "oneOf" for service provides, that will validate as long as any of them are chosen.
44 # However, this is wrong because what we really want is something like:
45 #     if component_type == docker
46 #       provides = foo
47 #     elif component_type == cdap
48 #       provides = bar
49 # The unlikely but problematic  case is the cdap developer gets a hold of the docker documentation, uses that, it validates, and blows up at cdap runtime
50
51
52 # TODO: The next step here is to decide how to manage the links to the schemas. Either:
53 #
54 #   a) Manage the links in the dcae-cli tool here and thus need to ask if this
55 #   belongs in the config to point to some remote server or even point to local
56 #   machine.
57 #   UPDATE: This item has been mostly completed where at least the path is configurable now.
58
59 #   b) Read the links to the schemas from the spec - self-describing jsons. Is
60 #   this even feasible?
61
62 #   c) Both
63 #
64 # TODO: Source this from app's configuration [ONAP URL TBD]
65 _nexus_uri = "http://make-me-valid"
66
67 class FetchSchemaError(RuntimeError):
68     pass
69
70 def _fetch_schema_from_web(server_uri, schema_path):
71     try:
72         schema_url = "{0}/{1}".format(server_uri, schema_path)
73         r = requests.get(schema_url)
74         r.raise_for_status()
75         return json.loads(r.text)
76     except requests.HTTPError as e:
77         raise FetchSchemaError("HTTP error from fetching schema", e)
78     except Exception as e:
79         raise FetchSchemaError("Unexpected error from fetching schema", e)
80
81 _fetch_schema_from_nexus = partial(_fetch_schema_from_web, _nexus_uri)
82
83
84 def _safe_dict(obj):
85     '''Returns a dict from a dict or json string'''
86     if isinstance(obj, str):
87         return json.loads(obj)
88     else:
89         return obj
90
91 def _validate(fetch_schema_func, schema_path, spec):
92     '''Validate the given spec
93
94     Fetch the schema and then validate. Upon a error from fetching or validation,
95     a DcaeException is raised.
96
97     Parameters
98     ----------
99     fetch_schema_func: function that takes schema_path -> dict representation of schema
100         throws a FetchSchemaError upon any failure
101     schema_path: string - path to schema
102     spec: dict or string representation of JSON of schema instance
103
104     Returns
105     -------
106     Nothing, silence is golden
107     '''
108     try:
109         schema = fetch_schema_func(schema_path)
110         validate(_safe_dict(spec), schema)
111     except ValidationError as e:
112         reraise_with_msg(e, as_dcae=True)
113     except FetchSchemaError as e:
114         reraise_with_msg(e, as_dcae=True)
115
116 _validate_using_nexus = partial(_validate, _fetch_schema_from_nexus)
117
118
119 _path_component_spec = cli_config.get_path_component_spec()
120
121 def apply_defaults(properties_definition, properties):
122     """Utility method to enforce expected defaults
123
124     This method is used to enforce properties that are *expected* to have at least
125     the default if not set by a user.  Expected properties are not required but
126     have a default set.  jsonschema does not provide this.
127
128     Parameters
129     ----------
130     properties_definition: dict of the schema definition of the properties to use
131         for verifying and applying defaults
132     properties: dict of the target properties to verify and apply defaults to
133
134     Return
135     ------
136     dict - a new version of properties that has the expected default values
137     """
138     # Recursively process all inner objects. Look for more properties and not match
139     # on type
140     for k,v in six.iteritems(properties_definition):
141         if "properties" in v:
142             properties[k] = apply_defaults(v["properties"], properties.get(k, {}))
143
144     # Collect defaults
145     defaults = [ (k, v["default"]) for k, v in properties_definition.items() if "default" in v ]
146
147     def apply_default(accumulator, default):
148         k, v = default
149         if k not in accumulator:
150             # Not doing data type checking and any casting. Assuming that this
151             # should have been taken care of in validation
152             accumulator[k] = v
153         return accumulator
154
155     return reduce(apply_default, defaults, properties)
156
157 def apply_defaults_docker_config(config):
158     """Apply expected defaults to Docker config
159     Parameters
160     ----------
161     config: Docker config dict
162     Return
163     ------
164     Updated Docker config dict
165     """
166     # Apply health check defaults
167     healthcheck_type = config["healthcheck"]["type"]
168     component_spec = _fetch_schema_from_nexus(_path_component_spec)
169
170     if healthcheck_type in ["http", "https"]:
171         apply_defaults_func = partial(apply_defaults,
172                 component_spec["definitions"]["docker_healthcheck_http"]["properties"])
173     elif healthcheck_type in ["script"]:
174         apply_defaults_func = partial(apply_defaults,
175                 component_spec["definitions"]["docker_healthcheck_script"]["properties"])
176     else:
177         # You should never get here
178         apply_defaults_func = lambda x: x
179
180     config["healthcheck"] = apply_defaults_func(config["healthcheck"])
181
182     return config
183
184 def validate_component(spec):
185     _validate_using_nexus(_path_component_spec, spec)
186
187     # REVIEW: Could not determine how to do this nicely in json schema. This is
188     # not ideal. We want json schema to be the "it" for validation.
189     ctype = component_type = spec["self"]["component_type"]
190
191     if ctype == "cdap":
192         invalid = [s for s in spec["streams"].get("subscribes", []) \
193                 if s["type"] in ["data_router", "data router"]]
194         if invalid:
195             raise DcaeException("Cdap component as data router subscriber is not supported.")
196
197 def validate_format(spec):
198     path = cli_config.get_path_data_format()
199     _validate_using_nexus(path, spec)