From 3f592284d97ed71e8d7bab435586f12623691e47 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 29 Oct 2018 13:42:42 +0100 Subject: [PATCH 01/16] Support of network slices Change-Id: I57a003b880c67bdeddae6f090da63e6d4747127b Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 457 ++++++++++++++++++++++++++++++++++++- osmclient/sol005/client.py | 4 + osmclient/sol005/ns.py | 1 - osmclient/sol005/nsi.py | 302 ++++++++++++++++++++++++ osmclient/sol005/nst.py | 179 +++++++++++++++ 5 files changed, 938 insertions(+), 5 deletions(-) create mode 100644 osmclient/sol005/nsi.py create mode 100644 osmclient/sol005/nst.py diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index c07b62d..94a503e 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -410,6 +410,128 @@ def ns_op_list(ctx, name): table.align = 'l' print(table) + +def nsi_list(ctx, filter): + '''list all Network Slice Instances''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nsi.list(filter) + except ClientException as inst: + print((inst.message)) + exit(1) + table = PrettyTable( + ['netslice instance name', + 'id', + 'operational status', + 'config status', + 'detailed status']) + for nsi in resp: + nsi_name = nsi['name'] + nsi_id = nsi['_id'] + opstatus = nsi['operational-status'] if 'operational-status' in nsi else 'Not found' + configstatus = nsi['config-status'] if 'config-status' in nsi else 'Not found' + detailed_status = nsi['detailed-status'] if 'detailed-status' in nsi else 'Not found' + if configstatus == "config_not_needed": + configstatus = "configured (no charms)" + table.add_row( + [nsi_name, + nsi_id, + opstatus, + configstatus, + detailed_status]) + table.align = 'l' + print(table) + + +@cli.command(name='nsi-list') +@click.option('--filter', default=None, + help='restricts the list to the Network Slice Instances matching the filter') +@click.pass_context +def nsi_list1(ctx, filter): + '''list all Network Slice Instances (NSI)''' + nsi_list(ctx,filter) + + +@cli.command(name='netslice-instance-list') +@click.option('--filter', default=None, + help='restricts the list to the Network Slice Instances matching the filter') +@click.pass_context +def nsi_list2(ctx, filter): + '''list all Network Slice Instances (NSI)''' + nsi_list(ctx,filter) + + +def nst_list(ctx, filter): + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nst.list(filter) + except ClientException as inst: + print((inst.message)) + exit(1) + #print yaml.safe_dump(resp) + table = PrettyTable(['nst name', 'id']) + for nst in resp: + name = nst['name'] if 'name' in nst else '-' + table.add_row([name, nst['_id']]) + table.align = 'l' + print(table) + + +@cli.command(name='nst-list') +@click.option('--filter', default=None, + help='restricts the list to the NST matching the filter') +@click.pass_context +def nst_list1(ctx, filter): + '''list all Network Slice Templates (NST) in the system''' + nst_list(ctx,filter) + + +@cli.command(name='netslice-template-list') +@click.option('--filter', default=None, + help='restricts the list to the NST matching the filter') +@click.pass_context +def nst_list2(ctx, filter): + '''list all Network Slice Templates (NST) in the system''' + nst_list(ctx,filter) + + +def nsi_op_list(ctx, name): + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nsi.list_op(name) + except ClientException as inst: + print((inst.message)) + exit(1) + table = PrettyTable(['id', 'operation', 'status']) + for op in resp: + table.add_row([op['id'], op['lcmOperationType'], + op['operationState']]) + table.align = 'l' + print(table) + + +@cli.command(name='nsi-op-list') +@click.argument('name') +@click.pass_context +def nsi_op_list1(ctx, name): + '''shows the history of operations over a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice Instance + ''' + nsi_op_list(ctx,name) + + +@cli.command(name='netslice-instance-op-list') +@click.argument('name') +@click.pass_context +def nsi_op_list2(ctx, name): + '''shows the history of operations over a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice Instance + ''' + nsi_op_list(ctx,name) + + #################### # SHOW operations #################### @@ -617,6 +739,7 @@ def ns_monitoring_show(ctx, ns_name): table.align = 'l' print(table) + @cli.command(name='ns-op-show', short_help='shows the info of an operation') @click.argument('id') @click.option('--filter', default=None) @@ -641,6 +764,142 @@ def ns_op_show(ctx, id, filter): print(table) +def nst_show(ctx, name, literal): + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.nst.get(name) + #resp = ctx.obj.nst.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 list(resp.items()): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + +@cli.command(name='nst-show', short_help='shows the content of a Network Slice Template (NST)') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def nst_show1(ctx, name, literal): + '''shows the content of a Network Slice Template (NST) + + NAME: name or ID of the NST + ''' + nst_show(ctx, name, literal) + + +@cli.command(name='netslice-template-show', short_help='shows the content of a Network Slice Template (NST)') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def nst_show2(ctx, name, literal): + '''shows the content of a Network Slice Template (NST) + + NAME: name or ID of the NST + ''' + nst_show(ctx, name, literal) + + +def nsi_show(ctx, name, literal, filter): + try: + check_client_version(ctx.obj, ctx.command.name) + nsi = ctx.obj.nsi.get(name) + except ClientException as inst: + print((inst.message)) + exit(1) + + if literal: + print(yaml.safe_dump(nsi)) + return + + table = PrettyTable(['field', 'value']) + + for k, v in list(nsi.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='nsi-show', short_help='shows the content of a Network Slice Instance (NSI)') +@click.argument('name') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--filter', default=None) +@click.pass_context +def nsi_show1(ctx, name, literal, filter): + '''shows the content of a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice Instance + ''' + nsi_show(ctx, name, literal, filter) + + +@cli.command(name='netslice-instance-show', short_help='shows the content of a Network Slice Instance (NSI)') +@click.argument('name') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--filter', default=None) +@click.pass_context +def nsi_show2(ctx, name, literal, filter): + '''shows the content of a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice Instance + ''' + nsi_show(ctx, name, literal, filter) + + +def nsi_op_show(ctx, id, filter): + try: + check_client_version(ctx.obj, ctx.command.name) + op_info = ctx.obj.nsi.get_op(id) + except ClientException as inst: + print((inst.message)) + exit(1) + + table = PrettyTable(['field', 'value']) + for k, v in list(op_info.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='nsi-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') +@click.argument('id') +@click.option('--filter', default=None) +@click.pass_context +def nsi_op_show1(ctx, id, filter): + '''shows the info of an operation over a Network Slice Instance(NSI) + + ID: operation identifier + ''' + nsi_op_show(ctx, id, filter) + + +@cli.command(name='netslice-instance-op-show', short_help='shows the info of an operation over a Network Slice Instance(NSI)') +@click.argument('id') +@click.option('--filter', default=None) +@click.pass_context +def nsi_op_show2(ctx, id, filter): + '''shows the info of an operation over a Network Slice Instance(NSI) + + ID: operation identifier + ''' + nsi_op_show(ctx, id, filter) + + #################### # CREATE operations #################### @@ -715,13 +974,13 @@ def vnfd_create2(ctx, filename, overwrite): vnfd_create(ctx, filename, overwrite) -@cli.command(name='ns-create') +@cli.command(name='ns-create', short_help='creates a new Network Service instance') @click.option('--ns_name', - prompt=True) + prompt=True, help='name of the NS instance') @click.option('--nsd_name', - prompt=True) + prompt=True, help='name of the NS descriptor') @click.option('--vim_account', - prompt=True) + prompt=True, help='default VIM account id or name for the deployment') @click.option('--admin_status', default='ENABLED', help='administration status') @@ -762,6 +1021,94 @@ def ns_create(ctx, exit(1) +def nst_create(ctx, filename, overwrite): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.create(filename, overwrite) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='nst-create', short_help='creates a new Network Slice Template (NST)') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in NST') +@click.pass_context +def nst_create1(ctx, filename, overwrite): + '''creates a new Network Slice Template (NST) + + FILENAME: NST yaml file or NSTpkg tar.gz file + ''' + nst_create(ctx, filename, overwrite) + + +@cli.command(name='netslice-template-create', short_help='creates a new Network Slice Template (NST)') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in NST') +@click.pass_context +def nst_create2(ctx, filename, overwrite): + '''creates a new Network Slice Template (NST) + + FILENAME: NST yaml file or NSTpkg tar.gz file + ''' + nst_create(ctx, filename, overwrite) + + +def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): + '''creates a new Network Slice Instance (NSI)''' + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, + account=vim_account) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='nsi-create', short_help='creates a new Network Slice Instance') +@click.option('--nsi_name', prompt=True, help='name of the Network Slice Instance') +@click.option('--nst_name', prompt=True, help='name of the Network Slice Template') +@click.option('--vim_account', prompt=True, help='default VIM account id or name for the deployment') +@click.option('--ssh_keys', default=None, + help='comma separated list of keys to inject to vnfs') +@click.option('--config', default=None, + help='Netslice specific yaml configuration:\n' + 'netslice_subnet: [\n' + 'id: TEXT, vim_account: TEXT,\n' + 'vnf: [member-vnf-index: TEXT, vim_account: TEXT]\n' + 'vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' + '],\n' + 'netslice-vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' + ) +@click.pass_context +def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): + '''creates a new Network Slice Instance (NSI)''' + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config) + + +@cli.command(name='netslice-instance-create', short_help='creates a new Network Slice Instance') +@click.option('--nsi_name', prompt=True, help='name of the Network Slice Instance') +@click.option('--nst_name', prompt=True, help='name of the Network Slice Template') +@click.option('--vim_account', prompt=True, help='default VIM account id or name for the deployment') +@click.option('--ssh_keys', default=None, + help='comma separated list of keys to inject to vnfs') +@click.option('--config', default=None, + help='Netslice specific yaml configuration:\n' + 'netslice_subnet: [\n' + 'id: TEXT, vim_account: TEXT,\n' + 'vnf: [member-vnf-index: TEXT, vim_account: TEXT]\n' + 'vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' + '],\n' + 'netslice-vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' + ) +@click.pass_context +def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): + '''creates a new Network Slice Instance (NSI)''' + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config) + + #################### # UPDATE operations #################### @@ -774,6 +1121,7 @@ def nsd_update(ctx, name, content): print((inst.message)) exit(1) + @cli.command(name='nsd-update', short_help='updates a NSD/NSpkg') @click.argument('name') @click.option('--content', default=None, @@ -835,6 +1183,41 @@ def vnfd_update2(ctx, name, content): vnfd_update(ctx, name, content) +def nst_update(ctx, name, content): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.update(name, content) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='nst-update', short_help='updates a Network Slice Template (NST)') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the NST/NSTpkg replacing the current one') +@click.pass_context +def nst_update1(ctx, name, content): + '''updates a Network Slice Template (NST) + + NAME: name or ID of the NSD/NSpkg + ''' + nst_update(ctx, name, content) + + +@cli.command(name='netslice-template-update', short_help='updates a Network Slice Template (NST)') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the NST/NSTpkg replacing the current one') +@click.pass_context +def nst_update2(ctx, name, content): + '''updates a Network Slice Template (NST) + + NAME: name or ID of the NSD/NSpkg + ''' + nst_update(ctx, name, content) + + #################### # DELETE operations #################### @@ -931,6 +1314,72 @@ def ns_delete(ctx, name, force): exit(1) +def nst_delete(ctx, name, force): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nst.delete(name, force) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='nst-delete', short_help='deletes a Network Slice Template (NST)') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def nst_delete1(ctx, name, force): + '''deletes a Network Slice Template (NST) + + NAME: name or ID of the NST/NSTpkg to be deleted + ''' + nst_delete(ctx, name, force) + + +@cli.command(name='netslice-template-delete', short_help='deletes a Network Slice Template (NST)') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def nst_delete2(ctx, name, force): + '''deletes a Network Slice Template (NST) + + NAME: name or ID of the NST/NSTpkg to be deleted + ''' + nst_delete(ctx, name, force) + + +def nsi_delete(ctx, name, force): + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.nsi.delete(name, force) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='nsi-delete', short_help='deletes a Network Slice Instance (NSI)') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def nsi_delete1(ctx, name, force): + '''deletes a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice instance to be deleted + ''' + nsi_delete(ctx, name, force) + + +@cli.command(name='netslice-instance-delete', short_help='deletes a Network Slice Instance (NSI)') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def nsi_delete2(ctx, name, force): + '''deletes a Network Slice Instance (NSI) + + NAME: name or ID of the Network Slice instance to be deleted + ''' + nsi_delete(ctx, name, force) + + #################### # VIM operations #################### diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 5de4ff0..7583165 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -21,6 +21,8 @@ OSM SOL005 client API #from osmclient.v1 import vca from osmclient.sol005 import vnfd from osmclient.sol005 import nsd +from osmclient.sol005 import nst +from osmclient.sol005 import nsi from osmclient.sol005 import ns from osmclient.sol005 import vnf from osmclient.sol005 import vim @@ -75,8 +77,10 @@ class Client(object): self.vnfd = vnfd.Vnfd(self._http_client, client=self) self.nsd = nsd.Nsd(self._http_client, client=self) + self.nst = nst.Nst(self._http_client, client=self) self.package = package.Package(self._http_client, client=self) self.ns = ns.Ns(self._http_client, client=self) + self.nsi = nsi.Nsi(self._http_client, client=self) self.vim = vim.Vim(self._http_client, client=self) self.sdnc = sdncontroller.SdnController(self._http_client, client=self) self.vnf = vnf.Vnf(self._http_client, client=self) diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py index bda7c3e..9d1fe95 100644 --- a/osmclient/sol005/ns.py +++ b/osmclient/sol005/ns.py @@ -145,7 +145,6 @@ class Ns(object): for vnf in ns_config["vnf"]: if vnf.get("vim_account"): vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) - ns["vnf"] = ns_config["vnf"] #print yaml.safe_dump(ns) diff --git a/osmclient/sol005/nsi.py b/osmclient/sol005/nsi.py new file mode 100644 index 0000000..f6009dd --- /dev/null +++ b/osmclient/sol005/nsi.py @@ -0,0 +1,302 @@ +# 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 NSI (Network Slice Instance) API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml +import json + + +class Nsi(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/nsilcm' + self._apiVersion = '/v1' + self._apiResource = '/netslice_instances_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + def list(self, filter=None): + """Returns a list of NSI + """ + 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 NSI based on name or id + """ + if utils.validate_uuid4(name): + for nsi in self.list(): + if name == nsi['_id']: + return nsi + else: + for nsi in self.list(): + if name == nsi['name']: + return nsi + raise NotFound("nsi {} not found".format(name)) + + def get_individual(self, name): + nsi_id = name + if not utils.validate_uuid4(name): + for nsi in self.list(): + if name == nsi['name']: + nsi_id = nsi['_id'] + break + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nsi_id)) + #resp = self._http.get_cmd('{}/{}/nsd_content'.format(self._apiBase, nsi_id)) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("nsi {} not found".format(name)) + + def delete(self, name, force=False): + nsi = self.get(name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + nsi['_id'], querystring)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 202: + print('Deletion in progress') + elif http_code == 204: + print('Deleted') + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete nsi {} - {}".format(name, msg)) + + def create(self, nst_name, nsi_name, account, config=None, + ssh_keys=None, description='default description', + admin_status='ENABLED'): + + nst = self._client.nst.get(nst_name) + + vim_account_id = {} + + def get_vim_account_id(vim_account): + if vim_account_id.get(vim_account): + return vim_account_id[vim_account] + + vim = self._client.vim.get(vim_account) + if vim is None: + raise NotFound("cannot find vim account '{}'".format(vim_account)) + vim_account_id[vim_account] = vim['_id'] + return vim['_id'] + + nsi = {} + nsi['nstId'] = nst['_id'] + nsi['nsiName'] = nsi_name + nsi['nsiDescription'] = description + nsi['vimAccountId'] = get_vim_account_id(account) + #nsi['userdata'] = {} + #nsi['userdata']['key1']='value1' + #nsi['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 + nsi['ssh_keys'] = [] + for pubkeyfile in ssh_keys.split(','): + with open(pubkeyfile, 'r') as f: + nsi['ssh_keys'].append(f.read()) + if config: + nsi_config = yaml.load(config) + if "netslice-vld" in nsi_config: + for vld in nsi_config["netslice-vld"]: + if vld.get("vim-network-name"): + if isinstance(vld["vim-network-name"], dict): + vim_network_name_dict = {} + for vim_account, vim_net in list(vld["vim-network-name"].items()): + vim_network_name_dict[get_vim_account_id(vim_account)] = vim_net + vld["vim-network-name"] = vim_network_name_dict + nsi["netslice-vld"] = nsi_config["netslice-vld"] + if "netslice-subnet" in nsi_config: + for nssubnet in nsi_config["netslice-subnet"]: + if "vld" in nssubnet: + for vld in nssubnet["vld"]: + if vld.get("vim-network-name"): + if isinstance(vld["vim-network-name"], dict): + vim_network_name_dict = {} + for vim_account, vim_net in list(vld["vim-network-name"].items()): + vim_network_name_dict[get_vim_account_id(vim_account)] = vim_net + vld["vim-network-name"] = vim_network_name_dict + if "vnf" in nssubnet: + for vnf in nsi_config["vnf"]: + if vnf.get("vim_account"): + vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) + nsi["netslice-subnet"] = nsi_config["netslice-subnet"] + + #print yaml.safe_dump(nsi) + try: + self._apiResource = '/netslice_instances_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + headers = self._client._headers + headers['Content-Type'] = 'application/yaml' + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + http_code, resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=nsi) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {} '.format( + resp)) + print(resp['id']) + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException(msg) + except ClientException as exc: + message="failed to create nsi: {} nst: {}\nerror:\n{}".format( + nsi_name, + nst_name, + exc.message) + raise ClientException(message) + + def list_op(self, name, filter=None): + """Returns the list of operations of a NSI + """ + nsi = self.get(name) + try: + self._apiResource = '/nsi_lcm_op_occs' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + filter_string = '' + if filter: + filter_string = '&{}'.format(filter) + http_code, resp = self._http.get2_cmd('{}?nsiInstanceId={}'.format( + self._apiBase, nsi['_id'], + filter_string) ) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 200: + if resp: + resp = json.loads(resp) + return resp + else: + raise ClientException('unexpected response from server') + else: + msg = "" + if resp: + try: + resp = json.loads(resp) + msg = resp['detail'] + except ValueError: + msg = resp + raise ClientException(msg) + except ClientException as exc: + message="failed to get operation list of NSI {}:\nerror:\n{}".format( + name, + exc.message) + raise ClientException(message) + + def get_op(self, operationId): + """Returns the status of an operation + """ + try: + self._apiResource = '/nsi_lcm_op_occs' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + http_code, resp = self._http.get2_cmd('{}/{}'.format(self._apiBase, operationId)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 200: + if resp: + resp = json.loads(resp) + return resp + else: + raise ClientException('unexpected response from server') + else: + msg = "" + if resp: + try: + resp = json.loads(resp) + msg = resp['detail'] + except ValueError: + msg = resp + raise ClientException(msg) + except ClientException as exc: + message="failed to get status of operation {}:\nerror:\n{}".format( + operationId, + exc.message) + raise ClientException(message) + + def exec_op(self, name, op_name, op_data=None): + """Executes an operation on a NSI + """ + nsi = self.get(name) + try: + self._apiResource = '/netslice_instances' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}/{}/{}'.format(self._apiBase, nsi['_id'], op_name) + #print 'OP_NAME: {}'.format(op_name) + #print 'OP_DATA: {}'.format(json.dumps(op_data)) + http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=op_data) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException(msg) + except ClientException as exc: + message="failed to exec operation {}:\nerror:\n{}".format( + name, + exc.message) + raise ClientException(message) + diff --git a/osmclient/sol005/nst.py b/osmclient/sol005/nst.py new file mode 100644 index 0000000..b7c67be --- /dev/null +++ b/osmclient/sol005/nst.py @@ -0,0 +1,179 @@ +# 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 NST (Network Slice Template) API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException +from osmclient.common import utils +import json +import magic +#from os import stat +#from os.path import basename + +class Nst(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/nst' + self._apiVersion = '/v1' + self._apiResource = '/netslice_templates' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + 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 nst in self.list(): + if name == nst['_id']: + return nst + else: + for nst in self.list(): + if 'name' in nst and name == nst['name']: + return nst + raise NotFound("nst {} not found".format(name)) + + def get_individual(self, name): + nst = self.get(name) + # It is redundant, since the previous one already gets the whole nstinfo + # The only difference is that a different primitive is exercised + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, nst['_id'])) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("nst {} not found".format(name)) + + def get_thing(self, name, thing, filename): + nst = self.get(name) + headers = self._client._headers + headers['Accept'] = 'application/binary' + http_code, resp = self._http.get2_cmd('{}/{}/{}'.format(self._apiBase, nst['_id'], thing)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + #store in a file + return resp + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to get {} from {} - {}".format(thing, name, msg)) + + def get_descriptor(self, name, filename): + self.get_thing(name, 'nst', filename) + + def get_package(self, name, filename): + self.get_thing(name, 'nst_content', filename) + + def get_artifact(self, name, artifact, filename): + self.get_thing(name, 'artifacts/{}'.format(artifact), filename) + + def delete(self, name, force=False): + nst = self.get(name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + nst['_id'], querystring)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 202: + print('Deletion in progress') + elif http_code == 204: + print('Deleted') + else: + msg = "" + if resp: + try: + resp = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete nst {} - {}".format(name, msg)) + + 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 in ['application/gzip', 'application/x-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 list(headers.items())] + self._http.set_http_header(http_header) + if update_endpoint: + http_code, resp = self._http.put_cmd(endpoint=update_endpoint, filename=filename) + else: + ow_string = '' + if overwrite: + ow_string = '?{}'.format(overwrite) + self._apiResource = '/netslice_templates_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = '{}{}'.format(self._apiBase,ow_string) + http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + else: + msg = "Error {}".format(http_code) + if resp: + try: + msg = "{} - {}".format(msg, json.loads(resp)) + except ValueError: + msg = "{} - {}".format(msg, resp) + raise ClientException("failed to create/update nst - {}".format(msg)) + + def update(self, name, filename): + nst = self.get(name) + endpoint = '{}/{}/netslice_templates_content'.format(self._apiBase, nst['_id']) + self.create(filename=filename, update_endpoint=endpoint) + -- 2.25.1 From fcb33ebcc77ac7bb6ba0e0989c54bd6f8cefe766 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Sat, 1 Dec 2018 01:12:19 +0100 Subject: [PATCH 02/16] osm.py: added config_file option to nsi_create Change-Id: Ief0707cec43fc5da3ff54117690fb121991924f0 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 94a503e..c2fe67f 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -1056,10 +1056,15 @@ def nst_create2(ctx, filename, overwrite): nst_create(ctx, filename, overwrite) -def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): +def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): '''creates a new Network Slice Instance (NSI)''' try: check_client_version(ctx.obj, ctx.command.name) + if config_file: + if config: + raise ClientException('"--config" option is incompatible with "--config_file" option') + with open(config_file, 'r') as cf: + config=cf.read() ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, account=vim_account) except ClientException as inst: @@ -1082,10 +1087,13 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): '],\n' 'netslice-vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' ) +@click.option('--config_file', + default=None, + help='nsi specific yaml configuration file') @click.pass_context -def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): +def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): '''creates a new Network Slice Instance (NSI)''' - nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config) + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file) @cli.command(name='netslice-instance-create', short_help='creates a new Network Slice Instance') @@ -1103,10 +1111,13 @@ def nsi_create1(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): '],\n' 'netslice-vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' ) +@click.option('--config_file', + default=None, + help='nsi specific yaml configuration file') @click.pass_context -def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config): +def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file): '''creates a new Network Slice Instance (NSI)''' - nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config) + nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file) #################### -- 2.25.1 From 2cc451122a28672aa0b928688fc76d633d5ece81 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Tue, 6 Nov 2018 05:56:15 +0100 Subject: [PATCH 03/16] support of PDUs Change-Id: Ifb20578016ef7212399db6b4c12aa5a30376c67e Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 131 +++++++++++++++++++++++++++++++++++++ osmclient/sol005/client.py | 2 + osmclient/sol005/pdud.py | 127 +++++++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+) create mode 100644 osmclient/sol005/pdud.py diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index c2fe67f..52e3bc8 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -532,6 +532,44 @@ def nsi_op_list2(ctx, name): nsi_op_list(ctx,name) +@cli.command(name='pdu-list') +@click.option('--filter', default=None, + help='restricts the list to the Physical Deployment Units matching the filter') +@click.pass_context +def pdu_list(ctx, filter): + '''list all Physical Deployment Units (PDU)''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.pdu.list(filter) + except ClientException as inst: + print((inst.message)) + exit(1) + table = PrettyTable( + ['pdu name', + 'id', + 'type', + 'shared', + 'mgmt ip address']) + for pdu in resp: + pdu_name = pdu['name'] + pdu_id = pdu['_id'] + pdu_type = pdu['type'] + pdu_shared = pdu['shared'] + pdu_ipaddress = "None" + for iface in pdu['interfaces']: + if iface['mgmt']: + pdu_ipaddress = iface['ip-address'] + break + table.add_row( + [pdu_name, + pdu_id, + pdu_type, + pdu_shared, + pdu_ipaddress]) + table.align = 'l' + print(table) + + #################### # SHOW operations #################### @@ -900,6 +938,38 @@ def nsi_op_show2(ctx, id, filter): nsi_op_show(ctx, id, filter) +@cli.command(name='pdu-show', short_help='shows the content of a Physical Deployment Unit (PDU)') +@click.argument('name') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.option('--filter', default=None) +@click.pass_context +def pdu_show(ctx, name, literal, filter): + '''shows the content of a Physical Deployment Unit (PDU) + + NAME: name or ID of the PDU + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + pdu = ctx.obj.pdu.get(name) + except ClientException as inst: + print((inst.message)) + exit(1) + + if literal: + print(yaml.safe_dump(pdu)) + return + + table = PrettyTable(['field', 'value']) + + for k, v in list(pdu.items()): + if filter is None or filter in k: + table.add_row([k, json.dumps(v, indent=2)]) + + table.align = 'l' + print(table) + + #################### # CREATE operations #################### @@ -1120,6 +1190,50 @@ def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_f nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_file) +@cli.command(name='pdu-create', short_help='adds a new Physical Deployment Unit to the catalog') +@click.option('--name', help='name of the Physical Deployment Unit') +@click.option('--pdu_type', help='type of PDU (e.g. router, firewall, FW001)') +@click.option('--interface', + help='interface(s) of the PDU: name=,mgmt=,ip-address='+ + '[,type=][,mac-address=][,vim-network-name=]', + multiple=True) +@click.option('--description', help='human readable description') +@click.option('--shared', is_flag=True, help='flag to indicate if the PDU is shared') +@click.option('--vimAccounts', help='list of VIM accounts where this PDU is physically connected') +@click.option('--descriptor_file', default=None, help='PDU descriptor file (as an alternative to using the other arguments') +@click.pass_context +#TODO +def pdu_create(ctx, name, pdu_type, interface, description, shared, vimAccounts, descriptor_file): + '''creates a new Physical Deployment Unit (PDU)''' + try: + check_client_version(ctx.obj, ctx.command.name) + pdu = {} + if not descriptor_file: + if not name: + raise ClientException('in absence of descriptor file, option "--name" is mandatory') + if not pdu_type: + raise ClientException('in absence of descriptor file, option "--pdu_type" is mandatory') + if not interface: + raise ClientException('in absence of descriptor file, option "--interface" is mandatory (at least once)') + else: + with open(descriptor_file, 'r') as df: + pdu = yaml.load(df.read()) + if name: pdu["name"] = name + if pdu_type: pdu["type"] = pdu_type + if description: pdu["description"] = description + if shared: pdu["shared"] = shared + if vimAccounts: pdu["vim_accounts"] = yaml.load(vimAccounts) + if interface: + ifaces_list = [] + for iface in interface: + ifaces_list.append({k:v for k,v in [i.split('=') for i in iface.split(',')]}) + pdu["interfaces"] = ifaces_list + ctx.obj.pdu.create(pdu) + except ClientException as inst: + print((inst.message)) + exit(1) + + #################### # UPDATE operations #################### @@ -1391,6 +1505,23 @@ def nsi_delete2(ctx, name, force): nsi_delete(ctx, name, force) +@cli.command(name='pdu-delete', short_help='deletes a Physical Deployment Unit (PDU)') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def pdu_delete(ctx, name, force): + '''deletes a Physical Deployment Unit (PDU) + + NAME: name or ID of the PDU to be deleted + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.pdu.delete(name, force) + except ClientException as inst: + print((inst.message)) + exit(1) + + #################### # VIM operations #################### diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 7583165..c70be23 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -31,6 +31,7 @@ from osmclient.sol005 import http from osmclient.sol005 import sdncontroller from osmclient.sol005 import project as projectmodule from osmclient.sol005 import user as usermodule +from osmclient.sol005 import pdud from osmclient.common.exceptions import ClientException import json @@ -86,6 +87,7 @@ class Client(object): self.vnf = vnf.Vnf(self._http_client, client=self) self.project = projectmodule.Project(self._http_client, client=self) self.user = usermodule.User(self._http_client, client=self) + self.pdu = pdud.Pdu(self._http_client, client=self) ''' self.vca = vca.Vca(http_client, client=self, **kwargs) self.utils = utils.Utils(http_client, **kwargs) diff --git a/osmclient/sol005/pdud.py b/osmclient/sol005/pdud.py new file mode 100644 index 0000000..b59b91d --- /dev/null +++ b/osmclient/sol005/pdud.py @@ -0,0 +1,127 @@ +# 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 pdud API handling +""" + +from osmclient.common.exceptions import NotFound +from osmclient.common.exceptions import ClientException +from osmclient.common import utils +import json + + +class Pdu(object): + + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/pdu' + self._apiVersion = '/v1' + self._apiResource = '/pdu_descriptors' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + 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 pdud in self.list(): + if name == pdud['_id']: + return pdud + else: + for pdud in self.list(): + if 'name' in pdud and name == pdud['name']: + return pdud + raise NotFound("pdud {} not found".format(name)) + + def get_individual(self, name): + pdud = self.get(name) + # It is redundant, since the previous one already gets the whole pdudInfo + # The only difference is that a different primitive is exercised + resp = self._http.get_cmd('{}/{}'.format(self._apiBase, pdud['_id'])) + #print yaml.safe_dump(resp) + if resp: + return resp + raise NotFound("pdu {} not found".format(name)) + + def delete(self, name, force=False): + pdud = self.get(name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + pdud['_id'], querystring)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 202: + print('Deletion in progress') + elif http_code == 204: + print('Deleted') + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete pdu {} - {}".format(name, msg)) + + def create(self, pdu, update_endpoint=None): + headers= self._client._headers + headers['Content-Type'] = 'text/plain' + http_header = ['{}: {}'.format(key,val) + for (key,val) in list(headers.items())] + self._http.set_http_header(http_header) + if update_endpoint: + http_code, resp = self._http.put_cmd(endpoint=update_endpoint, postfields_dict=pdu) + else: + self._apiResource = '/pdu_descriptors_content' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + endpoint = self._apiBase + #endpoint = '{}{}'.format(self._apiBase,ow_string) + http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=pdu) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server: '.format( + resp)) + print(resp['id']) + else: + msg = "Error {}".format(http_code) + if resp: + try: + msg = "{} - {}".format(msg, json.loads(resp)) + except ValueError: + msg = "{} - {}".format(msg, resp) + raise ClientException("failed to create/update pdu - {}".format(msg)) + + def update(self, name, filename): + pdud = self.get(name) + endpoint = '{}/{}'.format(self._apiBase, pdud['_id']) + self.create(filename=filename, update_endpoint=endpoint) + -- 2.25.1 From 796b6749d950882ec18e53688fd7447a7c627c14 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Sun, 2 Dec 2018 11:40:48 +0100 Subject: [PATCH 04/16] osm.py: new aliases nfpkg-* for vnfpkg-* Change-Id: I0007c206deb4e1108c21a7bfb0066210a265885e Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 52e3bc8..8201f7c 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -285,6 +285,15 @@ def vnfd_list2(ctx, filter): vnfd_list(ctx,filter) +@cli.command(name='nfpkg-list') +@click.option('--filter', default=None, + help='restricts the list to the xNFpkg matching the filter') +@click.pass_context +def nfpkg_list(ctx, filter): + '''list all NFpkg (VNFpkg, PNFpkg, HNFpkg) in the system''' + vnfd_list(ctx,filter) + + @cli.command(name='vnf-list') @click.option('--ns', default=None, help='NS instance id or name to restrict the VNF list') @click.option('--filter', default=None, @@ -664,6 +673,19 @@ def vnfd_show2(ctx, name, literal): vnfd_show(ctx, name, literal) +@cli.command(name='nfpkg-show', short_help='shows the content of a NF Descriptor') +@click.option('--literal', is_flag=True, + help='print literally, no pretty table') +@click.argument('name') +@click.pass_context +def nfpkg_show(ctx, name, literal): + '''shows the content of a NF Descriptor + + NAME: name or ID of the NFpkg + ''' + 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, @@ -1044,6 +1066,19 @@ def vnfd_create2(ctx, filename, overwrite): vnfd_create(ctx, filename, overwrite) +@cli.command(name='nfpkg-create', short_help='creates a new NFpkg') +@click.argument('filename') +@click.option('--overwrite', default=None, + help='overwrites some fields in NFD') +@click.pass_context +def nfpkg_create(ctx, filename, overwrite): + '''creates a new NFpkg + + FILENAME: NF Descriptor yaml file or NFpkg tar.gz file + ''' + vnfd_create(ctx, filename, overwrite) + + @cli.command(name='ns-create', short_help='creates a new Network Service instance') @click.option('--ns_name', prompt=True, help='name of the NS instance') @@ -1308,6 +1343,19 @@ def vnfd_update2(ctx, name, content): vnfd_update(ctx, name, content) +@cli.command(name='nfpkg-update', short_help='updates a NFpkg') +@click.argument('name') +@click.option('--content', default=None, + help='filename with the NFpkg replacing the current one') +@click.pass_context +def nfpkg_update(ctx, name, content): + '''updates a NFpkg + + NAME: NF Descriptor yaml file or NFpkg tar.gz file + ''' + vnfd_update(ctx, name, content) + + def nst_update(ctx, name, content): try: check_client_version(ctx.obj, ctx.command.name) @@ -1419,6 +1467,18 @@ def vnfd_delete2(ctx, name, force): vnfd_delete(ctx, name, force) +@cli.command(name='nfpkg-delete', short_help='deletes a NFpkg') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def nfpkg_delete(ctx, name, force): + '''deletes a NFpkg + + NAME: name or ID of the NFpkg to be deleted + ''' + vnfd_delete(ctx, name, force) + + @cli.command(name='ns-delete', short_help='deletes a NS instance') @click.argument('name') @click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') -- 2.25.1 From fa731467380ccd5e8828f5437976497965d6d071 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Sun, 2 Dec 2018 12:22:53 +0100 Subject: [PATCH 05/16] Updated .gitignore: dists and pools folders Change-Id: I3e1c5b28758d21c8bad3c6db0fa53f815180ecb0 Signed-off-by: garciadeblas --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 92e1416..3c0ccf8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ .cache deb_dist/ dist/ +dists/ +pool/ *.gz *egg-info/ .eggs -- 2.25.1 From fd75fc60d6c3d3b2aa55b48b44ccb69a8689ad23 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Sun, 2 Dec 2018 13:08:52 +0100 Subject: [PATCH 06/16] osm.py: updated nfpkg-list to include nf_type filter option Change-Id: I3d272aff9027495e7fef07e6a6dc6a2859637886 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 8201f7c..6cce5e5 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -286,12 +286,30 @@ def vnfd_list2(ctx, filter): @cli.command(name='nfpkg-list') +@click.option('--nf_type', help='type of NFPKG (vnf, pnf, hnf)') @click.option('--filter', default=None, help='restricts the list to the xNFpkg matching the filter') @click.pass_context -def nfpkg_list(ctx, filter): +def nfpkg_list(ctx, nf_type, filter): '''list all NFpkg (VNFpkg, PNFpkg, HNFpkg) in the system''' - vnfd_list(ctx,filter) + try: + check_client_version(ctx.obj, ctx.command.name) + check_client_version(ctx.obj) + if nf_type: + if nf_type == "vnf": + nf_filter = "_admin.type=vnfd" + elif nf_type == "pnf": + nf_filter = "_admin.type=pnfd" + elif nf_type == "hnf": + nf_filter = "_admin.type=hnfd" + else: + raise ClientException('wrong value for "--nf_type" option, allowed values: vnf, pnf, hnf') + if filter: + filter = '{}&{}'.format(nf_filter, filter) + vnfd_list(ctx,filter) + except ClientException as inst: + print((inst.message)) + exit(1) @cli.command(name='vnf-list') -- 2.25.1 From 4351fa0292204622120a52b4ffd40a97a643ea8e Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 3 Dec 2018 12:33:06 +0100 Subject: [PATCH 07/16] fix pdu_create and nfpkg_list Change-Id: Id9e9dd6bfe4e9c8bc664030af4ae1846860f7aae Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 19 ++++++++----------- osmclient/sol005/pdud.py | 5 +---- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 6cce5e5..a12cfb2 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -294,7 +294,6 @@ def nfpkg_list(ctx, nf_type, filter): '''list all NFpkg (VNFpkg, PNFpkg, HNFpkg) in the system''' try: check_client_version(ctx.obj, ctx.command.name) - check_client_version(ctx.obj) if nf_type: if nf_type == "vnf": nf_filter = "_admin.type=vnfd" @@ -575,13 +574,11 @@ def pdu_list(ctx, filter): ['pdu name', 'id', 'type', - 'shared', 'mgmt ip address']) for pdu in resp: pdu_name = pdu['name'] pdu_id = pdu['_id'] pdu_type = pdu['type'] - pdu_shared = pdu['shared'] pdu_ipaddress = "None" for iface in pdu['interfaces']: if iface['mgmt']: @@ -591,7 +588,6 @@ def pdu_list(ctx, filter): [pdu_name, pdu_id, pdu_type, - pdu_shared, pdu_ipaddress]) table.align = 'l' print(table) @@ -1251,12 +1247,10 @@ def nsi_create2(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_f '[,type=][,mac-address=][,vim-network-name=]', multiple=True) @click.option('--description', help='human readable description') -@click.option('--shared', is_flag=True, help='flag to indicate if the PDU is shared') -@click.option('--vimAccounts', help='list of VIM accounts where this PDU is physically connected') +@click.option('--vim_account', help='list of VIM accounts (in the same VIM) that can reach this PDU', multiple=True) @click.option('--descriptor_file', default=None, help='PDU descriptor file (as an alternative to using the other arguments') @click.pass_context -#TODO -def pdu_create(ctx, name, pdu_type, interface, description, shared, vimAccounts, descriptor_file): +def pdu_create(ctx, name, pdu_type, interface, description, vim_account, descriptor_file): '''creates a new Physical Deployment Unit (PDU)''' try: check_client_version(ctx.obj, ctx.command.name) @@ -1268,18 +1262,21 @@ def pdu_create(ctx, name, pdu_type, interface, description, shared, vimAccounts, raise ClientException('in absence of descriptor file, option "--pdu_type" is mandatory') if not interface: raise ClientException('in absence of descriptor file, option "--interface" is mandatory (at least once)') + if not vim_account: + raise ClientException('in absence of descriptor file, option "--vim_account" is mandatory (at least once)') else: with open(descriptor_file, 'r') as df: pdu = yaml.load(df.read()) if name: pdu["name"] = name if pdu_type: pdu["type"] = pdu_type if description: pdu["description"] = description - if shared: pdu["shared"] = shared - if vimAccounts: pdu["vim_accounts"] = yaml.load(vimAccounts) + if vim_account: pdu["vim_accounts"] = vim_account if interface: ifaces_list = [] for iface in interface: - ifaces_list.append({k:v for k,v in [i.split('=') for i in iface.split(',')]}) + new_iface={k:v for k,v in [i.split('=') for i in iface.split(',')]} + new_iface["mgmt"] = (new_iface.get("mgmt","false").lower() == "true") + ifaces_list.append(new_iface) pdu["interfaces"] = ifaces_list ctx.obj.pdu.create(pdu) except ClientException as inst: diff --git a/osmclient/sol005/pdud.py b/osmclient/sol005/pdud.py index b59b91d..e8ba9c4 100644 --- a/osmclient/sol005/pdud.py +++ b/osmclient/sol005/pdud.py @@ -89,16 +89,13 @@ class Pdu(object): def create(self, pdu, update_endpoint=None): headers= self._client._headers - headers['Content-Type'] = 'text/plain' + headers['Content-Type'] = 'application/yaml' http_header = ['{}: {}'.format(key,val) for (key,val) in list(headers.items())] self._http.set_http_header(http_header) if update_endpoint: http_code, resp = self._http.put_cmd(endpoint=update_endpoint, postfields_dict=pdu) else: - self._apiResource = '/pdu_descriptors_content' - self._apiBase = '{}{}{}'.format(self._apiName, - self._apiVersion, self._apiResource) endpoint = self._apiBase #endpoint = '{}{}'.format(self._apiBase,ow_string) http_code, resp = self._http.post_cmd(endpoint=endpoint, postfields_dict=pdu) -- 2.25.1 From 44314afa209f6843e4efd60bd3b31ccadee573a2 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 3 Dec 2018 12:58:13 +0100 Subject: [PATCH 08/16] vnfd-list and vnfpkg-list to use same options as nfpkg-list Change-Id: I610ea5b4635b78e76d5217bee1216cb84e8230a1 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 51 +++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index a12cfb2..8b9a43c 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -247,9 +247,25 @@ def nsd_list2(ctx, filter): nsd_list(ctx,filter) -def vnfd_list(ctx, filter): - if filter: +def vnfd_list(ctx, nf_type, filter): + if nf_type: + check_client_version(ctx.obj, '--nf_type') + elif filter: check_client_version(ctx.obj, '--filter') + if nf_type: + if nf_type == "vnf": + nf_filter = "_admin.type=vnfd" + elif nf_type == "pnf": + nf_filter = "_admin.type=pnfd" + elif nf_type == "hnf": + nf_filter = "_admin.type=hnfd" + else: + raise ClientException('wrong value for "--nf_type" option, allowed values: vnf, pnf, hnf') + if filter: + filter = '{}&{}'.format(nf_filter, filter) + else: + filter = nf_filter + if filter: resp = ctx.obj.vnfd.list(filter) else: resp = ctx.obj.vnfd.list() @@ -268,44 +284,35 @@ def vnfd_list(ctx, filter): @cli.command(name='vnfd-list') +@click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, - help='restricts the list to the VNFD/VNFpkg matching the filter') + help='restricts the list to the NFpkg matching the filter') @click.pass_context -def vnfd_list1(ctx, filter): +def vnfd_list1(ctx, nf_type, filter): '''list all VNFD/VNFpkg in the system''' - vnfd_list(ctx,filter) + vnfd_list(ctx,nf_type,filter) @cli.command(name='vnfpkg-list') +@click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, - help='restricts the list to the VNFD/VNFpkg matching the filter') + help='restricts the list to the NFpkg matching the filter') @click.pass_context -def vnfd_list2(ctx, filter): +def vnfd_list2(ctx, nf_type, filter): '''list all VNFD/VNFpkg in the system''' - vnfd_list(ctx,filter) + vnfd_list(ctx,nf_type,filter) @cli.command(name='nfpkg-list') -@click.option('--nf_type', help='type of NFPKG (vnf, pnf, hnf)') +@click.option('--nf_type', help='type of NF (vnf, pnf, hnf)') @click.option('--filter', default=None, - help='restricts the list to the xNFpkg matching the filter') + help='restricts the list to the NFpkg matching the filter') @click.pass_context def nfpkg_list(ctx, nf_type, filter): '''list all NFpkg (VNFpkg, PNFpkg, HNFpkg) in the system''' try: check_client_version(ctx.obj, ctx.command.name) - if nf_type: - if nf_type == "vnf": - nf_filter = "_admin.type=vnfd" - elif nf_type == "pnf": - nf_filter = "_admin.type=pnfd" - elif nf_type == "hnf": - nf_filter = "_admin.type=hnfd" - else: - raise ClientException('wrong value for "--nf_type" option, allowed values: vnf, pnf, hnf') - if filter: - filter = '{}&{}'.format(nf_filter, filter) - vnfd_list(ctx,filter) + vnfd_list(ctx,nf_type,filter) except ClientException as inst: print((inst.message)) exit(1) -- 2.25.1 From 3c72a200f05f35411722aba0f4b5170a3c38fc19 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 3 Dec 2018 18:31:51 +0100 Subject: [PATCH 09/16] fix update vnfd and nsd Change-Id: Ib38af258d4980f76880a40db0fdac29fe478be57 Signed-off-by: garciadeblas --- osmclient/sol005/nsd.py | 2 ++ osmclient/sol005/vnfd.py | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osmclient/sol005/nsd.py b/osmclient/sol005/nsd.py index 6a18828..b134461 100644 --- a/osmclient/sol005/nsd.py +++ b/osmclient/sol005/nsd.py @@ -166,6 +166,8 @@ class Nsd(object): raise ClientException('unexpected response from server - {}'.format( resp)) print(resp['id']) + elif http_code == 204: + print('Updated') else: msg = "Error {}".format(http_code) if resp: diff --git a/osmclient/sol005/vnfd.py b/osmclient/sol005/vnfd.py index e939efc..965b999 100644 --- a/osmclient/sol005/vnfd.py +++ b/osmclient/sol005/vnfd.py @@ -158,13 +158,15 @@ class Vnfd(object): http_code, resp = self._http.post_cmd(endpoint=endpoint, filename=filename) #print 'HTTP CODE: {}'.format(http_code) #print 'RESP: {}'.format(resp) - if http_code in (200, 201, 202, 204): + if http_code in (200, 201, 202): if resp: resp = json.loads(resp) if not resp or 'id' not in resp: raise ClientException('unexpected response from server: '.format( resp)) print(resp['id']) + elif http_code == 204: + print('Updated') else: msg = "Error {}".format(http_code) if resp: @@ -176,6 +178,6 @@ class Vnfd(object): def update(self, name, filename): vnfd = self.get(name) - endpoint = '{}/{}/vnfd_content'.format(self._apiBase, vnfd['_id']) + endpoint = '{}/{}/package_content'.format(self._apiBase, vnfd['_id']) self.create(filename=filename, update_endpoint=endpoint) -- 2.25.1 From c5899991af05f2fb029a49e174f76d511937f34d Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 3 Dec 2018 18:52:07 +0100 Subject: [PATCH 10/16] new alias nf-list for vnf-list Change-Id: I0d1c516a2e2185ff9ce88a107b1d9e3f6cf316b0 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 8b9a43c..bbc187d 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -290,7 +290,7 @@ def vnfd_list(ctx, nf_type, filter): @click.pass_context def vnfd_list1(ctx, nf_type, filter): '''list all VNFD/VNFpkg in the system''' - vnfd_list(ctx,nf_type,filter) + vnfd_list(ctx, nf_type, filter) @cli.command(name='vnfpkg-list') @@ -300,7 +300,7 @@ def vnfd_list1(ctx, nf_type, filter): @click.pass_context def vnfd_list2(ctx, nf_type, filter): '''list all VNFD/VNFpkg in the system''' - vnfd_list(ctx,nf_type,filter) + vnfd_list(ctx, nf_type, filter) @cli.command(name='nfpkg-list') @@ -318,11 +318,6 @@ def nfpkg_list(ctx, nf_type, filter): exit(1) -@cli.command(name='vnf-list') -@click.option('--ns', default=None, help='NS instance id or name to restrict the VNF list') -@click.option('--filter', default=None, - help='restricts the list to the VNF instances matching the filter.') -@click.pass_context def vnf_list(ctx, ns, filter): '''list all VNF instances @@ -421,6 +416,25 @@ def vnf_list(ctx, ns, filter): table.align = 'l' print(table) + +@cli.command(name='vnf-list') +@click.option('--ns', default=None, help='NS instance id or name to restrict the NF list') +@click.option('--filter', default=None, + help='restricts the list to the NF instances matching the filter.') +@click.pass_context +def vnf_list1(ctx, ns, filter): + vnf_list(ctx, ns, filter) + + +@cli.command(name='nf-list') +@click.option('--ns', default=None, help='NS instance id or name to restrict the NF list') +@click.option('--filter', default=None, + help='restricts the list to the NF instances matching the filter.') +@click.pass_context +def nf_list(ctx, ns, filter): + vnf_list(ctx, ns, filter) + + @cli.command(name='ns-op-list') @click.argument('name') @click.pass_context -- 2.25.1 From 9cd835db3dad5cb8e374b2c6b265201c71246318 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Tue, 4 Dec 2018 16:58:03 +0100 Subject: [PATCH 11/16] fix help message for osm vnf-list and nf-list Change-Id: I58232f98816e0e19c8aef8a35c475db725ea43a0 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 95 ++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index bbc187d..0a1f7c3 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -319,53 +319,6 @@ def nfpkg_list(ctx, nf_type, filter): def vnf_list(ctx, ns, filter): - '''list all VNF instances - - \b - Options: - --ns TEXT NS instance id or name to restrict the VNF list - --filter filterExpr Restricts the list to the VNF instances matching the filter - - \b - filterExpr consists of one or more strings formatted according to "simpleFilterExpr", - concatenated using the "&" character: - - \b - filterExpr := ["&"]* - simpleFilterExpr := ["."]*["."]"="[","]* - op := "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont" - attrName := string - value := scalar value - - \b - where: - * zero or more occurrences - ? zero or one occurrence - [] grouping of expressions to be used with ? and * - "" quotation marks for marking string constants - <> name separator - - \b - "AttrName" is the name of one attribute in the data type that defines the representation - of the resource. The dot (".") character in "simpleFilterExpr" allows concatenation of - entries to filter by attributes deeper in the hierarchy of a structured document. - "Op" stands for the comparison operator. If the expression has concatenated - entries, it means that the operator "op" is applied to the attribute addressed by the last - entry included in the concatenation. All simple filter expressions are combined - by the "AND" logical operator. In a concatenation of entries in a , - the rightmost "attrName" entry in a "simpleFilterExpr" is called "leaf attribute". The - concatenation of all "attrName" entries except the leaf attribute is called the "attribute - prefix". If an attribute referenced in an expression is an array, an object that contains a - corresponding array shall be considered to match the expression if any of the elements in the - array matches all expressions that have the same attribute prefix. - - \b - Filter examples: - --filter vim-account-id= - --filter vnfd-ref= - --filter vdur.ip-address= - --filter vnfd-ref=,vdur.ip-address= - ''' try: if ns or filter: if ns: @@ -423,6 +376,7 @@ def vnf_list(ctx, ns, filter): help='restricts the list to the NF instances matching the filter.') @click.pass_context def vnf_list1(ctx, ns, filter): + '''list all NF instances''' vnf_list(ctx, ns, filter) @@ -432,6 +386,53 @@ def vnf_list1(ctx, ns, filter): help='restricts the list to the NF instances matching the filter.') @click.pass_context def nf_list(ctx, ns, filter): + '''list all NF instances + + \b + Options: + --ns TEXT NS instance id or name to restrict the VNF list + --filter filterExpr Restricts the list to the VNF instances matching the filter + + \b + filterExpr consists of one or more strings formatted according to "simpleFilterExpr", + concatenated using the "&" character: + + \b + filterExpr := ["&"]* + simpleFilterExpr := ["."]*["."]"="[","]* + op := "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "cont" | "ncont" + attrName := string + value := scalar value + + \b + where: + * zero or more occurrences + ? zero or one occurrence + [] grouping of expressions to be used with ? and * + "" quotation marks for marking string constants + <> name separator + + \b + "AttrName" is the name of one attribute in the data type that defines the representation + of the resource. The dot (".") character in "simpleFilterExpr" allows concatenation of + entries to filter by attributes deeper in the hierarchy of a structured document. + "Op" stands for the comparison operator. If the expression has concatenated + entries, it means that the operator "op" is applied to the attribute addressed by the last + entry included in the concatenation. All simple filter expressions are combined + by the "AND" logical operator. In a concatenation of entries in a , + the rightmost "attrName" entry in a "simpleFilterExpr" is called "leaf attribute". The + concatenation of all "attrName" entries except the leaf attribute is called the "attribute + prefix". If an attribute referenced in an expression is an array, an object that contains a + corresponding array shall be considered to match the expression if any of the elements in the + array matches all expressions that have the same attribute prefix. + + \b + Filter examples: + --filter vim-account-id= + --filter vnfd-ref= + --filter vdur.ip-address= + --filter vnfd-ref=,vdur.ip-address= + ''' vnf_list(ctx, ns, filter) -- 2.25.1 From 88f9b44b6fcf1f9be8ee84f787a79a6ef1cd507c Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Mon, 14 Jan 2019 18:17:15 +0100 Subject: [PATCH 12/16] Support of WIM accounts in osmclient Change-Id: I3395f6dc78d8060d7ae3aa3cbbef7ca9fad8713a Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 161 ++++++++++++++++++++++++++++++++++ osmclient/sol005/client.py | 2 + osmclient/sol005/wim.py | 174 +++++++++++++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 osmclient/sol005/wim.py diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 0a1f7c3..2c151ff 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -1802,6 +1802,167 @@ def vim_show(ctx, name): print(table) +#################### +# WIM operations +#################### + +@cli.command(name='wim-create') +@click.option('--name', + prompt=True, + help='Name for the WIM account') +@click.option('--user', + help='WIM username') +@click.option('--password', + help='WIM password') +@click.option('--url', + prompt=True, + help='WIM url') +# @click.option('--tenant', +# help='wIM tenant name') +@click.option('--config', + default=None, + help='WIM specific config parameters') +@click.option('--wim_type', + help='WIM type') +@click.option('--description', + default='no description', + help='human readable description') +@click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@click.pass_context +def wim_create(ctx, + name, + user, + password, + url, + # tenant, + config, + wim_type, + description, + wim_port_mapping): + '''creates a new WIM account + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + # if sdn_controller: + # check_client_version(ctx.obj, '--sdn_controller') + # if sdn_port_mapping: + # check_client_version(ctx.obj, '--sdn_port_mapping') + wim = {} + if user: wim['user'] = user + if password: wim['password'] = password + if url: wim['wim_url'] = url + # if tenant: wim['tenant'] = tenant + wim['wim_type'] = wim_type + if description: wim['description'] = description + if config: wim['config'] = config + ctx.obj.wim.create(name, wim, wim_port_mapping) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='wim-update', short_help='updates a WIM account') +@click.argument('name') +@click.option('--newname', help='New name for the WIM account') +@click.option('--user', help='WIM username') +@click.option('--password', help='WIM password') +@click.option('--url', help='WIM url') +@click.option('--config', help='WIM specific config parameters') +@click.option('--wim_type', help='WIM type') +@click.option('--description', help='human readable description') +@click.option('--wim_port_mapping', default=None, help="File describing the port mapping between DC edge (datacenters, switches, ports) and WAN edge (WAN service endpoint id and info)") +@click.pass_context +def wim_update(ctx, + name, + newname, + user, + password, + url, + config, + wim_type, + description, + wim_port_mapping): + '''updates a WIM account + + NAME: name or ID of the WIM account + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + wim = {} + if newname: wim['name'] = newname + if user: wim['user'] = user + if password: wim['password'] = password + if url: wim['url'] = url + # if tenant: wim['tenant'] = tenant + if wim_type: wim['wim_type'] = wim_type + if description: wim['description'] = description + if config: wim['config'] = config + ctx.obj.wim.update(name, wim, wim_port_mapping) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='wim-delete') +@click.argument('name') +@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def wim_delete(ctx, name, force): + '''deletes a WIM account + + NAME: name or ID of the WIM account to be deleted + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.wim.delete(name, force) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='wim-list') +@click.option('--filter', default=None, + help='restricts the list to the WIM accounts matching the filter') +@click.pass_context +def wim_list(ctx, filter): + '''list all WIM accounts''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.wim.list(filter) + table = PrettyTable(['wim name', 'uuid']) + for wim in resp: + table.add_row([wim['name'], wim['uuid']]) + table.align = 'l' + print(table) + except ClientException as inst: + print((inst.message)) + exit(1) + + +@cli.command(name='wim-show') +@click.argument('name') +@click.pass_context +def wim_show(ctx, name): + '''shows the details of a WIM account + + NAME: name or ID of the WIM account + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.wim.get(name) + if 'password' in resp: + resp['wim_password']='********' + except ClientException as inst: + print((inst.message)) + exit(1) + + table = PrettyTable(['key', 'attribute']) + for k, v in list(resp.items()): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + #################### # SDN controller operations #################### diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index c70be23..32e8fb9 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -26,6 +26,7 @@ from osmclient.sol005 import nsi from osmclient.sol005 import ns from osmclient.sol005 import vnf from osmclient.sol005 import vim +from osmclient.sol005 import wim from osmclient.sol005 import package from osmclient.sol005 import http from osmclient.sol005 import sdncontroller @@ -83,6 +84,7 @@ class Client(object): self.ns = ns.Ns(self._http_client, client=self) self.nsi = nsi.Nsi(self._http_client, client=self) self.vim = vim.Vim(self._http_client, client=self) + self.wim = wim.Wim(self._http_client, client=self) self.sdnc = sdncontroller.SdnController(self._http_client, client=self) self.vnf = vnf.Vnf(self._http_client, client=self) self.project = projectmodule.Project(self._http_client, client=self) diff --git a/osmclient/sol005/wim.py b/osmclient/sol005/wim.py new file mode 100644 index 0000000..d2721cd --- /dev/null +++ b/osmclient/sol005/wim.py @@ -0,0 +1,174 @@ +# 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 wim API handling +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import yaml +import json + + +class Wim(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/wim_accounts' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + def create(self, name, wim_input, wim_port_mapping=None): + if 'wim_type' not in wim_input: + raise Exception("wim type not provided") + + wim_account = wim_input + wim_account["name"] = name + + wim_config = {} + if 'config' in wim_input and wim_input['config'] is not None: + wim_config = yaml.safe_load(wim_input['config']) + if wim_port_mapping: + with open(wim_port_mapping, 'r') as f: + wim_config['wim_port_mapping'] = yaml.safe_load(f.read()) + if wim_config: + wim_account['config'] = wim_config + #wim_account['config'] = json.dumps(wim_config) + + http_code, resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=wim_account) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + if resp: + resp = json.loads(resp) + if not resp or 'id' not in resp: + raise ClientException('unexpected response from server - {}'.format( + resp)) + print(resp['id']) + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to create wim {} - {}".format(name, msg)) + + def update(self, wim_name, wim_account, wim_port_mapping=None): + wim = self.get(wim_name) + + wim_config = {} + if 'config' in wim_account: + if wim_account.get('config')=="" and (wim_port_mapping): + raise ClientException("clearing config is incompatible with updating SDN info") + if wim_account.get('config')=="": + wim_config = None + else: + wim_config = yaml.safe_load(wim_account['config']) + if wim_port_mapping: + with open(wim_port_mapping, 'r') as f: + wim_config['wim_port_mapping'] = yaml.safe_load(f.read()) + wim_account['config'] = wim_config + #wim_account['config'] = json.dumps(wim_config) + http_code, resp = self._http.put_cmd(endpoint='{}/{}'.format(self._apiBase,wim['_id']), + postfields_dict=wim_account) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code in (200, 201, 202, 204): + pass + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to update wim {} - {}".format(wim_name, msg)) + + def update_wim_account_dict(self, wim_account, wim_input): + print (wim_input) + wim_account['wim_type'] = wim_input['wim_type'] + wim_account['description'] = wim_input['description'] + wim_account['wim_url'] = wim_input['url'] + wim_account['user'] = wim_input.get('wim-username') + wim_account['password'] = wim_input.get('wim-password') + return wim_account + + def get_id(self, name): + """Returns a VIM id from a VIM name + """ + for wim in self.list(): + if name == wim['name']: + return wim['uuid'] + raise NotFound("wim {} not found".format(name)) + + def delete(self, wim_name, force=False): + wim_id = wim_name + if not utils.validate_uuid4(wim_name): + wim_id = self.get_id(wim_name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + wim_id, querystring)) + #print 'HTTP CODE: {}'.format(http_code) + #print 'RESP: {}'.format(resp) + if http_code == 202: + print('Deletion in progress') + elif http_code == 204: + print('Deleted') + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete wim {} - {}".format(wim_name, msg)) + + 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() + wim_accounts = [] + for datacenter in resp: + wim_accounts.append({"name": datacenter['name'], "uuid": datacenter['_id'] + if '_id' in datacenter else None}) + return wim_accounts + + def get(self, name): + """Returns a VIM account based on name or id + """ + wim_id = name + if not utils.validate_uuid4(name): + wim_id = self.get_id(name) + resp = self._http.get_cmd('{}/{}'.format(self._apiBase,wim_id)) + if not resp or '_id' not in resp: + raise ClientException('failed to get wim info: '.format( + resp)) + else: + return resp + raise NotFound("wim {} not found".format(name)) + -- 2.25.1 From c9a3f9a87f0dde283341ba13ea6d430aff5b9634 Mon Sep 17 00:00:00 2001 From: tierno Date: Mon, 21 Jan 2019 14:57:13 +0000 Subject: [PATCH 13/16] adding SOL005 additionalParams to ns/nsi creation Change-Id: Ia871bd316a94af03acec1863dd2b8acfe43b3505 Signed-off-by: tierno --- osmclient/scripts/osm.py | 8 +++++--- osmclient/sol005/ns.py | 20 +++++++++++++++++++- osmclient/sol005/nsi.py | 20 +++++++++++++++++++- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 2c151ff..0a4c465 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -1207,9 +1207,9 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_fi with open(config_file, 'r') as cf: config=cf.read() ctx.obj.nsi.create(nst_name, nsi_name, config=config, ssh_keys=ssh_keys, - account=vim_account) + account=vim_account) except ClientException as inst: - print((inst.message)) + print(inst.message) exit(1) @@ -1224,7 +1224,9 @@ def nsi_create(ctx, nst_name, nsi_name, vim_account, ssh_keys, config, config_fi 'netslice_subnet: [\n' 'id: TEXT, vim_account: TEXT,\n' 'vnf: [member-vnf-index: TEXT, vim_account: TEXT]\n' - 'vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' + 'vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]\n' + 'additionalParamsForNsi: {param: value, ...}\n' + 'additionalParamsForsubnet: [{id: SUBNET_ID, additionalParamsForNs: {}, additionalParamsForVnf: {}}]\n' '],\n' 'netslice-vld: [name: TEXT, vim-network-name: TEXT or DICT with vim_account, vim_net entries]' ) diff --git a/osmclient/sol005/ns.py b/osmclient/sol005/ns.py index 9d1fe95..bd72d42 100644 --- a/osmclient/sol005/ns.py +++ b/osmclient/sol005/ns.py @@ -147,7 +147,25 @@ class Ns(object): vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) ns["vnf"] = ns_config["vnf"] - #print yaml.safe_dump(ns) + if "additionalParamsForNs" in ns_config: + ns["additionalParamsForNs"] = ns_config.pop("additionalParamsForNs") + if not isinstance(ns["additionalParamsForNs"], dict): + raise ValueError("Error at --config 'additionalParamsForNs' must be a dictionary") + if "additionalParamsForVnf" in ns_config: + ns["additionalParamsForVnf"] = ns_config.pop("additionalParamsForVnf") + if not isinstance(ns["additionalParamsForVnf"], list): + raise ValueError("Error at --config 'additionalParamsForVnf' must be a list") + for additional_param_vnf in ns["additionalParamsForVnf"]: + if not isinstance(additional_param_vnf, dict): + raise ValueError("Error at --config 'additionalParamsForVnf' items must be dictionaries") + if not additional_param_vnf.get("member-vnf-index"): + raise ValueError("Error at --config 'additionalParamsForVnf' items must contain " + "'member-vnf-index'") + if not additional_param_vnf.get("additionalParams"): + raise ValueError("Error at --config 'additionalParamsForVnf' items must contain " + "'additionalParams'") + + # print yaml.safe_dump(ns) try: self._apiResource = '/ns_instances_content' self._apiBase = '{}{}{}'.format(self._apiName, diff --git a/osmclient/sol005/nsi.py b/osmclient/sol005/nsi.py index f6009dd..cb87b85 100644 --- a/osmclient/sol005/nsi.py +++ b/osmclient/sol005/nsi.py @@ -161,7 +161,25 @@ class Nsi(object): vnf["vimAccountId"] = get_vim_account_id(vnf.pop("vim_account")) nsi["netslice-subnet"] = nsi_config["netslice-subnet"] - #print yaml.safe_dump(nsi) + if "additionalParamsForNsi" in nsi_config: + nsi["additionalParamsForNsi"] = nsi_config.pop("additionalParamsForNsi") + if not isinstance(nsi["additionalParamsForNsi"], dict): + raise ValueError("Error at --config 'additionalParamsForNsi' must be a dictionary") + if "additionalParamsForSubnet" in nsi_config: + nsi["additionalParamsForSubnet"] = nsi_config.pop("additionalParamsForSubnet") + if not isinstance(nsi["additionalParamsForSubnet"], list): + raise ValueError("Error at --config 'additionalParamsForSubnet' must be a list") + for additional_param_subnet in nsi["additionalParamsForSubnet"]: + if not isinstance(additional_param_subnet, dict): + raise ValueError("Error at --config 'additionalParamsForSubnet' items must be dictionaries") + if not additional_param_subnet.get("id"): + raise ValueError("Error at --config 'additionalParamsForSubnet' items must contain subnet 'id'") + if not additional_param_subnet.get("additionalParamsForNs") and\ + not additional_param_subnet.get("additionalParamsForVnf"): + raise ValueError("Error at --config 'additionalParamsForSubnet' items must contain " + "'additionalParamsForNs' and/or 'additionalParamsForVnf'") + + # print yaml.safe_dump(nsi) try: self._apiResource = '/netslice_instances_content' self._apiBase = '{}{}{}'.format(self._apiName, -- 2.25.1 From cbcb47feee334c67024ed7cabfade091accde04a Mon Sep 17 00:00:00 2001 From: tierno Date: Tue, 29 Jan 2019 11:43:10 +0000 Subject: [PATCH 14/16] bug 624 fix user management for sol005 Change-Id: I3aa13a9f9c5c22411af90ba98dbe5d0d5a0cfd5d Signed-off-by: tierno --- osmclient/scripts/osm.py | 5 +++-- osmclient/sol005/user.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 0a4c465..194196b 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -2236,7 +2236,8 @@ def project_show(ctx, name): confirmation_prompt=True, help='user password') @click.option('--projects', - default=None, + prompt=True, + multiple=True, help='list of project ids that the user belongs to') #@click.option('--description', # default='no description', @@ -2289,7 +2290,7 @@ def user_list(ctx, filter): exit(1) table = PrettyTable(['name', 'id']) for user in resp: - table.add_row([user['name'], user['_id']]) + table.add_row([user['username'], user['_id']]) table.align = 'l' print(table) diff --git a/osmclient/sol005/user.py b/osmclient/sol005/user.py index ee572f1..ad8027d 100644 --- a/osmclient/sol005/user.py +++ b/osmclient/sol005/user.py @@ -23,7 +23,7 @@ from osmclient.common import utils from osmclient.common.exceptions import ClientException from osmclient.common.exceptions import NotFound import json -import yaml +# import yaml class User(object): @@ -39,8 +39,6 @@ class User(object): def create(self, name, user): """Creates a new OSM user """ - if 'projects' in user and user['projects'] is not None: - user['projects'] = yaml.safe_load(user['projects']) http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=user) #print('HTTP CODE: {}'.format(http_code)) @@ -132,7 +130,7 @@ class User(object): return user else: for user in self.list(): - if name == user['name']: + if name == user['username']: return user raise NotFound("User {} not found".format(name)) -- 2.25.1 From 9f8f7aef0f26522bc6a22526a88df2ba8f1f5e46 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Wed, 30 Jan 2019 11:26:22 +0100 Subject: [PATCH 15/16] osm.py: updated table header for vnfd-list and aliases to be agnostic of VNF type Change-Id: I2823aff813aa75a4c21672283c1409e7b478c23f Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 194196b..821c1a9 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -270,7 +270,7 @@ def vnfd_list(ctx, nf_type, filter): else: resp = ctx.obj.vnfd.list() #print yaml.safe_dump(resp) - table = PrettyTable(['vnfd name', 'id']) + table = PrettyTable(['nfpkg name', 'id']) fullclassname = ctx.obj.__module__ + "." + ctx.obj.__class__.__name__ if fullclassname == 'osmclient.sol005.client.Client': for vnfd in resp: -- 2.25.1 From fdc97c8f4c123dcb23865e08d187df025123a7f8 Mon Sep 17 00:00:00 2001 From: tierno Date: Wed, 13 Mar 2019 09:55:51 +0000 Subject: [PATCH 16/16] fix bug 647 user creation with project prompt Change-Id: I6d006d25b33ec8693ffaf71252c6cc009a63b5ef Signed-off-by: tierno --- osmclient/scripts/osm.py | 3 ++- osmclient/sol005/user.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index 821c1a9..7aa9e39 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -2236,8 +2236,9 @@ def project_show(ctx, name): confirmation_prompt=True, help='user password') @click.option('--projects', - prompt=True, + prompt="Comma separate list of projects", multiple=True, + callback=lambda ctx, param, value: ''.join(value).split(',') if all(len(x)==1 for x in value) else value, help='list of project ids that the user belongs to') #@click.option('--description', # default='no description', diff --git a/osmclient/sol005/user.py b/osmclient/sol005/user.py index ad8027d..29635f9 100644 --- a/osmclient/sol005/user.py +++ b/osmclient/sol005/user.py @@ -39,6 +39,8 @@ class User(object): def create(self, name, user): """Creates a new OSM user """ + if len(user["projects"]) == 1: + user["projects"] = user["projects"][0].split(",") http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=user) #print('HTTP CODE: {}'.format(http_code)) -- 2.25.1