diff --git a/lbaas_mgm/__init__.py b/lbaas_mgm/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lbaas_mgm/faults.py b/lbaas_mgm/faults.py new file mode 100644 index 00000000..be74cae6 --- /dev/null +++ b/lbaas_mgm/faults.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# 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 + + +class ServiceFault(object): + def __init__(self, code, message, details): + self.code = code + self.message = message + self.details = details + + def to_json(self): + data = { + "serviceFault": { + "code": self.code, + "message": self.message, + "details": self.details + } + } + return data + + def __str__(self): + return json.dumps(self.to_json(), indent=4) + + +class BadRequest(ServiceFault): + def __init__(self, + validation_errors, + code="400", + message="Validation fault", + details="The object is not valid"): + ServiceFault.__init__(self, code, message, details) + self.validation_errors = validation_errors + + def to_json(self): + data = { + "badRequest": { + "code": self.code, + "message": self.message, + "details": self.details, + "validationErrors": { + "message": self.validation_errors + } + } + } + return data + + def __str__(self): + return json.dumps(self.to_json(), indent=4) diff --git a/lbaas_mgm/json_gearman.py b/lbaas_mgm/json_gearman.py new file mode 100644 index 00000000..a2da38af --- /dev/null +++ b/lbaas_mgm/json_gearman.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# 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 +from gearman import GearmanWorker, DataEncoder + + +class JSONDataEncoder(DataEncoder): + """ Class to transform data that the worker either receives or sends. """ + + @classmethod + def encode(cls, encodable_object): + """ Encode JSON object as string """ + return json.dumps(encodable_object) + + @classmethod + def decode(cls, decodable_string): + """ Decode string to JSON object """ + return json.loads(decodable_string) + + +class JSONGearmanWorker(GearmanWorker): + """ Overload the Gearman worker class so we can set the data encoder. """ + data_encoder = JSONDataEncoder diff --git a/lbaas_mgm/listener.py b/lbaas_mgm/listener.py new file mode 100644 index 00000000..dec9acfa --- /dev/null +++ b/lbaas_mgm/listener.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# 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 socket + +from json_gearman import JSONGearmanWorker +from lbaas_mgm.faults import BadRequest + + +class Listener(object): + def __init__(self, logger): + self.logger = logger + + def run(self): + my_ip = socket.gethostbyname(socket.gethostname()) + task_name = "lbaas-mgm-%s" % my_ip + self.logger.debug("Registering task %s" % task_name) + + worker = JSONGearmanWorker(['localhost:4730']) + worker.set_client_id(my_ip) + worker.register_task(task_name, self.task) + + def task(self, worker, job): + data = json.loads(job.data) + + if 'command' not in data: + return BadRequest("Missing 'command' element").to_json() + + command = data['command'] + self.logger.debug('Command: {cmd}'.format(cmd=command)) + if command == 'get': + self.logger.debug('Get one node from pool') + else: + return BadRequest("Invalid command").to_json() + + return data diff --git a/lbaas_mgm/mgm.py b/lbaas_mgm/mgm.py new file mode 100644 index 00000000..61233081 --- /dev/null +++ b/lbaas_mgm/mgm.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# Copyright 2012 Hewlett-Packard Development Company, L.P. +# +# 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 logging +import argparse +import daemon +import signal +import sys + +from lbaas_mgm.listener import Listener + + +class Server(object): + def __init__(self, logger, nodes): + self.logger = logger + self.nodes = nodes + + def main(self): + self.logger.info( + 'LBaaS Pool Manager started with {nodes} nodes' + .format(nodes=self.nodes) + ) + signal.signal(signal.SIGINT, self.exit_handler) + signal.signal(signal.SIGTERM, self.exit_handler) + listner = Listener(self.logger) + try: + listner.run() + except Exception as e: + self.logger.critical( + 'Exception: {eclass}, {exception}' + .format(eclass=e.__class__, exception=e) + ) + self.shutdown(True) + self.shutdown(False) + + def exit_handler(self, signum, frame): + signal.signal(signal.SIGINT, signal.SIG_IGN) + signal.signal(signal.SIGTERM, signal.SIG_IGN) + + def shutdown(self, error): + if not error: + self.logger.info('Safely shutting down') + sys.exit(0) + else: + self.logger.info('Shutting down due to error') + sys.exit(1) + + +def main(): + parser = argparse.ArgumentParser( + description='LBaaS Node Management Daemon' + ) + parser.add_argument('nodes', metavar='N', type=int, + help='number of nodes to maintain') + parser.add_argument('-d', dest='nodaemon', action='store_true', + help='do not run in daemon mode') + options = parser.parse_args() + + logging.basicConfig( + format='%(asctime)-6s: %(name)s - %(levelname)s - %(message)s', + filename='/var/log/lbaas/lbaas_mgm.log' + ) + logger = logging.getLogger('lbaas_mgm') + logger.setLevel(logging.INFO) + + pid_fn = '/var/run/lbaas_mgm/lbaas_mgm.pid' + pid = daemon.pidlockfile.TimeoutPIDLockFile(pid_fn, 10) + + server = Server(logger, options.nodes) + + if options.nodaemon: + server.main() + else: + with daemon.DaemonContext(pidfile=pid): + server.main() diff --git a/setup.py b/setup.py index eb3a23f0..01235fc9 100644 --- a/setup.py +++ b/setup.py @@ -32,12 +32,13 @@ setuptools.setup( name="lbaas_worker", description="Python LBaaS Gearman Worker", version="1.0", - author="David Shrewsbury", - author_email="shrewsbury.dave@gmail.com", + author="David Shrewsbury , \ + Andrew Hutchings ", packages=setuptools.find_packages(exclude=["*.tests"]), entry_points={ 'console_scripts': [ - 'lbaas_worker = lbaas_worker.worker:main' + 'lbaas_worker = lbaas_worker.worker:main', + 'lbaas_pool_mgm = lbaas_mgm.mgm:main' ] }, cmdclass={'test': PyTest}, diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/mock.py b/tests/mock.py new file mode 100644 index 00000000..b43c4707 --- /dev/null +++ b/tests/mock.py @@ -0,0 +1,30 @@ +import json +import logging + + +class FakeJob(object): + def __init__(self, data): + """ + data: JSON object to convert to a string + """ + self.data = json.dumps(data) + + +class MockLoggingHandler(logging.Handler): + """Mock logging handler to check for expected logs.""" + + def __init__(self, *args, **kwargs): + self.reset() + logging.Handler.__init__(self, *args, **kwargs) + + def emit(self, record): + self.messages[record.levelname.lower()].append(record.getMessage()) + + def reset(self): + self.messages = { + 'debug': [], + 'info': [], + 'warning': [], + 'error': [], + 'critical': [], + } diff --git a/tests/test_lbaas_mgm.py b/tests/test_lbaas_mgm.py new file mode 100644 index 00000000..a89a2eb3 --- /dev/null +++ b/tests/test_lbaas_mgm.py @@ -0,0 +1,37 @@ +import unittest +import logging + +import tests.mock + +from lbaas_mgm.listener import Listener + + +class TestLBaaSMgmTask(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger('lbass_mgm_test') + self.lh = tests.mock.MockLoggingHandler() + self.logger.setLevel(logging.DEBUG) + self.logger.addHandler(self.lh) + + def tearDown(self): + pass + + def testTaskGet(self): + listener = Listener(self.logger) + data = {'command': 'get'} + job = tests.mock.FakeJob(data) + result = listener.task(None, job) + self.assertIn('Command: get', self.lh.messages['debug']) + self.assertEqual(result['command'], data['command']) + + def testTaskBad(self): + listener = Listener(self.logger) + data = {'command': 'bad'} + job = tests.mock.FakeJob(data) + result = listener.task(None, job) + self.assertIn("badRequest", result) + self.assertIn("validationErrors", result['badRequest']) + self.assertEqual( + "Invalid command", + result['badRequest']['validationErrors']['message'] + )