From 6991001536f5cd7b4bda5d9c95e09646048cf9f3 Mon Sep 17 00:00:00 2001 From: garciadeblas Date: Tue, 29 May 2018 14:15:43 +0200 Subject: [PATCH] Support of user and project mgmt in sol005 client Change-Id: I5880e0cb1efecdbed8c5c3ab800e5e2ed6cda517 Signed-off-by: garciadeblas --- osmclient/scripts/osm.py | 210 +++++++++++++++++++++++++++++- osmclient/sol005/client.py | 26 ++-- osmclient/sol005/project.py | 136 +++++++++++++++++++ osmclient/sol005/sdncontroller.py | 1 + osmclient/sol005/user.py | 139 ++++++++++++++++++++ 5 files changed, 491 insertions(+), 21 deletions(-) create mode 100644 osmclient/sol005/project.py create mode 100644 osmclient/sol005/user.py diff --git a/osmclient/scripts/osm.py b/osmclient/scripts/osm.py index c80c3eb..c07b62d 100755 --- a/osmclient/scripts/osm.py +++ b/osmclient/scripts/osm.py @@ -50,6 +50,26 @@ def check_client_version(obj, what, version='sol005'): envvar='OSM_HOSTNAME', help='hostname of server. ' + 'Also can set OSM_HOSTNAME in environment') +@click.option('--sol005/--no-sol005', + default=True, + envvar='OSM_SOL005', + help='Use ETSI NFV SOL005 API (default) or the previous SO API. ' + + 'Also can set OSM_SOL005 in environment') +@click.option('--user', + default=None, + envvar='OSM_USER', + help='user (only from Release FOUR, defaults to admin). ' + + 'Also can set OSM_USER in environment') +@click.option('--password', + default=None, + envvar='OSM_PASSWORD', + help='password (only from Release FOUR, defaults to admin). ' + + 'Also can set OSM_PASSWORD in environment') +@click.option('--project', + default=None, + envvar='OSM_PROJECT', + help='project (only from Release FOUR, defaults to admin). ' + + 'Also can set OSM_PROJECT in environment') @click.option('--so-port', default=None, envvar='OSM_SO_PORT', @@ -66,16 +86,12 @@ def check_client_version(obj, what, version='sol005'): help='hostname of RO server. ' + 'Also can set OSM_RO_HOSTNAME in environment') @click.option('--ro-port', - default=9090, + default=None, envvar='OSM_RO_PORT', help='hostname of RO server. ' + 'Also can set OSM_RO_PORT in environment') -@click.option('--sol005/--no-sol005', - default=True, - envvar='OSM_SOL005', - help='Use ETSI NFV SOL005 API (default) or the previous SO API') @click.pass_context -def cli(ctx, hostname, so_port, so_project, ro_hostname, ro_port, sol005): +def cli(ctx, hostname, sol005, user, password, project, so_port, so_project, ro_hostname, ro_port): if hostname is None: print(( "either hostname option or OSM_HOSTNAME " + @@ -90,6 +106,12 @@ def cli(ctx, hostname, so_port, so_project, ro_hostname, ro_port, sol005): kwargs['ro_host']=ro_hostname if ro_port is not None: kwargs['ro_port']=ro_port + if user is not None: + kwargs['user']=user + if password is not None: + kwargs['password']=password + if project is not None: + kwargs['project']=project ctx.obj = client.Client(host=hostname, sol005=sol005, **kwargs) @@ -1269,6 +1291,182 @@ def sdnc_show(ctx, name): print(table) +#################### +# Project mgmt operations +#################### + +@cli.command(name='project-create') +@click.argument('name') +#@click.option('--description', +# default='no description', +# help='human readable description') +@click.pass_context +def project_create(ctx, name): + '''Creates a new project + + NAME: name of the project + ''' + project = {} + project['name'] = name + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.project.create(name, project) + except ClientException as inst: + print(inst.message) + + +@cli.command(name='project-delete') +@click.argument('name') +#@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def project_delete(ctx, name): + '''deletes a project + + NAME: name or ID of the project to be deleted + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.project.delete(name) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='project-list') +@click.option('--filter', default=None, + help='restricts the list to the projects matching the filter') +@click.pass_context +def project_list(ctx, filter): + '''list all projects''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.project.list(filter) + except ClientException as inst: + print(inst.message) + exit(1) + table = PrettyTable(['name', 'id']) + for proj in resp: + table.add_row([proj['name'], proj['_id']]) + table.align = 'l' + print(table) + + +@cli.command(name='project-show') +@click.argument('name') +@click.pass_context +def project_show(ctx, name): + '''shows the details of a project + + NAME: name or ID of the project + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.project.get(name) + except ClientException as inst: + print(inst.message) + exit(1) + + table = PrettyTable(['key', 'attribute']) + for k, v in resp.items(): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + +#################### +# User mgmt operations +#################### + +@cli.command(name='user-create') +@click.argument('username') +@click.option('--password', + prompt=True, + hide_input=True, + confirmation_prompt=True, + help='user password') +@click.option('--projects', + default=None, + help='list of project ids that the user belongs to') +#@click.option('--description', +# default='no description', +# help='human readable description') +@click.pass_context +def user_create(ctx, username, password, projects): + '''Creates a new user + + USERNAME: name of the user + ''' + user = {} + user['username'] = username + user['password'] = password + user['projects'] = projects + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.user.create(username, user) + except ClientException as inst: + print(inst.message) + + +@cli.command(name='user-delete') +@click.argument('name') +#@click.option('--force', is_flag=True, help='forces the deletion bypassing pre-conditions') +@click.pass_context +def user_delete(ctx, name): + '''deletes a user + + NAME: name or ID of the user to be deleted + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + ctx.obj.user.delete(name) + except ClientException as inst: + print(inst.message) + exit(1) + + +@cli.command(name='user-list') +@click.option('--filter', default=None, + help='restricts the list to the users matching the filter') +@click.pass_context +def user_list(ctx, filter): + '''list all users''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.user.list(filter) + except ClientException as inst: + print(inst.message) + exit(1) + table = PrettyTable(['name', 'id']) + for user in resp: + table.add_row([user['name'], user['_id']]) + table.align = 'l' + print(table) + + +@cli.command(name='user-show') +@click.argument('name') +@click.pass_context +def user_show(ctx, name): + '''shows the details of a user + + NAME: name or ID of the user + ''' + try: + check_client_version(ctx.obj, ctx.command.name) + resp = ctx.obj.user.get(name) + if 'password' in resp: + resp['password']='********' + except ClientException as inst: + print(inst.message) + exit(1) + + table = PrettyTable(['key', 'attribute']) + for k, v in resp.items(): + table.add_row([k, json.dumps(v, indent=2)]) + table.align = 'l' + print(table) + + #################### # Fault Management operations #################### diff --git a/osmclient/sol005/client.py b/osmclient/sol005/client.py index 479fc9a..5de4ff0 100644 --- a/osmclient/sol005/client.py +++ b/osmclient/sol005/client.py @@ -27,6 +27,8 @@ from osmclient.sol005 import vim from osmclient.sol005 import package 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.common.exceptions import ClientException import json @@ -36,15 +38,14 @@ class Client(object): self, host=None, so_port=9999, - so_project='admin', - ro_host=None, - ro_port=9090, + user='admin', + password='admin', + project='admin', **kwargs): - self._user = 'admin' - self._password = 'admin' - #self._project = so_project - self._project = 'admin' + self._user = user + self._password = password + self._project = project self._auth_endpoint = '/admin/v1/tokens' self._headers = {} @@ -56,13 +57,6 @@ class Client(object): self._host = host self._so_port = so_port - if ro_host is None: - ro_host = host - ro_http_client = http.Http('http://{}:{}/openmano'.format(ro_host, ro_port)) - ro_http_client.set_http_header( - ['Accept: application/json', - 'Content-Type: application/json']) - self._http_client = http.Http( 'https://{}:{}/osm'.format(self._host,self._so_port)) self._headers['Accept'] = 'application/json' @@ -86,6 +80,8 @@ class Client(object): 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) + self.project = projectmodule.Project(self._http_client, client=self) + self.user = usermodule.User(self._http_client, client=self) ''' self.vca = vca.Vca(http_client, client=self, **kwargs) self.utils = utils.Utils(http_client, **kwargs) @@ -94,7 +90,7 @@ class Client(object): def get_token(self): postfields_dict = {'username': self._user, 'password': self._password, - 'project-id': self._project} + 'project_id': self._project} http_code, resp = self._http_client.post_cmd(endpoint=self._auth_endpoint, postfields_dict=postfields_dict) if http_code not in (200, 201, 202, 204): diff --git a/osmclient/sol005/project.py b/osmclient/sol005/project.py new file mode 100644 index 0000000..5179f2e --- /dev/null +++ b/osmclient/sol005/project.py @@ -0,0 +1,136 @@ +# +# Copyright 2018 Telefonica Investigacion y Desarrollo S.A.U. +# +# 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 project mgmt API +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import json + + +class Project(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/projects' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + def create(self, name, project): + """Creates a new OSM project + """ + http_code, resp = self._http.post_cmd(endpoint=self._apiBase, + postfields_dict=project) + #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 project {} - {}".format(name, msg)) + + def update(self, name, project): + """Updates an OSM project identified by name + """ + proj = self.get(name) + http_code, resp = self._http.put_cmd(endpoint='{}/{}'.format(self._apiBase,proj['_id']), + postfields_dict=project) + #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 update project {} - {}".format(name, msg)) + + def delete(self, name, force=False): + """Deletes an OSM project identified by name + """ + project = self.get(name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + project['_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') + elif resp and 'result' in resp: + print('Deleted') + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete project {} - {}".format(name, msg)) + + def list(self, filter=None): + """Returns the list of OSM projects + """ + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + #print('RESP: {}'.format(resp)) + if resp: + return resp + return list() + + def get(self, name): + """Returns a specific OSM project based on name or id + """ + if utils.validate_uuid4(name): + for proj in self.list(): + if name == proj['_id']: + return proj + else: + for proj in self.list(): + if name == proj['name']: + return proj + raise NotFound("Project {} not found".format(name)) + + diff --git a/osmclient/sol005/sdncontroller.py b/osmclient/sol005/sdncontroller.py index 6129961..391089f 100644 --- a/osmclient/sol005/sdncontroller.py +++ b/osmclient/sol005/sdncontroller.py @@ -33,6 +33,7 @@ class SdnController(object): self._apiResource = '/sdns' self._apiBase = '{}{}{}'.format(self._apiName, self._apiVersion, self._apiResource) + def create(self, name, sdn_controller): http_code, resp = self._http.post_cmd(endpoint=self._apiBase, postfields_dict=sdn_controller) diff --git a/osmclient/sol005/user.py b/osmclient/sol005/user.py new file mode 100644 index 0000000..ee572f1 --- /dev/null +++ b/osmclient/sol005/user.py @@ -0,0 +1,139 @@ +# +# Copyright 2018 Telefonica Investigacion y Desarrollo S.A.U. +# +# 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 user mgmt API +""" + +from osmclient.common import utils +from osmclient.common.exceptions import ClientException +from osmclient.common.exceptions import NotFound +import json +import yaml + + +class User(object): + def __init__(self, http=None, client=None): + self._http = http + self._client = client + self._apiName = '/admin' + self._apiVersion = '/v1' + self._apiResource = '/users' + self._apiBase = '{}{}{}'.format(self._apiName, + self._apiVersion, self._apiResource) + + 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)) + #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 user {} - {}".format(name, msg)) + + def update(self, name, user): + """Updates an existing OSM user identified by name + """ + myuser = self.get(name) + http_code, resp = self._http.put_cmd(endpoint='{}/{}'.format(self._apiBase,myuser['_id']), + postfields_dict=user) + #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 update user {} - {}".format(name, msg)) + + def delete(self, name, force=False): + """Deletes an existing OSM user identified by name + """ + user = self.get(name) + querystring = '' + if force: + querystring = '?FORCE=True' + http_code, resp = self._http.delete_cmd('{}/{}{}'.format(self._apiBase, + user['_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') + elif resp and 'result' in resp: + print('Deleted') + else: + msg = "" + if resp: + try: + msg = json.loads(resp) + except ValueError: + msg = resp + raise ClientException("failed to delete user {} - {}".format(name, msg)) + + def list(self, filter=None): + """Returns the list of OSM users + """ + filter_string = '' + if filter: + filter_string = '?{}'.format(filter) + resp = self._http.get_cmd('{}{}'.format(self._apiBase,filter_string)) + #print('RESP: {}'.format(resp)) + if resp: + return resp + return list() + + def get(self, name): + """Returns an OSM user based on name or id + """ + if utils.validate_uuid4(name): + for user in self.list(): + if name == user['_id']: + return user + else: + for user in self.list(): + if name == user['name']: + return user + raise NotFound("User {} not found".format(name)) + + -- 2.17.1