43266362a8845619c3842b5d611a01b6269cfbef
[dcaegen2/platform/cli.git] / dcae-cli / dcae_cli / commands / component / commands.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 component commands
24 """
25 import json
26 from pprint import pformat
27
28 import click
29
30 from discovery_client import resolve_name
31
32 from dcae_cli.util import profiles, load_json, dmaap, inputs
33 from dcae_cli.util.run import run_component, dev_component
34 from dcae_cli.util import discovery as dis
35 from dcae_cli.util.discovery import DiscoveryNoDownstreamComponentError
36 from dcae_cli.util.undeploy import undeploy_component
37 from dcae_cli.util.exc import DcaeException
38 from dcae_cli.commands import util
39 from dcae_cli.commands.util import parse_input, parse_input_pair, create_table
40
41 from dcae_cli.catalog.exc import MissingEntry
42
43
44 @click.group()
45 def component():
46     pass
47
48
49 @component.command(name='list')
50 @click.option('--latest', is_flag=True, default=True, help='Only list the latest version of components which match the filter criteria')
51 @click.option('--subscribes', '-sub', multiple=True, help='Only list components which subscribe to FORMAT')
52 @click.option('--publishes', '-pub', multiple=True, help='Only list components which publish FORMAT')
53 @click.option('--provides', '-pro', multiple=True, type=(str, str), help='Only list components which provide services REQ_FORMAT RESP_FORMAT')
54 @click.option('--calls', '-cal', multiple=True, type=(str, str), help='Only list components which call services REQ_FORMAT RESP_FORMAT')
55 @click.option('--deployed', is_flag=True, default=False, help='Display the deployed view. Shows details of deployed instances.')
56 @click.pass_obj
57 def list_component(obj, latest, subscribes, publishes, provides, calls, deployed):
58     '''Lists components in the public catalog. Uses flags to filter results.'''
59     subs = list(map(parse_input, subscribes)) if subscribes else None
60     pubs = list(map(parse_input, publishes)) if publishes else None
61     provs = list(map(parse_input_pair, provides)) if provides else None
62     cals = list(map(parse_input_pair, calls)) if calls else None
63
64     user, catalog = obj['config']['user'], obj['catalog']
65     # TODO: How about components that you don't own but you have deployed?
66     comps = catalog.list_components(subs, pubs, provs, cals, latest, user=user)
67
68     active_profile = profiles.get_profile()
69     consul_host = active_profile.consul_host
70
71     click.echo("Active profile: {0}".format(profiles.get_active_name()))
72     click.echo("")
73
74     def format_resolve_results(results):
75         """Format the results from the resolve_name function call"""
76         if results:
77             # Most likely the results will always be length one until we migrate
78             # to a different way of registering names
79             return "\n".join([ pformat(result) for result in results ])
80         else:
81             return None
82
83     def get_instances_as_rows(comp):
84         """Get all deployed running instances of a component plus details about
85         those instances and return as a list of rows"""
86         cname = comp["name"]
87         cver = comp["version"]
88         ctype = comp["component_type"]
89
90         instances = dis.get_healthy_instances(user, cname, cver)
91         instances_status = ["Healthy"]*len(instances)
92         instances_conns = [ format_resolve_results(resolve_name(consul_host, instance)) \
93                 for instance in instances ]
94
95         instances_defective = dis.get_defective_instances(user, cname, cver)
96         instances_status += ["Defective"]*len(instances_defective)
97         instances_conns += [""]*len(instances_defective)
98
99         instances += instances_defective
100
101         return list(zip(instances, instances_status, instances_conns))
102
103     # Generate grouped rows where a grouped row is (name, version, type, [instances])
104     grouped_rows = [ (comp, get_instances_as_rows(comp)) for comp in comps ]
105
106     # Display
107     if deployed:
108         def display_deployed(comp, instances):
109             cname = comp["name"]
110             cver = comp["version"]
111             ctype = comp["component_type"]
112
113             click.echo("Name: {0}".format(cname))
114             click.echo("Version: {0}".format(cver))
115             click.echo("Type: {0}".format(ctype))
116             click.echo(create_table(('Instance', 'Status', 'Connection'), instances))
117             click.echo("")
118
119         [ display_deployed(*row) for row in grouped_rows ]
120     else:
121         def format_row(comp, instances):
122             return comp["name"], comp["version"], comp["component_type"], \
123                 util.format_description(comp["description"]), \
124                 util.get_status_string(comp), comp["modified"], len(instances)
125
126         rows = [ format_row(*grouped_row) for grouped_row in grouped_rows ]
127         click.echo(create_table(('Name', 'Version', 'Type', 'Description',
128             'Status', 'Modified', '#Deployed'), rows))
129         click.echo("\nUse the \"--deployed\" option to see more details on deployments")
130
131
132 @component.command()
133 @click.argument('component', metavar="name:version")
134 @click.pass_obj
135 def show(obj, component):
136     '''Provides more information about COMPONENT'''
137     cname, cver = parse_input(component)
138     catalog = obj['catalog']
139     comp_spec = catalog.get_component_spec(cname, cver)
140
141     click.echo(util.format_json(comp_spec))
142
143
144 _help_dmaap_file = """
145 Path to a file that contains a json of dmaap client information.  The structure of the json is expected to be:
146
147   {
148     <config_key1>: {..client object 1..},
149     <config_key2>: {..client object 2..},
150     ...
151   }
152
153 Where "client object" can be for message or data router. The "config_key" matches the value of specified in the message router "streams" in the component specification.
154
155 Please refer to the documentation for examples of "client object".
156 """
157
158 def _parse_dmaap_file(dmaap_file):
159     try:
160         with open(dmaap_file, 'r+') as f:
161             dmaap_map = json.load(f)
162             dmaap.validate_dmaap_map_schema(dmaap_map)
163             return dmaap.apply_defaults_dmaap_map(dmaap_map)
164     except Exception as e:
165         message = "Problems with parsing the dmaap file. Check to make sure that it is a valid json and is in the expected structure."
166         raise DcaeException(message)
167
168 _help_inputs_file = """
169 Path to a file that contains a json that contains values to be used to bind to configuration parameters that have been marked as "sourced_at_deployment". The structure of the json is expected to be:
170
171  {
172    <parameter1 name>: value,
173    <parameter2 name>: value
174  }
175
176 The "parameter name" is the value of the "name" property for the given configuration parameter.
177 """
178
179 def _parse_inputs_file(inputs_file):
180     try:
181         with open(inputs_file, 'r+') as f:
182             inputs_map = json.load(f)
183             # TODO: Validation of schema in the future? Skipping this because
184             # dti_payload is not being intended to be used.
185             return inputs_map
186     except Exception as e:
187         message = "Problems with parsing the inputs file. Check to make sure that it is a valid json and is in the expected structure."
188         raise DcaeException(message)
189
190
191 @component.command()
192 @click.option('--external-ip', '-ip', default=None, help='The external IP address of the Docker host. Only used for Docker components.')
193 @click.option('--additional-user', default=None, help='Additional user to grab instances from.')
194 @click.option('--attached', is_flag=True, help='(Docker) dcae-cli deploys then attaches to the component when set')
195 @click.option('--force', is_flag=True, help='Force component to run without valid downstream dependencies')
196 @click.option('--dmaap-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
197         help=_help_dmaap_file)
198 @click.option('--inputs-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
199         help=_help_inputs_file)
200 @click.argument('component')
201 @click.pass_obj
202 def run(obj, external_ip, additional_user, attached, force, dmaap_file, component,
203         inputs_file):
204     '''Runs the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION'''
205     cname, cver = parse_input(component)
206     user, catalog = obj['config']['user'], obj['catalog']
207
208     dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
209     inputs_map = _parse_inputs_file(inputs_file) if inputs_file else {}
210
211     try:
212         run_component(user, cname, cver, catalog, additional_user, attached, force,
213                 dmaap_map, inputs_map, external_ip)
214     except DiscoveryNoDownstreamComponentError as e:
215         message = "Either run a compatible downstream component first or run with the --force flag to ignore this error"
216         raise DcaeException(message)
217     except inputs.InputsValidationError as e:
218         click.echo("There is a problem. {0}".format(e))
219         message = "Component requires inputs. Please look at the use of --inputs-file and make sure the format is correct"
220         raise DcaeException(message)
221
222 @component.command()
223 @click.argument('component')
224 @click.pass_obj
225 def undeploy(obj,  component):
226     '''Undeploys the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION'''
227     cname, cver = parse_input(component)
228     user, catalog = obj['config']['user'], obj['catalog']
229     undeploy_component(user, cname, cver, catalog)
230
231
232 @component.command()
233 @click.argument('specification', type=click.Path(resolve_path=True, exists=True))
234 @click.option('--additional-user', default=None, help='Additional user to grab instances from.')
235 @click.option('--force', is_flag=True, help='Force component to run without valid downstream dependencies')
236 @click.option('--dmaap-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
237         help=_help_dmaap_file)
238 @click.option('--inputs-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
239         help=_help_inputs_file)
240 @click.pass_obj
241 def dev(obj, specification, additional_user, force, dmaap_file, inputs_file):
242     '''Set up component in development for discovery, use for local development'''
243     user, catalog = obj['config']['user'], obj['catalog']
244
245     dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
246     inputs_map = _parse_inputs_file(inputs_file) if inputs_file else {}
247
248     with open(specification, 'r+') as f:
249         spec = json.loads(f.read())
250         try:
251             dev_component(user, catalog, spec, additional_user, force, dmaap_map,
252                     inputs_map)
253         except DiscoveryNoDownstreamComponentError as e:
254             message = "Either run a compatible downstream component first or run with the --force flag to ignore this error"
255             raise DcaeException(message)
256         except inputs.InputsValidationError as e:
257             click.echo("There is a problem. {0}".format(e))
258             message = "Component requires inputs. Please look at the use of --inputs-file and make sure the format is correct"
259             raise DcaeException(message)
260
261
262 @component.command()
263 @click.argument('component')
264 @click.pass_obj
265 def publish(obj, component):
266     """Pushes COMPONENT to the public catalog"""
267     name, version = parse_input(component)
268     user, catalog = obj['config']['user'], obj['catalog']
269
270     try:
271         # Dependent data formats must be published first before publishing
272         # component. Check that here
273         unpub_formats = catalog.get_unpublished_formats(name, version)
274
275         if unpub_formats:
276             click.echo("You must publish dependent data formats first:")
277             click.echo("")
278             click.echo("\n".join([":".join(uf) for uf in unpub_formats]))
279             click.echo("")
280             return
281     except MissingEntry as e:
282         raise DcaeException("Component not found")
283
284     if catalog.publish_component(user, name, version):
285         click.echo("Component has been published")
286     else:
287         click.echo("Component could not be published")
288
289
290 @component.command()
291 @click.option('--update', is_flag=True, help='Updates a locally added component if it has not been already pushed')
292 @click.argument('specification', type=click.Path(resolve_path=True, exists=True))
293 @click.pass_obj
294 def add(obj, update, specification):
295     user, catalog = obj['config']['user'], obj['catalog']
296
297     spec = load_json(specification)
298     catalog.add_component(user, spec, update)