1 # ============LICENSE_START=======================================================
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
10 # http://www.apache.org/licenses/LICENSE-2.0
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=========================================================
19 # ECOMP is a trademark and service mark of AT&T Intellectual Property.
21 # -*- coding: utf-8 -*-
23 Provides component commands
26 from pprint import pformat
30 from discovery_client import resolve_name
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
41 from dcae_cli.catalog.exc import MissingEntry
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.')
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
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)
68 active_profile = profiles.get_profile()
69 consul_host = active_profile.consul_host
71 click.echo("Active profile: {0}".format(profiles.get_active_name()))
74 def format_resolve_results(results):
75 """Format the results from the resolve_name function call"""
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 ])
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"""
87 cver = comp["version"]
88 ctype = comp["component_type"]
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 ]
95 instances_defective = dis.get_defective_instances(user, cname, cver)
96 instances_status += ["Defective"]*len(instances_defective)
97 instances_conns += [""]*len(instances_defective)
99 instances += instances_defective
101 return list(zip(instances, instances_status, instances_conns))
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 ]
108 def display_deployed(comp, instances):
110 cver = comp["version"]
111 ctype = comp["component_type"]
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))
119 [ display_deployed(*row) for row in grouped_rows ]
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)
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")
133 @click.argument('component', metavar="name:version")
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)
141 click.echo(util.format_json(comp_spec))
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:
148 <config_key1>: {..client object 1..},
149 <config_key2>: {..client object 2..},
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.
155 Please refer to the documentation for examples of "client object".
158 def _parse_dmaap_file(dmaap_file):
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)
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:
172 <parameter1 name>: value,
173 <parameter2 name>: value
176 The "parameter name" is the value of the "name" property for the given configuration parameter.
179 def _parse_inputs_file(inputs_file):
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.
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)
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')
202 def run(obj, external_ip, additional_user, attached, force, dmaap_file, component,
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']
208 dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
209 inputs_map = _parse_inputs_file(inputs_file) if inputs_file else {}
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)
223 @click.argument('component')
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)
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)
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']
245 dmaap_map = _parse_dmaap_file(dmaap_file) if dmaap_file else {}
246 inputs_map = _parse_inputs_file(inputs_file) if inputs_file else {}
248 with open(specification, 'r+') as f:
249 spec = json.loads(f.read())
251 dev_component(user, catalog, spec, additional_user, force, dmaap_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)
263 @click.argument('component')
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']
271 # Dependent data formats must be published first before publishing
272 # component. Check that here
273 unpub_formats = catalog.get_unpublished_formats(name, version)
276 click.echo("You must publish dependent data formats first:")
278 click.echo("\n".join([":".join(uf) for uf in unpub_formats]))
281 except MissingEntry as e:
282 raise DcaeException("Component not found")
284 if catalog.publish_component(user, name, version):
285 click.echo("Component has been published")
287 click.echo("Component could not be published")
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))
294 def add(obj, update, specification):
295 user, catalog = obj['config']['user'], obj['catalog']
297 spec = load_json(specification)
298 catalog.add_component(user, spec, update)