diff options
author | 2018-10-04 20:56:01 +0000 | |
---|---|---|
committer | 2018-10-04 20:56:01 +0000 | |
commit | 1943cd6c3fd5b70209089d9d2135ffb344f0405a (patch) | |
tree | eb2fecee26c4624f881d0966836c99c43351d405 | |
parent | 2babf1a6d97b1958a436bf3cb9ac7c21e453272b (diff) | |
parent | 91a44d2ce8398f0fd98e520933df605b00f80eee (diff) |
Merge "Policy Reconfiguration, Component Spec, Help text"
19 files changed, 467 insertions, 92 deletions
diff --git a/dcae-cli/ChangeLog.md b/dcae-cli/ChangeLog.md index aa845bd..b0e97a3 100644 --- a/dcae-cli/ChangeLog.md +++ b/dcae-cli/ChangeLog.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [2.11.0] + +* Add Policy Configuration Support. +* Update and improve the Help Text that is displayed to the user. +* Component Spec schema additions for parameters section (policy, volumes). +* Component Spec schema updates to make the following required: (designer_editable, sourced_at_deployment, policy_editable). + ## [2.10.2] * Fix dependency conflict with python-consul @@ -126,4 +133,4 @@ EDIT: This one addressed the issue in the catalog ## [0.11.0] -* Make CDAP Paramaters follow parameters definitions +* Make CDAP Paramaters follow parameters definitions diff --git a/dcae-cli/dcae_cli/_version.py b/dcae-cli/dcae_cli/_version.py index ac49ad3..f8b2331 100644 --- a/dcae-cli/dcae_cli/_version.py +++ b/dcae-cli/dcae_cli/_version.py @@ -19,4 +19,4 @@ # ECOMP is a trademark and service mark of AT&T Intellectual Property. # -*- coding: utf-8 -*- -__version__ = "2.10.2" +__version__ = "2.11.0" diff --git a/dcae-cli/dcae_cli/catalog/mock/tests/test_mock_catalog.py b/dcae-cli/dcae_cli/catalog/mock/tests/test_mock_catalog.py index 98b7c54..0859c44 100644 --- a/dcae-cli/dcae_cli/catalog/mock/tests/test_mock_catalog.py +++ b/dcae-cli/dcae_cli/catalog/mock/tests/test_mock_catalog.py @@ -58,10 +58,16 @@ _c1_spec = {'self': {'name': 'std.comp_one', 'route': '/prov1'}]}, 'parameters': [{"name": "foo", "value": 1, - "description": "the foo thing"}, + "description": "the foo thing", + "designer_editable": False, + "sourced_at_deployment": False, + "policy_editable": False}, {"name": "bar", "value": 2, - "description": "the bar thing"} + "description": "the bar thing", + "designer_editable": False, + "sourced_at_deployment": False, + "policy_editable": False} ], 'artifacts': [{ "uri": "foo-image", "type": "docker image" }], 'auxilary': { diff --git a/dcae-cli/dcae_cli/catalog/mock/tests/test_schema.py b/dcae-cli/dcae_cli/catalog/mock/tests/test_schema.py index 1ac176b..90674d9 100644 --- a/dcae-cli/dcae_cli/catalog/mock/tests/test_schema.py +++ b/dcae-cli/dcae_cli/catalog/mock/tests/test_schema.py @@ -110,7 +110,10 @@ component_test = r''' { "name": "threshold", "value": 0.75, - "description": "Probability threshold to exceed to be anomalous" + "description": "Probability threshold to exceed to be anomalous", + "designer_editable": false, + "sourced_at_deployment": false, + "policy_editable": false } ], "artifacts": [ diff --git a/dcae-cli/dcae_cli/commands/catalog/commands.py b/dcae-cli/dcae_cli/commands/catalog/commands.py index 44771fa..dc6b27a 100644 --- a/dcae-cli/dcae_cli/commands/catalog/commands.py +++ b/dcae-cli/dcae_cli/commands/catalog/commands.py @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # org.onap.dcae # ================================================================================ -# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,11 @@ def catalog(): @catalog.command(name="list") -@click.option("--expanded", is_flag=True, default=False, help="Display the expanded view - show all versions and all status") +@click.option("--expanded", is_flag=True, default=False, help="Display the expanded view - show all versions and all statuses") #TODO: @click.argument('query') @click.pass_obj def action_list(obj, expanded): + """Lists resources in the onboarding catalog""" # Query both components and data formats. Display both sets. user, catalog = obj['config']['user'], obj['catalog'] @@ -82,6 +83,7 @@ def action_list(obj, expanded): @click.argument("resource", metavar="name:version") @click.pass_obj def action_show(obj, resource): + """Provides more information about a resource""" # Query both components and data formats. Display both sets. name, ver = util.parse_input(resource) catalog = obj['catalog'] diff --git a/dcae-cli/dcae_cli/commands/component/commands.py b/dcae-cli/dcae_cli/commands/component/commands.py index 4326636..d91027f 100644 --- a/dcae-cli/dcae_cli/commands/component/commands.py +++ b/dcae-cli/dcae_cli/commands/component/commands.py @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # org.onap.dcae # ================================================================================ -# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -26,15 +26,18 @@ import json from pprint import pformat import click +import os from discovery_client import resolve_name -from dcae_cli.util import profiles, load_json, dmaap, inputs +from dcae_cli.util import profiles, load_json, dmaap, inputs, policy from dcae_cli.util.run import run_component, dev_component from dcae_cli.util import discovery as dis +from dcae_cli.util import docker_util as du from dcae_cli.util.discovery import DiscoveryNoDownstreamComponentError from dcae_cli.util.undeploy import undeploy_component from dcae_cli.util.exc import DcaeException + from dcae_cli.commands import util from dcae_cli.commands.util import parse_input, parse_input_pair, create_table @@ -133,7 +136,7 @@ def list_component(obj, latest, subscribes, publishes, provides, calls, deployed @click.argument('component', metavar="name:version") @click.pass_obj def show(obj, component): - '''Provides more information about COMPONENT''' + '''Provides more information about a COMPONENT''' cname, cver = parse_input(component) catalog = obj['catalog'] comp_spec = catalog.get_component_spec(cname, cver) @@ -162,9 +165,10 @@ def _parse_dmaap_file(dmaap_file): dmaap.validate_dmaap_map_schema(dmaap_map) return dmaap.apply_defaults_dmaap_map(dmaap_map) except Exception as e: - message = "Problems with parsing the dmaap file. Check to make sure that it is a valid json and is in the expected structure." + message = "Problems with parsing the dmaap file. Check to make sure that it is a valid json and is in the expected format." raise DcaeException(message) + _help_inputs_file = """ 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: @@ -180,14 +184,36 @@ def _parse_inputs_file(inputs_file): try: with open(inputs_file, 'r+') as f: inputs_map = json.load(f) - # TODO: Validation of schema in the future? Skipping this because - # dti_payload is not being intended to be used. + # TODO: Validation of schema in the future? return inputs_map except Exception as e: - message = "Problems with parsing the inputs file. Check to make sure that it is a valid json and is in the expected structure." + message = "Problems with parsing the inputs file. Check to make sure that it is a valid json and is in the expected format." raise DcaeException(message) +_help_policy_file = """ +Path to a file that contains a json of an (update/remove) Policy change. +All "policies" can also be specified. +The structure of the json is expected to be: + +{ +"updated_policies": [{"policyName": "value", "": ""},{"policyName": "value", "": ""}], +"removed_policies": [{"policyName": "value", "": ""},{"policyName": "value", "": ""}], +"policies": [{"policyName": "value", "": ""},{"policyName": "value", "": ""}] +} +""" + +def _parse_policy_file(policy_file): + try: + with open(policy_file, 'r+') as f: + policy_change_file = json.load(f) + policy.validate_against_policy_schema(policy_change_file) + return policy_change_file + except Exception as e: + click.echo(format(e)) + message = "Problems with parsing the Policy file. Check to make sure that it is a valid json and is in the expected format." + raise DcaeException(message) + @component.command() @click.option('--external-ip', '-ip', default=None, help='The external IP address of the Docker host. Only used for Docker components.') @click.option('--additional-user', default=None, help='Additional user to grab instances from.') @@ -201,7 +227,11 @@ def _parse_inputs_file(inputs_file): @click.pass_obj def run(obj, external_ip, additional_user, attached, force, dmaap_file, component, inputs_file): - '''Runs the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION''' + '''Runs latest (or specific) COMPONENT version. You may optionally specify version via COMPONENT:VERSION''' + + click.echo("Running the Component.....") + click.echo("") + cname, cver = parse_input(component) user, catalog = obj['config']['user'], obj['catalog'] @@ -215,7 +245,8 @@ def run(obj, external_ip, additional_user, attached, force, dmaap_file, componen message = "Either run a compatible downstream component first or run with the --force flag to ignore this error" raise DcaeException(message) except inputs.InputsValidationError as e: - click.echo("There is a problem. {0}".format(e)) + click.echo("ERROR: There is a problem. {0}".format(e)) + click.echo("") message = "Component requires inputs. Please look at the use of --inputs-file and make sure the format is correct" raise DcaeException(message) @@ -223,7 +254,7 @@ def run(obj, external_ip, additional_user, attached, force, dmaap_file, componen @click.argument('component') @click.pass_obj def undeploy(obj, component): - '''Undeploys the latest version of COMPONENT. You may optionally specify version via COMPONENT:VERSION''' + '''Undeploy latest (or specific) COMPONENT version. You may optionally specify version via COMPONENT:VERSION''' cname, cver = parse_input(component) user, catalog = obj['config']['user'], obj['catalog'] undeploy_component(user, cname, cver, catalog) @@ -254,7 +285,8 @@ def dev(obj, specification, additional_user, force, dmaap_file, inputs_file): message = "Either run a compatible downstream component first or run with the --force flag to ignore this error" raise DcaeException(message) except inputs.InputsValidationError as e: - click.echo("There is a problem. {0}".format(e)) + click.echo("ERROR: There is a problem. {0}".format(e)) + click.echo("") message = "Component requires inputs. Please look at the use of --inputs-file and make sure the format is correct" raise DcaeException(message) @@ -263,7 +295,7 @@ def dev(obj, specification, additional_user, force, dmaap_file, inputs_file): @click.argument('component') @click.pass_obj def publish(obj, component): - """Pushes COMPONENT to the public catalog""" + """Pushes a COMPONENT to the public catalog""" name, version = parse_input(component) user, catalog = obj['config']['user'], obj['catalog'] @@ -273,7 +305,7 @@ def publish(obj, component): unpub_formats = catalog.get_unpublished_formats(name, version) if unpub_formats: - click.echo("You must publish dependent data formats first:") + click.echo("ERROR: You must publish dependent data formats first:") click.echo("") click.echo("\n".join([":".join(uf) for uf in unpub_formats])) click.echo("") @@ -284,15 +316,79 @@ def publish(obj, component): if catalog.publish_component(user, name, version): click.echo("Component has been published") else: - click.echo("Component could not be published") + click.echo("ERROR: Component could not be published") @component.command() -@click.option('--update', is_flag=True, help='Updates a locally added component if it has not been already pushed') +@click.option('--update', is_flag=True, help='Updates a locally added component if it has not already been published') @click.argument('specification', type=click.Path(resolve_path=True, exists=True)) @click.pass_obj def add(obj, update, specification): + """Add Component to local onboarding catalog""" user, catalog = obj['config']['user'], obj['catalog'] spec = load_json(specification) catalog.add_component(user, spec, update) + + +@component.command() +@click.option('--policy-file', type=click.Path(resolve_path=True, exists=True, dir_okay=False), help=_help_policy_file) +@click.argument('component') +@click.pass_obj +def reconfig(obj, policy_file, component): + """Reconfigure COMPONENT for Policy change. + Modify Consul KV pairs for ('updated_policies', 'removed_policies', and 'policies') for Policy change event, + Execute the reconfig script(s) in the Docker container""" + + click.echo("Running Component Reconfiguration.....") + click.echo("") + + # Read and Validate the policy-file + policy_change_file = _parse_policy_file(policy_file) if policy_file else {} + + if not (policy_change_file): + click.echo("ERROR: For component 'reconfig', you must specify a --policy-file") + click.echo("") + return + else: + # The Component Spec contains the Policy 'Reconfig Script Path/ScriptName' + cname, cver = parse_input(component) + catalog = obj['catalog'] + comp_spec = catalog.get_component_spec(cname, cver) + + # Check if component is running and healthy + active_profile = profiles.get_profile() + consul_host = active_profile.consul_host + service_name = os.environ["SERVICE_NAME"] + if dis.is_healthy(consul_host, service_name): + pass + else: + click.echo("ERROR: The component must be running and healthy. It is not.") + click.echo("") + return + + try: + policy_reconfig_path = comp_spec['auxilary']['policy']['script_path'] + except KeyError: + click.echo("ERROR: Policy Reconfig Path (auxilary/policy/script_path) is not specified in the Component Spec") + click.echo("") + return + + kvUpdated = dis.policy_update(policy_change_file) + + if kvUpdated: + active_profile = profiles.get_profile() + docker_logins = dis.get_docker_logins() + + command = dis.build_policy_command(policy_reconfig_path, policy_change_file) + + # Run the Policy Reconfig script + client = du.get_docker_client(active_profile, docker_logins) + du.reconfigure(client, service_name, command) + else: + click.echo("ERROR: There was a problem updating the policies in Consul") + click.echo("") + return + + click.echo("") + click.echo("The End of Component Reconfiguration") diff --git a/dcae-cli/dcae_cli/commands/data_format/commands.py b/dcae-cli/dcae_cli/commands/data_format/commands.py index b942442..b952336 100644 --- a/dcae-cli/dcae_cli/commands/data_format/commands.py +++ b/dcae-cli/dcae_cli/commands/data_format/commands.py @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # org.onap.dcae # ================================================================================ -# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -53,11 +53,11 @@ def data_format(): @data_format.command() -@click.option('--update', is_flag=True, help='Updates a locally added data format if it has not been already pushed') +@click.option('--update', is_flag=True, help='Updates a locally added data format if it has not already been published') @click.argument('specification', type=click.Path(resolve_path=True, exists=True)) @click.pass_obj def add(obj, update, specification): - '''Tracks a data format file SPECIFICATION locally but does not push to the catalog''' + '''Tracks a Format file Specification locally, but does not push to the catalog''' spec = load_json(specification) user, catalog = obj['config']['user'], obj['catalog'] catalog.add_format(spec, user, update) @@ -67,7 +67,7 @@ def add(obj, update, specification): @click.option('--latest', is_flag=True, help='Only list the latest version of data formats') @click.pass_obj def list_format(obj, latest): - """Lists all your data formats""" + """Lists all your Data Formats""" user, catalog = obj['config']['user'], obj['catalog'] dfs = catalog.list_formats(latest, user=user) @@ -87,7 +87,7 @@ def list_format(obj, latest): @click.argument('data-format', metavar="name:version") @click.pass_obj def show(obj, data_format): - '''Provides more information about FORMAT''' + '''Provides more information about a Data Format''' name, ver = parse_input(data_format) spec = obj['catalog'].get_format_spec(name, ver) @@ -98,14 +98,14 @@ def show(obj, data_format): @click.argument('data-format') @click.pass_obj def publish(obj, data_format): - """Publishes data format to make publicly available""" + """Publish Format to make publicly available""" name, version = parse_input(data_format) user, catalog = obj['config']['user'], obj['catalog'] if catalog.publish_format(user, name, version): click.echo("Data format has been published") else: - click.echo("Data format could not be published") + click.echo("ERROR: Data format could not be published") @data_format.command() @click.option('--keywords', is_flag=True, help='Adds a template of possible descriptive keywords', default=False) @@ -115,7 +115,7 @@ def publish(obj, data_format): def generate(obj, name_version, file_or_dir_path, keywords): '''Create schema from a file or directory examples''' name, version = parse_input(name_version) - if version == None: + if version == None: version = "" schema = genson.Schema() if os.path.isfile(file_or_dir_path): @@ -149,11 +149,11 @@ def generate(obj, name_version, file_or_dir_path, keywords): raise DcaeException('Problem with JSON generation') def addfile(filename, schema): - try: + try: fileadd = open(filename, "r") except IOError: raise DcaeException('Cannot open' + filename) - try: + try: json_object = json.loads(fileadd.read()) schema.add_object(json_object) except ValueError: @@ -161,4 +161,3 @@ def addfile(filename, schema): finally: fileadd.close() - diff --git a/dcae-cli/dcae_cli/commands/profiles/commands.py b/dcae-cli/dcae_cli/commands/profiles/commands.py index dfd5517..df34b5c 100644 --- a/dcae-cli/dcae_cli/commands/profiles/commands.py +++ b/dcae-cli/dcae_cli/commands/profiles/commands.py @@ -1,7 +1,7 @@ # ============LICENSE_START======================================================= # org.onap.dcae # ================================================================================ -# Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +# Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved. # ================================================================================ # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,7 +39,7 @@ def profiles(): @profiles.command() @click.argument('name') def activate(name): - '''Sets profile NAME as the active profile''' + '''Sets profile (name) as the active profile''' activate_profile(name) @@ -56,7 +56,7 @@ def list_profiles(): @profiles.command() @click.argument('name') def show(name): - '''Prints the profile dictionary''' + '''Provides more information about a Profile''' profiles = get_profiles() try: click.echo(json.dumps(profiles[name], sort_keys=True, indent=4)) @@ -67,7 +67,7 @@ def show(name): @profiles.command() @click.argument('name', type=click.STRING) def create(name): - '''Creates a new profile NAME initialized with defaults''' + '''Creates new profile (name), with defaults''' create_profile(name) @@ -76,12 +76,12 @@ def create(name): @click.argument('key') @click.argument('value') def update(name, key, value): - '''Updates profile NAME such that KEY=VALUE''' + '''Updates profile (name) for specific Key/Value''' update_profile(name, **{key: value}) @profiles.command() @click.argument('name') def delete(name): - '''Deletes profile NAME''' + '''Deletes profile (name)''' delete_profile(name) diff --git a/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_end.json b/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_end.json index f2f12f9..9642a6e 100644 --- a/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_end.json +++ b/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_end.json @@ -35,15 +35,28 @@ "parameters": { "app_config" : [ {"name" : "some_param", - "description" : "some desc", - "value" : "some_value"} + "description" : "some desc", + "value" : "some_value", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false} ], "app_preferences" : [ {"name" : "some_param2", "description" : "some desc2", - "value" : "some_value2"} + "value" : "some_value2", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false} ], - "program_preferences" : [{"program_type" : "flows", "program_id" : "WhoFlow", "program_pref" : [{"name" : "some_param3","description" : "some desc3", "value" : "some_value3"}]}] + "program_preferences" : [{"program_type" : "flows", + "program_id" : "WhoFlow", + "program_pref" : [{"name" : "some_param3", + "description" : "some desc3", + "value" : "some_value3", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false}]}] }, "auxilary": { "streamname":"who", diff --git a/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_start.json b/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_start.json index c4a807e..83b5c28 100644 --- a/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_start.json +++ b/dcae-cli/dcae_cli/commands/tests/mocked_components/cdap/spec_start.json @@ -35,14 +35,28 @@ "app_config" : [ {"name" : "some_param", "description" : "some desc", - "value" : "some_value"} + "value" : "some_value", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false} ], "app_preferences" : [ {"name" : "some_param2", "description" : "some desc2", - "value" : "some_value2"} + "value" : "some_value2", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false} ], - "program_preferences" : [{"program_type" : "flows", "program_id" : "WhoFlow", "program_pref" : [{"name" : "some_param3","description" : "some desc3", "value" : "some_value3"}]}] + "program_preferences" : [{"program_type" : "flows", + "program_id" : "WhoFlow", + "program_pref" : [{"name" : "some_param3", + "description" : "some desc3", + "value" : "some_value3", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false} + ]}] }, "auxilary": { "streamname":"who", diff --git a/dcae-cli/dcae_cli/commands/tests/mocked_components/collector/kpi-collector.comp.json b/dcae-cli/dcae_cli/commands/tests/mocked_components/collector/kpi-collector.comp.json index 5508e90..5b86d9c 100644 --- a/dcae-cli/dcae_cli/commands/tests/mocked_components/collector/kpi-collector.comp.json +++ b/dcae-cli/dcae_cli/commands/tests/mocked_components/collector/kpi-collector.comp.json @@ -24,7 +24,10 @@ { "name": "sleep_sec", "value": 0.75, - "description": "Number of seconds to sleep between publishes" + "description": "Number of seconds to sleep between publishes", + "designer_editable": false, + "sourced_at_deployment": false, + "policy_editable": false } ], "auxilary": { diff --git a/dcae-cli/dcae_cli/commands/tests/mocked_components/model/anomaly-model.comp.json b/dcae-cli/dcae_cli/commands/tests/mocked_components/model/anomaly-model.comp.json index e5f5889..3e2d142 100644 --- a/dcae-cli/dcae_cli/commands/tests/mocked_components/model/anomaly-model.comp.json +++ b/dcae-cli/dcae_cli/commands/tests/mocked_components/model/anomaly-model.comp.json @@ -29,7 +29,10 @@ { "name": "threshold", "value": 0.75, - "description": "Probability threshold to exceed to be anomalous" + "description": "Probability threshold to exceed to be anomalous", + "designer_editable" : false, + "sourced_at_deployment" : false, + "policy_editable" : false } ], "auxilary": { diff --git a/dcae-cli/dcae_cli/commands/tests/test_component_cmd.py b/dcae-cli/dcae_cli/commands/tests/test_component_cmd.py index 67769eb..2bba4cf 100644 --- a/dcae-cli/dcae_cli/commands/tests/test_component_cmd.py +++ b/dcae-cli/dcae_cli/commands/tests/test_component_cmd.py @@ -41,7 +41,7 @@ def _get_spec(path): def test_comp_docker(mock_cli_config, mock_db_url, obj=None): - obj = {'catalog': MockCatalog(purge_existing=True, db_name='dcae_cli.test.db', + obj = {'catalog': MockCatalog(purge_existing=True, db_name='dcae_cli.test.db', enforce_image=False, db_url=mock_db_url), 'config': {'user': 'test-user'}} @@ -113,63 +113,35 @@ def test_comp_cdap(obj=None): 3) runs a cdap component using our "Rework" broker 4) undeploys the cdap component using our "Rework" broker - NOTE: TODO: Mocking out the broker would be an improvement over this, probably. This is impure. Mocking the broker owuld be a huge undertaking, though. + NOTE: TODO: Mocking out the broker would be an improvement over this, probably. This is impure. Mocking the broker owuld be a huge undertaking, though. """ obj = {'catalog': MockCatalog(purge_existing=True, db_name='dcae_cli.test.db'), 'config': {'user': 'test-user'}} runner = CliRunner() - + #add the data format df = os.path.join(TEST_DIR, 'mocked_components', 'cdap', 'format.json') cmd = "data_format add {:}".format(df).split() assert runner.invoke(cli, cmd, obj=obj).exit_code == 0 - + #add the CDAP components # TODO: Need to update the host jar = 'http://make-me-valid/HelloWorld-3.4.3.jar' - + comp_cdap_start = os.path.join(TEST_DIR, 'mocked_components', 'cdap', 'spec_start.json') cmd = "component add {0}".format(comp_cdap_start).split() print(cmd) result = runner.invoke(cli, cmd, obj=obj) print(result.output) assert result.exit_code == 0 - + comp_cdap_end = os.path.join(TEST_DIR, 'mocked_components', 'cdap', 'spec_end.json') cmd = "component add {0}".format(comp_cdap_end).split() print(cmd) result = runner.invoke(cli, cmd, obj=obj) print(result.output) assert result.exit_code == 0 - - #run the terminating component first - cmd = "component run --force cdap.helloworld.mock.catalog.testing.endnode".split() - print(cmd) - result = runner.invoke(cli, cmd, obj=obj) - print(result.output) - assert result.exit_code == 0 - - #run the component again: this time the second component finds the first - cmd = "component run --force cdap.helloworld.mock.catalog.testing.startnode".split() - print(cmd) - result = runner.invoke(cli, cmd, obj=obj) - assert "config_key 'service_call_example' has no compatible downstream components." not in result.output #touchdown baby - assert result.exit_code == 0 - - #sleep - time.sleep(5) - - #delete the components - cmd = "component undeploy cdap.helloworld.mock.catalog.testing.startnode".split() - print(cmd) - result = runner.invoke(cli, cmd, obj=obj) - assert result.exit_code == 0 - - cmd = "component undeploy cdap.helloworld.mock.catalog.testing.endnode".split() - print(cmd) - result = runner.invoke(cli, cmd, obj=obj) - assert result.exit_code == 0 if __name__ == '__main__': '''Test area''' diff --git a/dcae-cli/dcae_cli/util/config.py b/dcae-cli/dcae_cli/util/config.py index c9df69a..7444fb1 100644 --- a/dcae-cli/dcae_cli/util/config.py +++ b/dcae-cli/dcae_cli/util/config.py @@ -124,7 +124,7 @@ def get_docker_logins_key(): def get_path_component_spec(): return get_config().get("path_component_spec", - "/component-json-schemas/component-specification/dcae-cli-v1/component-spec-schema.json") + "/component-json-schemas/component-specification/dcae-cli-v2/component-spec-schema.json") def get_path_data_format(): return get_config().get("path_data_format", diff --git a/dcae-cli/dcae_cli/util/discovery.py b/dcae-cli/dcae_cli/util/discovery.py index ba74f1f..0fc0165 100644 --- a/dcae-cli/dcae_cli/util/discovery.py +++ b/dcae-cli/dcae_cli/util/discovery.py @@ -28,6 +28,7 @@ import contextlib from collections import defaultdict from itertools import chain from functools import partial +from datetime import datetime from uuid import uuid4 import six @@ -39,6 +40,8 @@ from dcae_cli.util.exc import DcaeException from dcae_cli.util.profiles import get_profile from dcae_cli.util.config import get_docker_logins_key +import os +import click logger = get_logger('Discovery') @@ -342,6 +345,12 @@ def _create_dmaap_key(config_key): return "{:}:dmaap".format(config_key) +def _create_policies_key(config_key): + """Create policies key from config key + + Assumes config_key is well-formed""" + return "{:}:policies/".format(config_key) + def clear_user_instances(user, host=None): '''Removes all Consul key:value entries for a given user''' host = _choose_consul_host(host) @@ -510,6 +519,11 @@ def push_config(conf_key, conf, rels_key, rels, dmaap_key, dmaap_map, host=None) for k, v in ((conf_key, conf), (rels_key, rels), (dmaap_key, dmaap_map)): cons.kv.put(k, json.dumps(v)) + logger.info("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *") + logger.info("* If you run a 'component reconfig' command, you must first execute the following") + logger.info("* export SERVICE_NAME={:}".format(conf_key)) + logger.info("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *") + def remove_config(config_key, host=None): """Deletes a config from Consul @@ -520,9 +534,10 @@ def remove_config(config_key, host=None): """ host = _choose_consul_host(host) cons = Consul(host) - results = [ cons.kv.delete(k) for k in (config_key, _create_rels_key(config_key), \ - _create_dmaap_key(config_key)) ] - return all(results) + # "recurse=True" deletes the SERVICE_NAME KV and all other KVs with suffixes (:rel, :dmaap, :policies) + results = cons.kv.delete(config_key, recurse=True) + + return results def _group_config(config, config_key_map): @@ -559,7 +574,8 @@ def config_context(user, cname, cver, params, interface_map, instance_map, Args ---- - always_cleanup: (boolean) This context manager will cleanup the produced config + always_cleanup: (boolean) + This context manager will cleanup the produced config context always if this is True. When False, cleanup will only occur upon any exception getting thrown in the context manager block. Default is True. force: (boolean) @@ -596,3 +612,166 @@ def config_context(user, cname, cver, params, interface_map, instance_map, pass else: remove_config(conf_key, host) + + +def policy_update(policy_change_file): + + # Determine if it is an 'updated_policies' or 'removed_policies' change, or if user included ALL policies + policies = True if "policies" in policy_change_file.keys() else False + updated = True if "updated_policies" in policy_change_file.keys() else False + removed = True if "removed_policies" in policy_change_file.keys() else False + + cons = Consul(consul_host) + service_name = os.environ["SERVICE_NAME"] + policy_folder = service_name + ":policies/items/" + event_folder = service_name + ":policies/event" + + if policies: + # User specified ALL "policies" in the Policy File. Ignore "updated_policies"/"removed_policies" + logger.warning("The 'policies' specified in the 'policy-file' will replace all policies in Consul.") + allPolicies = policy_change_file['policies'] + if not update_all_policies(cons, policy_folder, allPolicies): + return False + + else: + # If 'removed_policies', delete the Policy from the Component KV pair + if removed: + policyDeletes = policy_change_file['removed_policies'] + if not remove_policies(cons, policy_folder, policyDeletes): + return False + + # If 'updated_policies', update the Component KV pair + if updated: + policyUpdates = policy_change_file['updated_policies'] + if not update_specified_policies(cons, policy_folder, policyUpdates): + return False + + return create_policy_event(cons, event_folder, policy_folder) + + +def create_policy_event(cons, event_folder, policy_folder): + """ Create a Policy 'event' KV pair in Consol """ + + timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + update_id = str(uuid4()) + policies = cons.kv.get(policy_folder, recurse=True) + policies_count = str(policies).count("'Key':") + + event = '{"action": "gathered", "timestamp": "' + timestamp + '", "update_id": "' + update_id + '", "policies_count": ' + str(policies_count) + '}' + if not cons.kv.put(event_folder, event): + logger.error("Policy 'Event' creation of ({:}) in Consul failed".format(event_folder)) + return False + + return True + + +def update_all_policies(cons, policy_folder, allPolicies): + """ Delete all policies from Consul, then add the policies the user specified in the 'policies' section of the policy-file """ + + if not cons.kv.delete(policy_folder, recurse=True): # Deletes all Policies under the /policies/items folder + logger.error("Policy delete of ({:}) in Consul failed".format(policy_folder)) + return False + + if not update_specified_policies(cons, policy_folder, allPolicies): + return False + + return True + +def update_specified_policies(cons, policy_folder, policyUpdates): + """ Replace the policies the user specified in the 'updated_policies' (or 'policies') section of the policy-file """ + + for policy in policyUpdates: + policy_folder_id = extract_policy_id(policy_folder, policy) + if policy_folder_id: + policyBody = json.dumps(policy) + if not cons.kv.put(policy_folder_id, policyBody): + logger.error("Policy update of ({:}) in Consul failed".format(policy_folder_id)) + return False + else: + return False + + return True + + +def remove_policies(cons, policy_folder, policyDeletes): + """ Delete the policies that the user specified in the 'removed_policies' section of the policy-file """ + + for policy in policyDeletes: + policy_folder_id = extract_policy_id(policy_folder, policy) + if policy_folder_id: + if not cons.kv.delete(policy_folder_id): + logger.error("Policy delete of ({:}) in Consul failed".format(policy_folder_id)) + return False + else: + return False + + return True + +def extract_policy_id(policy_folder, policy): + """ Extract the Policy ID from the policyName. + Return the Consul key (Policy Folder with Policy ID) """ + + policyId_re = re.compile(r"(.*)\.\d+\.[a-zA-Z]+$") + + policyName = policy['policyName'] # Extract the policy Id "Consul Key" from the policy name + match = policyId_re.match(policyName) + + if match: + policy_id = match.group(1) + policy_folder_id = policy_folder + policy_id + + return policy_folder_id + else: + logger.error("policyName ({:}) needs to end in '.#.xml' in order to extract the Policy ID".format(policyName)) + return + + +def build_policy_command(policy_reconfig_path, policy_change_file): + """ Build command to execute the Policy Reconfig script in the Docker container """ + + # Determine if it is an 'updated_policies' and/or 'removed_policies' change, or if user included ALL policies + all_policies = True if "policies" in policy_change_file.keys() else False + updated = True if "updated_policies" in policy_change_file.keys() else False + removed = True if "removed_policies" in policy_change_file.keys() else False + + # Create the Reconfig Script command (3 parts: Command and 2 ARGs) + command = [] + command.append(policy_reconfig_path) + command.append("policies") + + # Create a Dictionary of 'updated', 'removed', and 'ALL' policies + + # 'updated' policies - policies come from the --policy-file + if updated: + updated_policies = policy_change_file['updated_policies'] + else: updated_policies = [] + + policies = {} + policies["updated_policies"] = updated_policies + + # 'removed' policies - policies come from the --policy-file + if removed: + removed_policies = policy_change_file['removed_policies'] + else: removed_policies = [] + + policies["removed_policies"] = removed_policies + + # ALL 'policies' - policies come from Consul + cons = Consul(consul_host) + service_name = os.environ["SERVICE_NAME"] + policy_folder = service_name + ":policies/items/" + + id, consul_policies = cons.kv.get(policy_folder, recurse=True) + + policy_values = [] + if consul_policies: + for policy in consul_policies: + policy_value = json.loads(policy['Value']) + policy_values.append(policy_value) + + policies["policies"] = policy_values + + # Add the policies to the Docker "command" as a JSON string + command.append(json.dumps(policies)) + + return command diff --git a/dcae-cli/dcae_cli/util/docker_util.py b/dcae-cli/dcae_cli/util/docker_util.py index 7ae933f..3e29f5c 100644 --- a/dcae-cli/dcae_cli/util/docker_util.py +++ b/dcae-cli/dcae_cli/util/docker_util.py @@ -32,7 +32,6 @@ import dockering as doc from dcae_cli.util.logger import get_logger from dcae_cli.util.exc import DcaeException - dlog = get_logger('Docker') _reg_img = 'gliderlabs/registrator:latest' @@ -188,6 +187,7 @@ def deploy_component(profile, image, instance_name, docker_config, should_wait=F client = get_docker_client(profile, logins=logins) config = doc.create_container_config(client, image, envs, hcp) + return _run_container(client, config, name=instance_name, wait=should_wait) @@ -210,3 +210,17 @@ def undeploy_component(client, image, instance_name): except Exception as e: dlog.error("Error while undeploying Docker container/image: {0}".format(e)) return False + +def reconfigure(client, instance_name, command): + """ Execute the Reconfig script in the Docker container """ + + # 'command' has 3 parts in a list (1 Command and 2 ARGs) + exec_Id = client.exec_create(container=instance_name, cmd=command) + + exec_start_resp = client.exec_start(exec_Id, stream=True) + + # Using a 'single' generator response to solve issue of 'start_exec' returning control after 6 minutes + for response in exec_start_resp: + dlog.info("Reconfig Script execution response: {:}".format(response)) + exec_start_resp.close() + break diff --git a/dcae-cli/dcae_cli/util/policy.py b/dcae-cli/dcae_cli/util/policy.py new file mode 100644 index 0000000..2da9f0b --- /dev/null +++ b/dcae-cli/dcae_cli/util/policy.py @@ -0,0 +1,64 @@ +# ============LICENSE_START======================================================= +# org.onap.dcae +# ================================================================================ +# Copyright (c) 2018 AT&T Intellectual Property. All rights reserved. +# ================================================================================ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============LICENSE_END========================================================= +# +# ECOMP is a trademark and service mark of AT&T Intellectual Property. + +""" +Function for Policy schema validation +""" + +from jsonschema import validate, ValidationError +from dcae_cli.util.logger import get_logger +from dcae_cli.util import reraise_with_msg + +logger = get_logger('policy') + +_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Schema for policy changes", + "type": "object", + "properties": { + "updated_policies": {"type": "array"}, + "removed_policies": {"type": "array"}, + "policies": {"type": "array"} + }, + "additionalProperties": False +} + +_validation_msg = """ +Is your Policy file a valid json? +Does your Policy file follow this format? + +{ + "updated_policies": [{},{},...], + "removed_policies": [{},{},...], + "policies": [{},{},...] +} +""" + + +def validate_against_policy_schema(policy_file): + """Validate the policy file against the schema""" + + try: + validate(policy_file, _SCHEMA) + except ValidationError as e: + logger.error("Policy file validation issue") + logger.error(_validation_msg) + reraise_with_msg(e, as_dcae=True) +
\ No newline at end of file diff --git a/dcae-cli/dcae_cli/util/run.py b/dcae-cli/dcae_cli/util/run.py index f0f1309..293c725 100644 --- a/dcae-cli/dcae_cli/util/run.py +++ b/dcae-cli/dcae_cli/util/run.py @@ -130,8 +130,8 @@ def run_component(user, cname, cver, catalog, additional_user, attached, force, Args ---- force: (boolean) - Continue to run even when there are no valid downstream components when - this flag is set to True. + Continue to run even when there are no valid downstream components, + when this flag is set to True. dmaap_map: (dict) config_key to message router or data router connections. Used as a manual way to make available this information for the component. inputs_map: (dict) config_key to value that is intended to be provided at diff --git a/dcae-cli/pom.xml b/dcae-cli/pom.xml index 68a1651..17d0a96 100644 --- a/dcae-cli/pom.xml +++ b/dcae-cli/pom.xml @@ -1,7 +1,7 @@ <?xml version="1.0"?> <!-- ================================================================================ -Copyright (c) 2017 AT&T Intellectual Property. All rights reserved. +Copyright (c) 2017-2018 AT&T Intellectual Property. All rights reserved. ================================================================================ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,7 +28,7 @@ ECOMP is a trademark and service mark of AT&T Intellectual Property. <groupId>org.onap.dcaegen2.platform.cli</groupId> <artifactId>dcae-cli</artifactId> <name>dcaegen2-platform-cli-dcae-cli</name> - <version>2.10.1</version> + <version>2.11.0</version> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |