#!/usr/bin/env python # Copyright (C) 2021 by Samsung Electronics Co., Ltd. # # 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 """Cli application for ONAP DCAE Dashboard for managing DCAE Microservices. Implements core parts of the API defined here: https://git.onap.org/ccsdk/dashboard/tree/ccsdk-app-os/src/main/resources/swagger.json """ import argparse import base64 import json import os import re import sys import time from datetime import datetime import requests import yaml try: from urllib.parse import quote except ImportError: from urllib import pathname2url as quote # Suppress https ignoring warning to be printed on screen # InsecureRequestWarning: Unverified HTTPS request is being made... requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) ROOT_PATH = "/ccsdk-app/nb-api/v2" BLUEPRINTS_URL = ROOT_PATH + "/blueprints" DEPLOYMENTS_URL = ROOT_PATH + "/deployments" EMPTY_CHAR = "-" # Deployment operations (used in executions) DEPLOYMENT_INSTALL = "install" DEPLOYMENT_UNINSTALL = "uninstall" DEPLOYMENT_UPDATE = "update" USER_LOGIN = "su1234" USER_PASSWORD = "fusion" def get_url(postfix): return args.base_url.strip('/') + postfix def read_json_file(file_path): with open(file_path) as f: return json.load(f) def read_yaml_file(file_path): with open(file_path) as f: return yaml.safe_load(f) def print_rows_formatted(matrix): """Prints 2 dimensional array data (matrix) formatted to screen. """ col_width = max(len(word) for row in matrix for word in row) + 2 # padding for row in matrix: print("".join(word.ljust(col_width) for word in row)) print def str_2_date(_str): return datetime.strptime(_str, '%Y-%m-%dT%H:%M:%S.%fZ').strftime('%Y-%m-%d %H:%M:%S') def create_filter(_filter): return quote(json.dumps(_filter)) def http(verb, url, params=None, body=None): print(verb + " to " + url) if args.verbose: if params: print("PARAMS: ") print(params) print if body: print("BODY: ") print(body) print headers = {'Authorization': "Basic " + base64_authorization_value, 'Content-Type': 'application/json', 'Accept': 'application/json'} r = s.request(verb, url, headers=headers, params=params, # accept all server TLS certs verify=False, json=body) if r.status_code != 200 and r.status_code != 202: print("Request params:") print("Headers: " + str(r.request.headers)) print print("Body: " + str(r.request.body)) print raise RuntimeError('Response status code: {} with message: {}' .format(r.status_code, str(r.content))) if args.verbose: print("RESPONSE: ") print(r.json()) print print("SUCCESSFUL " + verb) print return r def list_blueprints(): r = http('GET', get_url(BLUEPRINTS_URL)) total_items = r.json()['totalItems'] items = r.json()['items'] list_headers = ['Blueprint Id', 'Blueprint Name', 'Blueprint version', 'Application/Component/Owner'] data = [list_headers] for bp in items: application = bp.get("application", EMPTY_CHAR) component = bp.get("component", EMPTY_CHAR) row = [bp['typeId'], bp['typeName'], str(bp['typeVersion']), application + '/' + component + '/' + bp['owner']] data.append(row) # Print it print_rows_formatted(data) print("Total " + str(total_items) + " blueprints.") return r def create_blueprint(body): r = http('POST', get_url(BLUEPRINTS_URL), body=body) if "error" in r.json(): err_msg = json.loads(r.json()['error'])["message"] if re.match('^DCAE services of type.*are still running:.*', err_msg): print("Blueprint already exists and cannot update it as there are deployments related to that running. First delete deployments for this blueprint!") else: print(r.json()['error']) sys.exit(1) print("typeId: " + str(r.json()['typeId'])) return r def get_blueprint(blueprint_name): blueprint_filter = create_filter({ "name": blueprint_name }) return check_error(http('GET', get_url(BLUEPRINTS_URL) + "/?filters=" + blueprint_filter)) def delete_blueprint(blueprint_id): return check_error(http('DELETE', get_url(BLUEPRINTS_URL) + "/" + blueprint_id)) def list_deployments(): r = http('GET', get_url(DEPLOYMENTS_URL)) total_items = r.json()['totalItems'] r_json = r.json() list_headers = ['Service Id', 'Created', 'Modified'] data = [list_headers] if "items" in r_json: for dep in r_json["items"]: row = [dep['id'], str_2_date(dep['created_at']), str_2_date(dep['updated_at'])] data.append(row) print_rows_formatted(data) print("Total " + str(total_items) + " deployments.") return r def get_deployment(deployment_id): return check_error(http('GET', get_url(DEPLOYMENTS_URL) + "/" + deployment_id)) def get_deployment_inputs(deployment_id, tenant): return check_error(http('GET', get_url(DEPLOYMENTS_URL) + "/" + deployment_id + "/inputs?tenant=" + tenant)) def create_deployment(body): return check_error(http('POST', get_url(DEPLOYMENTS_URL), body=body)) def update_deployment(deployment_id, body): return check_error(http('PUT', get_url(DEPLOYMENTS_URL) + "/" + deployment_id + "/update", body=body)) def delete_deployment(deployment_id, tenant): return check_error(http('DELETE', get_url(DEPLOYMENTS_URL) + "/" + deployment_id + "?tenant=" + tenant), fail_msg="Cannot delete deployment if install still ongoing.") def executions_status(deployment_id, tenant): return check_error(http('GET', get_url(DEPLOYMENTS_URL) + "/" + deployment_id + "/executions?tenant=" + tenant)) def deployment_exists(deployment_id, print_non_existence=True, fail_it=True): """Checks if deployment with given deployment-id exists. """ r = get_deployment(deployment_id) exists = check_deployment_exists(deployment_id, r.json()) msg = "Given deployment '" + deployment_id + "' " + ("does not exist!" if print_non_existence else "already/still exists!") if bool(print_non_existence) != bool(exists): # Separate checking of deployment existence is needed as API DELETE operation is success even if deployment does not exist. print(msg) if fail_it: sys.exit(1) return exists def check_deployment_exists(deployment_id, deployments): """deployments is the json [{deployment}, ...] payload of get_deployment method """ if not deployments: return False exists = False for dep in deployments: if "id" in dep and dep["id"] == deployment_id: exists = True return exists def check_error(response, fail_it=True, fail_msg=""): if "error" in response.json(): print(response.json()['error']) print(fail_msg) if fail_it: sys.exit(1) return response def print_get_payload(payload): print(json.dumps(payload.json()['items'], indent=2)) def get_executions_items(payload, key, value=None): """Executions array may have e.g. following content [ { "status": "terminated", "tenant_name": "default_tenant", "created_at": "2020-07-16T14:09:34.881Z", "workflow_id": "create_deployment_environment", "deployment_id": "dcae_k8s-datacollector", "id": "fb66d5f7-e957-4c75-bc11-f9ef9e2ae2ac" }, { "status": "failed", "tenant_name": "default_tenant", "created_at": "2020-07-16T14:10:05.933Z", "workflow_id": "install", "deployment_id": "dcae_k8s-datacollector", "id": "75dfe2e9-929a-46d0-9a8d-06e0e435051c" } ] This function returns array of maps filtered (with key and optional value) from the given source executions array. """ executions = payload.json()['items'] results = [] for execution in executions: if key in execution: if value: if execution[key] == value: results.append(execution) else: results.append(execution) return results class Timeout: def __init__(self, timeout, timeout_msg, sleep_time=2): self.counter = 0 self.timeout = timeout self.timeout_msg = timeout_msg self.sleep_time = sleep_time def expired(self): if self.counter > self.timeout: print("Timeout " + str(self.timeout) + " seconds expired while waiting " + self.timeout_msg) return True time.sleep(int(self.sleep_time)) self.counter += int(self.sleep_time) return False def wait_deployment(deployment_id, operation, timeout=240): def get_workflow_id(deployment_id, operation): r = executions_status(deployment_id, args.tenant) print_get_payload(r) print("Get status for operation: %s" % operation) return get_executions_items(r, "workflow_id", operation) failed = False op_timeout = Timeout(timeout, "deployment " + deployment_id + " operation " + operation) while True: wf = get_workflow_id(deployment_id, operation) if wf: status = wf[0]["status"] print("Operation status is %s" % status) if status in ["terminated", "failed"]: result = "SUCCESS" if status == "terminated" else "FAILED" if status == "failed": failed = True break if op_timeout.expired(): failed = True result = "FAILED" break # For uninstall wait executions to be removed by Cloudify as it will bother re-cretion of same deployment # There would be also "workflow_id": "delete_deployment_environment" state we should follow, but that can be disappearing so fast # so better to just wait all executions are removed. if operation == DEPLOYMENT_UNINSTALL and not failed: ex_timeout = Timeout(timeout, "deployment " + deployment_id + " operation " + operation + " executions to be removed.") while True: if not get_workflow_id(deployment_id, operation): if not deployment_exists(args.deployment_id, print_non_existence=False, fail_it=False): # Still wait a moment as deployment-handler may still have the deployment and # would return "409 Conflict" in case of creating again deployment with same name. time.sleep(7) break if ex_timeout.expired(): failed = True result = "FAILED" break print("Deployment " + deployment_id + " operation " + operation + " was " + result) if failed: sys.exit(1) def append_deployment_inputs_key_values(key_values, inputs): pairs = key_values.split(",") for key_value in pairs: key, value = key_value.split("=", 1) inputs[key] = value return inputs def parse_deployment_inputs(deployment_inputs, deployment_inputs_key_value): inputs = {} if deployment_inputs: inputs = read_json_file(deployment_inputs) if deployment_inputs_key_value: inputs = append_deployment_inputs_key_values(deployment_inputs_key_value, inputs) return inputs def parse_args(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter, epilog=''' Example commands: python dcae-cli.py --base_url https://infra:30983 --operation create_blueprint --blueprint_file my-blueprint.yaml python dcae-cli.py --base_url https://infra:30983 --operation create_blueprint --body_file create_blueprint.json --blueprint_file my-blueprint.yaml python dcae-cli.py --base_url https://infra:30983 --operation list_blueprints python dcae-cli.py --base_url https://infra:30983 --operation get_blueprint --blueprint_name my-blueprint python dcae-cli.py --base_url https://infra:30983 --operation delete_blueprint --blueprint_id bf31992b-8643-44ed-b9d1-f6f5a806e505 python dcae-cli.py --base_url https://infra:30983 --operation create_deployment --blueprint_id bf31992b-8643-44ed-b9d1-f6f5a806e505 --deployment_id samuli-testi --deployment_inputs inputs.yaml python dcae-cli.py --base_url https://infra:30983 --operation list_deployments python dcae-cli.py --base_url https://infra:30983 --operation delete_deployment --deployment_id dcae_samuli-testi python dcae-cli.py --base_url https://infra:30983 --operation executions_status --deployment_id dcae_samuli-testi ''') parser.add_argument('-u', '--base_url', required=True, help='Base url of the DCAE Dashboard API (e.g. http://127.0.0.1:30228)') parser.add_argument('-o', '--operation', required=True, choices=['list_blueprints', 'create_blueprint', 'get_blueprint', 'delete_blueprint', 'list_deployments', 'get_deployment', 'get_deployment_inputs', 'get_deployment_input', 'create_deployment', 'update_deployment', 'delete_deployment', 'executions_status'], help='Operation to execute towards DCAE Dashboard') parser.add_argument('-b', '--body_file', help="""File path for the body of the DCAE Dashboard operation. Json format file Given as file path to a file having the main body parameters for blueprint creation as json format. Example: { "typeName": "my-blueprint", # this is the blueprint name "typeVersion": 12345, # this is blueprint version "application": "DCAE-app", # OPTIONAL "component": "dcae-comp", # OPTIONAL "owner": "Samsung Guy" # Blueprint owner } Used for create_blueprint operation. Parameter is optional and by default following values are used: { "typeName": Filename of --blueprint_file parameter "typeVersion": 1, "application": "DCAE", "component": "dcae", "owner": "Samsung" } """) parser.add_argument('-bp', '--blueprint_file', help='File path for the Cloudify Blueprint file used as payload in DCAE Dashboard operation. Yaml format file') parser.add_argument('-id', '--blueprint_id', help='Blueprint Id (typeId) string parameter e.g. for delete_blueprint and create_deployment operations') parser.add_argument('-name', '--blueprint_name', help='Blueprint name string parameter given as "typeName" in --body_file when creating Blueprint. If --body_file is not given blueprint name is by default the name of the given blueprint file.') parser.add_argument('-tag', '--deployment_id', help='''Deployment tag / Service Id / Deployment Ref / Deployment Id. Many names for the identification of the deployment started from the blueprint. Used for create_deployment operation and for delete_deployment. Needs to uniquely identify deployment. This identification is labeled to Kubernetes resources e.g. PODs with key cfydeployment''') parser.add_argument('-prefix', '--deployment_id_prefix', default="samsung", help='Deployment Id Prefix is the optional component name prefixed to cfydeployment with underscore. If not given string "samsung" used by default.') parser.add_argument('-i', '--deployment_inputs', help='''Deployment input parameters for the Coudify blueprint. Given as file path to a file having input parameters as json format. Parameters given depends on the blueprint definition. Example: { "host_port": "30243", "service_id_name": "samsung-ves-rapp", "component_type_name": "samsung-rapp-service" } Used for create_deployment and update_deployment operations. Parameter is optional and by default no input parameters given for the blueprint.''') parser.add_argument('-kv', '--deployment_inputs_key_value', help='''Deployment input parameters for the Coudify blueprint. Same as --deployment_inputs but given on command line parameter with format of key value. key=value,key2=value2 Parameters given depends on the blueprint definition. Example: "host_port=30243,service_id_name=samsung-ves-rapp,component_type_name=samsung-rapp-service" Used for create_deployment and update_deployment operations. Parameter is optional and by default no input parameters given for the blueprint.''') parser.add_argument('-k', '--deployment_input_key', help='''Deployment input parameter key for the Coudify blueprint. Key string used for the deployment input. Used in get_deployment_input operation to identify what input parameter is wanted. ''') parser.add_argument('-t', '--tenant', default='default_tenant', help='''Tenant used for Cloudify. Optional, if not given default value "default_tenant" is used. Used for create_deployment and delete_deployment operations.''') parser.add_argument('-v', '--verbose', action='store_true', help='Output more') args = parser.parse_args() if args.blueprint_id is None and args.operation in ['create_deployment', 'delete_blueprint']: parser.error("--operation " + args.operation + " requires --blueprint_id argument.") if args.blueprint_name is None and args.operation in ['get_blueprint', 'update_deployment']: parser.error("--operation " + args.operation + " requires --blueprint_name argument.") if args.operation == 'create_blueprint' and args.blueprint_file is None: parser.error("--operation create_blueprint requires --blueprint_file arguments. Note also optional --body_file can be given.") if args.deployment_id is None and args.operation in ['create_deployment', 'get_deployment', 'get_deployment_inputs', 'get_deployment_input', 'update_deployment', 'delete_deployment', 'executions_status']: parser.error("--operation " + args.operation + " requires --deployment_id argument.") if (args.deployment_inputs is None and args.deployment_inputs_key_value is None) and args.operation in ['update_deployment']: parser.error("--operation " + args.operation + " requires --deployment_inputs or --deployment_inputs_key_value argument.") if args.deployment_input_key is None and args.operation in ['get_deployment_input']: parser.error("--operation " + args.operation + " requires --deployment_input_key argument.") return parser.parse_args() def get_authorization_value(user_login=USER_LOGIN, user_password=USER_PASSWORD): base64_bytes = base64.b64encode((user_login + ":" + user_password).encode('ascii')) return base64_bytes.decode('ascii') def main(): global args, s, base64_authorization_value args = parse_args() s = requests.Session() base64_authorization_value = get_authorization_value() if args.operation == "list_blueprints": list_blueprints() elif args.operation == "create_blueprint": bp_name = os.path.splitext(os.path.basename(args.blueprint_file))[0] body = { "typeName": bp_name, "typeVersion": 1, "application": "DCAE", "component": "dcae", "owner": USER_LOGIN } if args.body_file: body = read_json_file(args.body_file) blueprint = read_yaml_file(args.blueprint_file) # create/replace blueprint part in body body['blueprintTemplate'] = yaml.dump(blueprint) create_blueprint(body) elif args.operation == "get_blueprint": print_get_payload(get_blueprint(args.blueprint_name)) elif args.operation == "delete_blueprint": delete_blueprint(args.blueprint_id) elif args.operation == "create_deployment": full_deployment_id = args.deployment_id_prefix + "_" + args.deployment_id deployment_exists(full_deployment_id, print_non_existence=False) inputs = parse_deployment_inputs(args.deployment_inputs, args.deployment_inputs_key_value) body = { # component (deployment_id_prefix) will be prefixed to Kubernetes resources # label key cfydeployment with underscore. # E.g. cfydeployment=samsung_ # where is the given args.deployment_id. "component": args.deployment_id_prefix, "tag": args.deployment_id, "blueprintId": args.blueprint_id, "tenant": args.tenant, "inputs": inputs } create_deployment(body) print("DeploymentId: " + full_deployment_id) wait_deployment(full_deployment_id, DEPLOYMENT_INSTALL) elif args.operation == "list_deployments": list_deployments() elif args.operation == "get_deployment": deployment_exists(args.deployment_id) print_get_payload(get_deployment(args.deployment_id)) elif args.operation == "get_deployment_inputs": deployment_exists(args.deployment_id) print_get_payload(get_deployment_inputs(args.deployment_id, args.tenant)) elif args.operation == "get_deployment_input": deployment_exists(args.deployment_id) r = get_deployment_inputs(args.deployment_id, args.tenant) print(r.json()[0]["inputs"][args.deployment_input_key]) elif args.operation == "update_deployment": deployment_exists(args.deployment_id) inputs = parse_deployment_inputs(args.deployment_inputs, args.deployment_inputs_key_value) body = { "component": args.deployment_id_prefix, "tag": args.deployment_id, "blueprintName": args.blueprint_name, "blueprintVersion": 1, "tenant": args.tenant, "inputs": inputs } update_deployment(args.deployment_id, body) wait_deployment(args.deployment_id, DEPLOYMENT_UPDATE) elif args.operation == "delete_deployment": if deployment_exists(args.deployment_id, fail_it=False): delete_deployment(args.deployment_id, args.tenant) wait_deployment(args.deployment_id, DEPLOYMENT_UNINSTALL) elif args.operation == "executions_status": print_get_payload(executions_status(args.deployment_id, args.tenant)) else: print("No operation selected.") sys.exit(1) if __name__ == '__main__': main()