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:
marcrp
2013-06-06 12:54:21 -04:00
parent 89b2cf9a29
commit a8a96fc539
6 changed files with 502 additions and 29 deletions

View File

@@ -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)

View 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

View File

@@ -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()

View 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)()

View 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'}

View File

@@ -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)