Adding code for the device admin API. All GET, POST, PUT, and DELETE calls should be working
Change-Id: Ibada48f8987defcff37764f2dc391d5c6e9e0969
This commit is contained in:
@@ -14,41 +14,289 @@
|
||||
# under the License.
|
||||
|
||||
# pecan imports
|
||||
from pecan import expose # , abort, response, request
|
||||
import logging
|
||||
from pecan import expose, response, abort
|
||||
from pecan.rest import RestController
|
||||
import wsmeext.pecan as wsme_pecan
|
||||
#from wsme.exc import ClientSideError
|
||||
from libra.api_admin.model.validators import DeviceResp, DevicePost
|
||||
from wsme.exc import ClientSideError
|
||||
from usage import UsageController
|
||||
from libra.admin_api.model.validators import DeviceResp, DevicePost, DevicePut
|
||||
from libra.api.model.lbaas import LoadBalancer, Device, session
|
||||
from libra.api.model.lbaas import loadbalancers_devices
|
||||
|
||||
|
||||
class DevicesController(RestController):
|
||||
|
||||
usage = UsageController()
|
||||
|
||||
def __init__(self, devid=None):
|
||||
# Required for PUT requests. See _lookup() below
|
||||
self.devid = devid
|
||||
|
||||
@expose('json')
|
||||
def get(self, device_id=None, marker=None, limit=None):
|
||||
""" Gets either a list of devices or a single device details
|
||||
device_id is supplied if we are getting details of a single device
|
||||
marker and limit are used to paginate when device_id is not
|
||||
supplied. Currently this just supplies "LIMIT marker, limit" to
|
||||
MySQL which is fine.
|
||||
"""
|
||||
pass
|
||||
Gets either a list of all devices or a single device details.
|
||||
device_id is supplied if we are getting details of a single device
|
||||
marker and limit are used to paginate when device_id is not
|
||||
supplied. Currently this just supplies "LIMIT marker, limit" to
|
||||
MySQL which is fine.
|
||||
|
||||
@wsme_pecan.wsexpose(DeviceResp, body=DevicePost, status=202)
|
||||
:param device_id: id of device (unless getall)
|
||||
Url:
|
||||
GET /devices
|
||||
List all configured devices
|
||||
Url:
|
||||
GET /devices/{device_id}
|
||||
List details of a particular device
|
||||
Returns: dict
|
||||
"""
|
||||
|
||||
# if we don't have an id then we want a list of all devices
|
||||
if not device_id:
|
||||
# return all devices
|
||||
device = []
|
||||
|
||||
if marker is None:
|
||||
marker = 0
|
||||
if limit is None:
|
||||
limit = 100
|
||||
|
||||
devices = session.query(
|
||||
Device.id, Device.az, Device.updated, Device.created,
|
||||
Device.status, Device.publicIpAddr, Device.name,
|
||||
Device.type, Device.floatingIpAddr).offset(marker).limit(limit)
|
||||
|
||||
for item in devices:
|
||||
dev = item._asdict()
|
||||
dev['loadBalancers'] = []
|
||||
if dev['status'] != "OFFLINE":
|
||||
# Find loadbalancers using device
|
||||
lbids = session.query(
|
||||
loadbalancers_devices.c.loadbalancer).\
|
||||
filter(loadbalancers_devices.c.device == dev['id']).\
|
||||
all()
|
||||
|
||||
lblist = [i[0] for i in lbids]
|
||||
lbs = session.query(
|
||||
LoadBalancer.id, LoadBalancer.tenantid).\
|
||||
filter(LoadBalancer.id.in_(lblist)).all()
|
||||
|
||||
if lbs:
|
||||
for item in lbs:
|
||||
lb = item._asdict()
|
||||
dev['loadBalancers'].append(lb)
|
||||
|
||||
device.append(dev)
|
||||
|
||||
else:
|
||||
# return device detail
|
||||
device = session.query(
|
||||
Device.id, Device.az, Device.updated, Device.created,
|
||||
Device.status, Device.publicIpAddr, Device.name,
|
||||
Device.type, Device.floatingIpAddr
|
||||
).filter(Device.id == device_id).first()
|
||||
|
||||
if not device:
|
||||
response.status = 404
|
||||
session.rollback()
|
||||
return dict(
|
||||
status=404,
|
||||
message="device id " + device_id + "not found"
|
||||
)
|
||||
|
||||
device = device._asdict()
|
||||
device['loadBalancers'] = []
|
||||
|
||||
if device['status'] != "OFFLINE":
|
||||
lbids = session.query(
|
||||
loadbalancers_devices.c.loadbalancer).\
|
||||
filter(loadbalancers_devices.c.device == device['id']).\
|
||||
all()
|
||||
|
||||
lblist = [i[0] for i in lbids]
|
||||
lbs = session.query(
|
||||
LoadBalancer.id, LoadBalancer.tenantid).\
|
||||
filter(LoadBalancer.id.in_(lblist)).all()
|
||||
|
||||
if lbs:
|
||||
for item in lbs:
|
||||
lb = item._asdict()
|
||||
device['loadBalancers'].append(lb)
|
||||
|
||||
session.commit()
|
||||
response.status = 200
|
||||
return device
|
||||
|
||||
@wsme_pecan.wsexpose(DeviceResp, body=DevicePost, status_code=202)
|
||||
def post(self, body=None):
|
||||
""" Post a new device, DeviceResp and DevicePost not complete yet
|
||||
"""
|
||||
pass
|
||||
""" Creates a new device entry in devices table.
|
||||
:param None
|
||||
Url:
|
||||
POST /devices
|
||||
JSON Request Body
|
||||
{
|
||||
"name":"device name",
|
||||
"publicIpAddr":"15.x.x.x",
|
||||
"floatingIpAddr":"15.x.x.x",
|
||||
"az":2,
|
||||
"type":"type descr"
|
||||
}
|
||||
|
||||
@wsme_pecan.wsexpose(DeviceResp, body=DevicePost, status=202)
|
||||
def put(self):
|
||||
""" Updates a device only accepts status and statusDescription """
|
||||
pass
|
||||
Returns: dict
|
||||
{
|
||||
"status": "OFFLINE",
|
||||
"updated": "2013-06-06T10:17:19",
|
||||
"name": "device name",
|
||||
"created": "2013-06-06T10:17:19",
|
||||
"loadBalancers": [],
|
||||
"floatingIpAddr": "192.1678.98.99",
|
||||
"publicIpAddr": "192.1678.98.99",
|
||||
"az": 2,
|
||||
"type": "type descr",
|
||||
"id": 67
|
||||
}
|
||||
"""
|
||||
|
||||
# Get a new device object
|
||||
device = Device()
|
||||
device.name = body.name
|
||||
device.publicIpAddr = body.publicIpAddr
|
||||
device.floatingIpAddr = body.floatingIpAddr
|
||||
device.az = body.az
|
||||
device.type = body.type
|
||||
device.status = 'OFFLINE'
|
||||
device.created = None
|
||||
|
||||
# write to database
|
||||
session.add(device)
|
||||
session.flush()
|
||||
|
||||
#refresh the device record so we get the id back
|
||||
session.refresh(device)
|
||||
|
||||
try:
|
||||
return_data = DeviceResp()
|
||||
return_data.id = device.id
|
||||
return_data.name = device.name
|
||||
return_data.floatingIpAddr = device.floatingIpAddr
|
||||
return_data.publicIpAddr = device.publicIpAddr
|
||||
return_data.az = device.az
|
||||
return_data.type = device.type
|
||||
return_data.created = device.created
|
||||
return_data.updated = device.updated
|
||||
return_data.status = device.status
|
||||
return_data.loadBalancers = []
|
||||
session.commit()
|
||||
return return_data
|
||||
except:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception('Error communicating with load balancer pool')
|
||||
errstr = 'Error communicating with load balancer pool'
|
||||
session.rollback()
|
||||
raise ClientSideError(errstr)
|
||||
|
||||
@wsme_pecan.wsexpose(None, body=DevicePut, status_code=202)
|
||||
def put(self, body=None):
|
||||
""" Updates a device entry in devices table with new status.
|
||||
Also, updates status of loadbalancers using this device
|
||||
with ERROR or ACTIVE and the errmsg field
|
||||
:param - NOTE the _lookup() hack used to get the device id
|
||||
Url:
|
||||
PUT /devices/<device ID>
|
||||
JSON Request Body
|
||||
{
|
||||
"status": <ERROR | ONLINE>
|
||||
"statusDescription": "Error Description"
|
||||
}
|
||||
|
||||
Returns: None
|
||||
"""
|
||||
|
||||
if not self.devid:
|
||||
raise ClientSideError('Device ID is required')
|
||||
|
||||
device = session.query(Device).\
|
||||
filter(Device.id == self.devid).first()
|
||||
|
||||
if not device:
|
||||
raise ClientSideError('Device ID is not valid')
|
||||
|
||||
device.status = body.status
|
||||
session.flush()
|
||||
|
||||
lb_status = 'ACTIVE' if body.status == 'ONLINE' else body.status
|
||||
lb_descr = body.statusDescription
|
||||
|
||||
# Now find LB's associated with this Device and update their status
|
||||
lbs = session.query(
|
||||
loadbalancers_devices.c.loadbalancer).\
|
||||
filter(loadbalancers_devices.c.device == self.devid).\
|
||||
all()
|
||||
|
||||
for lb in lbs:
|
||||
session.query(LoadBalancer).\
|
||||
filter(LoadBalancer.id == lb[0]).\
|
||||
update({"status": lb_status, "errmsg": lb_descr},
|
||||
synchronize_session='fetch')
|
||||
|
||||
session.flush()
|
||||
|
||||
session.commit()
|
||||
return
|
||||
|
||||
@expose('json')
|
||||
def delete(self, device_id):
|
||||
""" Deletes a given device """
|
||||
pass
|
||||
""" Deletes a given device
|
||||
:param device_id: id of device to delete
|
||||
Urls:
|
||||
DELETE /devices/{device_id}
|
||||
Returns: None
|
||||
"""
|
||||
# check for the device
|
||||
device = session.query(Device.id).\
|
||||
filter(Device.id == device_id).first()
|
||||
|
||||
if device is None:
|
||||
response.status = 400
|
||||
return dict(
|
||||
faultcode="Client",
|
||||
faultstring="Device ID is not valid"
|
||||
)
|
||||
|
||||
# Is the device is attached to a LB
|
||||
lb = session.query(
|
||||
loadbalancers_devices.c.loadbalancer).\
|
||||
filter(loadbalancers_devices.c.device == device_id).\
|
||||
all()
|
||||
|
||||
if lb:
|
||||
response.status = 400
|
||||
return dict(
|
||||
faultcode="Client",
|
||||
faultstring="Device belongs to a loadbalancer"
|
||||
)
|
||||
try:
|
||||
session.query(Device).filter(Device.id == device_id).delete()
|
||||
session.flush()
|
||||
session.commit()
|
||||
response.status = 202
|
||||
return None
|
||||
except:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.exception('Error deleting device from pool')
|
||||
response.status = 500
|
||||
return dict(
|
||||
faultcode="Server",
|
||||
faultstring="Error deleting device from pool"
|
||||
)
|
||||
|
||||
@expose('json')
|
||||
def usage(self):
|
||||
""" Returns usage stats """
|
||||
pass
|
||||
def _lookup(self, devid, *remainder):
|
||||
"""Routes more complex url mapping for PUT
|
||||
Raises: 404
|
||||
"""
|
||||
# Kludgy fix for PUT since WSME doesn't like IDs on the path
|
||||
if devid:
|
||||
return DevicesController(devid), remainder
|
||||
abort(404)
|
||||
|
||||
49
libra/admin_api/controllers/usage.py
Normal file
49
libra/admin_api/controllers/usage.py
Normal file
@@ -0,0 +1,49 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 pecan import expose, response
|
||||
from libra.api.model.lbaas import Device, session
|
||||
from libra.admin_api.model.responses import Responses
|
||||
from pecan.rest import RestController
|
||||
|
||||
|
||||
class UsageController(RestController):
|
||||
|
||||
@expose('json')
|
||||
def get(self):
|
||||
"""Reports the device usage statistics for total, taken, and free
|
||||
:param None
|
||||
Url:
|
||||
GET /devices/usage
|
||||
Returns: dict
|
||||
"""
|
||||
total = session.query(Device).count()
|
||||
free = session.query(Device).filter(Device.status == 'OFFLINE').\
|
||||
count()
|
||||
session.commit()
|
||||
response.status = 200
|
||||
|
||||
return dict(
|
||||
total=total,
|
||||
free=free,
|
||||
taken=total - free
|
||||
)
|
||||
|
||||
@expose('json')
|
||||
def _default(self):
|
||||
"""default route.. acts as catch all for any wrong urls.
|
||||
For now it returns a 404 because no action is defined for /"""
|
||||
response.status = 404
|
||||
return Responses._default
|
||||
@@ -13,12 +13,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
#from pecan import expose, response
|
||||
from pecan import expose, response
|
||||
from devices import DevicesController
|
||||
#from libra.admin_api.model.responses import Responses
|
||||
from libra.admin_api.model.responses import Responses
|
||||
|
||||
|
||||
class V1Controller(object):
|
||||
"""v1 control object."""
|
||||
|
||||
@expose('json')
|
||||
def _default(self):
|
||||
"""default route.. acts as catch all for any wrong urls.
|
||||
For now it returns a 404 because no action is defined for /"""
|
||||
response.status = 404
|
||||
return Responses._default
|
||||
|
||||
devices = DevicesController()
|
||||
|
||||
117
libra/admin_api/model/lbaas.py
Normal file
117
libra/admin_api/model/lbaas.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 sqlalchemy import Table, Column, Integer, ForeignKey, create_engine
|
||||
from sqlalchemy import INTEGER, VARCHAR, BIGINT
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship, backref, sessionmaker
|
||||
import sqlalchemy.types as types
|
||||
from pecan import conf
|
||||
|
||||
# TODO replace this with something better
|
||||
conn_string = '''mysql://%s:%s@%s/%s''' % (
|
||||
conf.database.username,
|
||||
conf.database.password,
|
||||
conf.database.host,
|
||||
conf.database.schema
|
||||
)
|
||||
|
||||
engine = create_engine(conn_string, echo=True)
|
||||
DeclarativeBase = declarative_base()
|
||||
metadata = DeclarativeBase.metadata
|
||||
metadata.bind = engine
|
||||
|
||||
loadbalancers_devices = Table(
|
||||
'loadbalancers_devices',
|
||||
metadata,
|
||||
Column('loadbalancer', Integer, ForeignKey('loadbalancers.id')),
|
||||
Column('device', Integer, ForeignKey('devices.id'))
|
||||
)
|
||||
|
||||
|
||||
class FormatedDateTime(types.TypeDecorator):
|
||||
'''formats date to match iso 8601 standards
|
||||
'''
|
||||
|
||||
impl = types.DateTime
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
return value.strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
class Limits(DeclarativeBase):
|
||||
__tablename__ = 'global_limits'
|
||||
id = Column(u'id', Integer, primary_key=True, nullable=False)
|
||||
name = Column(u'name', VARCHAR(length=128), nullable=False)
|
||||
value = Column(u'value', BIGINT(), nullable=False)
|
||||
|
||||
|
||||
class Device(DeclarativeBase):
|
||||
"""device model"""
|
||||
__tablename__ = 'devices'
|
||||
#column definitions
|
||||
az = Column(u'az', INTEGER(), nullable=False)
|
||||
created = Column(u'created', FormatedDateTime(), nullable=False)
|
||||
floatingIpAddr = Column(
|
||||
u'floatingIpAddr', VARCHAR(length=128), nullable=False
|
||||
)
|
||||
id = Column(u'id', BIGINT(), primary_key=True, nullable=False)
|
||||
name = Column(u'name', VARCHAR(length=128), nullable=False)
|
||||
publicIpAddr = Column(u'publicIpAddr', VARCHAR(length=128), nullable=False)
|
||||
status = Column(u'status', VARCHAR(length=128), nullable=False)
|
||||
type = Column(u'type', VARCHAR(length=128), nullable=False)
|
||||
updated = Column(u'updated', FormatedDateTime(), nullable=False)
|
||||
|
||||
|
||||
class LoadBalancer(DeclarativeBase):
|
||||
"""load balancer model"""
|
||||
__tablename__ = 'loadbalancers'
|
||||
#column definitions
|
||||
algorithm = Column(u'algorithm', VARCHAR(length=80), nullable=False)
|
||||
errmsg = Column(u'errmsg', VARCHAR(length=128))
|
||||
id = Column(u'id', BIGINT(), primary_key=True, nullable=False)
|
||||
name = Column(u'name', VARCHAR(length=128), nullable=False)
|
||||
port = Column(u'port', INTEGER(), nullable=False)
|
||||
protocol = Column(u'protocol', VARCHAR(length=128), nullable=False)
|
||||
status = Column(u'status', VARCHAR(length=50), nullable=False)
|
||||
tenantid = Column(u'tenantid', VARCHAR(length=128), nullable=False)
|
||||
updated = Column(u'updated', FormatedDateTime(), nullable=False)
|
||||
created = Column(u'created', FormatedDateTime(), nullable=False)
|
||||
|
||||
nodes = relationship(
|
||||
'Node', backref=backref('loadbalancers', order_by='Node.id')
|
||||
)
|
||||
devices = relationship(
|
||||
'Device', secondary=loadbalancers_devices, backref='loadbalancers',
|
||||
lazy='joined'
|
||||
)
|
||||
|
||||
|
||||
class Node(DeclarativeBase):
|
||||
"""node model"""
|
||||
__tablename__ = 'nodes'
|
||||
#column definitions
|
||||
address = Column(u'address', VARCHAR(length=128), nullable=False)
|
||||
enabled = Column(u'enabled', Integer(), nullable=False)
|
||||
id = Column(u'id', BIGINT(), primary_key=True, nullable=False)
|
||||
lbid = Column(
|
||||
u'lbid', BIGINT(), ForeignKey('loadbalancers.id'), nullable=False
|
||||
)
|
||||
port = Column(u'port', INTEGER(), nullable=False)
|
||||
status = Column(u'status', VARCHAR(length=128), nullable=False)
|
||||
weight = Column(u'weight', INTEGER(), nullable=False)
|
||||
|
||||
|
||||
"""session"""
|
||||
session = sessionmaker(bind=engine)()
|
||||
29
libra/admin_api/model/responses.py
Normal file
29
libra/admin_api/model/responses.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 Responses
|
||||
responder objects for framework.
|
||||
"""
|
||||
|
||||
|
||||
class Responses(object):
|
||||
"""404 - not found"""
|
||||
_default = {'status': '404', 'message': 'Object not Found'}
|
||||
|
||||
"""not found """
|
||||
not_found = {'message': 'Object not Found'}
|
||||
|
||||
"""service_unavailable"""
|
||||
service_unavailable = {'message': 'Service Unavailable'}
|
||||
@@ -13,14 +13,37 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
#from wsme import types as wtypes
|
||||
#from wsme import wsattr
|
||||
from wsme.types import Base
|
||||
from wsme import types as wtypes
|
||||
from wsme import wsattr
|
||||
from wsme.types import Base, Enum
|
||||
|
||||
|
||||
class LB(Base):
|
||||
id = wsattr(int, mandatory=True)
|
||||
tenantid = wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
|
||||
class DevicePost(Base):
|
||||
pass
|
||||
name = wsattr(wtypes.text, mandatory=True)
|
||||
publicIpAddr = wsattr(wtypes.text, mandatory=True)
|
||||
floatingIpAddr = wsattr(wtypes.text, mandatory=True)
|
||||
az = wsattr(int, mandatory=True)
|
||||
type = wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
|
||||
class DeviceResp(Base):
|
||||
pass
|
||||
id = int
|
||||
name = wtypes.text
|
||||
floatingIpAddr = wtypes.text
|
||||
publicIpAddr = wtypes.text
|
||||
az = int
|
||||
type = wtypes.text
|
||||
created = wtypes.text
|
||||
updated = wtypes.text
|
||||
status = wtypes.text
|
||||
loadBalancers = wsattr(['LB'])
|
||||
|
||||
|
||||
class DevicePut(Base):
|
||||
status = Enum(wtypes.text, 'ONLINE', 'ERROR')
|
||||
statusDescription = wsattr(wtypes.text, mandatory=True)
|
||||
|
||||
Reference in New Issue
Block a user