get things kind of working with pecan

This commit is contained in:
Jim Rollenhagen 2014-03-07 15:36:22 -08:00
parent 1eb060db61
commit 18228e9093
12 changed files with 481 additions and 11 deletions

@ -1,5 +1,8 @@
Werkzeug==0.9.4
requests==2.0.0
cherrypy==3.2.4
stevedore==0.14
-e git+https://github.com/racker/teeth-rest.git@e876c0fddd5ce2f5223ab16936f711b0d57e19c4#egg=teeth_rest
wsgiref>=0.1.2
pecan>=0.4.5
oslo.config>=1.2.0
WSME>=0.6

@ -19,14 +19,14 @@ import random
import threading
import time
from cherrypy import wsgiserver
import pkg_resources
from stevedore import driver
import structlog
from teeth_rest import encoding
from teeth_rest import errors as rest_errors
from wsgiref import simple_server
from teeth_agent import api
from teeth_agent.api import app
from teeth_agent import base
from teeth_agent import errors
from teeth_agent import hardware
@ -112,7 +112,7 @@ class TeethAgent(object):
self.listen_address = listen_address
self.mode_implementation = None
self.version = pkg_resources.get_distribution('teeth-agent').version
self.api = api.TeethAgentAPIServer(self)
self.api = app.VersionSelectorApplication()
self.command_results = collections.OrderedDict()
self.heartbeater = TeethAgentHeartbeater(self)
self.hardware = hardware.get_manager()
@ -196,13 +196,16 @@ class TeethAgent(object):
"""Run the Teeth Agent."""
self.started_at = time.time()
self.heartbeater.start()
server = wsgiserver.CherryPyWSGIServer(self.listen_address, self.api)
wsgi = simple_server.make_server(
self.listen_address[0],
self.listen_address[1],
self.api,
server_class=simple_server.WSGIServer)
try:
server.start()
wsgi.serve_forever()
except BaseException as e:
self.log.error('shutting down', exception=e)
server.stop()
self.heartbeater.stop()

@ -0,0 +1,15 @@
"""
Copyright 2014 Rackspace, Inc.
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.
"""

66
teeth_agent/api/app.py Normal file

@ -0,0 +1,66 @@
"""
Copyright 2014 Rackspace, Inc.
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 oslo.config import cfg
import pecan
from teeth_agent.api import config
CONF = cfg.CONF
def get_pecan_config():
# Set up the pecan configuration
filename = config.__file__.replace('.pyc', '.py')
return pecan.configuration.conf_from_file(filename)
def setup_app(pecan_config=None, extra_hooks=None):
#policy.init()
#app_hooks = [hooks.ConfigHook(),
#hooks.DBHook(),
#hooks.ContextHook(pecan_config.app.acl_public_routes),
#hooks.RPCHook(),
#hooks.NoExceptionTracebackHook()]
#if extra_hooks:
#app_hooks.extend(extra_hooks)
if not pecan_config:
pecan_config = get_pecan_config()
pecan.configuration.set_config(dict(pecan_config), overwrite=True)
app = pecan.make_app(
pecan_config.app.root,
static_root=pecan_config.app.static_root,
debug=True,
#debug=CONF.debug,
force_canonical=getattr(pecan_config.app, 'force_canonical', True),
#hooks=app_hooks,
#wrap_app=middleware.ParsableErrorMiddleware,
)
return app
class VersionSelectorApplication(object):
def __init__(self):
pc = get_pecan_config()
self.v1 = setup_app(pecan_config=pc)
def __call__(self, environ, start_response):
return self.v1(environ, start_response)

0
teeth_agent/api/app.wsgi Normal file

39
teeth_agent/api/config.py Normal file

@ -0,0 +1,39 @@
"""
Copyright 2014 Rackspace, Inc.
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.
"""
# Server Specific Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
server = {
'port': '9999',
'host': '0.0.0.0'
}
# Pecan Application Configurations
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
app = {
'root': 'teeth_agent.api.controllers.root.RootController',
'modules': ['teeth_agent.api'],
'static_root': '%(confdir)s/public',
'debug': False,
'enable_acl': True,
'acl_public_routes': ['/', '/v1'],
}
# WSME Configurations
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
wsme = {
'debug': False,
}

@ -0,0 +1,15 @@
"""
Copyright 2014 Rackspace, Inc.
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.
"""

@ -0,0 +1,100 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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 pecan
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from teeth_agent.api.controllers import v1
from teeth_agent.api.controllers.v1 import base
from teeth_agent.api.controllers.v1 import link
class Version(base.APIBase):
"""An API version representation."""
id = wtypes.text
"The ID of the version, also acts as the release number"
links = [link.Link]
"A Link that point to a specific version of the API"
@classmethod
def convert(self, id):
version = Version()
version.id = id
version.links = [link.Link.make_link('self', pecan.request.host_url,
id, '', bookmark=True)]
return version
class Root(base.APIBase):
name = wtypes.text
"The name of the API"
description = wtypes.text
"Some information about this API"
versions = [Version]
"Links to all the versions available in this API"
default_version = Version
"A link to the default version of the API"
@classmethod
def convert(self):
root = Root()
root.name = "OpenStack Ironic API"
root.description = ("Ironic is an OpenStack project which aims to "
"provision baremetal machines.")
root.versions = [Version.convert('v1')]
root.default_version = Version.convert('v1')
return root
class RootController(rest.RestController):
_versions = ['v1']
"All supported API versions"
_default_version = 'v1'
"The default API version"
v1 = v1.Controller()
@wsme_pecan.wsexpose(Root)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return Root.convert()
@pecan.expose()
def _route(self, args):
"""Overrides the default routing behavior.
It redirects the request to the default version of the ironic API
if the version number is not specified in the url.
"""
if args[0] and args[0] not in self._versions:
args = [self._default_version] + args
return super(RootController, self)._route(args)

@ -0,0 +1,135 @@
# 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.
"""
Version 1 of the Ironic API
NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED.
Should maintain feature parity with Nova Baremetal Extension.
Specification can be found at ironic/doc/api/v1.rst
"""
import pecan
from pecan import rest
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from teeth_agent.api.controllers.v1 import base
#from ironic.api.controllers.v1 import chassis
#from ironic.api.controllers.v1 import driver
from teeth_agent.api.controllers.v1 import link
#from ironic.api.controllers.v1 import node
#from ironic.api.controllers.v1 import port
class MediaType(base.APIBase):
"""A media type representation."""
base = wtypes.text
type = wtypes.text
def __init__(self, base, type):
self.base = base
self.type = type
class V1(base.APIBase):
"""The representation of the version 1 of the API."""
id = wtypes.text
"The ID of the version, also acts as the release number"
media_types = [MediaType]
"An array of supported media types for this version"
links = [link.Link]
"Links that point to a specific URL for this version and documentation"
#chassis = [link.Link]
#"Links to the chassis resource"
#nodes = [link.Link]
#"Links to the nodes resource"
#ports = [link.Link]
#"Links to the ports resource"
#drivers = [link.Link]
#"Links to the drivers resource"
@classmethod
def convert(self):
v1 = V1()
v1.id = "v1"
v1.links = [
link.Link.make_link('self', pecan.request.host_url,
'v1', '', bookmark=True),
link.Link.make_link('describedby',
'http://docs.openstack.org',
'developer/ironic/dev',
'api-spec-v1.html',
bookmark=True, type='text/html')
]
v1.media_types = [MediaType('application/json',
'application/vnd.openstack.ironic.v1+json')]
#v1.chassis = [link.Link.make_link('self', pecan.request.host_url,
#'chassis', ''),
#link.Link.make_link('bookmark',
#pecan.request.host_url,
#'chassis', '',
#bookmark=True)
#]
#v1.nodes = [link.Link.make_link('self', pecan.request.host_url,
#'nodes', ''),
#link.Link.make_link('bookmark',
#pecan.request.host_url,
#'nodes', '',
#bookmark=True)
#]
#v1.ports = [link.Link.make_link('self', pecan.request.host_url,
#'ports', ''),
#link.Link.make_link('bookmark',
#pecan.request.host_url,
#'ports', '',
#bookmark=True)
#]
#v1.drivers = [link.Link.make_link('self', pecan.request.host_url,
#'drivers', ''),
#link.Link.make_link('bookmark',
#pecan.request.host_url,
#'drivers', '',
#bookmark=True)
#]
return v1
class Controller(rest.RestController):
"""Version 1 API controller root."""
#nodes = node.NodesController()
#ports = port.PortsController()
#chassis = chassis.ChassisController()
#drivers = driver.DriversController()
@wsme_pecan.wsexpose(V1)
def get(self):
# NOTE: The reason why convert() it's being called for every
# request is because we need to get the host url from
# the request object to make the links.
return V1.convert()
__all__ = (Controller)

@ -0,0 +1,47 @@
# 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 datetime
import wsme
from wsme import types as wtypes
class APIBase(wtypes.Base):
created_at = wsme.wsattr(datetime.datetime, readonly=True)
"The time in UTC at which the object is created"
updated_at = wsme.wsattr(datetime.datetime, readonly=True)
"The time in UTC at which the object is updated"
def as_dict(self):
"""Render this object as a dict of its fields."""
return dict((k, getattr(self, k))
for k in self.fields
if hasattr(self, k) and
getattr(self, k) != wsme.Unset)
def unset_fields_except(self, except_list=None):
"""Unset fields so they don't appear in the message body.
:param except_list: A list of fields that won't be touched.
"""
if except_list is None:
except_list = []
for k in self.as_dict():
if k not in except_list:
setattr(self, k, wsme.Unset)

@ -0,0 +1,43 @@
# Copyright 2013 Red Hat, 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.
from wsme import types as wtypes
from teeth_agent.api.controllers.v1 import base
class Link(base.APIBase):
"""A link representation."""
href = wtypes.text
"The url of a link."
rel = wtypes.text
"The name of a link."
type = wtypes.text
"Indicates the type of document/link."
@classmethod
def make_link(cls, rel_name, url, resource, resource_args,
bookmark=False, type=wtypes.Unset):
template = '%s/%s' if bookmark else '%s/v1/%s'
# FIXME(lucasagomes): I'm getting a 404 when doing a GET on
# a nested resource that the URL ends with a '/'.
# https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
template += '%s' if resource_args.startswith('?') else '/%s'
return Link(href=(template) % (url, resource, resource_args),
rel=rel_name, type=type)

@ -20,6 +20,7 @@ import unittest
import mock
import pkg_resources
from wsgiref import simple_server
from teeth_rest import encoding
@ -156,7 +157,7 @@ class TestBaseAgent(unittest.TestCase):
'do_something',
foo='bar')
@mock.patch('cherrypy.wsgiserver.CherryPyWSGIServer', autospec=True)
@mock.patch('wsgiref.simple_server.make_server', autospec=True)
def test_run(self, wsgi_server_cls):
wsgi_server = wsgi_server_cls.return_value
wsgi_server.start.side_effect = KeyboardInterrupt()
@ -165,9 +166,12 @@ class TestBaseAgent(unittest.TestCase):
self.agent.run()
listen_addr = ('localhost', 9999)
wsgi_server_cls.assert_called_once_with(listen_addr, self.agent.api)
wsgi_server.start.assert_called_once_with()
wsgi_server.stop.assert_called_once_with()
wsgi_server_cls.assert_called_once_with(
listen_addr[0],
listen_addr[1],
self.agent.api,
server_class=simple_server.WSGIServer)
wsgi_server.serve_forever.assert_called_once_with()
self.agent.heartbeater.start.assert_called_once_with()