Remove dependency on teeth-rest.

This commit:
- Removes all references to teeth-rest.
- Brings in encoding.py and errors.py from teeth-rest.
- Removes the "view" thing from the encoding module.
- Adds structlog as a dep. This was missing and overlooked
  because teeth-rest was installing it in the environment.
This commit is contained in:
Jim Rollenhagen 2014-03-17 10:58:39 -07:00
parent 7ecb8978d0
commit 2c77d82204
11 changed files with 124 additions and 38 deletions

View File

@ -13,7 +13,7 @@ RUN apt-get update && apt-get -y install \
# Install requirements separately, because pip understands a git+https url while setuptools doesn't
RUN pip install -r /tmp/teeth-agent/requirements.txt
# This will succeed because all the dependencies (including pesky teeth_rest) were installed previously
# This will succeed because all the dependencies were installed previously
RUN pip install /tmp/teeth-agent
CMD [ "/usr/local/bin/teeth-agent" ]

View File

@ -1,8 +1,8 @@
Werkzeug==0.9.4
requests==2.0.0
stevedore==0.14
-e git+https://github.com/racker/teeth-rest.git@e876c0fddd5ce2f5223ab16936f711b0d57e19c4#egg=teeth_rest
wsgiref>=0.1.2
pecan>=0.4.5
WSME>=0.6
six>=1.5.2
structlog==0.4.1

View File

@ -22,12 +22,11 @@ import time
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.api import app
from teeth_agent import base
from teeth_agent import encoding
from teeth_agent import errors
from teeth_agent import hardware
from teeth_agent import overlord_agent_api
@ -39,7 +38,7 @@ class TeethAgentStatus(encoding.Serializable):
self.started_at = started_at
self.version = version
def serialize(self, view):
def serialize(self):
"""Turn the status into a dict."""
return collections.OrderedDict([
('mode', self.mode),
@ -181,7 +180,7 @@ class TeethAgent(object):
try:
result = self.mode_implementation.execute(command_part,
**kwargs)
except rest_errors.InvalidContentError as e:
except errors.InvalidContentError as e:
# Any command may raise a InvalidContentError which will be
# returned to the caller directly.
raise e

View File

@ -18,9 +18,8 @@ import threading
import uuid
import structlog
from teeth_rest import encoding
from teeth_rest import errors as rest_errors
from teeth_agent import encoding
from teeth_agent import errors
@ -39,7 +38,7 @@ class BaseCommandResult(encoding.Serializable):
self.command_error = None
self.command_result = None
def serialize(self, view):
def serialize(self):
return dict((
(u'id', self.id),
(u'command_name', self.command_name),
@ -82,9 +81,9 @@ class AsyncCommandResult(BaseCommandResult):
self.execution_thread = threading.Thread(target=self.run,
name=thread_name)
def serialize(self, view):
def serialize(self):
with self.command_state_lock:
return super(AsyncCommandResult, self).serialize(view)
return super(AsyncCommandResult, self).serialize()
def start(self):
self.execution_thread.start()
@ -107,7 +106,7 @@ class AsyncCommandResult(BaseCommandResult):
self.command_status = AgentCommandStatus.SUCCEEDED
except Exception as e:
if not isinstance(e, rest_errors.RESTError):
if not isinstance(e, errors.RESTError):
e = errors.CommandExecutionError(str(e))
with self.command_state_lock:

53
teeth_agent/encoding.py Normal file
View File

@ -0,0 +1,53 @@
"""
Copyright 2013 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.
"""
import json
import uuid
class Serializable(object):
"""Base class for things that can be serialized."""
def serialize(self):
"""Turn this object into a dict."""
raise NotImplementedError()
class RESTJSONEncoder(json.JSONEncoder):
"""A slightly customized JSON encoder."""
def encode(self, o):
"""Turn an object into JSON.
Appends a newline to responses when configured to pretty-print,
in order to make use of curl less painful from most shells.
"""
delimiter = ''
# if indent is None, newlines are still inserted, so we should too.
if self.indent is not None:
delimiter = '\n'
return super(RESTJSONEncoder, self).encode(o) + delimiter
def default(self, o):
"""Turn an object into a serializable object. In particular, by
calling :meth:`.Serializable.serialize`.
"""
if isinstance(o, Serializable):
return o.serialize()
elif isinstance(o, uuid.UUID):
return str(o)
else:
return json.JSONEncoder.default(self, o)

View File

@ -14,10 +14,50 @@ See the License for the specific language governing permissions and
limitations under the License.
"""
from teeth_rest import errors
import collections
from teeth_agent import encoding
class CommandExecutionError(errors.RESTError):
class RESTError(Exception, encoding.Serializable):
"""Base class for errors generated in teeth."""
message = 'An error occurred'
details = 'An unexpected error occurred. Please try back later.'
status_code = 500
def serialize(self):
"""Turn a RESTError into a dict."""
return collections.OrderedDict([
('type', self.__class__.__name__),
('code', self.status_code),
('message', self.message),
('details', self.details),
])
class InvalidContentError(RESTError):
"""Error which occurs when a user supplies invalid content, either
because that content cannot be parsed according to the advertised
`Content-Type`, or due to a content validation error.
"""
message = 'Invalid request body'
status_code = 400
def __init__(self, details):
self.details = details
class NotFound(RESTError):
"""Error which occurs when a user supplies invalid content, either
because that content cannot be parsed according to the advertised
`Content-Type`, or due to a content validation error.
"""
message = 'Not found'
status_code = 404
details = 'The requested URL was not found.'
class CommandExecutionError(RESTError):
"""Error raised when a command fails to execute."""
message = 'Command execution failed'
@ -27,7 +67,7 @@ class CommandExecutionError(errors.RESTError):
self.details = details
class InvalidCommandError(errors.InvalidContentError):
class InvalidCommandError(InvalidContentError):
"""Error which is raised when an unknown command is issued."""
messsage = 'Invalid command'
@ -36,7 +76,7 @@ class InvalidCommandError(errors.InvalidContentError):
super(InvalidCommandError, self).__init__(details)
class InvalidCommandParamsError(errors.InvalidContentError):
class InvalidCommandParamsError(InvalidContentError):
"""Error which is raised when command parameters are invalid."""
message = 'Invalid command parameters'
@ -45,14 +85,14 @@ class InvalidCommandParamsError(errors.InvalidContentError):
super(InvalidCommandParamsError, self).__init__(details)
class RequestedObjectNotFoundError(errors.NotFound):
class RequestedObjectNotFoundError(NotFound):
def __init__(self, type_descr, obj_id):
details = '{} with id {} not found.'.format(type_descr, obj_id)
super(RequestedObjectNotFoundError, self).__init__(details)
self.details = details
class OverlordAPIError(errors.RESTError):
class OverlordAPIError(RESTError):
"""Error raised when a call to the agent API fails."""
message = 'Error in call to teeth-agent-api.'
@ -71,7 +111,7 @@ class HeartbeatError(OverlordAPIError):
super(HeartbeatError, self).__init__(details)
class ImageDownloadError(errors.RESTError):
class ImageDownloadError(RESTError):
"""Error raised when an image cannot be downloaded."""
message = 'Error downloading image.'
@ -81,7 +121,7 @@ class ImageDownloadError(errors.RESTError):
self.details = 'Could not download image with id {}.'.format(image_id)
class ImageChecksumError(errors.RESTError):
class ImageChecksumError(RESTError):
"""Error raised when an image fails to verify against its checksum."""
message = 'Error verifying image checksum.'
@ -92,7 +132,7 @@ class ImageChecksumError(errors.RESTError):
self.details = self.details.format(image_id)
class ImageWriteError(errors.RESTError):
class ImageWriteError(RESTError):
"""Error raised when an image cannot be written to a device."""
message = 'Error writing image to device.'
@ -103,7 +143,7 @@ class ImageWriteError(errors.RESTError):
self.details = self.details.format(device, exit_code)
class ConfigDriveWriteError(errors.RESTError):
class ConfigDriveWriteError(RESTError):
"""Error raised when a configdrive directory cannot be written to a
device.
"""
@ -117,7 +157,7 @@ class ConfigDriveWriteError(errors.RESTError):
self.details = details
class SystemRebootError(errors.RESTError):
class SystemRebootError(RESTError):
"""Error raised when a system cannot reboot."""
message = 'Error rebooting system.'

View File

@ -22,7 +22,7 @@ import subprocess
import stevedore
import structlog
from teeth_rest import encoding
from teeth_agent import encoding
_global_manager = None
@ -49,7 +49,7 @@ class HardwareInfo(encoding.Serializable):
self.type = type
self.id = id
def serialize(self, view):
def serialize(self):
return collections.OrderedDict([
('type', self.type),
('id', self.id),

View File

@ -38,7 +38,7 @@ def _format_event(logger, method, event):
have enough keys to format.
"""
if 'event' not in event:
# nothing to format, e.g. _log_request in teeth_rest/component
# nothing to format
return event
# Get a list of fields that need to be filled.
formatter = string.Formatter()

View File

@ -17,8 +17,8 @@ limitations under the License.
import json
import requests
from teeth_rest import encoding
from teeth_agent import encoding
from teeth_agent import errors
@ -28,8 +28,7 @@ class APIClient(object):
def __init__(self, api_url):
self.api_url = api_url.rstrip('/')
self.session = requests.Session()
self.encoder = encoding.RESTJSONEncoder(
encoding.SerializationViews.PUBLIC)
self.encoder = encoding.RESTJSONEncoder()
def _request(self, method, path, data=None):
request_url = '{api_url}{path}'.format(api_url=self.api_url, path=path)

View File

@ -22,10 +22,10 @@ import mock
import pkg_resources
from wsgiref import simple_server
from teeth_rest import encoding
from teeth_agent import agent
from teeth_agent import base
from teeth_agent import encoding
from teeth_agent import errors
from teeth_agent import hardware
@ -118,9 +118,7 @@ class TestHeartbeater(unittest.TestCase):
class TestBaseAgent(unittest.TestCase):
def setUp(self):
self.encoder = encoding.RESTJSONEncoder(
encoding.SerializationViews.PUBLIC,
indent=4)
self.encoder = encoding.RESTJSONEncoder(indent=4)
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
('localhost', 9999),
'192.168.1.1')

View File

@ -22,7 +22,6 @@ import unittest
from werkzeug import test
from werkzeug import wrappers
from teeth_rest import encoding
from teeth_agent import agent
from teeth_agent.api import app
@ -99,7 +98,7 @@ class TestTeethAPI(unittest.TestCase):
self.assertEqual(kwargs, {'key': 'value'})
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
expected_result = result.serialize(encoding.SerializationViews.PUBLIC)
expected_result = result.serialize()
self.assertEqual(data, expected_result)
def test_execute_agent_command_validation(self):
@ -150,7 +149,7 @@ class TestTeethAPI(unittest.TestCase):
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.data), {
u'commands': [
cmd_result.serialize(None),
cmd_result.serialize(),
],
})
@ -160,8 +159,7 @@ class TestTeethAPI(unittest.TestCase):
True,
{'test': 'result'})
serialized_cmd_result = cmd_result.serialize(
encoding.SerializationViews.PUBLIC)
serialized_cmd_result = cmd_result.serialize()
mock_agent = mock.create_autospec(agent.TeethAgent)
mock_agent.get_command_result.return_value = cmd_result