Add a basic HTTP API for rug-ctl commands.
Change-Id: I445108491e1cdd569a84a883239a60e1a64385b9
This commit is contained in:
parent
2aa3226fae
commit
a0471de7bc
|
@ -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)
|
|
@ -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'],
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
])
|
Loading…
Reference in New Issue