get things kind of working with pecan
This commit is contained in:
parent
1eb060db61
commit
18228e9093
requirements.txt
teeth_agent
@ -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()
|
||||
|
||||
|
15
teeth_agent/api/__init__.py
Normal file
15
teeth_agent/api/__init__.py
Normal file
@ -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
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
0
teeth_agent/api/app.wsgi
Normal file
39
teeth_agent/api/config.py
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,
|
||||
}
|
15
teeth_agent/api/controllers/__init__.py
Normal file
15
teeth_agent/api/controllers/__init__.py
Normal file
@ -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.
|
||||
"""
|
100
teeth_agent/api/controllers/root.py
Normal file
100
teeth_agent/api/controllers/root.py
Normal file
@ -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)
|
135
teeth_agent/api/controllers/v1/__init__.py
Normal file
135
teeth_agent/api/controllers/v1/__init__.py
Normal file
@ -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)
|
47
teeth_agent/api/controllers/v1/base.py
Normal file
47
teeth_agent/api/controllers/v1/base.py
Normal file
@ -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)
|
43
teeth_agent/api/controllers/v1/link.py
Normal file
43
teeth_agent/api/controllers/v1/link.py
Normal file
@ -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()
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user