From: garciadeblas Date: Tue, 13 Feb 2018 10:58:29 +0000 (+0100) Subject: Support of new OSM NB API following SOL005 X-Git-Url: https://osm.etsi.org/gitweb/?a=commitdiff_plain;h=c508429478c61e528ec6f5ddd4af8a1958add122;p=osm%2Fosmclient.git Support of new OSM NB API following SOL005 Change-Id: I24c990dca544410cf4f9273f56b39250cb44bcfa Signed-off-by: garciadeblas --- diff --git a/osmclient/client.py b/osmclient/client.py index 1016608..9692225 100644 --- a/osmclient/client.py +++ b/osmclient/client.py @@ -1,4 +1,5 @@ -# Copyright 2017 Sandvine +# Copyright 2017-2018 Sandvine +# Copyright 2018 Telefonica # # All Rights Reserved. # @@ -18,11 +19,18 @@ OSM client entry point """ -from osmclient.v1 import client +from osmclient.v1 import client as client +from osmclient.sol005 import client as sol005client - -def Client(version=1, host=None, *args, **kwargs): - if version == 1: - return client.Client(host, *args, **kwargs) +def Client(version=1, host=None, sol005=False, *args, **kwargs): + if not sol005: + if version == 1: + return client.Client(host, *args, **kwargs) + else: + raise Exception("Unsupported client version") else: - raise Exception("Unsupported client version") + if version == 1: + return sol005client.Client(host, *args, **kwargs) + else: + raise Exception("Unsupported client version") + diff --git a/osmclient/common/utils.py b/osmclient/common/utils.py index 7228e5c..a3f4bc8 100644 --- a/osmclient/common/utils.py +++ b/osmclient/common/utils.py @@ -15,7 +15,11 @@ # under the License. import time - +from uuid import UUID +import hashlib +import tarfile +import re +import yaml def wait_for_value(func, result=True, wait_time=10, catch_exception=None): maxtime = time.time() + wait_time @@ -30,3 +34,57 @@ def wait_for_value(func, result=True, wait_time=10, catch_exception=None): return func() == result except catch_exception: return False + + +def validate_uuid4(uuid_text): + try: + UUID(uuid_text) + return True + except (ValueError, TypeError): + return False + + +def md5(fname): + hash_md5 = hashlib.md5() + with open(fname, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_md5.update(chunk) + return hash_md5.hexdigest() + + +def get_key_val_from_pkg(descriptor_file): +# method opens up a package and finds the name of the resulting +# descriptor (vnfd or nsd name) + tar = tarfile.open(descriptor_file) + yamlfile = None + for member in tar.getmembers(): + if (re.match('.*.yaml', member.name) and + len(member.name.split('/')) == 2): + yamlfile = member.name + break + if yamlfile is None: + return None + + dict = yaml.load(tar.extractfile(yamlfile)) + result = {} + for k1, v1 in dict.items(): + if not k1.endswith('-catalog'): + continue + for k2, v2 in v1.items(): + if not k2.endswith('nsd') and not k2.endswith('vnfd'): + continue + + if 'nsd' in k2: + result['type'] = 'nsd' + else: + result['type'] = 'vnfd' + + for entry in v2: + for k3, v3 in entry.items(): + # strip off preceeding chars before : + key_name = k3.split(':').pop() + + result[key_name] = v3 + tar.close() + return result + diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index d0d78f5..c1a7f18 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -1,4 +1,5 @@ -# Copyright 2017 Sandvine +# Copyright 2017-2018 Sandvine +# Copyright 2018 Telefonica # # All Rights Reserved. # @@ -18,12 +19,29 @@ OSM shell/cli """ import click -from osmclient.client import client +from osmclient import client from osmclient.common.exceptions import ClientException from prettytable import PrettyTable +import yaml import json import time +def check_client_version(obj, what, version='sol005'): + ''' + Checks the version of the client object and raises error if it not the expected. + + :param obj: the client object + :what: the function or command under evaluation (used when an error is raised) + :return: - + :raises ClientError: if the specified version does not match the client version + ''' + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + message = 'the following commands or options are only supported with the option "--sol005": {}'.format(what) + if version == 'v1': + message = 'the following commands or options are not supported when using option "--sol005": {}'.format(what) + if fullclassname != 'osmclient.{}.client.Client'.format(version): + raise ClientException(message) + return @click.group() @click.option('--hostname', @@ -32,12 +50,12 @@ import time help='hostname of server. ' + 'Also can set OSM_HOSTNAME in environment') @click.option('--so-port', - default=8008, + default=None, envvar='OSM_SO_PORT', help='hostname of server. ' + 'Also can set OSM_SO_PORT in environment') @click.option('--so-project', - default='default', + default=None, envvar='OSM_SO_PROJECT', help='Project Name in SO. ' + 'Also can set OSM_SO_PROJECT in environment') @@ -51,67 +69,150 @@ import time envvar='OSM_RO_PORT', help='hostname of RO server. ' + 'Also can set OSM_RO_PORT in environment') +@click.option('--sol005', + is_flag=True, + envvar='OSM_SOL005', + help='Use ETSI NFV SOL005 API') @click.pass_context -def cli(ctx, hostname, so_port, so_project, ro_hostname, ro_port): +def cli(ctx, hostname, so_port, so_project, ro_hostname, ro_port, sol005): if hostname is None: print( "either hostname option or OSM_HOSTNAME " + "environment variable needs to be specified") exit(1) - ctx.obj = client.Client( - host=hostname, - so_port=so_port, - so_project=so_project, - ro_host=ro_hostname, - ro_port=ro_port) - + kwargs={} + if so_port is not None: + kwargs['so_port']=so_port + if so_project is not None: + kwargs['so_project']=so_project + if ro_hostname is not None: + kwargs['ro_host']=ro_hostname + if ro_port is not None: + kwargs['ro_port']=ro_port + + ctx.obj = client.Client(host=hostname, sol005=sol005, **kwargs) + + +#################### +# LIST operations +#################### @cli.command(name='ns-list') +@click.option('--filter', default=None, + help='restricts the list to the NS instances matching the filter') @click.pass_context -def ns_list(ctx): - resp = ctx.obj.ns.list() +def ns_list(ctx, filter): + '''list all NS instances''' + if filter: + check_client_version(ctx.obj, '--filter option') + resp = ctx.obj.ns.list(filter) + else: + resp = ctx.obj.ns.list() table = PrettyTable( ['ns instance name', 'id', 'operational status', 'config status']) for ns in resp: - nsopdata = ctx.obj.ns.get_opdata(ns['id']) - nsr = nsopdata['nsr:nsr'] + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname == 'osmclient.sol005.client.Client': + nsr = ns + else: + nsopdata = ctx.obj.ns.get_opdata(ns['id']) + nsr = nsopdata['nsr:nsr'] + opstatus = nsr['operational-status'] if 'operational-status' in nsr else 'Not found' + configstatus = nsr['config-status'] if 'config-status' in nsr else 'Not found' + if configstatus == "config_not_needed": + configstatus = "configured (no charms)" table.add_row( - [nsr['name-ref'], - nsr['ns-instance-config-ref'], - nsr['operational-status'], - nsr['config-status']]) + [nsr['name'], + nsr['_id'], + opstatus, + configstatus]) table.align = 'l' print(table) -@cli.command(name='nsd-list') -@click.pass_context -def nsd_list(ctx): - resp = ctx.obj.nsd.list() +def nsd_list(ctx, filter): + if filter: + check_client_version(ctx.obj, '--filter') + resp = ctx.obj.nsd.list(filter) + else: + resp = ctx.obj.nsd.list() + #print yaml.safe_dump(resp) table = PrettyTable(['nsd name', 'id']) - for ns in resp: - table.add_row([ns['name'], ns['id']]) + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname == 'osmclient.sol005.client.Client': + for ns in resp: + name = ns['name'] if 'name' in ns else '-' + table.add_row([name, ns['_id']]) + else: + for ns in resp: + table.add_row([ns['name'], ns['id']]) table.align = 'l' print(table) -@cli.command(name='vnfd-list') +@cli.command(name='nsd-list') +@click.option('--filter', default=None, + help='restricts the list to the NSD/NSpkg matching the filter') @click.pass_context -def vnfd_list(ctx): - resp = ctx.obj.vnfd.list() +def nsd_list1(ctx, filter): + '''list all NSD/NSpkg in the system''' + nsd_list(ctx,filter) + + +@cli.command(name='nspkg-list') +@click.option('--filter', default=None, + help='restricts the list to the NSD/NSpkg matching the filter') +@click.pass_context +def nsd_list2(ctx, filter): + '''list all NSD/NSpkg in the system''' + nsd_list(ctx,filter) + + +def vnfd_list(ctx, filter): + if filter: + check_client_version(ctx.obj, '--filter') + resp = ctx.obj.vnfd.list(filter) + else: + resp = ctx.obj.vnfd.list() + #print yaml.safe_dump(resp) table = PrettyTable(['vnfd name', 'id']) - for vnfd in resp: - table.add_row([vnfd['name'], vnfd['id']]) + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname == 'osmclient.sol005.client.Client': + for vnfd in resp: + name = vnfd['name'] if 'name' in vnfd else '-' + table.add_row([name, vnfd['_id']]) + else: + for vnfd in resp: + table.add_row([vnfd['name'], vnfd['id']]) table.align = 'l' print(table) +@cli.command(name='vnfd-list') +@click.option('--filter', default=None, + help='restricts the list to the VNFD/VNFpkg matching the filter') +@click.pass_context +def vnfd_list1(ctx, filter): + '''list all VNFD/VNFpkg in the system''' + vnfd_list(ctx,filter) + + +@cli.command(name='vnfpkg-list') +@click.option('--filter', default=None, + help='restricts the list to the VNFD/VNFpkg matching the filter') +@click.pass_context +def vnfd_list2(ctx, filter): + '''list all VNFD/VNFpkg in the system''' + vnfd_list(ctx,filter) + + @cli.command(name='vnf-list') @click.pass_context def vnf_list(ctx): + ''' list all VNF instances''' resp = ctx.obj.vnf.list() table = PrettyTable( ['vnf name', @@ -131,17 +232,160 @@ def vnf_list(ctx): print(table) -@cli.command(name='vnf-show') -@click.argument('vnf_name') +#################### +# SHOW operations +#################### + +def nsd_show(ctx, name, literal): + try: + resp = ctx.obj.nsd.get(name) + #resp = ctx.obj.nsd.get_individual(name) + except ClientException as inst: + print(inst.message) + exit(1) + + if literal: + print yaml.safe_dump(resp) + return + + table = PrettyTable(['field', 'value']) + for k, v in resp.items(): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + +@cli.command(name='nsd-show', short_help='shows the content of a NSD') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def nsd_show1(ctx, name, literal): + '''shows the content of a NSD + + NAME: name or ID of the NSD/NSpkg + ''' + nsd_show(ctx, name, literal) + + +@cli.command(name='nspkg-show', short_help='shows the content of a NSD') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def nsd_show2(ctx, name, literal): + '''shows the content of a NSD + + NAME: name or ID of the NSD/NSpkg + ''' + nsd_show(ctx, name, literal) + + +def vnfd_show(ctx, name, literal): + try: + resp = ctx.obj.vnfd.get(name) + #resp = ctx.obj.vnfd.get_individual(name) + except ClientException as inst: + print(inst.message) + exit(1) + + if literal: + print yaml.safe_dump(resp) + return + + table = PrettyTable(['field', 'value']) + for k, v in resp.items(): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + +@cli.command(name='vnfd-show', short_help='shows the content of a VNFD') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def vnfd_show1(ctx, name, literal): + '''shows the content of a VNFD + + NAME: name or ID of the VNFD/VNFpkg + ''' + vnfd_show(ctx, name, literal) + + +@cli.command(name='vnfpkg-show', short_help='shows the content of a VNFD') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def vnfd_show2(ctx, name, literal): + '''shows the content of a VNFD + + NAME: name or ID of the VNFD/VNFpkg + ''' + vnfd_show(ctx, name, literal) + + +@cli.command(name='ns-show', short_help='shows the info of a NS instance') +@click.argument('name') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--filter', default=None) +@click.pass_context +def ns_show(ctx, name, literal, filter): + '''shows the info of a NS instance + + NAME: name or ID of the NS instance + ''' + try: + ns = ctx.obj.ns.get(name) + except ClientException as inst: + print(inst.message) + exit(1) + + if literal: + print yaml.safe_dump(resp) + return + + table = PrettyTable(['field', 'value']) + + for k, v in ns.items(): + if filter is None or filter in k: + table.add_row([k, json.dumps(v, indent=2)]) + + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname != 'osmclient.sol005.client.Client': + nsopdata = ctx.obj.ns.get_opdata(ns['id']) + nsr_optdata = nsopdata['nsr:nsr'] + for k, v in nsr_optdata.items(): + if filter is None or filter in k: + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + +@cli.command(name='vnf-show', short_help='shows the info of a VNF instance') +@click.argument('name') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') @click.option('--filter', default=None) @click.pass_context -def vnf_show(ctx, vnf_name, filter): +def vnf_show(ctx, name, literal, filter): + '''shows the info of a VNF instance + + NAME: name or ID of the VNF instance + ''' try: - resp = ctx.obj.vnf.get(vnf_name) + check_client_version(ctx.obj, ctx.command.name, 'v1') + resp = ctx.obj.vnf.get(name) except ClientException as inst: print(inst.message) exit(1) + if literal: + print yaml.safe_dump(resp) + return + table = PrettyTable(['field', 'value']) for k, v in resp.items(): if filter is None or filter in k: @@ -155,6 +399,7 @@ def vnf_show(ctx, vnf_name, filter): @click.pass_context def vnf_monitoring_show(ctx, vnf_name): try: + check_client_version(ctx.obj, ctx.command.name, 'v1') resp = ctx.obj.vnf.get_monitoring(vnf_name) except ClientException as inst: print(inst.message) @@ -177,6 +422,7 @@ def vnf_monitoring_show(ctx, vnf_name): @click.pass_context def ns_monitoring_show(ctx, ns_name): try: + check_client_version(ctx.obj, ctx.command.name, 'v1') resp = ctx.obj.ns.get_monitoring(ns_name) except ClientException as inst: print(inst.message) @@ -194,6 +440,80 @@ def ns_monitoring_show(ctx, ns_name): print(table) +#################### +# CREATE operations +#################### + +def nsd_create(ctx, filename, overwrite): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsd.create(filename, overwrite) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='nsd-create', short_help='creates a new NSD/NSpkg') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in NSD') +@click.pass_context +def nsd_create1(ctx, filename, overwrite): + '''creates a new NSD/NSpkg + + FILENAME: NSD yaml file or NSpkg tar.gz file + ''' + nsd_create(ctx, filename, overwrite) + + +@cli.command(name='nspkg-create', short_help='creates a new NSD/NSpkg') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in NSD') +@click.pass_context +def nsd_create2(ctx, filename, overwrite): + '''creates a new NSD/NSpkg + + FILENAME: NSD yaml file or NSpkg tar.gz file + ''' + nsd_create(ctx, filename, overwrite) + + +def vnfd_create(ctx, filename, overwrite): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.vnfd.create(filename, overwrite) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='vnfd-create', short_help='creates a new VNFD/VNFpkg') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in VNFD') +@click.pass_context +def vnfd_create1(ctx, filename, overwrite): + '''creates a new VNFD/VNFpkg + + FILENAME: VNFD yaml file or VNFpkg tar.gz file + ''' + vnfd_create(ctx, filename, overwrite) + + +@cli.command(name='vnfpkg-create', short_help='creates a new VNFD/VNFpkg') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in VNFD') +@click.pass_context +def vnfd_create2(ctx, filename, overwrite): + '''creates a new VNFD/VNFpkg + + FILENAME: VNFD yaml file or VNFpkg tar.gz file + ''' + vnfd_create(ctx, filename, overwrite) + + @cli.command(name='ns-create') @click.option('--ns_name', prompt=True) @@ -218,7 +538,10 @@ def ns_create(ctx, admin_status, ssh_keys, config): + '''creates a new NS instance''' try: + if config: + check_client_version(ctx.obj, '--config', 'v1') ctx.obj.ns.create( nsd_name, ns_name, @@ -230,165 +553,162 @@ def ns_create(ctx, exit(1) -@cli.command(name='ns-delete') -@click.argument('ns_name') -@click.pass_context -def ns_delete(ctx, ns_name): +#################### +# UPDATE operations +#################### + +def nsd_update(ctx, name, content): try: - ctx.obj.ns.delete(ns_name) + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsd.update(name, content) except ClientException as inst: print(inst.message) exit(1) - -@cli.command(name='upload-package') -@click.argument('filename') +@cli.command(name='nsd-update', short_help='updates a NSD/NSpkg') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the NSD/NSpkg replacing the current one') @click.pass_context -def upload_package(ctx, filename): - try: - ctx.obj.package.upload(filename) - ctx.obj.package.wait_for_upload(filename) - except ClientException as inst: - print(inst.message) - exit(1) +def nsd_update1(ctx, name, content): + '''updates a NSD/NSpkg + NAME: name or ID of the NSD/NSpkg + ''' + nsd_update(ctx, name, content) -@cli.command(name='ns-show') -@click.argument('ns_name') -@click.option('--filter', default=None) + +@cli.command(name='nspkg-update', short_help='updates a NSD/NSpkg') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the NSD/NSpkg replacing the current one') @click.pass_context -def ns_show(ctx, ns_name, filter): +def nsd_update2(ctx, name, content): + '''updates a NSD/NSpkg + + NAME: name or ID of the NSD/NSpkg + ''' + nsd_update(ctx, name, content) + + +def vnfd_update(ctx, name, content): try: - ns = ctx.obj.ns.get(ns_name) + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.vnfd.update(name, content) except ClientException as inst: print(inst.message) exit(1) - table = PrettyTable(['field', 'value']) - for k, v in ns.items(): - if filter is None or filter in k: - table.add_row([k, json.dumps(v, indent=2)]) +@cli.command(name='vnfd-update', short_help='updates a new VNFD/VNFpkg') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the VNFD/VNFpkg replacing the current one') +@click.pass_context +def vnfd_update1(ctx, name, content): + '''updates a VNFD/VNFpkg - nsopdata = ctx.obj.ns.get_opdata(ns['id']) - nsr_optdata = nsopdata['nsr:nsr'] - for k, v in nsr_optdata.items(): - if filter is None or filter in k: - table.add_row([k, json.dumps(v, indent=2)]) - table.align = 'l' - print(table) + NAME: name or ID of the VNFD/VNFpkg + ''' + vnfd_update(ctx, name, content) -@cli.command(name='ns-scaling-show') -@click.argument('ns_name') +@cli.command(name='vnfpkg-update', short_help='updates a VNFD/VNFpkg') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the VNFD/VNFpkg replacing the current one') @click.pass_context -def show_ns_scaling(ctx, ns_name): - resp = ctx.obj.ns.list() - - table = PrettyTable( - ['group-name', - 'instance-id', - 'operational status', - 'create-time', - 'vnfr ids']) - - for ns in resp: - if ns_name == ns['name']: - nsopdata = ctx.obj.ns.get_opdata(ns['id']) - scaling_records = nsopdata['nsr:nsr']['scaling-group-record'] - for record in scaling_records: - if 'instance' in record: - instances = record['instance'] - for inst in instances: - table.add_row( - [record['scaling-group-name-ref'], - inst['instance-id'], - inst['op-status'], - time.strftime('%Y-%m-%d %H:%M:%S', - time.localtime( - inst['create-time'])), - inst['vnfrs']]) - table.align = 'l' - print(table) +def vnfd_update2(ctx, name, content): + '''updates a VNFD/VNFpkg + NAME: VNFD yaml file or VNFpkg tar.gz file + ''' + vnfd_update(ctx, name, content) -@cli.command(name='ns-scale') -@click.argument('ns_name') -@click.option('--ns_scale_group', prompt=True) -@click.option('--index', prompt=True) -@click.pass_context -def ns_scale(ctx, ns_name, ns_scale_group, index): - ctx.obj.ns.scale(ns_name, ns_scale_group, index) +#################### +# DELETE operations +#################### -@cli.command(name='nsd-delete') -@click.argument('nsd_name') -@click.pass_context -def nsd_delete(ctx, nsd_name): +def nsd_delete(ctx, name): try: - ctx.obj.nsd.delete(nsd_name) + ctx.obj.nsd.delete(name) except ClientException as inst: print(inst.message) exit(1) -@cli.command(name='vnfd-delete') -@click.argument('vnfd_name') +@cli.command(name='nsd-delete', short_help='deletes a NSD/NSpkg') +@click.argument('name') @click.pass_context -def vnfd_delete(ctx, vnfd_name): +def nsd_delete1(ctx, name): + '''deletes a NSD/NSpkg + + NAME: name or ID of the NSD/NSpkg to be deleted + ''' + nsd_delete(ctx, name) + + +@cli.command(name='nspkg-delete', short_help='deletes a NSD/NSpkg') +@click.argument('name') +@click.pass_context +def nsd_delete2(ctx, name): + '''deletes a NSD/NSpkg + + NAME: name or ID of the NSD/NSpkg to be deleted + ''' + nsd_delete(ctx, name) + + +def vnfd_delete(ctx, name): try: - ctx.obj.vnfd.delete(vnfd_name) + ctx.obj.vnfd.delete(name) except ClientException as inst: print(inst.message) exit(1) -@cli.command(name='config-agent-list') +@cli.command(name='vnfd-delete', short_help='deletes a VNFD/VNFpkg') +@click.argument('name') @click.pass_context -def config_agent_list(ctx): - table = PrettyTable(['name', 'account-type', 'details']) - for account in ctx.obj.vca.list(): - table.add_row( - [account['name'], - account['account-type'], - account['juju']]) - table.align = 'l' - print(table) +def vnfd_delete1(ctx, name): + '''deletes a VNFD/VNFpkg + NAME: name or ID of the VNFD/VNFpkg to be deleted + ''' + vnfd_delete(ctx, name) -@cli.command(name='config-agent-delete') + +@cli.command(name='vnfpkg-delete', short_help='deletes a VNFD/VNFpkg') @click.argument('name') @click.pass_context -def config_agent_delete(ctx, name): - try: - ctx.obj.vca.delete(name) - except ClientException as inst: - print(inst.message) - exit(1) +def vnfd_delete2(ctx, name): + '''deletes a VNFD/VNFpkg + NAME: name or ID of the VNFD/VNFpkg to be deleted + ''' + vnfd_delete(ctx, name) -@cli.command(name='config-agent-add') -@click.option('--name', - prompt=True) -@click.option('--account_type', - prompt=True) -@click.option('--server', - prompt=True) -@click.option('--user', - prompt=True) -@click.option('--secret', - prompt=True, - hide_input=True, - confirmation_prompt=True) + +@cli.command(name='ns-delete', short_help='deletes a NS instance') +@click.argument('name') @click.pass_context -def config_agent_add(ctx, name, account_type, server, user, secret): +def ns_delete(ctx, name): + '''deletes a NS instance + + NAME: name or ID of the NS instance to be deleted + ''' try: - ctx.obj.vca.create(name, account_type, server, user, secret) + ctx.obj.ns.delete(name) except ClientException as inst: print(inst.message) exit(1) +#################### +# VIM operations +#################### + @cli.command(name='vim-create') @click.option('--name', prompt=True, @@ -403,10 +723,10 @@ def config_agent_add(ctx, name, account_type, server, user, secret): help='VIM password') @click.option('--auth_url', prompt=True, - help='VIM connector url') + help='VIM url') @click.option('--tenant', prompt=True, - help='tenant name') + help='VIM tenant name') @click.option('--config', default=None, help='VIM specific config parameters') @@ -414,7 +734,8 @@ def config_agent_add(ctx, name, account_type, server, user, secret): default='openstack', help='VIM type') @click.option('--description', - default='no description') + default='no description', + help='human readable description') @click.pass_context def vim_create(ctx, name, @@ -425,6 +746,8 @@ def vim_create(ctx, config, account_type, description): + '''creates a new VIM account + ''' vim = {} vim['vim-username'] = user vim['vim-password'] = password @@ -440,10 +763,57 @@ def vim_create(ctx, exit(1) +@cli.command(name='vim-update', short_help='updates a VIM account') +@click.argument('name') +@click.option('--newname', default=None, help='New name for the VIM account') +@click.option('--user', default=None, help='VIM username') +@click.option('--password', default=None, help='VIM password') +@click.option('--auth_url', default=None, help='VIM url') +@click.option('--tenant', default=None, help='VIM tenant name') +@click.option('--config', default=None, help='VIM specific config parameters') +@click.option('--account_type', default=None, help='VIM type') +@click.option('--description', default=None, help='human readable description') +@click.pass_context +def vim_update(ctx, + name, + newname, + user, + password, + auth_url, + tenant, + config, + account_type, + description): + '''updates a VIM account + + NAME: name or ID of the VIM account + ''' + vim = {} + if newname: + vim['name'] = newname + vim['vim_user'] = user + vim['vim_password'] = password + vim['vim_url'] = auth_url + vim['vim-tenant-name'] = tenant + vim['config'] = config + vim['vim_type'] = account_type + vim['description'] = description + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.vim.update(name, vim) + except ClientException as inst: + print(inst.message) + exit(1) + + @cli.command(name='vim-delete') @click.argument('name') @click.pass_context def vim_delete(ctx, name): + '''deletes a VIM account + + NAME: name or ID of the VIM account to be deleted + ''' try: ctx.obj.vim.delete(name) except ClientException as inst: @@ -455,9 +825,20 @@ def vim_delete(ctx, name): @click.option('--ro_update/--no_ro_update', default=False, help='update list from RO') +@click.option('--filter', default=None, + help='restricts the list to the VIM accounts matching the filter') @click.pass_context -def vim_list(ctx, ro_update): - resp = ctx.obj.vim.list(ro_update) +def vim_list(ctx, ro_update, filter): + '''list all VIM accounts''' + if filter: + check_client_version(ctx.obj, '--filter') + if ro_update: + check_client_version(ctx.obj, '--ro_update', 'v1') + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname == 'osmclient.sol005.client.Client': + resp = ctx.obj.vim.list(filter) + else: + resp = ctx.obj.vim.list(ro_update) table = PrettyTable(['vim name', 'uuid']) for vim in resp: table.add_row([vim['name'], vim['uuid']]) @@ -469,6 +850,10 @@ def vim_list(ctx, ro_update): @click.argument('name') @click.pass_context def vim_show(ctx, name): + '''shows the details of a VIM account + + NAME: name or ID of the VIM account + ''' try: resp = ctx.obj.vim.get(name) except ClientException as inst: @@ -482,9 +867,124 @@ def vim_show(ctx, name): print(table) +#################### +# Other operations +#################### + +@cli.command(name='upload-package') +@click.argument('filename') +@click.pass_context +def upload_package(ctx, filename): + '''uploads a VNF package or NS package + + FILENAME: VNF or NS package file (tar.gz) + ''' + try: + ctx.obj.package.upload(filename) + fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ + if fullclassname != 'osmclient.sol005.client.Client': + ctx.obj.package.wait_for_upload(filename) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='ns-scaling-show') +@click.argument('ns_name') +@click.pass_context +def show_ns_scaling(ctx, ns_name): + check_client_version(ctx.obj, ctx.command.name, 'v1') + resp = ctx.obj.ns.list() + + table = PrettyTable( + ['group-name', + 'instance-id', + 'operational status', + 'create-time', + 'vnfr ids']) + + for ns in resp: + if ns_name == ns['name']: + nsopdata = ctx.obj.ns.get_opdata(ns['id']) + scaling_records = nsopdata['nsr:nsr']['scaling-group-record'] + for record in scaling_records: + if 'instance' in record: + instances = record['instance'] + for inst in instances: + table.add_row( + [record['scaling-group-name-ref'], + inst['instance-id'], + inst['op-status'], + time.strftime('%Y-%m-%d %H:%M:%S', + time.localtime( + inst['create-time'])), + inst['vnfrs']]) + table.align = 'l' + print(table) + + +@cli.command(name='ns-scale') +@click.argument('ns_name') +@click.option('--ns_scale_group', prompt=True) +@click.option('--index', prompt=True) +@click.pass_context +def ns_scale(ctx, ns_name, ns_scale_group, index): + check_client_version(ctx.obj, ctx.command.name, 'v1') + ctx.obj.ns.scale(ns_name, ns_scale_group, index) + + +@cli.command(name='config-agent-list') +@click.pass_context +def config_agent_list(ctx): + check_client_version(ctx.obj, ctx.command.name, 'v1') + table = PrettyTable(['name', 'account-type', 'details']) + for account in ctx.obj.vca.list(): + table.add_row( + [account['name'], + account['account-type'], + account['juju']]) + table.align = 'l' + print(table) + + +@cli.command(name='config-agent-delete') +@click.argument('name') +@click.pass_context +def config_agent_delete(ctx, name): + try: + check_client_version(ctx.obj, ctx.command.name, 'v1') + ctx.obj.vca.delete(name) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='config-agent-add') +@click.option('--name', + prompt=True) +@click.option('--account_type', + prompt=True) +@click.option('--server', + prompt=True) +@click.option('--user', + prompt=True) +@click.option('--secret', + prompt=True, + hide_input=True, + confirmation_prompt=True) +@click.pass_context +def config_agent_add(ctx, name, account_type, server, user, secret): + try: + check_client_version(ctx.obj, ctx.command.name, 'v1') + ctx.obj.vca.create(name, account_type, server, user, secret) + except ClientException as inst: + print(inst.message) + exit(1) + @cli.command(name='ro-dump') @click.pass_context def ro_dump(ctx): + check_client_version(ctx.obj, ctx.command.name, 'v1') resp = ctx.obj.vim.get_resource_orchestrator() table = PrettyTable(['key', 'attribute']) for k, v in resp.items(): @@ -496,6 +996,7 @@ def ro_dump(ctx): @cli.command(name='vcs-list') @click.pass_context def vcs_list(ctx): + check_client_version(ctx.obj, ctx.command.name, 'v1') resp = ctx.obj.utils.get_vcs_info() table = PrettyTable(['component name', 'state']) for component in resp: diff --git a/osmclient/sol005/__init__.py b/osmclient/sol005/__init__.py new file mode 100644 index 0000000..857e489 --- /dev/null +++ b/osmclient/sol005/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2018 Telefonica +# +# 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. diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py new file mode 100644 index 0000000..ac35d2f --- /dev/null +++ b/osmclient/sol005/client.py @@ -0,0 +1,102 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM SOL005 client API +""" + +#from osmclient.v1 import vnf +#from osmclient.v1 import ns +#from osmclient.v1 import vim +#from osmclient.v1 import vca +from osmclient.sol005 import vnfd +from osmclient.sol005 import nsd +from osmclient.sol005 import ns +from osmclient.sol005 import vim +from osmclient.sol005 import package +from osmclient.sol005 import http +from osmclient.common.exceptions import ClientException + +class Client(object): + + def __init__( + self, + host=None, + so_port=9999, + so_project='admin', + ro_host=None, + ro_port=9090, + **kwargs): + + self._user = 'admin' + self._password = 'admin' + #self._project = so_project + self._project = 'admin' + self._auth_endpoint = '/admin/v1/tokens' + self._headers = {} + + if len(host.split(':')) > 1: + # backwards compatible, port provided as part of host + self._host = host.split(':')[0] + self._so_port = host.split(':')[1] + else: + self._host = host + self._so_port = so_port + + if ro_host is None: + ro_host = host + ro_http_client = http.Http('http://{}:{}/'.format(ro_host, ro_port)) + ro_http_client.set_http_header( + ['Accept: application/vnd.yand.data+json', + 'Content-Type: application/json']) + + self._http_client = http.Http( + 'https://{}:{}/osm'.format(self._host,self._so_port)) + self._headers['Accept'] = 'application/json' + self._headers['Content-Type'] = 'application/yaml' + http_header = ['{}: {}'.format(key,val) + for (key,val) in self._headers.items()] + self._http_client.set_http_header(http_header) + + token = self.get_token() + if not token: + raise ClientException( + 'Authentication error: not possible to get auth token') + self._headers['Authorization'] = 'Bearer {}'.format(token) + http_header.append('Authorization: Bearer {}'.format(token)) + self._http_client.set_http_header(http_header) + + self.vnfd = vnfd.Vnfd(self._http_client, client=self) + self.nsd = nsd.Nsd(self._http_client, client=self) + self.package = package.Package(self._http_client, client=self) + self.ns = ns.Ns(self._http_client, client=self) + self.vim = vim.Vim(self._http_client, client=self) + ''' + self.vnf = vnf.Vnf(http_client, client=self, **kwargs) + self.vca = vca.Vca(http_client, client=self, **kwargs) + self.utils = utils.Utils(http_client, **kwargs) + ''' + + def get_token(self): + postfields_dict = {'username': self._user, + 'password': self._password, + 'project-id': self._project} + token = self._http_client.post_cmd(endpoint=self._auth_endpoint, + postfields_dict=postfields_dict) + if token is not None: + return token['_id'] + return None + diff --git a/osmclient/sol005/http.py b/osmclient/sol005/http.py new file mode 100644 index 0000000..11ddca6 --- /dev/null +++ b/osmclient/sol005/http.py @@ -0,0 +1,130 @@ +# Copyright 2018 Telefonica +# +# 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. + +from io import BytesIO +import pycurl +import json +import yaml +from osmclient.common import http +from osmclient.common.exceptions import ClientException + +class Http(http.Http): + + def __init__(self, url, user='admin', password='admin'): + self._url = url + self._user = user + self._password = password + self._http_header = None + + def _get_curl_cmd(self, endpoint): + curl_cmd = pycurl.Curl() + #print self._url + endpoint + curl_cmd.setopt(pycurl.URL, self._url + endpoint) + curl_cmd.setopt(pycurl.SSL_VERIFYPEER, 0) + curl_cmd.setopt(pycurl.SSL_VERIFYHOST, 0) + if self._http_header: + curl_cmd.setopt(pycurl.HTTPHEADER, self._http_header) + return curl_cmd + + def delete_cmd(self, endpoint): + data = BytesIO() + curl_cmd = self._get_curl_cmd(endpoint) + curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE") + curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + curl_cmd.perform() + http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + #print 'HTTP_CODE: {}'.format(http_code) + curl_cmd.close() + if http_code == 204: + return None + elif http_code == 404: + if data.getvalue(): + return json.loads(data.getvalue().decode()) + else: + return "NOT FOUND" + if data.getvalue(): + return json.loads(data.getvalue().decode()) + return "Failed" + + def send_cmd(self, endpoint='', postfields_dict=None, + formfile=None, filename=None, + put_method=False): + data = BytesIO() + curl_cmd = self._get_curl_cmd(endpoint) + if put_method: + curl_cmd.setopt(pycurl.PUT, 1) + else: + curl_cmd.setopt(pycurl.POST, 1) + curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + + if postfields_dict is not None: + jsondata = json.dumps(postfields_dict) + curl_cmd.setopt(pycurl.POSTFIELDS, jsondata) + elif formfile is not None: + curl_cmd.setopt( + pycurl.HTTPPOST, + [((formfile[0], + (pycurl.FORM_FILE, + formfile[1])))]) + elif filename is not None: + with open(filename, 'r') as stream: + postdata=stream.read() + curl_cmd.setopt(pycurl.POSTFIELDS, postdata) + + curl_cmd.perform() + http_code = curl_cmd.getinfo(pycurl.HTTP_CODE) + curl_cmd.close() + if http_code not in (200, 201, 202, 204): + raise ClientException(data.getvalue().decode()) + if postfields_dict is not None: + if data.getvalue(): + return json.loads(data.getvalue().decode()) + return None + elif formfile is not None: + if data.getvalue(): + return yaml.safe_load(data.getvalue().decode()) + return None + elif filename is not None: + if data.getvalue(): + return yaml.safe_load(data.getvalue().decode()) + return None + return None + + def post_cmd(self, endpoint='', postfields_dict=None, + formfile=None, filename=None): + return self.send_cmd(endpoint=endpoint, + postfields_dict=postfields_dict, + formfile=formfile, + filename=filename, + put_method=False) + + def put_cmd(self, endpoint='', postfields_dict=None, + formfile=None, filename=None): + return self.send_cmd(endpoint=endpoint, + postfields_dict=postfields_dict, + formfile=formfile, + filename=filename, + put_method=True) + + def get2_cmd(self, endpoint): + data = BytesIO() + curl_cmd = self._get_curl_cmd(endpoint) + curl_cmd.setopt(pycurl.HTTPGET, 1) + curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write) + curl_cmd.perform() + curl_cmd.close() + return data.getvalue() + diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py new file mode 100644 index 0000000..4134f77 --- /dev/null +++ b/osmclient/sol005/ns.py @@ -0,0 +1,133 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM ns API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml + + +class Ns(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/nslcm' + self._apiVersion = '/v1' + self._apiResource = '/ns_instances_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + def list(self, filter=None): + """Returns a list of NS + """ + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + if resp: + return resp + return list() + + def get(self, name): + """Returns an NS based on name or id + """ + if utils.validate_uuid4(name): + for ns in self.list(): + if name == ns['_id']: + return ns + else: + for ns in self.list(): + if name == ns['name']: + return ns + raise NotFound("ns {} not found".format(name)) + + def get_individual(self, name): + ns_id = name + if not utils.validate_uuid4(name): + for ns in self.list(): + if name == ns['name']: + ns_id = ns['_id'] + break + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, ns_id)) + #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, ns_id)) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("ns {} not found".format(name)) + + def delete(self, name): + ns = self.get(name) + resp = self._http.delete_cmd('{}/{}'.format(self._apiBase,ns['_id'])) + #print 'RESP: '.format(resp) + if resp is None: + print 'Deleted' + else: + raise ClientException("failed to delete ns {}: {}".format(name, resp)) + + def create(self, nsd_name, nsr_name, account, config=None, + ssh_keys=None, description='default description', + admin_status='ENABLED'): + + nsd = self._client.nsd.get(nsd_name) + + datacenter = self._client.vim.get(account) + if datacenter is None: + raise NotFound("cannot find datacenter account {}".format(account)) + + ns = {} + ns['nsdId'] = nsd['_id'] + ns['nsName'] = nsr_name + ns['nsDescription'] = description + #ns['vimAccountId'] = datacenter['_id'] + # TODO: Fix it to use datacenter _id + ns['vimAccountId'] = datacenter['name'] + #ns['userdata'] = {} + #ns['userdata']['key1']='value1' + #ns['userdata']['key2']='value2' + + if ssh_keys is not None: + # ssh_keys is comma separate list + ssh_keys_format = [] + for key in ssh_keys.split(','): + ssh_keys_format.append({'key-pair-ref': key}) + + ns['ssh-authorized-key'] = ssh_keys_format + + #print yaml.safe_dump(ns) + try: + self._apiResource = '/ns_instances_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + #print resp + resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=ns) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server: '.format( + resp)) + else: + print resp['id'] + except ClientException as exc: + message="failed to create ns: {} nsd: {}\nerror:\n{}".format( + nsr_name, + nsd_name, + exc.message) + raise ClientException(message) + diff --git a/osmclient/sol005/nsd.py b/osmclient/sol005/nsd.py new file mode 100644 index 0000000..3288e95 --- /dev/null +++ b/osmclient/sol005/nsd.py @@ -0,0 +1,146 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM nsd API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException +from osmclient.common import utils +import yaml +import magic +#from os import stat +#from os.path import basename + +class Nsd(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/nsd' + self._apiVersion = '/v1' + self._apiResource = '/ns_descriptors' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + #self._apiBase='/nsds' + + def list(self, filter=None): + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase, filter_string)) + #print yaml.safe_dump(resp) + if resp: + return resp + return list() + + def get(self, name): + if utils.validate_uuid4(name): + for nsd in self.list(): + if name == nsd['_id']: + return nsd + else: + for nsd in self.list(): + if 'name' in nsd and name == nsd['name']: + return nsd + raise NotFound("nsd {} not found".format(name)) + + def get_individual(self, name): + nsd = self.get(name) + # It is redundant, since the previous one already gets the whole nsdinfo + # The only difference is that a different primitive is exercised + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nsd['_id'])) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("nsd {} not found".format(name)) + + def get_thing(self, name, thing, filename): + nsd = self.get(name) + headers = self._client._headers + headers['Accept'] = 'application/binary' + resp2 = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, nsd['_id'], thing)) + #print yaml.safe_dump(resp2) + if resp2: + #store in a file + return resp2 + raise NotFound("nsd {} not found".format(name)) + + def get_descriptor(self, name, filename): + self.get_thing(name, 'nsd', filename) + + def get_package(self, name, filename): + self.get_thing(name, 'package_content', filename) + + def get_artifact(self, name, artifact, filename): + self.get_thing(name, 'artifacts/{}'.format(artifact), filename) + + def delete(self, name): + nsd = self.get(name) + resp = self._http.delete_cmd('{}/{}'.format(self._apiBase, nsd['_id'])) + #print 'RESP: '.format(resp) + if resp is None: + print 'Deleted' + else: + raise ClientException("failed to delete nsd {}: {}".format(name, resp)) + + def create(self, filename, overwrite=None, update_endpoint=None): + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "failed to guess MIME type for file '{}'".format(filename)) + headers= self._client._headers + if mime_type in ['application/yaml', 'text/plain']: + headers['Content-Type'] = 'application/yaml' + elif mime_type == 'application/gzip': + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + else: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in headers.items()] + self._http.set_http_header(http_header) + if update_endpoint: + resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/ns_descriptors_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print resp + if not resp or 'id' not in resp: + raise ClientException("failed to upload package") + else: + print resp['id'] + + def update(self, name, filename): + nsd = self.get(name) + endpoint = '{}/{}/nsd_content'.format(self._apiBase, nsd['_id']) + self.create(filename=filename, update_endpoint=endpoint) + diff --git a/osmclient/sol005/package.py b/osmclient/sol005/package.py new file mode 100644 index 0000000..f0a9e89 --- /dev/null +++ b/osmclient/sol005/package.py @@ -0,0 +1,68 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM package API handling +""" + +import tarfile +import re +import yaml +#from os import stat +#from os.path import basename +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +from osmclient.common import utils + + +class Package(object): + def __init__(self, http=None, client=None): + self._client = client + self._http = http + + def get_key_val_from_pkg(self, descriptor_file): + utils.get_key_val_from_pkg(descriptor_file) + + def upload(self, filename): + pkg_type = utils.get_key_val_from_pkg(filename) + if pkg_type is None: + raise ClientException("Cannot determine package type") + if pkg_type['type'] == 'nsd': + endpoint = '/nsd/v1/ns_descriptors' + else: + endpoint = '/vnfpkgm/v1/vnf_packages' + #endpoint = '/nsds' if pkg_type['type'] == 'nsd' else '/vnfds' + #print 'Endpoint: {}'.format(endpoint) + headers = self._client._headers + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in headers.items()] + self._http.set_http_header(http_header) + resp = self._http.post_cmd(endpoint=endpoint, + filename=filename) + #print 'RESP: {}'.format(yaml.safe_dump(resp)) + if not resp or 'id' not in resp: + raise ClientException("failed to upload package") + else: + print resp['id'] + + diff --git a/osmclient/sol005/sdncontroller.py b/osmclient/sol005/sdncontroller.py new file mode 100644 index 0000000..bc1f962 --- /dev/null +++ b/osmclient/sol005/sdncontroller.py @@ -0,0 +1,81 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM SDN controller API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml + + +class SdnController(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/sdn_controllers' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + def create(self, name, sdn_controller): + if 'type' not in vim_access: + raise Exception("type not provided") + + resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=sdn_controller) + if not resp or '_id' not in resp: + raise ClientException('failed to create SDN controller: '.format( + resp)) + else: + print resp['_id'] + + def delete(self, name): + sdn_controller = self.get(name) + resp = self._http.delete_cmd('{}/{}'.format(self._apiBase,sdn_controller['_id'])) + #print 'RESP: '.format(resp) + if 'result' not in resp: + raise ClientException("failed to delete vim {} - {}".format(name, resp)) + else: + print 'Deleted' + + def list(self, filter=None): + """Returns a list of SDN controllers + """ + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + if resp: + return resp + return list() + + def get(self, name): + """Returns an SDN controller based on name or id + """ + if utils.validate_uuid4(name): + for sdnc in self.list(): + if name == sdnc['_id']: + return sdnc + else: + for sdnc in self.list(): + if name == sdnc['name']: + return sdnc + raise NotFound("SDN controller {} not found".format(name)) + + diff --git a/osmclient/sol005/vim.py b/osmclient/sol005/vim.py new file mode 100644 index 0000000..8a0b11a --- /dev/null +++ b/osmclient/sol005/vim.py @@ -0,0 +1,125 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM vim API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml + + +class Vim(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/vims' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + def create(self, name, vim_access): + if 'vim-type' not in vim_access: + #'openstack' not in vim_access['vim-type']): + raise Exception("vim type not provided") + + vim_account = {} + vim_config = {'hello': 'hello'} + vim_account['name'] = name + vim_account = self.update_vim_account_dict(vim_account, vim_access) + + vim_config = {} + if 'config' in vim_access and vim_access['config'] is not None: + vim_config = yaml.load(vim_access['config']) + + vim_account['config'] = vim_config + + resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=vim_account) + if not resp or 'id' not in resp: + raise ClientException('failed to create vim {}: {}'.format( + name, resp)) + else: + print resp['id'] + + def update(self, vim_name, vim_account): + vim = self.get(vim_name) + resp = self._http.put_cmd(endpoint='{}/{}'.format(self._apiBase,vim['_id']), + postfields_dict=vim_account) + if not resp or '_id' not in resp: + raise ClientException('failed to update vim: '.format( + resp)) + else: + print resp['_id'] + + def update_vim_account_dict(self, vim_account, vim_access): + vim_account['vim_type'] = vim_access['vim-type'] + vim_account['description'] = vim_access['description'] + vim_account['vim_url'] = vim_access['vim-url'] + vim_account['vim_user'] = vim_access['vim-username'] + vim_account['vim_password'] = vim_access['vim-password'] + vim_account['vim_tenant_name'] = vim_access['vim-tenant-name'] + return vim_account + + def get_id(self, name): + """Returns a VIM id from a VIM name + """ + for vim in self.list(): + if name == vim['name']: + return vim['uuid'] + raise NotFound("vim {} not found".format(name)) + + def delete(self, vim_name): + vim_id = vim_name + if not utils.validate_uuid4(vim_name): + vim_id = self.get_id(vim_name) + resp = self._http.delete_cmd('{}/{}'.format(self._apiBase,vim_id)) + if resp is None: + print 'Deleted' + else: + raise ClientException("failed to delete vim {} - {}".format(vim_name, resp)) + + def list(self, filter=None): + """Returns a list of VIM accounts + """ + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + if not resp: + return list() + vim_accounts = [] + for datacenter in resp: + vim_accounts.append({"name": datacenter['name'], "uuid": datacenter['_id'] + if '_id' in datacenter else None}) + return vim_accounts + + def get(self, name): + """Returns a VIM account based on name or id + """ + vim_id = name + if not utils.validate_uuid4(name): + vim_id = self.get_id(name) + resp = self._http.get_cmd('{}/{}'.format(self._apiBase,vim_id)) + if not resp or '_id' not in resp: + raise ClientException('failed to get vim info: '.format( + resp)) + else: + return resp + raise NotFound("vim {} not found".format(name)) + diff --git a/osmclient/sol005/vnfd.py b/osmclient/sol005/vnfd.py new file mode 100644 index 0000000..bc77a8d --- /dev/null +++ b/osmclient/sol005/vnfd.py @@ -0,0 +1,145 @@ +# Copyright 2018 Telefonica +# +# 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. + +""" +OSM vnfd API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException +from osmclient.common import utils +import yaml +import magic +#from os import stat +#from os.path import basename + +class Vnfd(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/vnfpkgm' + self._apiVersion = '/v1' + self._apiResource = '/vnf_packages' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + #self._apiBase='/vnfds' + + def list(self, filter=None): + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + if resp: + return resp + return list() + + def get(self, name): + if utils.validate_uuid4(name): + for vnfd in self.list(): + if name == vnfd['_id']: + return vnfd + else: + for vnfd in self.list(): + if 'name' in vnfd and name == vnfd['name']: + return vnfd + raise NotFound("vnfd {} not found".format(name)) + + def get_individual(self, name): + vnfd = self.get(name) + # It is redundant, since the previous one already gets the whole vnfpkginfo + # The only difference is that a different primitive is exercised + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, vnfd['_id'])) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("vnfd {} not found".format(name)) + + def get_thing(self, name, thing, filename): + vnfd = self.get(name) + headers = self._client._headers + headers['Accept'] = 'application/binary' + resp2 = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, vnfd['_id'], thing)) + #print yaml.safe_dump(resp2) + if resp2: + #store in a file + return resp2 + raise NotFound("vnfd {} not found".format(name)) + + def get_descriptor(self, name, filename): + self.get_thing(name, 'vnfd', filename) + + def get_package(self, name, filename): + self.get_thing(name, 'package_content', filename) + + def get_artifact(self, name, artifact, filename): + self.get_thing(name, 'artifacts/{}'.format(artifact), filename) + + def delete(self, name): + vnfd = self.get(name) + resp = self._http.delete_cmd('{}/{}'.format(self._apiBase,vnfd['_id'])) + #print 'RESP: '.format(resp) + if resp is None: + print 'Deleted' + else: + raise ClientException("failed to delete vnfd {}: {}".format(name, resp)) + + def create(self, filename, overwrite=None, update_endpoint=None): + mime_type = magic.from_file(filename, mime=True) + if mime_type is None: + raise ClientException( + "failed to guess MIME type for file '{}'".format(filename)) + headers= self._client._headers + if mime_type in ['application/yaml', 'text/plain']: + headers['Content-Type'] = 'application/yaml' + elif mime_type == 'application/gzip': + headers['Content-Type'] = 'application/gzip' + #headers['Content-Type'] = 'application/binary' + # Next three lines are to be removed in next version + #headers['Content-Filename'] = basename(filename) + #file_size = stat(filename).st_size + #headers['Content-Range'] = 'bytes 0-{}/{}'.format(file_size - 1, file_size) + else: + raise ClientException( + "Unexpected MIME type for file {}: MIME type {}".format( + filename, mime_type) + ) + headers["Content-File-MD5"] = utils.md5(filename) + http_header = ['{}: {}'.format(key,val) + for (key,val) in headers.items()] + self._http.set_http_header(http_header) + if update_endpoint: + resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/vnf_packages_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print resp + if not resp or 'id' not in resp: + raise ClientException("failed to upload package") + else: + print resp['id'] + + def update(self, name, filename): + vnfd = self.get(name) + endpoint = '{}/{}/vnfd_content'.format(self._apiBase, vnfd['_id']) + self.create(filename=filename, update_endpoint=endpoint) + diff --git a/osmclient/v1/package.py b/osmclient/v1/package.py index b383003..49527c2 100644 --- a/osmclient/v1/package.py +++ b/osmclient/v1/package.py @@ -18,9 +18,6 @@ OSM package API handling """ -import tarfile -import re -import yaml from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound from osmclient.common import utils @@ -52,47 +49,14 @@ class Package(object): check_exists(lambda: get_method(pkg_type['name']))) - # method opens up a package and finds the name of the resulting - # descriptor (vnfd or nsd name) def get_key_val_from_pkg(self, descriptor_file): - tar = tarfile.open(descriptor_file) - yamlfile = None - for member in tar.getmembers(): - if (re.match('.*.yaml', member.name) and - len(member.name.split('/')) == 2): - yamlfile = member.name - break - if yamlfile is None: - return None - - dict = yaml.load(tar.extractfile(yamlfile)) - result = {} - for k1, v1 in dict.items(): - if not k1.endswith('-catalog'): - continue - for k2, v2 in v1.items(): - if not k2.endswith('nsd') and not k2.endswith('vnfd'): - continue - - if 'nsd' in k2: - result['type'] = 'nsd' - else: - result['type'] = 'vnfd' - - for entry in v2: - for k3, v3 in entry.items(): - # strip off preceeding chars before : - key_name = k3.split(':').pop() - - result[key_name] = v3 - tar.close() - return result + utils.get_key_val_from_pkg(descriptor_file) def wait_for_upload(self, filename): """wait(block) for an upload to succeed. The filename passed is assumed to be a descriptor tarball. """ - pkg_type = self.get_key_val_from_pkg(filename) + pkg_type = utils.get_key_val_from_pkg(filename) if pkg_type is None: raise ClientException("Cannot determine package type") diff --git a/setup.py b/setup.py index 8109b9f..25a397c 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( packages=find_packages(), include_package_data=True, install_requires=[ - 'Click', 'prettytable', 'pyyaml', 'pycurl' + 'Click', 'prettytable', 'pyyaml', 'pycurl', 'python-magic' ], setup_requires=['setuptools-version-command'], test_suite='nose.collector',