restructure osmclient 63/1763/1
authorMike Marchetti <mmarchetti@sandvine.com>
Thu, 4 May 2017 19:06:26 +0000 (15:06 -0400)
committerMike Marchetti <mmarchetti@sandvine.com>
Thu, 4 May 2017 19:06:26 +0000 (15:06 -0400)
- add layering
- add unit tests
- fixup apis for system test usage

Signed-off-by: Mike Marchetti <mmarchetti@sandvine.com>
26 files changed:
.gitignore [new file with mode: 0644]
osmclient/__init__.py
osmclient/client.py [new file with mode: 0644]
osmclient/common/__init__.py
osmclient/common/exceptions.py [new file with mode: 0644]
osmclient/common/http.py [new file with mode: 0644]
osmclient/common/test/test_utils.py [new file with mode: 0644]
osmclient/common/utils.py [new file with mode: 0644]
osmclient/scripts/__init__.py
osmclient/scripts/osm.py
osmclient/v1/__init__.py [new file with mode: 0644]
osmclient/v1/client.py [new file with mode: 0644]
osmclient/v1/key.py [new file with mode: 0644]
osmclient/v1/ns.py [new file with mode: 0644]
osmclient/v1/nsd.py [new file with mode: 0644]
osmclient/v1/package.py [new file with mode: 0644]
osmclient/v1/tests/test_ns.py [new file with mode: 0644]
osmclient/v1/tests/test_nsd.py [new file with mode: 0644]
osmclient/v1/tests/test_package.py [new file with mode: 0644]
osmclient/v1/tests/test_vnf.py [new file with mode: 0644]
osmclient/v1/tests/test_vnfd.py [new file with mode: 0644]
osmclient/v1/utils.py [new file with mode: 0644]
osmclient/v1/vca.py [new file with mode: 0644]
osmclient/v1/vim.py [new file with mode: 0644]
osmclient/v1/vnf.py [new file with mode: 0644]
osmclient/v1/vnfd.py [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..0d20b64
--- /dev/null
@@ -0,0 +1 @@
+*.pyc
index e69de29..0658c88 100644 (file)
@@ -0,0 +1,14 @@
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
diff --git a/osmclient/client.py b/osmclient/client.py
new file mode 100644 (file)
index 0000000..0c244ed
--- /dev/null
@@ -0,0 +1,28 @@
+# Copyright 2017 Sandvine
+#
+# 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 client entry point
+"""
+
+from osmclient.v1 import client
+
+
+def Client(version=1, host = None, *args, **kwargs):
+    if version == 1:
+        return client.Client(host, *args, **kwargs)
+    else:
+        raise Exception("Unsupported client version")
index e69de29..2bf7fed 100644 (file)
@@ -0,0 +1,15 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
diff --git a/osmclient/common/exceptions.py b/osmclient/common/exceptions.py
new file mode 100644 (file)
index 0000000..eb5fc72
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+
+class ClientException(Exception):
+    pass
+
+class NotFound(ClientException):
+    pass
diff --git a/osmclient/common/http.py b/osmclient/common/http.py
new file mode 100644 (file)
index 0000000..a748622
--- /dev/null
@@ -0,0 +1,83 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from io import BytesIO
+import pycurl
+import json
+
+
+class Http(object):
+
+    def __init__(self,url,user='admin',password='admin'):
+        self._url = url
+        self._user = user
+        self._password = password
+        self._http_header = None
+
+    def set_http_header(self,header):
+        self._http_header = header
+
+    def _get_curl_cmd(self,endpoint):
+        curl_cmd = pycurl.Curl()
+        curl_cmd.setopt(pycurl.URL, self._url + endpoint )
+        curl_cmd.setopt(pycurl.SSL_VERIFYPEER,0)
+        curl_cmd.setopt(pycurl.SSL_VERIFYHOST,0)
+        curl_cmd.setopt(pycurl.USERPWD, '{}:{}'.format(self._user,self._password))
+        if self._http_header:
+            curl_cmd.setopt(pycurl.HTTPHEADER, self._http_header)
+        return curl_cmd
+
+    def get_cmd( self, endpoint ):
+
+        data = BytesIO()
+        curl_cmd=self._get_curl_cmd(endpoint)
+        curl_cmd.setopt(pycurl.HTTPGET,1)
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        curl_cmd.perform() 
+        curl_cmd.close()
+        if data.getvalue():
+            return json.loads(data.getvalue().decode())
+        return None
+
+    def delete_cmd( self, endpoint ):
+        data = BytesIO()
+        curl_cmd=self._get_curl_cmd(endpoint)
+        curl_cmd.setopt(pycurl.CUSTOMREQUEST, "DELETE")
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        curl_cmd.perform() 
+        curl_cmd.close()
+        if data.getvalue():
+            return json.loads(data.getvalue().decode())
+        return None
+
+    def post_cmd( self, endpoint='', postfields_dict=None, formfile=None, ):
+        data = BytesIO()
+        curl_cmd=self._get_curl_cmd(endpoint)
+        curl_cmd.setopt(pycurl.POST,1)
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+
+        if postfields_dict is not None:
+            jsondata=json.dumps(postfields_dict)
+            curl_cmd.setopt(pycurl.POSTFIELDS,jsondata)
+
+        if formfile is not None:
+            curl_cmd.setopt(pycurl.HTTPPOST,[((formfile[0],(pycurl.FORM_FILE,formfile[1])))])
+
+        curl_cmd.perform() 
+        curl_cmd.close()
+        if data.getvalue():
+            return json.loads(data.getvalue().decode())
+        return None
diff --git a/osmclient/common/test/test_utils.py b/osmclient/common/test/test_utils.py
new file mode 100644 (file)
index 0000000..a14b636
--- /dev/null
@@ -0,0 +1,60 @@
+#  Copyright 2017 Sandvine
+#
+# 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.
+
+
+import unittest
+from osmclient.common import utils
+
+class TestUtil(unittest.TestCase):
+
+   def test_wait_for_method_basic(self):
+       def foobar():
+           return True
+       assert utils.wait_for_value(lambda: foobar())
+
+   def test_wait_for_method_timeout(self):
+       def foobar():
+           return False 
+       assert not utils.wait_for_value(lambda: foobar(),wait_time=0)
+
+   def test_wait_for_method_paramter(self):
+       def foobar(input):
+           return input
+       assert not utils.wait_for_value(lambda: foobar(False),wait_time=0)
+       assert utils.wait_for_value(lambda: foobar(True),wait_time=0)
+
+   def test_wait_for_method_wait_for_change(self):
+       def foobar():
+           if foobar.counter == 0:
+               return True
+           foobar.counter -=1
+           return False
+       foobar.counter=1
+       assert utils.wait_for_value(lambda: foobar(),wait_time=1)
+
+   def test_wait_for_method_exception(self):
+       def foobar():
+           raise Exception('send exception')
+       assert not utils.wait_for_value(lambda: foobar(),wait_time=0,catch_exception=Exception)
+
+   def test_wait_for_method_first_exception(self):
+       def foobar():
+           if foobar.counter == 0:
+               return True
+           foobar.counter -=1
+           raise Exception('send exception')
+       foobar.counter=1
+       assert utils.wait_for_value(lambda: foobar(),wait_time=1,catch_exception=Exception)
diff --git a/osmclient/common/utils.py b/osmclient/common/utils.py
new file mode 100644 (file)
index 0000000..8e78459
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import time
+
+
+def wait_for_value(func, result=True, wait_time=10, catch_exception = None ):
+    maxtime = time.time() + wait_time
+    while time.time() < maxtime:
+        try:
+            if func() == result:
+                return True 
+        except catch_exception as inst:
+            pass
+        time.sleep(1)
+    try:
+        return func() == result
+    except catch_exception as inst:
+        return False
index e69de29..2bf7fed 100644 (file)
@@ -0,0 +1,15 @@
+# Copyright 2017 Sandvine
+#
+# 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.
index d74580c..6f1c162 100755 (executable)
@@ -1,8 +1,28 @@
-import click
-from osmclient.common import OsmAPI
+# Copyright 2017 Sandvine
+#
+# 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 shell/cli
+"""
+
+import click 
+from osmclient.client import client
+from osmclient.common.exceptions import ClientException
 from prettytable import PrettyTable
-import pprint
-import textwrap
+import json
+
 
 @click.group()
 @click.option('--hostname',default=None,envvar='OSM_HOSTNAME',help='hostname of server.  Also can set OSM_HOSTNAME in environment')
@@ -11,42 +31,81 @@ def cli(ctx,hostname):
     if hostname is None:
         print("either hostname option or OSM_HOSTNAME environment variable needs to be specified") 
         exit(1)
-    ctx.obj=OsmAPI.OsmAPI(hostname)
+    ctx.obj=client.Client(host=hostname)
 
 @cli.command(name='ns-list')
 @click.pass_context
 def ns_list(ctx):
-    ctx.obj.list_ns_instance()
+    resp=ctx.obj.ns.list()
+    table=PrettyTable(['ns instance name','id','operational status','config status'])
+    for ns in resp:
+        nsopdata=ctx.obj.ns.get_opdata(ns['id'])
+        nsr=nsopdata['nsr:nsr']
+        table.add_row([nsr['name-ref'],nsr['ns-instance-config-ref'],nsr['operational-status'],nsr['config-status']])
+    table.align='l'
+    print(table)
 
 @cli.command(name='nsd-list')
 @click.pass_context
 def nsd_list(ctx):
-    ctx.obj.list_ns_catalog()
+    resp=ctx.obj.nsd.list()
+    table=PrettyTable(['nsd name','id'])
+    for ns in resp:
+        table.add_row([ns['name'],ns['id']])
+    table.align='l'
+    print(table)
 
 @cli.command(name='vnfd-list')
 @click.pass_context
 def vnfd_list(ctx):
-    ctx.obj.list_vnf_catalog()
+    resp = ctx.obj.vnfd.list()
+    table=PrettyTable(['vnfd name','id'])
+    for vnfd in resp:
+        table.add_row([vnfd['name'],vnfd['id']])
+    table.align='l'
+    print(table)
 
 @cli.command(name='vnf-list')
 @click.pass_context
 def vnf_list(ctx):
-    resp=ctx.obj.list_vnfr()
-    table=PrettyTable(['vnf name','id','operational status','config Status','mgmt interface','nsr id'])
-    if resp is not None:
-        for vnfr in resp['vnfr:vnfr']:
-            if not 'mgmt-interface' in vnfr:
-                vnfr['mgmt-interface'] = {}
-                vnfr['mgmt-interface']['ip-address'] = None
-            table.add_row([vnfr['name'],vnfr['id'],vnfr['operational-status'],vnfr['config-status'],vnfr['mgmt-interface']['ip-address'],vnfr['nsr-id-ref']])
-        table.align='l'
+    resp=ctx.obj.vnf.list()
+    table=PrettyTable(['vnf name','id','operational status','config status'])
+    for vnfr in resp:
+        if not 'mgmt-interface' in vnfr:
+            vnfr['mgmt-interface'] = {}
+            vnfr['mgmt-interface']['ip-address'] = None
+        table.add_row([vnfr['name'],vnfr['id'],vnfr['operational-status'],vnfr['config-status']])
+    table.align='l'
+    print(table)
+
+@cli.command(name='vnf-show')
+@click.argument('vnf_name')
+@click.option('--filter',default=None)
+@click.pass_context
+def vnf_show(ctx,vnf_name,filter):
+    try:
+        resp=ctx.obj.vnf.get(vnf_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
+    table=PrettyTable(['field','value'])
+    for k,v in resp.items():
+        if filter is None or filter in k:
+            table.add_row([k,json.dumps(v,indent=2)])
+    table.align='l'
     print(table)
 
 @cli.command(name='vnf-monitoring-show')
 @click.argument('vnf_name')
 @click.pass_context
 def vnf_monitoring_show(ctx,vnf_name):
-    resp=ctx.obj.get_vnf_monitoring(vnf_name)
+    try:  
+        resp=ctx.obj.vnf.get_monitoring(vnf_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
+
     table=PrettyTable(['vnf name','monitoring name','value','units'])
     if resp is not None:
         for monitor in resp:
@@ -58,79 +117,126 @@ def vnf_monitoring_show(ctx,vnf_name):
 @click.argument('ns_name')
 @click.pass_context
 def ns_monitoring_show(ctx,ns_name):
-    resp=ctx.obj.get_ns_monitoring(ns_name)
+    try:
+        resp=ctx.obj.ns.get_monitoring(ns_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
+
     table=PrettyTable(['vnf name','monitoring name','value','units'])
-    if resp is not None:
-        for key,val in resp.items():
-            for monitor in val:
-                table.add_row([key,monitor['name'],monitor['value-integer'],monitor['units']])
+    for key,val in resp.items():
+        for monitor in val:
+            table.add_row([key,monitor['name'],monitor['value-integer'],monitor['units']])
     table.align='l'
     print(table)
 
 @cli.command(name='ns-create')
-@click.argument('nsd_name')
-@click.argument('ns_name')
-@click.argument('vim_account')
+@click.option('--ns_name',prompt=True)
+@click.option('--nsd_name',prompt=True)
+@click.option('--vim_account',prompt=True)
 @click.option('--admin_status',default='ENABLED',help='administration status')
 @click.option('--ssh_keys',default=None,help='comma separated list of keys to inject to vnfs')
 @click.option('--vim_network_prefix',default=None,help='vim network name prefix')
 @click.pass_context
 def ns_create(ctx,nsd_name,ns_name,vim_account,admin_status,ssh_keys,vim_network_prefix):
-    ctx.obj.instantiate_ns(nsd_name,ns_name,vim_network_prefix=vim_network_prefix,ssh_keys=ssh_keys,account=vim_account)
+    try:
+        ctx.obj.ns.create(nsd_name,ns_name,vim_network_prefix=vim_network_prefix,ssh_keys=ssh_keys,account=vim_account)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1) 
 
 @cli.command(name='ns-delete')
 @click.argument('ns_name')
 @click.pass_context
 def ns_delete(ctx,ns_name):
-    ctx.obj.terminate_ns(ns_name)
-
-'''
-@cli.command(name='keypair-list')
-@click.pass_context
-def keypair_list(ctx):
-    resp=ctx.obj.list_key_pair()
-    table=PrettyTable(['key Name','key'])
-    for kp in resp:
-        table.add_row([kp['name'],kp['key']])
-    table.align='l'
-    print(table)
-'''
+    try:
+        ctx.obj.ns.delete(ns_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='upload-package')
 @click.argument('filename')
 @click.pass_context
 def upload_package(ctx,filename):
-    ctx.obj.upload_package(filename)
+    try:
+        ctx.obj.package.upload(filename)
+        ctx.obj.package.wait_for_upload(filename)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='ns-show')
 @click.argument('ns_name')
+@click.option('--filter',default=None)
 @click.pass_context
-def ns_show(ctx,ns_name):
-    ctx.obj.show_ns(ns_name)
+def ns_show(ctx,ns_name,filter):
+    try:
+        ns = ctx.obj.ns.get(ns_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
+
+    table=PrettyTable(['field','value'])
+
+    for k,v in ns.items():
+        if filter is None or filter in k:
+            table.add_row([k,json.dumps(v,indent=2)])
+
+    nsopdata=ctx.obj.ns.get_opdata(ns['id'])
+    nsr_optdata=nsopdata['nsr:nsr']
+    for k,v in nsr_optdata.items():
+        if filter is None or filter in k:
+            table.add_row([k,json.dumps(v,indent=2)])
+    table.align='l'
+    print(table)
 
 @cli.command(name='ns-scaling-show')
 @click.argument('ns_name')
 @click.pass_context
 def show_ns_scaling(ctx,ns_name):
-    ctx.obj.show_ns_scaling(ns_name)
+    resp = ctx.obj.ns.list()
+
+    table=PrettyTable(['instance-id','operational status','create-time','vnfr ids'])
+
+    if 'nsr' in resp:
+        for ns in resp['nsr']:
+            if ns_name == ns['name']:
+                nsopdata=ctx.obj.ns.get_opdata(ns['id'])
+                scaling_records=nsopdata['nsr:nsr']['scaling-group-record']
+                for record in scaling_records:
+                    if 'instance' in record:
+                        instances=record['instance'] 
+                        for inst in instances:
+                            table.add_row([inst['instance-id'],inst['op-status'],time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(inst['create-time'])),inst['vnfrs']])
+    table.align='l'
+    print(table)
 
 @cli.command(name='nsd-delete')
 @click.argument('nsd_name')
 @click.pass_context
 def nsd_delete(ctx,nsd_name):
-    ctx.obj.delete_nsd(nsd_name)
+    try:
+        ctx.obj.nsd.delete(nsd_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='vnfd-delete')
 @click.argument('vnfd_name')
 @click.pass_context
-def nsd_delete(ctx,vnfd_name):
-    ctx.obj.delete_vnfd(vnfd_name)
+def vnfd_delete(ctx,vnfd_name):
+    try:
+        ctx.obj.vnfd.delete(vnfd_name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='config-agent-list')
 @click.pass_context
 def config_agent_list(ctx):
     table=PrettyTable(['name','account-type','details'])
-    for account in ctx.obj.get_config_agents():
+    for account in ctx.obj.vca.list():
       table.add_row([account['name'],account['account-type'],account['juju']])
     table.align='l'
     print(table)
@@ -139,48 +245,103 @@ def config_agent_list(ctx):
 @click.argument('name')
 @click.pass_context
 def config_agent_delete(ctx,name):
-    ctx.obj.delete_config_agent(name)
+    try:
+        ctx.obj.vca.delete(name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='config-agent-add')
-@click.argument('name')
-@click.argument('account_type')
-@click.argument('server')
-@click.argument('user')
-@click.argument('secret')
+@click.option('--name',prompt=True)
+@click.option('--account_type',prompt=True)
+@click.option('--server',prompt=True)
+@click.option('--user',prompt=True)
+@click.option('--secret',prompt=True,hide_input=True,confirmation_prompt=True)
 @click.pass_context
 def config_agent_add(ctx,name,account_type,server,user,secret):
-    ctx.obj.add_config_agent(name,account_type,server,user,secret)
+    try:
+        ctx.obj.vca.create(name,account_type,server,user,secret)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
-'''
 @cli.command(name='vim-create')
-@click.argument('name')
-@click.argument('user')
-@click.argument('password')
-@click.argument('auth_url')
-@click.argument('tenant')
-@click.argument('mgmt_network')
-@click.argument('floating_ip_pool')
+@click.option('--name',prompt=True)
+@click.option('--user',prompt=True)
+@click.option('--password',prompt=True,hide_input=True,confirmation_prompt=True)
+@click.option('--auth_url',prompt=True)
+@click.option('--tenant',prompt=True)
+@click.option('--floating_ip_pool',default=None)
+@click.option('--keypair',default=None)
 @click.option('--account_type',default='openstack')
+@click.option('--description',default='no description')
 @click.pass_context
-def vim_create(ctx,name,user,password,auth_url,tenant,mgmt_network,floating_ip_pool,account_type):
-    ctx.obj.add_vim_account(name,user,password,auth_url,tenant,mgmt_network,floating_ip_pool,account_type)
-'''
+def vim_create(ctx,name,user,password,auth_url,tenant,floating_ip_pool,keypair,account_type,description):
+    vim={}
+    vim['os-username']=user
+    vim['os-password']=password
+    vim['os-url']=auth_url
+    vim['os-project-name']=tenant
+    vim['floating_ip_pool']=floating_ip_pool
+    vim['keypair']=keypair
+    vim['vim-type']='openstack'
+    vim['description']=description
+    try:
+        ctx.obj.vim.create(name,vim)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
+
+@cli.command(name='vim-delete')
+@click.argument('name')
+@click.pass_context
+def vim_delete(ctx,name):
+    try:
+        ctx.obj.vim.delete(name)
+    except ClientException as inst:
+        print(inst.message)
+        exit(1)
 
 @cli.command(name='vim-list')
 @click.pass_context
 def vim_list(ctx):
-    resp=ctx.obj.list_vim_accounts()
-    table=PrettyTable(['ro-account','datacenter name','uuid'])
-    for roaccount in resp:
-        for datacenter in roaccount['datacenters']:
-            table.add_row([roaccount['name'],datacenter['name'],datacenter['uuid']])
+    resp=ctx.obj.vim.list()
+    table=PrettyTable(['vim name','uuid'])
+    for vim in resp:
+        table.add_row([vim['name'],vim['uuid']])
+    table.align='l'
+    print(table)
+
+@cli.command(name='vim-show')
+@click.argument('name')
+@click.pass_context
+def vim_show(ctx,name):
+    try:
+        resp=ctx.obj.vim.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)
+
+@cli.command(name='ro-dump')
+@click.pass_context
+def ro_dump(ctx):
+    resp=ctx.obj.vim.get_resource_orchestrator()
+    table=PrettyTable(['key','attribute'])
+    for k,v in resp.items():
+        table.add_row([k,json.dumps(v,indent=2)])
     table.align='l'
     print(table)
 
 @cli.command(name='vcs-list')
 @click.pass_context
 def vcs_list(ctx):
-    resp=ctx.obj.get_vcs_info()
+    resp=ctx.obj.utils.get_vcs_info()
     table=PrettyTable(['component name','state'])
     for component in resp:
         table.add_row([component['component_name'],component['state']])
diff --git a/osmclient/v1/__init__.py b/osmclient/v1/__init__.py
new file mode 100644 (file)
index 0000000..2bf7fed
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
diff --git a/osmclient/v1/client.py b/osmclient/v1/client.py
new file mode 100644 (file)
index 0000000..6d5e4a1
--- /dev/null
@@ -0,0 +1,56 @@
+# Copyright 2017 Sandvine
+#
+# 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 v1 client API
+"""
+
+from osmclient.v1 import vnf
+from osmclient.v1 import vnfd
+from osmclient.v1 import ns
+from osmclient.v1 import nsd
+from osmclient.v1 import vim
+from osmclient.v1 import package
+from osmclient.v1 import vca
+from osmclient.v1 import utils
+from osmclient.common import http
+import pycurl
+
+
+class Client(object):
+
+    def __init__(self, host=None,ro_port=9090,upload_port=8443,**kwargs):
+        self._user='admin'
+        self._password='admin'
+        self._host = host.split(':')[0]
+        self._so_port = host.split(':')[1]
+
+        http_client    = http.Http('https://{}:{}/'.format(self._host,self._so_port))
+        http_client.set_http_header( ['Accept: application/vnd.yand.data+json','Content-Type: application/json'])
+
+        ro_http_client = http.Http('http://{}:{}/'.format(self._host,ro_port))
+        ro_http_client.set_http_header( ['Accept: application/vnd.yand.data+json','Content-Type: application/json'])
+
+        upload_client  = http.Http('https://{}:{}/composer/upload?api_server=https://localhost&upload_server=https://{}'.format(self._host,upload_port,self._host))
+
+        self.vnf  = vnf.Vnf(http_client,**kwargs)
+        self.vnfd = vnfd.Vnfd(http_client,**kwargs)
+        self.ns   = ns.Ns(http=http_client,client=self,**kwargs)
+        self.nsd  = nsd.Nsd(http_client,**kwargs)
+        self.vim  = vim.Vim(http=http_client,ro_http=ro_http_client,client=self,**kwargs)
+        self.package = package.Package(http=http_client,upload_http=upload_client,client=self,**kwargs)
+        self.vca     = vca.Vca(http_client,**kwargs)
+        self.utils   = utils.Utils(http_client,**kwargs)
diff --git a/osmclient/v1/key.py b/osmclient/v1/key.py
new file mode 100644 (file)
index 0000000..40b27f5
--- /dev/null
@@ -0,0 +1,41 @@
+# Copyright 2017 Sandvine
+#
+# 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 ssh-key API handling
+"""
+
+import json
+import pycurl
+from io import BytesIO
+
+
+class Key(object):
+
+    def __init__(self,client=None):
+        self._client=client
+
+    def list(self):
+        data = BytesIO()
+        curl_cmd=self._client.get_curl_cmd('v1/api/config/key-pair?deep')
+        curl_cmd.setopt(pycurl.HTTPGET,1)
+        curl_cmd.setopt(pycurl.WRITEFUNCTION, data.write)
+        curl_cmd.perform()
+        curl_cmd.close()
+        resp = json.loads(data.getvalue().decode())
+        if 'nsr:key-pair' in resp:
+            return resp['nsr:key-pair']
+        return list()
diff --git a/osmclient/v1/ns.py b/osmclient/v1/ns.py
new file mode 100644 (file)
index 0000000..d8631f5
--- /dev/null
@@ -0,0 +1,151 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+OSM ns API handling
+"""
+
+from osmclient.common import utils
+from osmclient.common.exceptions import ClientException
+from osmclient.common.exceptions import NotFound
+import uuid
+import pprint
+
+
+class Ns(object):
+    def __init__(self,http=None,client=None):
+        self._http=http
+        self._client=client
+
+    def list(self):
+        """Returns a list of ns's
+        """
+        resp = self._http.get_cmd('api/running/ns-instance-config')
+
+        if not resp or 'nsr:ns-instance-config' not in resp:
+            return list()
+
+        if 'nsr' not in resp['nsr:ns-instance-config']:
+            return list()
+
+        return resp['nsr:ns-instance-config']['nsr']
+
+    def get(self,name):
+        """Returns an ns based on name
+        """
+        for ns in self.list():
+           if name == ns['name']:
+               return ns
+        raise NotFound("ns {} not found".format(name))
+
+    def create(self,nsd_name,nsr_name,account,vim_network_prefix=None,ssh_keys=None,description='default description',admin_status='ENABLED'):
+        postdata={}
+        postdata['nsr'] = list()
+        nsr={}
+        nsr['id']=str(uuid.uuid1())
+
+        nsd=self._client.nsd.get(nsd_name)
+        
+        datacenter=self._client.vim.get_datacenter(account)
+        if datacenter is None:
+            raise NotFound("cannot find datacenter account {}".format(account)) 
+
+        nsr['nsd']=nsd
+        nsr['name']=nsr_name
+        nsr['short-name']=nsr_name
+        nsr['description']=description
+        nsr['admin-status']=admin_status
+        nsr['om-datacenter']=datacenter['uuid']
+
+        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})
+
+            nsr['ssh-authorized-key']=ssh_keys_format
+
+        if vim_network_prefix is not None:
+            for index,vld in enumerate(nsr['nsd']['vld']):
+                network_name = vld['name']
+                nsr['nsd']['vld'][index]['vim-network-name'] = '{}-{}'.format(vim_network_prefix,network_name)
+
+        postdata['nsr'].append(nsr)
+
+        resp = self._http.post_cmd('api/config/ns-instance-config/nsr',postdata)
+
+        if 'success' not in resp:
+            raise ClientException("failed to create ns: {} nsd: {} result: {}".format(nsr_name,nsd_name,resp))
+
+    def get_opdata(self,id):
+        return self._http.get_cmd('api/operational/ns-instance-opdata/nsr/{}?deep'.format(id))
+
+    def get_field(self,ns_name,field):
+        nsr=self.get(ns_name)
+        if nsr is None:
+            raise NotFound("failed to retrieve ns {}".format(ns_name))
+
+        if field in nsr:
+            return nsr[field]
+
+        nsopdata=self.get_opdata(nsr['id'])
+
+        if field in nsopdata['nsr:nsr']:
+            return nsopdata['nsr:nsr'][field]
+
+        raise NotFound("failed to find {} in ns {}".format(field,ns_name)) 
+
+    def _terminate(self,ns_name):
+        ns=self.get(ns_name)
+        if ns is None:
+            raise NotFound("cannot find ns {}".format(ns_name)) 
+  
+        return self._http.delete_cmd('api/config/ns-instance-config/nsr/'+ns['id'])
+
+    def delete(self,ns_name,wait=True):
+        vnfs = self.get_field(ns_name,'constituent-vnfr-ref')
+
+        resp=self._terminate(ns_name)
+        if 'success' not in resp:
+            raise ClientException("failed to delete ns {}".format(name))
+
+        # helper method to check if pkg exists
+        def check_not_exists( func ):
+            try:
+                func()
+                return False 
+            except NotFound:
+                return True 
+
+        for vnf in vnfs:
+            if not utils.wait_for_value(lambda: check_not_exists(lambda: self._client.vnf.get(vnf['vnfr-id']))):
+                raise ClientException("vnf {} failed to delete".format(vnf['vnfr-id']))
+        if not utils.wait_for_value(lambda: check_not_exists(lambda: self.get(ns_name))):
+            raise ClientException("ns {} failed to delete".format(ns_name))
+
+    def get_monitoring(self,ns_name):
+        ns=self.get(ns_name)
+        mon_list={}
+        if ns is None:
+            return mon_list
+
+        vnfs=self._client.vnf.list()
+        for vnf in vnfs:
+            if ns['id'] == vnf['nsr-id-ref']:
+                if 'monitoring-param' in vnf:
+                    mon_list[vnf['name']] = vnf['monitoring-param']
+
+        return mon_list
diff --git a/osmclient/v1/nsd.py b/osmclient/v1/nsd.py
new file mode 100644 (file)
index 0000000..c79ab27
--- /dev/null
@@ -0,0 +1,48 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+OSM nsd API handling
+"""
+
+from osmclient.common.exceptions import NotFound
+from osmclient.common.exceptions import ClientException
+
+
+class Nsd(object):
+    def __init__(self,http=None):
+        self._http=http
+
+    def list(self):
+        resp = self._http.get_cmd('api/running/nsd-catalog/nsd')
+        if resp and 'nsd:nsd' in resp:
+            return resp['nsd:nsd']
+        return list()
+
+    def get(self,name):
+        for nsd in self.list():
+           if name == nsd['name']:
+               return nsd
+        raise NotFound("cannot find nsd {}".format(name)) 
+
+    def delete(self,nsd_name):
+        nsd=self.get(nsd_name)
+        if nsd is None:
+            raise NotFound("cannot find nsd {}".format(nsd_name)) 
+
+        resp=self._http.delete_cmd('api/running/nsd-catalog/nsd/{}'.format(nsd['id']))
+        if 'success' not in resp:
+            raise ClientException("failed to delete nsd {}".format(nsd_name)) 
diff --git a/osmclient/v1/package.py b/osmclient/v1/package.py
new file mode 100644 (file)
index 0000000..16560c3
--- /dev/null
@@ -0,0 +1,107 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+OSM package API handling 
+"""
+
+import json
+import pycurl
+from io import BytesIO
+import tarfile
+import re 
+import yaml
+import time
+from osmclient.common.exceptions import ClientException
+from osmclient.common.exceptions import NotFound
+from osmclient.common import utils
+
+
+class Package(object):
+    def __init__(self,http=None,upload_http=None,client=None):
+        self._client=client
+        self._http=http
+        self._upload_http=upload_http
+
+    def _wait_for_package(self,pkg_type):
+        if 'vnfd' in pkg_type['type']:
+            get_method=self._client.vnfd.get
+        elif 'nsd' in pkg_type['type']:
+            get_method=self._client.nsd.get
+        else:
+            raise ClientException("no valid package type found")
+
+        # helper method to check if pkg exists
+        def check_exists( func ):
+            try:
+                func()
+            except NotFound:
+                return False
+            return True
+
+        return utils.wait_for_value(lambda: check_exists(lambda: get_method(pkg_type['name'])))
+
+    # method opens up a package and finds the name of the resulting
+    # descriptor (vnfd or nsd name)
+    def get_descriptor_type_from_pkg(self, descriptor_file):
+        tar = tarfile.open(descriptor_file)
+        yamlfile = None
+        for member in tar.getmembers():
+            if re.match('.*.yaml',member.name) and len(member.name.split('/')) == 2:
+                yamlfile = member.name
+                break
+        if yamlfile is None:
+            return None
+
+        dict=yaml.load(tar.extractfile(yamlfile))
+        result={}
+        for k1,v1 in dict.items():
+            if not k1.endswith('-catalog'):
+                continue
+            for k2,v2 in v1.items():
+                if not k2.endswith('nsd') and not k2.endswith('vnfd'):
+                    continue
+
+                if 'nsd' in k2:
+                    result['type'] = 'nsd'
+                else:
+                    result['type'] = 'vnfd'
+
+                for entry in v2:
+                    for k3,v3 in entry.items():
+                        if k3 == 'name' or k3.endswith(':name'):
+                            result['name'] = v3
+                            return result
+        tar.close()
+        return None
+
+    def wait_for_upload(self,filename):
+        """wait(block) for an upload to succeed.
+           
+           The filename passed is assumed to be a descriptor tarball.
+        """
+        pkg_type=self.get_descriptor_type_from_pkg(filename)
+
+        if pkg_type is None:
+            raise ClientException("Cannot determine package type")
+
+        if not self._wait_for_package(pkg_type):
+            raise ClientException("package {} failed to upload".format(filename))
+
+    def upload(self,filename):
+        resp=self._upload_http.post_cmd(formfile=('package',filename))
+        if not resp or 'transaction_id' not in resp:
+            raise ClientException("failed to upload package")
diff --git a/osmclient/v1/tests/test_ns.py b/osmclient/v1/tests/test_ns.py
new file mode 100644 (file)
index 0000000..d4a6012
--- /dev/null
@@ -0,0 +1,43 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import unittest
+from mock import Mock
+from osmclient.v1 import ns
+from osmclient.common.exceptions import NotFound
+
+
+class TestNs(unittest.TestCase):
+
+   def test_list_empty(self):
+       mock=Mock()
+       mock.get_cmd.return_value=list()
+       assert len(ns.Ns(mock).list()) == 0
+
+   def test_get_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,ns.Ns(mock).get,'bar')
+
+   def test_get_found(self):
+       mock=Mock()
+       mock.get_cmd.return_value={'nsr:ns-instance-config': { 'nsr': [{'name': 'foo' }]}}
+       assert ns.Ns(mock).get('foo')
+
+   def test_get_monitoring_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,ns.Ns(mock).get_monitoring,'bar')
diff --git a/osmclient/v1/tests/test_nsd.py b/osmclient/v1/tests/test_nsd.py
new file mode 100644 (file)
index 0000000..175860c
--- /dev/null
@@ -0,0 +1,37 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import unittest
+from mock import Mock
+from osmclient.v1 import nsd
+from osmclient.common.exceptions import NotFound
+
+class TestNsd(unittest.TestCase):
+
+   def test_list_empty(self):
+       mock=Mock()
+       mock.get_cmd.return_value=list()
+       assert len(nsd.Nsd(mock).list()) == 0
+
+   def test_get_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,nsd.Nsd(mock).get,'bar')
+
+   def test_get_found(self):
+       mock=Mock()
+       mock.get_cmd.return_value={'nsd:nsd': [{'name': 'foo' }]}
+       assert nsd.Nsd(mock).get('foo')
diff --git a/osmclient/v1/tests/test_package.py b/osmclient/v1/tests/test_package.py
new file mode 100644 (file)
index 0000000..3f49aa0
--- /dev/null
@@ -0,0 +1,36 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import unittest
+from mock import Mock
+from osmclient.v1 import package
+from osmclient.common.exceptions import ClientException
+
+
+class TestPackage(unittest.TestCase):
+
+   def test_upload_fail(self):
+       mock=Mock()
+       mock.post_cmd.return_value='foo'
+       self.assertRaises(ClientException,package.Package(upload_http=mock).upload,'bar')
+
+       mock.post_cmd.return_value=None
+       self.assertRaises(ClientException,package.Package(upload_http=mock).upload,'bar')
+
+   def test_wait_for_upload_bad_file(self):
+       mock=Mock()
+       mock.post_cmd.return_value='foo'
+       self.assertRaises(IOError,package.Package(upload_http=mock).wait_for_upload,'invalidfile')
diff --git a/osmclient/v1/tests/test_vnf.py b/osmclient/v1/tests/test_vnf.py
new file mode 100644 (file)
index 0000000..dfeea00
--- /dev/null
@@ -0,0 +1,48 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import unittest
+from mock import Mock
+from osmclient.v1 import vnf
+from osmclient.common.exceptions import NotFound
+
+
+class TestVnf(unittest.TestCase):
+
+   def test_list_empty(self):
+       mock=Mock()
+       mock.get_cmd.return_value=list()
+       assert len(vnf.Vnf(mock).list()) == 0
+
+   def test_get_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,vnf.Vnf(mock).get,'bar')
+
+   def test_get_found(self):
+       mock=Mock()
+       mock.get_cmd.return_value={'vnfr:vnfr': [{'name': 'foo' }]}
+       assert vnf.Vnf(mock).get('foo')
+
+   def test_get_monitoring_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,vnf.Vnf(mock).get_monitoring,'bar')
+
+   def test_get_monitoring_found(self):
+       mock=Mock()
+       mock.get_cmd.return_value={'vnfr:vnfr': [{'name': 'foo', 'monitoring-param': True }]}
+       assert vnf.Vnf(mock).get_monitoring('foo')
diff --git a/osmclient/v1/tests/test_vnfd.py b/osmclient/v1/tests/test_vnfd.py
new file mode 100644 (file)
index 0000000..0acc051
--- /dev/null
@@ -0,0 +1,38 @@
+# Copyright 2017 Sandvine
+#
+# 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.
+
+import unittest
+from mock import Mock
+from osmclient.v1 import vnfd
+from osmclient.common.exceptions import NotFound
+
+
+class TestVnfd(unittest.TestCase):
+
+   def test_list_empty(self):
+       mock=Mock()
+       mock.get_cmd.return_value=list()
+       assert len(vnfd.Vnfd(mock).list()) == 0
+
+   def test_get_notfound(self):
+       mock=Mock()
+       mock.get_cmd.return_value='foo'
+       self.assertRaises(NotFound,vnfd.Vnfd(mock).get,'bar')
+
+   def test_get_found(self):
+       mock=Mock()
+       mock.get_cmd.return_value={'vnfd:vnfd': [{'name': 'foo' }]}
+       assert vnfd.Vnfd(mock).get('foo')
diff --git a/osmclient/v1/utils.py b/osmclient/v1/utils.py
new file mode 100644 (file)
index 0000000..3a26910
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright 2017 Sandvine
+#
+# 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 utils
+"""
+
+class Utils(object):
+    def __init__(self,http=None):
+        self._http=http
+
+    def get_vcs_info(self):
+        resp=self._http.get_cmd('api/operational/vcs/info')
+        if resp:
+            return resp['rw-base:info']['components']['component_info']
+        return list()
+
diff --git a/osmclient/v1/vca.py b/osmclient/v1/vca.py
new file mode 100644 (file)
index 0000000..37f37aa
--- /dev/null
@@ -0,0 +1,52 @@
+# Copyright 2017 Sandvine
+#
+# 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 VCA API handling
+"""
+
+from osmclient.common.exceptions import ClientException
+
+class Vca(object):
+
+    def __init__(self,http=None):
+        self._http=http
+
+    def list(self):
+        resp = self._http.get_cmd('api/config/config-agent')
+        if resp and 'rw-config-agent:config-agent' in resp:
+            return resp['rw-config-agent:config-agent']['account']
+        return list()
+
+    def delete(self,name):
+        if 'success' not in self._http.delete_cmd('api/config/config-agent/account/{}'.format(name)):
+            raise ClientException("failed to delete config agent {}".format(name))
+
+    def create(self,name,account_type,server,user,secret):
+        postdata={}
+        postdata['account'] = list()
+
+        account={}
+        account['name'] = name
+        account['account-type'] = account_type
+        account['juju'] = {}
+        account['juju']['user'] = user 
+        account['juju']['secret'] = secret
+        account['juju']['ip-address'] = server
+        postdata['account'].append(account)
+
+        if 'success' not in self._http.post_cmd('api/config/config-agent',postdata):
+            raise ClientException("failed to create config agent {}".format(name))
diff --git a/osmclient/v1/vim.py b/osmclient/v1/vim.py
new file mode 100644 (file)
index 0000000..79701f8
--- /dev/null
@@ -0,0 +1,163 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+OSM vim API handling
+"""
+
+from osmclient.common.exceptions import ClientException
+from osmclient.common.exceptions import NotFound
+
+
+class Vim(object):
+    def __init__(self,http=None,ro_http=None,client=None):
+        self._client=client
+        self._ro_http=ro_http
+        self._http=http
+
+    def _attach(self,vim_name,username,secret,vim_tenant_name):
+        tenant_name='osm'
+        tenant=self._get_ro_tenant()
+        if tenant is None:
+            raise ClientException("tenant {} not found".format(tenant_name))
+        datacenter= self._get_ro_datacenter(vim_name)
+        if datacenter is None:
+            raise Exception('datacenter {} not found'.format(vim_name))
+
+        vim_account={}
+        vim_account['datacenter']={}
+
+        vim_account['datacenter']['vim_username'] = username 
+        vim_account['datacenter']['vim_password'] = secret 
+        vim_account['datacenter']['vim_tenant_name'] = vim_tenant_name
+        return self._ro_http.post_cmd('openmano/{}/datacenters/{}'.format(tenant['uuid'],datacenter['uuid']),vim_account)
+
+    def _detach(self,vim_name):
+        return self._ro_http.delete_cmd('openmano/{}/datacenters/{}'.format('osm',vim_name))
+
+    def create(self, name, vim_access):
+        vim_account={}
+        vim_account['datacenter']={}
+
+        # currently assumes vim_acc
+        if 'vim-type' not in vim_access or 'openstack' not in vim_access['vim-type']:
+            raise Exception("only vim type openstack support")
+
+        vim_account['datacenter']['name'] = name
+        vim_account['datacenter']['type'] = vim_access['vim-type']
+        vim_account['datacenter']['vim_url'] = vim_access['os-url']
+        vim_account['datacenter']['vim_url_admin'] = vim_access['os-url']
+        vim_account['datacenter']['description'] = vim_access['description']
+
+        vim_config = {}
+        vim_config['use_floating_ip'] = False
+
+        if 'floating_ip_pool' in vim_access and vim_access['floating_ip_pool'] is not None:
+            vim_config['use_floating_ip'] = True
+
+        if 'keypair' in vim_access and vim_access['keypair'] is not None:
+            vim_config['keypair'] = vim_access['keypair']
+
+        vim_account['datacenter']['config'] = vim_config
+
+        resp = self._ro_http.post_cmd('openmano/datacenters',vim_account)
+        if resp and 'error' in resp:
+            raise ClientException("failed to create vim")
+        else:
+            self._attach(name,vim_access['os-username'],vim_access['os-password'],vim_access['os-project-name'])
+
+    def delete(self,vim_name):
+        # first detach
+        resp=self._detach(vim_name)
+        # detach.  continue if error, it could be the datacenter is left without attachment
+
+        if 'result' not in self._ro_http.delete_cmd('openmano/datacenters/{}'.format(vim_name)):
+            raise ClientException("failed to delete vim {}".format(vim_name))
+
+    def list(self):
+        resp=self._http.get_cmd('v1/api/operational/datacenters')
+        if not resp or 'rw-launchpad:datacenters' not in resp:
+            return list()
+
+        datacenters=resp['rw-launchpad:datacenters']
+
+        vim_accounts = list()
+        if 'ro-accounts' not in datacenters:
+            return vim_accounts
+
+        tenant=self._get_ro_tenant()
+        if tenant is None:
+            return vim_accounts
+
+        for roaccount in datacenters['ro-accounts']:
+            if 'datacenters' not in roaccount:
+                continue
+            for datacenter in roaccount['datacenters']:
+               vim_accounts.append(self._get_ro_datacenter(datacenter['name'],tenant['uuid']))
+        return vim_accounts
+
+    def _get_ro_tenant(self,name='osm'):
+        resp=self._ro_http.get_cmd('openmano/tenants/{}'.format(name))
+
+        if not resp:
+            return None
+
+        if 'tenant' in resp and 'uuid' in resp['tenant']:
+            return resp['tenant']
+        else:
+            return None
+
+    def _get_ro_datacenter(self,name,tenant_uuid='any'):
+        resp=self._ro_http.get_cmd('openmano/{}/datacenters/{}'.format(tenant_uuid,name))
+        if not resp:
+            raise NotFound("datacenter {} not found".format(name))
+       
+        if 'datacenter' in resp and 'uuid' in resp['datacenter']:
+            return resp['datacenter']
+        else:
+            raise NotFound("datacenter {} not found".format(name))
+
+    def get(self,name):
+        tenant=self._get_ro_tenant()
+        if tenant is None:
+            raise NotFound("no ro tenant found")
+
+        return self._get_ro_datacenter(name,tenant['uuid'])
+
+    def get_datacenter(self,name):
+        resp=self._http.get_cmd('v1/api/operational/datacenters')
+        if not resp:
+            return None
+        datacenters=resp['rw-launchpad:datacenters']
+
+        if not resp or 'rw-launchpad:datacenters' not in resp:
+            return None
+        if 'ro-accounts' not in resp['rw-launchpad:datacenters']:
+            return None
+        for roaccount in resp['rw-launchpad:datacenters']['ro-accounts']:
+            if not 'datacenters' in roaccount:
+                continue
+            for datacenter in roaccount['datacenters']:
+                if datacenter['name'] == name:
+                    return datacenter
+        return None
+
+    def get_resource_orchestrator(self):
+        resp=self._http.get_cmd('v1/api/operational/resource-orchestrator')
+        if not resp or 'rw-launchpad:resource-orchestrator' not in resp:
+            return None
+        return resp['rw-launchpad:resource-orchestrator']
diff --git a/osmclient/v1/vnf.py b/osmclient/v1/vnf.py
new file mode 100644 (file)
index 0000000..a981bb3
--- /dev/null
@@ -0,0 +1,47 @@
+# Copyright 2017 Sandvine
+#
+# 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 vnf API handling
+"""
+
+from osmclient.common.exceptions import NotFound 
+
+
+class Vnf(object):
+    def __init__(self,http=None):
+        self._http=http
+
+    def list(self):
+        resp = self._http.get_cmd('v1/api/operational/vnfr-catalog/vnfr')
+        if resp and 'vnfr:vnfr' in resp:
+            return resp['vnfr:vnfr']
+        return list() 
+
+    def get(self,vnf_name):
+        vnfs=self.list()
+        for vnf in vnfs:
+            if vnf_name == vnf['name']:
+                return vnf
+            if vnf_name == vnf['id']:
+                return vnf
+        raise NotFound("vnf {} not found".format(vnf_name))
+
+    def get_monitoring(self,vnf_name):
+        vnf=self.get(vnf_name)
+        if vnf and 'monitoring-param' in vnf:
+            return vnf['monitoring-param']
+        return None
diff --git a/osmclient/v1/vnfd.py b/osmclient/v1/vnfd.py
new file mode 100644 (file)
index 0000000..5b2c5ee
--- /dev/null
@@ -0,0 +1,46 @@
+# Copyright 2017 Sandvine
+#
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+"""
+OSM vnfd API handling
+"""
+
+from osmclient.common.exceptions import NotFound 
+from osmclient.common.exceptions import ClientException
+
+
+class Vnfd(object):
+
+    def __init__(self,http=None):
+        self._http=http
+
+    def list(self):
+        resp = self._http.get_cmd('api/running/vnfd-catalog/vnfd')
+        if resp and 'vnfd:vnfd' in resp:
+            return resp['vnfd:vnfd']
+        return list()
+
+    def get(self,name):
+        for vnfd in self.list():
+           if name == vnfd['name']:
+               return vnfd
+        raise NotFound("vnfd {} not found".format(name))
+
+    def delete(self,vnfd_name):
+        vnfd=self.get(vnfd_name)
+        resp=self._http.delete_cmd('api/running/vnfd-catalog/vnfd/{}'.format(vnfd['id']))
+        if 'success' not in resp:
+            raise ClientException("failed to delete vnfd {}".format(vnfd_name))