Add a basic HTTP API for rug-ctl commands.

Change-Id: I445108491e1cdd569a84a883239a60e1a64385b9
This commit is contained in:
Ryan Petrello 2015-04-22 14:42:04 -04:00
parent 2aa3226fae
commit a0471de7bc
7 changed files with 331 additions and 1 deletions

103
akanda/rug/api/rug.py Normal file
View File

@ -0,0 +1,103 @@
# Copyright 2015 Akanda, Inc
#
# Author: Akanda, 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 socket
import eventlet
import eventlet.wsgi
import webob
import webob.dec
import webob.exc
from akanda.rug.cli import app
from akanda.rug.openstack.common import log as logging
LOG = logging.getLogger(__name__)
RUG_API_PORT = 44250 # 0xacda
class RugAPI(object):
def __init__(self, ctl=app.RugController):
self.ctl = ctl()
@webob.dec.wsgify(RequestClass=webob.Request)
def __call__(self, req):
try:
if req.method != 'PUT':
return webob.exc.HTTPMethodNotAllowed()
args = filter(None, req.path.split('/'))
if not args:
return webob.exc.HTTPNotFound()
command, _, _ = self.ctl.command_manager.find_command(args)
if command.interactive:
return webob.exc.HTTPNotImplemented()
return str(self.ctl.run(['--debug'] + args))
except SystemExit:
# cliff invokes -h (help) on argparse failure
# (which in turn results in sys.exit call)
return webob.exc.HTTPBadRequest()
except ValueError:
return webob.exc.HTTPNotFound()
except Exception:
LOG.exception("Unexpected error.")
msg = ('An unknown error has occurred. '
'Please try your request again.')
return webob.exc.HTTPInternalServerError(explanation=unicode(msg))
class RugAPIServer(object):
def __init__(self):
self.pool = eventlet.GreenPool(1000)
def run(self, ip_address, port=RUG_API_PORT):
app = RugAPI()
for i in xrange(5):
LOG.info(
'Starting the rug-api on %s/%s',
ip_address, port,
)
try:
sock = eventlet.listen(
(ip_address, port),
family=socket.AF_INET6,
backlog=128
)
except socket.error as err:
if err.errno != 99: # EADDRNOTAVAIL
raise
LOG.warn('Could not create rug-api socket: %s', err)
LOG.warn('Sleeping %s before trying again', i + 1)
eventlet.sleep(i + 1)
else:
break
else:
raise RuntimeError(
'Could not establish rug-api socket on %s/%s' %
(ip_address, port)
)
eventlet.wsgi.server(
sock,
app,
custom_pool=self.pool,
log=logging.WritableLogger(LOG))
def serve(ip_address):
RugAPIServer().run(ip_address)

View File

@ -42,7 +42,10 @@ class RugController(app.App):
def initialize_app(self, argv):
# Quiet logging for some request library
logging.getLogger('requests').setLevel(logging.WARN)
main.register_and_load_opts()
try:
main.register_and_load_opts()
except cfg.ArgsAlreadyParsedError:
pass
# Don't pass argv here because cfg.CONF will intercept the
# help options and exit.
cfg.CONF(['--config-file', '/etc/akanda-rug/rug.ini'],

View File

@ -200,6 +200,8 @@ def populate_routers(db, conf, workers):
class BrowseRouters(message.MessageSending):
log = logging.getLogger(__name__)
interactive = True
SCHEMA = '''CREATE TABLE routers (
id TEXT PRIMARY KEY,
name TEXT,

View File

@ -30,6 +30,7 @@ class MessageSending(command.Command):
__metaclass__ = abc.ABCMeta
log = logging.getLogger(__name__)
interactive = False
@abc.abstractmethod
def make_message(self, parsed_args):

View File

@ -137,6 +137,8 @@ class RouterManage(_TenantRouterCmd):
class RouterSSH(_TenantRouterCmd):
"""ssh into a router over the management network"""
interactive = True
def get_parser(self, prog_name):
p = super(RouterSSH, self).get_parser(prog_name)
p.add_argument('remainder', nargs=argparse.REMAINDER)

View File

@ -275,6 +275,14 @@ def main(argv=sys.argv[1:]):
)
metadata_proc.start()
from akanda.rug.api import rug as rug_api
rug_api_proc = multiprocessing.Process(
target=rug_api.serve,
args=(mgt_ip_address,),
name='rug-api'
)
rug_api_proc.start()
# Set up the notifications publisher
Publisher = (notifications.Publisher if cfg.CONF.ceilometer.enabled
else notifications.NoopPublisher)

View File

@ -0,0 +1,211 @@
import unittest
import mock
import socket
import webob
from cliff import commandmanager
from akanda.rug.api import rug
from akanda.rug.openstack.common import log as logging
class TestRugAPI(unittest.TestCase):
def setUp(self):
ctl = mock.Mock()
ctl.return_value.command_manager = commandmanager.CommandManager(
'akanda.rug.cli'
)
self.api = rug.RugAPI(ctl)
self.ctl = ctl.return_value
def test_browse(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/browse/'
}))
assert isinstance(resp, webob.exc.HTTPNotImplemented)
assert not self.ctl.run.called
def test_ssh(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/ssh/ROUTER123/'
}))
assert isinstance(resp, webob.exc.HTTPNotImplemented)
assert not self.ctl.run.called
def test_poll(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/poll/'
}))
self.ctl.run.assert_called_with(
['--debug', 'poll']
)
def test_missing_argument(self):
# argparse failures (e.g., a missing router ID) raise a SystemExit
# because cliff's behavior is to print a help message and sys.exit()
self.ctl.run.side_effect = SystemExit
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/debug/'
}))
assert isinstance(resp, webob.exc.HTTPBadRequest)
self.ctl.run.assert_called_with(
['--debug', 'router', 'debug']
)
def test_router_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/debug/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'debug', 'ROUTER123']
)
def test_router_manage(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/manage/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'manage', 'ROUTER123']
)
def test_router_update(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/update/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'update', 'ROUTER123']
)
def test_router_rebuild(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/rebuild/ROUTER123'
}))
self.ctl.run.assert_called_with(
['--debug', 'router', 'rebuild', 'ROUTER123']
)
def test_tenant_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/tenant/debug/TENANT123'
}))
self.ctl.run.assert_called_with(
['--debug', 'tenant', 'debug', 'TENANT123']
)
def test_tenant_manage(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/tenant/manage/TENANT123'
}))
self.ctl.run.assert_called_with(
['--debug', 'tenant', 'manage', 'TENANT123']
)
def test_workers_debug(self):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/workers/debug/'
}))
self.ctl.run.assert_called_with(
['--debug', 'workers', 'debug']
)
def test_invalid_router_action(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/router/breakdance/ROUTER123'
}))
assert isinstance(resp, webob.exc.HTTPNotFound)
assert not self.ctl.run.called
def test_multiple_calls(self):
for i in range(10):
self.api(webob.Request({
'REQUEST_METHOD': 'PUT',
'PATH_INFO': '/poll/'
}))
assert self.ctl.run.call_args_list == [
mock.call(['--debug', 'poll'])
for _ in range(10)
]
def test_invalid_request_method(self):
resp = self.api(webob.Request({
'REQUEST_METHOD': 'GET',
'PATH_INFO': '/poll/'
}))
assert isinstance(resp, webob.exc.HTTPMethodNotAllowed)
assert not self.ctl.run.called
class TestRugAPIServer(unittest.TestCase):
@mock.patch('eventlet.listen')
@mock.patch('eventlet.wsgi')
def test_bind_and_serve(self, wsgi, listen):
sock = listen.return_value
server = rug.RugAPIServer()
server.run('::1/128')
listen.assert_called_with(
('::1/128', 44250),
family=socket.AF_INET6,
backlog=128
)
args, kwargs = wsgi.server.call_args
assert all([
args[0] == sock,
isinstance(args[1], rug.RugAPI),
kwargs['custom_pool'] == server.pool,
isinstance(kwargs['log'], logging.WritableLogger)
])
@mock.patch('eventlet.listen')
@mock.patch('eventlet.sleep', lambda x: None)
def test_fail_to_bind(self, listen):
listen.side_effect = socket.error(
99, "Can't assign requested address"
)
server = rug.RugAPIServer()
self.assertRaises(
RuntimeError,
server.run,
'::1/128'
)
assert listen.call_args_list == [
mock.call(('::1/128', 44250), family=socket.AF_INET6, backlog=128)
for i in range(5)
]
@mock.patch('eventlet.listen')
@mock.patch('eventlet.wsgi')
@mock.patch('eventlet.sleep', lambda x: None)
def test_bind_fails_on_first_attempt(self, wsgi, listen):
sock = mock.Mock()
listen.side_effect = [
socket.error(99, "Can't assign requested address"),
sock
]
server = rug.RugAPIServer()
server.run('::1/128')
assert listen.call_args_list == [
mock.call(('::1/128', 44250), family=socket.AF_INET6, backlog=128)
for i in range(2) # fails the first time, succeeds the second
]
args, kwargs = wsgi.server.call_args
assert all([
args[0] == sock,
isinstance(args[1], rug.RugAPI),
kwargs['custom_pool'] == server.pool,
isinstance(kwargs['log'], logging.WritableLogger)
])