Add dcae-cli and component-json-schemas projects
[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
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
169 @component.command()
170 @click.option('--external-ip', '-ip', default=None, help='The external IP address of the Docker host. Only used for Docker components.')
171 @click.option('--additional-user', default=None, help='Additional user to grab instances from.')
172 @click.option('--attached', is_flag=True, help='(Docker) dcae-cli deploys then attaches to the component when set')
173 @click.option('--force', is_flag=True, help='Force component to run without valid downstream dependencies')
174 @click.option('--dmaap-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
175         help=_help_dmaap_file)
176 @click.argument('component')
177 @click.pass_obj
178 def run(obj, external_ip, additional_user, attached, force, dmaap_file, component):
179     '''Runs the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION'''
180     cname, cver = parse_input(component)
181     user, catalog = obj['config']['user'], obj['catalog']
182
183     dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
184
185     try:
186         run_component(user, cname, cver, catalog, additional_user, attached, force,
187                 dmaap_map, external_ip)
188     except DiscoveryNoDownstreamComponentError as e:
189         message = "Either run a compatible downstream component first or run with the --force flag to ignore this error"
190         raise DcaeException(message)
191
192 @component.command()
193 @click.argument('component')
194 @click.pass_obj
195 def undeploy(obj,  component):
196     '''Undeploys the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION'''
197     cname, cver = parse_input(component)
198     user, catalog = obj['config']['user'], obj['catalog']
199     undeploy_component(user, cname, cver, catalog)
200
201
202 @component.command()
203 @click.argument('specification', type=click.Path(resolve_path=True, exists=True))
204 @click.option('--additional-user', default=None, help='Additional user to grab instances from.')
205 @click.option('--force', is_flag=True, help='Force component to run without valid downstream dependencies')
206 @click.option('--dmaap-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False),
207         help=_help_dmaap_file)
208 @click.pass_obj
209 def dev(obj, specification, additional_user, force, dmaap_file):
210     '''Set up component in development for discovery, use for local development'''
211     user, catalog = obj['config']['user'], obj['catalog']
212
213     dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
214
215     with open(specification, 'r+') as f:
216         spec = json.loads(f.read())
217         try:
218             dev_component(user, catalog, spec, additional_user, force, dmaap_map)
219         except DiscoveryNoDownstreamComponentError as e:
220             message = "Either run a compatible downstream component first or run with the --force flag to ignore this error"
221             raise DcaeException(message)
222
223
224 @component.command()
225 @click.argument('component')
226 @click.pass_obj
227 def publish(obj, component):
228     """Pushes COMPONENT to the public catalog"""
229     name, version = parse_input(component)
230     user, catalog = obj['config']['user'], obj['catalog']
231
232     try:
233         # Dependent data formats must be published first before publishing
234         # component. Check that here
235         unpub_formats = catalog.get_unpublished_formats(name, version)
236
237         if unpub_formats:
238             click.echo("You must publish dependent data formats first:")
239             click.echo("")
240             click.echo("\n".join([":".join(uf) for uf in unpub_formats]))
241             click.echo("")
242             return
243     except MissingEntry as e:
244         raise DcaeException("Component not found")
245
246     if catalog.publish_component(user, name, version):
247         click.echo("Component has been published")
248     else:
249         click.echo("Component could not be published")
250
251
252 @component.command()
253 @click.option('--update', is_flag=True, help='Updates a locally added component if it has not been already pushed')
254 @click.argument('specification', type=click.Path(resolve_path=True, exists=True))
255 @click.pass_obj
256 def add(obj, update, specification):
257     user, catalog = obj['config']['user'], obj['catalog']
258
259     spec = load_json(specification)
260     catalog.add_component(user, spec, update)