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:
parent
7ecb8978d0
commit
2c77d82204
@ -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
|
# Install requirements separately, because pip understands a git+https url while setuptools doesn't
|
||||||
RUN pip install -r /tmp/teeth-agent/requirements.txt
|
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
|
RUN pip install /tmp/teeth-agent
|
||||||
|
|
||||||
CMD [ "/usr/local/bin/teeth-agent" ]
|
CMD [ "/usr/local/bin/teeth-agent" ]
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
Werkzeug==0.9.4
|
Werkzeug==0.9.4
|
||||||
requests==2.0.0
|
requests==2.0.0
|
||||||
stevedore==0.14
|
stevedore==0.14
|
||||||
-e git+https://github.com/racker/teeth-rest.git@e876c0fddd5ce2f5223ab16936f711b0d57e19c4#egg=teeth_rest
|
|
||||||
wsgiref>=0.1.2
|
wsgiref>=0.1.2
|
||||||
pecan>=0.4.5
|
pecan>=0.4.5
|
||||||
WSME>=0.6
|
WSME>=0.6
|
||||||
six>=1.5.2
|
six>=1.5.2
|
||||||
|
structlog==0.4.1
|
||||||
|
@ -22,12 +22,11 @@ import time
|
|||||||
import pkg_resources
|
import pkg_resources
|
||||||
from stevedore import driver
|
from stevedore import driver
|
||||||
import structlog
|
import structlog
|
||||||
from teeth_rest import encoding
|
|
||||||
from teeth_rest import errors as rest_errors
|
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
from teeth_agent.api import app
|
from teeth_agent.api import app
|
||||||
from teeth_agent import base
|
from teeth_agent import base
|
||||||
|
from teeth_agent import encoding
|
||||||
from teeth_agent import errors
|
from teeth_agent import errors
|
||||||
from teeth_agent import hardware
|
from teeth_agent import hardware
|
||||||
from teeth_agent import overlord_agent_api
|
from teeth_agent import overlord_agent_api
|
||||||
@ -39,7 +38,7 @@ class TeethAgentStatus(encoding.Serializable):
|
|||||||
self.started_at = started_at
|
self.started_at = started_at
|
||||||
self.version = version
|
self.version = version
|
||||||
|
|
||||||
def serialize(self, view):
|
def serialize(self):
|
||||||
"""Turn the status into a dict."""
|
"""Turn the status into a dict."""
|
||||||
return collections.OrderedDict([
|
return collections.OrderedDict([
|
||||||
('mode', self.mode),
|
('mode', self.mode),
|
||||||
@ -181,7 +180,7 @@ class TeethAgent(object):
|
|||||||
try:
|
try:
|
||||||
result = self.mode_implementation.execute(command_part,
|
result = self.mode_implementation.execute(command_part,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
except rest_errors.InvalidContentError as e:
|
except errors.InvalidContentError as e:
|
||||||
# Any command may raise a InvalidContentError which will be
|
# Any command may raise a InvalidContentError which will be
|
||||||
# returned to the caller directly.
|
# returned to the caller directly.
|
||||||
raise e
|
raise e
|
||||||
|
@ -18,9 +18,8 @@ import threading
|
|||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import structlog
|
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
|
from teeth_agent import errors
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ class BaseCommandResult(encoding.Serializable):
|
|||||||
self.command_error = None
|
self.command_error = None
|
||||||
self.command_result = None
|
self.command_result = None
|
||||||
|
|
||||||
def serialize(self, view):
|
def serialize(self):
|
||||||
return dict((
|
return dict((
|
||||||
(u'id', self.id),
|
(u'id', self.id),
|
||||||
(u'command_name', self.command_name),
|
(u'command_name', self.command_name),
|
||||||
@ -82,9 +81,9 @@ class AsyncCommandResult(BaseCommandResult):
|
|||||||
self.execution_thread = threading.Thread(target=self.run,
|
self.execution_thread = threading.Thread(target=self.run,
|
||||||
name=thread_name)
|
name=thread_name)
|
||||||
|
|
||||||
def serialize(self, view):
|
def serialize(self):
|
||||||
with self.command_state_lock:
|
with self.command_state_lock:
|
||||||
return super(AsyncCommandResult, self).serialize(view)
|
return super(AsyncCommandResult, self).serialize()
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.execution_thread.start()
|
self.execution_thread.start()
|
||||||
@ -107,7 +106,7 @@ class AsyncCommandResult(BaseCommandResult):
|
|||||||
self.command_status = AgentCommandStatus.SUCCEEDED
|
self.command_status = AgentCommandStatus.SUCCEEDED
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if not isinstance(e, rest_errors.RESTError):
|
if not isinstance(e, errors.RESTError):
|
||||||
e = errors.CommandExecutionError(str(e))
|
e = errors.CommandExecutionError(str(e))
|
||||||
|
|
||||||
with self.command_state_lock:
|
with self.command_state_lock:
|
||||||
|
53
teeth_agent/encoding.py
Normal file
53
teeth_agent/encoding.py
Normal 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)
|
@ -14,10 +14,50 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
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."""
|
"""Error raised when a command fails to execute."""
|
||||||
|
|
||||||
message = 'Command execution failed'
|
message = 'Command execution failed'
|
||||||
@ -27,7 +67,7 @@ class CommandExecutionError(errors.RESTError):
|
|||||||
self.details = details
|
self.details = details
|
||||||
|
|
||||||
|
|
||||||
class InvalidCommandError(errors.InvalidContentError):
|
class InvalidCommandError(InvalidContentError):
|
||||||
"""Error which is raised when an unknown command is issued."""
|
"""Error which is raised when an unknown command is issued."""
|
||||||
|
|
||||||
messsage = 'Invalid command'
|
messsage = 'Invalid command'
|
||||||
@ -36,7 +76,7 @@ class InvalidCommandError(errors.InvalidContentError):
|
|||||||
super(InvalidCommandError, self).__init__(details)
|
super(InvalidCommandError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class InvalidCommandParamsError(errors.InvalidContentError):
|
class InvalidCommandParamsError(InvalidContentError):
|
||||||
"""Error which is raised when command parameters are invalid."""
|
"""Error which is raised when command parameters are invalid."""
|
||||||
|
|
||||||
message = 'Invalid command parameters'
|
message = 'Invalid command parameters'
|
||||||
@ -45,14 +85,14 @@ class InvalidCommandParamsError(errors.InvalidContentError):
|
|||||||
super(InvalidCommandParamsError, self).__init__(details)
|
super(InvalidCommandParamsError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class RequestedObjectNotFoundError(errors.NotFound):
|
class RequestedObjectNotFoundError(NotFound):
|
||||||
def __init__(self, type_descr, obj_id):
|
def __init__(self, type_descr, obj_id):
|
||||||
details = '{} with id {} not found.'.format(type_descr, obj_id)
|
details = '{} with id {} not found.'.format(type_descr, obj_id)
|
||||||
super(RequestedObjectNotFoundError, self).__init__(details)
|
super(RequestedObjectNotFoundError, self).__init__(details)
|
||||||
self.details = details
|
self.details = details
|
||||||
|
|
||||||
|
|
||||||
class OverlordAPIError(errors.RESTError):
|
class OverlordAPIError(RESTError):
|
||||||
"""Error raised when a call to the agent API fails."""
|
"""Error raised when a call to the agent API fails."""
|
||||||
|
|
||||||
message = 'Error in call to teeth-agent-api.'
|
message = 'Error in call to teeth-agent-api.'
|
||||||
@ -71,7 +111,7 @@ class HeartbeatError(OverlordAPIError):
|
|||||||
super(HeartbeatError, self).__init__(details)
|
super(HeartbeatError, self).__init__(details)
|
||||||
|
|
||||||
|
|
||||||
class ImageDownloadError(errors.RESTError):
|
class ImageDownloadError(RESTError):
|
||||||
"""Error raised when an image cannot be downloaded."""
|
"""Error raised when an image cannot be downloaded."""
|
||||||
|
|
||||||
message = 'Error downloading image.'
|
message = 'Error downloading image.'
|
||||||
@ -81,7 +121,7 @@ class ImageDownloadError(errors.RESTError):
|
|||||||
self.details = 'Could not download image with id {}.'.format(image_id)
|
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."""
|
"""Error raised when an image fails to verify against its checksum."""
|
||||||
|
|
||||||
message = 'Error verifying image checksum.'
|
message = 'Error verifying image checksum.'
|
||||||
@ -92,7 +132,7 @@ class ImageChecksumError(errors.RESTError):
|
|||||||
self.details = self.details.format(image_id)
|
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."""
|
"""Error raised when an image cannot be written to a device."""
|
||||||
|
|
||||||
message = 'Error writing image to device.'
|
message = 'Error writing image to device.'
|
||||||
@ -103,7 +143,7 @@ class ImageWriteError(errors.RESTError):
|
|||||||
self.details = self.details.format(device, exit_code)
|
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
|
"""Error raised when a configdrive directory cannot be written to a
|
||||||
device.
|
device.
|
||||||
"""
|
"""
|
||||||
@ -117,7 +157,7 @@ class ConfigDriveWriteError(errors.RESTError):
|
|||||||
self.details = details
|
self.details = details
|
||||||
|
|
||||||
|
|
||||||
class SystemRebootError(errors.RESTError):
|
class SystemRebootError(RESTError):
|
||||||
"""Error raised when a system cannot reboot."""
|
"""Error raised when a system cannot reboot."""
|
||||||
|
|
||||||
message = 'Error rebooting system.'
|
message = 'Error rebooting system.'
|
||||||
|
@ -22,7 +22,7 @@ import subprocess
|
|||||||
import stevedore
|
import stevedore
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
from teeth_rest import encoding
|
from teeth_agent import encoding
|
||||||
|
|
||||||
_global_manager = None
|
_global_manager = None
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ class HardwareInfo(encoding.Serializable):
|
|||||||
self.type = type
|
self.type = type
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def serialize(self, view):
|
def serialize(self):
|
||||||
return collections.OrderedDict([
|
return collections.OrderedDict([
|
||||||
('type', self.type),
|
('type', self.type),
|
||||||
('id', self.id),
|
('id', self.id),
|
||||||
|
@ -38,7 +38,7 @@ def _format_event(logger, method, event):
|
|||||||
have enough keys to format.
|
have enough keys to format.
|
||||||
"""
|
"""
|
||||||
if 'event' not in event:
|
if 'event' not in event:
|
||||||
# nothing to format, e.g. _log_request in teeth_rest/component
|
# nothing to format
|
||||||
return event
|
return event
|
||||||
# Get a list of fields that need to be filled.
|
# Get a list of fields that need to be filled.
|
||||||
formatter = string.Formatter()
|
formatter = string.Formatter()
|
||||||
|
@ -17,8 +17,8 @@ limitations under the License.
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from teeth_rest import encoding
|
|
||||||
|
|
||||||
|
from teeth_agent import encoding
|
||||||
from teeth_agent import errors
|
from teeth_agent import errors
|
||||||
|
|
||||||
|
|
||||||
@ -28,8 +28,7 @@ class APIClient(object):
|
|||||||
def __init__(self, api_url):
|
def __init__(self, api_url):
|
||||||
self.api_url = api_url.rstrip('/')
|
self.api_url = api_url.rstrip('/')
|
||||||
self.session = requests.Session()
|
self.session = requests.Session()
|
||||||
self.encoder = encoding.RESTJSONEncoder(
|
self.encoder = encoding.RESTJSONEncoder()
|
||||||
encoding.SerializationViews.PUBLIC)
|
|
||||||
|
|
||||||
def _request(self, method, path, data=None):
|
def _request(self, method, path, data=None):
|
||||||
request_url = '{api_url}{path}'.format(api_url=self.api_url, path=path)
|
request_url = '{api_url}{path}'.format(api_url=self.api_url, path=path)
|
||||||
|
@ -22,10 +22,10 @@ import mock
|
|||||||
import pkg_resources
|
import pkg_resources
|
||||||
from wsgiref import simple_server
|
from wsgiref import simple_server
|
||||||
|
|
||||||
from teeth_rest import encoding
|
|
||||||
|
|
||||||
from teeth_agent import agent
|
from teeth_agent import agent
|
||||||
from teeth_agent import base
|
from teeth_agent import base
|
||||||
|
from teeth_agent import encoding
|
||||||
from teeth_agent import errors
|
from teeth_agent import errors
|
||||||
from teeth_agent import hardware
|
from teeth_agent import hardware
|
||||||
|
|
||||||
@ -118,9 +118,7 @@ class TestHeartbeater(unittest.TestCase):
|
|||||||
|
|
||||||
class TestBaseAgent(unittest.TestCase):
|
class TestBaseAgent(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.encoder = encoding.RESTJSONEncoder(
|
self.encoder = encoding.RESTJSONEncoder(indent=4)
|
||||||
encoding.SerializationViews.PUBLIC,
|
|
||||||
indent=4)
|
|
||||||
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
|
self.agent = agent.TeethAgent('https://fake_api.example.org:8081/',
|
||||||
('localhost', 9999),
|
('localhost', 9999),
|
||||||
'192.168.1.1')
|
'192.168.1.1')
|
||||||
|
@ -22,7 +22,6 @@ import unittest
|
|||||||
from werkzeug import test
|
from werkzeug import test
|
||||||
from werkzeug import wrappers
|
from werkzeug import wrappers
|
||||||
|
|
||||||
from teeth_rest import encoding
|
|
||||||
|
|
||||||
from teeth_agent import agent
|
from teeth_agent import agent
|
||||||
from teeth_agent.api import app
|
from teeth_agent.api import app
|
||||||
@ -99,7 +98,7 @@ class TestTeethAPI(unittest.TestCase):
|
|||||||
self.assertEqual(kwargs, {'key': 'value'})
|
self.assertEqual(kwargs, {'key': 'value'})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
data = json.loads(response.data)
|
data = json.loads(response.data)
|
||||||
expected_result = result.serialize(encoding.SerializationViews.PUBLIC)
|
expected_result = result.serialize()
|
||||||
self.assertEqual(data, expected_result)
|
self.assertEqual(data, expected_result)
|
||||||
|
|
||||||
def test_execute_agent_command_validation(self):
|
def test_execute_agent_command_validation(self):
|
||||||
@ -150,7 +149,7 @@ class TestTeethAPI(unittest.TestCase):
|
|||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertEqual(json.loads(response.data), {
|
self.assertEqual(json.loads(response.data), {
|
||||||
u'commands': [
|
u'commands': [
|
||||||
cmd_result.serialize(None),
|
cmd_result.serialize(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -160,8 +159,7 @@ class TestTeethAPI(unittest.TestCase):
|
|||||||
True,
|
True,
|
||||||
{'test': 'result'})
|
{'test': 'result'})
|
||||||
|
|
||||||
serialized_cmd_result = cmd_result.serialize(
|
serialized_cmd_result = cmd_result.serialize()
|
||||||
encoding.SerializationViews.PUBLIC)
|
|
||||||
|
|
||||||
mock_agent = mock.create_autospec(agent.TeethAgent)
|
mock_agent = mock.create_autospec(agent.TeethAgent)
|
||||||
mock_agent.get_command_result.return_value = cmd_result
|
mock_agent.get_command_result.return_value = cmd_result
|
||||||
|
Loading…
x
Reference in New Issue
Block a user