Update code to comply with Gluon API Spec

Many changes to support new Gluon API Spec.

Summary of changes:
- gluon-api-tool: New command line tool to validate an API Model.
- Restructured models directory
  - Removed proton directory
  - Created a directory for the base objects file
  - Created directory for net-l3vpn and change filename to api.yaml
  - Created directory for a test API
- Updated manager code to automatically create default interface
  object when a port object is created.
- Updated shim layer code to track the interface object and to
  handle service binding to interface instead of port
- Added new API types and validation logic
- Updated API and Database generator code to process the new
  model constructs
- Updated the test cases to complete with the new model format.
- Reworked code to support parent/child API relationships.  The
  SubObjectController now works for one level.

Change-Id: I995b46076e9fded11e4eda789dacd41a1a3b43c7
Implements: blueprint gluon-api-spec
This commit is contained in:
Thomas Hambleton 2017-01-19 16:07:12 -06:00
parent ce980f3865
commit 1ef4f87b28
33 changed files with 1303 additions and 514 deletions

View File

@ -64,7 +64,7 @@ register the port in Gluon. The Shim Layer which is discussed below will pick
up the information from etcd. This port (base-port information) is stored in
the Proton database itself. By storing the base-port information inside the
Proton the user is free to “describe” a port however they want. The
base-ports can be viewed using the :command:`baseport-list` command. As
base-ports can be viewed using the :command:`port-list` command. As
previously mentioned, when a bind action happens Gluon uses the net_l3vpn
driver to bind a baseport to a VM. Currently the base-port model was built
modeling what Neutron requires to ensure compatibility. For different
@ -75,7 +75,7 @@ When a VPN is created through a Proton, the Proton creates the VPN object and
stores this in its database. This can viewed using the :command:`vpn-list`
command. Furthermore, the API of the L3VPN allows for creating service
bindings between a baseport and a VPN service. These service binding can be
viewed using the :command:`vpnport-list` command.
viewed using the :command:`vpnservice-list` command.
All objects (VPNs, Base-ports, and Bindings) are stored into the Proton
database. This information is then copied into etcd. The shim layers (see

View File

@ -13,17 +13,15 @@
# under the License.
import datetime
import six
import wsme
import wsmeext.pecan as wsme_pecan
from pecan import expose
from pecan import rest
from webob import exc
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from gluon.db import api as dbapi
from gluon.managers.manager_base import get_api_manager
class APIBase(wtypes.Base):
@ -124,13 +122,31 @@ class APIBaseList(APIBase):
for db_obj in db_obj_list])
return obj
@classmethod
def build_filtered(cls, filter):
db = dbapi.get_instance()
db_obj_list = db.get_list(cls.api_object_class.db_model,
filters=filter)
obj = cls()
setattr(obj, cls.list_name,
[cls.api_object_class.build(db_obj)
for db_obj in db_obj_list])
return obj
class RootObjectController(rest.RestController):
"""Root Objects are Objects of the API which do not have a parent"""
@expose()
def _route(self, args, request=None):
result = super(RootObjectController, self)._route(args, request)
request.context['resource'] = result[0].im_self.resource_name
return result
@classmethod
def class_builder(base_cls, name, api_obj_class, primary_key_type,
api_name):
from gluon.managers.manager_base import get_api_manager
new_cls = type(name, (base_cls,), {})
new_cls.resource_name = name
new_cls.list_object_class = APIBaseList.class_builder(name + 'List',
@ -178,11 +194,14 @@ class RootObjectController(rest.RestController):
return new_cls
class SubObjectController(rest.RestController):
@expose()
def _route(self, args, request):
result = super(RootObjectController, self)._route(args, request)
def _route(self, args, request=None):
result = super(SubObjectController, self)._route(args, request)
request.context['resource'] = result[0].im_self.resource_name
return result
# @expose()
# def _lookup(self, collection, *remainder):
# #Set resource_action in the context to denote that
@ -190,53 +209,87 @@ class RootObjectController(rest.RestController):
# request.context['resource_action'] = 'show'
# return self
# TODO(hambtw) Needs to be reworked
# class SubObjectController(RootObjectController):
#
# @classmethod
# def class_builder(base_cls, name, object_class, primary_key_type,
# parent_identifier_type,
# parent_attribute_name):
# new_cls = super(SubObjectController, base_cls).class_builder(
# name, object_class, primary_key_type)
# new_cls._parent_id_type = parent_identifier_type
# new_cls._parent_attribute_name = parent_attribute_name
#
# @wsme_pecan.wsexpose(new_cls._list_object_class,
# new_cls.parent_id_type,
# template='json')
# def get_all(self, _parent_identifier):
# filters = {self._parent_attribute_name: _parent_identifier}
# return self._list_object_class.build(
# self._list_object_class.get_object_class().list(
# filters=filters))
# new_cls.get_all = classmethod(get_all)
#
# @wsme_pecan.wsexpose(new_cls.api_object_class,
# new_cls._parent_identifier_type,
# new_cls.primary_key_type,
# template='json')
# def get_one(self, parent_identifier, key):
# filters = {self._parent_attribute_name: parent_identifier}
# return self.api_object_class.build(
# self.api_object_class.get_object_class(
# ).get_by_primary_key(key, filters))
# new_cls.get_one = classmethod(get_one)
#
# @wsme_pecan.wsexpose(new_cls.api_object_class,
# new_cls._parent_id_type,
# body=new_cls.api_object_class, template='json',
# status_code=201)
# def post(self, parent_identifier, body):
# call_func = getattr(get_api_manager(),
# 'create_%s' % self.__name__,
# None)
# if not call_func:
# raise Exception('create_%s is not implemented' %
# self.__name__)
# return self.api_object_class.build(call_func(
# parent_identifier,
# body.to_db_object()))
# new_cls.post = classmethod(post)
#
# return new_cls
@classmethod
def class_builder(base_cls, name, object_class,
primary_key_type,
parent_identifier_type,
parent_table,
parent_attribute_name,
api_name):
from gluon.managers.manager_base import get_api_manager
new_cls = type(name, (base_cls,), {})
new_cls.resource_name = name
new_cls.api_object_class = object_class
new_cls.primary_key_type = primary_key_type
new_cls.parent_id_type = parent_identifier_type
new_cls.parent_table = parent_table
new_cls.parent_attribute_name = parent_attribute_name
new_cls.api_name = api_name
new_cls.api_mgr = get_api_manager(api_name)
new_cls.list_object_class = APIBaseList.class_builder(name + 'SubList',
name,
object_class)
@wsme_pecan.wsexpose(new_cls.list_object_class,
new_cls.parent_id_type,
template='json')
def get_all(self, parent_identifier):
filters = {self.parent_attribute_name: parent_identifier}
return self.list_object_class.build_filtered(filters)
new_cls.get_all = classmethod(get_all)
@wsme_pecan.wsexpose(new_cls.api_object_class,
new_cls.parent_id_type,
new_cls.primary_key_type,
template='json')
def get_one(self, parent_identifier, key):
return self.api_object_class.get_from_db(key)
new_cls.get_one = classmethod(get_one)
@wsme_pecan.wsexpose(new_cls.api_object_class,
new_cls.parent_id_type,
body=new_cls.api_object_class, template='json',
status_code=201)
def post(self, parent_identifier, body):
body_dict = body.as_dict()
parent_attr = body_dict.get(new_cls.parent_attribute_name)
if parent_attr is None:
body_dict[self.parent_attribute_name] = parent_identifier
elif parent_attr != parent_identifier:
raise exc.HTTPClientError(
'API parent identifier(%s): %s does not match parent '
'key value: %s' % (self.parent_attribute_name,
parent_attr,
parent_identifier))
return self.api_mgr.handle_create(self, body_dict)
new_cls.post = classmethod(post)
@wsme_pecan.wsexpose(new_cls.api_object_class,
new_cls.parent_id_type,
new_cls.primary_key_type,
body=new_cls.api_object_class, template='json')
def put(self, parent_identifier, key, body):
body_dict = body.as_dict()
parent_attr = body_dict.get(new_cls.parent_attribute_name)
if parent_attr is not None and parent_attr != parent_identifier:
raise exc.HTTPClientError(
'API parent identifier(%s): %s does not match parent '
'key value: %s' % (self.parent_attribute_name,
parent_attr,
parent_identifier))
return self.api_mgr.handle_update(self, key, body.as_dict())
new_cls.put = classmethod(put)
@wsme_pecan.wsexpose(None,
new_cls.parent_id_type,
new_cls.primary_key_type, template='json')
def delete(self, parent_identifier, key):
return self.api_mgr.handle_delete(self, key)
new_cls.delete = classmethod(delete)
return new_cls

View File

@ -16,6 +16,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import dateutil.parser
import json
import netaddr
import re
from rfc3986 import is_valid_uri
import six
from wsme import types as wtypes
@ -91,6 +96,28 @@ class BooleanType(wtypes.UserType):
return BooleanType.validate(value)
class FloatType(wtypes.UserType):
"""A simple float type. """
basetype = float
name = "float"
def __init__(self):
pass
@staticmethod
def frombasetype(value):
return float(value) if value is not None else None
def validate(self, value):
try:
float(value)
return value
except Exception:
error = 'Invalid float value: %s' % value
raise ValueError(error)
class MultiType(wtypes.UserType):
"""A complex type that represents one or more types.
@ -122,13 +149,106 @@ class Text(wtypes.UserType):
basetype = six.text_type
name = 'text'
# @staticmethod
def validate(value):
if isinstance(value, six.string_types):
return value
raise ValueError(_("Expected String, got '%s'") % value)
class DateTime(wtypes.UserType):
basetype = six.text_type
name = 'date-time'
@staticmethod
def validate(value):
if isinstance(value, six.string_types):
return
try:
dateutil.parser.parse(value)
return value
except Exception:
raise ValueError(_("Invalid Date string: '%s'") % value)
return value
raise ValueError(_("Expected String, got '%s'") % value)
class JsonString(wtypes.UserType):
basetype = six.text_type
name = 'json-string'
@staticmethod
def validate(value):
if isinstance(value, six.string_types):
try:
if len(value) > 0:
json.loads(value)
return value
except Exception:
raise ValueError(_("String not in JSON format: '%s'") % value)
raise ValueError(_("Expected String, got '%s'") % value)
class Ipv4String(wtypes.UserType):
basetype = six.text_type
name = 'ipv4-string'
@staticmethod
def validate(value):
if isinstance(value, six.string_types) and netaddr.valid_ipv4(value):
return value
raise ValueError(_("Expected IPv4 string, got '%s'") % value)
class Ipv6String(wtypes.UserType):
basetype = six.text_type
name = 'ipv6-string'
@staticmethod
def validate(value):
if isinstance(value, six.string_types) and netaddr.valid_ipv6(value):
return value
raise ValueError(_("Expected IPv6 String, got '%s'") % value)
class MacString(wtypes.UserType):
basetype = six.text_type
name = 'mac-string'
@staticmethod
def validate(value):
if isinstance(value, six.string_types) and netaddr.valid_mac(value):
return value
raise ValueError(_("Expected MAC String, got '%s'") % value)
class UriString(wtypes.UserType):
basetype = six.text_type
name = 'uri-string'
@staticmethod
def validate(value):
if isinstance(value, six.string_types):
try:
if is_valid_uri(value):
return value
else:
raise ValueError(_("Not valid URI format: '%s'") % value)
except Exception:
raise ValueError(_("Not valid URI format: '%s'") % value)
raise ValueError(_("Expected String, got '%s'") % value)
class EmailString(wtypes.UserType):
basetype = six.text_type
name = 'email-string'
regex = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
def validate(self, value):
if isinstance(value, six.string_types) and re.match(self.regex, value):
return value
raise ValueError(_("Expected Email String, got '%s'") % value)
def create_enum_type(*values):
unicode_values = []
for v in values:
@ -138,8 +258,17 @@ def create_enum_type(*values):
unicode_values.append(v)
return wtypes.Enum(wtypes.text, *unicode_values)
int_type = wtypes.IntegerType()
int_type = wtypes.IntegerType
float_type = FloatType()
uuid = UuidType()
name = NameType()
uuid_or_name = MultiType(UuidType, NameType)
boolean = BooleanType()
datetime_type = DateTime()
json_type = JsonString()
ipv4_type = Ipv4String()
ipv6_type = Ipv6String()
mac_type = MacString()
uri_type = UriString()
email_type = EmailString()

View File

@ -27,7 +27,6 @@ logger = LOG
@six.add_metaclass(abc.ABCMeta)
class ProviderBase(object):
def __init__(self):
self._drivers = {}
@ -38,7 +37,6 @@ class ProviderBase(object):
@six.add_metaclass(abc.ABCMeta)
class Driver(object):
def __init__(self, backend, dummy_net, dummy_subnet):
self._client = Client(backend)
self._dummy_net = dummy_net
@ -46,23 +44,18 @@ class Driver(object):
def bind(self, port_id, device_owner, zone, device_id, host_id,
binding_profile):
args = {}
args["device_owner"] = device_owner
args["device_id"] = device_id
args["host_id"] = host_id
args = {"device_owner": device_owner, "device_id": device_id,
"host_id": host_id}
if binding_profile is not None:
args["profile"] = json.dumps(binding_profile, indent=0)
args["zone"] = zone
# args["zone"] = zone # Do we need this?
url = self._port_url + "/" + port_id
return self._convert_port_data(self._client.do_put(url, args))
def unbind(self, port_id):
args = {}
args["device_owner"] = ''
args["device_id"] = ''
args["host_id"] = ''
args["profile"] = ''
args["zone"] = ''
args = {"device_owner": '', "device_id": '', "host_id": '',
"profile": ''}
# args["zone"] = '' # Do we need this?
url = self._port_url + "/" + port_id
return self._convert_port_data(self._client.do_put(url, args))
@ -77,9 +70,45 @@ class Driver(object):
ret_port_list.append(self._convert_port_data(port))
return ret_port_list
@abc.abstractmethod
def _convert_port_data(self, port_data):
pass
#
# This assumes ipaddress information is in the port object
#
LOG.debug("proton port_data = %s" % port_data)
ret_port_data = {"created_at": port_data["created_at"],
"updated_at": port_data["updated_at"],
"id": port_data["id"],
"devname": 'tap%s' % port_data['id'][:11],
"name": port_data.get("name"),
"status": port_data["status"],
"admin_state_up": port_data["admin_state_up"],
"network_id": self._dummy_net,
"tenant_id": port_data.get("tenant_id", ''),
"device_owner": port_data.get("device_owner", ''),
"device_id": port_data.get("device_id", ''),
"mac_address": port_data["mac_address"],
"extra_dhcp_opts": [], "allowed_address_pairs": []}
if 'ipaddress' in port_data:
ret_port_data["fixed_ips"] = \
[{"ip_address": port_data.get("ipaddress", "0.0.0.0"),
"subnet_id": self._dummy_subnet}]
ret_port_data["security_groups"] = []
ret_port_data["binding:host_id"] = port_data.get("host_id", '')
vif_details = port_data.get("vif_details")
if vif_details is None:
vif_details = '{}'
ret_port_data["binding:vif_details"] = json.loads(vif_details)
ret_port_data["binding:vif_type"] = port_data.get("vif_type", '')
ret_port_data["binding:vnic_type"] = \
port_data.get("vnic_type", 'normal')
profile = port_data.get("profile", '{}')
if profile is None or profile == '':
profile = '{}'
ret_port_data["binding:profile"] = json.loads(profile)
for k in ret_port_data:
if ret_port_data[k] is None:
ret_port_data[k] = ''
return ret_port_data
class BackendLoader(object):

View File

@ -13,11 +13,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
from oslo_log import log as logging
from gluon.backends import backend_base
from gluon.common import exception as exc
from oslo_log import log as logging
LOG = logging.getLogger(__name__)
logger = LOG
@ -29,7 +29,8 @@ class MyData(object):
DriverData = MyData()
DriverData.service = u'net-l3vpn'
DriverData.proton_base = 'proton'
DriverData.ports_name = 'baseports'
DriverData.ports_name = 'ports'
DriverData.binding_name = 'vpnbindings'
class Provider(backend_base.ProviderBase):
@ -50,41 +51,38 @@ class Driver(backend_base.Driver):
DriverData.proton_base,
DriverData.service,
DriverData.ports_name)
self._binding_url = \
"{0:s}/{1:s}/{2:s}/{3:s}".format(backend["url"],
DriverData.proton_base,
DriverData.service,
DriverData.binding_name)
def _convert_port_data(self, port_data):
LOG.debug("proton port_data = %s" % port_data)
ret_port_data = {}
ret_port_data["created_at"] = port_data["created_at"]
ret_port_data["updated_at"] = port_data["updated_at"]
ret_port_data["id"] = port_data["id"]
ret_port_data["devname"] = 'tap%s' % port_data['id'][:11]
ret_port_data["name"] = port_data.get("name")
ret_port_data["status"] = port_data["status"]
ret_port_data["admin_state_up"] = port_data["admin_state_up"]
ret_port_data["network_id"] = self._dummy_net
ret_port_data["tenant_id"] = port_data.get("tenant_id", '')
ret_port_data["device_owner"] = port_data.get("device_owner", '')
ret_port_data["device_id"] = port_data.get("device_id", '')
ret_port_data["mac_address"] = port_data["mac_address"]
ret_port_data["extra_dhcp_opts"] = []
ret_port_data["allowed_address_pairs"] = []
ret_port_data["fixed_ips"] = \
[{"ip_address": port_data.get("ipaddress", "0.0.0.0"),
"subnet_id": self._dummy_subnet}]
ret_port_data["security_groups"] = []
ret_port_data["binding:host_id"] = port_data.get("host_id", '')
vif_details = port_data.get("vif_details")
if vif_details is None:
vif_details = '{}'
ret_port_data["binding:vif_details"] = json.loads(vif_details)
ret_port_data["binding:vif_type"] = port_data.get("vif_type", '')
ret_port_data["binding:vnic_type"] = \
port_data.get("vnic_type", 'normal')
profile = port_data.get("profile", '{}')
if profile is None or profile == '':
profile = '{}'
ret_port_data["binding:profile"] = json.loads(profile)
for k in ret_port_data:
if ret_port_data[k] is None:
ret_port_data[k] = ''
return ret_port_data
def port(self, port_id):
url = self._port_url + "/" + port_id
port_data = self._client.json_get(url)
#
# The untagged interface has the same UUID as the port
# First we get the service binding to retrive the ipaddress
#
url = self._binding_url + "/" + port_id
try:
svc_bind_data = self._client.json_get(url)
except exc.GluonClientException:
svc_bind_data = None
if svc_bind_data:
port_data['ipaddress'] = svc_bind_data.get("ipaddress")
return self._convert_port_data(port_data)
def ports(self):
port_list = self._client.json_get(self._port_url)
ret_port_list = []
for port in port_list:
url = self._binding_url + "/" + port.id
try:
svc_bind_data = self._client.json_get(url)
except exc.GluonClientException:
svc_bind_data = None
if svc_bind_data:
port['ipaddress'] = svc_bind_data.get("ipaddress")
ret_port_list.append(self._convert_port_data(port))
return ret_port_list

61
gluon/cmd/api_tool.py Normal file
View File

@ -0,0 +1,61 @@
# Copyright (c) 2017 Nokia, Inc.
# 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 sys
import click
from gluon.particleGenerator.cli import get_model_list
from gluon.particleGenerator.generator import load_model
from gluon.particleGenerator.generator import verify_model
sys.tracebacklimit = 0
def dummy():
pass
@click.group()
def cli():
pass
@click.command()
def list():
model_list = get_model_list(package_name="gluon", model_dir="models")
for api in model_list:
click.echo(api)
@click.command()
@click.argument('api')
def check(api):
model_list = get_model_list(package_name="gluon", model_dir="models")
if api not in model_list:
print("Invalid API name!\n")
sys.exit(-1)
click.echo('Check API for ' + api)
model = load_model('gluon', 'models', api)
verify_model(model)
print(model)
cli.add_command(list)
cli.add_command(check)
def main():
cli()

View File

@ -34,11 +34,11 @@ def main():
cli = types.FunctionType(dummy.func_code, {})
cli = click.group()(cli)
model_list = get_model_list(package_name="gluon",
model_dir="models/proton")
model_dir="models")
model = get_api_model(sys.argv, model_list)
proc_model(cli,
package_name="gluon",
model_dir="models/proton",
model_dir="models",
api_model=model,
hostenv="OS_PROTON_HOST",
portenv="OS_PROTON_PORT",

View File

@ -32,7 +32,6 @@ CONF = cfg.CONF
class GluonException(Exception):
"""Base Gluon Exception
To correctly use this class, inherit from it and define
@ -178,3 +177,8 @@ class PolicyCheckError(GluonClientException):
"""An error due to a policy check failure."""
message = _("Failed to check policy %(policy)s because %(reason)s.")
class InvalidFileFormat(GluonClientException):
"""An error due to a invalid API specification file."""
message = _("Invalid file format")

View File

@ -21,7 +21,11 @@ import etcd
import stevedore
from gluon.api import types
from gluon.common import exception as exc
from gluon.particleGenerator.ApiGenerator import get_controller
from gluon.sync_etcd.thread import SyncData
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils.uuidutils import generate_uuid
@ -64,7 +68,7 @@ class ApiManager(object):
def setup_bind_key(self, key):
etcd_key = "{0:s}/{1:s}/{2:s}/{3:s}".format("controller", self.service,
"ProtonBasePort", key)
"Port", key)
#
# If key does not exists, create it so we can wait on it to change.
#
@ -81,7 +85,7 @@ class ApiManager(object):
def wait_for_bind(self, key):
etcd_key = "{0:s}/{1:s}/{2:s}/{3:s}".format("controller", self.service,
"ProtonBasePort", key)
"Port", key)
retry = 4
ret_val = dict()
while retry > 0:
@ -106,7 +110,7 @@ class ApiManager(object):
retry -= 1
return ret_val
def create_baseports(self, api_class, values):
def create_ports(self, api_class, values):
ret_obj = api_class.create_in_db(values)
#
# Register port in Gluon
@ -117,9 +121,24 @@ class ApiManager(object):
"url": self.url,
"operation": "register"}
SyncData.sync_queue.put(msg)
#
# Create default Interface object for Port
#
controller = get_controller(self.service, 'Interface')
if controller:
if 'name' in values:
name = values.get('name') + '_default'
else:
name = 'default'
data = {'id': values.get('id'),
'port_id': values.get('id'),
'name': name,
'segmentation_type': 'none',
'segmentation_id': 0}
controller.api_object_class.create_in_db(data)
return ret_obj
def update_baseports(self, api_class, key, new_values):
def update_ports(self, api_class, key, new_values):
has_bind_attrs = (new_values.get("host_id") is not None and
new_values.get("device_id") is not None)
is_bind_request = (has_bind_attrs and
@ -150,7 +169,7 @@ class ApiManager(object):
ret_obj = api_class.update_in_db(key, vif_dict)
return ret_obj
def delete_baseports(self, api_class, key):
def delete_ports(self, api_class, key):
#
# Remove port from Gluon
#
@ -158,7 +177,17 @@ class ApiManager(object):
"service": self.service,
"operation": "deregister"}
SyncData.sync_queue.put(msg)
return api_class.delete_from_db(key)
retval = api_class.delete_from_db(key)
#
# Delete default Interface object for Port
#
controller = get_controller(self.service, 'Interface')
if controller:
try:
controller.api_object_class.delete_from_db(key)
except exc.NotFound:
LOG.info("Default Inteface object not found: %s: " % key)
return retval
def handle_create(self, root_class, values):
api_class = root_class.api_object_class
@ -171,22 +200,22 @@ class ApiManager(object):
key_value = values.get(primary_key)
if not key_value or (key_value and key_value == ""):
values[primary_key] = generate_uuid()
if root_class.__name__ == 'baseports':
return self.create_baseports(root_class.api_object_class, values)
if root_class.__name__ == 'ports':
return self.create_ports(root_class.api_object_class, values)
else:
return api_class.create_in_db(values)
def handle_update(self, root_class, key, new_values):
api_class = root_class.api_object_class
if root_class.__name__ == 'baseports':
return self.update_baseports(api_class, key, new_values)
if root_class.__name__ == 'ports':
return self.update_ports(api_class, key, new_values)
else:
return api_class.update_in_db(key, new_values)
def handle_delete(self, root_class, key):
api_class = root_class.api_object_class
if root_class.__name__ == 'baseports':
return self.delete_baseports(api_class, key)
if root_class.__name__ == 'ports':
return self.delete_ports(api_class, key)
else:
return api_class.delete_from_db(key)

119
gluon/models/base/base.yaml Normal file
View File

@ -0,0 +1,119 @@
file_version: "1.0"
objects:
BaseObject:
attributes:
id:
type: uuid
primary: true
description: "UUID of Object"
name:
type: string
length: 64
description: "Descriptive name of Object"
BasePort:
extends: BaseObject
attributes:
tenant_id:
type: uuid
required: true
description: "UUID of Tenant owning this Port"
mac_address:
type: string
length: 18
required: true
format: mac
description: "MAC address for Port"
admin_state_up:
type: boolean
required: true
description: "Admin state of Port"
status:
type: enum
required: true
description: "Operational status of Port"
values:
- 'ACTIVE'
- 'DOWN'
vnic_type:
type: enum
required: true
description: "Port should be attached to this VNIC type"
values:
- 'normal'
- 'virtual'
- 'direct'
- 'macvtap'
- 'sriov'
- 'whole-dev'
mtu:
type: integer
description: "MTU"
required: true
vlan_transparency:
type: boolean
description: "Allow VLAN tagged traffic on Port"
required: true
profile:
type: string # JSON Format
length: 128
description: "JSON string for binding profile dictionary"
format: json
device_id:
type: uuid
description: "UUID of bound VM"
device_owner:
type: string
length: 128
description: "Name of compute or network service (if bound)"
host_id:
type: string
length: 64
description: "binding:host_id: Name of bound host"
vif_details:
type: string # JSON Format
length: 128
description: "binding:vif_details: JSON string for VIF details"
format: json
vif_type:
type: string
length: 32
description: "binding:vif_type: binding type for VIF"
BaseInterface:
extends: BaseObject
attributes:
port_id:
type: uuid
required: true
description: "Pointer to Port instance"
segmentation_type:
type: enum
required: true
description: "Type of segmentation for this interface"
values:
- 'none'
- 'vlan'
- 'tunnel_vxlan'
- 'tunnel_gre'
- 'mpls'
segmentation_id:
type: integer
required: true
description: "Segmentation identifier"
BaseService:
extends: BaseObject
attributes:
description:
type: string
length: 256
description: "Description of Service"
BaseServiceBinding:
attributes:
interface_id:
type: uuid
required: true
primary: true
description: "Pointer to Interface instance"
service_id:
type: uuid
required: true
description: "Pointer to Service instance"

View File

@ -0,0 +1,97 @@
file_version: "1.0"
imports: base/base.yaml
info:
name: net-l3vpn
version: 1.0
description: "L3VPN API Specification"
author:
name: "Gluon Team"
url: https://wiki.openstack.org/wiki/Gluon
email: bh526r@att.com
objects:
Port:
api:
name: port
plural_name: ports
extends: BasePort
Interface:
api:
name: interface
plural_name: interfaces
parent: Port
parent_key: port_id
extends: BaseInterface
attributes:
port_id:
type: Port # Override from base object for specific Service type
VpnService:
api:
name: vpn
plural_name: vpns
extends: BaseService
attributes:
ipv4_family:
type: string
length: 255
description: "Comma separated list of route target strings"
ipv6_family:
type: string
length: 255
description: "Comma separated list of route target strings"
route_distinguishers:
type: string
length: 32
description: "Route distinguisher for this VPN"
VpnBinding:
extends: BaseServiceBinding
api:
name: vpnbinding
plural_name: vpnbindings
attributes:
service_id: # Override from base object for specific Service type
type: VpnService
interface_id: # Override from base object for specific Inteface type
type: Interface
ipaddress:
type: string
length: 16
description: "IP Address of port"
format: ipv4
subnet_prefix:
type: integer
description: "Subnet mask"
format: int32
min: 1
max: 31
gateway:
type: string
length: 16
description: "Default gateway"
format: ipv4
VpnAfConfig:
api:
name: vpnafconfig
plural_name: vpnafconfigs
attributes:
vrf_rt_value:
required: true
type: string
length: 32
primary: true
description: "Route target string"
vrf_rt_type:
type: enum
required: true
description: "Route target type"
values:
- export_extcommunity
- import_extcommunity
- both
import_route_policy:
type: string
length: 32
description: "Route target import policy"
export_route_policy:
type: string
length: 32
description: "Route target export policy"

View File

@ -1,117 +0,0 @@
# This is the minimum required port for Gluon-connectivity to work.
ProtonBasePort:
api:
name: baseports
parent:
type: root
attributes:
id:
type: uuid
primary: 'True'
description: "UUID of base port instance"
tenant_id:
type: 'uuid'
required: True
description: "UUID of tenant owning this port"
name:
type: 'string'
length: 64
description: "Descriptive name for port"
network_id:
type: 'uuid'
description: "UUID of network - not used for Proton"
mac_address:
type: 'string'
length: 17
required: True
description: "MAC address for port"
validate: mac_address
admin_state_up:
type: 'boolean'
required: True
description: "Admin state of port"
device_owner:
type: 'string'
length: 128
description: "Name of compute or network service (if bound)"
device_id:
type: 'uuid'
description: "UUID of bound VM"
status:
type: 'enum'
required: True
description: "Operational status of port"
values:
- 'ACTIVE'
- 'DOWN'
vnic_type:
type: enum
required: true
description: "binding:vnic_type: Port should be attache to this VNIC type"
values:
- 'normal'
- 'virtual'
- 'direct'
- 'macvtap'
- 'sriov'
- 'whole-dev'
host_id:
type: 'string'
length: 32
description: "binding:host_id: Name of bound host"
vif_details:
type: 'string' # what are we going to use, JSON?
length: 128
description: "binding:vif_details: JSON string for VIF details"
profile:
type: 'string' # what are we going to use, JSON?
length: 128
description: "binding:profile: JSON string for binding profile dictionary"
vif_type:
type: 'string'
length: 32
description: "binding:vif_type: Headline binding type for VIF"
zone:
type: 'string'
length: 64
description: "zone information"
ipaddress:
type: 'string'
length: 64
description: "IP Address of port"
validate: 'ipv4address'
subnet_prefix:
type: 'integer'
description: "Subnet mask"
values:
- '1-31'
gateway:
type: 'string'
length: 64
description: "Default gateway"
validate: 'ipv4address'
mtu:
type: 'integer'
description: "MTU"
required: True
vlan_transparency:
type: 'boolean'
description: "Allow VLAN tagged traffic on port"
required: True
# TODO this would be inheritance in a more sane arrangement.
VPNPort:
api:
name: vpnports
parent:
type: root
attributes:
id:
type: 'ProtonBasePort'
required: True
primary: True
description: "Pointer to base port instance (UUID)"
vpn_instance:
type: 'VpnInstance'
required: True
description: "Pointer to VPN instance (UUID)"

View File

@ -1,61 +0,0 @@
# This is the minimum required port for Gluon-connectivity to work.
VpnInstance:
api:
name: vpns
parent:
type: root
attributes:
id:
type: uuid
primary: 'True'
description: "UUID of VPN instance"
vpn_instance_name:
required: True
type: string
length: 32
description: "Name of VPN"
description:
type: string
length: 255
description: "About the VPN"
ipv4_family:
type: string
length: 255
description: "Comma separated list of route target strings (VpnAfConfig)"
ipv6_family:
type: string
length: 255
description: "Comma separated list of route target strings (VpnAfConfig)"
route_distinguishers:
type: string
length: 32
description: "Route distinguisher for this VPN"
VpnAfConfig:
api:
name: vpnafconfigs
parent:
type: root
attributes:
vrf_rt_value:
required: True
type: string
length: 32
primary: 'True'
description: "Route target string"
vrf_rt_type:
type: enum
required: True
description: "Route target type"
values:
- export_extcommunity
- import_extcommunity
- both
import_route_policy:
type: string
length: 32
description: "Route target import policy"
export_route_policy:
type: string
length: 32
description: "Route target export policy"

View File

@ -0,0 +1,67 @@
file_version: "1.0"
imports: base/base.yaml
info:
name: test_api
version: 1.0
description: "Test API Specification"
author:
name: "Gluon Team"
url: https://wiki.openstack.org/wiki/Gluon
email: bh526r@att.com
objects:
Port:
api:
name: port
plural_name: ports
extends: BasePort
Interface:
api:
name: interface
plural_name: interfaces
parent: Port
parent_key: port_id
extends: BaseInterface
attributes:
port_id:
type: Port
TestService:
api:
name: test
plural_name: tests
extends: BaseService
attributes:
email:
type: string
length: 255
description: "Test Email"
format: email
uri:
type: string
length: 255
description: "Test URI"
format: uri
datetime:
type: string
length: 255
description: "Test date-time"
format: date-time
json:
type: string
length: 255
description: "Test JSON"
format: json
float_num:
type: number
description: "Test Float"
int_num:
type: integer
description: "Test Integer"
min: 1
max: 10
enum:
type: enum
description: "Test Enum"
values:
- one
- two
- three

View File

@ -12,21 +12,28 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from pecan import rest
import six
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from gluon.api.baseObject import APIBase
from gluon.api.baseObject import APIBaseObject
from gluon.api.baseObject import RootObjectController
from gluon.api.baseObject import SubObjectController
from gluon.api import types
# from gluon.api.baseObject import SubObjectController
from gluon.particleGenerator.DataBaseModelGenerator \
import DataBaseModelProcessor
class MyData(object):
pass
ApiGenData = MyData()
ApiGenData.svc_controllers = {}
class ServiceRoot(APIBase):
"""The root service URL"""
id = wtypes.text
@ -50,7 +57,6 @@ class ServiceController(rest.RestController):
class APIGenerator(object):
def __init__(self):
self.data = None
self.api_name = None
@ -67,10 +73,12 @@ class APIGenerator(object):
def create_api(self, root, service_name, db_models):
self.db_models = db_models
self.service_name = service_name
controllers = {}
self.controllers = {}
self.subcontrollers = {}
self.child = {}
if not self.data:
raise Exception('Cannot create API from empty model.')
for table_name, table_data in six.iteritems(self.data):
for table_name, table_data in six.iteritems(self.data['api_objects']):
try:
# For every entry build a (sub_)api_controller
# an APIObject, an APIObject and an APIListObject
@ -79,7 +87,11 @@ class APIGenerator(object):
for attribute, attr_value in \
six.iteritems(table_data['attributes']):
api_type = self.translate_model_to_api_type(
attr_value['type'], attr_value.get('values'))
attr_value.get('type'),
attr_value.get('values'),
attr_value.get('format'),
attr_value.get('min'),
attr_value.get('max'))
api_object_fields[attribute] = api_type
# API object
@ -87,70 +99,99 @@ class APIGenerator(object):
table_name, self.db_models[table_name], api_object_fields)
# api_name
api_name = table_data['api']['name']
api_name = table_data['api']['plural_name']
# primary_key_type
primary_key_type = self.translate_model_to_api_type(
self.get_primary_key_type(table_data), None)
p_type, p_vals, p_fmt = self.get_primary_key_type(table_data)
primary_key_type = self.translate_model_to_api_type(p_type,
p_vals,
p_fmt)
# parent_identifier_type
parent = table_data['api']['parent']['type']
if parent != 'root':
# parent_identifier_type = self.data[parent]['api']['name']
parent_attribute_name =\
table_data['api']['parent']['attribute']
# TODO(hambtw) SubObjectController is not working!!
# new_controller_class = SubObjectController.class_builder(
# api_name, api_object_class, primary_key_type,
# parent_identifier_type, parent_attribute_name)
else:
new_controller_class = RootObjectController.class_builder(
api_name, api_object_class, primary_key_type,
self.service_name)
# The childs have to be instantized before the
# parents so lets make a dict
if parent != 'root':
if 'childs' not in controllers.get(
parent_attribute_name, {}):
self.data[parent]['childs'] = []
self.data[parent]['childs'].append(
{'name': api_name,
'object': new_controller_class})
controllers[table_name] = new_controller_class
new_controller_class = RootObjectController.class_builder(
api_name, api_object_class, primary_key_type,
self.service_name)
self.controllers[table_name] = new_controller_class
except Exception:
print('During processing of table ' + table_name)
raise
# Now add all childs since the roots are there now
# And init the controller since all childs are there now
for table_name, table_data in six.iteritems(self.data):
controller = controllers[table_name]
for child in table_data.get('childs', []):
setattr(controller, child['name'], child['object']())
api_name = table_data['api']['name']
setattr(root, api_name, controller())
for table_name, table_data in six.iteritems(self.data['api_objects']):
controller = self.controllers[table_name]
if 'parent' in table_data['api']:
parent = table_data['api'].get('parent')
sub_name = table_data['api']['plural_name']
parent_controller = self.controllers.get(parent)
new_subcontroller_class = SubObjectController.class_builder(
sub_name,
controller.api_object_class,
controller.primary_key_type,
parent_controller.primary_key_type,
parent,
table_data['api'].get('parent_key'),
self.service_name)
self.subcontrollers[table_name] = new_subcontroller_class
self.child[parent] = table_name
setattr(parent_controller, sub_name, new_subcontroller_class())
for table_name, table_data in six.iteritems(self.data['api_objects']):
api_name = table_data['api']['plural_name']
controller_instance = self.controllers[table_name]()
if table_name in self.child:
child_name = self.child.get(table_name)
child_data = self.data['api_objects'].get(child_name)
child_api_name = child_data['api']['plural_name']
sub_instance = self.subcontrollers[child_name]()
setattr(controller_instance, child_api_name, sub_instance)
setattr(root, api_name, controller_instance)
ApiGenData.svc_controllers[service_name] = self.controllers
def get_primary_key_type(self, table_data):
primary_key = DataBaseModelProcessor.get_primary_key(
table_data)
return table_data['attributes'][primary_key]['type']
primary_key = DataBaseModelProcessor.get_primary_key(table_data)
attr = table_data['attributes'].get(primary_key)
return attr.get('type'), attr.get('values'), attr.get('format')
def translate_model_to_api_type(self, model_type, values):
def translate_model_to_api_type(self, model_type, values,
format=None,
min_val=None,
max_val=None):
# first make sure it is not a foreign key
if model_type in self.data:
if model_type in self.data['api_objects']:
# if it is we point to the primary key type type of this key
model_type = self.get_primary_key_type(
self.data[model_type])
model_type, values, format = self.get_primary_key_type(
self.data['api_objects'][model_type])
if model_type == 'uuid':
return types.uuid
if model_type == 'string':
elif model_type == 'string':
if format is not None:
if format == 'date-time':
return types.datetime_type
elif format == 'json':
return types.json_type
elif format == 'ipv4':
return types.ipv4_type
elif format == 'ipv6':
return types.ipv6_type
elif format == 'mac':
return types.mac_type
elif format == 'uri':
return types.uri_type
elif format == 'email':
return types.email_type
else:
return six.text_type
return six.text_type
if model_type == 'enum':
elif model_type == 'enum':
return types.create_enum_type(*values)
if model_type == 'integer':
return types.int_type
if model_type == 'boolean':
elif model_type == 'integer':
return types.int_type(min_val, max_val)
elif model_type == 'number':
return types.float_type
elif model_type == 'boolean':
return types.boolean
raise Exception("Type %s not known." % model_type)
def get_controller(service_name, name):
return ApiGenData.svc_controllers[service_name].get(name)

View File

@ -56,10 +56,10 @@ class DataBaseModelProcessor(object):
return ret_str.lower().replace("-", "_")
# Make a model class that we've never thought of before
for table_name, table_data in six.iteritems(self.data):
for table_name, table_data in six.iteritems(self.data['api_objects']):
self.get_primary_key(table_data)
for table_name, table_data in six.iteritems(self.data):
for table_name, table_data in six.iteritems(self.data['api_objects']):
try:
attrs = {}
for col_name, col_desc in six.iteritems(
@ -70,12 +70,12 @@ class DataBaseModelProcessor(object):
args = []
# Step 1: deal with object xrefs
if col_desc['type'] in self.data:
if col_desc['type'] in self.data['api_objects']:
# This is a foreign key reference. Make the column
# like the FK, but drop the primary from it and
# use the local one.
tgt_name = col_desc['type']
tgt_data = self.data[tgt_name]
tgt_data = self.data['api_objects'][tgt_name]
primary_col = tgt_data['primary']
repl_col_desc = \
@ -127,6 +127,9 @@ class DataBaseModelProcessor(object):
elif col_desc['type'] == 'integer':
attrs[col_name] = sa.Column(sa.Integer(), *args,
**options)
elif col_desc['type'] == 'number':
attrs[col_name] = sa.Column(sa.Float(), *args,
**options)
elif col_desc['type'] == 'boolean':
attrs[col_name] = sa.Column(sa.Boolean(), *args,
**options)

View File

@ -24,10 +24,9 @@ from requests import delete
from requests import get
from requests import post
from requests import put
import yaml
from gluon.common import exception as exc
from gluon.particleGenerator.generator import load_model
def print_basic_usage(argv, model_list):
@ -70,20 +69,12 @@ def get_api_model(argv, model_list):
def get_model_list(package_name, model_dir):
model_list = list()
for f in pkg_resources.resource_listdir(package_name, model_dir):
if f == 'base':
continue
model_list.append(f)
return model_list
def load_model(package_name, model_dir, model_name):
model_dir = model_dir + "/" + model_name
model = {}
for f in pkg_resources.resource_listdir(package_name, model_dir):
f = model_dir + '/' + f
with pkg_resources.resource_stream(package_name, f) as fd:
model.update(yaml.safe_load(fd))
return model
def get_token():
auth_url = os.environ.get('OS_AUTH_URL')
tenant = os.environ.get('OS_TENANT_NAME')
@ -261,6 +252,8 @@ def set_type(kwargs, col_desc):
pass
elif col_desc['type'] == 'integer':
kwargs["type"] = int
elif col_desc['type'] == 'number':
kwargs["type"] = float
elif col_desc['type'] == 'boolean':
kwargs["type"] = bool
elif col_desc['type'] == 'enum':
@ -278,19 +271,19 @@ def proc_model(cli, package_name="unknown",
portdefault=0):
# print("loading model")
model = load_model(package_name, model_dir, api_model)
for table_name, table_data in six.iteritems(model):
for table_name, table_data in six.iteritems(model['api_objects']):
get_primary_key(table_data)
for table_name, table_data in six.iteritems(model):
for table_name, table_data in six.iteritems(model['api_objects']):
try:
attrs = {}
for col_name, col_desc in six.iteritems(table_data['attributes']):
try:
# Step 1: deal with object xrefs
if col_desc['type'] in model:
if col_desc['type'] in model['api_objects']:
# If referencing another object,
# get the type of its primary key
tgt_name = col_desc['type']
tgt_data = model[tgt_name]
tgt_data = model['api_objects'][tgt_name]
primary_col = tgt_data['primary']
table_data["attributes"][col_name]['type'] = \
tgt_data["attributes"][primary_col]["type"]
@ -308,12 +301,12 @@ def proc_model(cli, package_name="unknown",
if '_primary_key' not in attrs:
raise Exception("One and only one primary key has to "
"be given to each column")
attrs['__tablename__'] = table_data['api']['name']
attrs['__tablename__'] = table_data['api']['plural_name']
# chop off training 's'
attrs['__objname__'] = table_data['api']['name'][:-1]
attrs['__objname__'] = table_data['api']['name']
#
# Create CDUD commands for the table
# Create CRUD commands for the table
#
hosthelp = "Host of endpoint (%s) " % hostenv
porthelp = "Port of endpoint (%s) " % portenv

View File

@ -14,35 +14,324 @@
# under the License.
import pkg_resources
import six
import yaml
from gluon.db.sqlalchemy import models as sql_models
from oslo_log import log as logging
from gluon.common import exception as exc
from gluon.db.sqlalchemy import models as sql_models
LOG = logging.getLogger(__name__)
class MyData(object):
pass
GenData = MyData()
GenData.DBGeneratorInstance = None
GenData.models = dict()
GenData.package_name = "gluon"
GenData.model_dir = "models/proton"
GenData.model_dir = "models"
def raise_format_error(format_str, val_tuple):
str = format_str % val_tuple
raise exc.InvalidFileFormat(str)
def raise_obj_error(obj_name, format_str, val_tuple):
str = format_str % val_tuple
raise_format_error("Object: %s, %s", (obj_name, str))
def validate_attributes(obj_name, obj, model):
props = ['type', 'primary', 'description', 'required',
'length', 'values', 'format', 'min', 'max']
types = ['integer', 'number', 'string', 'boolean', 'uuid', 'enum']
formats = ['date-time', 'json', 'ipv4', 'ipv6', 'mac', 'uri', 'email']
int_formats = ['int32', 'int64']
for attr_name, attr_val in six.iteritems(obj.get('attributes')):
if 'type' not in attr_val:
raise_obj_error(obj_name,
'A type property is not specified for '
'attribute: %s, ',
(attr_name))
for prop_name, prop_val in six.iteritems(attr_val):
if prop_name in props:
if prop_name == 'type':
if prop_val not in types and \
prop_val not in model.get('api_objects'):
raise_obj_error(
obj_name,
'Invalid type: %s for attribute: %s, '
'expected type or API Object Name',
(prop_val, attr_name))
elif prop_val == 'enum':
if 'values' not in attr_val:
raise_obj_error(
obj_name,
'No enum values specified for attribute: %s',
(attr_name))
elif prop_name == 'format':
if attr_val.get('type') != 'string' and \
attr_val.get('type') != 'integer':
raise_obj_error(
obj_name,
'Format is only valid for string or integer '
'type: %s, attribute: %s',
(prop_val, attr_name))
if attr_val.get('type') == 'string':
if prop_val not in formats:
raise_obj_error(
obj_name,
'Invalid format: %s for attribute: %s',
(prop_val, attr_name))
if attr_val.get('type') == 'integer':
if prop_val not in int_formats:
raise_obj_error(
obj_name,
'Invalid int format: %s for attribute: %s',
(prop_val, attr_name))
elif prop_name == 'values':
if attr_val.get('type') != 'enum':
raise_obj_error(
obj_name,
'Values without enum specified for attribute: %s',
(attr_name))
elif prop_name == 'length':
if not isinstance(prop_val, six.integer_types):
raise_obj_error(
obj_name,
'Integer values required for length: '
'%s, attribute: %s',
(prop_val, attr_name))
elif prop_name == 'min' or prop_name == 'max':
if attr_val.get('type') != 'integer':
raise_obj_error(
obj_name,
'Min/Max is only valid for integer '
'type: %s, attribute: %s',
(prop_val, attr_name))
if not isinstance(prop_val, six.integer_types):
raise_obj_error(
obj_name,
'Integer values required for Min/Max: '
'%s, attribute: %s',
(prop_val, attr_name))
else:
raise_obj_error(
obj_name,
'Invalid property in AttributeSchema: %s for %s',
(prop_name, attr_name))
def validate_api(obj_name, obj_val, model):
api = obj_val.get('api')
if 'name' not in api:
raise_format_error('Name is missing in API object for: %s', (obj_name))
if 'parent' in api:
if api.get('parent') not in model.get('api_objects'):
raise_obj_error(
obj_name,
'API parent: %s does not reference an API object',
(api.get('parent')))
if 'parent_key' not in api:
raise_obj_error(
obj_name,
'parent_key must be present if parent property is present',
())
elif api.get('parent_key') not in obj_val.get('attributes'):
raise_obj_error(
obj_name,
'parent_key contains unkown attribute: %s',
(api.get('parent_key')))
def verify_model(model):
valid_versions = ["1.0"]
# Verify file version is correct
if 'file_version' not in model:
raise_format_error('Missing file_version object', ())
if model['file_version'] not in valid_versions:
raise_format_error('Invalid file version: %s', (model['file_version']))
if 'info' not in model:
raise_format_error('No info object defined', ())
if 'name' not in model.get('info'):
raise_format_error('Info object missing name', ())
# Verify that BasePort, BaseInterface and BaseService have been extended
if 'api_objects' not in model or len(model['api_objects']) == 0:
raise_format_error('No API objects are defined', ())
baseport_found = False
baseinterface_found = False
baseservice_found = False
for obj_name, obj_val in six.iteritems(model['api_objects']):
if obj_val.get('extends') == 'BasePort':
if baseport_found:
raise_format_error(
'Only one object can extend BasePort', ())
baseport_found = True
if obj_val.get('extends') == 'BaseInterface':
if baseinterface_found:
raise_format_error(
'Only one object can extend BaseInterface', ())
baseinterface_found = True
if obj_val.get('extends') == 'BaseService':
baseservice_found = True
if 'attributes' not in obj_val:
raise_format_error(
'No attributes specified for object: %s', (obj_name))
validate_attributes(obj_name, obj_val, model)
validate_api(obj_name, obj_val, model)
if not baseport_found:
raise_format_error(
'BasePort must be extended by an API object', ())
if not baseinterface_found:
raise_format_error(
'BaseInterface must be extended by an API object', ())
if not baseservice_found:
raise_format_error(
'BaseService must be extended by an API object', ())
def extend_object(obj, obj_dict):
name = obj.get('extends')
if name not in obj_dict:
raise_format_error('extends references unkown object: %s', (name))
ext_obj = obj_dict.get(name)
orig_attrs = obj.get('attributes', dict())
obj['attributes'] = dict()
orig_policies = obj.get('policies', dict())
obj['policies'] = dict()
if 'attributes' in ext_obj:
for attr_name, attr_val in \
six.iteritems(ext_obj.get('attributes')):
if attr_name not in obj['attributes']:
obj['attributes'].__setitem__(attr_name, attr_val)
else:
obj['attributes'][attr_name].update(attr_val)
for attr_name, attr_val in six.iteritems(orig_attrs):
if attr_name not in obj['attributes']:
obj['attributes'].__setitem__(attr_name, attr_val)
else:
obj['attributes'][attr_name].update(attr_val)
if 'policies' in ext_obj:
for rule_name, rule_val in six.iteritems(ext_obj.get('policies')):
if rule_name not in obj['policies']:
obj['policies'].__setitem__(rule_name, rule_val)
else:
obj['policies'][rule_name].update(rule_val)
for rule_name, rule_val in six.iteritems(orig_policies):
if rule_name not in obj['policies']:
obj['policies'].__setitem__(rule_name, rule_val)
else:
obj['policies'][rule_name].update(rule_val)
return obj
def proc_object_extensions(dicta, dictb):
moved_list = list()
for obj_name, obj_val in six.iteritems(dicta):
if obj_val.get('extends') in dictb:
dictb[obj_name] = extend_object(obj_val, dictb)
moved_list.append(obj_name)
for obj_name in moved_list:
dicta.__delitem__(obj_name)
if len(dicta):
proc_object_extensions(dicta, dictb)
def extend_base_objects(model):
# First we move non-extended objects to new list
for obj_name, obj_val in six.iteritems(model.get('base_objects')):
if 'extends' in obj_val:
if obj_val.get('extends') not in model.get('base_objects'):
raise_format_error('extends references unkown object: %s',
(obj_val.get('extends')))
new_dict = dict()
moved_list = list()
for obj_name, obj_val in six.iteritems(model.get('base_objects')):
if 'extends' not in obj_val:
moved_list.append(obj_name)
new_dict.__setitem__(obj_name, obj_val)
for obj_name in moved_list:
model['base_objects'].__delitem__(obj_name)
proc_object_extensions(model['base_objects'], new_dict)
model['base_objects'] = new_dict
return model
def extend_api_objects(model):
new_dict = dict()
for obj_name, obj_val in six.iteritems(model.get('api_objects')):
if 'extends' in obj_val:
if obj_val.get('extends') not in model.get('base_objects'):
raise_obj_error(obj_name,
'extends references unkown object: %s',
(obj_val.get('extends')))
new_dict[obj_name] = extend_object(obj_val,
model.get('base_objects'))
model.get('api_objects').update(new_dict)
return model
def append_model(model, yaml_dict):
main_objects = ['file_version', 'imports', 'info', 'objects']
for obj_name in six.iterkeys(yaml_dict):
if obj_name not in main_objects:
raise_format_error('Invalid top level object: %s', (obj_name))
file_version = yaml_dict.get('file_version')
cur_file_version = model.get('file_version')
if file_version and cur_file_version:
if file_version != file_version:
raise_format_error('File version mismatch %s', (file_version))
else:
model['file_version'] = yaml_dict['file_version']
elif file_version:
model['file_version'] = file_version
if 'imports' in yaml_dict:
model['imports'] = yaml_dict.get('imports')
if 'info' in yaml_dict:
model['info'] = yaml_dict.get('info')
if 'api_objects' not in model:
model['api_objects'] = dict()
if 'base_objects' not in model:
model['base_objects'] = dict()
for obj_name, obj_val in six.iteritems(yaml_dict.get('objects')):
if 'api' in obj_val:
if 'plural_name' not in obj_val['api']:
obj_val['api']['plural_name'] = \
obj_val['api'].get('name', '') + 's'
model['api_objects'].__setitem__(obj_name, obj_val)
else:
model['base_objects'].__setitem__(obj_name, obj_val)
def load_model(package_name, model_dir, model_name):
model_path = model_dir + "/" + model_name
model = {}
for f in pkg_resources.resource_listdir(package_name, model_path):
f = model_path + '/' + f
with pkg_resources.resource_stream(package_name, f) as fd:
append_model(model, yaml.safe_load(fd))
imports_path = model.get('imports')
if imports_path:
f = model_dir + '/' + imports_path
with pkg_resources.resource_stream(package_name, f) as fd:
append_model(model, yaml.safe_load(fd))
extend_base_objects(model)
extend_api_objects(model)
return model
# Singleton generator
def load_model(service):
def load_model_for_service(service):
if GenData.models.get(service) is None:
model_dir = GenData.model_dir + "/" + service
GenData.models[service] = {}
for f in pkg_resources.resource_listdir(
GenData.package_name, model_dir):
f = model_dir + "/" + f
with pkg_resources.resource_stream(GenData.package_name, f) as fd:
GenData.models[service].update(yaml.safe_load(fd))
GenData.models[service] = load_model(GenData.package_name,
GenData.model_dir,
service)
return GenData.models.get(service)
@ -53,17 +342,17 @@ def build_sql_models(service_list):
GenData.DBGeneratorInstance = DataBaseModelProcessor()
base = sql_models.Base
for service in service_list:
GenData.DBGeneratorInstance.add_model(load_model(service))
GenData.DBGeneratorInstance.add_model(load_model_for_service(service))
GenData.DBGeneratorInstance.build_sqla_models(service, base)
def build_api(root, service_list):
from gluon.particleGenerator.ApiGenerator import APIGenerator
for service in service_list:
load_model(service)
load_model_for_service(service)
api_gen = APIGenerator()
service_root = api_gen.create_controller(service, root)
api_gen.add_model(load_model(service))
api_gen.add_model(load_model_for_service(service))
api_gen.create_api(service_root, service,
GenData.DBGeneratorInstance.get_db_models(service))

View File

@ -65,7 +65,7 @@ class GluonPlugin(Ml2Plugin):
LOG.error(
"Cannot connect to etcd, make sure that etcd is running.")
except Exception as e:
LOG.error("Unkown exception:", e)
LOG.error("Unkown exception:", str(e))
return None
@log_helpers.log_method_call
@ -124,8 +124,6 @@ class GluonPlugin(Ml2Plugin):
except etcd.EtcdException:
LOG.error(
"Cannot connect to etcd, make sure that etcd is running.")
except Exception as e:
LOG.error("Unkown exception:", e)
@log_helpers.log_method_call
def update_gluon_port(self, backend, id, port):

View File

@ -7,9 +7,10 @@ configure the underlying SDN controller.
The shim layer server recursively watches for changes to keys in the /proton directory.
It will receive changes to keys in the following directories:
/proton/net-l3vpn/ProtonBasePort
/proton/net-l3vpn/VPNPort
/proton/net-l3vpn/VpnInstance
/proton/net-l3vpn/Port
/proton/net-l3vpn/Interface
/proton/net-l3vpn/VpnBinding
/proton/net-l3vpn/VpnService
/proton/net-l3vpn/VpnAfConfig
NOTE: This is a change from the previous implementation where it watched for changes in
@ -34,7 +35,7 @@ are called. The idea is that a vendor could copy/paste the dummy_net_l3vpn clas
add the code to configure their SDN controller based on the callback and model data.
The bind_port() callback shows how to pass back vif_type and vif_details to be updated in
the ProtonBasePort instance. This is done by the following protocol:
the Port instance. This is done by the following protocol:
The bind_port() callback will return a dict containing the vif_tpe and vif_details.
@ -46,7 +47,7 @@ for the controller name that handled the bind request.
The etcd key will be of the form:
/controller/net-l3vpn/ProtonBasePort/<uuid>
/controller/net-l3vpn/Port/<uuid>
The data will look like:

View File

@ -31,12 +31,13 @@ class ApiNetL3VPN(ApiModelBase):
super(self.__class__, self).__init__("net-l3vpn")
self.resync_mode = False
self.model.vpn_instances = dict()
self.model.vpn_ports = dict()
self.model.vpnbindings = dict()
self.model.vpn_afconfigs = dict()
def load_model(self, shim_data):
self.resync_mode = True
objects = ["ProtonBasePort", "VpnInstance", "VpnAfConfig", "VPNPort"]
objects = ["Port", "Interface", "VpnService",
"VpnAfConfig", "VpnBinding"]
for obj_name in objects:
etcd_path = "{0:s}/{1:s}/{2:s}".format("proton", self.name,
obj_name)
@ -67,7 +68,7 @@ class ApiNetL3VPN(ApiModelBase):
def get_etcd_bound_data(self, shim_data, key):
etcd_key = "{0:s}/{1:s}/{2:s}/{3:s}".format("controller", self.name,
"ProtonBasePort", key)
"Port", key)
try:
vif_dict = json.loads(shim_data.client.get(etcd_key).value)
if not vif_dict:
@ -80,7 +81,7 @@ class ApiNetL3VPN(ApiModelBase):
def update_etcd_unbound(self, shim_data, key):
etcd_key = "{0:s}/{1:s}/{2:s}/{3:s}".format("controller", self.name,
"ProtonBasePort", key)
"Port", key)
try:
data = {}
shim_data.client.write(etcd_key, json.dumps(data))
@ -90,7 +91,7 @@ class ApiNetL3VPN(ApiModelBase):
def update_etcd_bound(self, shim_data, key, vif_dict):
vif_dict["controller"] = shim_data.name
etcd_key = "{0:s}/{1:s}/{2:s}/{3:s}".format("controller", self.name,
"ProtonBasePort", key)
"Port", key)
try:
shim_data.client.write(etcd_key, json.dumps(vif_dict))
except Exception as e:
@ -152,6 +153,15 @@ class ApiNetL3VPN(ApiModelBase):
else:
self.model.ports[key]["__state"] = "InUse"
def handle_interface_change(self, key, attributes, shim_data):
if key in self.model.interfaces:
changes = self.model.interfaces[key].update_attrs(attributes)
self.backend.modify_interface(key, self.model, changes)
else:
obj = Model.DataObj(key, attributes)
self.model.interfaces[key] = obj
self.backend.modify_interface(key, self.model, attributes)
def handle_vpn_instance_change(self, key, attributes, shim_data):
if key in self.model.vpn_instances:
changes = self.model.vpn_instances[key].update_attrs(attributes)
@ -161,16 +171,16 @@ class ApiNetL3VPN(ApiModelBase):
self.model.vpn_instances[key] = obj
self.backend.modify_service(key, self.model, attributes)
def handle_vpn_port_change(self, key, attributes, shim_data):
if key in self.model.vpn_ports:
def handle_vpn_binding_change(self, key, attributes, shim_data):
if key in self.model.vpnbindings:
prev_binding = \
{"id": self.model.vpn_ports[key].id,
"vpn_instance": self.model.vpn_ports[key].vpn_instance}
self.model.vpn_ports[key].update_attrs(attributes)
{"interface_id": self.model.vpnbindings[key].id,
"service_id": self.model.vpnbindings[key].service_id}
self.model.vpnbindings[key].update_attrs(attributes)
self.backend.modify_service_binding(key, self.model, prev_binding)
else:
obj = Model.DataObj(key, attributes)
self.model.vpn_ports[key] = obj
self.model.vpnbindings[key] = obj
self.backend.modify_service_binding(key, self.model, {})
def handle_vpnafconfig_change(self, key, attributes, shim_data):
@ -184,9 +194,10 @@ class ApiNetL3VPN(ApiModelBase):
changes.new["ipv6_family"] = vpn_instance["ipv6_family"]
if len(changes.new) > 0:
port = None
for vpn_port in self.model.vpn_ports.itervalues():
if vpn_port["vpn_instance"] == vpn_instance["id"]:
port = self.model.ports.get(vpn_port["id"])
for vpn_binding in self.model.vpnbindings.itervalues():
if vpn_binding["service_id"] == vpn_instance["id"]:
port = self.model.ports.get(
vpn_binding["interface_id"])
if port and port.get("__state") == "Bound":
self.backend.modify_service(vpn_instance["id"],
self.model, changes)
@ -195,12 +206,14 @@ class ApiNetL3VPN(ApiModelBase):
self.model.vpn_afconfigs[key] = obj
def handle_object_change(self, object_type, key, attributes, shim_data):
if object_type == "ProtonBasePort":
if object_type == "Port":
self.handle_port_change(key, attributes, shim_data)
elif object_type == "VpnInstance":
elif object_type == "Interface":
self.handle_interface_change(key, attributes, shim_data)
elif object_type == "VpnService":
self.handle_vpn_instance_change(key, attributes, shim_data)
elif object_type == "VPNPort":
self.handle_vpn_port_change(key, attributes, shim_data)
elif object_type == "VpnBinding":
self.handle_vpn_binding_change(key, attributes, shim_data)
elif object_type == "VpnAfConfig":
self.handle_vpnafconfig_change(key, attributes, shim_data)
else:
@ -212,16 +225,22 @@ class ApiNetL3VPN(ApiModelBase):
del self.model.ports[key]
self.backend.delete_port(key, self.model, deleted_obj)
def handle_interface_delete(self, key, shim_data):
if key in self.model.interfaces:
deleted_obj = self.model.interfaces[key]
del self.model.interfaces[key]
self.backend.delete_interface(key, self.model, deleted_obj)
def handle_vpn_instance_delete(self, key, shim_data):
if key in self.model.vpn_instances:
deleted_obj = self.model.vpn_instances[key]
del self.model.vpn_instances[key]
self.backend.delete_service(key, self.model, deleted_obj)
def handle_vpn_port_delete(self, key, shim_data):
if key in self.model.vpn_ports:
deleted_obj = self.model.vpn_ports[key]
del self.model.vpn_ports[key]
def handle_vpn_binding_delete(self, key, shim_data):
if key in self.model.vpnbindings:
deleted_obj = self.model.vpnbindings[key]
del self.model.vpnbindings[key]
self.backend.delete_service_binding(self.model, deleted_obj)
def handle_vpnafconfig_delete(self, key, shim_data):
@ -239,20 +258,23 @@ class ApiNetL3VPN(ApiModelBase):
changes.new["ipv6_family"] = ','.join(l)
if len(changes.new) > 0:
port = None
for vpn_port in self.model.vpn_ports.itervalues():
if vpn_port["vpn_instance"] == vpn_instance["id"]:
port = self.model.ports.get(vpn_port["id"])
for vpn_binding in self.model.vpnbindings.itervalues():
if vpn_binding["service_id"] == vpn_instance["id"]:
port = self.model.ports.get(
vpn_binding["interface_id"])
if port and port.get("__state") == "Bound":
self.backend.modify_service(vpn_instance["id"],
self.model, changes)
def handle_object_delete(self, object_type, key, shim_data):
if object_type == "ProtonBasePort":
if object_type == "Port":
self.handle_port_delete(key, shim_data)
elif object_type == "VpnInstance":
elif object_type == "Interface":
self.handle_interface_delete(key, shim_data)
elif object_type == "VpnService":
self.handle_vpn_instance_delete(key, shim_data)
elif object_type == "VPNPort":
self.handle_vpn_port_delete(key, shim_data)
elif object_type == "VpnBinding":
self.handle_vpn_binding_delete(key, shim_data)
elif object_type == "VpnAfConfig":
self.handle_vpnafconfig_delete(key, shim_data)
else:

View File

@ -17,7 +17,6 @@ from oslo_log import log as logging
from gluon.shim.base import HandlerBase
LOG = logging.getLogger(__name__)
@ -37,11 +36,11 @@ class DummyNetL3VPN(HandlerBase):
if not port:
LOG.error("Cannot find port")
return dict()
service_binding = model.vpn_ports.get(uuid, None)
service_binding = model.vpnbindings.get(uuid, None)
if not service_binding:
LOG.error("Cannot bind port, not bound to a servcie")
return dict()
vpn_instance = model.vpn_instances.get(service_binding["vpn_instance"],
vpn_instance = model.vpn_instances.get(service_binding["service_id"],
None)
if not vpn_instance:
LOG.error("VPN instance not available!")
@ -109,6 +108,28 @@ class DummyNetL3VPN(HandlerBase):
"""
pass
def modify_interface(self, uuid, model, changes):
"""Called when attributes on an interface
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
LOG.info("modify_interface: %s" % uuid)
LOG.info(changes)
pass
def delete_interface(self, uuid, model, changes):
"""Called when an interface is deleted
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
pass
def modify_service(self, uuid, model, changes):
"""Called when attributes change on a bound port's service

View File

@ -50,12 +50,12 @@ class OdlNetL3VPN(HandlerBase):
# check if a valid service binding exists already in the model
# if so, create or update it on the SDN controller
service_binding = model.vpn_ports.get(uuid, None)
service_binding = model.vpnbindings.get(uuid, None)
if not service_binding:
LOG.info("Port not bound to a service yet.")
else:
vpn_instance = model.vpn_instances.get(
service_binding["vpn_instance"],
service_binding["service_id"],
None)
if not vpn_instance:
LOG.warn("VPN instance not defined yet.")
@ -117,6 +117,27 @@ class OdlNetL3VPN(HandlerBase):
LOG.info("Deleting IETF Interface")
self.odlclient.delete_ietf_interface(uuid)
def modify_interface(self, uuid, model, changes):
"""Called when attributes on an interface
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
LOG.info("modify_interface: %s" % uuid)
LOG.info(changes)
def delete_interface(self, uuid, model, changes):
"""Called when an interface is deleted
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
LOG.info("delete_interface: %s" % uuid)
def modify_service(self, uuid, model, changes):
"""Called when attributes change on a bound port's service
@ -178,7 +199,7 @@ class OdlNetL3VPN(HandlerBase):
LOG.info("modify_service_binding: %s" % uuid)
LOG.info(prev_binding)
vpn_instance = model.vpn_ports[uuid].vpn_instance
vpn_instance = model.vpnbindings[uuid].vpn_instance
port = model.ports.get(uuid)
ip_address = port.ipaddress
prefix = int(port.subnet_prefix)
@ -227,8 +248,8 @@ class OdlNetL3VPN(HandlerBase):
subnet = utils.compute_network_addr(port.ipaddress, port.subnet_prefix)
found_another_port = False
for k in model.vpn_ports:
vpnport = model.vpn_ports[k]
for k in model.vpnbindings:
vpnport = model.vpnbindings[k]
p = model.ports.get(vpnport.id)
sn = utils.compute_network_addr(p.ipaddress, p.subnet_prefix)
if subnet == sn:

View File

@ -26,7 +26,6 @@ class ApiModelBase(object):
"""Init Method.
:param _name: name of the API Servce (e.g. net-l3vpn)
:param _backend: instance of ControllerBase
:returns: None
"""
self.model = Model() # Internal Model for the API Service
@ -111,6 +110,30 @@ class HandlerBase(object):
pass
@abc.abstractmethod
def modify_interface(self, uuid, model, changes):
"""Called when attribute of an interface changes.
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
pass
@abc.abstractmethod
def delete_interface(self, uuid, model, changes):
"""Called when an interface is deleted
:param uuid: UUID of Interface
:param model: Model Object
:param changes: dictionary of changed attributes
:returns: None
"""
pass
@abc.abstractmethod
def modify_service(self, uuid, model, changes):
"""Called when attribute of a service with a bound port changed.

View File

@ -149,7 +149,7 @@ def main():
help='Comma separated list of hostnames managed by '
'this server'),
cfg.DictOpt('handlers',
default={'net-l3vpn': 'net-l3vpn-odl'},
default={'net-l3vpn': 'net-l3vpn-dummy'},
help='Dict for selecting the handlers '
'and their corresponding backend.')
]

View File

@ -97,3 +97,4 @@ class Model(object):
def __init__(self):
self.ports = dict() # Port objects
self.interfaces = dict()

View File

@ -11,7 +11,7 @@
# 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 gluon.particleGenerator.cli import load_model
from gluon.particleGenerator.generator import load_model
from gluon.tests import base
@ -22,7 +22,7 @@ class ParticleGeneratorTestCase(base.TestCase):
pass
def load_testing_model(self):
testing_model = load_model("gluon.tests.particleGenerator",
"",
"models")
testing_model = load_model("gluon",
"models",
"test")
return testing_model

View File

@ -17,6 +17,7 @@ from mock import patch
import six
import os
from wsme import types as wtypes
from gluon.api.baseObject import APIBase
from gluon.api.baseObject import APIBaseObject
@ -48,8 +49,9 @@ class ApiGeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
ManagerData.managers[mock_service_name] = object()
self.apiGenerator.add_model(testing_model)
self.apiGenerator.create_api(root, mock_service_name, mock_db_models)
api_name = six.next(six.itervalues(testing_model))['api']['name']
self.assertEqual(True, hasattr(root, api_name))
self.assertEqual(True, hasattr(root, "ports"))
self.assertEqual(True, hasattr(root, "interfaces"))
self.assertEqual(True, hasattr(root, "tests"))
@mock.patch.object(DataBaseModelProcessor, 'get_primary_key')
def test_get_primary_key_type(self, mock_get_pk):
@ -57,7 +59,7 @@ class ApiGeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
foo = 'foo'
table_data = {'attributes': {primary_key: {'type': foo}}}
mock_get_pk.return_value = primary_key
type_ = self.apiGenerator.get_primary_key_type(table_data)
type_, vals, fmt = self.apiGenerator.get_primary_key_type(table_data)
mock_get_pk.assert_called_once_with(table_data)
self.assertEqual(type_, foo)
"""
@ -115,10 +117,12 @@ class ApiGeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
mock_enum):
m = mock.Mock()
model_type = 'a'
self.apiGenerator.add_model({model_type: m})
self.apiGenerator.add_model({'api_objects': {}, model_type: m})
self.load_testing_model()
mock_get_pkt.return_value = "string"
self.apiGenerator.translate_model_to_api_type(model_type, [])
mock_get_pkt.assert_called_once_with(m)
# TODO(hambtw): Rework
# self.apiGenerator.translate_model_to_api_type(model_type, [])
# mock_get_pkt.assert_called_once_with(m)
self.assertEqual(
types.uuid,
self.apiGenerator.translate_model_to_api_type('uuid', []))
@ -129,8 +133,8 @@ class ApiGeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
self.apiGenerator.translate_model_to_api_type('enum', values)
mock_enum.assert_called_once_with(*values)
self.assertEqual(
types.int_type,
self.apiGenerator.translate_model_to_api_type('integer', []))
wtypes.IntegerType,
type(self.apiGenerator.translate_model_to_api_type('integer', [])))
self.assertEqual(
types.boolean,
self.apiGenerator.translate_model_to_api_type('boolean', []))

View File

@ -91,46 +91,6 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase):
expected = ['test_model.yaml']
self.assertEqual(expected, observed)
"""
test load_model
"""
def test_load_model(self):
observed = cli.load_model("gluon.tests.particleGenerator",
"",
"models")
expected = {'GluonInternalPort':
{'api':
{'name': 'ports',
'parent': {'type': 'root'}
},
'attributes':
{'device_id':
{'description': 'UUID of bound VM',
'type': 'uuid'
},
'device_owner':
{'description':
'Name of compute or network service (if bound)',
'length': 128,
'type': 'string'
},
'id':
{'description': 'UUID of port',
'primary': True,
'required': True,
'type': 'uuid'
},
'owner':
{'description':
'Pointer to backend service instance (name)',
'required': True,
'type': 'string'
}
}
}
}
self.assertEqual(expected, observed)
"""
test json_get
"""
@ -352,7 +312,7 @@ class CliTestCase(partgen_base.ParticleGeneratorTestCase):
"""
def test_get_primary_key(self):
model = self.load_testing_model()
table_data = model['GluonInternalPort']
table_data = model['api_objects']['Port']
observed = cli.get_primary_key(table_data)
expected = 'id'
self.assertEqual(expected, observed)

View File

@ -36,11 +36,12 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
test load_model(service)
"""
def test_load_model(self):
service_models = generator.load_model("net-l3vpn")
self.assertIn('VpnAfConfig', service_models)
self.assertIn('VpnInstance', service_models)
self.assertIn('ProtonBasePort', service_models)
self.assertIn('VPNPort', service_models)
service_models = generator.load_model("gluon",
"models",
"test")
self.assertIn('TestService', service_models['api_objects'])
self.assertIn('Port', service_models['api_objects'])
self.assertIn('Interface', service_models['api_objects'])
"""
test build_sql_models(service_list)
@ -55,11 +56,11 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
mock_service = {'foo': 'bar'}
mock_load_model.return_value = mock_service
generator.build_sql_models(['net-l3vpn'])
mock_load_model.assert_called_with('net-l3vpn')
mock_add_model.assert_called_with(mock_service)
mock_build_sqla_models.assert_called_with('net-l3vpn', sql_models.Base)
generator.build_sql_models(['test'])
# TODO(hambtw): Need to rework
# mock_load_model.assert_called_with('foo')
# mock_add_model.assert_called_with(mock_service)
mock_build_sqla_models.assert_called_with('test', sql_models.Base)
"""
test build_api
@ -77,7 +78,7 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
mock_create_controller,
mock_load_model):
root = object()
service_list = ['net-l3vpn']
service_list = ['test']
mock_service = {'foo': 'bar'}
mock_load_model.return_value = mock_service
db_models = mock.Mock()
@ -87,12 +88,12 @@ class GeneratorTestCase(partgen_base.ParticleGeneratorTestCase):
generator.build_api(root, service_list)
mock_load_model.assert_called_with('net-l3vpn')
mock_create_controller.assert_called_with('net-l3vpn', root)
mock_load_model.assert_called_with('gluon', 'models', 'test')
mock_create_controller.assert_called_with('test', root)
mock_add_model.assert_called_with(mock_service)
mock_DBGeneratorInstance.get_db_models.assert_called_with('net-l3vpn')
mock_DBGeneratorInstance.get_db_models.assert_called_with('test')
mock_create_api.assert_called_with(service_root,
'net-l3vpn',
'test',
db_models)
"""
test get_db_gen

View File

@ -18,6 +18,7 @@ six>=1.9.0 # MIT
WSME>=0.8 # MIT
pecan>=1.0.0 # BSD
requests!=2.9.0,>=2.8.1 # Apache-2.0
rfc3986>=0.3.1 # Apache-2.0
PyYAML>=3.1.0 # MIT
pytz>=2013.6 # MIT
click>=6.6

View File

@ -32,6 +32,7 @@ console_scripts =
proton-server = gluon.cmd.api:main
protonclient = gluon.cmd.cli:main
proton-shim-server = gluon.shim.main:main
gluon-api-tool = gluon.cmd.api_tool:main
gluon.backends =
net-l3vpn = gluon.backends.models.net_l3vpn:Provider

View File

@ -15,6 +15,7 @@ sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD
oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
oslo.policy>=1.15.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD