From 2c77d82204d4708f5993cb7f1181aea52037ad0c Mon Sep 17 00:00:00 2001 From: Jim Rollenhagen Date: Mon, 17 Mar 2014 10:58:39 -0700 Subject: [PATCH] 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. --- Dockerfile | 2 +- requirements.txt | 2 +- teeth_agent/agent.py | 7 ++-- teeth_agent/base.py | 11 +++--- teeth_agent/encoding.py | 53 ++++++++++++++++++++++++++ teeth_agent/errors.py | 62 +++++++++++++++++++++++++------ teeth_agent/hardware.py | 4 +- teeth_agent/logging.py | 2 +- teeth_agent/overlord_agent_api.py | 5 +-- teeth_agent/tests/agent.py | 6 +-- teeth_agent/tests/api.py | 8 ++-- 11 files changed, 124 insertions(+), 38 deletions(-) create mode 100644 teeth_agent/encoding.py diff --git a/Dockerfile b/Dockerfile index 82836887a..7fa950b14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] diff --git a/requirements.txt b/requirements.txt index 54d08099f..7df51dbf8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index 62292fdde..fbc263951 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -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 diff --git a/teeth_agent/base.py b/teeth_agent/base.py index 3c55ea026..8147c43d9 100644 --- a/teeth_agent/base.py +++ b/teeth_agent/base.py @@ -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: diff --git a/teeth_agent/encoding.py b/teeth_agent/encoding.py new file mode 100644 index 000000000..a6386579b --- /dev/null +++ b/teeth_agent/encoding.py @@ -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) diff --git a/teeth_agent/errors.py b/teeth_agent/errors.py index 0e1abf1dd..2b32a9033 100644 --- a/teeth_agent/errors.py +++ b/teeth_agent/errors.py @@ -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.' diff --git a/teeth_agent/hardware.py b/teeth_agent/hardware.py index bc79db391..a8e4e0fd9 100644 --- a/teeth_agent/hardware.py +++ b/teeth_agent/hardware.py @@ -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), diff --git a/teeth_agent/logging.py b/teeth_agent/logging.py index 1429f9932..5c66bc900 100644 --- a/teeth_agent/logging.py +++ b/teeth_agent/logging.py @@ -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() diff --git a/teeth_agent/overlord_agent_api.py b/teeth_agent/overlord_agent_api.py index 7fa41ade5..969d375cb 100644 --- a/teeth_agent/overlord_agent_api.py +++ b/teeth_agent/overlord_agent_api.py @@ -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) diff --git a/teeth_agent/tests/agent.py b/teeth_agent/tests/agent.py index 33b011267..83313606f 100644 --- a/teeth_agent/tests/agent.py +++ b/teeth_agent/tests/agent.py @@ -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') diff --git a/teeth_agent/tests/api.py b/teeth_agent/tests/api.py index 655bbae1c..bbcf47e29 100644 --- a/teeth_agent/tests/api.py +++ b/teeth_agent/tests/api.py @@ -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