From 2114f93d6e352193a19d7aaf898f203f1d62c0c5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Sat, 17 Oct 2015 16:04:53 -0400 Subject: [PATCH] Retire stackforge/staccato --- .gitreview | 4 - HACKING.rst | 256 ------ LICENSE | 176 ---- README.rst | 29 +- doc/source/api.rst | 139 --- doc/source/architecture.rst | 84 -- doc/source/executor_consitency.rst | 67 -- doc/source/index.rst | 37 - doc/source/need.rst | 71 -- doc/source/quickstart.rst | 69 -- doc/source/staccato_internal_architecture.png | Bin 38684 -> 0 bytes doc/source/staccato_plugin_2_party.png | Bin 49989 -> 0 bytes doc/source/staccato_plugin_3rd_party.png | Bin 70621 -> 0 bytes etc/staccato-api-paste.ini | 37 - etc/staccato-api.conf | 76 -- etc/staccato-protocols.json | 4 - nova_plugin/README.rst | 6 - nova_plugin/requirements.txt | 3 - nova_plugin/setup.cfg | 34 - nova_plugin/setup.py | 7 - .../staccato_nova_download/__init__.py | 161 ---- .../staccato_nova_download/tests/__init__.py | 1 - .../staccato_nova_download/tests/base.py | 19 - .../tests/unit/__init__.py | 0 .../tests/unit/test_basic.py | 268 ------ nova_plugin/test-requirements.txt | 0 openstack-common.conf | 18 - requirements.txt | 39 - setup.cfg | 33 - setup.py | 53 -- staccato/__init__.py | 18 - staccato/api/__init__.py | 0 staccato/api/v1/__init__.py | 0 staccato/api/v1/xfer.py | 279 ------ staccato/api/versions.py | 30 - staccato/cmd/__init__.py | 16 - staccato/cmd/api.py | 43 - staccato/cmd/manage.py | 76 -- staccato/cmd/scheduler.py | 29 - staccato/common/__init__.py | 0 staccato/common/config.py | 124 --- staccato/common/exceptions.py | 51 -- staccato/common/state_machine.py | 80 -- staccato/common/utils.py | 112 --- staccato/db/__init__.py | 245 ----- staccato/db/migrate_repo/__init__.py | 0 staccato/db/migrate_repo/migrate.cfg | 20 - .../migrate_repo/versions/001_placeholder.py | 6 - staccato/db/migrate_repo/versions/__init__.py | 1 - staccato/db/migration.py | 117 --- staccato/db/models.py | 54 -- staccato/openstack/__init__.py | 0 staccato/openstack/common/__init__.py | 0 staccato/openstack/common/context.py | 82 -- .../openstack/common/eventlet_backdoor.py | 89 -- staccato/openstack/common/exception.py | 142 --- staccato/openstack/common/excutils.py | 51 -- staccato/openstack/common/gettextutils.py | 50 - staccato/openstack/common/importutils.py | 67 -- staccato/openstack/common/jsonutils.py | 169 ---- staccato/openstack/common/local.py | 48 - staccato/openstack/common/log.py | 558 ------------ staccato/openstack/common/loopingcall.py | 147 --- .../openstack/common/middleware/__init__.py | 0 .../openstack/common/middleware/context.py | 64 -- .../openstack/common/middleware/sizelimit.py | 84 -- staccato/openstack/common/network_utils.py | 69 -- .../openstack/common/notifier/__init__.py | 14 - staccato/openstack/common/notifier/api.py | 182 ---- .../openstack/common/notifier/log_notifier.py | 35 - .../common/notifier/no_op_notifier.py | 19 - .../openstack/common/notifier/rpc_notifier.py | 46 - .../common/notifier/rpc_notifier2.py | 52 -- .../common/notifier/test_notifier.py | 22 - staccato/openstack/common/pastedeploy.py | 164 ---- staccato/openstack/common/policy.py | 780 ---------------- staccato/openstack/common/processutils.py | 247 ----- staccato/openstack/common/rpc/__init__.py | 307 ------- staccato/openstack/common/rpc/amqp.py | 677 -------------- staccato/openstack/common/rpc/common.py | 514 ----------- staccato/openstack/common/rpc/dispatcher.py | 153 ---- staccato/openstack/common/rpc/impl_fake.py | 195 ---- staccato/openstack/common/rpc/impl_kombu.py | 838 ----------------- staccato/openstack/common/rpc/impl_qpid.py | 650 ------------- staccato/openstack/common/rpc/impl_zmq.py | 851 ------------------ staccato/openstack/common/rpc/matchmaker.py | 425 --------- .../openstack/common/rpc/matchmaker_redis.py | 149 --- staccato/openstack/common/rpc/proxy.py | 187 ---- staccato/openstack/common/rpc/service.py | 75 -- staccato/openstack/common/rpc/zmq_receiver.py | 41 - staccato/openstack/common/service.py | 333 ------- staccato/openstack/common/setup.py | 367 -------- staccato/openstack/common/sslutils.py | 80 -- staccato/openstack/common/threadgroup.py | 121 --- staccato/openstack/common/timeutils.py | 186 ---- staccato/openstack/common/uuidutils.py | 39 - staccato/openstack/common/version.py | 94 -- staccato/openstack/common/wsgi.py | 800 ---------------- staccato/openstack/common/xmlutils.py | 74 -- staccato/protocols/__init__.py | 0 staccato/protocols/file/__init__.py | 119 --- staccato/protocols/http/__init__.py | 80 -- staccato/protocols/interface.py | 39 - staccato/scheduler/__init__.py | 0 staccato/scheduler/interface.py | 6 - staccato/scheduler/simple_thread.py | 49 - staccato/tests/__init__.py | 1 - staccato/tests/functional/__init__.py | 0 staccato/tests/functional/test_db.py | 376 -------- staccato/tests/integration/__init__.py | 1 - staccato/tests/integration/base.py | 102 --- staccato/tests/integration/test_api.py | 235 ----- staccato/tests/unit/__init__.py | 0 staccato/tests/unit/test_config.py | 29 - staccato/tests/unit/test_protocol_loading.py | 102 --- staccato/tests/unit/test_statemachine.py | 55 -- staccato/tests/unit/test_utils.py | 107 --- staccato/tests/unit/v1/__init__.py | 0 staccato/tests/unit/v1/test_api.py | 200 ---- staccato/tests/utils.py | 109 --- staccato/version.py | 20 - staccato/xfer/__init__.py | 0 staccato/xfer/constants.py | 26 - staccato/xfer/events.py | 126 --- staccato/xfer/executor.py | 78 -- staccato/xfer/utils.py | 129 --- test-requirements.txt | 29 - tools/install_venv_common.py | 222 ----- tools/make_state_machine.sh | 9 - tox.ini | 32 - 130 files changed, 5 insertions(+), 15605 deletions(-) delete mode 100644 .gitreview delete mode 100644 HACKING.rst delete mode 100644 LICENSE delete mode 100644 doc/source/api.rst delete mode 100644 doc/source/architecture.rst delete mode 100644 doc/source/executor_consitency.rst delete mode 100644 doc/source/index.rst delete mode 100644 doc/source/need.rst delete mode 100644 doc/source/quickstart.rst delete mode 100644 doc/source/staccato_internal_architecture.png delete mode 100644 doc/source/staccato_plugin_2_party.png delete mode 100644 doc/source/staccato_plugin_3rd_party.png delete mode 100644 etc/staccato-api-paste.ini delete mode 100644 etc/staccato-api.conf delete mode 100644 etc/staccato-protocols.json delete mode 100644 nova_plugin/README.rst delete mode 100644 nova_plugin/requirements.txt delete mode 100644 nova_plugin/setup.cfg delete mode 100644 nova_plugin/setup.py delete mode 100644 nova_plugin/staccato_nova_download/__init__.py delete mode 100644 nova_plugin/staccato_nova_download/tests/__init__.py delete mode 100644 nova_plugin/staccato_nova_download/tests/base.py delete mode 100644 nova_plugin/staccato_nova_download/tests/unit/__init__.py delete mode 100644 nova_plugin/staccato_nova_download/tests/unit/test_basic.py delete mode 100644 nova_plugin/test-requirements.txt delete mode 100644 openstack-common.conf delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 staccato/__init__.py delete mode 100644 staccato/api/__init__.py delete mode 100644 staccato/api/v1/__init__.py delete mode 100644 staccato/api/v1/xfer.py delete mode 100644 staccato/api/versions.py delete mode 100644 staccato/cmd/__init__.py delete mode 100755 staccato/cmd/api.py delete mode 100644 staccato/cmd/manage.py delete mode 100644 staccato/cmd/scheduler.py delete mode 100644 staccato/common/__init__.py delete mode 100644 staccato/common/config.py delete mode 100644 staccato/common/exceptions.py delete mode 100644 staccato/common/state_machine.py delete mode 100644 staccato/common/utils.py delete mode 100644 staccato/db/__init__.py delete mode 100644 staccato/db/migrate_repo/__init__.py delete mode 100644 staccato/db/migrate_repo/migrate.cfg delete mode 100644 staccato/db/migrate_repo/versions/001_placeholder.py delete mode 100644 staccato/db/migrate_repo/versions/__init__.py delete mode 100644 staccato/db/migration.py delete mode 100644 staccato/db/models.py delete mode 100644 staccato/openstack/__init__.py delete mode 100644 staccato/openstack/common/__init__.py delete mode 100644 staccato/openstack/common/context.py delete mode 100644 staccato/openstack/common/eventlet_backdoor.py delete mode 100644 staccato/openstack/common/exception.py delete mode 100644 staccato/openstack/common/excutils.py delete mode 100644 staccato/openstack/common/gettextutils.py delete mode 100644 staccato/openstack/common/importutils.py delete mode 100644 staccato/openstack/common/jsonutils.py delete mode 100644 staccato/openstack/common/local.py delete mode 100644 staccato/openstack/common/log.py delete mode 100644 staccato/openstack/common/loopingcall.py delete mode 100644 staccato/openstack/common/middleware/__init__.py delete mode 100644 staccato/openstack/common/middleware/context.py delete mode 100644 staccato/openstack/common/middleware/sizelimit.py delete mode 100644 staccato/openstack/common/network_utils.py delete mode 100644 staccato/openstack/common/notifier/__init__.py delete mode 100644 staccato/openstack/common/notifier/api.py delete mode 100644 staccato/openstack/common/notifier/log_notifier.py delete mode 100644 staccato/openstack/common/notifier/no_op_notifier.py delete mode 100644 staccato/openstack/common/notifier/rpc_notifier.py delete mode 100644 staccato/openstack/common/notifier/rpc_notifier2.py delete mode 100644 staccato/openstack/common/notifier/test_notifier.py delete mode 100644 staccato/openstack/common/pastedeploy.py delete mode 100644 staccato/openstack/common/policy.py delete mode 100644 staccato/openstack/common/processutils.py delete mode 100644 staccato/openstack/common/rpc/__init__.py delete mode 100644 staccato/openstack/common/rpc/amqp.py delete mode 100644 staccato/openstack/common/rpc/common.py delete mode 100644 staccato/openstack/common/rpc/dispatcher.py delete mode 100644 staccato/openstack/common/rpc/impl_fake.py delete mode 100644 staccato/openstack/common/rpc/impl_kombu.py delete mode 100644 staccato/openstack/common/rpc/impl_qpid.py delete mode 100644 staccato/openstack/common/rpc/impl_zmq.py delete mode 100644 staccato/openstack/common/rpc/matchmaker.py delete mode 100644 staccato/openstack/common/rpc/matchmaker_redis.py delete mode 100644 staccato/openstack/common/rpc/proxy.py delete mode 100644 staccato/openstack/common/rpc/service.py delete mode 100755 staccato/openstack/common/rpc/zmq_receiver.py delete mode 100644 staccato/openstack/common/service.py delete mode 100644 staccato/openstack/common/setup.py delete mode 100644 staccato/openstack/common/sslutils.py delete mode 100644 staccato/openstack/common/threadgroup.py delete mode 100644 staccato/openstack/common/timeutils.py delete mode 100644 staccato/openstack/common/uuidutils.py delete mode 100644 staccato/openstack/common/version.py delete mode 100644 staccato/openstack/common/wsgi.py delete mode 100644 staccato/openstack/common/xmlutils.py delete mode 100644 staccato/protocols/__init__.py delete mode 100644 staccato/protocols/file/__init__.py delete mode 100644 staccato/protocols/http/__init__.py delete mode 100644 staccato/protocols/interface.py delete mode 100644 staccato/scheduler/__init__.py delete mode 100644 staccato/scheduler/interface.py delete mode 100644 staccato/scheduler/simple_thread.py delete mode 100644 staccato/tests/__init__.py delete mode 100644 staccato/tests/functional/__init__.py delete mode 100644 staccato/tests/functional/test_db.py delete mode 100644 staccato/tests/integration/__init__.py delete mode 100644 staccato/tests/integration/base.py delete mode 100644 staccato/tests/integration/test_api.py delete mode 100644 staccato/tests/unit/__init__.py delete mode 100644 staccato/tests/unit/test_config.py delete mode 100644 staccato/tests/unit/test_protocol_loading.py delete mode 100644 staccato/tests/unit/test_statemachine.py delete mode 100644 staccato/tests/unit/test_utils.py delete mode 100644 staccato/tests/unit/v1/__init__.py delete mode 100644 staccato/tests/unit/v1/test_api.py delete mode 100644 staccato/tests/utils.py delete mode 100644 staccato/version.py delete mode 100644 staccato/xfer/__init__.py delete mode 100644 staccato/xfer/constants.py delete mode 100644 staccato/xfer/events.py delete mode 100644 staccato/xfer/executor.py delete mode 100644 staccato/xfer/utils.py delete mode 100644 test-requirements.txt delete mode 100644 tools/install_venv_common.py delete mode 100755 tools/make_state_machine.sh delete mode 100644 tox.ini diff --git a/.gitreview b/.gitreview deleted file mode 100644 index 454f0bb..0000000 --- a/.gitreview +++ /dev/null @@ -1,4 +0,0 @@ -[gerrit] -host=review.openstack.org -port=29418 -project=stackforge/staccato.git diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index f37ffb3..0000000 --- a/HACKING.rst +++ /dev/null @@ -1,256 +0,0 @@ -Staccato Style Commandments -=========================== - -- Step 1: Read http://www.python.org/dev/peps/pep-0008/ -- Step 2: Read http://www.python.org/dev/peps/pep-0008/ again -- Step 3: Read on - - -General -------- -- Put two newlines between top-level code (funcs, classes, etc) -- Put one newline between methods in classes and anywhere else -- Do not write "except:", use "except Exception:" at the very least -- Include your name with TODOs as in "#TODO(termie)" -- Do not name anything the same name as a built-in or reserved word -- Use the "is not" operator when testing for unequal identities. Example:: - - if not X is Y: # BAD, intended behavior is ambiguous - pass - - if X is not Y: # OKAY, intuitive - pass - -- Use the "not in" operator for evaluating membership in a collection. Example:: - - if not X in Y: # BAD, intended behavior is ambiguous - pass - - if X not in Y: # OKAY, intuitive - pass - - if not (X in Y or X in Z): # OKAY, still better than all those 'not's - pass - - -Imports -------- -- Do not make relative imports -- Order your imports by the full module path -- Organize your imports according to the following template - -Example:: - - # vim: tabstop=4 shiftwidth=4 softtabstop=4 - {{stdlib imports in human alphabetical order}} - \n - {{third-party lib imports in human alphabetical order}} - \n - {{staccato imports in human alphabetical order}} - \n - \n - {{begin your code}} - - -Human Alphabetical Order Examples ---------------------------------- -Example:: - - import httplib - import logging - import random - import StringIO - import time - import unittest - - import eventlet - import webob.exc - - import staccato.api.middleware - from staccato.api import images - from staccato.auth import users - import staccato.common - from staccato.endpoint import cloud - from staccato import test - - -Docstrings ----------- - -Docstrings are required for all functions and methods. - -Docstrings should ONLY use triple-double-quotes (``"""``) - -Single-line docstrings should NEVER have extraneous whitespace -between enclosing triple-double-quotes. - -**INCORRECT** :: - - """ There is some whitespace between the enclosing quotes :( """ - -**CORRECT** :: - - """There is no whitespace between the enclosing quotes :)""" - -Docstrings that span more than one line should look like this: - -Example:: - - """ - Start the docstring on the line following the opening triple-double-quote - - If you are going to describe parameters and return values, use Sphinx, the - appropriate syntax is as follows. - - :param foo: the foo parameter - :param bar: the bar parameter - :returns: return_type -- description of the return value - :returns: description of the return value - :raises: AttributeError, KeyError - """ - -**DO NOT** leave an extra newline before the closing triple-double-quote. - - -Dictionaries/Lists ------------------- -If a dictionary (dict) or list object is longer than 80 characters, its items -should be split with newlines. Embedded iterables should have their items -indented. Additionally, the last item in the dictionary should have a trailing -comma. This increases readability and simplifies future diffs. - -Example:: - - my_dictionary = { - "image": { - "name": "Just a Snapshot", - "size": 2749573, - "properties": { - "user_id": 12, - "arch": "x86_64", - }, - "things": [ - "thing_one", - "thing_two", - ], - "status": "ACTIVE", - }, - } - - -Calling Methods ---------------- -Calls to methods 80 characters or longer should format each argument with -newlines. This is not a requirement, but a guideline:: - - unnecessarily_long_function_name('string one', - 'string two', - kwarg1=constants.ACTIVE, - kwarg2=['a', 'b', 'c']) - - -Rather than constructing parameters inline, it is better to break things up:: - - list_of_strings = [ - 'what_a_long_string', - 'not as long', - ] - - dict_of_numbers = { - 'one': 1, - 'two': 2, - 'twenty four': 24, - } - - object_one.call_a_method('string three', - 'string four', - kwarg1=list_of_strings, - kwarg2=dict_of_numbers) - - -Internationalization (i18n) Strings ------------------------------------ -In order to support multiple languages, we have a mechanism to support -automatic translations of exception and log strings. - -Example:: - - msg = _("An error occurred") - raise HTTPBadRequest(explanation=msg) - -If you have a variable to place within the string, first internationalize the -template string then do the replacement. - -Example:: - - msg = _("Missing parameter: %s") % ("flavor",) - LOG.error(msg) - -If you have multiple variables to place in the string, use keyword parameters. -This helps our translators reorder parameters when needed. - -Example:: - - msg = _("The server with id %(s_id)s has no key %(m_key)s") - LOG.error(msg % {"s_id": "1234", "m_key": "imageId"}) - - -Creating Unit Tests -------------------- -For every new feature, unit tests should be created that both test and -(implicitly) document the usage of said feature. If submitting a patch for a -bug that had no unit test, a new passing unit test should be added. If a -submitted bug fix does have a unit test, be sure to add a new one that fails -without the patch and passes with the patch. - - -Commit Messages ---------------- -Using a common format for commit messages will help keep our git history -readable. Follow these guidelines: - - First, provide a brief summary of 50 characters or less. Summaries - of greater then 72 characters will be rejected by the gate. - - The first line of the commit message should provide an accurate - description of the change, not just a reference to a bug or - blueprint. It must be followed by a single blank line. - - Following your brief summary, provide a more detailed description of - the patch, manually wrapping the text at 72 characters. This - description should provide enough detail that one does not have to - refer to external resources to determine its high-level functionality. - - Once you use 'git review', two lines will be appended to the commit - message: a blank line followed by a 'Change-Id'. This is important - to correlate this commit with a specific review in Gerrit, and it - should not be modified. - -For further information on constructing high quality commit messages, -and how to split up commits into a series of changes, consult the -project wiki: - - http://wiki.openstack.org/GitCommitMessages - - -openstack-common ----------------- - -A number of modules from openstack-common are imported into the project. - -These modules are "incubating" in openstack-common and are kept in sync -with the help of openstack-common's update.py script. See: - - http://wiki.openstack.org/CommonLibrary#Incubation - -The copy of the code should never be directly modified here. Please -always update openstack-common first and then run the script to copy -the changes across. - - -Logging -------- -Use __name__ as the name of your logger and name your module-level logger -objects 'LOG':: - - LOG = logging.getLogger(__name__) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68c771a..0000000 --- a/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - diff --git a/README.rst b/README.rst index d2fe832..9006052 100644 --- a/README.rst +++ b/README.rst @@ -1,26 +1,7 @@ -======== -Staccato -======== +This project is no longer maintained. -Introduction -============ +The contents of this repository are still available in the Git source code +management system. To see the contents of this repository before it reached +its end of life, please check out the previous commit with +"git checkout HEAD^1". -Staccato is the name given to lightning which appears as a single very -bright, short-duration stroke that often has considerable branching. -The name was choosen for this service because it is intended to transfer -data to and from clouds very quickly and and with the option of multicast. - -TODO -==== - -Short Term ----------- -* Change the scheduler/executor relationship to be based on oslo messaging -* Add a glance plugin -* Add a swift plugin -* More test coverage (a plug in tester) - -Long Term ---------- -* Add a bittorrent plugin -* Create 3rd party transfer API diff --git a/doc/source/api.rst b/doc/source/api.rst deleted file mode 100644 index 3c6e4b6..0000000 --- a/doc/source/api.rst +++ /dev/null @@ -1,139 +0,0 @@ -Stacatto REST API -================= - -This document describes the current v1 Stacatto REST API - -Data Types ----------- - -States -****** -* STATE_NEW -* STATE_RUNNING -* STATE_CANCELING -* STATE_CANCELED -* STATE_ERRORING -* STATE_ERROR -* STATE_COMPLETE -* STATE_DELETED - -.. _xfer-doc-yype: -Xfer Document Type -****************** -* id : UUID - -* source_url : string - - The URL of the source of the data to be transferred. - -* destination_url : string - - The URL of the destination where the source URL will be copied. - -* state : State - - The current state of the transfer. - -* progress : integer - - The number of bytes safely transferred to the destination storage system - thus far. - -* start_offset - - The offset into the source data set from which the transfer will begin. - -* end_offset : integer - - The offset into the source data set at which the transfer will end. - -* destination_options : JSON document - - A JSON document that is defined by the transfer service protocol plugin - in use. That plugin is determined by the scheme portion of the - destination URL. - -* source_options : JSON document - - A JSON document that is defined by the transfer service protocol plugin - in use. That plugin is determined by the scheme portion of the - source URL. - -Example:: - - {"start_offset": 0, - "id": "590edf8c-1b2b-44d0-af6a-d9190753b6eb", - "state": "STATE_NEW", - "progress": 0, - "end_offset": -1, - "source_url": "file:///bin/bash", - "destination_options": {}, - "destination_url": "file:///tmp/ooo", - "source_options": {}} - - -List All Transfers ------------------- -GET /v1/transfers - -Options: -* limit -* next... - -Response -******** -* code: 200 -* A list of xfer document types - -Request a Transfer ------------------- -POST /v1/transfers - -Required Parameters -******************* -* source_url -* destination_url - -Optional -******** -* source_options -* destination_options -* start_offset -* end_offset - -Response -******** -* code: 201 -* xfer document type - -Check Transfer Status ---------------------- -GET /v1/transfers/{transfer id} - -Response -******** -* code: 200 -* xfer document type - -Cancel A Transfer ------------------ -POST /v1/transfers/{transfer id}/action - -Required Parameters: -- xferaction: cancel - -"Content-Type: application/json" - -Response -******** -* code: 202 (if async) - 204 (if sync) - -Delete A Transfer ------------------ -DELETE /v1/transfers/{transfer id} - -Response -******** -* code: 202 (if async) - 204 (if sync) diff --git a/doc/source/architecture.rst b/doc/source/architecture.rst deleted file mode 100644 index 171fb4b..0000000 --- a/doc/source/architecture.rst +++ /dev/null @@ -1,84 +0,0 @@ - -Stacatto Architecture -===================== - -This document describes the basic internal architecture of the Staccato -system. - - -.. image:: staccato_internal_architecture.png - :width: 738 - :height: 499 - -REST API --------- - -The user facing service is a REST API implemented using WSGI. A -reference runner is provided with this distribution but it is also -possible to run it using other WSGI containers like apache2+mod_wsgi and -cherrypy. The REST service is assembled using paste deploy - -This comment takes uses requests, validates them, and then interacts -with the database. For new transfer requests it adds information to the -DB about what the user has requested. For cancels, deletes, or any -other update the associate request is looked up in the database and -updated. For lists or status requests the associated transfer requests -(or set of transfer requests) is pulled out of the database and the -relevant information is returned to the user according to the defined -protocol. - -The REST API process does no further work. It simply vets a client and -its request and then translates the information between the user and the -database where the worker processes can consume it. - -Scheduler ---------- - -The scheduler is responsible for deciding when a transfer request is -ready to be executed. When one is selected as ready its corresponding -database entry will be marked. The scheduler does no further work. - -Because it is likely that various different scheduling techniques will -evolve over time and that different deployments will require different -scheduler, this is implemented in a plug-in fashion. The first and most -basic scheduler will be one that allows N transfer to happen at one -time. - -Executor --------- - -The executor is the process that handles the actual transfer of data. -It connects to the database and looks for entries that the scheduler has -marked as *ready*. It then examines the request and opens up the -protocol module needed for the source and the protocol module needed for -the destination. Connections are formed and the data is routed from the -source to the destination. As the transfer progresses updates are -written to the database. In this way if a transfer fails part of the way -through it can be restarted from the last known point of progress that -was recorded in the database. - -Protocol Plugins ----------------- - -The implementation of each protocol that Staccato can speak is -abstracted away into protocol modules. This will allow maximal code -reuse and ease of protocol creation. Also, it will allow future -developers to easily create protocols without having to make them part -of the Staccato distribution. - -For the sake of clarity we offer one simple and common use case. When -nova-compute does an boot of an instance it downloads an image from -Glance. When using Staccato the source protocol module would be a -*Glance* plugin and the destination protocol module would be a *file -system* plugin. - -The following diagrams show how the plugin will work in some common cases: - - -.. image:: staccato_plugin_2_party.png - :width: 529 - :height: 397 - -.. image:: staccato_plugin_3rd_party.png - :width: 529 - :height: 397 diff --git a/doc/source/executor_consitency.rst b/doc/source/executor_consitency.rst deleted file mode 100644 index af567ea..0000000 --- a/doc/source/executor_consitency.rst +++ /dev/null @@ -1,67 +0,0 @@ - -Only One Executor At A Time -=========================== - -It is important that only 1 process is ever working on a transfer at a time. -It is further important that no one transfer is stalled out because the -executor that began it failed part of the way through. This document describes -the plan to achieve this. - -The Problem ------------ - -Say that Staccato is configured with multiple executors. Partially through -a transfer one of the executors fails, however the other executor is running -perfectly well. We need something to detect that the transfer which was -running (and which is still in the running state) is no longer active and -needs to be placed back into a pending state (NEW or ERROR at the time of -this writing). Once placed in such a state it will be active for scheduling -again. - -We also want to avoid the situation where there are two executors and they -both select the same transfer and thus work is redundantly done. This could -happen via a race, or it could happen when it appears that an executor has -died but in reality it is (or will soon be) transferring data. - -Redundant Transfer ------------------- - -At the time of this writing staccato does allow for the possibility of -redundant transfers. The contract with the user is that some (or all) -of the data set may be transfers twice. This contract is there to release -the staccato implementation and architecture from complicated and slow -inter-process locking mechanisms which would needed to avoid every single -case. However, this contract is not there to allow staccato to entirely -ignore the problem. Redundant transfers are unwelcome because they use -resource. By the very nature of this problem, redudnant transfer will only -happen when the system is unaware of all but one of the unneeded transfers -(if we know about them, we would kill them) thus staccato cannot properly -manage the resources. - -Solution --------- - -Each executor will be associated with a UUID that lives in the process space -of that executor (it is not written to the database). Each row in the database -represents a requested transfer. When the state column in that row moves to -RUNNING the executor ID will be recorded in another column in that row. As -the executor is performing a transfer it periodically checks the row on which -it is working to verify that its UUID is still the one in the database. If it -is not, it must immediately terminate its workload without making any further -updates to the database. The time window in which it checks the database is -configurable and will define the window of possibility for redundant transfers. - -The executor UUID will be some combination of hostname and pid. This will make -it easier for an operator to determine what is happening. - -Clean Up -~~~~~~~~ - -We also need to determine if a transfer is marked as running, but the executor -has unexpectedly died. In order to determine this we will look at the -'updated_at' time stamp that is associated with every transfer row. If the -row has not been updated in N times the configurable update window then -staccato assumes that the executor is dead, it clears the executor UUID from -the row, and moves the transfer back to a pending state. - - diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index ddbadda..0000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,37 +0,0 @@ - -Welcome to Staccato's documentation! -==================================== - -The Staccato project provides a service for transferring data (most -commonly the data transferred are VM images) from a source end point -to a destination end point. Staccato is does not manage or control -the storage of that data, it interacts with other services (swift, -glance, file systems, etc) which handle that. Staccato's only job -is to manage the actual transfer of the data from one storage system -to another. Below are a few of the tasks needed to accomplish that job: - -* Monitor the progress of a transfer and restart it when needed. -* Negotiate the best possible protocol (for example bittorrent for - multicast). -* Manage resource (NIC, disk I/O, etc) and protect them from overheating. -* Verify data integrity and security of the bytes in flight. - -Staccato is as a service that does a upload or download of an image on -behalf of a client. The client can issue a single, short lived request -to move an image. Unlike traditional upload/downloads the client does -not have to live for the length of the transfer marshaling the protocol -de jour for every single byte. Instead it issues a request and then -ends. Later it can check back in with the service to determine progress. -Staccato does the work of protocol negotiation and optimal parameter -setting, scheduling the transfer, error recovery, and much more. - -Contents -============ - -.. toctree:: - :maxdepth: 1 - - quickstart - architecture - need - api diff --git a/doc/source/need.rst b/doc/source/need.rst deleted file mode 100644 index 5042d38..0000000 --- a/doc/source/need.rst +++ /dev/null @@ -1,71 +0,0 @@ - -The Need For Staccato -===================== - -In this document we describe why a transfer service like Staccato is -needed. This need breaks down into three major areas: - -Robustness -Efficiency -Workload - -Robust Transfers ----------------- - -Data transfers fail. Transmitting large amount of data can be -expensive. Ideally transfers are check pointed along the way so that -when the inevitable failure occurs, the transfer can be restarted from -the last known checkpoint thus minimizing the redundant data that is -sent. - -Unfortunately performing such check-pointing is non trivial. The -information needs to be consistently stored in a way that will survive -the termination of both source and destination transfer endpoints. -Locking mechanisms and consistency measures must be in place to be -certain that only one transfer takes place at a time. Certain protocols -do not allow for partial transfer in which case a cache is needed to -minimize the potential of transmission. - -There are many more complications that make the job of monitoring a -transfer difficult. Rather than trying to embed all of the needed -complicated logic into a traditional client, this is the kind of thing -best implemented with a service. - -Efficient Transfers -------------------- - -Commonly the protocol used for data transfer is defined by the storage -system in which the source data lives. This is not always the best -choice, and it conflates the concepts of an access protocol and a -transfer protocol. Often times the best protocol to use is determined -not only by the architecture and workload of the source storage system, -but also that of the destination as well as that of the network. - -A service like Staccato is in a architectural position to know more -about what is happening on all three of these components, the source -storage system, the destination storage system, and the network. It can -avoid thrashing and overheating of resources by scheduling transfers at -optimal times, select optimal protocols (think of bittorrent when a -single source is requested for download to many destinations), and -setting more optimal parameters on protocols for the transfer at hand -(think of TCP buffer sizes). - -Having this knowledge and functionality in a traditional client would be -overly complicated it not impossible. - -Workload --------- - -Clients often wish to do download a data set to a local file, or upload a -local file to a more well managed storage system. Such clients are the -target users for this service. As it commonly stands today clients -download files by connecting to a remote storage system by speaking its -protocol and marshaling ever byte of that protocol (including security -signing and other potentially processor intensive work). The workload -put upon the client scales with the size of the image and the protocol -in use. Rarely does the client plan its resources and time outs around -these things. In these case the client really just wants file, it -doesn't want to do the work or contribute the resources (CPU, NIC, -memory) to do it. - -Because of this a service that offloads this burden makes sense. diff --git a/doc/source/quickstart.rst b/doc/source/quickstart.rst deleted file mode 100644 index 32862c6..0000000 --- a/doc/source/quickstart.rst +++ /dev/null @@ -1,69 +0,0 @@ - -Stacatto Quick Start -==================== - -This document describes the fastest way to get the transfer service -rolling. - -Installation ------------- - -create a python virtual environment and install staccatto into it:: - - $ virtualenv --no-site-packages staccattoVE - $ source staccattoVE/bin/activate - $ python setup.py install - -Configuration Files -------------------- - -There are three major configuration files: - -- staccato-api.conf -- staccato-api-paste.ini -- staccato-protocols.json - -sample files that will work for the purposes of this quick start -guide are included in the etc directory. - -API Program ------------ - -The API program runs the REST interpreter. To start it run:: - - $ staccato-api --config-file etc/staccato-api.conf - -Scheduler Program ------------------ - -The scheduler program checks for transfers that are ready to go and -starts them. To start the scheduler run the following:: - - $ staccato-scheduler --config-file etc/staccato-api.conf - -Interact With curl ------------------- - -Request a transfer:: - - $ curl -X POST http://localhost:5309/v1/transfers --data '{"source_url": "file:///bin/bash", "destination_url": "file:///tmp/ooo"}' -H "Content-Type: application/json" - {"start_offset": 0, "id": "2eade223-b11b-413b-9185-7b16c1b2ed6d", "state": "STATE_NEW", "progress": 0, "end_offset": -1, "source_url": "file:///bin/bash", "destination_options": {}, "destination_url": "file:///tmp/ooo", "source_options": {}} - -Check the status:: - - $ curl -X GET http://localhost:5309/v1/transfers/2eade223-b11b-413b-9185-7b16c1b2ed6d - {"start_offset": 0, "id": "2eade223-b11b-413b-9185-7b16c1b2ed6d", "state": "STATE_NEW", "progress": 0, "end_offset": -1, "source_url": "file:///bin/bash", "destination_options": {}, "destination_url": "file:///tmp/ooo", "source_options": {} - - -Cancel:: - - $ curl -X POST http://localhost:5309/v1/transfers/2eade223-b11b-413b-9185-7b16c1b2ed6d/action --data '{"xferaction": "cancel"}' -H "Content-Type: application/json" - - -Clean up:: - - $ curl -X DELETE http://localhost:5309/v1/transfers/2eade223-b11b-413b-9185-7b16c1b2ed6d - -List all:: - - $ curl -X GET http://localhost:5309/v1/transfers diff --git a/doc/source/staccato_internal_architecture.png b/doc/source/staccato_internal_architecture.png deleted file mode 100644 index 8ef3cbfb405de28439ba8e7511d31ab6597bd19e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 38684 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%0A-OtCFXQYOM>3Lit5S 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 diff --git a/doc/source/staccato_plugin_3rd_party.png b/doc/source/staccato_plugin_3rd_party.png deleted file mode 100644 index 61dcf60c3f92459630995d51495ca771fd97a7c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 70621 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

I5-RryGGcQymyh$+o|%qdt6u=vR7JaZ91fy z_JM|;oazYsQOF#?p{6xkc$dnAut<%&4af z=2KJxtkH~xEvaKdpW)rRZ%7wFYYEL_r!V#nP7YC#k(ap3JBO=4lIOJeU%YHW%hy>& zCl(}eZLzS{C{IBqC#R++0EgR~pxfJ`UuZc${crz-4myvLTbE|D=Q7!6MN1kZ1`3hJ zf65#CHlsWEESjaol{rmI#7#|2IRQxJB|L)lQKN)!@ kw*Os+ - -[keystone_authtoken] -auth_host = 127.0.0.1 -auth_port = 35357 -auth_protocol = http -admin_tenant_name = %SERVICE_TENANT_NAME% -admin_user = %SERVICE_USER% -admin_password = %SERVICE_PASSWORD% - -[paste_deploy] -config_file = staccato-api-paste.ini -#flavor=None diff --git a/etc/staccato-protocols.json b/etc/staccato-protocols.json deleted file mode 100644 index d4a7ef1..0000000 --- a/etc/staccato-protocols.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "file": [{"module": "staccato.protocols.file.FileProtocol"}], - "http": [{"module": "staccato.protocols.http.HttpProtocol"}] -} diff --git a/nova_plugin/README.rst b/nova_plugin/README.rst deleted file mode 100644 index 5db8bbb..0000000 --- a/nova_plugin/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -OpenStack Nova Staccato Plugin -============================== - -This plugin will be installed into the python environment as an entry point. -Nova can then load it to manage transfers. This must be installed in the -python environment which nova compute uses. \ No newline at end of file diff --git a/nova_plugin/requirements.txt b/nova_plugin/requirements.txt deleted file mode 100644 index 24f789a..0000000 --- a/nova_plugin/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -d2to1>=0.2.10,<0.3 -pbr>=0.5.16,<0.6 -nova \ No newline at end of file diff --git a/nova_plugin/setup.cfg b/nova_plugin/setup.cfg deleted file mode 100644 index 630bbd9..0000000 --- a/nova_plugin/setup.cfg +++ /dev/null @@ -1,34 +0,0 @@ -[metadata] -name = staccato_nova_download -version = 2013.2 -summary = A plugin for nova that will handle image downloads via staccato -description-file = README.rst -author = OpenStack -author-email = openstack-dev@lists.openstack.org -home-page = http://www.openstack.org/ -classifier = - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 2.7 - Programming Language :: Python :: 2.6 - -[global] -setup-hooks = - pbr.hooks.setup_hook - -[files] -packages = staccato_nova_download - -[entry_points] -nova.image.download.modules = - staccato = staccato_nova_download - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 diff --git a/nova_plugin/setup.py b/nova_plugin/setup.py deleted file mode 100644 index 47eceb6..0000000 --- a/nova_plugin/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env python - -import setuptools - -setuptools.setup( - setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5,<0.6'], - d2to1=True) diff --git a/nova_plugin/staccato_nova_download/__init__.py b/nova_plugin/staccato_nova_download/__init__.py deleted file mode 100644 index 6b0ca9d..0000000 --- a/nova_plugin/staccato_nova_download/__init__.py +++ /dev/null @@ -1,161 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Red Hat, Inc. -# All Rights Reserved. -# -# 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 httplib -import json -import logging - -from oslo.config import cfg - -from nova import exception -import nova.image.download.base as xfer_base -from nova.openstack.common.gettextutils import _ - - -CONF = cfg.CONF -LOG = logging.getLogger(__name__) - -opt_groups = [cfg.StrOpt(name='hostname', default='127.0.0.1', - help=_('The hostname of the staccato service.')), - cfg.IntOpt(name='port', default=5309, - help=_('The port where the staccato service is ' - 'listening.')), - cfg.IntOpt(name='poll_interval', default=1, - help=_('The amount of time in second to poll for ' - 'transfer completion')) - ] - -CONF.register_opts(opt_groups, group="staccato_nova_download_module") - - -class StaccatoTransfer(xfer_base.TransferBase): - - def __init__(self): - self.conf_group = CONF['staccato_nova_download_module'] - self.client = httplib.HTTPConnection(self.conf_group.hostname, - self.conf_group.port) - - def _delete(self, xfer_id, headers): - path = '/v1/transfers/%s' % xfer_id - self.client.request('DELETE', path, headers=headers) - response = self.client.getresponse() - if response.status != 204: - msg = _('Error deleting transfer %s') % response.read() - LOG.error(msg) - raise exception.ImageDownloadModuleError( - {'reason': msg, 'module': unicode(self)}) - - def _wait_for_complete(self, xfer_id, headers): - error_states = ['STATE_CANCELED', 'STATE_ERROR', 'STATE_DELETED'] - - path = '/v1/transfers/%s' % xfer_id - while True: - self.client.request('GET', path, headers=headers) - response = self.client.getresponse() - if response.status != 200: - msg = _('Error requesting a new transfer %s') % response.read() - LOG.error(msg) - try: - self._delete(xfer_id, headers) - except Exception as ex: - LOG.error(ex) - raise exception.ImageDownloadModuleError( - {'reason': msg, 'module': unicode(self)}) - - body = response.read() - response_dict = json.loads(body) - if response_dict['state'] == 'STATE_COMPLETE': - break - - if response_dict['state'] in error_states: - try: - self._delete(xfer_id, headers) - except Exception as ex: - LOG.error(ex) - msg = (_('The transfer could not be completed in state %s') - % response_dict['state']) - raise exception.ImageDownloadModuleError( - {'reason': msg, 'module': unicode(self)}) - - def download(self, context, url_parts, dst_file, metadata, **kwargs): - LOG.info((_('Attemption to use %(module)s to download %(url)s')) % - {'module': unicode(self), 'url': url_parts.geturl()}) - - headers = {'Content-Type': 'application/json'} - if CONF.auth_strategy == 'keystone': - headers['X-Auth-Token'] = getattr(context, 'auth_token', None) - headers['X-User-Id'] = getattr(context, 'user', None) - headers['X-Tenant-Id'] = getattr(context, 'tenant', None) - - data = {'source_url': url_parts.geturl(), - 'destination_url': 'file://%s' % dst_file} - try: - self.client.request('POST', '/v1/transfers', - headers=headers, body=json.dumps(data)) - response = self.client.getresponse() - if response.status != 200: - msg = (_('Error requesting a new transfer %s. Status = %d') % - (response.read(), response.status)) - LOG.error(msg) - raise exception.ImageDownloadModuleError( - {'reason': msg, 'module': unicode(self)}) - body = response.read() - response_dict = json.loads(body) - - self._wait_for_complete(response_dict['id'], headers) - except exception.ImageDownloadModuleError: - raise - except Exception as ex: - msg = unicode(ex.message) - LOG.exception(ex) - raise exception.ImageDownloadModuleError( - {'reason': msg, 'module': u'StaccatoTransfer'}) - - -def get_download_hander(**kwargs): - return StaccatoTransfer() - - -def get_schemes(): - conf_group = CONF['staccato_nova_download_module'] - try: - LOG.info(("Staccato get_schemes(): %s:%s" % - (conf_group.hostname, conf_group.port))) - client = httplib.HTTPConnection(conf_group.hostname, conf_group.port) - client.request('GET', '/') - response = client.getresponse() - body = response.read() - LOG.info("Staccato version info %s" % body) - json_body = json.loads(body) - version_json = json_body['versions'] - if 'v1' not in version_json: - reason = 'The staccato service does not support v1' - LOG.error(reason) - raise exception.ImageDownloadModuleError({'reason': reason, - 'module': u'staccato'}) - - version_json = version_json['v1'] - LOG.info("Staccato offers %s" % str(version_json)) - return version_json['protocols'] - except Exception as ex: - LOG.exception(ex) - reason = str(ex) - LOG.error(reason) - return [] -#NOTE(jbresnah) nova doesn't properly handle this yet -# raise exception.ImageDownloadModuleError({'reason': reason, -# 'module': u'staccato'}) diff --git a/nova_plugin/staccato_nova_download/tests/__init__.py b/nova_plugin/staccato_nova_download/tests/__init__.py deleted file mode 100644 index 7fcc62c..0000000 --- a/nova_plugin/staccato_nova_download/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'jbresnah' diff --git a/nova_plugin/staccato_nova_download/tests/base.py b/nova_plugin/staccato_nova_download/tests/base.py deleted file mode 100644 index fb60a0a..0000000 --- a/nova_plugin/staccato_nova_download/tests/base.py +++ /dev/null @@ -1,19 +0,0 @@ -from oslo.config import cfg - -import testtools - - -CONF = cfg.CONF - - -class BaseTest(testtools.TestCase): - def setUp(self): - super(BaseTest, self).setUp() - - def tearDown(self): - super(BaseTest, self).tearDown() - - def config(self, **kw): - group = kw.pop('group', None) - for k, v in kw.iteritems(): - CONF.set_override(k, v, group) diff --git a/nova_plugin/staccato_nova_download/tests/unit/__init__.py b/nova_plugin/staccato_nova_download/tests/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/nova_plugin/staccato_nova_download/tests/unit/test_basic.py b/nova_plugin/staccato_nova_download/tests/unit/test_basic.py deleted file mode 100644 index c68f6df..0000000 --- a/nova_plugin/staccato_nova_download/tests/unit/test_basic.py +++ /dev/null @@ -1,268 +0,0 @@ -import httplib -import json -import urlparse - -import mox -from nova import exception -from oslo.config import cfg - -from staccato.common import config -import staccato_nova_download - -import staccato_nova_download.tests.base as base - - -CONF = cfg.CONF - -CONF.import_opt('auth_strategy', 'nova.api.auth') - - -class TestBasic(base.BaseTest): - - def setUp(self): - super(TestBasic, self).setUp() - self.mox = mox.Mox() - - def tearDown(self): - super(TestBasic, self).tearDown() - self.mox.UnsetStubs() - - def test_get_schemes(self): - start_protocols = ["file", "http", "somethingelse"] - version_info_back = {'protocols': start_protocols} - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - response = self.mox.CreateMockAnything() - http_obj.request('GET', '/').AndReturn(response) - response.read().AndReturn(json.dumps(version_info_back)) - - self.mox.ReplayAll() - protocols = staccato_nova_download.get_schemes() - self.mox.VerifyAll() - self.assertEqual(start_protocols, protocols) - - def test_get_schemes_failed_connection(self): - start_protocols = ["file", "http", "somethingelse"] - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - http_obj.request('GET', '/').AndRaise(Exception("message")) - - self.mox.ReplayAll() - self.assertRaises(exception.ImageDownloadModuleError, - staccato_nova_download.get_schemes) - self.mox.VerifyAll() - - def test_successfull_download(self): - class FakeResponse(object): - def __init__(self, status, reply): - self.status = status - self.reply = reply - - def read(self): - return json.dumps(self.reply) - - self.config(auth_strategy='notkeystone') - - xfer_id = 'someidstring' - src_url = 'file:///etc/group' - dst_url = 'file:///tmp/group' - data = {'source_url': src_url, 'destination_url': dst_url} - - headers = {'Content-Type': 'application/json'} - - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - - http_obj.request('POST', '/v1/transfers', - headers=headers, body=data) - http_obj.getresponse().AndReturn(FakeResponse(201, {'id': xfer_id})) - - path = '/v1/transfers/%s' % xfer_id - http_obj.request('GET', path, headers=headers) - http_obj.getresponse().AndReturn( - FakeResponse(200, {'status': 'STATE_COMPLETE'})) - - self.mox.ReplayAll() - st_plugin = staccato_nova_download.StaccatoTransfer() - - url_parts = urlparse.urlparse(src_url) - dst_url_parts = urlparse.urlparse(dst_url) - st_plugin.download(url_parts, dst_url_parts.path, {}) - self.mox.VerifyAll() - - def test_successful_download_with_keystone(self): - class FakeContext(object): - auth_token = 'sdfsdf' - user = 'buzztroll' - tenant = 'staccato' - - class FakeResponse(object): - def __init__(self, status, reply): - self.status = status - self.reply = reply - - def read(self): - return json.dumps(self.reply) - - self.config(auth_strategy='keystone') - - xfer_id = 'someidstring' - src_url = 'file:///etc/group' - dst_url = 'file:///tmp/group' - data = {'source_url': src_url, 'destination_url': dst_url} - - context = FakeContext() - headers = {'Content-Type': 'application/json', - 'X-Auth-Token': context.auth_token, - 'X-User-Id': context.user, - 'X-Tenant-Id': context.tenant} - - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - - http_obj.request('POST', '/v1/transfers', - headers=headers, body=data) - http_obj.getresponse().AndReturn(FakeResponse(201, {'id': xfer_id})) - - path = '/v1/transfers/%s' % xfer_id - http_obj.request('GET', path, headers=headers) - http_obj.getresponse().AndReturn( - FakeResponse(200, {'status': 'STATE_COMPLETE'})) - - self.mox.ReplayAll() - st_plugin = staccato_nova_download.StaccatoTransfer() - - url_parts = urlparse.urlparse(src_url) - dst_url_parts = urlparse.urlparse(dst_url) - st_plugin.download(url_parts, dst_url_parts.path, - {}, context=context) - self.mox.VerifyAll() - - def test_download_post_error(self): - class FakeResponse(object): - def __init__(self, status, reply): - self.status = status - self.reply = reply - - def read(self): - return json.dumps(self.reply) - - self.config(auth_strategy='notkeystone') - - xfer_id = 'someidstring' - src_url = 'file:///etc/group' - dst_url = 'file:///tmp/group' - data = {'source_url': src_url, 'destination_url': dst_url} - - headers = {'Content-Type': 'application/json'} - - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - - http_obj.request('POST', '/v1/transfers', - headers=headers, body=data) - http_obj.getresponse().AndReturn(FakeResponse(400, {'id': xfer_id})) - - self.mox.ReplayAll() - st_plugin = staccato_nova_download.StaccatoTransfer() - - url_parts = urlparse.urlparse(src_url) - dst_url_parts = urlparse.urlparse(dst_url) - - self.assertRaises(exception.ImageDownloadModuleError, - st_plugin.download, - url_parts, - dst_url_parts.path, - {}) - - self.mox.VerifyAll() - - def test_successful_error_case(self): - class FakeResponse(object): - def __init__(self, status, reply): - self.status = status - self.reply = reply - - def read(self): - return json.dumps(self.reply) - - self.config(auth_strategy='notkeystone') - - xfer_id = 'someidstring' - src_url = 'file:///etc/group' - dst_url = 'file:///tmp/group' - data = {'source_url': src_url, 'destination_url': dst_url} - - headers = {'Content-Type': 'application/json'} - - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - - http_obj.request('POST', '/v1/transfers', - headers=headers, body=data) - http_obj.getresponse().AndReturn(FakeResponse(201, {'id': xfer_id})) - - path = '/v1/transfers/%s' % xfer_id - http_obj.request('GET', path, headers=headers) - http_obj.getresponse().AndReturn( - FakeResponse(200, {'status': 'STATE_ERROR'})) - path = '/v1/transfers/%s' % xfer_id - http_obj.request('DELETE', path, headers=headers) - http_obj.getresponse() - - self.mox.ReplayAll() - st_plugin = staccato_nova_download.StaccatoTransfer() - - url_parts = urlparse.urlparse(src_url) - dst_url_parts = urlparse.urlparse(dst_url) - self.assertRaises(exception.ImageDownloadModuleError, - st_plugin.download, - url_parts, - dst_url_parts.path, - {}) - self.mox.VerifyAll() - - def test_status_error_case(self): - class FakeResponse(object): - def __init__(self, status, reply): - self.status = status - self.reply = reply - - def read(self): - return json.dumps(self.reply) - - self.config(auth_strategy='notkeystone') - - xfer_id = 'someidstring' - src_url = 'file:///etc/group' - dst_url = 'file:///tmp/group' - data = {'source_url': src_url, 'destination_url': dst_url} - - headers = {'Content-Type': 'application/json'} - - self.mox.StubOutClassWithMocks(httplib, 'HTTPConnection') - http_obj = httplib.HTTPConnection('127.0.0.1', 5309) - - http_obj.request('POST', '/v1/transfers', - headers=headers, body=data) - http_obj.getresponse().AndReturn(FakeResponse(201, {'id': xfer_id})) - - path = '/v1/transfers/%s' % xfer_id - http_obj.request('GET', path, headers=headers) - http_obj.getresponse().AndReturn( - FakeResponse(500, {'status': 'STATE_COMPLETE'})) - - path = '/v1/transfers/%s' % xfer_id - http_obj.request('DELETE', path, headers=headers) - http_obj.getresponse() - self.mox.ReplayAll() - st_plugin = staccato_nova_download.StaccatoTransfer() - - url_parts = urlparse.urlparse(src_url) - dst_url_parts = urlparse.urlparse(dst_url) - self.assertRaises(exception.ImageDownloadModuleError, - st_plugin.download, - url_parts, - dst_url_parts.path, - {}) - self.mox.VerifyAll() diff --git a/nova_plugin/test-requirements.txt b/nova_plugin/test-requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/openstack-common.conf b/openstack-common.conf deleted file mode 100644 index 2aeb98f..0000000 --- a/openstack-common.conf +++ /dev/null @@ -1,18 +0,0 @@ -[DEFAULT] - -# The list of modules to copy from openstack-common -module=gettextutils -module=importutils -module=install_venv_common -module=jsonutils -module=local -module=log -module=notifier -module=policy -module=setup -module=timeutils -module=uuidutils -module=version - -# The base module to hold the copy of openstack.common -base=staccato diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7fe4b35..0000000 --- a/requirements.txt +++ /dev/null @@ -1,39 +0,0 @@ -# The greenlet package must be compiled with gcc and needs -# the Python.h headers. Make sure you install the python-dev -# package to get the right headers... -greenlet>=0.3.1 - -# < 0.8.0/0.8 does not work, see https://bugs.launchpad.net/bugs/1153983 -SQLAlchemy>=0.7.8,<=0.7.99 -anyjson -eventlet>=0.9.12 -PasteDeploy -routes -WebOb>=1.2 -wsgiref -argparse -boto -sqlalchemy-migrate>=0.7 -httplib2 -kombu -pycrypto>=2.1.0alpha1 -iso8601>=0.1.4 -oslo.config>=1.1.0 - - -# For Swift storage backend. -python-swiftclient>=1.2,<2 - -# Note you will need gcc buildtools installed and must -# have installed libxml headers for lxml to be successfully -# installed using pip, therefore you will need to install the -# libxml2-dev and libxslt-dev Ubuntu packages. -lxml - -# For paste.util.template used in keystone.common.template -Paste - -passlib -jsonschema -python-keystoneclient>=0.2.0 -pyOpenSSL diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 54ae538..0000000 --- a/setup.cfg +++ /dev/null @@ -1,33 +0,0 @@ -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source - -[egg_info] -tag_build = -tag_date = 0 -tag_svn_revision = 0 - -[compile_catalog] -directory = staccato/locale -domain = staccato - -[update_catalog] -domain = staccato -output_dir = staccato/locale -input_file = staccato/locale/staccato.pot - -[extract_messages] -keywords = _ gettext ngettext l_ lazy_gettext -mapping_file = babel.cfg -output_file = staccato/locale/staccato.pot - -[nosetests] -# NOTE(jkoelker) To run the test suite under nose install the following -# coverage http://pypi.python.org/pypi/coverage -# tissue http://pypi.python.org/pypi/tissue (pep8 checker) -# openstack-nose https://github.com/jkoelker/openstack-nose -verbosity=2 -cover-package = staccato -cover-html = true -cover-erase = true diff --git a/setup.py b/setup.py deleted file mode 100644 index a26637b..0000000 --- a/setup.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2010 OpenStack, LLC. -# -# 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 setuptools - -from staccato.openstack.common import setup - -requires = setup.parse_requirements() -depend_links = setup.parse_dependency_links() -project = 'staccato' - -setuptools.setup( - name=project, - version='0.1', - description='The Staccato project provides data transfer services ' - 'to OpenStack services and users. It is primarily used ' - 'for VM image propagation.', - license='Apache License (2.0)', - author='OpenStack', - author_email='openstack@lists.launchpad.net', - url='http://staccato.openstack.org/', - packages=setuptools.find_packages(exclude=['']), - test_suite='nose.collector', - cmdclass=setup.get_cmdclass(), - include_package_data=True, - install_requires=requires, - dependency_links=depend_links, - classifiers=[ - 'Development Status :: 4 - Beta', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX :: Linux', - 'Programming Language :: Python :: 2.6', - 'Environment :: No Input/Output (Daemon)', - 'Environment :: OpenStack', - ], - entry_points={'console_scripts': - ['staccato-api=staccato.cmd.api:main', - 'staccato-manage=staccato.cmd.manage:main', - 'staccato-scheduler=staccato.cmd.scheduler:main']}, - py_modules=[]) diff --git a/staccato/__init__.py b/staccato/__init__.py deleted file mode 100644 index 5a20894..0000000 --- a/staccato/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010-2011 OpenStack LLC. -# All Rights Reserved. -# -# 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. -from staccato.openstack.common import gettextutils -gettextutils.install('staccato') diff --git a/staccato/api/__init__.py b/staccato/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/api/v1/__init__.py b/staccato/api/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/api/v1/xfer.py b/staccato/api/v1/xfer.py deleted file mode 100644 index 4eba53a..0000000 --- a/staccato/api/v1/xfer.py +++ /dev/null @@ -1,279 +0,0 @@ -import json -import logging -import urlparse -import uuid - -import routes -import webob -import webob.exc - -import staccato.openstack.common.wsgi as os_wsgi -import staccato.openstack.common.middleware.context as os_context -import staccato.xfer.executor as executor -import staccato.xfer.events as xfer_events -from staccato import db -from staccato.xfer.constants import Events -from staccato.common import config, exceptions -from staccato.common import utils - -LOG = logging.getLogger(__name__) - - -def _make_request_id(user, tenant): - return str(uuid.uuid4()) - - -class UnauthTestMiddleware(os_context.ContextMiddleware): - def __init__(self, app, options): - self.options = options - super(UnauthTestMiddleware, self).__init__(app, options) - - def process_request(self, req): - LOG.debug('Making an unauthenticated context.') - req.context = self.make_context(is_admin=True, - user='admin') - req.context.owner = 'admin' - - -class AuthContextMiddleware(os_context.ContextMiddleware): - def __init__(self, app, options): - self.options = options - print options - super(AuthContextMiddleware, self).__init__(app, options) - - def process_request(self, req): - if req.headers.get('X-Identity-Status') == 'Confirmed': - req.context = self._get_authenticated_context(req) - else: - raise webob.exc.HTTPUnauthorized() - - def _get_authenticated_context(self, req): - LOG.debug('Making an authenticated context.') - auth_token = req.headers.get('X-Auth-Token') - user = req.headers.get('X-User-Id') - tenant = req.headers.get('X-Tenant-Id') - is_admin = self.options.admin_user_id.strip().lower() == user - - request_id = _make_request_id(user, tenant) - - context = self.make_context(is_admin=is_admin, user=user, - tenant=tenant, auth_token=auth_token, - request_id=request_id) - context.owner = user - - return context - - -class XferController(object): - - def __init__(self, db_con, sm, conf): - self.sm = sm - self.db_con = db_con - self.log = logging - self.conf = conf - - def _xfer_from_db(self, xfer_id, owner): - return self.db_con.lookup_xfer_request_by_id( - xfer_id, owner=owner) - - def _to_state_machine(self, event, xfer_request, name): - self.sm.event_occurred(event, - xfer_request=xfer_request, - db=self.db_con) - - @utils.StaccatoErrorToHTTP('Create a new transfer', LOG) - def newtransfer(self, request, source_url, destination_url, owner, - source_options=None, destination_options=None, - start_offset=0, end_offset=None): - - srcurl_parts = urlparse.urlparse(source_url) - dsturl_parts = urlparse.urlparse(destination_url) - - dstopts = {} - srcopts = {} - - if source_options is not None: - srcopts = source_options - if destination_options is not None: - dstopts = destination_options - - plugin_policy = config.get_protocol_policy(self.conf) - src_module_name = utils.find_protocol_module_name(plugin_policy, - srcurl_parts) - dst_module_name = utils.find_protocol_module_name(plugin_policy, - dsturl_parts) - - src_module = utils.load_protocol_module(src_module_name, self.conf) - dst_module = utils.load_protocol_module(dst_module_name, self.conf) - - dstopts = dst_module.new_write(dsturl_parts, dstopts) - srcopts = src_module.new_read(srcurl_parts, srcopts) - - xfer = self.db_con.get_new_xfer(owner, - source_url, - destination_url, - src_module_name, - dst_module_name, - start_ndx=start_offset, - end_ndx=end_offset, - source_opts=srcopts, - dest_opts=dstopts) - return xfer - - @utils.StaccatoErrorToHTTP('Check the status', LOG) - def status(self, request, xfer_id, owner): - xfer = self._xfer_from_db(xfer_id, owner) - return xfer - - @utils.StaccatoErrorToHTTP('List transfers', LOG) - def list(self, request, owner, limit=None): - return self.db_con.lookup_xfer_request_all(owner=owner, limit=limit) - - @utils.StaccatoErrorToHTTP('Delete a transfer', LOG) - def delete(self, request, xfer_id, owner): - xfer_request = self._xfer_from_db(xfer_id, owner) - self._to_state_machine(Events.EVENT_DELETE, - xfer_request, - 'delete') - - @utils.StaccatoErrorToHTTP('Cancel a transfer', LOG) - def xferaction(self, request, xfer_id, owner, xferaction, **kwvals): - xfer_request = self._xfer_from_db(xfer_id, owner) - self._to_state_machine(Events.EVENT_CANCEL, - xfer_request, - 'cancel') - - -class XferHeaderDeserializer(os_wsgi.RequestHeadersDeserializer): - def default(self, request): - return {'owner': request.context.owner} - - -class XferDeserializer(os_wsgi.JSONDeserializer): - """Default request headers deserializer""" - - def _validate(self, body, required, optional): - request = {} - for k in body: - if k not in required and k not in optional: - msg = '%s is an unknown option.' % k - raise webob.exc.HTTPBadRequest(explanation=msg) - for k in required: - if k not in body: - msg = 'The option %s must be specified.' % k - raise webob.exc.HTTPBadRequest(explanation=msg) - request[k] = body[k] - for k in optional: - if k in body: - request[k] = body[k] - return request - - def newtransfer(self, body): - _required = ['source_url', 'destination_url'] - _optional = ['source_options', 'destination_options', 'start_offset', - 'end_offset'] - request = self._validate(self._from_json(body), _required, _optional) - return request - - def list(self, body): - _required = [] - _optional = ['limit', 'next', 'filter'] - request = self._validate(self._from_json(body), _required, _optional) - return request - - def cancel(self, body): - _required = ['xferaction'] - _optional = ['async'] - request = self._validate(body, _required, _optional) - return request - - def xferaction(self, body): - body = self._from_json(body) - actions = {'cancel': self.cancel} - action_key = 'xferaction' - if action_key not in body: - msg = 'You must have an action entry in the body with one of ' \ - 'the following %s' % str(actions) - raise webob.exc.HTTPBadRequest(explanation=msg) - action = body[action_key] - if action not in actions: - msg = '%s is not a valid action.' % action - raise webob.exc.HTTPBadRequest(explanation=msg) - func = actions[action] - return func(body) - - -class XferSerializer(os_wsgi.JSONDictSerializer): - - def serialize(self, data, action='default', *args): - return super(XferSerializer, self).serialize(data, args[0]) - - def _xfer_to_json(self, data): - x = data - d = {} - d['id'] = x.id - d['source_url'] = x.srcurl - d['destination_url'] = x.dsturl - d['state'] = x.state - d['start_offset'] = x.start_ndx - d['end_offset'] = x.end_ndx - d['progress'] = x.next_ndx - d['source_options'] = x.source_opts - d['destination_options'] = x.dest_opts - return d - - def default(self, data): - d = self._xfer_to_json(data) - return json.dumps(d) - - def list(self, data): - xfer_list = [] - for xfer in data: - xfer_list.append(self._xfer_to_json(xfer)) - return json.dumps(xfer_list) - - -class API(os_wsgi.Router): - - def __init__(self, conf): - - self.conf = conf - self.db_con = db.StaccatoDB(conf) - - self.executor = executor.SimpleThreadExecutor(self.conf) - self.sm = xfer_events.XferStateMachine(self.executor) - - controller = XferController(self.db_con, self.sm, self.conf) - mapper = routes.Mapper() - - body_deserializers = {'application/json': XferDeserializer()} - deserializer = os_wsgi.RequestDeserializer( - body_deserializers=body_deserializers, - headers_deserializer=XferHeaderDeserializer()) - serializer = XferSerializer() - transfer_resource = os_wsgi.Resource(controller, - deserializer=deserializer, - serializer=serializer) - - mapper.connect('/transfers', - controller=transfer_resource, - action='newtransfer', - conditions={'method': ['POST']}) - mapper.connect('/transfers', - controller=transfer_resource, - action='list', - conditions={'method': ['GET']}) - mapper.connect('/transfers/{xfer_id}', - controller=transfer_resource, - action='status', - conditions={'method': ['GET']}) - mapper.connect('/transfers/{xfer_id}', - controller=transfer_resource, - action='delete', - conditions={'method': ['DELETE']}) - mapper.connect('/transfers/{xfer_id}/action', - controller=transfer_resource, - action='xferaction', - conditions={'method': ['POST']}) - - super(API, self).__init__(mapper) diff --git a/staccato/api/versions.py b/staccato/api/versions.py deleted file mode 100644 index 4577260..0000000 --- a/staccato/api/versions.py +++ /dev/null @@ -1,30 +0,0 @@ -import httplib -import json -import webob - -from staccato.common import config -import staccato.openstack.common.wsgi as os_wsgi - - -class VersionApp(object): - """ - A single WSGI application that just returns version information - """ - def __init__(self, conf): - self.conf = conf - - @webob.dec.wsgify(RequestClass=os_wsgi.Request) - def __call__(self, req): - version_info = {'id': self.conf.service_id, - 'version': self.conf.version, - 'status': 'active', - } - protocols = config.get_protocol_policy(self.conf).keys() - version_info['protocols'] = protocols - version_objs = {'v1': version_info} - - response = webob.Response(request=req, - status=httplib.MULTIPLE_CHOICES, - content_type='application/json') - response.body = json.dumps(dict(versions=version_objs)) - return response diff --git a/staccato/cmd/__init__.py b/staccato/cmd/__init__.py deleted file mode 100644 index c65c52d..0000000 --- a/staccato/cmd/__init__.py +++ /dev/null @@ -1,16 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 OpenStack LLC. -# All Rights Reserved. -# -# 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. diff --git a/staccato/cmd/api.py b/staccato/cmd/api.py deleted file mode 100755 index 7427a3c..0000000 --- a/staccato/cmd/api.py +++ /dev/null @@ -1,43 +0,0 @@ -import eventlet -import gettext -import sys - -from staccato.common import config -import staccato.openstack.common.log as log -import staccato.openstack.common.wsgi as os_wsgi -import staccato.openstack.common.pastedeploy as os_pastedeploy - -# Monkey patch socket and time -eventlet.patcher.monkey_patch(all=False, socket=True, time=True) - -gettext.install('staccato', unicode=1) - - -def fail(returncode, e): - sys.stderr.write("ERROR: %s\n" % e) - sys.exit(returncode) - - -def main(): - try: - conf = config.get_config_object() - - paste_file = conf.find_file(conf.paste_deploy.config_file) - if conf.paste_deploy.flavor is None: - flavor = 'staccato-api' - else: - flavor = 'staccato-api-' + conf.paste_deploy.flavor - - log.setup('staccato') - wsgi_app = os_pastedeploy.paste_deploy_app(paste_file, - flavor, - conf) - server = os_wsgi.Service(wsgi_app, conf.bind_port) - server.start() - server.wait() - except RuntimeError as e: - fail(1, e) - - -if __name__ == '__main__': - main() diff --git a/staccato/cmd/manage.py b/staccato/cmd/manage.py deleted file mode 100644 index a290de5..0000000 --- a/staccato/cmd/manage.py +++ /dev/null @@ -1,76 +0,0 @@ -""" -Staccato Management Utility -""" -import sys - -from oslo.config import cfg - -from staccato.common import config -import staccato.openstack.common.log as log -import staccato.db -import staccato.db.migration - - -def do_db_version(conf): - """Print database's current migration level""" - print staccato.db.migration.db_version(conf) - - -def do_upgrade(conf): - staccato.db.migration.upgrade(conf, conf.command.version) - - -def do_downgrade(conf): - staccato.db.migration.downgrade(conf, conf.command.version) - - -def do_version_control(conf): - staccato.db.migration.version_control(conf, conf.command.version) - - -def do_db_sync(conf): - staccato.db.migration.db_sync(conf, - conf.command.version, - conf.command.current_version) - - -def add_command_parsers(subparsers): - parser = subparsers.add_parser('db_version') - parser.set_defaults(func=do_db_version) - - parser = subparsers.add_parser('upgrade') - parser.set_defaults(func=do_upgrade) - parser.add_argument('version', nargs='?') - - parser = subparsers.add_parser('downgrade') - parser.set_defaults(func=do_downgrade) - parser.add_argument('version') - - parser = subparsers.add_parser('version_control') - parser.set_defaults(func=do_version_control) - parser.add_argument('version', nargs='?') - - parser = subparsers.add_parser('db_sync') - parser.set_defaults(func=do_db_sync) - parser.add_argument('version', nargs='?') - parser.add_argument('current_version', nargs='?') - -command_opt = cfg.SubCommandOpt('command', - title='Commands', - help='Available commands', - handler=add_command_parsers) - -cfg.CONF.register_cli_opt(command_opt) - - -def main(): - conf = config.get_config_object_no_parse() - conf.register_cli_opt(command_opt) - conf = config.parse_config_object(conf, skip_global=False) - - log.setup('staccato') - conf.command.func(conf) - - -if __name__ == '__main__': - main() diff --git a/staccato/cmd/scheduler.py b/staccato/cmd/scheduler.py deleted file mode 100644 index f61ca61..0000000 --- a/staccato/cmd/scheduler.py +++ /dev/null @@ -1,29 +0,0 @@ -import eventlet -import gettext -import sys - -from staccato.common import config -import staccato.openstack.common.log as log -import staccato.scheduler.interface as scheduler - -# Monkey patch socket and time -eventlet.patcher.monkey_patch(all=False, socket=True, time=True) - -gettext.install('staccato', unicode=1) - - -def fail(returncode, e): - sys.stderr.write("ERROR: %s\n" % e) - sys.exit(returncode) - - -def main(): - try: - conf = config.get_config_object() - log.setup('staccato') - - sched = scheduler.get_scheduler(conf) - sched.start() - sched.wait() - except RuntimeError as e: - fail(1, e) diff --git a/staccato/common/__init__.py b/staccato/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/common/config.py b/staccato/common/config.py deleted file mode 100644 index f44afb2..0000000 --- a/staccato/common/config.py +++ /dev/null @@ -1,124 +0,0 @@ -import json -import logging - -from oslo.config import cfg - -from staccato.version import version_info as version - -paste_deploy_opts = [ - cfg.StrOpt('flavor', - help=_('Partial name of a pipeline in your paste configuration ' - 'file with the service name removed. For example, if ' - 'your paste section name is ' - '[pipeline:staccato-api-keystone] use the value ' - '"keystone"')), - cfg.StrOpt('config_file', - help=_('Name of the paste configuration file.')), -] - -common_opts = [ - cfg.ListOpt('protocol_plugins', - default=['staccato.protocols.file.FileProtocol', - ]), - cfg.StrOpt('sql_connection', - default='sqlite:///staccato.sqlite', - secret=True, - metavar='CONNECTION', - help='A valid SQLAlchemy connection string for the registry ' - 'database. Default: %(default)s'), - cfg.IntOpt('sql_idle_timeout', default=3600, - help=_('Period in seconds after which SQLAlchemy should ' - 'reestablish its connection to the database.')), - cfg.IntOpt('sql_max_retries', default=60, - help=_('The number of times to retry a connection to the SQL' - 'server.')), - cfg.IntOpt('sql_retry_interval', default=1, - help=_('The amount of time to wait (in seconds) before ' - 'attempting to retry the SQL connection.')), - cfg.BoolOpt('db_auto_create', default=False, - help=_('A boolean that determines if the database will be ' - 'automatically created.')), - cfg.BoolOpt('db_auto_create', default=False, - help=_('A boolean that determines if the database will be ' - 'automatically created.')), - - cfg.StrOpt('log_level', - default='INFO', - help='', - dest='str_log_level'), - cfg.StrOpt('protocol_policy', default='staccato-protocols.json', - help=''), - cfg.StrOpt('service_id', default='staccato1234', - help=''), - cfg.StrOpt('admin_user_id', default='admin', - help='The user ID of the staccato admin'), -] - -bind_opts = [ - cfg.StrOpt('bind_host', default='0.0.0.0', - help=_('Address to bind the server. Useful when ' - 'selecting a particular network interface.')), - cfg.IntOpt('bind_port', - help=_('The port on which the server will listen.')), -] - - -def _log_string_to_val(conf): - str_lvl = conf.str_log_level.lower() - - val = logging.INFO - if str_lvl == 'error': - val = logging.ERROR - elif str_lvl == 'warn' or str_lvl == 'warning': - val = logging.WARN - elif str_lvl == "DEBUG": - val = logging.DEBUG - setattr(conf, 'log_level', val) - - -def get_config_object_no_parse(): - conf = cfg.ConfigOpts() - conf.register_opts(common_opts) - conf.register_opts(bind_opts) - conf.register_opts(paste_deploy_opts, group='paste_deploy') - return conf - - -def parse_config_object(conf, args=None, usage=None, - default_config_files=None, - skip_global=False): - conf(args=args, - project='staccato', - version=version.cached_version_string(), - usage=usage, - default_config_files=default_config_files) - _log_string_to_val(conf) - - # to make keystone client middleware work (massive bummer) - if not skip_global: - cfg.CONF(args=args, - project='staccato', - version=version.cached_version_string(), - usage=usage, - default_config_files=default_config_files) - - return conf - - -def get_config_object(args=None, usage=None, - default_config_files=None, - skip_global=False): - conf = get_config_object_no_parse() - conf = parse_config_object(conf, args=args, usage=usage, - default_config_files=default_config_files, - skip_global=skip_global) - return conf - - -def get_protocol_policy(conf): - protocol_conf_file = conf.protocol_policy - if protocol_conf_file is None: - return {} - proto_file = conf.find_file(protocol_conf_file) - policy = json.load(open(proto_file, 'r')) - return policy diff --git a/staccato/common/exceptions.py b/staccato/common/exceptions.py deleted file mode 100644 index 6d0f5d0..0000000 --- a/staccato/common/exceptions.py +++ /dev/null @@ -1,51 +0,0 @@ - - -class StaccatoBaseException(Exception): - pass - - -class StaccatoNotImplementedException(StaccatoBaseException): - pass - - -class StaccatoProtocolConnectionException(StaccatoBaseException): - pass - - -class StaccatoCancelException(StaccatoBaseException): - pass - - -class StaccatoIOException(StaccatoBaseException): - pass - - -class StaccatoParameterError(StaccatoBaseException): - pass - - -class StaccatoMisconfigurationException(StaccatoBaseException): - pass - - -class StaccatoDataBaseException(StaccatoBaseException): - pass - - -class StaccatoEventException(StaccatoBaseException): - pass - - -class StaccatoInvalidStateTransitionException(StaccatoEventException): - pass - - -class StaccatoDatabaseException(StaccatoBaseException): - pass - - -class StaccatoNotFoundInDBException(StaccatoDataBaseException): - - def __init__(self, ex, unfound_item): - super(StaccatoNotFoundInDBException, self).__init__(self, ex) - self.unfound_item = unfound_item diff --git a/staccato/common/state_machine.py b/staccato/common/state_machine.py deleted file mode 100644 index e543478..0000000 --- a/staccato/common/state_machine.py +++ /dev/null @@ -1,80 +0,0 @@ -from staccato.common import exceptions - - -class StateMachine(object): - - def __init__(self): - # set up the transition table - self._transitions = {} - self._state_funcs = {} - self._state_observer_funcs = {} - - def set_state_func(self, state, func): - self._state_funcs[state] = func - - def set_state_observer(self, state, func): - if state not in self._state_observer_funcs: - self._state_observer_funcs[state] = [] - self._state_observer_funcs[state].append(func) - - def set_mapping(self, state, event, next_state, func=None): - if state not in self._transitions: - self._transitions[state] = {} - - event_dict = self._transitions[state] - if event not in event_dict: - event_dict[event] = {} - - if func is None: - func = self._state_funcs[next_state] - - self._transitions[state][event] = (next_state, func) - - def _state_changed(self, current_state, event, new_state, **kwvals): - raise Exception("this needs to be implemented") - - def _get_current_state(self, **kwvals): - raise Exception("This needs to be implemented") - - def event_occurred(self, event, **kwvals): - - current_state = self._get_current_state(**kwvals) - if current_state not in self._transitions: - raise exceptions.StaccatoInvalidStateTransitionException( - "Undefined event %s at state %s" % (event, current_state)) - state_ent = self._transitions[current_state] - if event not in state_ent: - raise exceptions.StaccatoInvalidStateTransitionException( - "Undefined event %s at state %s" % (event, current_state)) - - next_state, function = state_ent[event] - - self._state_changed(current_state, event, next_state, **kwvals) - # call all observors. They are not allowed to effect state change - for f in self._state_observer_funcs: - try: - f(current_state, event, next_state, **kwvals) - except Exception, ex: - raise - # log the change - if function: - try: - function(current_state, event, next_state, **kwvals) - except Exception, ex: - # TODO: deal with the exception in a sane way. we likely need - # to trigger an event signifying and error occured but we - # may not want to recurse - raise - - def mapping_to_digraph(self): - print 'digraph {' - for start_state in self._transitions: - for event in self._transitions[start_state]: - ent = self._transitions[start_state][event] - if ent is not None: - p_end_state = ent[0].replace("STATE_", '') - p_start_state = start_state.replace("STATE_", '') - p_event = event.replace("EVENT_", '') - print '%s -> %s [ label = "%s" ];'\ - % (p_start_state, p_end_state, p_event) - print '}' diff --git a/staccato/common/utils.py b/staccato/common/utils.py deleted file mode 100644 index 647ae7f..0000000 --- a/staccato/common/utils.py +++ /dev/null @@ -1,112 +0,0 @@ -import logging -import re - -from paste import deploy -import webob -import webob.exc - -from staccato.common import exceptions -from staccato.openstack.common import importutils - - -def not_implemented_decorator(func): - def call(self, *args, **kwargs): - def raise_error(func): - raise exceptions.StaccatoNotImplementedException( - "function %s must be implemented" % (func.func_name)) - return raise_error(func) - return call - - -class StaccatoErrorToHTTP(object): - - def __init__(self, operation, log): - self.operation = operation - self.log = log - - def __call__(self, func): - def inner(*args, **kwargs): - try: - return func(*args, **kwargs) - except exceptions.StaccatoNotFoundInDBException as ex: - msg = _("Failed to %s. %s not found.") % (self.operation, - ex.unfound_item) - self.log.error(msg) - raise webob.exc.HTTPNotFound(explanation=msg, - content_type="text/plain") - - except exceptions.StaccatoInvalidStateTransitionException, ex: - msg = _('Failed to %s. You cannot %s a transfer that is in ' - 'the %s state. %s' % (self.operation, - ex.attempted_event, - ex.current_state, - ex)) - self.log.error(msg) - raise webob.exc.HTTPBadRequest(explanation=msg, - content_type="text/plain") - - except exceptions.StaccatoParameterError as ex: - msg = _('Failed to %s. %s' % (self.operation, ex)) - self.log.error(msg) - raise webob.exc.HTTPBadRequest(msg) - - except Exception as ex: - msg = _('Failed to %s. %s' % (self.operation, ex)) - self.log.error(msg) - raise webob.exc.HTTPBadRequest(msg) - return inner - - -def load_paste_app(app_name, conf_file, conf): - try: - logger = logging.getLogger(__name__) - logger.debug(_("Loading %(app_name)s from %(conf_file)s"), - {'conf_file': conf_file, 'app_name': app_name}) - - app = deploy.loadapp("config:%s" % conf_file, - name=app_name, - global_conf={'CONF': conf}) - - return app - except (LookupError, ImportError) as e: - msg = _("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r") % locals() - logger.error(msg) - raise RuntimeError(msg) - - -def find_protocol_module_name(lookup_dict, url_parts): - if url_parts.scheme not in lookup_dict: - raise exceptions.StaccatoParameterError( - '%s protocol not found' % url_parts.scheme) - p_list = lookup_dict[url_parts.scheme] - - for entry in p_list: - match_keys = ['netloc', 'path', 'params', 'query'] - ndx = 0 - found = True - for k in match_keys: - ndx = ndx + 1 - if k in entry: - needle = url_parts[ndx] - haystack = entry[k] - found = re.match(haystack, needle) - if found: - return entry['module'] - - raise exceptions.StaccatoParameterError( - 'The url %s is not supported' % url_parts.geturl()) - - -def load_protocol_module(module_name, CONF): - try: - protocol_cls = importutils.import_class(module_name) - except ImportError, ie: - raise exceptions.StaccatoParameterError( - "The protocol module %s could not be loaded. %s" % - (module_name, ie)) - - protocol_instance = protocol_cls(CONF) - - return protocol_instance diff --git a/staccato/db/__init__.py b/staccato/db/__init__.py deleted file mode 100644 index 9b0a422..0000000 --- a/staccato/db/__init__.py +++ /dev/null @@ -1,245 +0,0 @@ -import time - -import sqlalchemy -import sqlalchemy.orm as sa_orm -import sqlalchemy.orm.exc as orm_exc -import sqlalchemy.sql.expression as sql_expression - -from staccato.db import migration, models -import staccato.openstack.common.log as os_logging -from staccato.common import exceptions -from staccato.db import models -import staccato.xfer.constants as constants - - -LOG = os_logging.getLogger(__name__) - - -class StaccatoDB(object): - - def __init__(self, CONF, autocommit=True, expire_on_commit=False): - self.CONF = CONF - self.engine = _get_db_object(CONF) - self.maker = sa_orm.sessionmaker(bind=self.engine, - autocommit=autocommit, - expire_on_commit=expire_on_commit) - - def get_sessions(self): - return self.maker() - - def get_new_xfer(self, - owner, - srcurl, - dsturl, - src_module_name, - dst_module_name, - start_ndx=0, - end_ndx=-1, - source_opts=None, - dest_opts=None, - session=None): - - if session is None: - session = self.get_sessions() - - with session.begin(): - xfer_request = models.XferRequest() - xfer_request.owner = owner - xfer_request.srcurl = srcurl - xfer_request.dsturl = dsturl - xfer_request.src_module_name = src_module_name - xfer_request.dst_module_name = dst_module_name - xfer_request.start_ndx = start_ndx - xfer_request.next_ndx = start_ndx - xfer_request.end_ndx = end_ndx - xfer_request.dest_opts = dest_opts - xfer_request.source_opts = source_opts - xfer_request.state = "STATE_NEW" - - session.add(xfer_request) - session.flush() - - return xfer_request - - def save_db_obj(self, db_obj, session=None): - if session is None: - session = self.get_sessions() - - with session.begin(): - session.add(db_obj) - session.flush() - - def lookup_xfer_request_by_id(self, xfer_id, owner=None, session=None): - try: - if session is None: - session = self.get_sessions() - - with session.begin(): - query = session.query(models.XferRequest) - if owner is not None: - query = query.filter(sql_expression.and_( - models.XferRequest.owner == owner, - models.XferRequest.id == xfer_id)) - else: - query = query.filter(models.XferRequest.id == xfer_id) - xfer_request = query.one() - return xfer_request - except orm_exc.NoResultFound, nf_ex: - raise exceptions.StaccatoNotFoundInDBException(nf_ex, xfer_id) - except Exception, ex: - raise exceptions.StaccatoDataBaseException(ex) - - def lookup_xfer_request_all(self, owner=None, session=None, limit=None): - try: - if session is None: - session = self.get_sessions() - - with session.begin(): - query = session.query(models.XferRequest) - if owner is not None: - query = query.filter(models.XferRequest.owner == owner) - if limit is not None: - query = query.limit(limit) - xfer_requests = query.all() - return xfer_requests - except orm_exc.NoResultFound, nf_ex: - raise exceptions.StaccatoNotFoundInDBException(nf_ex, owner) - except Exception, ex: - raise exceptions.StaccatoDataBaseException(ex) - - def get_all_ready(self, owner=None, limit=None, session=None): - if session is None: - session = self.get_sessions() - - with session.begin(): - query = session.query(models.XferRequest) - if owner is not None: - query = query.filter(sql_expression.and_( - models.XferRequest.owner == owner, - sql_expression.or_( - models.XferRequest.state == constants.States.STATE_NEW, - models.XferRequest.state == - constants.States.STATE_ERROR))) - else: - query = query.filter(sql_expression.or_( - models.XferRequest.state == constants.States.STATE_NEW, - models.XferRequest.state == constants.States.STATE_ERROR)) - - if limit is not None: - query = query.limit(limit) - xfer_requests = query.all() - return xfer_requests - - def get_all_running(self, owner=None, limit=None, session=None): - if session is None: - session = self.get_sessions() - - with session.begin(): - query = session.query(models.XferRequest) - if owner is not None: - query = query.filter(sql_expression.and_( - models.XferRequest.owner == owner, - models.XferRequest.state == - constants.States.STATE_RUNNING)) - else: - query = query.filter( - models.XferRequest.state == constants.States.STATE_RUNNING) - if limit is not None: - query = query.limit(limit) - xfer_requests = query.all() - return xfer_requests - - def get_xfer_requests(self, ids, owner=None, session=None): - if session is None: - session = self.get_sessions() - - with session.begin(): - query = session.query(models.XferRequest) - if owner is not None: - query = query.filter( - sql_expression.and_(models.XferRequest.owner == owner, - models.XferRequest.id.in_(ids))) - else: - query = query.filter(models.XferRequest.id.in_(ids)) - xfer_requests = query.all() - return xfer_requests - - def delete_db_obj(self, db_obj, session=None): - if session is None: - session = self.get_sessions() - - with session.begin(): - session.delete(db_obj) - session.flush() - - -def _get_db_object(CONF): - sqlalchemy.engine.url.make_url(CONF.sql_connection) - engine_args = { - 'pool_recycle': CONF.sql_idle_timeout, - 'echo': False, - 'convert_unicode': True} - - try: - engine = sqlalchemy.create_engine(CONF.sql_connection, **engine_args) - engine.connect = wrap_db_error(engine.connect, CONF) - engine.connect() - except Exception as err: - msg = _("Error configuring registry database with supplied " - "sql_connection '%s'. " - "Got error:\n%s") % (CONF.sql_connection, err) - LOG.error(msg) - raise - - if CONF.db_auto_create: - LOG.info(_('auto-creating staccato registry DB')) - models.register_models(engine) - try: - migration.version_control(CONF) - except exceptions.StaccatoDataBaseException: - # only arises when the DB exists and is under version control - pass - else: - LOG.info(_('not auto-creating staccato registry DB')) - - return engine - - -def is_db_connection_error(args): - """Return True if error in connecting to db.""" - # NOTE(adam_g): This is currently MySQL specific and needs to be extended - # to support Postgres and others. - conn_err_codes = ('2002', '2003', '2006') - for err_code in conn_err_codes: - if args.find(err_code) != -1: - return True - return False - - -def wrap_db_error(f, CONF): - """Retry DB connection. Copied from nova and modified.""" - def _wrap(*args, **kwargs): - try: - return f(*args, **kwargs) - except sqlalchemy.exc.OperationalError, e: - if not is_db_connection_error(e.args[0]): - raise - - remaining_attempts = CONF.sql_max_retries - while True: - LOG.warning(_('SQL connection failed. %d attempts left.'), - remaining_attempts) - remaining_attempts -= 1 - time.sleep(CONF.sql_retry_interval) - try: - return f(*args, **kwargs) - except sqlalchemy.exc.OperationalError, e: - if (remaining_attempts == 0 or - not is_db_connection_error(e.args[0])): - raise - except sqlalchemy.exc.DBAPIError: - raise - except sqlalchemy.exc.DBAPIError: - raise - _wrap.func_name = f.func_name - return _wrap diff --git a/staccato/db/migrate_repo/__init__.py b/staccato/db/migrate_repo/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/db/migrate_repo/migrate.cfg b/staccato/db/migrate_repo/migrate.cfg deleted file mode 100644 index 41885ed..0000000 --- a/staccato/db/migrate_repo/migrate.cfg +++ /dev/null @@ -1,20 +0,0 @@ -[db_settings] -# Used to identify which repository this database is versioned under. -# You can use the name of your project. -repository_id=Staccato Migrations - -# The name of the database table used to track the schema version. -# This name shouldn't already be used by your project. -# If this is changed once a database is under version control, you'll need to -# change the table name in each database too. -version_table=migrate_version - -# When committing a change script, Migrate will attempt to generate the -# sql for all supported databases; normally, if one of them fails - probably -# because you don't have that database installed - it is ignored and the -# commit continues, perhaps ending successfully. -# Databases in this list MUST compile successfully during a commit, or the -# entire commit will fail. List the databases your application will actually -# be using to ensure your updates to that database work properly. -# This must be a list; example: ['postgres','sqlite'] -required_dbs=[] diff --git a/staccato/db/migrate_repo/versions/001_placeholder.py b/staccato/db/migrate_repo/versions/001_placeholder.py deleted file mode 100644 index 463e02c..0000000 --- a/staccato/db/migrate_repo/versions/001_placeholder.py +++ /dev/null @@ -1,6 +0,0 @@ -def upgrade(migrate_engine): - pass - - -def downgrade(migration_engine): - pass diff --git a/staccato/db/migrate_repo/versions/__init__.py b/staccato/db/migrate_repo/versions/__init__.py deleted file mode 100644 index 7fcc62c..0000000 --- a/staccato/db/migrate_repo/versions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'jbresnah' diff --git a/staccato/db/migration.py b/staccato/db/migration.py deleted file mode 100644 index 3985261..0000000 --- a/staccato/db/migration.py +++ /dev/null @@ -1,117 +0,0 @@ -import os - -from migrate.versioning import api as versioning_api -try: - from migrate.versioning import exceptions as versioning_exceptions -except ImportError: - from migrate import exceptions as versioning_exceptions -from migrate.versioning import repository as versioning_repository - -from staccato.common import exceptions -import staccato.openstack.common.log as logging - -LOG = logging.getLogger(__name__) - - -def db_version(CONF): - """ - Return the database's current migration number - - :retval version number - """ - repo_path = get_migrate_repo_path() - sql_connection = CONF.sql_connection - try: - return versioning_api.db_version(sql_connection, repo_path) - except versioning_exceptions.DatabaseNotControlledError, e: - msg = (_("database '%(sql_connection)s' is not under " - "migration control") % locals()) - raise exceptions.StaccatoDataBaseException(msg) - - -def upgrade(CONF, version=None): - """ - Upgrade the database's current migration level - - :param version: version to upgrade (defaults to latest) - :retval version number - """ - db_version(CONF) # Ensure db is under migration control - repo_path = get_migrate_repo_path() - sql_connection = CONF.sql_connection - version_str = version or 'latest' - LOG.info(_("Upgrading %(sql_connection)s to version %(version_str)s") % - locals()) - return versioning_api.upgrade(sql_connection, repo_path, version) - - -def downgrade(CONF, version): - """ - Downgrade the database's current migration level - - :param version: version to downgrade to - :retval version number - """ - db_version() # Ensure db is under migration control - repo_path = get_migrate_repo_path() - sql_connection = CONF.sql_connection - LOG.info(_("Downgrading %(sql_connection)s to version %(version)s") % - locals()) - return versioning_api.downgrade(sql_connection, repo_path, version) - - -def version_control(CONF, version=None): - """ - Place a database under migration control - """ - sql_connection = CONF.sql_connection - try: - _version_control(CONF, version) - except versioning_exceptions.DatabaseAlreadyControlledError, e: - msg = (_("database '%(sql_connection)s' is already under migration " - "control") % locals()) - raise exceptions.StaccatoDataBaseException(msg) - - -def _version_control(CONF, version): - """ - Place a database under migration control - - This will only set the specific version of a database, it won't - run any migrations. - """ - repo_path = get_migrate_repo_path() - sql_connection = CONF.sql_connection - if version is None: - version = versioning_repository.Repository(repo_path).latest - return versioning_api.version_control(sql_connection, repo_path, version) - - -def db_sync(CONF, version=None, current_version=None): - """ - Place a database under migration control and perform an upgrade - - :retval version number - """ - sql_connection = CONF.sql_connection - try: - _version_control(CONF, current_version or 0) - except versioning_exceptions.DatabaseAlreadyControlledError, e: - pass - - if current_version is None: - current_version = int(db_version(CONF)) - if version is not None and int(version) < current_version: - downgrade(CONF, version=version) - elif version is None or int(version) > current_version: - upgrade(CONF, version=version) - - -def get_migrate_repo_path(): - """Get the path for the migrate repository.""" - path = os.path.join(os.path.abspath(os.path.dirname(__file__)), - 'migrate_repo') - if not os.path.exists(path): - raise exceptions.StaccatoMisconfigurationException( - "The path % should exist." % path) - return path diff --git a/staccato/db/models.py b/staccato/db/models.py deleted file mode 100644 index 9c3d205..0000000 --- a/staccato/db/models.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -SQLAlchemy models for staccato data -""" - -from sqlalchemy import Column -from sqlalchemy import DateTime -from sqlalchemy import Integer -from sqlalchemy import PickleType -from sqlalchemy import String -from sqlalchemy.ext.declarative import declarative_base - -from staccato.openstack.common import timeutils -from staccato.openstack.common import uuidutils - - -BASE = declarative_base() - - -class ModelBase(object): - """Base class for Nova and Glance Models""" - __table_args__ = {'mysql_engine': 'InnoDB'} - __table_initialized__ = False - __protected_attributes__ = set([ - "created_at", "updated_at"]) - - created_at = Column(DateTime, default=timeutils.utcnow, - nullable=False) - updated_at = Column(DateTime, default=timeutils.utcnow, - nullable=False, onupdate=timeutils.utcnow) - - -class XferRequest(BASE, ModelBase): - __tablename__ = 'xfer_requests' - - id = Column(String(36), primary_key=True, default=uuidutils.generate_uuid) - srcurl = Column(String(2048), nullable=False) - dsturl = Column(String(2048), nullable=False) - owner = Column(String(128), nullable=False) - src_module_name = Column(String(512), nullable=False) - dst_module_name = Column(String(512), nullable=False) - state = Column(Integer(), nullable=False) - start_ndx = Column(Integer(), nullable=False, default=0) - next_ndx = Column(Integer(), nullable=False) - end_ndx = Column(Integer(), nullable=False, default=-1) - # TODO add protocol specific json documents - source_opts = Column(PickleType()) - dest_opts = Column(PickleType()) - executor_uuid = Column(String(512), nullable=True) - - -def register_models(engine): - models = (XferRequest,) - for model in models: - model.metadata.create_all(engine) diff --git a/staccato/openstack/__init__.py b/staccato/openstack/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/openstack/common/__init__.py b/staccato/openstack/common/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/openstack/common/context.py b/staccato/openstack/common/context.py deleted file mode 100644 index f1e3f96..0000000 --- a/staccato/openstack/common/context.py +++ /dev/null @@ -1,82 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Simple class that stores security context information in the web request. - -Projects should subclass this class if they wish to enhance the request -context or provide additional information in their specific WSGI pipeline. -""" - -import itertools - -from staccato.openstack.common import uuidutils - - -def generate_request_id(): - return 'req-%s' % uuidutils.generate_uuid() - - -class RequestContext(object): - - """ - Stores information about the security context under which the user - accesses the system, as well as additional request information. - """ - - def __init__(self, auth_token=None, user=None, tenant=None, is_admin=False, - read_only=False, show_deleted=False, request_id=None): - self.auth_token = auth_token - self.user = user - self.tenant = tenant - self.is_admin = is_admin - self.read_only = read_only - self.show_deleted = show_deleted - if not request_id: - request_id = generate_request_id() - self.request_id = request_id - - def to_dict(self): - return {'user': self.user, - 'tenant': self.tenant, - 'is_admin': self.is_admin, - 'read_only': self.read_only, - 'show_deleted': self.show_deleted, - 'auth_token': self.auth_token, - 'request_id': self.request_id} - - -def get_admin_context(show_deleted="no"): - context = RequestContext(None, - tenant=None, - is_admin=True, - show_deleted=show_deleted) - return context - - -def get_context_from_function_and_args(function, args, kwargs): - """Find an arg of type RequestContext and return it. - - This is useful in a couple of decorators where we don't - know much about the function we're wrapping. - """ - - for arg in itertools.chain(kwargs.values(), args): - if isinstance(arg, RequestContext): - return arg - - return None diff --git a/staccato/openstack/common/eventlet_backdoor.py b/staccato/openstack/common/eventlet_backdoor.py deleted file mode 100644 index 57b89ae..0000000 --- a/staccato/openstack/common/eventlet_backdoor.py +++ /dev/null @@ -1,89 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 OpenStack Foundation. -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -from __future__ import print_function - -import gc -import pprint -import sys -import traceback - -import eventlet -import eventlet.backdoor -import greenlet -from oslo.config import cfg - -eventlet_backdoor_opts = [ - cfg.IntOpt('backdoor_port', - default=None, - help='port for eventlet backdoor to listen') -] - -CONF = cfg.CONF -CONF.register_opts(eventlet_backdoor_opts) - - -def _dont_use_this(): - print("Don't use this, just disconnect instead") - - -def _find_objects(t): - return filter(lambda o: isinstance(o, t), gc.get_objects()) - - -def _print_greenthreads(): - for i, gt in enumerate(_find_objects(greenlet.greenlet)): - print(i, gt) - traceback.print_stack(gt.gr_frame) - print() - - -def _print_nativethreads(): - for threadId, stack in sys._current_frames().items(): - print(threadId) - traceback.print_stack(stack) - print() - - -def initialize_if_enabled(): - backdoor_locals = { - 'exit': _dont_use_this, # So we don't exit the entire process - 'quit': _dont_use_this, # So we don't exit the entire process - 'fo': _find_objects, - 'pgt': _print_greenthreads, - 'pnt': _print_nativethreads, - } - - if CONF.backdoor_port is None: - return None - - # NOTE(johannes): The standard sys.displayhook will print the value of - # the last expression and set it to __builtin__._, which overwrites - # the __builtin__._ that gettext sets. Let's switch to using pprint - # since it won't interact poorly with gettext, and it's easier to - # read the output too. - def displayhook(val): - if val is not None: - pprint.pprint(val) - sys.displayhook = displayhook - - sock = eventlet.listen(('localhost', CONF.backdoor_port)) - port = sock.getsockname()[1] - eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock, - locals=backdoor_locals) - return port diff --git a/staccato/openstack/common/exception.py b/staccato/openstack/common/exception.py deleted file mode 100644 index d6731de..0000000 --- a/staccato/openstack/common/exception.py +++ /dev/null @@ -1,142 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Exceptions common to OpenStack projects -""" - -import logging - -from staccato.openstack.common.gettextutils import _ - -_FATAL_EXCEPTION_FORMAT_ERRORS = False - - -class Error(Exception): - def __init__(self, message=None): - super(Error, self).__init__(message) - - -class ApiError(Error): - def __init__(self, message='Unknown', code='Unknown'): - self.message = message - self.code = code - super(ApiError, self).__init__('%s: %s' % (code, message)) - - -class NotFound(Error): - pass - - -class UnknownScheme(Error): - - msg = "Unknown scheme '%s' found in URI" - - def __init__(self, scheme): - msg = self.__class__.msg % scheme - super(UnknownScheme, self).__init__(msg) - - -class BadStoreUri(Error): - - msg = "The Store URI %s was malformed. Reason: %s" - - def __init__(self, uri, reason): - msg = self.__class__.msg % (uri, reason) - super(BadStoreUri, self).__init__(msg) - - -class Duplicate(Error): - pass - - -class NotAuthorized(Error): - pass - - -class NotEmpty(Error): - pass - - -class Invalid(Error): - pass - - -class BadInputError(Exception): - """Error resulting from a client sending bad input to a server""" - pass - - -class MissingArgumentError(Error): - pass - - -class DatabaseMigrationError(Error): - pass - - -class ClientConnectionError(Exception): - """Error resulting from a client connecting to a server""" - pass - - -def wrap_exception(f): - def _wrap(*args, **kw): - try: - return f(*args, **kw) - except Exception as e: - if not isinstance(e, Error): - #exc_type, exc_value, exc_traceback = sys.exc_info() - logging.exception(_('Uncaught exception')) - #logging.error(traceback.extract_stack(exc_traceback)) - raise Error(str(e)) - raise - _wrap.func_name = f.func_name - return _wrap - - -class OpenstackException(Exception): - """ - Base Exception - - To correctly use this class, inherit from it and define - a 'message' property. That message will get printf'd - with the keyword arguments provided to the constructor. - """ - message = "An unknown exception occurred" - - def __init__(self, **kwargs): - try: - self._error_string = self.message % kwargs - - except Exception as e: - if _FATAL_EXCEPTION_FORMAT_ERRORS: - raise e - else: - # at least get the core message out if something happened - self._error_string = self.message - - def __str__(self): - return self._error_string - - -class MalformedRequestBody(OpenstackException): - message = "Malformed message body: %(reason)s" - - -class InvalidContentType(OpenstackException): - message = "Invalid content type %(content_type)s" diff --git a/staccato/openstack/common/excutils.py b/staccato/openstack/common/excutils.py deleted file mode 100644 index b48a73f..0000000 --- a/staccato/openstack/common/excutils.py +++ /dev/null @@ -1,51 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# Copyright 2012, Red Hat, 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. - -""" -Exception related utilities. -""" - -import contextlib -import logging -import sys -import traceback - -from staccato.openstack.common.gettextutils import _ - - -@contextlib.contextmanager -def save_and_reraise_exception(): - """Save current exception, run some code and then re-raise. - - In some cases the exception context can be cleared, resulting in None - being attempted to be re-raised after an exception handler is run. This - can happen when eventlet switches greenthreads or when running an - exception handler, code raises and catches an exception. In both - cases the exception context will be cleared. - - To work around this, we save the exception state, run handler code, and - then re-raise the original exception. If another exception occurs, the - saved exception is logged and the new exception is re-raised. - """ - type_, value, tb = sys.exc_info() - try: - yield - except Exception: - logging.error(_('Original exception being dropped: %s'), - traceback.format_exception(type_, value, tb)) - raise - raise type_, value, tb diff --git a/staccato/openstack/common/gettextutils.py b/staccato/openstack/common/gettextutils.py deleted file mode 100644 index af7ad2d..0000000 --- a/staccato/openstack/common/gettextutils.py +++ /dev/null @@ -1,50 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, Inc. -# All Rights Reserved. -# -# 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. - -""" -gettext for openstack-common modules. - -Usual usage in an openstack.common module: - - from staccato.openstack.common.gettextutils import _ -""" - -import gettext -import os - -_localedir = os.environ.get('staccato'.upper() + '_LOCALEDIR') -_t = gettext.translation('staccato', localedir=_localedir, fallback=True) - - -def _(msg): - return _t.ugettext(msg) - - -def install(domain): - """Install a _() function using the given translation domain. - - Given a translation domain, install a _() function using gettext's - install() function. - - The main difference from gettext.install() is that we allow - overriding the default localedir (e.g. /usr/share/locale) using - a translation-domain-specific environment variable (e.g. - NOVA_LOCALEDIR). - """ - gettext.install(domain, - localedir=os.environ.get(domain.upper() + '_LOCALEDIR'), - unicode=True) diff --git a/staccato/openstack/common/importutils.py b/staccato/openstack/common/importutils.py deleted file mode 100644 index 3bd277f..0000000 --- a/staccato/openstack/common/importutils.py +++ /dev/null @@ -1,67 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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 related utilities and helper functions. -""" - -import sys -import traceback - - -def import_class(import_str): - """Returns a class from a string including module and class""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ValueError, AttributeError): - raise ImportError('Class %s cannot be found (%s)' % - (class_str, - traceback.format_exception(*sys.exc_info()))) - - -def import_object(import_str, *args, **kwargs): - """Import a class and return an instance of it.""" - return import_class(import_str)(*args, **kwargs) - - -def import_object_ns(name_space, import_str, *args, **kwargs): - """ - Import a class and return an instance of it, first by trying - to find the class in a default namespace, then failing back to - a full path if not found in the default namespace. - """ - import_value = "%s.%s" % (name_space, import_str) - try: - return import_class(import_value)(*args, **kwargs) - except ImportError: - return import_class(import_str)(*args, **kwargs) - - -def import_module(import_str): - """Import a module.""" - __import__(import_str) - return sys.modules[import_str] - - -def try_import(import_str, default=None): - """Try to import a module and if it fails return default.""" - try: - return import_module(import_str) - except ImportError: - return default diff --git a/staccato/openstack/common/jsonutils.py b/staccato/openstack/common/jsonutils.py deleted file mode 100644 index 06df677..0000000 --- a/staccato/openstack/common/jsonutils.py +++ /dev/null @@ -1,169 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. - -''' -JSON related utilities. - -This module provides a few things: - - 1) A handy function for getting an object down to something that can be - JSON serialized. See to_primitive(). - - 2) Wrappers around loads() and dumps(). The dumps() wrapper will - automatically use to_primitive() for you if needed. - - 3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson - is available. -''' - - -import datetime -import functools -import inspect -import itertools -import json -import types -import xmlrpclib - -import six - -from staccato.openstack.common import timeutils - - -_nasty_type_tests = [inspect.ismodule, inspect.isclass, inspect.ismethod, - inspect.isfunction, inspect.isgeneratorfunction, - inspect.isgenerator, inspect.istraceback, inspect.isframe, - inspect.iscode, inspect.isbuiltin, inspect.isroutine, - inspect.isabstract] - -_simple_types = (types.NoneType, int, basestring, bool, float, long) - - -def to_primitive(value, convert_instances=False, convert_datetime=True, - level=0, max_depth=3): - """Convert a complex object into primitives. - - Handy for JSON serialization. We can optionally handle instances, - but since this is a recursive function, we could have cyclical - data structures. - - To handle cyclical data structures we could track the actual objects - visited in a set, but not all objects are hashable. Instead we just - track the depth of the object inspections and don't go too deep. - - Therefore, convert_instances=True is lossy ... be aware. - - """ - # handle obvious types first - order of basic types determined by running - # full tests on nova project, resulting in the following counts: - # 572754 - # 460353 - # 379632 - # 274610 - # 199918 - # 114200 - # 51817 - # 26164 - # 6491 - # 283 - # 19 - if isinstance(value, _simple_types): - return value - - if isinstance(value, datetime.datetime): - if convert_datetime: - return timeutils.strtime(value) - else: - return value - - # value of itertools.count doesn't get caught by nasty_type_tests - # and results in infinite loop when list(value) is called. - if type(value) == itertools.count: - return six.text_type(value) - - # FIXME(vish): Workaround for LP bug 852095. Without this workaround, - # tests that raise an exception in a mocked method that - # has a @wrap_exception with a notifier will fail. If - # we up the dependency to 0.5.4 (when it is released) we - # can remove this workaround. - if getattr(value, '__module__', None) == 'mox': - return 'mock' - - if level > max_depth: - return '?' - - # The try block may not be necessary after the class check above, - # but just in case ... - try: - recursive = functools.partial(to_primitive, - convert_instances=convert_instances, - convert_datetime=convert_datetime, - level=level, - max_depth=max_depth) - if isinstance(value, dict): - return dict((k, recursive(v)) for k, v in value.iteritems()) - elif isinstance(value, (list, tuple)): - return [recursive(lv) for lv in value] - - # It's not clear why xmlrpclib created their own DateTime type, but - # for our purposes, make it a datetime type which is explicitly - # handled - if isinstance(value, xmlrpclib.DateTime): - value = datetime.datetime(*tuple(value.timetuple())[:6]) - - if convert_datetime and isinstance(value, datetime.datetime): - return timeutils.strtime(value) - elif hasattr(value, 'iteritems'): - return recursive(dict(value.iteritems()), level=level + 1) - elif hasattr(value, '__iter__'): - return recursive(list(value)) - elif convert_instances and hasattr(value, '__dict__'): - # Likely an instance of something. Watch for cycles. - # Ignore class member vars. - return recursive(value.__dict__, level=level + 1) - else: - if any(test(value) for test in _nasty_type_tests): - return six.text_type(value) - return value - except TypeError: - # Class objects are tricky since they may define something like - # __iter__ defined but it isn't callable as list(). - return six.text_type(value) - - -def dumps(value, default=to_primitive, **kwargs): - return json.dumps(value, default=default, **kwargs) - - -def loads(s): - return json.loads(s) - - -def load(s): - return json.load(s) - - -try: - import anyjson -except ImportError: - pass -else: - anyjson._modules.append((__name__, 'dumps', TypeError, - 'loads', ValueError, 'load')) - anyjson.force_implementation(__name__) diff --git a/staccato/openstack/common/local.py b/staccato/openstack/common/local.py deleted file mode 100644 index f1bfc82..0000000 --- a/staccato/openstack/common/local.py +++ /dev/null @@ -1,48 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -"""Greenthread local storage of variables using weak references""" - -import weakref - -from eventlet import corolocal - - -class WeakLocal(corolocal.local): - def __getattribute__(self, attr): - rval = corolocal.local.__getattribute__(self, attr) - if rval: - # NOTE(mikal): this bit is confusing. What is stored is a weak - # reference, not the value itself. We therefore need to lookup - # the weak reference and return the inner value here. - rval = rval() - return rval - - def __setattr__(self, attr, value): - value = weakref.ref(value) - return corolocal.local.__setattr__(self, attr, value) - - -# NOTE(mikal): the name "store" should be deprecated in the future -store = WeakLocal() - -# A "weak" store uses weak references and allows an object to fall out of scope -# when it falls out of scope in the code that uses the thread local storage. A -# "strong" store will hold a reference to the object so that it never falls out -# of scope. -weak_store = WeakLocal() -strong_store = corolocal.local diff --git a/staccato/openstack/common/log.py b/staccato/openstack/common/log.py deleted file mode 100644 index 9eb6f91..0000000 --- a/staccato/openstack/common/log.py +++ /dev/null @@ -1,558 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# -# 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. - -"""Openstack logging handler. - -This module adds to logging functionality by adding the option to specify -a context object when calling the various log methods. If the context object -is not specified, default formatting is used. Additionally, an instance uuid -may be passed as part of the log message, which is intended to make it easier -for admins to find messages related to a specific instance. - -It also allows setting of formatting information through conf. - -""" - -import ConfigParser -import cStringIO -import inspect -import itertools -import logging -import logging.config -import logging.handlers -import os -import sys -import traceback - -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import jsonutils -from staccato.openstack.common import local - - -_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S" - -common_cli_opts = [ - cfg.BoolOpt('debug', - short='d', - default=False, - help='Print debugging output (set logging level to ' - 'DEBUG instead of default WARNING level).'), - cfg.BoolOpt('verbose', - short='v', - default=False, - help='Print more verbose output (set logging level to ' - 'INFO instead of default WARNING level).'), -] - -logging_cli_opts = [ - cfg.StrOpt('log-config', - metavar='PATH', - help='If this option is specified, the logging configuration ' - 'file specified is used and overrides any other logging ' - 'options specified. Please see the Python logging module ' - 'documentation for details on logging configuration ' - 'files.'), - cfg.StrOpt('log-format', - default=None, - metavar='FORMAT', - help='A logging.Formatter log message format string which may ' - 'use any of the available logging.LogRecord attributes. ' - 'This option is deprecated. Please use ' - 'logging_context_format_string and ' - 'logging_default_format_string instead.'), - cfg.StrOpt('log-date-format', - default=_DEFAULT_LOG_DATE_FORMAT, - metavar='DATE_FORMAT', - help='Format string for %%(asctime)s in log records. ' - 'Default: %(default)s'), - cfg.StrOpt('log-file', - metavar='PATH', - deprecated_name='logfile', - help='(Optional) Name of log file to output to. ' - 'If no default is set, logging will go to stdout.'), - cfg.StrOpt('log-dir', - deprecated_name='logdir', - help='(Optional) The base directory used for relative ' - '--log-file paths'), - cfg.BoolOpt('use-syslog', - default=False, - help='Use syslog for logging.'), - cfg.StrOpt('syslog-log-facility', - default='LOG_USER', - help='syslog facility to receive log lines') -] - -generic_log_opts = [ - cfg.BoolOpt('use_stderr', - default=True, - help='Log output to standard error') -] - -log_opts = [ - cfg.StrOpt('logging_context_format_string', - default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [%(request_id)s %(user)s %(tenant)s] ' - '%(instance)s%(message)s', - help='format string to use for log messages with context'), - cfg.StrOpt('logging_default_format_string', - default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [-] %(instance)s%(message)s', - help='format string to use for log messages without context'), - cfg.StrOpt('logging_debug_format_suffix', - default='%(funcName)s %(pathname)s:%(lineno)d', - help='data to append to log format when level is DEBUG'), - cfg.StrOpt('logging_exception_prefix', - default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s ' - '%(instance)s', - help='prefix each line of exception output with this format'), - cfg.ListOpt('default_log_levels', - default=[ - 'amqplib=WARN', - 'sqlalchemy=WARN', - 'boto=WARN', - 'suds=INFO', - 'keystone=INFO', - 'eventlet.wsgi.server=WARN' - ], - help='list of logger=LEVEL pairs'), - cfg.BoolOpt('publish_errors', - default=False, - help='publish error events'), - cfg.BoolOpt('fatal_deprecations', - default=False, - help='make deprecations fatal'), - - # NOTE(mikal): there are two options here because sometimes we are handed - # a full instance (and could include more information), and other times we - # are just handed a UUID for the instance. - cfg.StrOpt('instance_format', - default='[instance: %(uuid)s] ', - help='If an instance is passed with the log message, format ' - 'it like this'), - cfg.StrOpt('instance_uuid_format', - default='[instance: %(uuid)s] ', - help='If an instance UUID is passed with the log message, ' - 'format it like this'), -] - -CONF = cfg.CONF -CONF.register_cli_opts(common_cli_opts) -CONF.register_cli_opts(logging_cli_opts) -CONF.register_opts(generic_log_opts) -CONF.register_opts(log_opts) - -# our new audit level -# NOTE(jkoelker) Since we synthesized an audit level, make the logging -# module aware of it so it acts like other levels. -logging.AUDIT = logging.INFO + 1 -logging.addLevelName(logging.AUDIT, 'AUDIT') - - -try: - NullHandler = logging.NullHandler -except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7 - class NullHandler(logging.Handler): - def handle(self, record): - pass - - def emit(self, record): - pass - - def createLock(self): - self.lock = None - - -def _dictify_context(context): - if context is None: - return None - if not isinstance(context, dict) and getattr(context, 'to_dict', None): - context = context.to_dict() - return context - - -def _get_binary_name(): - return os.path.basename(inspect.stack()[-1][1]) - - -def _get_log_file_path(binary=None): - logfile = CONF.log_file - logdir = CONF.log_dir - - if logfile and not logdir: - return logfile - - if logfile and logdir: - return os.path.join(logdir, logfile) - - if logdir: - binary = binary or _get_binary_name() - return '%s.log' % (os.path.join(logdir, binary),) - - -class BaseLoggerAdapter(logging.LoggerAdapter): - - def audit(self, msg, *args, **kwargs): - self.log(logging.AUDIT, msg, *args, **kwargs) - - -class LazyAdapter(BaseLoggerAdapter): - def __init__(self, name='unknown', version='unknown'): - self._logger = None - self.extra = {} - self.name = name - self.version = version - - @property - def logger(self): - if not self._logger: - self._logger = getLogger(self.name, self.version) - return self._logger - - -class ContextAdapter(BaseLoggerAdapter): - warn = logging.LoggerAdapter.warning - - def __init__(self, logger, project_name, version_string): - self.logger = logger - self.project = project_name - self.version = version_string - - @property - def handlers(self): - return self.logger.handlers - - def deprecated(self, msg, *args, **kwargs): - stdmsg = _("Deprecated: %s") % msg - if CONF.fatal_deprecations: - self.critical(stdmsg, *args, **kwargs) - raise DeprecatedConfig(msg=stdmsg) - else: - self.warn(stdmsg, *args, **kwargs) - - def process(self, msg, kwargs): - if 'extra' not in kwargs: - kwargs['extra'] = {} - extra = kwargs['extra'] - - context = kwargs.pop('context', None) - if not context: - context = getattr(local.store, 'context', None) - if context: - extra.update(_dictify_context(context)) - - instance = kwargs.pop('instance', None) - instance_extra = '' - if instance: - instance_extra = CONF.instance_format % instance - else: - instance_uuid = kwargs.pop('instance_uuid', None) - if instance_uuid: - instance_extra = (CONF.instance_uuid_format - % {'uuid': instance_uuid}) - extra.update({'instance': instance_extra}) - - extra.update({"project": self.project}) - extra.update({"version": self.version}) - extra['extra'] = extra.copy() - return msg, kwargs - - -class JSONFormatter(logging.Formatter): - def __init__(self, fmt=None, datefmt=None): - # NOTE(jkoelker) we ignore the fmt argument, but its still there - # since logging.config.fileConfig passes it. - self.datefmt = datefmt - - def formatException(self, ei, strip_newlines=True): - lines = traceback.format_exception(*ei) - if strip_newlines: - lines = [itertools.ifilter( - lambda x: x, - line.rstrip().splitlines()) for line in lines] - lines = list(itertools.chain(*lines)) - return lines - - def format(self, record): - message = {'message': record.getMessage(), - 'asctime': self.formatTime(record, self.datefmt), - 'name': record.name, - 'msg': record.msg, - 'args': record.args, - 'levelname': record.levelname, - 'levelno': record.levelno, - 'pathname': record.pathname, - 'filename': record.filename, - 'module': record.module, - 'lineno': record.lineno, - 'funcname': record.funcName, - 'created': record.created, - 'msecs': record.msecs, - 'relative_created': record.relativeCreated, - 'thread': record.thread, - 'thread_name': record.threadName, - 'process_name': record.processName, - 'process': record.process, - 'traceback': None} - - if hasattr(record, 'extra'): - message['extra'] = record.extra - - if record.exc_info: - message['traceback'] = self.formatException(record.exc_info) - - return jsonutils.dumps(message) - - -def _create_logging_excepthook(product_name): - def logging_excepthook(type, value, tb): - extra = {} - if CONF.verbose: - extra['exc_info'] = (type, value, tb) - getLogger(product_name).critical(str(value), **extra) - return logging_excepthook - - -class LogConfigError(Exception): - - message = _('Error loading logging config %(log_config)s: %(err_msg)s') - - def __init__(self, log_config, err_msg): - self.log_config = log_config - self.err_msg = err_msg - - def __str__(self): - return self.message % dict(log_config=self.log_config, - err_msg=self.err_msg) - - -def _load_log_config(log_config): - try: - logging.config.fileConfig(log_config) - except ConfigParser.Error as exc: - raise LogConfigError(log_config, str(exc)) - - -def setup(product_name): - """Setup logging.""" - if CONF.log_config: - _load_log_config(CONF.log_config) - else: - _setup_logging_from_conf() - sys.excepthook = _create_logging_excepthook(product_name) - - -def set_defaults(logging_context_format_string): - cfg.set_defaults(log_opts, - logging_context_format_string= - logging_context_format_string) - - -def _find_facility_from_conf(): - facility_names = logging.handlers.SysLogHandler.facility_names - facility = getattr(logging.handlers.SysLogHandler, - CONF.syslog_log_facility, - None) - - if facility is None and CONF.syslog_log_facility in facility_names: - facility = facility_names.get(CONF.syslog_log_facility) - - if facility is None: - valid_facilities = facility_names.keys() - consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON', - 'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS', - 'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP', - 'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3', - 'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7'] - valid_facilities.extend(consts) - raise TypeError(_('syslog facility must be one of: %s') % - ', '.join("'%s'" % fac - for fac in valid_facilities)) - - return facility - - -def _setup_logging_from_conf(): - log_root = getLogger(None).logger - for handler in log_root.handlers: - log_root.removeHandler(handler) - - if CONF.use_syslog: - facility = _find_facility_from_conf() - syslog = logging.handlers.SysLogHandler(address='/dev/log', - facility=facility) - log_root.addHandler(syslog) - - logpath = _get_log_file_path() - if logpath: - filelog = logging.handlers.WatchedFileHandler(logpath) - log_root.addHandler(filelog) - - if CONF.use_stderr: - streamlog = ColorHandler() - log_root.addHandler(streamlog) - - elif not CONF.log_file: - # pass sys.stdout as a positional argument - # python2.6 calls the argument strm, in 2.7 it's stream - streamlog = logging.StreamHandler(sys.stdout) - log_root.addHandler(streamlog) - - if CONF.publish_errors: - handler = importutils.import_object( - "staccato.openstack.common.log_handler.PublishErrorsHandler", - logging.ERROR) - log_root.addHandler(handler) - - datefmt = CONF.log_date_format - for handler in log_root.handlers: - # NOTE(alaski): CONF.log_format overrides everything currently. This - # should be deprecated in favor of context aware formatting. - if CONF.log_format: - handler.setFormatter(logging.Formatter(fmt=CONF.log_format, - datefmt=datefmt)) - log_root.info('Deprecated: log_format is now deprecated and will ' - 'be removed in the next release') - else: - handler.setFormatter(ContextFormatter(datefmt=datefmt)) - - if CONF.debug: - log_root.setLevel(logging.DEBUG) - elif CONF.verbose: - log_root.setLevel(logging.INFO) - else: - log_root.setLevel(logging.WARNING) - - for pair in CONF.default_log_levels: - mod, _sep, level_name = pair.partition('=') - level = logging.getLevelName(level_name) - logger = logging.getLogger(mod) - logger.setLevel(level) - -_loggers = {} - - -def getLogger(name='unknown', version='unknown'): - if name not in _loggers: - _loggers[name] = ContextAdapter(logging.getLogger(name), - name, - version) - return _loggers[name] - - -def getLazyLogger(name='unknown', version='unknown'): - """ - create a pass-through logger that does not create the real logger - until it is really needed and delegates all calls to the real logger - once it is created - """ - return LazyAdapter(name, version) - - -class WritableLogger(object): - """A thin wrapper that responds to `write` and logs.""" - - def __init__(self, logger, level=logging.INFO): - self.logger = logger - self.level = level - - def write(self, msg): - self.logger.log(self.level, msg) - - -class ContextFormatter(logging.Formatter): - """A context.RequestContext aware formatter configured through flags. - - The flags used to set format strings are: logging_context_format_string - and logging_default_format_string. You can also specify - logging_debug_format_suffix to append extra formatting if the log level is - debug. - - For information about what variables are available for the formatter see: - http://docs.python.org/library/logging.html#formatter - - """ - - def format(self, record): - """Uses contextstring if request_id is set, otherwise default.""" - # NOTE(sdague): default the fancier formating params - # to an empty string so we don't throw an exception if - # they get used - for key in ('instance', 'color'): - if key not in record.__dict__: - record.__dict__[key] = '' - - if record.__dict__.get('request_id', None): - self._fmt = CONF.logging_context_format_string - else: - self._fmt = CONF.logging_default_format_string - - if (record.levelno == logging.DEBUG and - CONF.logging_debug_format_suffix): - self._fmt += " " + CONF.logging_debug_format_suffix - - # Cache this on the record, Logger will respect our formated copy - if record.exc_info: - record.exc_text = self.formatException(record.exc_info, record) - return logging.Formatter.format(self, record) - - def formatException(self, exc_info, record=None): - """Format exception output with CONF.logging_exception_prefix.""" - if not record: - return logging.Formatter.formatException(self, exc_info) - - stringbuffer = cStringIO.StringIO() - traceback.print_exception(exc_info[0], exc_info[1], exc_info[2], - None, stringbuffer) - lines = stringbuffer.getvalue().split('\n') - stringbuffer.close() - - if CONF.logging_exception_prefix.find('%(asctime)') != -1: - record.asctime = self.formatTime(record, self.datefmt) - - formatted_lines = [] - for line in lines: - pl = CONF.logging_exception_prefix % record.__dict__ - fl = '%s%s' % (pl, line) - formatted_lines.append(fl) - return '\n'.join(formatted_lines) - - -class ColorHandler(logging.StreamHandler): - LEVEL_COLORS = { - logging.DEBUG: '\033[00;32m', # GREEN - logging.INFO: '\033[00;36m', # CYAN - logging.AUDIT: '\033[01;36m', # BOLD CYAN - logging.WARN: '\033[01;33m', # BOLD YELLOW - logging.ERROR: '\033[01;31m', # BOLD RED - logging.CRITICAL: '\033[01;31m', # BOLD RED - } - - def format(self, record): - record.color = self.LEVEL_COLORS[record.levelno] - return logging.StreamHandler.format(self, record) - - -class DeprecatedConfig(Exception): - message = _("Fatal call to deprecated config: %(msg)s") - - def __init__(self, msg): - super(Exception, self).__init__(self.message % dict(msg=msg)) diff --git a/staccato/openstack/common/loopingcall.py b/staccato/openstack/common/loopingcall.py deleted file mode 100644 index 0b2daa5..0000000 --- a/staccato/openstack/common/loopingcall.py +++ /dev/null @@ -1,147 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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 sys - -from eventlet import event -from eventlet import greenthread - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging -from staccato.openstack.common import timeutils - -LOG = logging.getLogger(__name__) - - -class LoopingCallDone(Exception): - """Exception to break out and stop a LoopingCall. - - The poll-function passed to LoopingCall can raise this exception to - break out of the loop normally. This is somewhat analogous to - StopIteration. - - An optional return-value can be included as the argument to the exception; - this return-value will be returned by LoopingCall.wait() - - """ - - def __init__(self, retvalue=True): - """:param retvalue: Value that LoopingCall.wait() should return.""" - self.retvalue = retvalue - - -class LoopingCallBase(object): - def __init__(self, f=None, *args, **kw): - self.args = args - self.kw = kw - self.f = f - self._running = False - self.done = None - - def stop(self): - self._running = False - - def wait(self): - return self.done.wait() - - -class FixedIntervalLoopingCall(LoopingCallBase): - """A fixed interval looping call.""" - - def start(self, interval, initial_delay=None): - self._running = True - done = event.Event() - - def _inner(): - if initial_delay: - greenthread.sleep(initial_delay) - - try: - while self._running: - start = timeutils.utcnow() - self.f(*self.args, **self.kw) - end = timeutils.utcnow() - if not self._running: - break - delay = interval - timeutils.delta_seconds(start, end) - if delay <= 0: - LOG.warn(_('task run outlasted interval by %s sec') % - -delay) - greenthread.sleep(delay if delay > 0 else 0) - except LoopingCallDone as e: - self.stop() - done.send(e.retvalue) - except Exception: - LOG.exception(_('in fixed duration looping call')) - done.send_exception(*sys.exc_info()) - return - else: - done.send(True) - - self.done = done - - greenthread.spawn_n(_inner) - return self.done - - -# TODO(mikal): this class name is deprecated in Havana and should be removed -# in the I release -LoopingCall = FixedIntervalLoopingCall - - -class DynamicLoopingCall(LoopingCallBase): - """A looping call which sleeps until the next known event. - - The function called should return how long to sleep for before being - called again. - """ - - def start(self, initial_delay=None, periodic_interval_max=None): - self._running = True - done = event.Event() - - def _inner(): - if initial_delay: - greenthread.sleep(initial_delay) - - try: - while self._running: - idle = self.f(*self.args, **self.kw) - if not self._running: - break - - if periodic_interval_max is not None: - idle = min(idle, periodic_interval_max) - LOG.debug(_('Dynamic looping call sleeping for %.02f ' - 'seconds'), idle) - greenthread.sleep(idle) - except LoopingCallDone as e: - self.stop() - done.send(e.retvalue) - except Exception: - LOG.exception(_('in dynamic looping call')) - done.send_exception(*sys.exc_info()) - return - else: - done.send(True) - - self.done = done - - greenthread.spawn(_inner) - return self.done diff --git a/staccato/openstack/common/middleware/__init__.py b/staccato/openstack/common/middleware/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/openstack/common/middleware/context.py b/staccato/openstack/common/middleware/context.py deleted file mode 100644 index b006850..0000000 --- a/staccato/openstack/common/middleware/context.py +++ /dev/null @@ -1,64 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Middleware that attaches a context to the WSGI request -""" - -from staccato.openstack.common import context -from staccato.openstack.common import importutils -from staccato.openstack.common import wsgi - - -class ContextMiddleware(wsgi.Middleware): - def __init__(self, app, options): - self.options = options - super(ContextMiddleware, self).__init__(app) - - def make_context(self, *args, **kwargs): - """ - Create a context with the given arguments. - """ - - # Determine the context class to use - ctxcls = context.RequestContext - if 'context_class' in self.options: - ctxcls = importutils.import_class(self.options['context_class']) - - return ctxcls(*args, **kwargs) - - def process_request(self, req): - """ - Extract any authentication information in the request and - construct an appropriate context from it. - """ - # Use the default empty context, with admin turned on for - # backwards compatibility - req.context = self.make_context(is_admin=True) - - -def filter_factory(global_conf, **local_conf): - """ - Factory method for paste.deploy - """ - conf = global_conf.copy() - conf.update(local_conf) - - def filter(app): - return ContextMiddleware(app, conf) - - return filter diff --git a/staccato/openstack/common/middleware/sizelimit.py b/staccato/openstack/common/middleware/sizelimit.py deleted file mode 100644 index 081d300..0000000 --- a/staccato/openstack/common/middleware/sizelimit.py +++ /dev/null @@ -1,84 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 Red Hat, 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. -""" -Request Body limiting middleware. - -""" - -from oslo.config import cfg -import webob.dec -import webob.exc - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import wsgi - - -#default request size is 112k -max_req_body_size = cfg.IntOpt('max_request_body_size', - deprecated_name='osapi_max_request_body_size', - default=114688, - help='the maximum body size ' - 'per each request(bytes)') - -CONF = cfg.CONF -CONF.register_opt(max_req_body_size) - - -class LimitingReader(object): - """Reader to limit the size of an incoming request.""" - def __init__(self, data, limit): - """ - :param data: Underlying data object - :param limit: maximum number of bytes the reader should allow - """ - self.data = data - self.limit = limit - self.bytes_read = 0 - - def __iter__(self): - for chunk in self.data: - self.bytes_read += len(chunk) - if self.bytes_read > self.limit: - msg = _("Request is too large.") - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) - else: - yield chunk - - def read(self, i=None): - result = self.data.read(i) - self.bytes_read += len(result) - if self.bytes_read > self.limit: - msg = _("Request is too large.") - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) - return result - - -class RequestBodySizeLimiter(wsgi.Middleware): - """Limit the size of incoming requests.""" - - def __init__(self, *args, **kwargs): - super(RequestBodySizeLimiter, self).__init__(*args, **kwargs) - - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): - if req.content_length > CONF.max_request_body_size: - msg = _("Request is too large.") - raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg) - if req.content_length is None and req.is_body_readable: - limiter = LimitingReader(req.body_file, - CONF.max_request_body_size) - req.body_file = limiter - return self.application diff --git a/staccato/openstack/common/network_utils.py b/staccato/openstack/common/network_utils.py deleted file mode 100644 index 2f988ca..0000000 --- a/staccato/openstack/common/network_utils.py +++ /dev/null @@ -1,69 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Network-related utilities and helper functions. -""" - -from staccato.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) - - -def parse_host_port(address, default_port=None): - """ - Interpret a string as a host:port pair. - An IPv6 address MUST be escaped if accompanied by a port, - because otherwise ambiguity ensues: 2001:db8:85a3::8a2e:370:7334 - means both [2001:db8:85a3::8a2e:370:7334] and - [2001:db8:85a3::8a2e:370]:7334. - - >>> parse_host_port('server01:80') - ('server01', 80) - >>> parse_host_port('server01') - ('server01', None) - >>> parse_host_port('server01', default_port=1234) - ('server01', 1234) - >>> parse_host_port('[::1]:80') - ('::1', 80) - >>> parse_host_port('[::1]') - ('::1', None) - >>> parse_host_port('[::1]', default_port=1234) - ('::1', 1234) - >>> parse_host_port('2001:db8:85a3::8a2e:370:7334', default_port=1234) - ('2001:db8:85a3::8a2e:370:7334', 1234) - - """ - if address[0] == '[': - # Escaped ipv6 - _host, _port = address[1:].split(']') - host = _host - if ':' in _port: - port = _port.split(':')[1] - else: - port = default_port - else: - if address.count(':') == 1: - host, port = address.split(':') - else: - # 0 means ipv4, >1 means ipv6. - # We prohibit unescaped ipv6 addresses with port. - host = address - port = default_port - - return (host, None if port is None else int(port)) diff --git a/staccato/openstack/common/notifier/__init__.py b/staccato/openstack/common/notifier/__init__.py deleted file mode 100644 index 45c3b46..0000000 --- a/staccato/openstack/common/notifier/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. diff --git a/staccato/openstack/common/notifier/api.py b/staccato/openstack/common/notifier/api.py deleted file mode 100644 index 195a04c..0000000 --- a/staccato/openstack/common/notifier/api.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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 uuid - -from oslo.config import cfg - -from staccato.openstack.common import context -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import jsonutils -from staccato.openstack.common import log as logging -from staccato.openstack.common import timeutils - - -LOG = logging.getLogger(__name__) - -notifier_opts = [ - cfg.MultiStrOpt('notification_driver', - default=[], - help='Driver or drivers to handle sending notifications'), - cfg.StrOpt('default_notification_level', - default='INFO', - help='Default notification level for outgoing notifications'), - cfg.StrOpt('default_publisher_id', - default='$host', - help='Default publisher_id for outgoing notifications'), -] - -CONF = cfg.CONF -CONF.register_opts(notifier_opts) - -WARN = 'WARN' -INFO = 'INFO' -ERROR = 'ERROR' -CRITICAL = 'CRITICAL' -DEBUG = 'DEBUG' - -log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL) - - -class BadPriorityException(Exception): - pass - - -def notify_decorator(name, fn): - """ decorator for notify which is used from utils.monkey_patch() - - :param name: name of the function - :param function: - object of the function - :returns: function -- decorated function - - """ - def wrapped_func(*args, **kwarg): - body = {} - body['args'] = [] - body['kwarg'] = {} - for arg in args: - body['args'].append(arg) - for key in kwarg: - body['kwarg'][key] = kwarg[key] - - ctxt = context.get_context_from_function_and_args(fn, args, kwarg) - notify(ctxt, - CONF.default_publisher_id, - name, - CONF.default_notification_level, - body) - return fn(*args, **kwarg) - return wrapped_func - - -def publisher_id(service, host=None): - if not host: - host = CONF.host - return "%s.%s" % (service, host) - - -def notify(context, publisher_id, event_type, priority, payload): - """Sends a notification using the specified driver - - :param publisher_id: the source worker_type.host of the message - :param event_type: the literal type of event (ex. Instance Creation) - :param priority: patterned after the enumeration of Python logging - levels in the set (DEBUG, WARN, INFO, ERROR, CRITICAL) - :param payload: A python dictionary of attributes - - Outgoing message format includes the above parameters, and appends the - following: - - message_id - a UUID representing the id for this notification - - timestamp - the GMT timestamp the notification was sent at - - The composite message will be constructed as a dictionary of the above - attributes, which will then be sent via the transport mechanism defined - by the driver. - - Message example:: - - {'message_id': str(uuid.uuid4()), - 'publisher_id': 'compute.host1', - 'timestamp': timeutils.utcnow(), - 'priority': 'WARN', - 'event_type': 'compute.create_instance', - 'payload': {'instance_id': 12, ... }} - - """ - if priority not in log_levels: - raise BadPriorityException( - _('%s not in valid priorities') % priority) - - # Ensure everything is JSON serializable. - payload = jsonutils.to_primitive(payload, convert_instances=True) - - msg = dict(message_id=str(uuid.uuid4()), - publisher_id=publisher_id, - event_type=event_type, - priority=priority, - payload=payload, - timestamp=str(timeutils.utcnow())) - - for driver in _get_drivers(): - try: - driver.notify(context, msg) - except Exception as e: - LOG.exception(_("Problem '%(e)s' attempting to " - "send to notification system. " - "Payload=%(payload)s") - % dict(e=e, payload=payload)) - - -_drivers = None - - -def _get_drivers(): - """Instantiate, cache, and return drivers based on the CONF.""" - global _drivers - if _drivers is None: - _drivers = {} - for notification_driver in CONF.notification_driver: - add_driver(notification_driver) - - return _drivers.values() - - -def add_driver(notification_driver): - """Add a notification driver at runtime.""" - # Make sure the driver list is initialized. - _get_drivers() - if isinstance(notification_driver, basestring): - # Load and add - try: - driver = importutils.import_module(notification_driver) - _drivers[notification_driver] = driver - except ImportError: - LOG.exception(_("Failed to load notifier %s. " - "These notifications will not be sent.") % - notification_driver) - else: - # Driver is already loaded; just add the object. - _drivers[notification_driver] = notification_driver - - -def _reset_drivers(): - """Used by unit tests to reset the drivers.""" - global _drivers - _drivers = None diff --git a/staccato/openstack/common/notifier/log_notifier.py b/staccato/openstack/common/notifier/log_notifier.py deleted file mode 100644 index f4cf0c3..0000000 --- a/staccato/openstack/common/notifier/log_notifier.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -from oslo.config import cfg - -from staccato.openstack.common import jsonutils -from staccato.openstack.common import log as logging - - -CONF = cfg.CONF - - -def notify(_context, message): - """Notifies the recipient of the desired event given the model. - Log notifications using openstack's default logging system""" - - priority = message.get('priority', - CONF.default_notification_level) - priority = priority.lower() - logger = logging.getLogger( - 'staccato.openstack.common.notification.%s' % - message['event_type']) - getattr(logger, priority)(jsonutils.dumps(message)) diff --git a/staccato/openstack/common/notifier/no_op_notifier.py b/staccato/openstack/common/notifier/no_op_notifier.py deleted file mode 100644 index bc7a56c..0000000 --- a/staccato/openstack/common/notifier/no_op_notifier.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - - -def notify(_context, message): - """Notifies the recipient of the desired event given the model""" - pass diff --git a/staccato/openstack/common/notifier/rpc_notifier.py b/staccato/openstack/common/notifier/rpc_notifier.py deleted file mode 100644 index a08b1ea..0000000 --- a/staccato/openstack/common/notifier/rpc_notifier.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -from oslo.config import cfg - -from staccato.openstack.common import context as req_context -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging -from staccato.openstack.common import rpc - -LOG = logging.getLogger(__name__) - -notification_topic_opt = cfg.ListOpt( - 'notification_topics', default=['notifications', ], - help='AMQP topic used for openstack notifications') - -CONF = cfg.CONF -CONF.register_opt(notification_topic_opt) - - -def notify(context, message): - """Sends a notification via RPC""" - if not context: - context = req_context.get_admin_context() - priority = message.get('priority', - CONF.default_notification_level) - priority = priority.lower() - for topic in CONF.notification_topics: - topic = '%s.%s' % (topic, priority) - try: - rpc.notify(context, topic, message) - except Exception: - LOG.exception(_("Could not send notification to %(topic)s. " - "Payload=%(message)s"), locals()) diff --git a/staccato/openstack/common/notifier/rpc_notifier2.py b/staccato/openstack/common/notifier/rpc_notifier2.py deleted file mode 100644 index 3a66ae3..0000000 --- a/staccato/openstack/common/notifier/rpc_notifier2.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -'''messaging based notification driver, with message envelopes''' - -from oslo.config import cfg - -from staccato.openstack.common import context as req_context -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging -from staccato.openstack.common import rpc - -LOG = logging.getLogger(__name__) - -notification_topic_opt = cfg.ListOpt( - 'topics', default=['notifications', ], - help='AMQP topic(s) used for openstack notifications') - -opt_group = cfg.OptGroup(name='rpc_notifier2', - title='Options for rpc_notifier2') - -CONF = cfg.CONF -CONF.register_group(opt_group) -CONF.register_opt(notification_topic_opt, opt_group) - - -def notify(context, message): - """Sends a notification via RPC""" - if not context: - context = req_context.get_admin_context() - priority = message.get('priority', - CONF.default_notification_level) - priority = priority.lower() - for topic in CONF.rpc_notifier2.topics: - topic = '%s.%s' % (topic, priority) - try: - rpc.notify(context, topic, message, envelope=True) - except Exception: - LOG.exception(_("Could not send notification to %(topic)s. " - "Payload=%(message)s"), locals()) diff --git a/staccato/openstack/common/notifier/test_notifier.py b/staccato/openstack/common/notifier/test_notifier.py deleted file mode 100644 index 96c1746..0000000 --- a/staccato/openstack/common/notifier/test_notifier.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - - -NOTIFICATIONS = [] - - -def notify(_context, message): - """Test notifier, stores notifications in memory for unittests.""" - NOTIFICATIONS.append(message) diff --git a/staccato/openstack/common/pastedeploy.py b/staccato/openstack/common/pastedeploy.py deleted file mode 100644 index 3630a2e..0000000 --- a/staccato/openstack/common/pastedeploy.py +++ /dev/null @@ -1,164 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, 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 sys - -from paste import deploy - -from staccato.openstack.common import local - - -class BasePasteFactory(object): - - """A base class for paste app and filter factories. - - Sub-classes must override the KEY class attribute and provide - a __call__ method. - """ - - KEY = None - - def __init__(self, data): - self.data = data - - def _import_factory(self, local_conf): - """Import an app/filter class. - - Lookup the KEY from the PasteDeploy local conf and import the - class named there. This class can then be used as an app or - filter factory. - - Note we support the : format. - - Note also that if you do e.g. - - key = - value - - then ConfigParser returns a value with a leading newline, so - we strip() the value before using it. - """ - mod_str, _sep, class_str = local_conf[self.KEY].strip().rpartition(':') - del local_conf[self.KEY] - - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - - -class AppFactory(BasePasteFactory): - - """A Generic paste.deploy app factory. - - This requires openstack.app_factory to be set to a callable which returns a - WSGI app when invoked. The format of the name is : e.g. - - [app:myfooapp] - paste.app_factory = openstack.common.pastedeploy:app_factory - openstack.app_factory = myapp:Foo - - The WSGI app constructor must accept a data object and a local config - dict as its two arguments. - """ - - KEY = 'openstack.app_factory' - - def __call__(self, global_conf, **local_conf): - """The actual paste.app_factory protocol method.""" - factory = self._import_factory(local_conf) - return factory(self.data, **local_conf) - - -class FilterFactory(AppFactory): - - """A Generic paste.deploy filter factory. - - This requires openstack.filter_factory to be set to a callable which - returns a WSGI filter when invoked. The format is : e.g. - - [filter:myfoofilter] - paste.filter_factory = openstack.common.pastedeploy:filter_factory - openstack.filter_factory = myfilter:Foo - - The WSGI filter constructor must accept a WSGI app, a data object and - a local config dict as its three arguments. - """ - - KEY = 'openstack.filter_factory' - - def __call__(self, global_conf, **local_conf): - """The actual paste.filter_factory protocol method.""" - factory = self._import_factory(local_conf) - - def filter(app): - return factory(app, self.data, **local_conf) - - return filter - - -def app_factory(global_conf, **local_conf): - """A paste app factory used with paste_deploy_app().""" - return local.store.app_factory(global_conf, **local_conf) - - -def filter_factory(global_conf, **local_conf): - """A paste filter factory used with paste_deploy_app().""" - return local.store.filter_factory(global_conf, **local_conf) - - -def paste_deploy_app(paste_config_file, app_name, data): - """Load a WSGI app from a PasteDeploy configuration. - - Use deploy.loadapp() to load the app from the PasteDeploy configuration, - ensuring that the supplied data object is passed to the app and filter - factories defined in this module. - - To use these factories and the data object, the configuration should look - like this: - - [app:myapp] - paste.app_factory = openstack.common.pastedeploy:app_factory - openstack.app_factory = myapp:App - ... - [filter:myfilter] - paste.filter_factory = openstack.common.pastedeploy:filter_factory - openstack.filter_factory = myapp:Filter - - and then: - - myapp.py: - - class App(object): - def __init__(self, data): - ... - - class Filter(object): - def __init__(self, app, data): - ... - - :param paste_config_file: a PasteDeploy config file - :param app_name: the name of the app/pipeline to load from the file - :param data: a data object to supply to the app and its filters - :returns: the WSGI app - """ - (af, ff) = (AppFactory(data), FilterFactory(data)) - - local.store.app_factory = af - local.store.filter_factory = ff - try: - return deploy.loadapp("config:%s" % paste_config_file, name=app_name) - finally: - del local.store.app_factory - del local.store.filter_factory diff --git a/staccato/openstack/common/policy.py b/staccato/openstack/common/policy.py deleted file mode 100644 index 1115396..0000000 --- a/staccato/openstack/common/policy.py +++ /dev/null @@ -1,780 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Common Policy Engine Implementation - -Policies can be expressed in one of two forms: A list of lists, or a -string written in the new policy language. - -In the list-of-lists representation, each check inside the innermost -list is combined as with an "and" conjunction--for that check to pass, -all the specified checks must pass. These innermost lists are then -combined as with an "or" conjunction. This is the original way of -expressing policies, but there now exists a new way: the policy -language. - -In the policy language, each check is specified the same way as in the -list-of-lists representation: a simple "a:b" pair that is matched to -the correct code to perform that check. However, conjunction -operators are available, allowing for more expressiveness in crafting -policies. - -As an example, take the following rule, expressed in the list-of-lists -representation:: - - [["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]] - -In the policy language, this becomes:: - - role:admin or (project_id:%(project_id)s and role:projectadmin) - -The policy language also has the "not" operator, allowing a richer -policy rule:: - - project_id:%(project_id)s and not role:dunce - -Finally, two special policy checks should be mentioned; the policy -check "@" will always accept an access, and the policy check "!" will -always reject an access. (Note that if a rule is either the empty -list ("[]") or the empty string, this is equivalent to the "@" policy -check.) Of these, the "!" policy check is probably the most useful, -as it allows particular rules to be explicitly disabled. -""" - -import abc -import re -import urllib - -import six -import urllib2 - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import jsonutils -from staccato.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) - - -_rules = None -_checks = {} - - -class Rules(dict): - """ - A store for rules. Handles the default_rule setting directly. - """ - - @classmethod - def load_json(cls, data, default_rule=None): - """ - Allow loading of JSON rule data. - """ - - # Suck in the JSON data and parse the rules - rules = dict((k, parse_rule(v)) for k, v in - jsonutils.loads(data).items()) - - return cls(rules, default_rule) - - def __init__(self, rules=None, default_rule=None): - """Initialize the Rules store.""" - - super(Rules, self).__init__(rules or {}) - self.default_rule = default_rule - - def __missing__(self, key): - """Implements the default rule handling.""" - - # If the default rule isn't actually defined, do something - # reasonably intelligent - if not self.default_rule or self.default_rule not in self: - raise KeyError(key) - - return self[self.default_rule] - - def __str__(self): - """Dumps a string representation of the rules.""" - - # Start by building the canonical strings for the rules - out_rules = {} - for key, value in self.items(): - # Use empty string for singleton TrueCheck instances - if isinstance(value, TrueCheck): - out_rules[key] = '' - else: - out_rules[key] = str(value) - - # Dump a pretty-printed JSON representation - return jsonutils.dumps(out_rules, indent=4) - - -# Really have to figure out a way to deprecate this -def set_rules(rules): - """Set the rules in use for policy checks.""" - - global _rules - - _rules = rules - - -# Ditto -def reset(): - """Clear the rules used for policy checks.""" - - global _rules - - _rules = None - - -def check(rule, target, creds, exc=None, *args, **kwargs): - """ - Checks authorization of a rule against the target and credentials. - - :param rule: The rule to evaluate. - :param target: As much information about the object being operated - on as possible, as a dictionary. - :param creds: As much information about the user performing the - action as possible, as a dictionary. - :param exc: Class of the exception to raise if the check fails. - Any remaining arguments passed to check() (both - positional and keyword arguments) will be passed to - the exception class. If exc is not provided, returns - False. - - :return: Returns False if the policy does not allow the action and - exc is not provided; otherwise, returns a value that - evaluates to True. Note: for rules using the "case" - expression, this True value will be the specified string - from the expression. - """ - - # Allow the rule to be a Check tree - if isinstance(rule, BaseCheck): - result = rule(target, creds) - elif not _rules: - # No rules to reference means we're going to fail closed - result = False - else: - try: - # Evaluate the rule - result = _rules[rule](target, creds) - except KeyError: - # If the rule doesn't exist, fail closed - result = False - - # If it is False, raise the exception if requested - if exc and result is False: - raise exc(*args, **kwargs) - - return result - - -class BaseCheck(object): - """ - Abstract base class for Check classes. - """ - - __metaclass__ = abc.ABCMeta - - @abc.abstractmethod - def __str__(self): - """ - Retrieve a string representation of the Check tree rooted at - this node. - """ - - pass - - @abc.abstractmethod - def __call__(self, target, cred): - """ - Perform the check. Returns False to reject the access or a - true value (not necessary True) to accept the access. - """ - - pass - - -class FalseCheck(BaseCheck): - """ - A policy check that always returns False (disallow). - """ - - def __str__(self): - """Return a string representation of this check.""" - - return "!" - - def __call__(self, target, cred): - """Check the policy.""" - - return False - - -class TrueCheck(BaseCheck): - """ - A policy check that always returns True (allow). - """ - - def __str__(self): - """Return a string representation of this check.""" - - return "@" - - def __call__(self, target, cred): - """Check the policy.""" - - return True - - -class Check(BaseCheck): - """ - A base class to allow for user-defined policy checks. - """ - - def __init__(self, kind, match): - """ - :param kind: The kind of the check, i.e., the field before the - ':'. - :param match: The match of the check, i.e., the field after - the ':'. - """ - - self.kind = kind - self.match = match - - def __str__(self): - """Return a string representation of this check.""" - - return "%s:%s" % (self.kind, self.match) - - -class NotCheck(BaseCheck): - """ - A policy check that inverts the result of another policy check. - Implements the "not" operator. - """ - - def __init__(self, rule): - """ - Initialize the 'not' check. - - :param rule: The rule to negate. Must be a Check. - """ - - self.rule = rule - - def __str__(self): - """Return a string representation of this check.""" - - return "not %s" % self.rule - - def __call__(self, target, cred): - """ - Check the policy. Returns the logical inverse of the wrapped - check. - """ - - return not self.rule(target, cred) - - -class AndCheck(BaseCheck): - """ - A policy check that requires that a list of other checks all - return True. Implements the "and" operator. - """ - - def __init__(self, rules): - """ - Initialize the 'and' check. - - :param rules: A list of rules that will be tested. - """ - - self.rules = rules - - def __str__(self): - """Return a string representation of this check.""" - - return "(%s)" % ' and '.join(str(r) for r in self.rules) - - def __call__(self, target, cred): - """ - Check the policy. Requires that all rules accept in order to - return True. - """ - - for rule in self.rules: - if not rule(target, cred): - return False - - return True - - def add_check(self, rule): - """ - Allows addition of another rule to the list of rules that will - be tested. Returns the AndCheck object for convenience. - """ - - self.rules.append(rule) - return self - - -class OrCheck(BaseCheck): - """ - A policy check that requires that at least one of a list of other - checks returns True. Implements the "or" operator. - """ - - def __init__(self, rules): - """ - Initialize the 'or' check. - - :param rules: A list of rules that will be tested. - """ - - self.rules = rules - - def __str__(self): - """Return a string representation of this check.""" - - return "(%s)" % ' or '.join(str(r) for r in self.rules) - - def __call__(self, target, cred): - """ - Check the policy. Requires that at least one rule accept in - order to return True. - """ - - for rule in self.rules: - if rule(target, cred): - return True - - return False - - def add_check(self, rule): - """ - Allows addition of another rule to the list of rules that will - be tested. Returns the OrCheck object for convenience. - """ - - self.rules.append(rule) - return self - - -def _parse_check(rule): - """ - Parse a single base check rule into an appropriate Check object. - """ - - # Handle the special checks - if rule == '!': - return FalseCheck() - elif rule == '@': - return TrueCheck() - - try: - kind, match = rule.split(':', 1) - except Exception: - LOG.exception(_("Failed to understand rule %(rule)s") % locals()) - # If the rule is invalid, we'll fail closed - return FalseCheck() - - # Find what implements the check - if kind in _checks: - return _checks[kind](kind, match) - elif None in _checks: - return _checks[None](kind, match) - else: - LOG.error(_("No handler for matches of kind %s") % kind) - return FalseCheck() - - -def _parse_list_rule(rule): - """ - Provided for backwards compatibility. Translates the old - list-of-lists syntax into a tree of Check objects. - """ - - # Empty rule defaults to True - if not rule: - return TrueCheck() - - # Outer list is joined by "or"; inner list by "and" - or_list = [] - for inner_rule in rule: - # Elide empty inner lists - if not inner_rule: - continue - - # Handle bare strings - if isinstance(inner_rule, basestring): - inner_rule = [inner_rule] - - # Parse the inner rules into Check objects - and_list = [_parse_check(r) for r in inner_rule] - - # Append the appropriate check to the or_list - if len(and_list) == 1: - or_list.append(and_list[0]) - else: - or_list.append(AndCheck(and_list)) - - # If we have only one check, omit the "or" - if not or_list: - return FalseCheck() - elif len(or_list) == 1: - return or_list[0] - - return OrCheck(or_list) - - -# Used for tokenizing the policy language -_tokenize_re = re.compile(r'\s+') - - -def _parse_tokenize(rule): - """ - Tokenizer for the policy language. - - Most of the single-character tokens are specified in the - _tokenize_re; however, parentheses need to be handled specially, - because they can appear inside a check string. Thankfully, those - parentheses that appear inside a check string can never occur at - the very beginning or end ("%(variable)s" is the correct syntax). - """ - - for tok in _tokenize_re.split(rule): - # Skip empty tokens - if not tok or tok.isspace(): - continue - - # Handle leading parens on the token - clean = tok.lstrip('(') - for i in range(len(tok) - len(clean)): - yield '(', '(' - - # If it was only parentheses, continue - if not clean: - continue - else: - tok = clean - - # Handle trailing parens on the token - clean = tok.rstrip(')') - trail = len(tok) - len(clean) - - # Yield the cleaned token - lowered = clean.lower() - if lowered in ('and', 'or', 'not'): - # Special tokens - yield lowered, clean - elif clean: - # Not a special token, but not composed solely of ')' - if len(tok) >= 2 and ((tok[0], tok[-1]) in - [('"', '"'), ("'", "'")]): - # It's a quoted string - yield 'string', tok[1:-1] - else: - yield 'check', _parse_check(clean) - - # Yield the trailing parens - for i in range(trail): - yield ')', ')' - - -class ParseStateMeta(type): - """ - Metaclass for the ParseState class. Facilitates identifying - reduction methods. - """ - - def __new__(mcs, name, bases, cls_dict): - """ - Create the class. Injects the 'reducers' list, a list of - tuples matching token sequences to the names of the - corresponding reduction methods. - """ - - reducers = [] - - for key, value in cls_dict.items(): - if not hasattr(value, 'reducers'): - continue - for reduction in value.reducers: - reducers.append((reduction, key)) - - cls_dict['reducers'] = reducers - - return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict) - - -def reducer(*tokens): - """ - Decorator for reduction methods. Arguments are a sequence of - tokens, in order, which should trigger running this reduction - method. - """ - - def decorator(func): - # Make sure we have a list of reducer sequences - if not hasattr(func, 'reducers'): - func.reducers = [] - - # Add the tokens to the list of reducer sequences - func.reducers.append(list(tokens)) - - return func - - return decorator - - -class ParseState(object): - """ - Implement the core of parsing the policy language. Uses a greedy - reduction algorithm to reduce a sequence of tokens into a single - terminal, the value of which will be the root of the Check tree. - - Note: error reporting is rather lacking. The best we can get with - this parser formulation is an overall "parse failed" error. - Fortunately, the policy language is simple enough that this - shouldn't be that big a problem. - """ - - __metaclass__ = ParseStateMeta - - def __init__(self): - """Initialize the ParseState.""" - - self.tokens = [] - self.values = [] - - def reduce(self): - """ - Perform a greedy reduction of the token stream. If a reducer - method matches, it will be executed, then the reduce() method - will be called recursively to search for any more possible - reductions. - """ - - for reduction, methname in self.reducers: - if (len(self.tokens) >= len(reduction) and - self.tokens[-len(reduction):] == reduction): - # Get the reduction method - meth = getattr(self, methname) - - # Reduce the token stream - results = meth(*self.values[-len(reduction):]) - - # Update the tokens and values - self.tokens[-len(reduction):] = [r[0] for r in results] - self.values[-len(reduction):] = [r[1] for r in results] - - # Check for any more reductions - return self.reduce() - - def shift(self, tok, value): - """Adds one more token to the state. Calls reduce().""" - - self.tokens.append(tok) - self.values.append(value) - - # Do a greedy reduce... - self.reduce() - - @property - def result(self): - """ - Obtain the final result of the parse. Raises ValueError if - the parse failed to reduce to a single result. - """ - - if len(self.values) != 1: - raise ValueError("Could not parse rule") - return self.values[0] - - @reducer('(', 'check', ')') - @reducer('(', 'and_expr', ')') - @reducer('(', 'or_expr', ')') - def _wrap_check(self, _p1, check, _p2): - """Turn parenthesized expressions into a 'check' token.""" - - return [('check', check)] - - @reducer('check', 'and', 'check') - def _make_and_expr(self, check1, _and, check2): - """ - Create an 'and_expr' from two checks joined by the 'and' - operator. - """ - - return [('and_expr', AndCheck([check1, check2]))] - - @reducer('and_expr', 'and', 'check') - def _extend_and_expr(self, and_expr, _and, check): - """ - Extend an 'and_expr' by adding one more check. - """ - - return [('and_expr', and_expr.add_check(check))] - - @reducer('check', 'or', 'check') - def _make_or_expr(self, check1, _or, check2): - """ - Create an 'or_expr' from two checks joined by the 'or' - operator. - """ - - return [('or_expr', OrCheck([check1, check2]))] - - @reducer('or_expr', 'or', 'check') - def _extend_or_expr(self, or_expr, _or, check): - """ - Extend an 'or_expr' by adding one more check. - """ - - return [('or_expr', or_expr.add_check(check))] - - @reducer('not', 'check') - def _make_not_expr(self, _not, check): - """Invert the result of another check.""" - - return [('check', NotCheck(check))] - - -def _parse_text_rule(rule): - """ - Translates a policy written in the policy language into a tree of - Check objects. - """ - - # Empty rule means always accept - if not rule: - return TrueCheck() - - # Parse the token stream - state = ParseState() - for tok, value in _parse_tokenize(rule): - state.shift(tok, value) - - try: - return state.result - except ValueError: - # Couldn't parse the rule - LOG.exception(_("Failed to understand rule %(rule)r") % locals()) - - # Fail closed - return FalseCheck() - - -def parse_rule(rule): - """ - Parses a policy rule into a tree of Check objects. - """ - - # If the rule is a string, it's in the policy language - if isinstance(rule, basestring): - return _parse_text_rule(rule) - return _parse_list_rule(rule) - - -def register(name, func=None): - """ - Register a function or Check class as a policy check. - - :param name: Gives the name of the check type, e.g., 'rule', - 'role', etc. If name is None, a default check type - will be registered. - :param func: If given, provides the function or class to register. - If not given, returns a function taking one argument - to specify the function or class to register, - allowing use as a decorator. - """ - - # Perform the actual decoration by registering the function or - # class. Returns the function or class for compliance with the - # decorator interface. - def decorator(func): - _checks[name] = func - return func - - # If the function or class is given, do the registration - if func: - return decorator(func) - - return decorator - - -@register("rule") -class RuleCheck(Check): - def __call__(self, target, creds): - """ - Recursively checks credentials based on the defined rules. - """ - - try: - return _rules[self.match](target, creds) - except KeyError: - # We don't have any matching rule; fail closed - return False - - -@register("role") -class RoleCheck(Check): - def __call__(self, target, creds): - """Check that there is a matching role in the cred dict.""" - - return self.match.lower() in [x.lower() for x in creds['roles']] - - -@register('http') -class HttpCheck(Check): - def __call__(self, target, creds): - """ - Check http: rules by calling to a remote server. - - This example implementation simply verifies that the response - is exactly 'True'. - """ - - url = ('http:' + self.match) % target - data = {'target': jsonutils.dumps(target), - 'credentials': jsonutils.dumps(creds)} - post_data = urllib.urlencode(data) - f = urllib2.urlopen(url, post_data) - return f.read() == "True" - - -@register(None) -class GenericCheck(Check): - def __call__(self, target, creds): - """ - Check an individual match. - - Matches look like: - - tenant:%(tenant_id)s - role:compute:admin - """ - - # TODO(termie): do dict inspection via dot syntax - match = self.match % target - if self.kind in creds: - return match == six.text_type(creds[self.kind]) - return False diff --git a/staccato/openstack/common/processutils.py b/staccato/openstack/common/processutils.py deleted file mode 100644 index 76d58e2..0000000 --- a/staccato/openstack/common/processutils.py +++ /dev/null @@ -1,247 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -System-level utilities and helper functions. -""" - -import os -import random -import shlex -import signal - -from eventlet.green import subprocess -from eventlet import greenthread - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) - - -class InvalidArgumentError(Exception): - def __init__(self, message=None): - super(InvalidArgumentError, self).__init__(message) - - -class UnknownArgumentError(Exception): - def __init__(self, message=None): - super(UnknownArgumentError, self).__init__(message) - - -class ProcessExecutionError(Exception): - def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None, - description=None): - self.exit_code = exit_code - self.stderr = stderr - self.stdout = stdout - self.cmd = cmd - self.description = description - - if description is None: - description = "Unexpected error while running command." - if exit_code is None: - exit_code = '-' - message = ("%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" - % (description, cmd, exit_code, stdout, stderr)) - super(ProcessExecutionError, self).__init__(message) - - -class NoRootWrapSpecified(Exception): - def __init__(self, message=None): - super(NoRootWrapSpecified, self).__init__(message) - - -def _subprocess_setup(): - # Python installs a SIGPIPE handler by default. This is usually not what - # non-Python subprocesses expect. - signal.signal(signal.SIGPIPE, signal.SIG_DFL) - - -def execute(*cmd, **kwargs): - """ - Helper method to shell out and execute a command through subprocess with - optional retry. - - :param cmd: Passed to subprocess.Popen. - :type cmd: string - :param process_input: Send to opened process. - :type proces_input: string - :param check_exit_code: Single bool, int, or list of allowed exit - codes. Defaults to [0]. Raise - :class:`ProcessExecutionError` unless - program exits with one of these code. - :type check_exit_code: boolean, int, or [int] - :param delay_on_retry: True | False. Defaults to True. If set to True, - wait a short amount of time before retrying. - :type delay_on_retry: boolean - :param attempts: How many times to retry cmd. - :type attempts: int - :param run_as_root: True | False. Defaults to False. If set to True, - the command is prefixed by the command specified - in the root_helper kwarg. - :type run_as_root: boolean - :param root_helper: command to prefix to commands called with - run_as_root=True - :type root_helper: string - :param shell: whether or not there should be a shell used to - execute this command. Defaults to false. - :type shell: boolean - :returns: (stdout, stderr) from process execution - :raises: :class:`UnknownArgumentError` on - receiving unknown arguments - :raises: :class:`ProcessExecutionError` - """ - - process_input = kwargs.pop('process_input', None) - check_exit_code = kwargs.pop('check_exit_code', [0]) - ignore_exit_code = False - delay_on_retry = kwargs.pop('delay_on_retry', True) - attempts = kwargs.pop('attempts', 1) - run_as_root = kwargs.pop('run_as_root', False) - root_helper = kwargs.pop('root_helper', '') - shell = kwargs.pop('shell', False) - - if isinstance(check_exit_code, bool): - ignore_exit_code = not check_exit_code - check_exit_code = [0] - elif isinstance(check_exit_code, int): - check_exit_code = [check_exit_code] - - if kwargs: - raise UnknownArgumentError(_('Got unknown keyword args ' - 'to utils.execute: %r') % kwargs) - - if run_as_root and os.geteuid() != 0: - if not root_helper: - raise NoRootWrapSpecified( - message=('Command requested root, but did not specify a root ' - 'helper.')) - cmd = shlex.split(root_helper) + list(cmd) - - cmd = map(str, cmd) - - while attempts > 0: - attempts -= 1 - try: - LOG.debug(_('Running cmd (subprocess): %s'), ' '.join(cmd)) - _PIPE = subprocess.PIPE # pylint: disable=E1101 - - if os.name == 'nt': - preexec_fn = None - close_fds = False - else: - preexec_fn = _subprocess_setup - close_fds = True - - obj = subprocess.Popen(cmd, - stdin=_PIPE, - stdout=_PIPE, - stderr=_PIPE, - close_fds=close_fds, - preexec_fn=preexec_fn, - shell=shell) - result = None - if process_input is not None: - result = obj.communicate(process_input) - else: - result = obj.communicate() - obj.stdin.close() # pylint: disable=E1101 - _returncode = obj.returncode # pylint: disable=E1101 - if _returncode: - LOG.debug(_('Result was %s') % _returncode) - if not ignore_exit_code and _returncode not in check_exit_code: - (stdout, stderr) = result - raise ProcessExecutionError(exit_code=_returncode, - stdout=stdout, - stderr=stderr, - cmd=' '.join(cmd)) - return result - except ProcessExecutionError: - if not attempts: - raise - else: - LOG.debug(_('%r failed. Retrying.'), cmd) - if delay_on_retry: - greenthread.sleep(random.randint(20, 200) / 100.0) - finally: - # NOTE(termie): this appears to be necessary to let the subprocess - # call clean something up in between calls, without - # it two execute calls in a row hangs the second one - greenthread.sleep(0) - - -def trycmd(*args, **kwargs): - """ - A wrapper around execute() to more easily handle warnings and errors. - - Returns an (out, err) tuple of strings containing the output of - the command's stdout and stderr. If 'err' is not empty then the - command can be considered to have failed. - - :discard_warnings True | False. Defaults to False. If set to True, - then for succeeding commands, stderr is cleared - - """ - discard_warnings = kwargs.pop('discard_warnings', False) - - try: - out, err = execute(*args, **kwargs) - failed = False - except ProcessExecutionError, exn: - out, err = '', str(exn) - failed = True - - if not failed and discard_warnings and err: - # Handle commands that output to stderr but otherwise succeed - err = '' - - return out, err - - -def ssh_execute(ssh, cmd, process_input=None, - addl_env=None, check_exit_code=True): - LOG.debug(_('Running cmd (SSH): %s'), cmd) - if addl_env: - raise InvalidArgumentError(_('Environment not supported over SSH')) - - if process_input: - # This is (probably) fixable if we need it... - raise InvalidArgumentError(_('process_input not supported over SSH')) - - stdin_stream, stdout_stream, stderr_stream = ssh.exec_command(cmd) - channel = stdout_stream.channel - - # NOTE(justinsb): This seems suspicious... - # ...other SSH clients have buffering issues with this approach - stdout = stdout_stream.read() - stderr = stderr_stream.read() - stdin_stream.close() - - exit_status = channel.recv_exit_status() - - # exit_status == -1 if no exit code was returned - if exit_status != -1: - LOG.debug(_('Result was %s') % exit_status) - if check_exit_code and exit_status != 0: - raise ProcessExecutionError(exit_code=exit_status, - stdout=stdout, - stderr=stderr, - cmd=cmd) - - return (stdout, stderr) diff --git a/staccato/openstack/common/rpc/__init__.py b/staccato/openstack/common/rpc/__init__.py deleted file mode 100644 index 91e116d..0000000 --- a/staccato/openstack/common/rpc/__init__.py +++ /dev/null @@ -1,307 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# Copyright 2011 Red Hat, 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. - -""" -A remote procedure call (rpc) abstraction. - -For some wrappers that add message versioning to rpc, see: - rpc.dispatcher - rpc.proxy -""" - -import inspect - -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import local -from staccato.openstack.common import log as logging - - -LOG = logging.getLogger(__name__) - - -rpc_opts = [ - cfg.StrOpt('rpc_backend', - default='%s.impl_kombu' % __package__, - help="The messaging module to use, defaults to kombu."), - cfg.IntOpt('rpc_thread_pool_size', - default=64, - help='Size of RPC thread pool'), - cfg.IntOpt('rpc_conn_pool_size', - default=30, - help='Size of RPC connection pool'), - cfg.IntOpt('rpc_response_timeout', - default=60, - help='Seconds to wait for a response from call or multicall'), - cfg.IntOpt('rpc_cast_timeout', - default=30, - help='Seconds to wait before a cast expires (TTL). ' - 'Only supported by impl_zmq.'), - cfg.ListOpt('allowed_rpc_exception_modules', - default=['staccato.openstack.common.exception', - 'nova.exception', - 'cinder.exception', - 'exceptions', - ], - help='Modules of exceptions that are permitted to be recreated' - 'upon receiving exception data from an rpc call.'), - cfg.BoolOpt('fake_rabbit', - default=False, - help='If passed, use a fake RabbitMQ provider'), - cfg.StrOpt('control_exchange', - default='openstack', - help='AMQP exchange to connect to if using RabbitMQ or Qpid'), -] - -CONF = cfg.CONF -CONF.register_opts(rpc_opts) - - -def set_defaults(control_exchange): - cfg.set_defaults(rpc_opts, - control_exchange=control_exchange) - - -def create_connection(new=True): - """Create a connection to the message bus used for rpc. - - For some example usage of creating a connection and some consumers on that - connection, see nova.service. - - :param new: Whether or not to create a new connection. A new connection - will be created by default. If new is False, the - implementation is free to return an existing connection from a - pool. - - :returns: An instance of openstack.common.rpc.common.Connection - """ - return _get_impl().create_connection(CONF, new=new) - - -def _check_for_lock(): - if not CONF.debug: - return None - - if ((hasattr(local.strong_store, 'locks_held') - and local.strong_store.locks_held)): - stack = ' :: '.join([frame[3] for frame in inspect.stack()]) - LOG.warn(_('A RPC is being made while holding a lock. The locks ' - 'currently held are %(locks)s. This is probably a bug. ' - 'Please report it. Include the following: [%(stack)s].'), - {'locks': local.strong_store.locks_held, - 'stack': stack}) - return True - - return False - - -def call(context, topic, msg, timeout=None, check_for_lock=False): - """Invoke a remote method that returns something. - - :param context: Information that identifies the user that has made this - request. - :param topic: The topic to send the rpc message to. This correlates to the - topic argument of - openstack.common.rpc.common.Connection.create_consumer() - and only applies when the consumer was created with - fanout=False. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - :param timeout: int, number of seconds to use for a response timeout. - If set, this overrides the rpc_response_timeout option. - :param check_for_lock: if True, a warning is emitted if a RPC call is made - with a lock held. - - :returns: A dict from the remote method. - - :raises: openstack.common.rpc.common.Timeout if a complete response - is not received before the timeout is reached. - """ - if check_for_lock: - _check_for_lock() - return _get_impl().call(CONF, context, topic, msg, timeout) - - -def cast(context, topic, msg): - """Invoke a remote method that does not return anything. - - :param context: Information that identifies the user that has made this - request. - :param topic: The topic to send the rpc message to. This correlates to the - topic argument of - openstack.common.rpc.common.Connection.create_consumer() - and only applies when the consumer was created with - fanout=False. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - - :returns: None - """ - return _get_impl().cast(CONF, context, topic, msg) - - -def fanout_cast(context, topic, msg): - """Broadcast a remote method invocation with no return. - - This method will get invoked on all consumers that were set up with this - topic name and fanout=True. - - :param context: Information that identifies the user that has made this - request. - :param topic: The topic to send the rpc message to. This correlates to the - topic argument of - openstack.common.rpc.common.Connection.create_consumer() - and only applies when the consumer was created with - fanout=True. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - - :returns: None - """ - return _get_impl().fanout_cast(CONF, context, topic, msg) - - -def multicall(context, topic, msg, timeout=None, check_for_lock=False): - """Invoke a remote method and get back an iterator. - - In this case, the remote method will be returning multiple values in - separate messages, so the return values can be processed as the come in via - an iterator. - - :param context: Information that identifies the user that has made this - request. - :param topic: The topic to send the rpc message to. This correlates to the - topic argument of - openstack.common.rpc.common.Connection.create_consumer() - and only applies when the consumer was created with - fanout=False. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - :param timeout: int, number of seconds to use for a response timeout. - If set, this overrides the rpc_response_timeout option. - :param check_for_lock: if True, a warning is emitted if a RPC call is made - with a lock held. - - :returns: An iterator. The iterator will yield a tuple (N, X) where N is - an index that starts at 0 and increases by one for each value - returned and X is the Nth value that was returned by the remote - method. - - :raises: openstack.common.rpc.common.Timeout if a complete response - is not received before the timeout is reached. - """ - if check_for_lock: - _check_for_lock() - return _get_impl().multicall(CONF, context, topic, msg, timeout) - - -def notify(context, topic, msg, envelope=False): - """Send notification event. - - :param context: Information that identifies the user that has made this - request. - :param topic: The topic to send the notification to. - :param msg: This is a dict of content of event. - :param envelope: Set to True to enable message envelope for notifications. - - :returns: None - """ - return _get_impl().notify(cfg.CONF, context, topic, msg, envelope) - - -def cleanup(): - """Clean up resoruces in use by implementation. - - Clean up any resources that have been allocated by the RPC implementation. - This is typically open connections to a messaging service. This function - would get called before an application using this API exits to allow - connections to get torn down cleanly. - - :returns: None - """ - return _get_impl().cleanup() - - -def cast_to_server(context, server_params, topic, msg): - """Invoke a remote method that does not return anything. - - :param context: Information that identifies the user that has made this - request. - :param server_params: Connection information - :param topic: The topic to send the notification to. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - - :returns: None - """ - return _get_impl().cast_to_server(CONF, context, server_params, topic, - msg) - - -def fanout_cast_to_server(context, server_params, topic, msg): - """Broadcast to a remote method invocation with no return. - - :param context: Information that identifies the user that has made this - request. - :param server_params: Connection information - :param topic: The topic to send the notification to. - :param msg: This is a dict in the form { "method" : "method_to_invoke", - "args" : dict_of_kwargs } - - :returns: None - """ - return _get_impl().fanout_cast_to_server(CONF, context, server_params, - topic, msg) - - -def queue_get_for(context, topic, host): - """Get a queue name for a given topic + host. - - This function only works if this naming convention is followed on the - consumer side, as well. For example, in nova, every instance of the - nova-foo service calls create_consumer() for two topics: - - foo - foo. - - Messages sent to the 'foo' topic are distributed to exactly one instance of - the nova-foo service. The services are chosen in a round-robin fashion. - Messages sent to the 'foo.' topic are sent to the nova-foo service on - . - """ - return '%s.%s' % (topic, host) if host else topic - - -_RPCIMPL = None - - -def _get_impl(): - """Delay import of rpc_backend until configuration is loaded.""" - global _RPCIMPL - if _RPCIMPL is None: - try: - _RPCIMPL = importutils.import_module(CONF.rpc_backend) - except ImportError: - # For backwards compatibility with older nova config. - impl = CONF.rpc_backend.replace('nova.rpc', - 'nova.openstack.common.rpc') - _RPCIMPL = importutils.import_module(impl) - return _RPCIMPL diff --git a/staccato/openstack/common/rpc/amqp.py b/staccato/openstack/common/rpc/amqp.py deleted file mode 100644 index b228c48..0000000 --- a/staccato/openstack/common/rpc/amqp.py +++ /dev/null @@ -1,677 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# Copyright 2011 - 2012, Red Hat, 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. - -""" -Shared code between AMQP based openstack.common.rpc implementations. - -The code in this module is shared between the rpc implemenations based on AMQP. -Specifically, this includes impl_kombu and impl_qpid. impl_carrot also uses -AMQP, but is deprecated and predates this code. -""" - -import collections -import inspect -import sys -import uuid - -from eventlet import greenpool -from eventlet import pools -from eventlet import queue -from eventlet import semaphore -# TODO(pekowsk): Remove import cfg and below comment in Havana. -# This import should no longer be needed when the amqp_rpc_single_reply_queue -# option is removed. -from oslo.config import cfg - -from staccato.openstack.common import excutils -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import local -from staccato.openstack.common import log as logging -from staccato.openstack.common.rpc import common as rpc_common - - -# TODO(pekowski): Remove this option in Havana. -amqp_opts = [ - cfg.BoolOpt('amqp_rpc_single_reply_queue', - default=False, - help='Enable a fast single reply queue if using AMQP based ' - 'RPC like RabbitMQ or Qpid.'), -] - -cfg.CONF.register_opts(amqp_opts) - -UNIQUE_ID = '_unique_id' -LOG = logging.getLogger(__name__) - - -class Pool(pools.Pool): - """Class that implements a Pool of Connections.""" - def __init__(self, conf, connection_cls, *args, **kwargs): - self.connection_cls = connection_cls - self.conf = conf - kwargs.setdefault("max_size", self.conf.rpc_conn_pool_size) - kwargs.setdefault("order_as_stack", True) - super(Pool, self).__init__(*args, **kwargs) - self.reply_proxy = None - - # TODO(comstud): Timeout connections not used in a while - def create(self): - LOG.debug(_('Pool creating new connection')) - return self.connection_cls(self.conf) - - def empty(self): - while self.free_items: - self.get().close() - # Force a new connection pool to be created. - # Note that this was added due to failing unit test cases. The issue - # is the above "while loop" gets all the cached connections from the - # pool and closes them, but never returns them to the pool, a pool - # leak. The unit tests hang waiting for an item to be returned to the - # pool. The unit tests get here via the teatDown() method. In the run - # time code, it gets here via cleanup() and only appears in service.py - # just before doing a sys.exit(), so cleanup() only happens once and - # the leakage is not a problem. - self.connection_cls.pool = None - - -_pool_create_sem = semaphore.Semaphore() - - -def get_connection_pool(conf, connection_cls): - with _pool_create_sem: - # Make sure only one thread tries to create the connection pool. - if not connection_cls.pool: - connection_cls.pool = Pool(conf, connection_cls) - return connection_cls.pool - - -class ConnectionContext(rpc_common.Connection): - """The class that is actually returned to the caller of - create_connection(). This is essentially a wrapper around - Connection that supports 'with'. It can also return a new - Connection, or one from a pool. The function will also catch - when an instance of this class is to be deleted. With that - we can return Connections to the pool on exceptions and so - forth without making the caller be responsible for catching - them. If possible the function makes sure to return a - connection to the pool. - """ - - def __init__(self, conf, connection_pool, pooled=True, server_params=None): - """Create a new connection, or get one from the pool""" - self.connection = None - self.conf = conf - self.connection_pool = connection_pool - if pooled: - self.connection = connection_pool.get() - else: - self.connection = connection_pool.connection_cls( - conf, - server_params=server_params) - self.pooled = pooled - - def __enter__(self): - """When with ConnectionContext() is used, return self""" - return self - - def _done(self): - """If the connection came from a pool, clean it up and put it back. - If it did not come from a pool, close it. - """ - if self.connection: - if self.pooled: - # Reset the connection so it's ready for the next caller - # to grab from the pool - self.connection.reset() - self.connection_pool.put(self.connection) - else: - try: - self.connection.close() - except Exception: - pass - self.connection = None - - def __exit__(self, exc_type, exc_value, tb): - """End of 'with' statement. We're done here.""" - self._done() - - def __del__(self): - """Caller is done with this connection. Make sure we cleaned up.""" - self._done() - - def close(self): - """Caller is done with this connection.""" - self._done() - - def create_consumer(self, topic, proxy, fanout=False): - self.connection.create_consumer(topic, proxy, fanout) - - def create_worker(self, topic, proxy, pool_name): - self.connection.create_worker(topic, proxy, pool_name) - - def join_consumer_pool(self, callback, pool_name, topic, exchange_name): - self.connection.join_consumer_pool(callback, - pool_name, - topic, - exchange_name) - - def consume_in_thread(self): - self.connection.consume_in_thread() - - def __getattr__(self, key): - """Proxy all other calls to the Connection instance""" - if self.connection: - return getattr(self.connection, key) - else: - raise rpc_common.InvalidRPCConnectionReuse() - - -class ReplyProxy(ConnectionContext): - """ Connection class for RPC replies / callbacks """ - def __init__(self, conf, connection_pool): - self._call_waiters = {} - self._num_call_waiters = 0 - self._num_call_waiters_wrn_threshhold = 10 - self._reply_q = 'reply_' + uuid.uuid4().hex - super(ReplyProxy, self).__init__(conf, connection_pool, pooled=False) - self.declare_direct_consumer(self._reply_q, self._process_data) - self.consume_in_thread() - - def _process_data(self, message_data): - msg_id = message_data.pop('_msg_id', None) - waiter = self._call_waiters.get(msg_id) - if not waiter: - LOG.warn(_('no calling threads waiting for msg_id : %s' - ', message : %s') % (msg_id, message_data)) - else: - waiter.put(message_data) - - def add_call_waiter(self, waiter, msg_id): - self._num_call_waiters += 1 - if self._num_call_waiters > self._num_call_waiters_wrn_threshhold: - LOG.warn(_('Number of call waiters is greater than warning ' - 'threshhold: %d. There could be a MulticallProxyWaiter ' - 'leak.') % self._num_call_waiters_wrn_threshhold) - self._num_call_waiters_wrn_threshhold *= 2 - self._call_waiters[msg_id] = waiter - - def del_call_waiter(self, msg_id): - self._num_call_waiters -= 1 - del self._call_waiters[msg_id] - - def get_reply_q(self): - return self._reply_q - - -def msg_reply(conf, msg_id, reply_q, connection_pool, reply=None, - failure=None, ending=False, log_failure=True): - """Sends a reply or an error on the channel signified by msg_id. - - Failure should be a sys.exc_info() tuple. - - """ - with ConnectionContext(conf, connection_pool) as conn: - if failure: - failure = rpc_common.serialize_remote_exception(failure, - log_failure) - - try: - msg = {'result': reply, 'failure': failure} - except TypeError: - msg = {'result': dict((k, repr(v)) - for k, v in reply.__dict__.iteritems()), - 'failure': failure} - if ending: - msg['ending'] = True - _add_unique_id(msg) - # If a reply_q exists, add the msg_id to the reply and pass the - # reply_q to direct_send() to use it as the response queue. - # Otherwise use the msg_id for backward compatibilty. - if reply_q: - msg['_msg_id'] = msg_id - conn.direct_send(reply_q, rpc_common.serialize_msg(msg)) - else: - conn.direct_send(msg_id, rpc_common.serialize_msg(msg)) - - -class RpcContext(rpc_common.CommonRpcContext): - """Context that supports replying to a rpc.call""" - def __init__(self, **kwargs): - self.msg_id = kwargs.pop('msg_id', None) - self.reply_q = kwargs.pop('reply_q', None) - self.conf = kwargs.pop('conf') - super(RpcContext, self).__init__(**kwargs) - - def deepcopy(self): - values = self.to_dict() - values['conf'] = self.conf - values['msg_id'] = self.msg_id - values['reply_q'] = self.reply_q - return self.__class__(**values) - - def reply(self, reply=None, failure=None, ending=False, - connection_pool=None, log_failure=True): - if self.msg_id: - msg_reply(self.conf, self.msg_id, self.reply_q, connection_pool, - reply, failure, ending, log_failure) - if ending: - self.msg_id = None - - -def unpack_context(conf, msg): - """Unpack context from msg.""" - context_dict = {} - for key in list(msg.keys()): - # NOTE(vish): Some versions of python don't like unicode keys - # in kwargs. - key = str(key) - if key.startswith('_context_'): - value = msg.pop(key) - context_dict[key[9:]] = value - context_dict['msg_id'] = msg.pop('_msg_id', None) - context_dict['reply_q'] = msg.pop('_reply_q', None) - context_dict['conf'] = conf - ctx = RpcContext.from_dict(context_dict) - rpc_common._safe_log(LOG.debug, _('unpacked context: %s'), ctx.to_dict()) - return ctx - - -def pack_context(msg, context): - """Pack context into msg. - - Values for message keys need to be less than 255 chars, so we pull - context out into a bunch of separate keys. If we want to support - more arguments in rabbit messages, we may want to do the same - for args at some point. - - """ - context_d = dict([('_context_%s' % key, value) - for (key, value) in context.to_dict().iteritems()]) - msg.update(context_d) - - -class _MsgIdCache(object): - """This class checks any duplicate messages.""" - - # NOTE: This value is considered can be a configuration item, but - # it is not necessary to change its value in most cases, - # so let this value as static for now. - DUP_MSG_CHECK_SIZE = 16 - - def __init__(self, **kwargs): - self.prev_msgids = collections.deque([], - maxlen=self.DUP_MSG_CHECK_SIZE) - - def check_duplicate_message(self, message_data): - """AMQP consumers may read same message twice when exceptions occur - before ack is returned. This method prevents doing it. - """ - if UNIQUE_ID in message_data: - msg_id = message_data[UNIQUE_ID] - if msg_id not in self.prev_msgids: - self.prev_msgids.append(msg_id) - else: - raise rpc_common.DuplicateMessageError(msg_id=msg_id) - - -def _add_unique_id(msg): - """Add unique_id for checking duplicate messages.""" - unique_id = uuid.uuid4().hex - msg.update({UNIQUE_ID: unique_id}) - LOG.debug(_('UNIQUE_ID is %s.') % (unique_id)) - - -class _ThreadPoolWithWait(object): - """Base class for a delayed invocation manager used by - the Connection class to start up green threads - to handle incoming messages. - """ - - def __init__(self, conf, connection_pool): - self.pool = greenpool.GreenPool(conf.rpc_thread_pool_size) - self.connection_pool = connection_pool - self.conf = conf - - def wait(self): - """Wait for all callback threads to exit.""" - self.pool.waitall() - - -class CallbackWrapper(_ThreadPoolWithWait): - """Wraps a straight callback to allow it to be invoked in a green - thread. - """ - - def __init__(self, conf, callback, connection_pool): - """ - :param conf: cfg.CONF instance - :param callback: a callable (probably a function) - :param connection_pool: connection pool as returned by - get_connection_pool() - """ - super(CallbackWrapper, self).__init__( - conf=conf, - connection_pool=connection_pool, - ) - self.callback = callback - - def __call__(self, message_data): - self.pool.spawn_n(self.callback, message_data) - - -class ProxyCallback(_ThreadPoolWithWait): - """Calls methods on a proxy object based on method and args.""" - - def __init__(self, conf, proxy, connection_pool): - super(ProxyCallback, self).__init__( - conf=conf, - connection_pool=connection_pool, - ) - self.proxy = proxy - self.msg_id_cache = _MsgIdCache() - - def __call__(self, message_data): - """Consumer callback to call a method on a proxy object. - - Parses the message for validity and fires off a thread to call the - proxy object method. - - Message data should be a dictionary with two keys: - method: string representing the method to call - args: dictionary of arg: value - - Example: {'method': 'echo', 'args': {'value': 42}} - - """ - # It is important to clear the context here, because at this point - # the previous context is stored in local.store.context - if hasattr(local.store, 'context'): - del local.store.context - rpc_common._safe_log(LOG.debug, _('received %s'), message_data) - self.msg_id_cache.check_duplicate_message(message_data) - ctxt = unpack_context(self.conf, message_data) - method = message_data.get('method') - args = message_data.get('args', {}) - version = message_data.get('version') - namespace = message_data.get('namespace') - if not method: - LOG.warn(_('no method for message: %s') % message_data) - ctxt.reply(_('No method for message: %s') % message_data, - connection_pool=self.connection_pool) - return - self.pool.spawn_n(self._process_data, ctxt, version, method, - namespace, args) - - def _process_data(self, ctxt, version, method, namespace, args): - """Process a message in a new thread. - - If the proxy object we have has a dispatch method - (see rpc.dispatcher.RpcDispatcher), pass it the version, - method, and args and let it dispatch as appropriate. If not, use - the old behavior of magically calling the specified method on the - proxy we have here. - """ - ctxt.update_store() - try: - rval = self.proxy.dispatch(ctxt, version, method, namespace, - **args) - # Check if the result was a generator - if inspect.isgenerator(rval): - for x in rval: - ctxt.reply(x, None, connection_pool=self.connection_pool) - else: - ctxt.reply(rval, None, connection_pool=self.connection_pool) - # This final None tells multicall that it is done. - ctxt.reply(ending=True, connection_pool=self.connection_pool) - except rpc_common.ClientException as e: - LOG.debug(_('Expected exception during message handling (%s)') % - e._exc_info[1]) - ctxt.reply(None, e._exc_info, - connection_pool=self.connection_pool, - log_failure=False) - except Exception: - # sys.exc_info() is deleted by LOG.exception(). - exc_info = sys.exc_info() - LOG.error(_('Exception during message handling'), - exc_info=exc_info) - ctxt.reply(None, exc_info, connection_pool=self.connection_pool) - - -class MulticallProxyWaiter(object): - def __init__(self, conf, msg_id, timeout, connection_pool): - self._msg_id = msg_id - self._timeout = timeout or conf.rpc_response_timeout - self._reply_proxy = connection_pool.reply_proxy - self._done = False - self._got_ending = False - self._conf = conf - self._dataqueue = queue.LightQueue() - # Add this caller to the reply proxy's call_waiters - self._reply_proxy.add_call_waiter(self, self._msg_id) - self.msg_id_cache = _MsgIdCache() - - def put(self, data): - self._dataqueue.put(data) - - def done(self): - if self._done: - return - self._done = True - # Remove this caller from reply proxy's call_waiters - self._reply_proxy.del_call_waiter(self._msg_id) - - def _process_data(self, data): - result = None - self.msg_id_cache.check_duplicate_message(data) - if data['failure']: - failure = data['failure'] - result = rpc_common.deserialize_remote_exception(self._conf, - failure) - elif data.get('ending', False): - self._got_ending = True - else: - result = data['result'] - return result - - def __iter__(self): - """Return a result until we get a reply with an 'ending" flag""" - if self._done: - raise StopIteration - while True: - try: - data = self._dataqueue.get(timeout=self._timeout) - result = self._process_data(data) - except queue.Empty: - self.done() - raise rpc_common.Timeout() - except Exception: - with excutils.save_and_reraise_exception(): - self.done() - if self._got_ending: - self.done() - raise StopIteration - if isinstance(result, Exception): - self.done() - raise result - yield result - - -#TODO(pekowski): Remove MulticallWaiter() in Havana. -class MulticallWaiter(object): - def __init__(self, conf, connection, timeout): - self._connection = connection - self._iterator = connection.iterconsume(timeout=timeout or - conf.rpc_response_timeout) - self._result = None - self._done = False - self._got_ending = False - self._conf = conf - self.msg_id_cache = _MsgIdCache() - - def done(self): - if self._done: - return - self._done = True - self._iterator.close() - self._iterator = None - self._connection.close() - - def __call__(self, data): - """The consume() callback will call this. Store the result.""" - self.msg_id_cache.check_duplicate_message(data) - if data['failure']: - failure = data['failure'] - self._result = rpc_common.deserialize_remote_exception(self._conf, - failure) - - elif data.get('ending', False): - self._got_ending = True - else: - self._result = data['result'] - - def __iter__(self): - """Return a result until we get a 'None' response from consumer""" - if self._done: - raise StopIteration - while True: - try: - self._iterator.next() - except Exception: - with excutils.save_and_reraise_exception(): - self.done() - if self._got_ending: - self.done() - raise StopIteration - result = self._result - if isinstance(result, Exception): - self.done() - raise result - yield result - - -def create_connection(conf, new, connection_pool): - """Create a connection""" - return ConnectionContext(conf, connection_pool, pooled=not new) - - -_reply_proxy_create_sem = semaphore.Semaphore() - - -def multicall(conf, context, topic, msg, timeout, connection_pool): - """Make a call that returns multiple times.""" - # TODO(pekowski): Remove all these comments in Havana. - # For amqp_rpc_single_reply_queue = False, - # Can't use 'with' for multicall, as it returns an iterator - # that will continue to use the connection. When it's done, - # connection.close() will get called which will put it back into - # the pool - # For amqp_rpc_single_reply_queue = True, - # The 'with' statement is mandatory for closing the connection - LOG.debug(_('Making synchronous call on %s ...'), topic) - msg_id = uuid.uuid4().hex - msg.update({'_msg_id': msg_id}) - LOG.debug(_('MSG_ID is %s') % (msg_id)) - _add_unique_id(msg) - pack_context(msg, context) - - # TODO(pekowski): Remove this flag and the code under the if clause - # in Havana. - if not conf.amqp_rpc_single_reply_queue: - conn = ConnectionContext(conf, connection_pool) - wait_msg = MulticallWaiter(conf, conn, timeout) - conn.declare_direct_consumer(msg_id, wait_msg) - conn.topic_send(topic, rpc_common.serialize_msg(msg), timeout) - else: - with _reply_proxy_create_sem: - if not connection_pool.reply_proxy: - connection_pool.reply_proxy = ReplyProxy(conf, connection_pool) - msg.update({'_reply_q': connection_pool.reply_proxy.get_reply_q()}) - wait_msg = MulticallProxyWaiter(conf, msg_id, timeout, connection_pool) - with ConnectionContext(conf, connection_pool) as conn: - conn.topic_send(topic, rpc_common.serialize_msg(msg), timeout) - return wait_msg - - -def call(conf, context, topic, msg, timeout, connection_pool): - """Sends a message on a topic and wait for a response.""" - rv = multicall(conf, context, topic, msg, timeout, connection_pool) - # NOTE(vish): return the last result from the multicall - rv = list(rv) - if not rv: - return - return rv[-1] - - -def cast(conf, context, topic, msg, connection_pool): - """Sends a message on a topic without waiting for a response.""" - LOG.debug(_('Making asynchronous cast on %s...'), topic) - _add_unique_id(msg) - pack_context(msg, context) - with ConnectionContext(conf, connection_pool) as conn: - conn.topic_send(topic, rpc_common.serialize_msg(msg)) - - -def fanout_cast(conf, context, topic, msg, connection_pool): - """Sends a message on a fanout exchange without waiting for a response.""" - LOG.debug(_('Making asynchronous fanout cast...')) - _add_unique_id(msg) - pack_context(msg, context) - with ConnectionContext(conf, connection_pool) as conn: - conn.fanout_send(topic, rpc_common.serialize_msg(msg)) - - -def cast_to_server(conf, context, server_params, topic, msg, connection_pool): - """Sends a message on a topic to a specific server.""" - _add_unique_id(msg) - pack_context(msg, context) - with ConnectionContext(conf, connection_pool, pooled=False, - server_params=server_params) as conn: - conn.topic_send(topic, rpc_common.serialize_msg(msg)) - - -def fanout_cast_to_server(conf, context, server_params, topic, msg, - connection_pool): - """Sends a message on a fanout exchange to a specific server.""" - _add_unique_id(msg) - pack_context(msg, context) - with ConnectionContext(conf, connection_pool, pooled=False, - server_params=server_params) as conn: - conn.fanout_send(topic, rpc_common.serialize_msg(msg)) - - -def notify(conf, context, topic, msg, connection_pool, envelope): - """Sends a notification event on a topic.""" - LOG.debug(_('Sending %(event_type)s on %(topic)s'), - dict(event_type=msg.get('event_type'), - topic=topic)) - _add_unique_id(msg) - pack_context(msg, context) - with ConnectionContext(conf, connection_pool) as conn: - if envelope: - msg = rpc_common.serialize_msg(msg) - conn.notify_send(topic, msg) - - -def cleanup(connection_pool): - if connection_pool: - connection_pool.empty() - - -def get_control_exchange(conf): - return conf.control_exchange diff --git a/staccato/openstack/common/rpc/common.py b/staccato/openstack/common/rpc/common.py deleted file mode 100644 index 2ca090a..0000000 --- a/staccato/openstack/common/rpc/common.py +++ /dev/null @@ -1,514 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# Copyright 2011 Red Hat, 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 copy -import sys -import traceback - -from oslo.config import cfg -import six - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import jsonutils -from staccato.openstack.common import local -from staccato.openstack.common import log as logging - - -CONF = cfg.CONF -LOG = logging.getLogger(__name__) - - -'''RPC Envelope Version. - -This version number applies to the top level structure of messages sent out. -It does *not* apply to the message payload, which must be versioned -independently. For example, when using rpc APIs, a version number is applied -for changes to the API being exposed over rpc. This version number is handled -in the rpc proxy and dispatcher modules. - -This version number applies to the message envelope that is used in the -serialization done inside the rpc layer. See serialize_msg() and -deserialize_msg(). - -The current message format (version 2.0) is very simple. It is: - - { - 'oslo.version': , - 'oslo.message': - } - -Message format version '1.0' is just considered to be the messages we sent -without a message envelope. - -So, the current message envelope just includes the envelope version. It may -eventually contain additional information, such as a signature for the message -payload. - -We will JSON encode the application message payload. The message envelope, -which includes the JSON encoded application message body, will be passed down -to the messaging libraries as a dict. -''' -_RPC_ENVELOPE_VERSION = '2.0' - -_VERSION_KEY = 'oslo.version' -_MESSAGE_KEY = 'oslo.message' - - -class RPCException(Exception): - message = _("An unknown RPC related exception occurred.") - - def __init__(self, message=None, **kwargs): - self.kwargs = kwargs - - if not message: - try: - message = self.message % kwargs - - except Exception: - # kwargs doesn't match a variable in the message - # log the issue and the kwargs - LOG.exception(_('Exception in string format operation')) - for name, value in kwargs.iteritems(): - LOG.error("%s: %s" % (name, value)) - # at least get the core message out if something happened - message = self.message - - super(RPCException, self).__init__(message) - - -class RemoteError(RPCException): - """Signifies that a remote class has raised an exception. - - Contains a string representation of the type of the original exception, - the value of the original exception, and the traceback. These are - sent to the parent as a joined string so printing the exception - contains all of the relevant info. - - """ - message = _("Remote error: %(exc_type)s %(value)s\n%(traceback)s.") - - def __init__(self, exc_type=None, value=None, traceback=None): - self.exc_type = exc_type - self.value = value - self.traceback = traceback - super(RemoteError, self).__init__(exc_type=exc_type, - value=value, - traceback=traceback) - - -class Timeout(RPCException): - """Signifies that a timeout has occurred. - - This exception is raised if the rpc_response_timeout is reached while - waiting for a response from the remote side. - """ - message = _('Timeout while waiting on RPC response - ' - 'topic: "%(topic)s", RPC method: "%(method)s" ' - 'info: "%(info)s"') - - def __init__(self, info=None, topic=None, method=None): - """ - :param info: Extra info to convey to the user - :param topic: The topic that the rpc call was sent to - :param rpc_method_name: The name of the rpc method being - called - """ - self.info = info - self.topic = topic - self.method = method - super(Timeout, self).__init__( - None, - info=info or _(''), - topic=topic or _(''), - method=method or _('')) - - -class DuplicateMessageError(RPCException): - message = _("Found duplicate message(%(msg_id)s). Skipping it.") - - -class InvalidRPCConnectionReuse(RPCException): - message = _("Invalid reuse of an RPC connection.") - - -class UnsupportedRpcVersion(RPCException): - message = _("Specified RPC version, %(version)s, not supported by " - "this endpoint.") - - -class UnsupportedRpcEnvelopeVersion(RPCException): - message = _("Specified RPC envelope version, %(version)s, " - "not supported by this endpoint.") - - -class RpcVersionCapError(RPCException): - message = _("Specified RPC version cap, %(version_cap)s, is too low") - - -class Connection(object): - """A connection, returned by rpc.create_connection(). - - This class represents a connection to the message bus used for rpc. - An instance of this class should never be created by users of the rpc API. - Use rpc.create_connection() instead. - """ - def close(self): - """Close the connection. - - This method must be called when the connection will no longer be used. - It will ensure that any resources associated with the connection, such - as a network connection, and cleaned up. - """ - raise NotImplementedError() - - def create_consumer(self, topic, proxy, fanout=False): - """Create a consumer on this connection. - - A consumer is associated with a message queue on the backend message - bus. The consumer will read messages from the queue, unpack them, and - dispatch them to the proxy object. The contents of the message pulled - off of the queue will determine which method gets called on the proxy - object. - - :param topic: This is a name associated with what to consume from. - Multiple instances of a service may consume from the same - topic. For example, all instances of nova-compute consume - from a queue called "compute". In that case, the - messages will get distributed amongst the consumers in a - round-robin fashion if fanout=False. If fanout=True, - every consumer associated with this topic will get a - copy of every message. - :param proxy: The object that will handle all incoming messages. - :param fanout: Whether or not this is a fanout topic. See the - documentation for the topic parameter for some - additional comments on this. - """ - raise NotImplementedError() - - def create_worker(self, topic, proxy, pool_name): - """Create a worker on this connection. - - A worker is like a regular consumer of messages directed to a - topic, except that it is part of a set of such consumers (the - "pool") which may run in parallel. Every pool of workers will - receive a given message, but only one worker in the pool will - be asked to process it. Load is distributed across the members - of the pool in round-robin fashion. - - :param topic: This is a name associated with what to consume from. - Multiple instances of a service may consume from the same - topic. - :param proxy: The object that will handle all incoming messages. - :param pool_name: String containing the name of the pool of workers - """ - raise NotImplementedError() - - def join_consumer_pool(self, callback, pool_name, topic, exchange_name): - """Register as a member of a group of consumers for a given topic from - the specified exchange. - - Exactly one member of a given pool will receive each message. - - A message will be delivered to multiple pools, if more than - one is created. - - :param callback: Callable to be invoked for each message. - :type callback: callable accepting one argument - :param pool_name: The name of the consumer pool. - :type pool_name: str - :param topic: The routing topic for desired messages. - :type topic: str - :param exchange_name: The name of the message exchange where - the client should attach. Defaults to - the configured exchange. - :type exchange_name: str - """ - raise NotImplementedError() - - def consume_in_thread(self): - """Spawn a thread to handle incoming messages. - - Spawn a thread that will be responsible for handling all incoming - messages for consumers that were set up on this connection. - - Message dispatching inside of this is expected to be implemented in a - non-blocking manner. An example implementation would be having this - thread pull messages in for all of the consumers, but utilize a thread - pool for dispatching the messages to the proxy objects. - """ - raise NotImplementedError() - - -def _safe_log(log_func, msg, msg_data): - """Sanitizes the msg_data field before logging.""" - SANITIZE = {'set_admin_password': [('args', 'new_pass')], - 'run_instance': [('args', 'admin_password')], - 'route_message': [('args', 'message', 'args', 'method_info', - 'method_kwargs', 'password'), - ('args', 'message', 'args', 'method_info', - 'method_kwargs', 'admin_password')]} - - has_method = 'method' in msg_data and msg_data['method'] in SANITIZE - has_context_token = '_context_auth_token' in msg_data - has_token = 'auth_token' in msg_data - - if not any([has_method, has_context_token, has_token]): - return log_func(msg, msg_data) - - msg_data = copy.deepcopy(msg_data) - - if has_method: - for arg in SANITIZE.get(msg_data['method'], []): - try: - d = msg_data - for elem in arg[:-1]: - d = d[elem] - d[arg[-1]] = '' - except KeyError as e: - LOG.info(_('Failed to sanitize %(item)s. Key error %(err)s'), - {'item': arg, - 'err': e}) - - if has_context_token: - msg_data['_context_auth_token'] = '' - - if has_token: - msg_data['auth_token'] = '' - - return log_func(msg, msg_data) - - -def serialize_remote_exception(failure_info, log_failure=True): - """Prepares exception data to be sent over rpc. - - Failure_info should be a sys.exc_info() tuple. - - """ - tb = traceback.format_exception(*failure_info) - failure = failure_info[1] - if log_failure: - LOG.error(_("Returning exception %s to caller"), - six.text_type(failure)) - LOG.error(tb) - - kwargs = {} - if hasattr(failure, 'kwargs'): - kwargs = failure.kwargs - - data = { - 'class': str(failure.__class__.__name__), - 'module': str(failure.__class__.__module__), - 'message': six.text_type(failure), - 'tb': tb, - 'args': failure.args, - 'kwargs': kwargs - } - - json_data = jsonutils.dumps(data) - - return json_data - - -def deserialize_remote_exception(conf, data): - failure = jsonutils.loads(str(data)) - - trace = failure.get('tb', []) - message = failure.get('message', "") + "\n" + "\n".join(trace) - name = failure.get('class') - module = failure.get('module') - - # NOTE(ameade): We DO NOT want to allow just any module to be imported, in - # order to prevent arbitrary code execution. - if module not in conf.allowed_rpc_exception_modules: - return RemoteError(name, failure.get('message'), trace) - - try: - mod = importutils.import_module(module) - klass = getattr(mod, name) - if not issubclass(klass, Exception): - raise TypeError("Can only deserialize Exceptions") - - failure = klass(*failure.get('args', []), **failure.get('kwargs', {})) - except (AttributeError, TypeError, ImportError): - return RemoteError(name, failure.get('message'), trace) - - ex_type = type(failure) - str_override = lambda self: message - new_ex_type = type(ex_type.__name__ + "_Remote", (ex_type,), - {'__str__': str_override, '__unicode__': str_override}) - try: - # NOTE(ameade): Dynamically create a new exception type and swap it in - # as the new type for the exception. This only works on user defined - # Exceptions and not core python exceptions. This is important because - # we cannot necessarily change an exception message so we must override - # the __str__ method. - failure.__class__ = new_ex_type - except TypeError: - # NOTE(ameade): If a core exception then just add the traceback to the - # first exception argument. - failure.args = (message,) + failure.args[1:] - return failure - - -class CommonRpcContext(object): - def __init__(self, **kwargs): - self.values = kwargs - - def __getattr__(self, key): - try: - return self.values[key] - except KeyError: - raise AttributeError(key) - - def to_dict(self): - return copy.deepcopy(self.values) - - @classmethod - def from_dict(cls, values): - return cls(**values) - - def deepcopy(self): - return self.from_dict(self.to_dict()) - - def update_store(self): - local.store.context = self - - def elevated(self, read_deleted=None, overwrite=False): - """Return a version of this context with admin flag set.""" - # TODO(russellb) This method is a bit of a nova-ism. It makes - # some assumptions about the data in the request context sent - # across rpc, while the rest of this class does not. We could get - # rid of this if we changed the nova code that uses this to - # convert the RpcContext back to its native RequestContext doing - # something like nova.context.RequestContext.from_dict(ctxt.to_dict()) - - context = self.deepcopy() - context.values['is_admin'] = True - - context.values.setdefault('roles', []) - - if 'admin' not in context.values['roles']: - context.values['roles'].append('admin') - - if read_deleted is not None: - context.values['read_deleted'] = read_deleted - - return context - - -class ClientException(Exception): - """This encapsulates some actual exception that is expected to be - hit by an RPC proxy object. Merely instantiating it records the - current exception information, which will be passed back to the - RPC client without exceptional logging.""" - def __init__(self): - self._exc_info = sys.exc_info() - - -def catch_client_exception(exceptions, func, *args, **kwargs): - try: - return func(*args, **kwargs) - except Exception as e: - if type(e) in exceptions: - raise ClientException() - else: - raise - - -def client_exceptions(*exceptions): - """Decorator for manager methods that raise expected exceptions. - Marking a Manager method with this decorator allows the declaration - of expected exceptions that the RPC layer should not consider fatal, - and not log as if they were generated in a real error scenario. Note - that this will cause listed exceptions to be wrapped in a - ClientException, which is used internally by the RPC layer.""" - def outer(func): - def inner(*args, **kwargs): - return catch_client_exception(exceptions, func, *args, **kwargs) - return inner - return outer - - -def version_is_compatible(imp_version, version): - """Determine whether versions are compatible. - - :param imp_version: The version implemented - :param version: The version requested by an incoming message. - """ - version_parts = version.split('.') - imp_version_parts = imp_version.split('.') - if int(version_parts[0]) != int(imp_version_parts[0]): # Major - return False - if int(version_parts[1]) > int(imp_version_parts[1]): # Minor - return False - return True - - -def serialize_msg(raw_msg): - # NOTE(russellb) See the docstring for _RPC_ENVELOPE_VERSION for more - # information about this format. - msg = {_VERSION_KEY: _RPC_ENVELOPE_VERSION, - _MESSAGE_KEY: jsonutils.dumps(raw_msg)} - - return msg - - -def deserialize_msg(msg): - # NOTE(russellb): Hang on to your hats, this road is about to - # get a little bumpy. - # - # Robustness Principle: - # "Be strict in what you send, liberal in what you accept." - # - # At this point we have to do a bit of guessing about what it - # is we just received. Here is the set of possibilities: - # - # 1) We received a dict. This could be 2 things: - # - # a) Inspect it to see if it looks like a standard message envelope. - # If so, great! - # - # b) If it doesn't look like a standard message envelope, it could either - # be a notification, or a message from before we added a message - # envelope (referred to as version 1.0). - # Just return the message as-is. - # - # 2) It's any other non-dict type. Just return it and hope for the best. - # This case covers return values from rpc.call() from before message - # envelopes were used. (messages to call a method were always a dict) - - if not isinstance(msg, dict): - # See #2 above. - return msg - - base_envelope_keys = (_VERSION_KEY, _MESSAGE_KEY) - if not all(map(lambda key: key in msg, base_envelope_keys)): - # See #1.b above. - return msg - - # At this point we think we have the message envelope - # format we were expecting. (#1.a above) - - if not version_is_compatible(_RPC_ENVELOPE_VERSION, msg[_VERSION_KEY]): - raise UnsupportedRpcEnvelopeVersion(version=msg[_VERSION_KEY]) - - raw_msg = jsonutils.loads(msg[_MESSAGE_KEY]) - - return raw_msg diff --git a/staccato/openstack/common/rpc/dispatcher.py b/staccato/openstack/common/rpc/dispatcher.py deleted file mode 100644 index 65b9b5b..0000000 --- a/staccato/openstack/common/rpc/dispatcher.py +++ /dev/null @@ -1,153 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, 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. - -""" -Code for rpc message dispatching. - -Messages that come in have a version number associated with them. RPC API -version numbers are in the form: - - Major.Minor - -For a given message with version X.Y, the receiver must be marked as able to -handle messages of version A.B, where: - - A = X - - B >= Y - -The Major version number would be incremented for an almost completely new API. -The Minor version number would be incremented for backwards compatible changes -to an existing API. A backwards compatible change could be something like -adding a new method, adding an argument to an existing method (but not -requiring it), or changing the type for an existing argument (but still -handling the old type as well). - -The conversion over to a versioned API must be done on both the client side and -server side of the API at the same time. However, as the code stands today, -there can be both versioned and unversioned APIs implemented in the same code -base. - -EXAMPLES -======== - -Nova was the first project to use versioned rpc APIs. Consider the compute rpc -API as an example. The client side is in nova/compute/rpcapi.py and the server -side is in nova/compute/manager.py. - - -Example 1) Adding a new method. -------------------------------- - -Adding a new method is a backwards compatible change. It should be added to -nova/compute/manager.py, and RPC_API_VERSION should be bumped from X.Y to -X.Y+1. On the client side, the new method in nova/compute/rpcapi.py should -have a specific version specified to indicate the minimum API version that must -be implemented for the method to be supported. For example:: - - def get_host_uptime(self, ctxt, host): - topic = _compute_topic(self.topic, ctxt, host, None) - return self.call(ctxt, self.make_msg('get_host_uptime'), topic, - version='1.1') - -In this case, version '1.1' is the first version that supported the -get_host_uptime() method. - - -Example 2) Adding a new parameter. ----------------------------------- - -Adding a new parameter to an rpc method can be made backwards compatible. The -RPC_API_VERSION on the server side (nova/compute/manager.py) should be bumped. -The implementation of the method must not expect the parameter to be present.:: - - def some_remote_method(self, arg1, arg2, newarg=None): - # The code needs to deal with newarg=None for cases - # where an older client sends a message without it. - pass - -On the client side, the same changes should be made as in example 1. The -minimum version that supports the new parameter should be specified. -""" - -from staccato.openstack.common.rpc import common as rpc_common - - -class RpcDispatcher(object): - """Dispatch rpc messages according to the requested API version. - - This class can be used as the top level 'manager' for a service. It - contains a list of underlying managers that have an API_VERSION attribute. - """ - - def __init__(self, callbacks): - """Initialize the rpc dispatcher. - - :param callbacks: List of proxy objects that are an instance - of a class with rpc methods exposed. Each proxy - object should have an RPC_API_VERSION attribute. - """ - self.callbacks = callbacks - super(RpcDispatcher, self).__init__() - - def dispatch(self, ctxt, version, method, namespace, **kwargs): - """Dispatch a message based on a requested version. - - :param ctxt: The request context - :param version: The requested API version from the incoming message - :param method: The method requested to be called by the incoming - message. - :param namespace: The namespace for the requested method. If None, - the dispatcher will look for a method on a callback - object with no namespace set. - :param kwargs: A dict of keyword arguments to be passed to the method. - - :returns: Whatever is returned by the underlying method that gets - called. - """ - if not version: - version = '1.0' - - had_compatible = False - for proxyobj in self.callbacks: - # Check for namespace compatibility - try: - cb_namespace = proxyobj.RPC_API_NAMESPACE - except AttributeError: - cb_namespace = None - - if namespace != cb_namespace: - continue - - # Check for version compatibility - try: - rpc_api_version = proxyobj.RPC_API_VERSION - except AttributeError: - rpc_api_version = '1.0' - - is_compatible = rpc_common.version_is_compatible(rpc_api_version, - version) - had_compatible = had_compatible or is_compatible - - if not hasattr(proxyobj, method): - continue - if is_compatible: - return getattr(proxyobj, method)(ctxt, **kwargs) - - if had_compatible: - raise AttributeError("No such RPC function '%s'" % method) - else: - raise rpc_common.UnsupportedRpcVersion(version=version) diff --git a/staccato/openstack/common/rpc/impl_fake.py b/staccato/openstack/common/rpc/impl_fake.py deleted file mode 100644 index 019d046..0000000 --- a/staccato/openstack/common/rpc/impl_fake.py +++ /dev/null @@ -1,195 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation -# -# 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. -"""Fake RPC implementation which calls proxy methods directly with no -queues. Casts will block, but this is very useful for tests. -""" - -import inspect -# NOTE(russellb): We specifically want to use json, not our own jsonutils. -# jsonutils has some extra logic to automatically convert objects to primitive -# types so that they can be serialized. We want to catch all cases where -# non-primitive types make it into this code and treat it as an error. -import json -import time - -import eventlet - -from staccato.openstack.common.rpc import common as rpc_common - -CONSUMERS = {} - - -class RpcContext(rpc_common.CommonRpcContext): - def __init__(self, **kwargs): - super(RpcContext, self).__init__(**kwargs) - self._response = [] - self._done = False - - def deepcopy(self): - values = self.to_dict() - new_inst = self.__class__(**values) - new_inst._response = self._response - new_inst._done = self._done - return new_inst - - def reply(self, reply=None, failure=None, ending=False): - if ending: - self._done = True - if not self._done: - self._response.append((reply, failure)) - - -class Consumer(object): - def __init__(self, topic, proxy): - self.topic = topic - self.proxy = proxy - - def call(self, context, version, method, namespace, args, timeout): - done = eventlet.event.Event() - - def _inner(): - ctxt = RpcContext.from_dict(context.to_dict()) - try: - rval = self.proxy.dispatch(context, version, method, - namespace, **args) - res = [] - # Caller might have called ctxt.reply() manually - for (reply, failure) in ctxt._response: - if failure: - raise failure[0], failure[1], failure[2] - res.append(reply) - # if ending not 'sent'...we might have more data to - # return from the function itself - if not ctxt._done: - if inspect.isgenerator(rval): - for val in rval: - res.append(val) - else: - res.append(rval) - done.send(res) - except rpc_common.ClientException as e: - done.send_exception(e._exc_info[1]) - except Exception as e: - done.send_exception(e) - - thread = eventlet.greenthread.spawn(_inner) - - if timeout: - start_time = time.time() - while not done.ready(): - eventlet.greenthread.sleep(1) - cur_time = time.time() - if (cur_time - start_time) > timeout: - thread.kill() - raise rpc_common.Timeout() - - return done.wait() - - -class Connection(object): - """Connection object.""" - - def __init__(self): - self.consumers = [] - - def create_consumer(self, topic, proxy, fanout=False): - consumer = Consumer(topic, proxy) - self.consumers.append(consumer) - if topic not in CONSUMERS: - CONSUMERS[topic] = [] - CONSUMERS[topic].append(consumer) - - def close(self): - for consumer in self.consumers: - CONSUMERS[consumer.topic].remove(consumer) - self.consumers = [] - - def consume_in_thread(self): - pass - - -def create_connection(conf, new=True): - """Create a connection""" - return Connection() - - -def check_serialize(msg): - """Make sure a message intended for rpc can be serialized.""" - json.dumps(msg) - - -def multicall(conf, context, topic, msg, timeout=None): - """Make a call that returns multiple times.""" - - check_serialize(msg) - - method = msg.get('method') - if not method: - return - args = msg.get('args', {}) - version = msg.get('version', None) - namespace = msg.get('namespace', None) - - try: - consumer = CONSUMERS[topic][0] - except (KeyError, IndexError): - return iter([None]) - else: - return consumer.call(context, version, method, namespace, args, - timeout) - - -def call(conf, context, topic, msg, timeout=None): - """Sends a message on a topic and wait for a response.""" - rv = multicall(conf, context, topic, msg, timeout) - # NOTE(vish): return the last result from the multicall - rv = list(rv) - if not rv: - return - return rv[-1] - - -def cast(conf, context, topic, msg): - check_serialize(msg) - try: - call(conf, context, topic, msg) - except Exception: - pass - - -def notify(conf, context, topic, msg, envelope): - check_serialize(msg) - - -def cleanup(): - pass - - -def fanout_cast(conf, context, topic, msg): - """Cast to all consumers of a topic""" - check_serialize(msg) - method = msg.get('method') - if not method: - return - args = msg.get('args', {}) - version = msg.get('version', None) - namespace = msg.get('namespace', None) - - for consumer in CONSUMERS.get(topic, []): - try: - consumer.call(context, version, method, namespace, args, None) - except Exception: - pass diff --git a/staccato/openstack/common/rpc/impl_kombu.py b/staccato/openstack/common/rpc/impl_kombu.py deleted file mode 100644 index 7901a0a..0000000 --- a/staccato/openstack/common/rpc/impl_kombu.py +++ /dev/null @@ -1,838 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation -# -# 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 functools -import itertools -import socket -import ssl -import sys -import time -import uuid - -import eventlet -import greenlet -import kombu -import kombu.connection -import kombu.entity -import kombu.messaging -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import network_utils -from staccato.openstack.common.rpc import amqp as rpc_amqp -from staccato.openstack.common.rpc import common as rpc_common - -kombu_opts = [ - cfg.StrOpt('kombu_ssl_version', - default='', - help='SSL version to use (valid only if SSL enabled)'), - cfg.StrOpt('kombu_ssl_keyfile', - default='', - help='SSL key file (valid only if SSL enabled)'), - cfg.StrOpt('kombu_ssl_certfile', - default='', - help='SSL cert file (valid only if SSL enabled)'), - cfg.StrOpt('kombu_ssl_ca_certs', - default='', - help=('SSL certification authority file ' - '(valid only if SSL enabled)')), - cfg.StrOpt('rabbit_host', - default='localhost', - help='The RabbitMQ broker address where a single node is used'), - cfg.IntOpt('rabbit_port', - default=5672, - help='The RabbitMQ broker port where a single node is used'), - cfg.ListOpt('rabbit_hosts', - default=['$rabbit_host:$rabbit_port'], - help='RabbitMQ HA cluster host:port pairs'), - cfg.BoolOpt('rabbit_use_ssl', - default=False, - help='connect over SSL for RabbitMQ'), - cfg.StrOpt('rabbit_userid', - default='guest', - help='the RabbitMQ userid'), - cfg.StrOpt('rabbit_password', - default='guest', - help='the RabbitMQ password', - secret=True), - cfg.StrOpt('rabbit_virtual_host', - default='/', - help='the RabbitMQ virtual host'), - cfg.IntOpt('rabbit_retry_interval', - default=1, - help='how frequently to retry connecting with RabbitMQ'), - cfg.IntOpt('rabbit_retry_backoff', - default=2, - help='how long to backoff for between retries when connecting ' - 'to RabbitMQ'), - cfg.IntOpt('rabbit_max_retries', - default=0, - help='maximum retries with trying to connect to RabbitMQ ' - '(the default of 0 implies an infinite retry count)'), - cfg.BoolOpt('rabbit_durable_queues', - default=False, - help='use durable queues in RabbitMQ'), - cfg.BoolOpt('rabbit_ha_queues', - default=False, - help='use H/A queues in RabbitMQ (x-ha-policy: all).' - 'You need to wipe RabbitMQ database when ' - 'changing this option.'), - -] - -cfg.CONF.register_opts(kombu_opts) - -LOG = rpc_common.LOG - - -def _get_queue_arguments(conf): - """Construct the arguments for declaring a queue. - - If the rabbit_ha_queues option is set, we declare a mirrored queue - as described here: - - http://www.rabbitmq.com/ha.html - - Setting x-ha-policy to all means that the queue will be mirrored - to all nodes in the cluster. - """ - return {'x-ha-policy': 'all'} if conf.rabbit_ha_queues else {} - - -class ConsumerBase(object): - """Consumer base class.""" - - def __init__(self, channel, callback, tag, **kwargs): - """Declare a queue on an amqp channel. - - 'channel' is the amqp channel to use - 'callback' is the callback to call when messages are received - 'tag' is a unique ID for the consumer on the channel - - queue name, exchange name, and other kombu options are - passed in here as a dictionary. - """ - self.callback = callback - self.tag = str(tag) - self.kwargs = kwargs - self.queue = None - self.reconnect(channel) - - def reconnect(self, channel): - """Re-declare the queue after a rabbit reconnect""" - self.channel = channel - self.kwargs['channel'] = channel - self.queue = kombu.entity.Queue(**self.kwargs) - self.queue.declare() - - def consume(self, *args, **kwargs): - """Actually declare the consumer on the amqp channel. This will - start the flow of messages from the queue. Using the - Connection.iterconsume() iterator will process the messages, - calling the appropriate callback. - - If a callback is specified in kwargs, use that. Otherwise, - use the callback passed during __init__() - - If kwargs['nowait'] is True, then this call will block until - a message is read. - - Messages will automatically be acked if the callback doesn't - raise an exception - """ - - options = {'consumer_tag': self.tag} - options['nowait'] = kwargs.get('nowait', False) - callback = kwargs.get('callback', self.callback) - if not callback: - raise ValueError("No callback defined") - - def _callback(raw_message): - message = self.channel.message_to_python(raw_message) - try: - msg = rpc_common.deserialize_msg(message.payload) - callback(msg) - except Exception: - LOG.exception(_("Failed to process message... skipping it.")) - finally: - message.ack() - - self.queue.consume(*args, callback=_callback, **options) - - def cancel(self): - """Cancel the consuming from the queue, if it has started""" - try: - self.queue.cancel(self.tag) - except KeyError as e: - # NOTE(comstud): Kludge to get around a amqplib bug - if str(e) != "u'%s'" % self.tag: - raise - self.queue = None - - -class DirectConsumer(ConsumerBase): - """Queue/consumer class for 'direct'""" - - def __init__(self, conf, channel, msg_id, callback, tag, **kwargs): - """Init a 'direct' queue. - - 'channel' is the amqp channel to use - 'msg_id' is the msg_id to listen on - 'callback' is the callback to call when messages are received - 'tag' is a unique ID for the consumer on the channel - - Other kombu options may be passed - """ - # Default options - options = {'durable': False, - 'queue_arguments': _get_queue_arguments(conf), - 'auto_delete': True, - 'exclusive': False} - options.update(kwargs) - exchange = kombu.entity.Exchange(name=msg_id, - type='direct', - durable=options['durable'], - auto_delete=options['auto_delete']) - super(DirectConsumer, self).__init__(channel, - callback, - tag, - name=msg_id, - exchange=exchange, - routing_key=msg_id, - **options) - - -class TopicConsumer(ConsumerBase): - """Consumer class for 'topic'""" - - def __init__(self, conf, channel, topic, callback, tag, name=None, - exchange_name=None, **kwargs): - """Init a 'topic' queue. - - :param channel: the amqp channel to use - :param topic: the topic to listen on - :paramtype topic: str - :param callback: the callback to call when messages are received - :param tag: a unique ID for the consumer on the channel - :param name: optional queue name, defaults to topic - :paramtype name: str - - Other kombu options may be passed as keyword arguments - """ - # Default options - options = {'durable': conf.rabbit_durable_queues, - 'queue_arguments': _get_queue_arguments(conf), - 'auto_delete': False, - 'exclusive': False} - options.update(kwargs) - exchange_name = exchange_name or rpc_amqp.get_control_exchange(conf) - exchange = kombu.entity.Exchange(name=exchange_name, - type='topic', - durable=options['durable'], - auto_delete=options['auto_delete']) - super(TopicConsumer, self).__init__(channel, - callback, - tag, - name=name or topic, - exchange=exchange, - routing_key=topic, - **options) - - -class FanoutConsumer(ConsumerBase): - """Consumer class for 'fanout'""" - - def __init__(self, conf, channel, topic, callback, tag, **kwargs): - """Init a 'fanout' queue. - - 'channel' is the amqp channel to use - 'topic' is the topic to listen on - 'callback' is the callback to call when messages are received - 'tag' is a unique ID for the consumer on the channel - - Other kombu options may be passed - """ - unique = uuid.uuid4().hex - exchange_name = '%s_fanout' % topic - queue_name = '%s_fanout_%s' % (topic, unique) - - # Default options - options = {'durable': False, - 'queue_arguments': _get_queue_arguments(conf), - 'auto_delete': True, - 'exclusive': False} - options.update(kwargs) - exchange = kombu.entity.Exchange(name=exchange_name, type='fanout', - durable=options['durable'], - auto_delete=options['auto_delete']) - super(FanoutConsumer, self).__init__(channel, callback, tag, - name=queue_name, - exchange=exchange, - routing_key=topic, - **options) - - -class Publisher(object): - """Base Publisher class""" - - def __init__(self, channel, exchange_name, routing_key, **kwargs): - """Init the Publisher class with the exchange_name, routing_key, - and other options - """ - self.exchange_name = exchange_name - self.routing_key = routing_key - self.kwargs = kwargs - self.reconnect(channel) - - def reconnect(self, channel): - """Re-establish the Producer after a rabbit reconnection""" - self.exchange = kombu.entity.Exchange(name=self.exchange_name, - **self.kwargs) - self.producer = kombu.messaging.Producer(exchange=self.exchange, - channel=channel, - routing_key=self.routing_key) - - def send(self, msg, timeout=None): - """Send a message""" - if timeout: - # - # AMQP TTL is in milliseconds when set in the header. - # - self.producer.publish(msg, headers={'ttl': (timeout * 1000)}) - else: - self.producer.publish(msg) - - -class DirectPublisher(Publisher): - """Publisher class for 'direct'""" - def __init__(self, conf, channel, msg_id, **kwargs): - """init a 'direct' publisher. - - Kombu options may be passed as keyword args to override defaults - """ - - options = {'durable': False, - 'auto_delete': True, - 'exclusive': False} - options.update(kwargs) - super(DirectPublisher, self).__init__(channel, msg_id, msg_id, - type='direct', **options) - - -class TopicPublisher(Publisher): - """Publisher class for 'topic'""" - def __init__(self, conf, channel, topic, **kwargs): - """init a 'topic' publisher. - - Kombu options may be passed as keyword args to override defaults - """ - options = {'durable': conf.rabbit_durable_queues, - 'auto_delete': False, - 'exclusive': False} - options.update(kwargs) - exchange_name = rpc_amqp.get_control_exchange(conf) - super(TopicPublisher, self).__init__(channel, - exchange_name, - topic, - type='topic', - **options) - - -class FanoutPublisher(Publisher): - """Publisher class for 'fanout'""" - def __init__(self, conf, channel, topic, **kwargs): - """init a 'fanout' publisher. - - Kombu options may be passed as keyword args to override defaults - """ - options = {'durable': False, - 'auto_delete': True, - 'exclusive': False} - options.update(kwargs) - super(FanoutPublisher, self).__init__(channel, '%s_fanout' % topic, - None, type='fanout', **options) - - -class NotifyPublisher(TopicPublisher): - """Publisher class for 'notify'""" - - def __init__(self, conf, channel, topic, **kwargs): - self.durable = kwargs.pop('durable', conf.rabbit_durable_queues) - self.queue_arguments = _get_queue_arguments(conf) - super(NotifyPublisher, self).__init__(conf, channel, topic, **kwargs) - - def reconnect(self, channel): - super(NotifyPublisher, self).reconnect(channel) - - # NOTE(jerdfelt): Normally the consumer would create the queue, but - # we do this to ensure that messages don't get dropped if the - # consumer is started after we do - queue = kombu.entity.Queue(channel=channel, - exchange=self.exchange, - durable=self.durable, - name=self.routing_key, - routing_key=self.routing_key, - queue_arguments=self.queue_arguments) - queue.declare() - - -class Connection(object): - """Connection object.""" - - pool = None - - def __init__(self, conf, server_params=None): - self.consumers = [] - self.consumer_thread = None - self.proxy_callbacks = [] - self.conf = conf - self.max_retries = self.conf.rabbit_max_retries - # Try forever? - if self.max_retries <= 0: - self.max_retries = None - self.interval_start = self.conf.rabbit_retry_interval - self.interval_stepping = self.conf.rabbit_retry_backoff - # max retry-interval = 30 seconds - self.interval_max = 30 - self.memory_transport = False - - if server_params is None: - server_params = {} - # Keys to translate from server_params to kombu params - server_params_to_kombu_params = {'username': 'userid'} - - ssl_params = self._fetch_ssl_params() - params_list = [] - for adr in self.conf.rabbit_hosts: - hostname, port = network_utils.parse_host_port( - adr, default_port=self.conf.rabbit_port) - - params = { - 'hostname': hostname, - 'port': port, - 'userid': self.conf.rabbit_userid, - 'password': self.conf.rabbit_password, - 'virtual_host': self.conf.rabbit_virtual_host, - } - - for sp_key, value in server_params.iteritems(): - p_key = server_params_to_kombu_params.get(sp_key, sp_key) - params[p_key] = value - - if self.conf.fake_rabbit: - params['transport'] = 'memory' - if self.conf.rabbit_use_ssl: - params['ssl'] = ssl_params - - params_list.append(params) - - self.params_list = params_list - - self.memory_transport = self.conf.fake_rabbit - - self.connection = None - self.reconnect() - - def _fetch_ssl_params(self): - """Handles fetching what ssl params - should be used for the connection (if any)""" - ssl_params = dict() - - # http://docs.python.org/library/ssl.html - ssl.wrap_socket - if self.conf.kombu_ssl_version: - ssl_params['ssl_version'] = self.conf.kombu_ssl_version - if self.conf.kombu_ssl_keyfile: - ssl_params['keyfile'] = self.conf.kombu_ssl_keyfile - if self.conf.kombu_ssl_certfile: - ssl_params['certfile'] = self.conf.kombu_ssl_certfile - if self.conf.kombu_ssl_ca_certs: - ssl_params['ca_certs'] = self.conf.kombu_ssl_ca_certs - # We might want to allow variations in the - # future with this? - ssl_params['cert_reqs'] = ssl.CERT_REQUIRED - - if not ssl_params: - # Just have the default behavior - return True - else: - # Return the extended behavior - return ssl_params - - def _connect(self, params): - """Connect to rabbit. Re-establish any queues that may have - been declared before if we are reconnecting. Exceptions should - be handled by the caller. - """ - if self.connection: - LOG.info(_("Reconnecting to AMQP server on " - "%(hostname)s:%(port)d") % params) - try: - self.connection.release() - except self.connection_errors: - pass - # Setting this in case the next statement fails, though - # it shouldn't be doing any network operations, yet. - self.connection = None - self.connection = kombu.connection.BrokerConnection(**params) - self.connection_errors = self.connection.connection_errors - if self.memory_transport: - # Kludge to speed up tests. - self.connection.transport.polling_interval = 0.0 - self.consumer_num = itertools.count(1) - self.connection.connect() - self.channel = self.connection.channel() - # work around 'memory' transport bug in 1.1.3 - if self.memory_transport: - self.channel._new_queue('ae.undeliver') - for consumer in self.consumers: - consumer.reconnect(self.channel) - LOG.info(_('Connected to AMQP server on %(hostname)s:%(port)d') % - params) - - def reconnect(self): - """Handles reconnecting and re-establishing queues. - Will retry up to self.max_retries number of times. - self.max_retries = 0 means to retry forever. - Sleep between tries, starting at self.interval_start - seconds, backing off self.interval_stepping number of seconds - each attempt. - """ - - attempt = 0 - while True: - params = self.params_list[attempt % len(self.params_list)] - attempt += 1 - try: - self._connect(params) - return - except (IOError, self.connection_errors) as e: - pass - except Exception as e: - # NOTE(comstud): Unfortunately it's possible for amqplib - # to return an error not covered by its transport - # connection_errors in the case of a timeout waiting for - # a protocol response. (See paste link in LP888621) - # So, we check all exceptions for 'timeout' in them - # and try to reconnect in this case. - if 'timeout' not in str(e): - raise - - log_info = {} - log_info['err_str'] = str(e) - log_info['max_retries'] = self.max_retries - log_info.update(params) - - if self.max_retries and attempt == self.max_retries: - LOG.error(_('Unable to connect to AMQP server on ' - '%(hostname)s:%(port)d after %(max_retries)d ' - 'tries: %(err_str)s') % log_info) - # NOTE(comstud): Copied from original code. There's - # really no better recourse because if this was a queue we - # need to consume on, we have no way to consume anymore. - sys.exit(1) - - if attempt == 1: - sleep_time = self.interval_start or 1 - elif attempt > 1: - sleep_time += self.interval_stepping - if self.interval_max: - sleep_time = min(sleep_time, self.interval_max) - - log_info['sleep_time'] = sleep_time - LOG.error(_('AMQP server on %(hostname)s:%(port)d is ' - 'unreachable: %(err_str)s. Trying again in ' - '%(sleep_time)d seconds.') % log_info) - time.sleep(sleep_time) - - def ensure(self, error_callback, method, *args, **kwargs): - while True: - try: - return method(*args, **kwargs) - except (self.connection_errors, socket.timeout, IOError) as e: - if error_callback: - error_callback(e) - except Exception as e: - # NOTE(comstud): Unfortunately it's possible for amqplib - # to return an error not covered by its transport - # connection_errors in the case of a timeout waiting for - # a protocol response. (See paste link in LP888621) - # So, we check all exceptions for 'timeout' in them - # and try to reconnect in this case. - if 'timeout' not in str(e): - raise - if error_callback: - error_callback(e) - self.reconnect() - - def get_channel(self): - """Convenience call for bin/clear_rabbit_queues""" - return self.channel - - def close(self): - """Close/release this connection""" - self.cancel_consumer_thread() - self.wait_on_proxy_callbacks() - self.connection.release() - self.connection = None - - def reset(self): - """Reset a connection so it can be used again""" - self.cancel_consumer_thread() - self.wait_on_proxy_callbacks() - self.channel.close() - self.channel = self.connection.channel() - # work around 'memory' transport bug in 1.1.3 - if self.memory_transport: - self.channel._new_queue('ae.undeliver') - self.consumers = [] - - def declare_consumer(self, consumer_cls, topic, callback): - """Create a Consumer using the class that was passed in and - add it to our list of consumers - """ - - def _connect_error(exc): - log_info = {'topic': topic, 'err_str': str(exc)} - LOG.error(_("Failed to declare consumer for topic '%(topic)s': " - "%(err_str)s") % log_info) - - def _declare_consumer(): - consumer = consumer_cls(self.conf, self.channel, topic, callback, - self.consumer_num.next()) - self.consumers.append(consumer) - return consumer - - return self.ensure(_connect_error, _declare_consumer) - - def iterconsume(self, limit=None, timeout=None): - """Return an iterator that will consume from all queues/consumers""" - - info = {'do_consume': True} - - def _error_callback(exc): - if isinstance(exc, socket.timeout): - LOG.debug(_('Timed out waiting for RPC response: %s') % - str(exc)) - raise rpc_common.Timeout() - else: - LOG.exception(_('Failed to consume message from queue: %s') % - str(exc)) - info['do_consume'] = True - - def _consume(): - if info['do_consume']: - queues_head = self.consumers[:-1] - queues_tail = self.consumers[-1] - for queue in queues_head: - queue.consume(nowait=True) - queues_tail.consume(nowait=False) - info['do_consume'] = False - return self.connection.drain_events(timeout=timeout) - - for iteration in itertools.count(0): - if limit and iteration >= limit: - raise StopIteration - yield self.ensure(_error_callback, _consume) - - def cancel_consumer_thread(self): - """Cancel a consumer thread""" - if self.consumer_thread is not None: - self.consumer_thread.kill() - try: - self.consumer_thread.wait() - except greenlet.GreenletExit: - pass - self.consumer_thread = None - - def wait_on_proxy_callbacks(self): - """Wait for all proxy callback threads to exit.""" - for proxy_cb in self.proxy_callbacks: - proxy_cb.wait() - - def publisher_send(self, cls, topic, msg, timeout=None, **kwargs): - """Send to a publisher based on the publisher class""" - - def _error_callback(exc): - log_info = {'topic': topic, 'err_str': str(exc)} - LOG.exception(_("Failed to publish message to topic " - "'%(topic)s': %(err_str)s") % log_info) - - def _publish(): - publisher = cls(self.conf, self.channel, topic, **kwargs) - publisher.send(msg, timeout) - - self.ensure(_error_callback, _publish) - - def declare_direct_consumer(self, topic, callback): - """Create a 'direct' queue. - In nova's use, this is generally a msg_id queue used for - responses for call/multicall - """ - self.declare_consumer(DirectConsumer, topic, callback) - - def declare_topic_consumer(self, topic, callback=None, queue_name=None, - exchange_name=None): - """Create a 'topic' consumer.""" - self.declare_consumer(functools.partial(TopicConsumer, - name=queue_name, - exchange_name=exchange_name, - ), - topic, callback) - - def declare_fanout_consumer(self, topic, callback): - """Create a 'fanout' consumer""" - self.declare_consumer(FanoutConsumer, topic, callback) - - def direct_send(self, msg_id, msg): - """Send a 'direct' message""" - self.publisher_send(DirectPublisher, msg_id, msg) - - def topic_send(self, topic, msg, timeout=None): - """Send a 'topic' message""" - self.publisher_send(TopicPublisher, topic, msg, timeout) - - def fanout_send(self, topic, msg): - """Send a 'fanout' message""" - self.publisher_send(FanoutPublisher, topic, msg) - - def notify_send(self, topic, msg, **kwargs): - """Send a notify message on a topic""" - self.publisher_send(NotifyPublisher, topic, msg, None, **kwargs) - - def consume(self, limit=None): - """Consume from all queues/consumers""" - it = self.iterconsume(limit=limit) - while True: - try: - it.next() - except StopIteration: - return - - def consume_in_thread(self): - """Consumer from all queues/consumers in a greenthread""" - def _consumer_thread(): - try: - self.consume() - except greenlet.GreenletExit: - return - if self.consumer_thread is None: - self.consumer_thread = eventlet.spawn(_consumer_thread) - return self.consumer_thread - - def create_consumer(self, topic, proxy, fanout=False): - """Create a consumer that calls a method in a proxy object""" - proxy_cb = rpc_amqp.ProxyCallback( - self.conf, proxy, - rpc_amqp.get_connection_pool(self.conf, Connection)) - self.proxy_callbacks.append(proxy_cb) - - if fanout: - self.declare_fanout_consumer(topic, proxy_cb) - else: - self.declare_topic_consumer(topic, proxy_cb) - - def create_worker(self, topic, proxy, pool_name): - """Create a worker that calls a method in a proxy object""" - proxy_cb = rpc_amqp.ProxyCallback( - self.conf, proxy, - rpc_amqp.get_connection_pool(self.conf, Connection)) - self.proxy_callbacks.append(proxy_cb) - self.declare_topic_consumer(topic, proxy_cb, pool_name) - - def join_consumer_pool(self, callback, pool_name, topic, - exchange_name=None): - """Register as a member of a group of consumers for a given topic from - the specified exchange. - - Exactly one member of a given pool will receive each message. - - A message will be delivered to multiple pools, if more than - one is created. - """ - callback_wrapper = rpc_amqp.CallbackWrapper( - conf=self.conf, - callback=callback, - connection_pool=rpc_amqp.get_connection_pool(self.conf, - Connection), - ) - self.proxy_callbacks.append(callback_wrapper) - self.declare_topic_consumer( - queue_name=pool_name, - topic=topic, - exchange_name=exchange_name, - callback=callback_wrapper, - ) - - -def create_connection(conf, new=True): - """Create a connection""" - return rpc_amqp.create_connection( - conf, new, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def multicall(conf, context, topic, msg, timeout=None): - """Make a call that returns multiple times.""" - return rpc_amqp.multicall( - conf, context, topic, msg, timeout, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def call(conf, context, topic, msg, timeout=None): - """Sends a message on a topic and wait for a response.""" - return rpc_amqp.call( - conf, context, topic, msg, timeout, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def cast(conf, context, topic, msg): - """Sends a message on a topic without waiting for a response.""" - return rpc_amqp.cast( - conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def fanout_cast(conf, context, topic, msg): - """Sends a message on a fanout exchange without waiting for a response.""" - return rpc_amqp.fanout_cast( - conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def cast_to_server(conf, context, server_params, topic, msg): - """Sends a message on a topic to a specific server.""" - return rpc_amqp.cast_to_server( - conf, context, server_params, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def fanout_cast_to_server(conf, context, server_params, topic, msg): - """Sends a message on a fanout exchange to a specific server.""" - return rpc_amqp.fanout_cast_to_server( - conf, context, server_params, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def notify(conf, context, topic, msg, envelope): - """Sends a notification event on a topic.""" - return rpc_amqp.notify( - conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection), - envelope) - - -def cleanup(): - return rpc_amqp.cleanup(Connection.pool) diff --git a/staccato/openstack/common/rpc/impl_qpid.py b/staccato/openstack/common/rpc/impl_qpid.py deleted file mode 100644 index 8347d1a..0000000 --- a/staccato/openstack/common/rpc/impl_qpid.py +++ /dev/null @@ -1,650 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation -# Copyright 2011 - 2012, Red Hat, 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 functools -import itertools -import time -import uuid - -import eventlet -import greenlet -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import jsonutils -from staccato.openstack.common import log as logging -from staccato.openstack.common.rpc import amqp as rpc_amqp -from staccato.openstack.common.rpc import common as rpc_common - -qpid_messaging = importutils.try_import("qpid.messaging") -qpid_exceptions = importutils.try_import("qpid.messaging.exceptions") - -LOG = logging.getLogger(__name__) - -qpid_opts = [ - cfg.StrOpt('qpid_hostname', - default='localhost', - help='Qpid broker hostname'), - cfg.IntOpt('qpid_port', - default=5672, - help='Qpid broker port'), - cfg.ListOpt('qpid_hosts', - default=['$qpid_hostname:$qpid_port'], - help='Qpid HA cluster host:port pairs'), - cfg.StrOpt('qpid_username', - default='', - help='Username for qpid connection'), - cfg.StrOpt('qpid_password', - default='', - help='Password for qpid connection', - secret=True), - cfg.StrOpt('qpid_sasl_mechanisms', - default='', - help='Space separated list of SASL mechanisms to use for auth'), - cfg.IntOpt('qpid_heartbeat', - default=60, - help='Seconds between connection keepalive heartbeats'), - cfg.StrOpt('qpid_protocol', - default='tcp', - help="Transport to use, either 'tcp' or 'ssl'"), - cfg.BoolOpt('qpid_tcp_nodelay', - default=True, - help='Disable Nagle algorithm'), -] - -cfg.CONF.register_opts(qpid_opts) - - -class ConsumerBase(object): - """Consumer base class.""" - - def __init__(self, session, callback, node_name, node_opts, - link_name, link_opts): - """Declare a queue on an amqp session. - - 'session' is the amqp session to use - 'callback' is the callback to call when messages are received - 'node_name' is the first part of the Qpid address string, before ';' - 'node_opts' will be applied to the "x-declare" section of "node" - in the address string. - 'link_name' goes into the "name" field of the "link" in the address - string - 'link_opts' will be applied to the "x-declare" section of "link" - in the address string. - """ - self.callback = callback - self.receiver = None - self.session = None - - addr_opts = { - "create": "always", - "node": { - "type": "topic", - "x-declare": { - "durable": True, - "auto-delete": True, - }, - }, - "link": { - "name": link_name, - "durable": True, - "x-declare": { - "durable": False, - "auto-delete": True, - "exclusive": False, - }, - }, - } - addr_opts["node"]["x-declare"].update(node_opts) - addr_opts["link"]["x-declare"].update(link_opts) - - self.address = "%s ; %s" % (node_name, jsonutils.dumps(addr_opts)) - - self.reconnect(session) - - def reconnect(self, session): - """Re-declare the receiver after a qpid reconnect""" - self.session = session - self.receiver = session.receiver(self.address) - self.receiver.capacity = 1 - - def consume(self): - """Fetch the message and pass it to the callback object""" - message = self.receiver.fetch() - try: - msg = rpc_common.deserialize_msg(message.content) - self.callback(msg) - except Exception: - LOG.exception(_("Failed to process message... skipping it.")) - finally: - self.session.acknowledge(message) - - def get_receiver(self): - return self.receiver - - -class DirectConsumer(ConsumerBase): - """Queue/consumer class for 'direct'""" - - def __init__(self, conf, session, msg_id, callback): - """Init a 'direct' queue. - - 'session' is the amqp session to use - 'msg_id' is the msg_id to listen on - 'callback' is the callback to call when messages are received - """ - - super(DirectConsumer, self).__init__(session, callback, - "%s/%s" % (msg_id, msg_id), - {"type": "direct"}, - msg_id, - {"exclusive": True}) - - -class TopicConsumer(ConsumerBase): - """Consumer class for 'topic'""" - - def __init__(self, conf, session, topic, callback, name=None, - exchange_name=None): - """Init a 'topic' queue. - - :param session: the amqp session to use - :param topic: is the topic to listen on - :paramtype topic: str - :param callback: the callback to call when messages are received - :param name: optional queue name, defaults to topic - """ - - exchange_name = exchange_name or rpc_amqp.get_control_exchange(conf) - super(TopicConsumer, self).__init__(session, callback, - "%s/%s" % (exchange_name, topic), - {}, name or topic, {}) - - -class FanoutConsumer(ConsumerBase): - """Consumer class for 'fanout'""" - - def __init__(self, conf, session, topic, callback): - """Init a 'fanout' queue. - - 'session' is the amqp session to use - 'topic' is the topic to listen on - 'callback' is the callback to call when messages are received - """ - - super(FanoutConsumer, self).__init__( - session, callback, - "%s_fanout" % topic, - {"durable": False, "type": "fanout"}, - "%s_fanout_%s" % (topic, uuid.uuid4().hex), - {"exclusive": True}) - - -class Publisher(object): - """Base Publisher class""" - - def __init__(self, session, node_name, node_opts=None): - """Init the Publisher class with the exchange_name, routing_key, - and other options - """ - self.sender = None - self.session = session - - addr_opts = { - "create": "always", - "node": { - "type": "topic", - "x-declare": { - "durable": False, - # auto-delete isn't implemented for exchanges in qpid, - # but put in here anyway - "auto-delete": True, - }, - }, - } - if node_opts: - addr_opts["node"]["x-declare"].update(node_opts) - - self.address = "%s ; %s" % (node_name, jsonutils.dumps(addr_opts)) - - self.reconnect(session) - - def reconnect(self, session): - """Re-establish the Sender after a reconnection""" - self.sender = session.sender(self.address) - - def send(self, msg): - """Send a message""" - self.sender.send(msg) - - -class DirectPublisher(Publisher): - """Publisher class for 'direct'""" - def __init__(self, conf, session, msg_id): - """Init a 'direct' publisher.""" - super(DirectPublisher, self).__init__(session, msg_id, - {"type": "Direct"}) - - -class TopicPublisher(Publisher): - """Publisher class for 'topic'""" - def __init__(self, conf, session, topic): - """init a 'topic' publisher. - """ - exchange_name = rpc_amqp.get_control_exchange(conf) - super(TopicPublisher, self).__init__(session, - "%s/%s" % (exchange_name, topic)) - - -class FanoutPublisher(Publisher): - """Publisher class for 'fanout'""" - def __init__(self, conf, session, topic): - """init a 'fanout' publisher. - """ - super(FanoutPublisher, self).__init__( - session, - "%s_fanout" % topic, {"type": "fanout"}) - - -class NotifyPublisher(Publisher): - """Publisher class for notifications""" - def __init__(self, conf, session, topic): - """init a 'topic' publisher. - """ - exchange_name = rpc_amqp.get_control_exchange(conf) - super(NotifyPublisher, self).__init__(session, - "%s/%s" % (exchange_name, topic), - {"durable": True}) - - -class Connection(object): - """Connection object.""" - - pool = None - - def __init__(self, conf, server_params=None): - if not qpid_messaging: - raise ImportError("Failed to import qpid.messaging") - - self.session = None - self.consumers = {} - self.consumer_thread = None - self.proxy_callbacks = [] - self.conf = conf - - if server_params and 'hostname' in server_params: - # NOTE(russellb) This enables support for cast_to_server. - server_params['qpid_hosts'] = [ - '%s:%d' % (server_params['hostname'], - server_params.get('port', 5672)) - ] - - params = { - 'qpid_hosts': self.conf.qpid_hosts, - 'username': self.conf.qpid_username, - 'password': self.conf.qpid_password, - } - params.update(server_params or {}) - - self.brokers = params['qpid_hosts'] - self.username = params['username'] - self.password = params['password'] - self.connection_create(self.brokers[0]) - self.reconnect() - - def connection_create(self, broker): - # Create the connection - this does not open the connection - self.connection = qpid_messaging.Connection(broker) - - # Check if flags are set and if so set them for the connection - # before we call open - self.connection.username = self.username - self.connection.password = self.password - - self.connection.sasl_mechanisms = self.conf.qpid_sasl_mechanisms - # Reconnection is done by self.reconnect() - self.connection.reconnect = False - self.connection.heartbeat = self.conf.qpid_heartbeat - self.connection.transport = self.conf.qpid_protocol - self.connection.tcp_nodelay = self.conf.qpid_tcp_nodelay - - def _register_consumer(self, consumer): - self.consumers[str(consumer.get_receiver())] = consumer - - def _lookup_consumer(self, receiver): - return self.consumers[str(receiver)] - - def reconnect(self): - """Handles reconnecting and re-establishing sessions and queues""" - attempt = 0 - delay = 1 - while True: - # Close the session if necessary - if self.connection.opened(): - try: - self.connection.close() - except qpid_exceptions.ConnectionError: - pass - - broker = self.brokers[attempt % len(self.brokers)] - attempt += 1 - - try: - self.connection_create(broker) - self.connection.open() - except qpid_exceptions.ConnectionError as e: - msg_dict = dict(e=e, delay=delay) - msg = _("Unable to connect to AMQP server: %(e)s. " - "Sleeping %(delay)s seconds") % msg_dict - LOG.error(msg) - time.sleep(delay) - delay = min(2 * delay, 60) - else: - LOG.info(_('Connected to AMQP server on %s'), broker) - break - - self.session = self.connection.session() - - if self.consumers: - consumers = self.consumers - self.consumers = {} - - for consumer in consumers.itervalues(): - consumer.reconnect(self.session) - self._register_consumer(consumer) - - LOG.debug(_("Re-established AMQP queues")) - - def ensure(self, error_callback, method, *args, **kwargs): - while True: - try: - return method(*args, **kwargs) - except (qpid_exceptions.Empty, - qpid_exceptions.ConnectionError) as e: - if error_callback: - error_callback(e) - self.reconnect() - - def close(self): - """Close/release this connection""" - self.cancel_consumer_thread() - self.wait_on_proxy_callbacks() - self.connection.close() - self.connection = None - - def reset(self): - """Reset a connection so it can be used again""" - self.cancel_consumer_thread() - self.wait_on_proxy_callbacks() - self.session.close() - self.session = self.connection.session() - self.consumers = {} - - def declare_consumer(self, consumer_cls, topic, callback): - """Create a Consumer using the class that was passed in and - add it to our list of consumers - """ - def _connect_error(exc): - log_info = {'topic': topic, 'err_str': str(exc)} - LOG.error(_("Failed to declare consumer for topic '%(topic)s': " - "%(err_str)s") % log_info) - - def _declare_consumer(): - consumer = consumer_cls(self.conf, self.session, topic, callback) - self._register_consumer(consumer) - return consumer - - return self.ensure(_connect_error, _declare_consumer) - - def iterconsume(self, limit=None, timeout=None): - """Return an iterator that will consume from all queues/consumers""" - - def _error_callback(exc): - if isinstance(exc, qpid_exceptions.Empty): - LOG.debug(_('Timed out waiting for RPC response: %s') % - str(exc)) - raise rpc_common.Timeout() - else: - LOG.exception(_('Failed to consume message from queue: %s') % - str(exc)) - - def _consume(): - nxt_receiver = self.session.next_receiver(timeout=timeout) - try: - self._lookup_consumer(nxt_receiver).consume() - except Exception: - LOG.exception(_("Error processing message. Skipping it.")) - - for iteration in itertools.count(0): - if limit and iteration >= limit: - raise StopIteration - yield self.ensure(_error_callback, _consume) - - def cancel_consumer_thread(self): - """Cancel a consumer thread""" - if self.consumer_thread is not None: - self.consumer_thread.kill() - try: - self.consumer_thread.wait() - except greenlet.GreenletExit: - pass - self.consumer_thread = None - - def wait_on_proxy_callbacks(self): - """Wait for all proxy callback threads to exit.""" - for proxy_cb in self.proxy_callbacks: - proxy_cb.wait() - - def publisher_send(self, cls, topic, msg): - """Send to a publisher based on the publisher class""" - - def _connect_error(exc): - log_info = {'topic': topic, 'err_str': str(exc)} - LOG.exception(_("Failed to publish message to topic " - "'%(topic)s': %(err_str)s") % log_info) - - def _publisher_send(): - publisher = cls(self.conf, self.session, topic) - publisher.send(msg) - - return self.ensure(_connect_error, _publisher_send) - - def declare_direct_consumer(self, topic, callback): - """Create a 'direct' queue. - In nova's use, this is generally a msg_id queue used for - responses for call/multicall - """ - self.declare_consumer(DirectConsumer, topic, callback) - - def declare_topic_consumer(self, topic, callback=None, queue_name=None, - exchange_name=None): - """Create a 'topic' consumer.""" - self.declare_consumer(functools.partial(TopicConsumer, - name=queue_name, - exchange_name=exchange_name, - ), - topic, callback) - - def declare_fanout_consumer(self, topic, callback): - """Create a 'fanout' consumer""" - self.declare_consumer(FanoutConsumer, topic, callback) - - def direct_send(self, msg_id, msg): - """Send a 'direct' message""" - self.publisher_send(DirectPublisher, msg_id, msg) - - def topic_send(self, topic, msg, timeout=None): - """Send a 'topic' message""" - # - # We want to create a message with attributes, e.g. a TTL. We - # don't really need to keep 'msg' in its JSON format any longer - # so let's create an actual qpid message here and get some - # value-add on the go. - # - # WARNING: Request timeout happens to be in the same units as - # qpid's TTL (seconds). If this changes in the future, then this - # will need to be altered accordingly. - # - qpid_message = qpid_messaging.Message(content=msg, ttl=timeout) - self.publisher_send(TopicPublisher, topic, qpid_message) - - def fanout_send(self, topic, msg): - """Send a 'fanout' message""" - self.publisher_send(FanoutPublisher, topic, msg) - - def notify_send(self, topic, msg, **kwargs): - """Send a notify message on a topic""" - self.publisher_send(NotifyPublisher, topic, msg) - - def consume(self, limit=None): - """Consume from all queues/consumers""" - it = self.iterconsume(limit=limit) - while True: - try: - it.next() - except StopIteration: - return - - def consume_in_thread(self): - """Consumer from all queues/consumers in a greenthread""" - def _consumer_thread(): - try: - self.consume() - except greenlet.GreenletExit: - return - if self.consumer_thread is None: - self.consumer_thread = eventlet.spawn(_consumer_thread) - return self.consumer_thread - - def create_consumer(self, topic, proxy, fanout=False): - """Create a consumer that calls a method in a proxy object""" - proxy_cb = rpc_amqp.ProxyCallback( - self.conf, proxy, - rpc_amqp.get_connection_pool(self.conf, Connection)) - self.proxy_callbacks.append(proxy_cb) - - if fanout: - consumer = FanoutConsumer(self.conf, self.session, topic, proxy_cb) - else: - consumer = TopicConsumer(self.conf, self.session, topic, proxy_cb) - - self._register_consumer(consumer) - - return consumer - - def create_worker(self, topic, proxy, pool_name): - """Create a worker that calls a method in a proxy object""" - proxy_cb = rpc_amqp.ProxyCallback( - self.conf, proxy, - rpc_amqp.get_connection_pool(self.conf, Connection)) - self.proxy_callbacks.append(proxy_cb) - - consumer = TopicConsumer(self.conf, self.session, topic, proxy_cb, - name=pool_name) - - self._register_consumer(consumer) - - return consumer - - def join_consumer_pool(self, callback, pool_name, topic, - exchange_name=None): - """Register as a member of a group of consumers for a given topic from - the specified exchange. - - Exactly one member of a given pool will receive each message. - - A message will be delivered to multiple pools, if more than - one is created. - """ - callback_wrapper = rpc_amqp.CallbackWrapper( - conf=self.conf, - callback=callback, - connection_pool=rpc_amqp.get_connection_pool(self.conf, - Connection), - ) - self.proxy_callbacks.append(callback_wrapper) - - consumer = TopicConsumer(conf=self.conf, - session=self.session, - topic=topic, - callback=callback_wrapper, - name=pool_name, - exchange_name=exchange_name) - - self._register_consumer(consumer) - return consumer - - -def create_connection(conf, new=True): - """Create a connection""" - return rpc_amqp.create_connection( - conf, new, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def multicall(conf, context, topic, msg, timeout=None): - """Make a call that returns multiple times.""" - return rpc_amqp.multicall( - conf, context, topic, msg, timeout, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def call(conf, context, topic, msg, timeout=None): - """Sends a message on a topic and wait for a response.""" - return rpc_amqp.call( - conf, context, topic, msg, timeout, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def cast(conf, context, topic, msg): - """Sends a message on a topic without waiting for a response.""" - return rpc_amqp.cast( - conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def fanout_cast(conf, context, topic, msg): - """Sends a message on a fanout exchange without waiting for a response.""" - return rpc_amqp.fanout_cast( - conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def cast_to_server(conf, context, server_params, topic, msg): - """Sends a message on a topic to a specific server.""" - return rpc_amqp.cast_to_server( - conf, context, server_params, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def fanout_cast_to_server(conf, context, server_params, topic, msg): - """Sends a message on a fanout exchange to a specific server.""" - return rpc_amqp.fanout_cast_to_server( - conf, context, server_params, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection)) - - -def notify(conf, context, topic, msg, envelope): - """Sends a notification event on a topic.""" - return rpc_amqp.notify(conf, context, topic, msg, - rpc_amqp.get_connection_pool(conf, Connection), - envelope) - - -def cleanup(): - return rpc_amqp.cleanup(Connection.pool) diff --git a/staccato/openstack/common/rpc/impl_zmq.py b/staccato/openstack/common/rpc/impl_zmq.py deleted file mode 100644 index c9ab653..0000000 --- a/staccato/openstack/common/rpc/impl_zmq.py +++ /dev/null @@ -1,851 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Cloudscaling Group, 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 os -import pprint -import re -import socket -import sys -import types -import uuid - -import eventlet -import greenlet -from oslo.config import cfg - -from staccato.openstack.common import excutils -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import jsonutils -from staccato.openstack.common import processutils as utils -from staccato.openstack.common.rpc import common as rpc_common - -zmq = importutils.try_import('eventlet.green.zmq') - -# for convenience, are not modified. -pformat = pprint.pformat -Timeout = eventlet.timeout.Timeout -LOG = rpc_common.LOG -RemoteError = rpc_common.RemoteError -RPCException = rpc_common.RPCException - -zmq_opts = [ - cfg.StrOpt('rpc_zmq_bind_address', default='*', - help='ZeroMQ bind address. Should be a wildcard (*), ' - 'an ethernet interface, or IP. ' - 'The "host" option should point or resolve to this ' - 'address.'), - - # The module.Class to use for matchmaking. - cfg.StrOpt( - 'rpc_zmq_matchmaker', - default=('staccato.openstack.common.rpc.' - 'matchmaker.MatchMakerLocalhost'), - help='MatchMaker driver', - ), - - # The following port is unassigned by IANA as of 2012-05-21 - cfg.IntOpt('rpc_zmq_port', default=9501, - help='ZeroMQ receiver listening port'), - - cfg.IntOpt('rpc_zmq_contexts', default=1, - help='Number of ZeroMQ contexts, defaults to 1'), - - cfg.IntOpt('rpc_zmq_topic_backlog', default=None, - help='Maximum number of ingress messages to locally buffer ' - 'per topic. Default is unlimited.'), - - cfg.StrOpt('rpc_zmq_ipc_dir', default='/var/run/openstack', - help='Directory for holding IPC sockets'), - - cfg.StrOpt('rpc_zmq_host', default=socket.gethostname(), - help='Name of this node. Must be a valid hostname, FQDN, or ' - 'IP address. Must match "host" option, if running Nova.') -] - - -CONF = cfg.CONF -CONF.register_opts(zmq_opts) - -ZMQ_CTX = None # ZeroMQ Context, must be global. -matchmaker = None # memoized matchmaker object - - -def _serialize(data): - """ - Serialization wrapper - We prefer using JSON, but it cannot encode all types. - Error if a developer passes us bad data. - """ - try: - return jsonutils.dumps(data, ensure_ascii=True) - except TypeError: - with excutils.save_and_reraise_exception(): - LOG.error(_("JSON serialization failed.")) - - -def _deserialize(data): - """ - Deserialization wrapper - """ - LOG.debug(_("Deserializing: %s"), data) - return jsonutils.loads(data) - - -class ZmqSocket(object): - """ - A tiny wrapper around ZeroMQ to simplify the send/recv protocol - and connection management. - - Can be used as a Context (supports the 'with' statement). - """ - - def __init__(self, addr, zmq_type, bind=True, subscribe=None): - self.sock = _get_ctxt().socket(zmq_type) - self.addr = addr - self.type = zmq_type - self.subscriptions = [] - - # Support failures on sending/receiving on wrong socket type. - self.can_recv = zmq_type in (zmq.PULL, zmq.SUB) - self.can_send = zmq_type in (zmq.PUSH, zmq.PUB) - self.can_sub = zmq_type in (zmq.SUB, ) - - # Support list, str, & None for subscribe arg (cast to list) - do_sub = { - list: subscribe, - str: [subscribe], - type(None): [] - }[type(subscribe)] - - for f in do_sub: - self.subscribe(f) - - str_data = {'addr': addr, 'type': self.socket_s(), - 'subscribe': subscribe, 'bind': bind} - - LOG.debug(_("Connecting to %(addr)s with %(type)s"), str_data) - LOG.debug(_("-> Subscribed to %(subscribe)s"), str_data) - LOG.debug(_("-> bind: %(bind)s"), str_data) - - try: - if bind: - self.sock.bind(addr) - else: - self.sock.connect(addr) - except Exception: - raise RPCException(_("Could not open socket.")) - - def socket_s(self): - """Get socket type as string.""" - t_enum = ('PUSH', 'PULL', 'PUB', 'SUB', 'REP', 'REQ', 'ROUTER', - 'DEALER') - return dict(map(lambda t: (getattr(zmq, t), t), t_enum))[self.type] - - def subscribe(self, msg_filter): - """Subscribe.""" - if not self.can_sub: - raise RPCException("Cannot subscribe on this socket.") - LOG.debug(_("Subscribing to %s"), msg_filter) - - try: - self.sock.setsockopt(zmq.SUBSCRIBE, msg_filter) - except Exception: - return - - self.subscriptions.append(msg_filter) - - def unsubscribe(self, msg_filter): - """Unsubscribe.""" - if msg_filter not in self.subscriptions: - return - self.sock.setsockopt(zmq.UNSUBSCRIBE, msg_filter) - self.subscriptions.remove(msg_filter) - - def close(self): - if self.sock is None or self.sock.closed: - return - - # We must unsubscribe, or we'll leak descriptors. - if self.subscriptions: - for f in self.subscriptions: - try: - self.sock.setsockopt(zmq.UNSUBSCRIBE, f) - except Exception: - pass - self.subscriptions = [] - - try: - # Default is to linger - self.sock.close() - except Exception: - # While this is a bad thing to happen, - # it would be much worse if some of the code calling this - # were to fail. For now, lets log, and later evaluate - # if we can safely raise here. - LOG.error("ZeroMQ socket could not be closed.") - self.sock = None - - def recv(self): - if not self.can_recv: - raise RPCException(_("You cannot recv on this socket.")) - return self.sock.recv_multipart() - - def send(self, data): - if not self.can_send: - raise RPCException(_("You cannot send on this socket.")) - self.sock.send_multipart(data) - - -class ZmqClient(object): - """Client for ZMQ sockets.""" - - def __init__(self, addr, socket_type=None, bind=False): - if socket_type is None: - socket_type = zmq.PUSH - self.outq = ZmqSocket(addr, socket_type, bind=bind) - - def cast(self, msg_id, topic, data, envelope=False): - msg_id = msg_id or 0 - - if not envelope: - self.outq.send(map(bytes, - (msg_id, topic, 'cast', _serialize(data)))) - return - - rpc_envelope = rpc_common.serialize_msg(data[1], envelope) - zmq_msg = reduce(lambda x, y: x + y, rpc_envelope.items()) - self.outq.send(map(bytes, - (msg_id, topic, 'impl_zmq_v2', data[0]) + zmq_msg)) - - def close(self): - self.outq.close() - - -class RpcContext(rpc_common.CommonRpcContext): - """Context that supports replying to a rpc.call.""" - def __init__(self, **kwargs): - self.replies = [] - super(RpcContext, self).__init__(**kwargs) - - def deepcopy(self): - values = self.to_dict() - values['replies'] = self.replies - return self.__class__(**values) - - def reply(self, reply=None, failure=None, ending=False): - if ending: - return - self.replies.append(reply) - - @classmethod - def marshal(self, ctx): - ctx_data = ctx.to_dict() - return _serialize(ctx_data) - - @classmethod - def unmarshal(self, data): - return RpcContext.from_dict(_deserialize(data)) - - -class InternalContext(object): - """Used by ConsumerBase as a private context for - methods.""" - - def __init__(self, proxy): - self.proxy = proxy - self.msg_waiter = None - - def _get_response(self, ctx, proxy, topic, data): - """Process a curried message and cast the result to topic.""" - LOG.debug(_("Running func with context: %s"), ctx.to_dict()) - data.setdefault('version', None) - data.setdefault('args', {}) - - try: - result = proxy.dispatch( - ctx, data['version'], data['method'], - data.get('namespace'), **data['args']) - return ConsumerBase.normalize_reply(result, ctx.replies) - except greenlet.GreenletExit: - # ignore these since they are just from shutdowns - pass - except rpc_common.ClientException as e: - LOG.debug(_("Expected exception during message handling (%s)") % - e._exc_info[1]) - return {'exc': - rpc_common.serialize_remote_exception(e._exc_info, - log_failure=False)} - except Exception: - LOG.error(_("Exception during message handling")) - return {'exc': - rpc_common.serialize_remote_exception(sys.exc_info())} - - def reply(self, ctx, proxy, - msg_id=None, context=None, topic=None, msg=None): - """Reply to a casted call.""" - # NOTE(ewindisch): context kwarg exists for Grizzly compat. - # this may be able to be removed earlier than - # 'I' if ConsumerBase.process were refactored. - if type(msg) is list: - payload = msg[-1] - else: - payload = msg - - response = ConsumerBase.normalize_reply( - self._get_response(ctx, proxy, topic, payload), - ctx.replies) - - LOG.debug(_("Sending reply")) - _multi_send(_cast, ctx, topic, { - 'method': '-process_reply', - 'args': { - 'msg_id': msg_id, # Include for Folsom compat. - 'response': response - } - }, _msg_id=msg_id) - - -class ConsumerBase(object): - """Base Consumer.""" - - def __init__(self): - self.private_ctx = InternalContext(None) - - @classmethod - def normalize_reply(self, result, replies): - #TODO(ewindisch): re-evaluate and document this method. - if isinstance(result, types.GeneratorType): - return list(result) - elif replies: - return replies - else: - return [result] - - def process(self, proxy, ctx, data): - data.setdefault('version', None) - data.setdefault('args', {}) - - # Method starting with - are - # processed internally. (non-valid method name) - method = data.get('method') - if not method: - LOG.error(_("RPC message did not include method.")) - return - - # Internal method - # uses internal context for safety. - if method == '-reply': - self.private_ctx.reply(ctx, proxy, **data['args']) - return - - proxy.dispatch(ctx, data['version'], - data['method'], data.get('namespace'), **data['args']) - - -class ZmqBaseReactor(ConsumerBase): - """ - A consumer class implementing a - centralized casting broker (PULL-PUSH) - for RoundRobin requests. - """ - - def __init__(self, conf): - super(ZmqBaseReactor, self).__init__() - - self.mapping = {} - self.proxies = {} - self.threads = [] - self.sockets = [] - self.subscribe = {} - - self.pool = eventlet.greenpool.GreenPool(conf.rpc_thread_pool_size) - - def register(self, proxy, in_addr, zmq_type_in, out_addr=None, - zmq_type_out=None, in_bind=True, out_bind=True, - subscribe=None): - - LOG.info(_("Registering reactor")) - - if zmq_type_in not in (zmq.PULL, zmq.SUB): - raise RPCException("Bad input socktype") - - # Items push in. - inq = ZmqSocket(in_addr, zmq_type_in, bind=in_bind, - subscribe=subscribe) - - self.proxies[inq] = proxy - self.sockets.append(inq) - - LOG.info(_("In reactor registered")) - - if not out_addr: - return - - if zmq_type_out not in (zmq.PUSH, zmq.PUB): - raise RPCException("Bad output socktype") - - # Items push out. - outq = ZmqSocket(out_addr, zmq_type_out, bind=out_bind) - - self.mapping[inq] = outq - self.mapping[outq] = inq - self.sockets.append(outq) - - LOG.info(_("Out reactor registered")) - - def consume_in_thread(self): - def _consume(sock): - LOG.info(_("Consuming socket")) - while True: - self.consume(sock) - - for k in self.proxies.keys(): - self.threads.append( - self.pool.spawn(_consume, k) - ) - - def wait(self): - for t in self.threads: - t.wait() - - def close(self): - for s in self.sockets: - s.close() - - for t in self.threads: - t.kill() - - -class ZmqProxy(ZmqBaseReactor): - """ - A consumer class implementing a - topic-based proxy, forwarding to - IPC sockets. - """ - - def __init__(self, conf): - super(ZmqProxy, self).__init__(conf) - pathsep = set((os.path.sep or '', os.path.altsep or '', '/', '\\')) - self.badchars = re.compile(r'[%s]' % re.escape(''.join(pathsep))) - - self.topic_proxy = {} - - def consume(self, sock): - ipc_dir = CONF.rpc_zmq_ipc_dir - - #TODO(ewindisch): use zero-copy (i.e. references, not copying) - data = sock.recv() - topic = data[1] - - LOG.debug(_("CONSUMER GOT %s"), ' '.join(map(pformat, data))) - - if topic.startswith('fanout~'): - sock_type = zmq.PUB - topic = topic.split('.', 1)[0] - elif topic.startswith('zmq_replies'): - sock_type = zmq.PUB - else: - sock_type = zmq.PUSH - - if topic not in self.topic_proxy: - def publisher(waiter): - LOG.info(_("Creating proxy for topic: %s"), topic) - - try: - # The topic is received over the network, - # don't trust this input. - if self.badchars.search(topic) is not None: - emsg = _("Topic contained dangerous characters.") - LOG.warn(emsg) - raise RPCException(emsg) - - out_sock = ZmqSocket("ipc://%s/zmq_topic_%s" % - (ipc_dir, topic), - sock_type, bind=True) - except RPCException: - waiter.send_exception(*sys.exc_info()) - return - - self.topic_proxy[topic] = eventlet.queue.LightQueue( - CONF.rpc_zmq_topic_backlog) - self.sockets.append(out_sock) - - # It takes some time for a pub socket to open, - # before we can have any faith in doing a send() to it. - if sock_type == zmq.PUB: - eventlet.sleep(.5) - - waiter.send(True) - - while(True): - data = self.topic_proxy[topic].get() - out_sock.send(data) - LOG.debug(_("ROUTER RELAY-OUT SUCCEEDED %(data)s") % - {'data': data}) - - wait_sock_creation = eventlet.event.Event() - eventlet.spawn(publisher, wait_sock_creation) - - try: - wait_sock_creation.wait() - except RPCException: - LOG.error(_("Topic socket file creation failed.")) - return - - try: - self.topic_proxy[topic].put_nowait(data) - LOG.debug(_("ROUTER RELAY-OUT QUEUED %(data)s") % - {'data': data}) - except eventlet.queue.Full: - LOG.error(_("Local per-topic backlog buffer full for topic " - "%(topic)s. Dropping message.") % {'topic': topic}) - - def consume_in_thread(self): - """Runs the ZmqProxy service""" - ipc_dir = CONF.rpc_zmq_ipc_dir - consume_in = "tcp://%s:%s" % \ - (CONF.rpc_zmq_bind_address, - CONF.rpc_zmq_port) - consumption_proxy = InternalContext(None) - - if not os.path.isdir(ipc_dir): - try: - utils.execute('mkdir', '-p', ipc_dir, run_as_root=True) - utils.execute('chown', "%s:%s" % (os.getuid(), os.getgid()), - ipc_dir, run_as_root=True) - utils.execute('chmod', '750', ipc_dir, run_as_root=True) - except utils.ProcessExecutionError: - with excutils.save_and_reraise_exception(): - LOG.error(_("Could not create IPC directory %s") % - (ipc_dir, )) - - try: - self.register(consumption_proxy, - consume_in, - zmq.PULL, - out_bind=True) - except zmq.ZMQError: - with excutils.save_and_reraise_exception(): - LOG.error(_("Could not create ZeroMQ receiver daemon. " - "Socket may already be in use.")) - - super(ZmqProxy, self).consume_in_thread() - - -def unflatten_envelope(packenv): - """Unflattens the RPC envelope. - Takes a list and returns a dictionary. - i.e. [1,2,3,4] => {1: 2, 3: 4} - """ - i = iter(packenv) - h = {} - try: - while True: - k = i.next() - h[k] = i.next() - except StopIteration: - return h - - -class ZmqReactor(ZmqBaseReactor): - """ - A consumer class implementing a - consumer for messages. Can also be - used as a 1:1 proxy - """ - - def __init__(self, conf): - super(ZmqReactor, self).__init__(conf) - - def consume(self, sock): - #TODO(ewindisch): use zero-copy (i.e. references, not copying) - data = sock.recv() - LOG.debug(_("CONSUMER RECEIVED DATA: %s"), data) - if sock in self.mapping: - LOG.debug(_("ROUTER RELAY-OUT %(data)s") % { - 'data': data}) - self.mapping[sock].send(data) - return - - proxy = self.proxies[sock] - - if data[2] == 'cast': # Legacy protocol - packenv = data[3] - - ctx, msg = _deserialize(packenv) - request = rpc_common.deserialize_msg(msg) - ctx = RpcContext.unmarshal(ctx) - elif data[2] == 'impl_zmq_v2': - packenv = data[4:] - - msg = unflatten_envelope(packenv) - request = rpc_common.deserialize_msg(msg) - - # Unmarshal only after verifying the message. - ctx = RpcContext.unmarshal(data[3]) - else: - LOG.error(_("ZMQ Envelope version unsupported or unknown.")) - return - - self.pool.spawn_n(self.process, proxy, ctx, request) - - -class Connection(rpc_common.Connection): - """Manages connections and threads.""" - - def __init__(self, conf): - self.topics = [] - self.reactor = ZmqReactor(conf) - - def create_consumer(self, topic, proxy, fanout=False): - # Register with matchmaker. - _get_matchmaker().register(topic, CONF.rpc_zmq_host) - - # Subscription scenarios - if fanout: - sock_type = zmq.SUB - subscribe = ('', fanout)[type(fanout) == str] - topic = 'fanout~' + topic.split('.', 1)[0] - else: - sock_type = zmq.PULL - subscribe = None - topic = '.'.join((topic.split('.', 1)[0], CONF.rpc_zmq_host)) - - if topic in self.topics: - LOG.info(_("Skipping topic registration. Already registered.")) - return - - # Receive messages from (local) proxy - inaddr = "ipc://%s/zmq_topic_%s" % \ - (CONF.rpc_zmq_ipc_dir, topic) - - LOG.debug(_("Consumer is a zmq.%s"), - ['PULL', 'SUB'][sock_type == zmq.SUB]) - - self.reactor.register(proxy, inaddr, sock_type, - subscribe=subscribe, in_bind=False) - self.topics.append(topic) - - def close(self): - _get_matchmaker().stop_heartbeat() - for topic in self.topics: - _get_matchmaker().unregister(topic, CONF.rpc_zmq_host) - - self.reactor.close() - self.topics = [] - - def wait(self): - self.reactor.wait() - - def consume_in_thread(self): - _get_matchmaker().start_heartbeat() - self.reactor.consume_in_thread() - - -def _cast(addr, context, topic, msg, timeout=None, envelope=False, - _msg_id=None): - timeout_cast = timeout or CONF.rpc_cast_timeout - payload = [RpcContext.marshal(context), msg] - - with Timeout(timeout_cast, exception=rpc_common.Timeout): - try: - conn = ZmqClient(addr) - - # assumes cast can't return an exception - conn.cast(_msg_id, topic, payload, envelope) - except zmq.ZMQError: - raise RPCException("Cast failed. ZMQ Socket Exception") - finally: - if 'conn' in vars(): - conn.close() - - -def _call(addr, context, topic, msg, timeout=None, - envelope=False): - # timeout_response is how long we wait for a response - timeout = timeout or CONF.rpc_response_timeout - - # The msg_id is used to track replies. - msg_id = uuid.uuid4().hex - - # Replies always come into the reply service. - reply_topic = "zmq_replies.%s" % CONF.rpc_zmq_host - - LOG.debug(_("Creating payload")) - # Curry the original request into a reply method. - mcontext = RpcContext.marshal(context) - payload = { - 'method': '-reply', - 'args': { - 'msg_id': msg_id, - 'topic': reply_topic, - # TODO(ewindisch): safe to remove mcontext in I. - 'msg': [mcontext, msg] - } - } - - LOG.debug(_("Creating queue socket for reply waiter")) - - # Messages arriving async. - # TODO(ewindisch): have reply consumer with dynamic subscription mgmt - with Timeout(timeout, exception=rpc_common.Timeout): - try: - msg_waiter = ZmqSocket( - "ipc://%s/zmq_topic_zmq_replies.%s" % - (CONF.rpc_zmq_ipc_dir, - CONF.rpc_zmq_host), - zmq.SUB, subscribe=msg_id, bind=False - ) - - LOG.debug(_("Sending cast")) - _cast(addr, context, topic, payload, envelope) - - LOG.debug(_("Cast sent; Waiting reply")) - # Blocks until receives reply - msg = msg_waiter.recv() - LOG.debug(_("Received message: %s"), msg) - LOG.debug(_("Unpacking response")) - - if msg[2] == 'cast': # Legacy version - raw_msg = _deserialize(msg[-1])[-1] - elif msg[2] == 'impl_zmq_v2': - rpc_envelope = unflatten_envelope(msg[4:]) - raw_msg = rpc_common.deserialize_msg(rpc_envelope) - else: - raise rpc_common.UnsupportedRpcEnvelopeVersion( - _("Unsupported or unknown ZMQ envelope returned.")) - - responses = raw_msg['args']['response'] - # ZMQError trumps the Timeout error. - except zmq.ZMQError: - raise RPCException("ZMQ Socket Error") - except (IndexError, KeyError): - raise RPCException(_("RPC Message Invalid.")) - finally: - if 'msg_waiter' in vars(): - msg_waiter.close() - - # It seems we don't need to do all of the following, - # but perhaps it would be useful for multicall? - # One effect of this is that we're checking all - # responses for Exceptions. - for resp in responses: - if isinstance(resp, types.DictType) and 'exc' in resp: - raise rpc_common.deserialize_remote_exception(CONF, resp['exc']) - - return responses[-1] - - -def _multi_send(method, context, topic, msg, timeout=None, - envelope=False, _msg_id=None): - """ - Wraps the sending of messages, - dispatches to the matchmaker and sends - message to all relevant hosts. - """ - conf = CONF - LOG.debug(_("%(msg)s") % {'msg': ' '.join(map(pformat, (topic, msg)))}) - - queues = _get_matchmaker().queues(topic) - LOG.debug(_("Sending message(s) to: %s"), queues) - - # Don't stack if we have no matchmaker results - if not queues: - LOG.warn(_("No matchmaker results. Not casting.")) - # While not strictly a timeout, callers know how to handle - # this exception and a timeout isn't too big a lie. - raise rpc_common.Timeout(_("No match from matchmaker.")) - - # This supports brokerless fanout (addresses > 1) - for queue in queues: - (_topic, ip_addr) = queue - _addr = "tcp://%s:%s" % (ip_addr, conf.rpc_zmq_port) - - if method.__name__ == '_cast': - eventlet.spawn_n(method, _addr, context, - _topic, msg, timeout, envelope, - _msg_id) - return - return method(_addr, context, _topic, msg, timeout, - envelope) - - -def create_connection(conf, new=True): - return Connection(conf) - - -def multicall(conf, *args, **kwargs): - """Multiple calls.""" - return _multi_send(_call, *args, **kwargs) - - -def call(conf, *args, **kwargs): - """Send a message, expect a response.""" - data = _multi_send(_call, *args, **kwargs) - return data[-1] - - -def cast(conf, *args, **kwargs): - """Send a message expecting no reply.""" - _multi_send(_cast, *args, **kwargs) - - -def fanout_cast(conf, context, topic, msg, **kwargs): - """Send a message to all listening and expect no reply.""" - # NOTE(ewindisch): fanout~ is used because it avoid splitting on . - # and acts as a non-subtle hint to the matchmaker and ZmqProxy. - _multi_send(_cast, context, 'fanout~' + str(topic), msg, **kwargs) - - -def notify(conf, context, topic, msg, envelope): - """ - Send notification event. - Notifications are sent to topic-priority. - This differs from the AMQP drivers which send to topic.priority. - """ - # NOTE(ewindisch): dot-priority in rpc notifier does not - # work with our assumptions. - topic = topic.replace('.', '-') - cast(conf, context, topic, msg, envelope=envelope) - - -def cleanup(): - """Clean up resources in use by implementation.""" - global ZMQ_CTX - if ZMQ_CTX: - ZMQ_CTX.term() - ZMQ_CTX = None - - global matchmaker - matchmaker = None - - -def _get_ctxt(): - if not zmq: - raise ImportError("Failed to import eventlet.green.zmq") - - global ZMQ_CTX - if not ZMQ_CTX: - ZMQ_CTX = zmq.Context(CONF.rpc_zmq_contexts) - return ZMQ_CTX - - -def _get_matchmaker(*args, **kwargs): - global matchmaker - if not matchmaker: - matchmaker = importutils.import_object( - CONF.rpc_zmq_matchmaker, *args, **kwargs) - return matchmaker diff --git a/staccato/openstack/common/rpc/matchmaker.py b/staccato/openstack/common/rpc/matchmaker.py deleted file mode 100644 index e87c298..0000000 --- a/staccato/openstack/common/rpc/matchmaker.py +++ /dev/null @@ -1,425 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 Cloudscaling Group, 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. -""" -The MatchMaker classes should except a Topic or Fanout exchange key and -return keys for direct exchanges, per (approximate) AMQP parlance. -""" - -import contextlib -import itertools -import json - -import eventlet -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging - - -matchmaker_opts = [ - # Matchmaker ring file - cfg.StrOpt('matchmaker_ringfile', - default='/etc/nova/matchmaker_ring.json', - help='Matchmaker ring file (JSON)'), - cfg.IntOpt('matchmaker_heartbeat_freq', - default=300, - help='Heartbeat frequency'), - cfg.IntOpt('matchmaker_heartbeat_ttl', - default=600, - help='Heartbeat time-to-live.'), -] - -CONF = cfg.CONF -CONF.register_opts(matchmaker_opts) -LOG = logging.getLogger(__name__) -contextmanager = contextlib.contextmanager - - -class MatchMakerException(Exception): - """Signified a match could not be found.""" - message = _("Match not found by MatchMaker.") - - -class Exchange(object): - """ - Implements lookups. - Subclass this to support hashtables, dns, etc. - """ - def __init__(self): - pass - - def run(self, key): - raise NotImplementedError() - - -class Binding(object): - """ - A binding on which to perform a lookup. - """ - def __init__(self): - pass - - def test(self, key): - raise NotImplementedError() - - -class MatchMakerBase(object): - """ - Match Maker Base Class. - Build off HeartbeatMatchMakerBase if building a - heartbeat-capable MatchMaker. - """ - def __init__(self): - # Array of tuples. Index [2] toggles negation, [3] is last-if-true - self.bindings = [] - - self.no_heartbeat_msg = _('Matchmaker does not implement ' - 'registration or heartbeat.') - - def register(self, key, host): - """ - Register a host on a backend. - Heartbeats, if applicable, may keepalive registration. - """ - pass - - def ack_alive(self, key, host): - """ - Acknowledge that a key.host is alive. - Used internally for updating heartbeats, - but may also be used publically to acknowledge - a system is alive (i.e. rpc message successfully - sent to host) - """ - pass - - def is_alive(self, topic, host): - """ - Checks if a host is alive. - """ - pass - - def expire(self, topic, host): - """ - Explicitly expire a host's registration. - """ - pass - - def send_heartbeats(self): - """ - Send all heartbeats. - Use start_heartbeat to spawn a heartbeat greenthread, - which loops this method. - """ - pass - - def unregister(self, key, host): - """ - Unregister a topic. - """ - pass - - def start_heartbeat(self): - """ - Spawn heartbeat greenthread. - """ - pass - - def stop_heartbeat(self): - """ - Destroys the heartbeat greenthread. - """ - pass - - def add_binding(self, binding, rule, last=True): - self.bindings.append((binding, rule, False, last)) - - #NOTE(ewindisch): kept the following method in case we implement the - # underlying support. - #def add_negate_binding(self, binding, rule, last=True): - # self.bindings.append((binding, rule, True, last)) - - def queues(self, key): - workers = [] - - # bit is for negate bindings - if we choose to implement it. - # last stops processing rules if this matches. - for (binding, exchange, bit, last) in self.bindings: - if binding.test(key): - workers.extend(exchange.run(key)) - - # Support last. - if last: - return workers - return workers - - -class HeartbeatMatchMakerBase(MatchMakerBase): - """ - Base for a heart-beat capable MatchMaker. - Provides common methods for registering, - unregistering, and maintaining heartbeats. - """ - def __init__(self): - self.hosts = set() - self._heart = None - self.host_topic = {} - - super(HeartbeatMatchMakerBase, self).__init__() - - def send_heartbeats(self): - """ - Send all heartbeats. - Use start_heartbeat to spawn a heartbeat greenthread, - which loops this method. - """ - for key, host in self.host_topic: - self.ack_alive(key, host) - - def ack_alive(self, key, host): - """ - Acknowledge that a host.topic is alive. - Used internally for updating heartbeats, - but may also be used publically to acknowledge - a system is alive (i.e. rpc message successfully - sent to host) - """ - raise NotImplementedError("Must implement ack_alive") - - def backend_register(self, key, host): - """ - Implements registration logic. - Called by register(self,key,host) - """ - raise NotImplementedError("Must implement backend_register") - - def backend_unregister(self, key, key_host): - """ - Implements de-registration logic. - Called by unregister(self,key,host) - """ - raise NotImplementedError("Must implement backend_unregister") - - def register(self, key, host): - """ - Register a host on a backend. - Heartbeats, if applicable, may keepalive registration. - """ - self.hosts.add(host) - self.host_topic[(key, host)] = host - key_host = '.'.join((key, host)) - - self.backend_register(key, key_host) - - self.ack_alive(key, host) - - def unregister(self, key, host): - """ - Unregister a topic. - """ - if (key, host) in self.host_topic: - del self.host_topic[(key, host)] - - self.hosts.discard(host) - self.backend_unregister(key, '.'.join((key, host))) - - LOG.info(_("Matchmaker unregistered: %s, %s" % (key, host))) - - def start_heartbeat(self): - """ - Implementation of MatchMakerBase.start_heartbeat - Launches greenthread looping send_heartbeats(), - yielding for CONF.matchmaker_heartbeat_freq seconds - between iterations. - """ - if not self.hosts: - raise MatchMakerException( - _("Register before starting heartbeat.")) - - def do_heartbeat(): - while True: - self.send_heartbeats() - eventlet.sleep(CONF.matchmaker_heartbeat_freq) - - self._heart = eventlet.spawn(do_heartbeat) - - def stop_heartbeat(self): - """ - Destroys the heartbeat greenthread. - """ - if self._heart: - self._heart.kill() - - -class DirectBinding(Binding): - """ - Specifies a host in the key via a '.' character - Although dots are used in the key, the behavior here is - that it maps directly to a host, thus direct. - """ - def test(self, key): - if '.' in key: - return True - return False - - -class TopicBinding(Binding): - """ - Where a 'bare' key without dots. - AMQP generally considers topic exchanges to be those *with* dots, - but we deviate here in terminology as the behavior here matches - that of a topic exchange (whereas where there are dots, behavior - matches that of a direct exchange. - """ - def test(self, key): - if '.' not in key: - return True - return False - - -class FanoutBinding(Binding): - """Match on fanout keys, where key starts with 'fanout.' string.""" - def test(self, key): - if key.startswith('fanout~'): - return True - return False - - -class StubExchange(Exchange): - """Exchange that does nothing.""" - def run(self, key): - return [(key, None)] - - -class RingExchange(Exchange): - """ - Match Maker where hosts are loaded from a static file containing - a hashmap (JSON formatted). - - __init__ takes optional ring dictionary argument, otherwise - loads the ringfile from CONF.mathcmaker_ringfile. - """ - def __init__(self, ring=None): - super(RingExchange, self).__init__() - - if ring: - self.ring = ring - else: - fh = open(CONF.matchmaker_ringfile, 'r') - self.ring = json.load(fh) - fh.close() - - self.ring0 = {} - for k in self.ring.keys(): - self.ring0[k] = itertools.cycle(self.ring[k]) - - def _ring_has(self, key): - if key in self.ring0: - return True - return False - - -class RoundRobinRingExchange(RingExchange): - """A Topic Exchange based on a hashmap.""" - def __init__(self, ring=None): - super(RoundRobinRingExchange, self).__init__(ring) - - def run(self, key): - if not self._ring_has(key): - LOG.warn( - _("No key defining hosts for topic '%s', " - "see ringfile") % (key, ) - ) - return [] - host = next(self.ring0[key]) - return [(key + '.' + host, host)] - - -class FanoutRingExchange(RingExchange): - """Fanout Exchange based on a hashmap.""" - def __init__(self, ring=None): - super(FanoutRingExchange, self).__init__(ring) - - def run(self, key): - # Assume starts with "fanout~", strip it for lookup. - nkey = key.split('fanout~')[1:][0] - if not self._ring_has(nkey): - LOG.warn( - _("No key defining hosts for topic '%s', " - "see ringfile") % (nkey, ) - ) - return [] - return map(lambda x: (key + '.' + x, x), self.ring[nkey]) - - -class LocalhostExchange(Exchange): - """Exchange where all direct topics are local.""" - def __init__(self, host='localhost'): - self.host = host - super(Exchange, self).__init__() - - def run(self, key): - return [('.'.join((key.split('.')[0], self.host)), self.host)] - - -class DirectExchange(Exchange): - """ - Exchange where all topic keys are split, sending to second half. - i.e. "compute.host" sends a message to "compute.host" running on "host" - """ - def __init__(self): - super(Exchange, self).__init__() - - def run(self, key): - e = key.split('.', 1)[1] - return [(key, e)] - - -class MatchMakerRing(MatchMakerBase): - """ - Match Maker where hosts are loaded from a static hashmap. - """ - def __init__(self, ring=None): - super(MatchMakerRing, self).__init__() - self.add_binding(FanoutBinding(), FanoutRingExchange(ring)) - self.add_binding(DirectBinding(), DirectExchange()) - self.add_binding(TopicBinding(), RoundRobinRingExchange(ring)) - - -class MatchMakerLocalhost(MatchMakerBase): - """ - Match Maker where all bare topics resolve to localhost. - Useful for testing. - """ - def __init__(self, host='localhost'): - super(MatchMakerLocalhost, self).__init__() - self.add_binding(FanoutBinding(), LocalhostExchange(host)) - self.add_binding(DirectBinding(), DirectExchange()) - self.add_binding(TopicBinding(), LocalhostExchange(host)) - - -class MatchMakerStub(MatchMakerBase): - """ - Match Maker where topics are untouched. - Useful for testing, or for AMQP/brokered queues. - Will not work where knowledge of hosts is known (i.e. zeromq) - """ - def __init__(self): - super(MatchMakerLocalhost, self).__init__() - - self.add_binding(FanoutBinding(), StubExchange()) - self.add_binding(DirectBinding(), StubExchange()) - self.add_binding(TopicBinding(), StubExchange()) diff --git a/staccato/openstack/common/rpc/matchmaker_redis.py b/staccato/openstack/common/rpc/matchmaker_redis.py deleted file mode 100644 index a355fe8..0000000 --- a/staccato/openstack/common/rpc/matchmaker_redis.py +++ /dev/null @@ -1,149 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 Cloudscaling Group, 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. -""" -The MatchMaker classes should accept a Topic or Fanout exchange key and -return keys for direct exchanges, per (approximate) AMQP parlance. -""" - -from oslo.config import cfg - -from staccato.openstack.common import importutils -from staccato.openstack.common import log as logging -from staccato.openstack.common.rpc import matchmaker as mm_common - -redis = importutils.try_import('redis') - - -matchmaker_redis_opts = [ - cfg.StrOpt('host', - default='127.0.0.1', - help='Host to locate redis'), - cfg.IntOpt('port', - default=6379, - help='Use this port to connect to redis host.'), - cfg.StrOpt('password', - default=None, - help='Password for Redis server. (optional)'), -] - -CONF = cfg.CONF -opt_group = cfg.OptGroup(name='matchmaker_redis', - title='Options for Redis-based MatchMaker') -CONF.register_group(opt_group) -CONF.register_opts(matchmaker_redis_opts, opt_group) -LOG = logging.getLogger(__name__) - - -class RedisExchange(mm_common.Exchange): - def __init__(self, matchmaker): - self.matchmaker = matchmaker - self.redis = matchmaker.redis - super(RedisExchange, self).__init__() - - -class RedisTopicExchange(RedisExchange): - """ - Exchange where all topic keys are split, sending to second half. - i.e. "compute.host" sends a message to "compute" running on "host" - """ - def run(self, topic): - while True: - member_name = self.redis.srandmember(topic) - - if not member_name: - # If this happens, there are no - # longer any members. - break - - if not self.matchmaker.is_alive(topic, member_name): - continue - - host = member_name.split('.', 1)[1] - return [(member_name, host)] - return [] - - -class RedisFanoutExchange(RedisExchange): - """ - Return a list of all hosts. - """ - def run(self, topic): - topic = topic.split('~', 1)[1] - hosts = self.redis.smembers(topic) - good_hosts = filter( - lambda host: self.matchmaker.is_alive(topic, host), hosts) - - return [(x, x.split('.', 1)[1]) for x in good_hosts] - - -class MatchMakerRedis(mm_common.HeartbeatMatchMakerBase): - """ - MatchMaker registering and looking-up hosts with a Redis server. - """ - def __init__(self): - super(MatchMakerRedis, self).__init__() - - if not redis: - raise ImportError("Failed to import module redis.") - - self.redis = redis.StrictRedis( - host=CONF.matchmaker_redis.host, - port=CONF.matchmaker_redis.port, - password=CONF.matchmaker_redis.password) - - self.add_binding(mm_common.FanoutBinding(), RedisFanoutExchange(self)) - self.add_binding(mm_common.DirectBinding(), mm_common.DirectExchange()) - self.add_binding(mm_common.TopicBinding(), RedisTopicExchange(self)) - - def ack_alive(self, key, host): - topic = "%s.%s" % (key, host) - if not self.redis.expire(topic, CONF.matchmaker_heartbeat_ttl): - # If we could not update the expiration, the key - # might have been pruned. Re-register, creating a new - # key in Redis. - self.register(self.topic_host[host], host) - - def is_alive(self, topic, host): - if self.redis.ttl(host) == -1: - self.expire(topic, host) - return False - return True - - def expire(self, topic, host): - with self.redis.pipeline() as pipe: - pipe.multi() - pipe.delete(host) - pipe.srem(topic, host) - pipe.execute() - - def backend_register(self, key, key_host): - with self.redis.pipeline() as pipe: - pipe.multi() - pipe.sadd(key, key_host) - - # No value is needed, we just - # care if it exists. Sets aren't viable - # because only keys can expire. - pipe.set(key_host, '') - - pipe.execute() - - def backend_unregister(self, key, key_host): - with self.redis.pipeline() as pipe: - pipe.multi() - pipe.srem(key, key_host) - pipe.delete(key_host) - pipe.execute() diff --git a/staccato/openstack/common/rpc/proxy.py b/staccato/openstack/common/rpc/proxy.py deleted file mode 100644 index 6a57891..0000000 --- a/staccato/openstack/common/rpc/proxy.py +++ /dev/null @@ -1,187 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012-2013 Red Hat, 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. - -""" -A helper class for proxy objects to remote APIs. - -For more information about rpc API version numbers, see: - rpc/dispatcher.py -""" - - -from staccato.openstack.common import rpc -from staccato.openstack.common.rpc import common as rpc_common - - -class RpcProxy(object): - """A helper class for rpc clients. - - This class is a wrapper around the RPC client API. It allows you to - specify the topic and API version in a single place. This is intended to - be used as a base class for a class that implements the client side of an - rpc API. - """ - - def __init__(self, topic, default_version, version_cap=None): - """Initialize an RpcProxy. - - :param topic: The topic to use for all messages. - :param default_version: The default API version to request in all - outgoing messages. This can be overridden on a per-message - basis. - :param version_cap: Optionally cap the maximum version used for sent - messages. - """ - self.topic = topic - self.default_version = default_version - self.version_cap = version_cap - super(RpcProxy, self).__init__() - - def _set_version(self, msg, vers): - """Helper method to set the version in a message. - - :param msg: The message having a version added to it. - :param vers: The version number to add to the message. - """ - v = vers if vers else self.default_version - if (self.version_cap and not - rpc_common.version_is_compatible(self.version_cap, v)): - raise rpc_common.RpcVersionCapError(version=self.version_cap) - msg['version'] = v - - def _get_topic(self, topic): - """Return the topic to use for a message.""" - return topic if topic else self.topic - - @staticmethod - def make_namespaced_msg(method, namespace, **kwargs): - return {'method': method, 'namespace': namespace, 'args': kwargs} - - @staticmethod - def make_msg(method, **kwargs): - return RpcProxy.make_namespaced_msg(method, None, **kwargs) - - def call(self, context, msg, topic=None, version=None, timeout=None): - """rpc.call() a remote method. - - :param context: The request context - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - :param timeout: (Optional) A timeout to use when waiting for the - response. If no timeout is specified, a default timeout will be - used that is usually sufficient. - - :returns: The return value from the remote method. - """ - self._set_version(msg, version) - real_topic = self._get_topic(topic) - try: - return rpc.call(context, real_topic, msg, timeout) - except rpc.common.Timeout as exc: - raise rpc.common.Timeout( - exc.info, real_topic, msg.get('method')) - - def multicall(self, context, msg, topic=None, version=None, timeout=None): - """rpc.multicall() a remote method. - - :param context: The request context - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - :param timeout: (Optional) A timeout to use when waiting for the - response. If no timeout is specified, a default timeout will be - used that is usually sufficient. - - :returns: An iterator that lets you process each of the returned values - from the remote method as they arrive. - """ - self._set_version(msg, version) - real_topic = self._get_topic(topic) - try: - return rpc.multicall(context, real_topic, msg, timeout) - except rpc.common.Timeout as exc: - raise rpc.common.Timeout( - exc.info, real_topic, msg.get('method')) - - def cast(self, context, msg, topic=None, version=None): - """rpc.cast() a remote method. - - :param context: The request context - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - - :returns: None. rpc.cast() does not wait on any return value from the - remote method. - """ - self._set_version(msg, version) - rpc.cast(context, self._get_topic(topic), msg) - - def fanout_cast(self, context, msg, topic=None, version=None): - """rpc.fanout_cast() a remote method. - - :param context: The request context - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - - :returns: None. rpc.fanout_cast() does not wait on any return value - from the remote method. - """ - self._set_version(msg, version) - rpc.fanout_cast(context, self._get_topic(topic), msg) - - def cast_to_server(self, context, server_params, msg, topic=None, - version=None): - """rpc.cast_to_server() a remote method. - - :param context: The request context - :param server_params: Server parameters. See rpc.cast_to_server() for - details. - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - - :returns: None. rpc.cast_to_server() does not wait on any - return values. - """ - self._set_version(msg, version) - rpc.cast_to_server(context, server_params, self._get_topic(topic), msg) - - def fanout_cast_to_server(self, context, server_params, msg, topic=None, - version=None): - """rpc.fanout_cast_to_server() a remote method. - - :param context: The request context - :param server_params: Server parameters. See rpc.cast_to_server() for - details. - :param msg: The message to send, including the method and args. - :param topic: Override the topic for this message. - :param version: (Optional) Override the requested API version in this - message. - - :returns: None. rpc.fanout_cast_to_server() does not wait on any - return values. - """ - self._set_version(msg, version) - rpc.fanout_cast_to_server(context, server_params, - self._get_topic(topic), msg) diff --git a/staccato/openstack/common/rpc/service.py b/staccato/openstack/common/rpc/service.py deleted file mode 100644 index 07837bd..0000000 --- a/staccato/openstack/common/rpc/service.py +++ /dev/null @@ -1,75 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# All Rights Reserved. -# Copyright 2011 Red Hat, 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. - -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import log as logging -from staccato.openstack.common import rpc -from staccato.openstack.common.rpc import dispatcher as rpc_dispatcher -from staccato.openstack.common import service - - -LOG = logging.getLogger(__name__) - - -class Service(service.Service): - """Service object for binaries running on hosts. - - A service enables rpc by listening to queues based on topic and host.""" - def __init__(self, host, topic, manager=None): - super(Service, self).__init__() - self.host = host - self.topic = topic - if manager is None: - self.manager = self - else: - self.manager = manager - - def start(self): - super(Service, self).start() - - self.conn = rpc.create_connection(new=True) - LOG.debug(_("Creating Consumer connection for Service %s") % - self.topic) - - dispatcher = rpc_dispatcher.RpcDispatcher([self.manager]) - - # Share this same connection for these Consumers - self.conn.create_consumer(self.topic, dispatcher, fanout=False) - - node_topic = '%s.%s' % (self.topic, self.host) - self.conn.create_consumer(node_topic, dispatcher, fanout=False) - - self.conn.create_consumer(self.topic, dispatcher, fanout=True) - - # Hook to allow the manager to do other initializations after - # the rpc connection is created. - if callable(getattr(self.manager, 'initialize_service_hook', None)): - self.manager.initialize_service_hook(self) - - # Consume from all consumers in a thread - self.conn.consume_in_thread() - - def stop(self): - # Try to shut the connection down, but if we get any sort of - # errors, go ahead and ignore them.. as we're shutting down anyway - try: - self.conn.close() - except Exception: - pass - super(Service, self).stop() diff --git a/staccato/openstack/common/rpc/zmq_receiver.py b/staccato/openstack/common/rpc/zmq_receiver.py deleted file mode 100755 index 30d2ed6..0000000 --- a/staccato/openstack/common/rpc/zmq_receiver.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation -# -# 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 eventlet -eventlet.monkey_patch() - -import contextlib -import sys - -from oslo.config import cfg - -from staccato.openstack.common import log as logging -from staccato.openstack.common import rpc -from staccato.openstack.common.rpc import impl_zmq - -CONF = cfg.CONF -CONF.register_opts(rpc.rpc_opts) -CONF.register_opts(impl_zmq.zmq_opts) - - -def main(): - CONF(sys.argv[1:], project='oslo') - logging.setup("oslo") - - with contextlib.closing(impl_zmq.ZmqProxy(CONF)) as reactor: - reactor.consume_in_thread() - reactor.wait() diff --git a/staccato/openstack/common/service.py b/staccato/openstack/common/service.py deleted file mode 100644 index 7148237..0000000 --- a/staccato/openstack/common/service.py +++ /dev/null @@ -1,333 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2010 United States Government as represented by the -# Administrator of the National Aeronautics and Space Administration. -# Copyright 2011 Justin Santa Barbara -# All Rights Reserved. -# -# 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. - -"""Generic Node base class for all workers that run on hosts.""" - -import errno -import os -import random -import signal -import sys -import time - -import eventlet -import logging as std_logging -from oslo.config import cfg - -from staccato.openstack.common import eventlet_backdoor -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import importutils -from staccato.openstack.common import log as logging -from staccato.openstack.common import threadgroup - - -rpc = importutils.try_import('staccato.openstack.common.rpc') -CONF = cfg.CONF -LOG = logging.getLogger(__name__) - - -class Launcher(object): - """Launch one or more services and wait for them to complete.""" - - def __init__(self): - """Initialize the service launcher. - - :returns: None - - """ - self._services = threadgroup.ThreadGroup() - self.backdoor_port = eventlet_backdoor.initialize_if_enabled() - - @staticmethod - def run_service(service): - """Start and wait for a service to finish. - - :param service: service to run and wait for. - :returns: None - - """ - service.start() - service.wait() - - def launch_service(self, service): - """Load and start the given service. - - :param service: The service you would like to start. - :returns: None - - """ - service.backdoor_port = self.backdoor_port - self._services.add_thread(self.run_service, service) - - def stop(self): - """Stop all services which are currently running. - - :returns: None - - """ - self._services.stop() - - def wait(self): - """Waits until all services have been stopped, and then returns. - - :returns: None - - """ - self._services.wait() - - -class SignalExit(SystemExit): - def __init__(self, signo, exccode=1): - super(SignalExit, self).__init__(exccode) - self.signo = signo - - -class ServiceLauncher(Launcher): - def _handle_signal(self, signo, frame): - # Allow the process to be killed again and die from natural causes - signal.signal(signal.SIGTERM, signal.SIG_DFL) - signal.signal(signal.SIGINT, signal.SIG_DFL) - - raise SignalExit(signo) - - def wait(self): - signal.signal(signal.SIGTERM, self._handle_signal) - signal.signal(signal.SIGINT, self._handle_signal) - - LOG.debug(_('Full set of CONF:')) - CONF.log_opt_values(LOG, std_logging.DEBUG) - - status = None - try: - super(ServiceLauncher, self).wait() - except SignalExit as exc: - signame = {signal.SIGTERM: 'SIGTERM', - signal.SIGINT: 'SIGINT'}[exc.signo] - LOG.info(_('Caught %s, exiting'), signame) - status = exc.code - except SystemExit as exc: - status = exc.code - finally: - if rpc: - rpc.cleanup() - self.stop() - return status - - -class ServiceWrapper(object): - def __init__(self, service, workers): - self.service = service - self.workers = workers - self.children = set() - self.forktimes = [] - - -class ProcessLauncher(object): - def __init__(self): - self.children = {} - self.sigcaught = None - self.running = True - rfd, self.writepipe = os.pipe() - self.readpipe = eventlet.greenio.GreenPipe(rfd, 'r') - - signal.signal(signal.SIGTERM, self._handle_signal) - signal.signal(signal.SIGINT, self._handle_signal) - - def _handle_signal(self, signo, frame): - self.sigcaught = signo - self.running = False - - # Allow the process to be killed again and die from natural causes - signal.signal(signal.SIGTERM, signal.SIG_DFL) - signal.signal(signal.SIGINT, signal.SIG_DFL) - - def _pipe_watcher(self): - # This will block until the write end is closed when the parent - # dies unexpectedly - self.readpipe.read() - - LOG.info(_('Parent process has died unexpectedly, exiting')) - - sys.exit(1) - - def _child_process(self, service): - # Setup child signal handlers differently - def _sigterm(*args): - signal.signal(signal.SIGTERM, signal.SIG_DFL) - raise SignalExit(signal.SIGTERM) - - signal.signal(signal.SIGTERM, _sigterm) - # Block SIGINT and let the parent send us a SIGTERM - signal.signal(signal.SIGINT, signal.SIG_IGN) - - # Reopen the eventlet hub to make sure we don't share an epoll - # fd with parent and/or siblings, which would be bad - eventlet.hubs.use_hub() - - # Close write to ensure only parent has it open - os.close(self.writepipe) - # Create greenthread to watch for parent to close pipe - eventlet.spawn_n(self._pipe_watcher) - - # Reseed random number generator - random.seed() - - launcher = Launcher() - launcher.run_service(service) - - def _start_child(self, wrap): - if len(wrap.forktimes) > wrap.workers: - # Limit ourselves to one process a second (over the period of - # number of workers * 1 second). This will allow workers to - # start up quickly but ensure we don't fork off children that - # die instantly too quickly. - if time.time() - wrap.forktimes[0] < wrap.workers: - LOG.info(_('Forking too fast, sleeping')) - time.sleep(1) - - wrap.forktimes.pop(0) - - wrap.forktimes.append(time.time()) - - pid = os.fork() - if pid == 0: - # NOTE(johannes): All exceptions are caught to ensure this - # doesn't fallback into the loop spawning children. It would - # be bad for a child to spawn more children. - status = 0 - try: - self._child_process(wrap.service) - except SignalExit as exc: - signame = {signal.SIGTERM: 'SIGTERM', - signal.SIGINT: 'SIGINT'}[exc.signo] - LOG.info(_('Caught %s, exiting'), signame) - status = exc.code - except SystemExit as exc: - status = exc.code - except BaseException: - LOG.exception(_('Unhandled exception')) - status = 2 - finally: - wrap.service.stop() - - os._exit(status) - - LOG.info(_('Started child %d'), pid) - - wrap.children.add(pid) - self.children[pid] = wrap - - return pid - - def launch_service(self, service, workers=1): - wrap = ServiceWrapper(service, workers) - - LOG.info(_('Starting %d workers'), wrap.workers) - while self.running and len(wrap.children) < wrap.workers: - self._start_child(wrap) - - def _wait_child(self): - try: - # Don't block if no child processes have exited - pid, status = os.waitpid(0, os.WNOHANG) - if not pid: - return None - except OSError as exc: - if exc.errno not in (errno.EINTR, errno.ECHILD): - raise - return None - - if os.WIFSIGNALED(status): - sig = os.WTERMSIG(status) - LOG.info(_('Child %(pid)d killed by signal %(sig)d'), - dict(pid=pid, sig=sig)) - else: - code = os.WEXITSTATUS(status) - LOG.info(_('Child %(pid)s exited with status %(code)d'), - dict(pid=pid, code=code)) - - if pid not in self.children: - LOG.warning(_('pid %d not in child list'), pid) - return None - - wrap = self.children.pop(pid) - wrap.children.remove(pid) - return wrap - - def wait(self): - """Loop waiting on children to die and respawning as necessary""" - - LOG.debug(_('Full set of CONF:')) - CONF.log_opt_values(LOG, std_logging.DEBUG) - - while self.running: - wrap = self._wait_child() - if not wrap: - # Yield to other threads if no children have exited - # Sleep for a short time to avoid excessive CPU usage - # (see bug #1095346) - eventlet.greenthread.sleep(.01) - continue - - while self.running and len(wrap.children) < wrap.workers: - self._start_child(wrap) - - if self.sigcaught: - signame = {signal.SIGTERM: 'SIGTERM', - signal.SIGINT: 'SIGINT'}[self.sigcaught] - LOG.info(_('Caught %s, stopping children'), signame) - - for pid in self.children: - try: - os.kill(pid, signal.SIGTERM) - except OSError as exc: - if exc.errno != errno.ESRCH: - raise - - # Wait for children to die - if self.children: - LOG.info(_('Waiting on %d children to exit'), len(self.children)) - while self.children: - self._wait_child() - - -class Service(object): - """Service object for binaries running on hosts.""" - - def __init__(self, threads=1000): - self.tg = threadgroup.ThreadGroup(threads) - - def start(self): - pass - - def stop(self): - self.tg.stop() - - def wait(self): - self.tg.wait() - - -def launch(service, workers=None): - if workers: - launcher = ProcessLauncher() - launcher.launch_service(service, workers=workers) - else: - launcher = ServiceLauncher() - launcher.launch_service(service) - return launcher diff --git a/staccato/openstack/common/setup.py b/staccato/openstack/common/setup.py deleted file mode 100644 index 1b3a127..0000000 --- a/staccato/openstack/common/setup.py +++ /dev/null @@ -1,367 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# Copyright 2012-2013 Hewlett-Packard Development Company, L.P. -# All Rights Reserved. -# -# 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. - -""" -Utilities with minimum-depends for use in setup.py -""" - -from __future__ import print_function - -import email -import os -import re -import subprocess -import sys - -from setuptools.command import sdist - - -def parse_mailmap(mailmap='.mailmap'): - mapping = {} - if os.path.exists(mailmap): - with open(mailmap, 'r') as fp: - for l in fp: - try: - canonical_email, alias = re.match( - r'[^#]*?(<.+>).*(<.+>).*', l).groups() - except AttributeError: - continue - mapping[alias] = canonical_email - return mapping - - -def _parse_git_mailmap(git_dir, mailmap='.mailmap'): - mailmap = os.path.join(os.path.dirname(git_dir), mailmap) - return parse_mailmap(mailmap) - - -def canonicalize_emails(changelog, mapping): - """Takes in a string and an email alias mapping and replaces all - instances of the aliases in the string with their real email. - """ - for alias, email_address in mapping.iteritems(): - changelog = changelog.replace(alias, email_address) - return changelog - - -# Get requirements from the first file that exists -def get_reqs_from_files(requirements_files): - for requirements_file in requirements_files: - if os.path.exists(requirements_file): - with open(requirements_file, 'r') as fil: - return fil.read().split('\n') - return [] - - -def parse_requirements(requirements_files=['requirements.txt', - 'tools/pip-requires']): - requirements = [] - for line in get_reqs_from_files(requirements_files): - # For the requirements list, we need to inject only the portion - # after egg= so that distutils knows the package it's looking for - # such as: - # -e git://github.com/openstack/nova/master#egg=nova - if re.match(r'\s*-e\s+', line): - requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1', - line)) - # such as: - # http://github.com/openstack/nova/zipball/master#egg=nova - elif re.match(r'\s*https?:', line): - requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1', - line)) - # -f lines are for index locations, and don't get used here - elif re.match(r'\s*-f\s+', line): - pass - # argparse is part of the standard library starting with 2.7 - # adding it to the requirements list screws distro installs - elif line == 'argparse' and sys.version_info >= (2, 7): - pass - else: - requirements.append(line) - - return requirements - - -def parse_dependency_links(requirements_files=['requirements.txt', - 'tools/pip-requires']): - dependency_links = [] - # dependency_links inject alternate locations to find packages listed - # in requirements - for line in get_reqs_from_files(requirements_files): - # skip comments and blank lines - if re.match(r'(\s*#)|(\s*$)', line): - continue - # lines with -e or -f need the whole line, minus the flag - if re.match(r'\s*-[ef]\s+', line): - dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line)) - # lines that are only urls can go in unmolested - elif re.match(r'\s*https?:', line): - dependency_links.append(line) - return dependency_links - - -def _run_shell_command(cmd, throw_on_error=False): - if os.name == 'nt': - output = subprocess.Popen(["cmd.exe", "/C", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - else: - output = subprocess.Popen(["/bin/sh", "-c", cmd], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out = output.communicate() - if output.returncode and throw_on_error: - raise Exception("%s returned %d" % cmd, output.returncode) - if not out: - return None - return out[0].strip() or None - - -def _get_git_directory(): - parent_dir = os.path.dirname(__file__) - while True: - git_dir = os.path.join(parent_dir, '.git') - if os.path.exists(git_dir): - return git_dir - parent_dir, child = os.path.split(parent_dir) - if not child: # reached to root dir - return None - - -def write_git_changelog(): - """Write a changelog based on the git changelog.""" - new_changelog = 'ChangeLog' - git_dir = _get_git_directory() - if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'): - if git_dir: - git_log_cmd = 'git --git-dir=%s log' % git_dir - changelog = _run_shell_command(git_log_cmd) - mailmap = _parse_git_mailmap(git_dir) - with open(new_changelog, "w") as changelog_file: - changelog_file.write(canonicalize_emails(changelog, mailmap)) - else: - open(new_changelog, 'w').close() - - -def generate_authors(): - """Create AUTHORS file using git commits.""" - jenkins_email = 'jenkins@review.(openstack|stackforge).org' - old_authors = 'AUTHORS.in' - new_authors = 'AUTHORS' - git_dir = _get_git_directory() - if not os.getenv('SKIP_GENERATE_AUTHORS'): - if git_dir: - # don't include jenkins email address in AUTHORS file - git_log_cmd = ("git --git-dir=" + git_dir + - " log --format='%aN <%aE>' | sort -u | " - "egrep -v '" + jenkins_email + "'") - changelog = _run_shell_command(git_log_cmd) - signed_cmd = ("git --git-dir=" + git_dir + - " log | grep -i Co-authored-by: | sort -u") - signed_entries = _run_shell_command(signed_cmd) - if signed_entries: - new_entries = "\n".join( - [signed.split(":", 1)[1].strip() - for signed in signed_entries.split("\n") if signed]) - changelog = "\n".join((changelog, new_entries)) - mailmap = _parse_git_mailmap(git_dir) - with open(new_authors, 'w') as new_authors_fh: - new_authors_fh.write(canonicalize_emails(changelog, mailmap)) - if os.path.exists(old_authors): - with open(old_authors, "r") as old_authors_fh: - new_authors_fh.write('\n' + old_authors_fh.read()) - else: - open(new_authors, 'w').close() - - -_rst_template = """%(heading)s -%(underline)s - -.. automodule:: %(module)s - :members: - :undoc-members: - :show-inheritance: -""" - - -def get_cmdclass(): - """Return dict of commands to run from setup.py.""" - - cmdclass = dict() - - def _find_modules(arg, dirname, files): - for filename in files: - if filename.endswith('.py') and filename != '__init__.py': - arg["%s.%s" % (dirname.replace('/', '.'), - filename[:-3])] = True - - class LocalSDist(sdist.sdist): - """Builds the ChangeLog and Authors files from VC first.""" - - def run(self): - write_git_changelog() - generate_authors() - # sdist.sdist is an old style class, can't use super() - sdist.sdist.run(self) - - cmdclass['sdist'] = LocalSDist - - # If Sphinx is installed on the box running setup.py, - # enable setup.py to build the documentation, otherwise, - # just ignore it - try: - from sphinx.setup_command import BuildDoc - - class LocalBuildDoc(BuildDoc): - - builders = ['html', 'man'] - - def generate_autoindex(self): - print("**Autodocumenting from %s" % os.path.abspath(os.curdir)) - modules = {} - option_dict = self.distribution.get_option_dict('build_sphinx') - source_dir = os.path.join(option_dict['source_dir'][1], 'api') - if not os.path.exists(source_dir): - os.makedirs(source_dir) - for pkg in self.distribution.packages: - if '.' not in pkg: - os.path.walk(pkg, _find_modules, modules) - module_list = modules.keys() - module_list.sort() - autoindex_filename = os.path.join(source_dir, 'autoindex.rst') - with open(autoindex_filename, 'w') as autoindex: - autoindex.write(""".. toctree:: - :maxdepth: 1 - -""") - for module in module_list: - output_filename = os.path.join(source_dir, - "%s.rst" % module) - heading = "The :mod:`%s` Module" % module - underline = "=" * len(heading) - values = dict(module=module, heading=heading, - underline=underline) - - print("Generating %s" % output_filename) - with open(output_filename, 'w') as output_file: - output_file.write(_rst_template % values) - autoindex.write(" %s.rst\n" % module) - - def run(self): - if not os.getenv('SPHINX_DEBUG'): - self.generate_autoindex() - - for builder in self.builders: - self.builder = builder - self.finalize_options() - self.project = self.distribution.get_name() - self.version = self.distribution.get_version() - self.release = self.distribution.get_version() - BuildDoc.run(self) - - class LocalBuildLatex(LocalBuildDoc): - builders = ['latex'] - - cmdclass['build_sphinx'] = LocalBuildDoc - cmdclass['build_sphinx_latex'] = LocalBuildLatex - except ImportError: - pass - - return cmdclass - - -def _get_revno(git_dir): - """Return the number of commits since the most recent tag. - - We use git-describe to find this out, but if there are no - tags then we fall back to counting commits since the beginning - of time. - """ - describe = _run_shell_command( - "git --git-dir=%s describe --always" % git_dir) - if "-" in describe: - return describe.rsplit("-", 2)[-2] - - # no tags found - revlist = _run_shell_command( - "git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir) - return len(revlist.splitlines()) - - -def _get_version_from_git(pre_version): - """Return a version which is equal to the tag that's on the current - revision if there is one, or tag plus number of additional revisions - if the current revision has no tag.""" - - git_dir = _get_git_directory() - if git_dir: - if pre_version: - try: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --exact-match", - throw_on_error=True).replace('-', '.') - except Exception: - sha = _run_shell_command( - "git --git-dir=" + git_dir + " log -n1 --pretty=format:%h") - return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha) - else: - return _run_shell_command( - "git --git-dir=" + git_dir + " describe --always").replace( - '-', '.') - return None - - -def _get_version_from_pkg_info(package_name): - """Get the version from PKG-INFO file if we can.""" - try: - pkg_info_file = open('PKG-INFO', 'r') - except (IOError, OSError): - return None - try: - pkg_info = email.message_from_file(pkg_info_file) - except email.MessageError: - return None - # Check to make sure we're in our own dir - if pkg_info.get('Name', None) != package_name: - return None - return pkg_info.get('Version', None) - - -def get_version(package_name, pre_version=None): - """Get the version of the project. First, try getting it from PKG-INFO, if - it exists. If it does, that means we're in a distribution tarball or that - install has happened. Otherwise, if there is no PKG-INFO file, pull the - version from git. - - We do not support setup.py version sanity in git archive tarballs, nor do - we support packagers directly sucking our git repo into theirs. We expect - that a source tarball be made from our git repo - or that if someone wants - to make a source tarball from a fork of our repo with additional tags in it - that they understand and desire the results of doing that. - """ - version = os.environ.get("OSLO_PACKAGE_VERSION", None) - if version: - return version - version = _get_version_from_pkg_info(package_name) - if version: - return version - version = _get_version_from_git(pre_version) - if version: - return version - raise Exception("Versioning for this project requires either an sdist" - " tarball, or access to an upstream git repository.") diff --git a/staccato/openstack/common/sslutils.py b/staccato/openstack/common/sslutils.py deleted file mode 100644 index a765146..0000000 --- a/staccato/openstack/common/sslutils.py +++ /dev/null @@ -1,80 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 IBM Corp. -# -# 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 os -import ssl - -from oslo.config import cfg - -from staccato.openstack.common.gettextutils import _ - - -ssl_opts = [ - cfg.StrOpt('ca_file', - default=None, - help="CA certificate file to use to verify " - "connecting clients"), - cfg.StrOpt('cert_file', - default=None, - help="Certificate file to use when starting " - "the server securely"), - cfg.StrOpt('key_file', - default=None, - help="Private key file to use when starting " - "the server securely"), -] - - -CONF = cfg.CONF -CONF.register_opts(ssl_opts, "ssl") - - -def is_enabled(): - cert_file = CONF.ssl.cert_file - key_file = CONF.ssl.key_file - ca_file = CONF.ssl.ca_file - use_ssl = cert_file or key_file - - if cert_file and not os.path.exists(cert_file): - raise RuntimeError(_("Unable to find cert_file : %s") % cert_file) - - if ca_file and not os.path.exists(ca_file): - raise RuntimeError(_("Unable to find ca_file : %s") % ca_file) - - if key_file and not os.path.exists(key_file): - raise RuntimeError(_("Unable to find key_file : %s") % key_file) - - if use_ssl and (not cert_file or not key_file): - raise RuntimeError(_("When running server in SSL mode, you must " - "specify both a cert_file and key_file " - "option value in your configuration file")) - - return use_ssl - - -def wrap(sock): - ssl_kwargs = { - 'server_side': True, - 'certfile': CONF.ssl.cert_file, - 'keyfile': CONF.ssl.key_file, - 'cert_reqs': ssl.CERT_NONE, - } - - if CONF.ssl.ca_file: - ssl_kwargs['ca_certs'] = CONF.ssl.ca_file - ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED - - return ssl.wrap_socket(sock, **ssl_kwargs) diff --git a/staccato/openstack/common/threadgroup.py b/staccato/openstack/common/threadgroup.py deleted file mode 100644 index 10e120a..0000000 --- a/staccato/openstack/common/threadgroup.py +++ /dev/null @@ -1,121 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 Red Hat, 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. - -from eventlet import greenlet -from eventlet import greenpool -from eventlet import greenthread - -from staccato.openstack.common import log as logging -from staccato.openstack.common import loopingcall - - -LOG = logging.getLogger(__name__) - - -def _thread_done(gt, *args, **kwargs): - """ Callback function to be passed to GreenThread.link() when we spawn() - Calls the :class:`ThreadGroup` to notify if. - - """ - kwargs['group'].thread_done(kwargs['thread']) - - -class Thread(object): - """ Wrapper around a greenthread, that holds a reference to the - :class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when - it has done so it can be removed from the threads list. - """ - def __init__(self, thread, group): - self.thread = thread - self.thread.link(_thread_done, group=group, thread=self) - - def stop(self): - self.thread.kill() - - def wait(self): - return self.thread.wait() - - -class ThreadGroup(object): - """ The point of the ThreadGroup classis to: - - * keep track of timers and greenthreads (making it easier to stop them - when need be). - * provide an easy API to add timers. - """ - def __init__(self, thread_pool_size=10): - self.pool = greenpool.GreenPool(thread_pool_size) - self.threads = [] - self.timers = [] - - def add_dynamic_timer(self, callback, initial_delay=None, - periodic_interval_max=None, *args, **kwargs): - timer = loopingcall.DynamicLoopingCall(callback, *args, **kwargs) - timer.start(initial_delay=initial_delay, - periodic_interval_max=periodic_interval_max) - self.timers.append(timer) - - def add_timer(self, interval, callback, initial_delay=None, - *args, **kwargs): - pulse = loopingcall.FixedIntervalLoopingCall(callback, *args, **kwargs) - pulse.start(interval=interval, - initial_delay=initial_delay) - self.timers.append(pulse) - - def add_thread(self, callback, *args, **kwargs): - gt = self.pool.spawn(callback, *args, **kwargs) - th = Thread(gt, self) - self.threads.append(th) - - def thread_done(self, thread): - self.threads.remove(thread) - - def stop(self): - current = greenthread.getcurrent() - for x in self.threads: - if x is current: - # don't kill the current thread. - continue - try: - x.stop() - except Exception as ex: - LOG.exception(ex) - - for x in self.timers: - try: - x.stop() - except Exception as ex: - LOG.exception(ex) - self.timers = [] - - def wait(self): - for x in self.timers: - try: - x.wait() - except greenlet.GreenletExit: - pass - except Exception as ex: - LOG.exception(ex) - current = greenthread.getcurrent() - for x in self.threads: - if x is current: - continue - try: - x.wait() - except greenlet.GreenletExit: - pass - except Exception as ex: - LOG.exception(ex) diff --git a/staccato/openstack/common/timeutils.py b/staccato/openstack/common/timeutils.py deleted file mode 100644 index 6094365..0000000 --- a/staccato/openstack/common/timeutils.py +++ /dev/null @@ -1,186 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -""" -Time related utilities and helper functions. -""" - -import calendar -import datetime - -import iso8601 - - -# ISO 8601 extended time format with microseconds -_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f' -_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S' -PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND - - -def isotime(at=None, subsecond=False): - """Stringify time in ISO 8601 format""" - if not at: - at = utcnow() - st = at.strftime(_ISO8601_TIME_FORMAT - if not subsecond - else _ISO8601_TIME_FORMAT_SUBSECOND) - tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC' - st += ('Z' if tz == 'UTC' else tz) - return st - - -def parse_isotime(timestr): - """Parse time from ISO 8601 format""" - try: - return iso8601.parse_date(timestr) - except iso8601.ParseError as e: - raise ValueError(e.message) - except TypeError as e: - raise ValueError(e.message) - - -def strtime(at=None, fmt=PERFECT_TIME_FORMAT): - """Returns formatted utcnow.""" - if not at: - at = utcnow() - return at.strftime(fmt) - - -def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT): - """Turn a formatted time back into a datetime.""" - return datetime.datetime.strptime(timestr, fmt) - - -def normalize_time(timestamp): - """Normalize time in arbitrary timezone to UTC naive object""" - offset = timestamp.utcoffset() - if offset is None: - return timestamp - return timestamp.replace(tzinfo=None) - offset - - -def is_older_than(before, seconds): - """Return True if before is older than seconds.""" - if isinstance(before, basestring): - before = parse_strtime(before).replace(tzinfo=None) - return utcnow() - before > datetime.timedelta(seconds=seconds) - - -def is_newer_than(after, seconds): - """Return True if after is newer than seconds.""" - if isinstance(after, basestring): - after = parse_strtime(after).replace(tzinfo=None) - return after - utcnow() > datetime.timedelta(seconds=seconds) - - -def utcnow_ts(): - """Timestamp version of our utcnow function.""" - return calendar.timegm(utcnow().timetuple()) - - -def utcnow(): - """Overridable version of utils.utcnow.""" - if utcnow.override_time: - try: - return utcnow.override_time.pop(0) - except AttributeError: - return utcnow.override_time - return datetime.datetime.utcnow() - - -def iso8601_from_timestamp(timestamp): - """Returns a iso8601 formated date from timestamp""" - return isotime(datetime.datetime.utcfromtimestamp(timestamp)) - - -utcnow.override_time = None - - -def set_time_override(override_time=datetime.datetime.utcnow()): - """ - Override utils.utcnow to return a constant time or a list thereof, - one at a time. - """ - utcnow.override_time = override_time - - -def advance_time_delta(timedelta): - """Advance overridden time using a datetime.timedelta.""" - assert(not utcnow.override_time is None) - try: - for dt in utcnow.override_time: - dt += timedelta - except TypeError: - utcnow.override_time += timedelta - - -def advance_time_seconds(seconds): - """Advance overridden time by seconds.""" - advance_time_delta(datetime.timedelta(0, seconds)) - - -def clear_time_override(): - """Remove the overridden time.""" - utcnow.override_time = None - - -def marshall_now(now=None): - """Make an rpc-safe datetime with microseconds. - - Note: tzinfo is stripped, but not required for relative times.""" - if not now: - now = utcnow() - return dict(day=now.day, month=now.month, year=now.year, hour=now.hour, - minute=now.minute, second=now.second, - microsecond=now.microsecond) - - -def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], - month=tyme['month'], - year=tyme['year'], - hour=tyme['hour'], - minute=tyme['minute'], - second=tyme['second'], - microsecond=tyme['microsecond']) - - -def delta_seconds(before, after): - """ - Compute the difference in seconds between two date, time, or - datetime objects (as a float, to microsecond resolution). - """ - delta = after - before - try: - return delta.total_seconds() - except AttributeError: - return ((delta.days * 24 * 3600) + delta.seconds + - float(delta.microseconds) / (10 ** 6)) - - -def is_soon(dt, window): - """ - Determines if time is going to happen in the next window seconds. - - :params dt: the time - :params window: minimum seconds to remain to consider the time not soon - - :return: True if expiration is within the given duration - """ - soon = (utcnow() + datetime.timedelta(seconds=window)) - return normalize_time(dt) <= soon diff --git a/staccato/openstack/common/uuidutils.py b/staccato/openstack/common/uuidutils.py deleted file mode 100644 index 7608acb..0000000 --- a/staccato/openstack/common/uuidutils.py +++ /dev/null @@ -1,39 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright (c) 2012 Intel Corporation. -# All Rights Reserved. -# -# 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. - -""" -UUID related utilities and helper functions. -""" - -import uuid - - -def generate_uuid(): - return str(uuid.uuid4()) - - -def is_uuid_like(val): - """Returns validation of a value as a UUID. - - For our purposes, a UUID is a canonical form string: - aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa - - """ - try: - return str(uuid.UUID(val)) == val - except (TypeError, ValueError, AttributeError): - return False diff --git a/staccato/openstack/common/version.py b/staccato/openstack/common/version.py deleted file mode 100644 index e382497..0000000 --- a/staccato/openstack/common/version.py +++ /dev/null @@ -1,94 +0,0 @@ - -# Copyright 2012 OpenStack Foundation -# Copyright 2012-2013 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. - -""" -Utilities for consuming the version from pkg_resources. -""" - -import pkg_resources - - -class VersionInfo(object): - - def __init__(self, package): - """Object that understands versioning for a package - :param package: name of the python package, such as glance, or - python-glanceclient - """ - self.package = package - self.release = None - self.version = None - self._cached_version = None - - def __str__(self): - """Make the VersionInfo object behave like a string.""" - return self.version_string() - - def __repr__(self): - """Include the name.""" - return "VersionInfo(%s:%s)" % (self.package, self.version_string()) - - def _get_version_from_pkg_resources(self): - """Get the version of the package from the pkg_resources record - associated with the package.""" - try: - requirement = pkg_resources.Requirement.parse(self.package) - provider = pkg_resources.get_provider(requirement) - return provider.version - except pkg_resources.DistributionNotFound: - # The most likely cause for this is running tests in a tree - # produced from a tarball where the package itself has not been - # installed into anything. Revert to setup-time logic. - from staccato.openstack.common import setup - return setup.get_version(self.package) - - def release_string(self): - """Return the full version of the package including suffixes indicating - VCS status. - """ - if self.release is None: - self.release = self._get_version_from_pkg_resources() - - return self.release - - def version_string(self): - """Return the short version minus any alpha/beta tags.""" - if self.version is None: - parts = [] - for part in self.release_string().split('.'): - if part[0].isdigit(): - parts.append(part) - else: - break - self.version = ".".join(parts) - - return self.version - - # Compatibility functions - canonical_version_string = version_string - version_string_with_vcs = release_string - - def cached_version_string(self, prefix=""): - """Generate an object which will expand in a string context to - the results of version_string(). We do this so that don't - call into pkg_resources every time we start up a program when - passing version information into the CONF constructor, but - rather only do the calculation when and if a version is requested - """ - if not self._cached_version: - self._cached_version = "%s%s" % (prefix, - self.version_string()) - return self._cached_version diff --git a/staccato/openstack/common/wsgi.py b/staccato/openstack/common/wsgi.py deleted file mode 100644 index c789605..0000000 --- a/staccato/openstack/common/wsgi.py +++ /dev/null @@ -1,800 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2011 OpenStack Foundation. -# All Rights Reserved. -# -# 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. - -"""Utility methods for working with WSGI servers.""" - -from __future__ import print_function - -import eventlet -eventlet.patcher.monkey_patch(all=False, socket=True) - -import datetime -import errno -import socket -import sys -import time - -import eventlet.wsgi -from oslo.config import cfg -import routes -import routes.middleware -import six -import webob.dec -import webob.exc -from xml.dom import minidom -from xml.parsers import expat - -from staccato.openstack.common import exception -from staccato.openstack.common.gettextutils import _ -from staccato.openstack.common import jsonutils -from staccato.openstack.common import log as logging -from staccato.openstack.common import service -from staccato.openstack.common import sslutils -from staccato.openstack.common import xmlutils - -socket_opts = [ - cfg.IntOpt('backlog', - default=4096, - help="Number of backlog requests to configure the socket with"), - cfg.IntOpt('tcp_keepidle', - default=600, - help="Sets the value of TCP_KEEPIDLE in seconds for each " - "server socket. Not supported on OS X."), -] - -CONF = cfg.CONF -CONF.register_opts(socket_opts) - -LOG = logging.getLogger(__name__) - - -def run_server(application, port, **kwargs): - """Run a WSGI server with the given application.""" - sock = eventlet.listen(('0.0.0.0', port)) - eventlet.wsgi.server(sock, application, **kwargs) - - -class Service(service.Service): - """ - Provides a Service API for wsgi servers. - - This gives us the ability to launch wsgi servers with the - Launcher classes in service.py. - """ - - def __init__(self, application, port, - host='0.0.0.0', backlog=4096, threads=1000): - self.application = application - self._port = port - self._host = host - self._backlog = backlog if backlog else CONF.backlog - self._socket = self._get_socket(host, port, self._backlog) - super(Service, self).__init__(threads) - - def _get_socket(self, host, port, backlog): - # TODO(dims): eventlet's green dns/socket module does not actually - # support IPv6 in getaddrinfo(). We need to get around this in the - # future or monitor upstream for a fix - info = socket.getaddrinfo(host, - port, - socket.AF_UNSPEC, - socket.SOCK_STREAM)[0] - family = info[0] - bind_addr = info[-1] - - sock = None - retry_until = time.time() + 30 - while not sock and time.time() < retry_until: - try: - sock = eventlet.listen(bind_addr, - backlog=backlog, - family=family) - if sslutils.is_enabled(): - sock = sslutils.wrap(sock) - - except socket.error as err: - if err.args[0] != errno.EADDRINUSE: - raise - eventlet.sleep(0.1) - if not sock: - raise RuntimeError(_("Could not bind to %(host)s:%(port)s " - "after trying for 30 seconds") % - {'host': host, 'port': port}) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - # sockets can hang around forever without keepalive - sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - - # This option isn't available in the OS X version of eventlet - if hasattr(socket, 'TCP_KEEPIDLE'): - sock.setsockopt(socket.IPPROTO_TCP, - socket.TCP_KEEPIDLE, - CONF.tcp_keepidle) - - return sock - - def start(self): - """Start serving this service using the provided server instance. - - :returns: None - - """ - super(Service, self).start() - self.tg.add_thread(self._run, self.application, self._socket) - - @property - def backlog(self): - return self._backlog - - @property - def host(self): - return self._socket.getsockname()[0] if self._socket else self._host - - @property - def port(self): - return self._socket.getsockname()[1] if self._socket else self._port - - def stop(self): - """Stop serving this API. - - :returns: None - - """ - super(Service, self).stop() - - def _run(self, application, socket): - """Start a WSGI server in a new green thread.""" - logger = logging.getLogger('eventlet.wsgi') - eventlet.wsgi.server(socket, - application, - custom_pool=self.tg.pool, - log=logging.WritableLogger(logger)) - - -class Middleware(object): - """ - Base WSGI middleware wrapper. These classes require an application to be - initialized that will be called next. By default the middleware will - simply call its wrapped app, or you can override __call__ to customize its - behavior. - """ - - def __init__(self, application): - self.application = application - - def process_request(self, req): - """ - Called on each request. - - If this returns None, the next application down the stack will be - executed. If it returns a response then that response will be returned - and execution will stop here. - """ - return None - - def process_response(self, response): - """Do whatever you'd like to the response.""" - return response - - @webob.dec.wsgify - def __call__(self, req): - response = self.process_request(req) - if response: - return response - response = req.get_response(self.application) - return self.process_response(response) - - -class Debug(Middleware): - """ - Helper class that can be inserted into any WSGI application chain - to get information about the request and response. - """ - - @webob.dec.wsgify - def __call__(self, req): - print(("*" * 40) + " REQUEST ENVIRON") - for key, value in req.environ.items(): - print(key, "=", value) - print() - resp = req.get_response(self.application) - - print(("*" * 40) + " RESPONSE HEADERS") - for (key, value) in resp.headers.iteritems(): - print(key, "=", value) - print() - - resp.app_iter = self.print_generator(resp.app_iter) - - return resp - - @staticmethod - def print_generator(app_iter): - """ - Iterator that prints the contents of a wrapper string iterator - when iterated. - """ - print(("*" * 40) + " BODY") - for part in app_iter: - sys.stdout.write(part) - sys.stdout.flush() - yield part - print() - - -class Router(object): - - """ - WSGI middleware that maps incoming requests to WSGI apps. - """ - - def __init__(self, mapper): - """ - Create a router for the given routes.Mapper. - - Each route in `mapper` must specify a 'controller', which is a - WSGI app to call. You'll probably want to specify an 'action' as - well and have your controller be a wsgi.Controller, who will route - the request to the action method. - - Examples: - mapper = routes.Mapper() - sc = ServerController() - - # Explicit mapping of one route to a controller+action - mapper.connect(None, "/svrlist", controller=sc, action="list") - - # Actions are all implicitly defined - mapper.resource("server", "servers", controller=sc) - - # Pointing to an arbitrary WSGI app. You can specify the - # {path_info:.*} parameter so the target app can be handed just that - # section of the URL. - mapper.connect(None, "/v1.0/{path_info:.*}", controller=BlogApp()) - """ - self.map = mapper - self._router = routes.middleware.RoutesMiddleware(self._dispatch, - self.map) - - @webob.dec.wsgify - def __call__(self, req): - """ - Route the incoming request to a controller based on self.map. - If no match, return a 404. - """ - return self._router - - @staticmethod - @webob.dec.wsgify - def _dispatch(req): - """ - Called by self._router after matching the incoming request to a route - and putting the information into req.environ. Either returns 404 - or the routed WSGI app's response. - """ - match = req.environ['wsgiorg.routing_args'][1] - if not match: - return webob.exc.HTTPNotFound() - app = match['controller'] - return app - - -class Request(webob.Request): - """Add some Openstack API-specific logic to the base webob.Request.""" - - default_request_content_types = ('application/json', 'application/xml') - default_accept_types = ('application/json', 'application/xml') - default_accept_type = 'application/json' - - def best_match_content_type(self, supported_content_types=None): - """Determine the requested response content-type. - - Based on the query extension then the Accept header. - Defaults to default_accept_type if we don't find a preference - - """ - supported_content_types = (supported_content_types or - self.default_accept_types) - - parts = self.path.rsplit('.', 1) - if len(parts) > 1: - ctype = 'application/{0}'.format(parts[1]) - if ctype in supported_content_types: - return ctype - - bm = self.accept.best_match(supported_content_types) - return bm or self.default_accept_type - - def get_content_type(self, allowed_content_types=None): - """Determine content type of the request body. - - Does not do any body introspection, only checks header - - """ - if "Content-Type" not in self.headers: - return None - - content_type = self.content_type - allowed_content_types = (allowed_content_types or - self.default_request_content_types) - - if content_type not in allowed_content_types: - raise exception.InvalidContentType(content_type=content_type) - return content_type - - -class Resource(object): - """ - WSGI app that handles (de)serialization and controller dispatch. - - Reads routing information supplied by RoutesMiddleware and calls - the requested action method upon its deserializer, controller, - and serializer. Those three objects may implement any of the basic - controller action methods (create, update, show, index, delete) - along with any that may be specified in the api router. A 'default' - method may also be implemented to be used in place of any - non-implemented actions. Deserializer methods must accept a request - argument and return a dictionary. Controller methods must accept a - request argument. Additionally, they must also accept keyword - arguments that represent the keys returned by the Deserializer. They - may raise a webob.exc exception or return a dict, which will be - serialized by requested content type. - """ - def __init__(self, controller, deserializer=None, serializer=None): - """ - :param controller: object that implement methods created by routes lib - :param deserializer: object that supports webob request deserialization - through controller-like actions - :param serializer: object that supports webob response serialization - through controller-like actions - """ - self.controller = controller - self.serializer = serializer or ResponseSerializer() - self.deserializer = deserializer or RequestDeserializer() - - @webob.dec.wsgify(RequestClass=Request) - def __call__(self, request): - """WSGI method that controls (de)serialization and method dispatch.""" - - try: - action, action_args, accept = self.deserialize_request(request) - except exception.InvalidContentType: - msg = _("Unsupported Content-Type") - return webob.exc.HTTPUnsupportedMediaType(explanation=msg) - except exception.MalformedRequestBody: - msg = _("Malformed request body") - return webob.exc.HTTPBadRequest(explanation=msg) - - action_result = self.execute_action(action, request, **action_args) - try: - return self.serialize_response(action, action_result, accept) - # return unserializable result (typically a webob exc) - except Exception, ex: - return action_result - - def deserialize_request(self, request): - return self.deserializer.deserialize(request) - - def serialize_response(self, action, action_result, accept): - return self.serializer.serialize(action_result, accept, action) - - def execute_action(self, action, request, **action_args): - return self.dispatch(self.controller, action, request, **action_args) - - def dispatch(self, obj, action, *args, **kwargs): - """Find action-specific method on self and call it.""" - try: - method = getattr(obj, action) - except AttributeError: - method = getattr(obj, 'default') - - return method(*args, **kwargs) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args - - -class ActionDispatcher(object): - """Maps method name to local methods through action name.""" - - def dispatch(self, *args, **kwargs): - """Find and call local method.""" - action = kwargs.pop('action', 'default') - action_method = getattr(self, str(action), self.default) - return action_method(*args, **kwargs) - - def default(self, data): - raise NotImplementedError() - - -class DictSerializer(ActionDispatcher): - """Default request body serialization""" - - def serialize(self, data, action='default'): - return self.dispatch(data, action=action) - - def default(self, data): - return "" - - -class JSONDictSerializer(DictSerializer): - """Default JSON request body serialization""" - - def default(self, data): - def sanitizer(obj): - if isinstance(obj, datetime.datetime): - _dtime = obj - datetime.timedelta(microseconds=obj.microsecond) - return _dtime.isoformat() - return six.text_type(obj) - return jsonutils.dumps(data, default=sanitizer) - - -class XMLDictSerializer(DictSerializer): - - def __init__(self, metadata=None, xmlns=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - :param xmlns: XML namespace to include with serialized xml - """ - super(XMLDictSerializer, self).__init__() - self.metadata = metadata or {} - self.xmlns = xmlns - - def default(self, data): - # We expect data to contain a single key which is the XML root. - root_key = data.keys()[0] - doc = minidom.Document() - node = self._to_xml_node(doc, self.metadata, root_key, data[root_key]) - - return self.to_xml_string(node) - - def to_xml_string(self, node, has_atom=False): - self._add_xmlns(node, has_atom) - return node.toprettyxml(indent=' ', encoding='UTF-8') - - #NOTE (ameade): the has_atom should be removed after all of the - # xml serializers and view builders have been updated to the current - # spec that required all responses include the xmlns:atom, the has_atom - # flag is to prevent current tests from breaking - def _add_xmlns(self, node, has_atom=False): - if self.xmlns is not None: - node.setAttribute('xmlns', self.xmlns) - if has_atom: - node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom") - - def _to_xml_node(self, doc, metadata, nodename, data): - """Recursive method to convert data members to XML nodes.""" - result = doc.createElement(nodename) - - # Set the xml namespace if one is specified - # TODO(justinsb): We could also use prefixes on the keys - xmlns = metadata.get('xmlns', None) - if xmlns: - result.setAttribute('xmlns', xmlns) - - #TODO(bcwaldon): accomplish this without a type-check - if type(data) is list: - collections = metadata.get('list_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for item in data: - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(item)) - result.appendChild(node) - return result - singular = metadata.get('plurals', {}).get(nodename, None) - if singular is None: - if nodename.endswith('s'): - singular = nodename[:-1] - else: - singular = 'item' - for item in data: - node = self._to_xml_node(doc, metadata, singular, item) - result.appendChild(node) - #TODO(bcwaldon): accomplish this without a type-check - elif type(data) is dict: - collections = metadata.get('dict_collections', {}) - if nodename in collections: - metadata = collections[nodename] - for k, v in data.items(): - node = doc.createElement(metadata['item_name']) - node.setAttribute(metadata['item_key'], str(k)) - text = doc.createTextNode(str(v)) - node.appendChild(text) - result.appendChild(node) - return result - attrs = metadata.get('attributes', {}).get(nodename, {}) - for k, v in data.items(): - if k in attrs: - result.setAttribute(k, str(v)) - else: - node = self._to_xml_node(doc, metadata, k, v) - result.appendChild(node) - else: - # Type is atom - node = doc.createTextNode(str(data)) - result.appendChild(node) - return result - - def _create_link_nodes(self, xml_doc, links): - link_nodes = [] - for link in links: - link_node = xml_doc.createElement('atom:link') - link_node.setAttribute('rel', link['rel']) - link_node.setAttribute('href', link['href']) - if 'type' in link: - link_node.setAttribute('type', link['type']) - link_nodes.append(link_node) - return link_nodes - - -class ResponseHeadersSerializer(ActionDispatcher): - """Default response headers serialization""" - - def serialize(self, response, data, action): - self.dispatch(response, data, action=action) - - def default(self, response, data): - response.status_int = 200 - - -class ResponseSerializer(object): - """Encode the necessary pieces into a response object""" - - def __init__(self, body_serializers=None, headers_serializer=None): - self.body_serializers = { - 'application/xml': XMLDictSerializer(), - 'application/json': JSONDictSerializer(), - } - self.body_serializers.update(body_serializers or {}) - - self.headers_serializer = (headers_serializer or - ResponseHeadersSerializer()) - - def serialize(self, response_data, content_type, action='default'): - """Serialize a dict into a string and wrap in a wsgi.Request object. - - :param response_data: dict produced by the Controller - :param content_type: expected mimetype of serialized response body - - """ - response = webob.Response() - self.serialize_headers(response, response_data, action) - self.serialize_body(response, response_data, content_type, action) - return response - - def serialize_headers(self, response, data, action): - self.headers_serializer.serialize(response, data, action) - - def serialize_body(self, response, data, content_type, action): - response.headers['Content-Type'] = content_type - if data is not None: - serializer = self.get_body_serializer(content_type) - response.body = serializer.serialize(data, action) - - def get_body_serializer(self, content_type): - try: - return self.body_serializers[content_type] - except (KeyError, TypeError): - raise exception.InvalidContentType(content_type=content_type) - - -class RequestHeadersDeserializer(ActionDispatcher): - """Default request headers deserializer""" - - def deserialize(self, request, action): - return self.dispatch(request, action=action) - - def default(self, request): - return {} - - -class RequestDeserializer(object): - """Break up a Request object into more useful pieces.""" - - def __init__(self, body_deserializers=None, headers_deserializer=None, - supported_content_types=None): - - self.supported_content_types = supported_content_types - - self.body_deserializers = { - 'application/xml': XMLDeserializer(), - 'application/json': JSONDeserializer(), - } - self.body_deserializers.update(body_deserializers or {}) - - self.headers_deserializer = (headers_deserializer or - RequestHeadersDeserializer()) - - def deserialize(self, request): - """Extract necessary pieces of the request. - - :param request: Request object - :returns: tuple of (expected controller action name, dictionary of - keyword arguments to pass to the controller, the expected - content type of the response) - - """ - action_args = self.get_action_args(request.environ) - action = action_args.pop('action', None) - - action_args.update(self.deserialize_headers(request, action)) - action_args.update(self.deserialize_body(request, action)) - - accept = self.get_expected_content_type(request) - - return (action, action_args, accept) - - def deserialize_headers(self, request, action): - return self.headers_deserializer.deserialize(request, action) - - def deserialize_body(self, request, action): - if not request.body: - LOG.debug(_("Empty body provided in request")) - return {} - - try: - content_type = request.get_content_type() - except exception.InvalidContentType: - LOG.debug(_("Unrecognized Content-Type provided in request")) - raise - - if content_type is None: - LOG.debug(_("No Content-Type provided in request")) - return {} - - try: - deserializer = self.get_body_deserializer(content_type) - except exception.InvalidContentType: - LOG.debug(_("Unable to deserialize body as provided Content-Type")) - raise - - return deserializer.deserialize(request.body, action) - - def get_body_deserializer(self, content_type): - try: - return self.body_deserializers[content_type] - except (KeyError, TypeError): - raise exception.InvalidContentType(content_type=content_type) - - def get_expected_content_type(self, request): - return request.best_match_content_type(self.supported_content_types) - - def get_action_args(self, request_environment): - """Parse dictionary created by routes library.""" - try: - args = request_environment['wsgiorg.routing_args'][1].copy() - except Exception: - return {} - - try: - del args['controller'] - except KeyError: - pass - - try: - del args['format'] - except KeyError: - pass - - return args - - -class TextDeserializer(ActionDispatcher): - """Default request body deserialization""" - - def deserialize(self, datastring, action='default'): - return self.dispatch(datastring, action=action) - - def default(self, datastring): - return {} - - -class JSONDeserializer(TextDeserializer): - - def _from_json(self, datastring): - try: - return jsonutils.loads(datastring) - except ValueError: - msg = _("cannot understand JSON") - raise exception.MalformedRequestBody(reason=msg) - - def default(self, datastring): - return {'body': self._from_json(datastring)} - - -class XMLDeserializer(TextDeserializer): - - def __init__(self, metadata=None): - """ - :param metadata: information needed to deserialize xml into - a dictionary. - """ - super(XMLDeserializer, self).__init__() - self.metadata = metadata or {} - - def _from_xml(self, datastring): - plurals = set(self.metadata.get('plurals', {})) - - try: - node = xmlutils.safe_minidom_parse_string(datastring).childNodes[0] - return {node.nodeName: self._from_xml_node(node, plurals)} - except expat.ExpatError: - msg = _("cannot understand XML") - raise exception.MalformedRequestBody(reason=msg) - - def _from_xml_node(self, node, listnames): - """Convert a minidom node to a simple Python type. - - :param listnames: list of XML node names whose subnodes should - be considered list items. - - """ - - if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: - return node.childNodes[0].nodeValue - elif node.nodeName in listnames: - return [self._from_xml_node(n, listnames) for n in node.childNodes] - else: - result = dict() - for attr in node.attributes.keys(): - result[attr] = node.attributes[attr].nodeValue - for child in node.childNodes: - if child.nodeType != node.TEXT_NODE: - result[child.nodeName] = self._from_xml_node(child, - listnames) - return result - - def find_first_child_named(self, parent, name): - """Search a nodes children for the first child with a given name""" - for node in parent.childNodes: - if node.nodeName == name: - return node - return None - - def find_children_named(self, parent, name): - """Return all of a nodes children who have the given name""" - for node in parent.childNodes: - if node.nodeName == name: - yield node - - def extract_text(self, node): - """Get the text field contained by the given node""" - if len(node.childNodes) == 1: - child = node.childNodes[0] - if child.nodeType == child.TEXT_NODE: - return child.nodeValue - return "" - - def default(self, datastring): - return {'body': self._from_xml(datastring)} diff --git a/staccato/openstack/common/xmlutils.py b/staccato/openstack/common/xmlutils.py deleted file mode 100644 index b131d3e..0000000 --- a/staccato/openstack/common/xmlutils.py +++ /dev/null @@ -1,74 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 IBM Corp. -# -# 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. - -from xml.dom import minidom -from xml.parsers import expat -from xml import sax -from xml.sax import expatreader - - -class ProtectedExpatParser(expatreader.ExpatParser): - """An expat parser which disables DTD's and entities by default.""" - - def __init__(self, forbid_dtd=True, forbid_entities=True, - *args, **kwargs): - # Python 2.x old style class - expatreader.ExpatParser.__init__(self, *args, **kwargs) - self.forbid_dtd = forbid_dtd - self.forbid_entities = forbid_entities - - def start_doctype_decl(self, name, sysid, pubid, has_internal_subset): - raise ValueError("Inline DTD forbidden") - - def entity_decl(self, entityName, is_parameter_entity, value, base, - systemId, publicId, notationName): - raise ValueError(" entity declaration forbidden") - - def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): - # expat 1.2 - raise ValueError(" unparsed entity forbidden") - - def external_entity_ref(self, context, base, systemId, publicId): - raise ValueError(" external entity forbidden") - - def notation_decl(self, name, base, sysid, pubid): - raise ValueError(" notation forbidden") - - def reset(self): - expatreader.ExpatParser.reset(self) - if self.forbid_dtd: - self._parser.StartDoctypeDeclHandler = self.start_doctype_decl - self._parser.EndDoctypeDeclHandler = None - if self.forbid_entities: - self._parser.EntityDeclHandler = self.entity_decl - self._parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl - self._parser.ExternalEntityRefHandler = self.external_entity_ref - self._parser.NotationDeclHandler = self.notation_decl - try: - self._parser.SkippedEntityHandler = None - except AttributeError: - # some pyexpat versions do not support SkippedEntity - pass - - -def safe_minidom_parse_string(xml_string): - """Parse an XML string using minidom safely. - - """ - try: - return minidom.parseString(xml_string, parser=ProtectedExpatParser()) - except sax.SAXParseException: - raise expat.ExpatError() diff --git a/staccato/protocols/__init__.py b/staccato/protocols/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/protocols/file/__init__.py b/staccato/protocols/file/__init__.py deleted file mode 100644 index 2275132..0000000 --- a/staccato/protocols/file/__init__.py +++ /dev/null @@ -1,119 +0,0 @@ -import staccato.protocols.interface as base -from staccato.common import exceptions - - -class FileProtocol(base.BaseProtocolInterface): - - def __init__(self, service_config): - self.conf = service_config - - def _validate_url(self, url_parts): - pass - - def new_write(self, dsturl_parts, dst_opts): - return dst_opts - - def new_read(self, srcurl_parts, src_opts): - return src_opts - - def get_reader(self, url_parts, writer, monitor, source_opts, start=0, - end=None, **kwvals): - self._validate_url(url_parts) - - return FileReadConnection(url_parts.path, - writer, - monitor, - start=start, - end=end, - buflen=65536, - **kwvals) - - def get_writer(self, url_parts, dest_opts, checkpointer, **kwvals): - self._validate_url(url_parts) - - return FileWriteConnection(url_parts.path, checkpointer=checkpointer, - **kwvals) - - -class FileReadConnection(base.BaseReadConnection): - - def __init__(self, - path, - writer, - monitor, - start=0, - end=None, - buflen=65536, - **kwvals): - - try: - self.fptr = open(path, 'rb') - except IOError, ioe: - raise exceptions.StaccatoProtocolConnectionException( - ioe.message) - self.pos = start - self.eof = False - self.writer = writer - self.path = path - self.buflen = buflen - self.end = end - self.monitor = monitor - - def _read(self, buflen): - current_pos = self.fptr.tell() - if current_pos != self.pos: - self.fptr.seek(self.pos) - - if self.end and self.pos + buflen > self.end: - buflen = self.end - self.pos - buf = self.fptr.read(buflen) - if not buf: - return True, 0 - self.writer.write(buf, self.pos) - - self.pos = self.pos + len(buf) - if self.end and self.pos >= self.end: - return True, len(buf) - if len(buf) < buflen: - return True, len(buf) - return False, len(buf) - - def process(self): - if isinstance(self.writer, FileWriteConnection): - # TODO here we can do a system copy optimization - pass - - try: - while not self.monitor.is_done() and not self.eof: - self.eof, read_len = self._read(self.buflen) - finally: - self.fptr.close() - - -class FileWriteConnection(base.BaseWriteConnection): - - def __init__(self, path, checkpointer=None, **kwvals): - self.count = 0 - self.persist = checkpointer - try: - self.fptr = open(path, 'wb') - except IOError, ioe: - raise exceptions.StaccatoProtocolConnectionException( - ioe.message) - - def write(self, buffer, offset): - self.fptr.seek(offset, 0) - rc = self.fptr.write(buffer) - if self.persist: - self.persist.update(offset, offset + len(buffer)) - self.count = self.count + 1 - if self.count > 10: - self.count = 0 - self.fptr.flush() - if self.persist: - self.persist.sync({}) - - return rc - - def close(self): - return self.fptr.close() diff --git a/staccato/protocols/http/__init__.py b/staccato/protocols/http/__init__.py deleted file mode 100644 index f80597f..0000000 --- a/staccato/protocols/http/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -import urllib2 - -import staccato.protocols.interface as base -from staccato.common import exceptions - - -class HttpProtocol(base.BaseProtocolInterface): - - def __init__(self, service_config): - self.conf = service_config - - def _validate_url(self, url_parts): - pass - - def _parse_opts(self, opts): - return opts - - def new_write(self, dsturl_parts, dst_opts): - opts = self._parse_opts(dst_opts) - return opts - - def new_read(self, srcurl_parts, src_opts): - opts = self._parse_opts(src_opts) - return opts - - def get_reader(self, url_parts, writer, monitor, source_opts, start=0, - end=None, **kwvals): - self._validate_url(url_parts) - - return HttpReadConnection(url_parts, - writer, - monitor, - start=start, - end=end, - **kwvals) - - def get_writer(self, url_parts, dest_opts, checkpointer, **kwvals): - raise exceptions.StaccatoNotImplementedException( - _('The HTTP protocol is read only')) - - -class HttpReadConnection(base.BaseReadConnection): - - def __init__(self, - url_parts, - writer, - monitor, - start=0, - end=None, - buflen=65536, - **kwvals): - whole_url = url_parts.geturl() - - req = urllib2.Request(whole_url) - range_str = 'bytes=%sd-' % start - if end and end > start: - range_str = range_str + str(end) - req.headers['Range'] = range_str - self.h_con = urllib2.urlopen(req) - self.pos = start - self.eof = False - self.writer = writer - self.buflen = buflen - self.monitor = monitor - - def _read(self, buflen): - buf = self.h_con.read(buflen) - if not buf: - return True, 0 - self.writer.write(buf, self.pos) - - self.pos = self.pos + len(buf) - return False, len(buf) - - def process(self): - try: - while not self.monitor.is_done() and not self.eof: - self.eof, read_len = self._read(self.buflen) - finally: - self.h_con.close() diff --git a/staccato/protocols/interface.py b/staccato/protocols/interface.py deleted file mode 100644 index 7c1f6dd..0000000 --- a/staccato/protocols/interface.py +++ /dev/null @@ -1,39 +0,0 @@ -from staccato.common import utils - - -class BaseProtocolInterface(object): - - @utils.not_implemented_decorator - def get_reader(self, url_parts, writer, monitor, source_opts, start=0, - end=None, **kwvals): - pass - - @utils.not_implemented_decorator - def get_writer(self, url_parts, dest_opts, checkpointer, **kwvals): - pass - - @utils.not_implemented_decorator - def new_write(self, request, dsturl_parts, dst_opts): - pass - - @utils.not_implemented_decorator - def new_read(self, request, srcurl_parts, src_opts): - pass - - -class BaseReadConnection(object): - - @utils.not_implemented_decorator - def process(self): - pass - - -class BaseWriteConnection(object): - - @utils.not_implemented_decorator - def write(self, buffer, offset): - pass - - @utils.not_implemented_decorator - def close(self): - pass diff --git a/staccato/scheduler/__init__.py b/staccato/scheduler/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/scheduler/interface.py b/staccato/scheduler/interface.py deleted file mode 100644 index b73a401..0000000 --- a/staccato/scheduler/interface.py +++ /dev/null @@ -1,6 +0,0 @@ -import staccato.scheduler.simple_thread as simple_thread - - -def get_scheduler(conf): - # todo: pull this from the configuration information - return simple_thread.SimpleCountSchedler(conf) diff --git a/staccato/scheduler/simple_thread.py b/staccato/scheduler/simple_thread.py deleted file mode 100644 index 130f349..0000000 --- a/staccato/scheduler/simple_thread.py +++ /dev/null @@ -1,49 +0,0 @@ -import time - -import staccato.openstack.common.service as os_service -import staccato.xfer.events as s_events -import staccato.xfer.executor as s_executor -import staccato.xfer.constants as s_constants -from staccato.xfer.constants import Events -import staccato.db as s_db - - -class SimpleCountSchedler(os_service.Service): - - def __init__(self, conf): - super(SimpleCountSchedler, self).__init__() - self.max_at_once = 1 - self.db_obj = s_db.StaccatoDB(conf) - self.executor = s_executor.SimpleThreadExecutor(conf) - self.state_machine = s_events.XferStateMachine(self.executor) - self.running = 0 - self.done = False - self._started_ids = [] - - def _poll_db(self): - while not self.done: - time.sleep(1) - self._check_for_transfers() - - def _new_transfer(self, request): - self.running += 1 - self._started_ids.append(request.id) - self.state_machine.event_occurred(Events.EVENT_START, - xfer_request=request, - db=self.db_obj) - - def _transfer_complete(self): - self.running -= 1 - - def _check_for_transfers(self): - requests = self.db_obj.get_xfer_requests(self._started_ids) - for r in requests: - if s_constants.is_state_done_running(r.state): - self._started_ids.remove(r.id) - avail = self.max_at_once - len(self._started_ids) - xfer_request_ready = self.db_obj.get_all_ready(limit=avail) - for request in xfer_request_ready: - self._new_transfer(request) - - def start(self): - self.tg.add_thread(self._poll_db) diff --git a/staccato/tests/__init__.py b/staccato/tests/__init__.py deleted file mode 100644 index 7fcc62c..0000000 --- a/staccato/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'jbresnah' diff --git a/staccato/tests/functional/__init__.py b/staccato/tests/functional/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/tests/functional/test_db.py b/staccato/tests/functional/test_db.py deleted file mode 100644 index d65c47d..0000000 --- a/staccato/tests/functional/test_db.py +++ /dev/null @@ -1,376 +0,0 @@ -import os -from staccato.tests import utils -from staccato.common import config -import staccato.common.exceptions as exceptions -import staccato.xfer.constants as constants - - -class TestDB(utils.TempFileCleanupBaseTest): - - def setUp(self): - super(TestDB, self).setUp() - - self.owner = 'someperson' - self.tmp_db = self.get_tempfile() - self.db_url = 'sqlite:///%s' % (self.tmp_db) - - conf_d = {'sql_connection': self.db_url, - 'protocol_policy': ''} - - self.conf_file = self.make_confile(conf_d) - self.conf = config.get_config_object( - args=[], - default_config_files=[self.conf_file]) - self.db = self.make_db(self.conf) - - def test_db_creation(self): - self.assertTrue(os.path.exists(self.tmp_db)) - - def test_db_new_xfer(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - xfer = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - self.assertEqual(src, xfer.srcurl) - self.assertEqual(dst, xfer.dsturl) - self.assertEqual(sm, xfer.src_module_name) - self.assertEqual(dm, xfer.dst_module_name) - - def test_db_xfer_lookup(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.lookup_xfer_request_by_id(xfer1.id) - self.assertEqual(xfer1.id, xfer2.id) - - def test_db_xfer_lookup_with_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.lookup_xfer_request_by_id(xfer1.id, owner=self.owner) - self.assertEqual(xfer1.id, xfer2.id) - - def test_db_xfer_lookup_with_wrong_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - self.assertRaises(exceptions.StaccatoNotFoundInDBException, - self.db.lookup_xfer_request_by_id, - xfer1.id, **{'owner': 'someoneelse'}) - - def test_db_xfer_lookup_not_there(self): - self.assertRaises(exceptions.StaccatoNotFoundInDBException, - self.db.lookup_xfer_request_by_id, - "notthere") - - def test_db_xfer_update(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer1.next_ndx = 10 - self.db.save_db_obj(xfer1) - xfer2 = self.db.lookup_xfer_request_by_id(xfer1.id) - self.assertEqual(xfer2.next_ndx, 10) - - def test_lookup_all_no_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.lookup_xfer_request_all() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_lookup_all_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.lookup_xfer_request_all(owner=self.owner) - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_lookup_all_wrong_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.lookup_xfer_request_all(owner='notme') - self.assertEqual(len(xfer_list), 0) - - def test_lookup_all_many_owners(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer('notme', src, dst, sm, dm) - xfer4 = self.db.get_new_xfer('notme', src, dst, sm, dm) - xfer5 = self.db.get_new_xfer('someoneelse', src, dst, sm, dm) - - xfer_list = self.db.lookup_xfer_request_all(owner=self.owner) - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_get_all_ready_new_no_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.get_all_ready() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_get_all_ready_new_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.get_all_ready(owner=self.owner) - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_get_all_ready_wrong_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer_list = self.db.get_all_ready(owner='notme') - self.assertEqual(len(xfer_list), 0) - - def test_get_all_ready_some_not(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer3.state = constants.States.STATE_RUNNING - self.db.save_db_obj(xfer3) - - xfer_list = self.db.get_all_ready() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_get_all_ready_some_error(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - xfer2.state = constants.States.STATE_ERROR - self.db.save_db_obj(xfer2) - - xfer_list = self.db.get_all_ready() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - - def test_get_all_running(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - for x in [xfer1, xfer2, xfer3]: - x.state = constants.States.STATE_RUNNING - self.db.save_db_obj(x) - - xfer_list = self.db.get_all_running() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 3) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_all_running_some_not(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - for x in [xfer1, xfer3]: - x.state = constants.States.STATE_RUNNING - self.db.save_db_obj(x) - - xfer_list = self.db.get_all_running() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_delete_from_db(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - self.db.delete_db_obj(xfer2) - - xfer_list = self.db.get_all_ready() - id_list = [x.id for x in xfer_list] - - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_many_requests_no_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - id_list = [xfer1.id, xfer2.id, xfer3.id] - xfer_list = self.db.get_xfer_requests(ids=id_list) - self.assertEqual(len(xfer_list), 3) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_many_requests_owner(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - id_list = [xfer1.id, xfer2.id, xfer3.id] - xfer_list = self.db.get_xfer_requests(ids=id_list, owner=self.owner) - self.assertEqual(len(xfer_list), 3) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_many_requests_owner_subset(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - id_list = [xfer1.id, xfer3.id] - xfer_list = self.db.get_xfer_requests(ids=id_list, owner=self.owner) - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_many_requests_some_wrong_owner_subset(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer3 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer4 = self.db.get_new_xfer('notme', src, dst, sm, dm) - - id_list = [xfer1.id, xfer3.id] - xfer_list = self.db.get_xfer_requests(ids=id_list, owner=self.owner) - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer3.id in id_list) - - def test_get_many_requests_get_invalid(self): - src = "src://url" - dst = "dst://url" - sm = "src.module" - dm = "dst.module" - - xfer1 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - xfer2 = self.db.get_new_xfer(self.owner, src, dst, sm, dm) - - id_list = [xfer1.id, xfer2.id, 'nothereatall'] - xfer_list = self.db.get_xfer_requests(ids=id_list, owner=self.owner) - self.assertEqual(len(xfer_list), 2) - self.assertTrue(xfer1.id in id_list) - self.assertTrue(xfer2.id in id_list) diff --git a/staccato/tests/integration/__init__.py b/staccato/tests/integration/__init__.py deleted file mode 100644 index 7fcc62c..0000000 --- a/staccato/tests/integration/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'jbresnah' diff --git a/staccato/tests/integration/base.py b/staccato/tests/integration/base.py deleted file mode 100644 index 3729b0b..0000000 --- a/staccato/tests/integration/base.py +++ /dev/null @@ -1,102 +0,0 @@ -# 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 os.path - -import json - -import staccato.common.config as config -import staccato.common.utils as staccato_utils -import staccato.openstack.common.pastedeploy as os_pastedeploy -import staccato.tests.utils as test_utils - -TESTING_API_PASTE_CONF = """ -[pipeline:staccato-api] -pipeline = unauthenticated-context rootapp - -# Use this pipeline for keystone auth -[pipeline:staccato-api-keystone] -pipeline = authtoken context rootapp - -[app:rootapp] -use = egg:Paste#urlmap -/v1: apiv1app -/: apiversions - -[app:apiversions] -paste.app_factory = staccato.openstack.common.pastedeploy:app_factory -openstack.app_factory = staccato.api.versions:VersionApp - -[app:apiv1app] -paste.app_factory = staccato.openstack.common.pastedeploy:app_factory -openstack.app_factory = staccato.api.v1.xfer:API - -[filter:unauthenticated-context] -paste.filter_factory = staccato.openstack.common.pastedeploy:filter_factory -openstack.filter_factory = staccato.api.v1.xfer:UnauthTestMiddleware - -[filter:authtoken] -paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory -delay_auth_decision = true - -[filter:context] -paste.filter_factory = staccato.openstack.common.pastedeploy:filter_factory -openstack.filter_factory = staccato.api.v1.xfer:AuthContextMiddleware -""" - - -class ApiTestBase(test_utils.TempFileCleanupBaseTest): - def setUp(self): - super(ApiTestBase, self).setUp() - self.test_dir = self.get_tempdir() - self.sql_connection = 'sqlite://' - self.conf = config.get_config_object(args=[]) - self.config(sql_connection=self.sql_connection) - self.write_protocol_module_file() - self.config(db_auto_create=True) - self.needs_database = True - - def get_http_client(self): - staccato_api = self._load_paste_app( - 'staccato-api', TESTING_API_PASTE_CONF, self.conf) - return test_utils.Httplib2WsgiAdapter(staccato_api) - - def _load_paste_app(self, name, paste_conf, conf): - conf_file_path = os.path.join(self.test_dir, '%s-paste.ini' % name) - with open(conf_file_path, 'wb') as conf_file: - conf_file.write(paste_conf) - conf_file.flush() - - return os_pastedeploy.paste_deploy_app(conf_file_path, - name, - conf) - - def tearDown(self): - super(ApiTestBase, self).tearDown() - - def config(self, **kw): - group = kw.pop('group', None) - for k, v in kw.iteritems(): - self.conf.set_override(k, v, group) - - def write_protocol_module_file(self, protocols=None): - if protocols is None: - protocols = { - "file": [{"module": "staccato.protocols.file.FileProtocol"}], - "http": [{"module": "staccato.protocols.http.HttpProtocol"}] - } - temp_file = self.get_tempfile() - with open(temp_file, 'w') as fp: - json.dump(protocols, fp) - - self.config(protocol_policy=temp_file) - return temp_file diff --git a/staccato/tests/integration/test_api.py b/staccato/tests/integration/test_api.py deleted file mode 100644 index 5a934c7..0000000 --- a/staccato/tests/integration/test_api.py +++ /dev/null @@ -1,235 +0,0 @@ -import json - -import staccato.tests.integration.base as base - - -class TestApiNoSchedulerBasicFunctions(base.ApiTestBase): - - def _list_transfers(self, http_client): - path = "/v1/transfers" - response, content = http_client.request(path, 'GET') - self.assertEqual(response.status, 200) - data = json.loads(content) - return data - - def _cancel_transfer(self, http_client, id): - data_json = {'xferaction': 'cancel'} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - path = "/v1/transfers/%s/action" % id - return http_client.request(path, 'POST', headers=headers, body=data) - - def _delete_transfer(self, http_client, id): - path = "/v1/transfers/%s" % id - return http_client.request(path, 'DELETE') - - def _status_transfer(self, http_client, id): - path = "/v1/transfers/%s" % id - return http_client.request(path, 'GET') - - def _create_xfer(self, http_client, src='file:///etc/group', - dst='file:///dev/null'): - path = "/v1/transfers" - - data_json = {'source_url': src, - 'destination_url': dst} - data = json.dumps(data_json) - - headers = {'content-type': 'application/json'} - response, content = http_client.request(path, 'POST', body=data, - headers=headers) - return response, content - - def test_get_simple_empty_list(self): - http_client = self.get_http_client() - data = self._list_transfers(http_client) - self.assertEqual([], data) - - def test_simple_create_transfer(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - - def test_simple_create_transfer_list(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = self._list_transfers(http_client) - self.assertEqual(len(data), 1) - - def test_simple_create_transfer_list_delete(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = self._list_transfers(http_client) - self.assertEqual(len(data), 1) - response, content = self._delete_transfer(http_client, data[0]['id']) - self.assertEqual(response.status, 200) - data = self._list_transfers(http_client) - self.assertEqual(data, []) - - def test_simple_create_transfer_status(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - response, content = self._status_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - data_status = json.loads(content) - self.assertEquals(data, data_status) - - def test_delete_unknown(self): - http_client = self.get_http_client() - response, content = self._delete_transfer(http_client, 'notreal') - self.assertEqual(response.status, 404) - - def test_delete_twice(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - response, content = self._delete_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - response, content = self._delete_transfer(http_client, data['id']) - self.assertEqual(response.status, 404) - - def test_status_unknown(self): - http_client = self.get_http_client() - response, content = self._delete_transfer(http_client, 'notreal') - self.assertEqual(response.status, 404) - - def test_status_after_delete(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - response, content = self._delete_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - response, content = self._status_transfer(http_client, data['id']) - self.assertEqual(response.status, 404) - - def test_create_state(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - self.assertEqual(data['state'], 'STATE_NEW') - - def test_create_cancel(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - response, content = self._cancel_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - response, content = self._status_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - data = json.loads(content) - self.assertEqual(data['state'], 'STATE_CANCELED') - - def test_create_cancel_delete(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - data = json.loads(content) - response, content = self._cancel_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - response, content = self._status_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - response, content = self._delete_transfer(http_client, data['id']) - self.assertEqual(response.status, 200) - - def test_cancel_unknown(self): - http_client = self.get_http_client() - response, content = self._cancel_transfer(http_client, 'notreal') - self.assertEqual(response.status, 404) - - def test_simple_create_bad_source(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client, src="bad_form") - self.assertEqual(response.status, 400) - - def test_simple_create_bad_dest(self): - http_client = self.get_http_client() - response, content = self._create_xfer(http_client, dst="bad_form") - self.assertEqual(response.status, 400) - - def test_bad_update(self): - http_client = self.get_http_client() - data_json = {'notaction': 'cancel'} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - path = "/v1/transfers/%s/action" % id - response, content = http_client.request(path, 'POST', headers=headers, - body=data) - self.assertEqual(response.status, 400) - - def test_bad_action(self): - http_client = self.get_http_client() - data_json = {'xferaction': 'applesauce'} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - path = "/v1/transfers/%s/action" % id - response, content = http_client.request( - path, 'POST', headers=headers, body=data) - self.assertEqual(response.status, 400) - - def test_create_url_options(self): - path = "/v1/transfers" - http_client = self.get_http_client() - - data_json = {'source_url': 'file:///etc/group', - 'destination_url': 'file:///dev/null', - 'source_options': {'key': 10}, - 'destination_options': [1, 3, 5]} - data = json.dumps(data_json) - - headers = {'content-type': 'application/json'} - response, content = http_client.request(path, 'POST', body=data, - headers=headers) - self.assertEqual(response.status, 200) - data_out = json.loads(content) - self.assertEqual(data_json['source_options'], - data_out['source_options']) - self.assertEqual(data_json['destination_options'], - data_out['destination_options']) - - def test_create_missing_url(self): - path = "/v1/transfers" - http_client = self.get_http_client() - - data_json = {'source_url': 'file:///etc/group'} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - response, content = http_client.request(path, 'POST', body=data, - headers=headers) - self.assertEqual(response.status, 400) - - def test_create_uknown_option(self): - path = "/v1/transfers" - http_client = self.get_http_client() - - data_json = {'source_url': 'file:///etc/group', - 'destination_url': 'file:///dev/zero', - 'random': 90} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - response, content = http_client.request(path, 'POST', body=data, - headers=headers) - self.assertEqual(response.status, 400) - - def test_list_limit(self): - http_client = self.get_http_client() - for i in range(10): - response, content = self._create_xfer(http_client) - self.assertEqual(response.status, 200) - - path = "/v1/transfers" - data_json = {'limit': 5} - data = json.dumps(data_json) - headers = {'content-type': 'application/json'} - response, content = http_client.request(path, 'GET', body=data, - headers=headers) - self.assertEqual(response.status, 200) - data = json.loads(content) - self.assertEqual(len(data), 5) diff --git a/staccato/tests/unit/__init__.py b/staccato/tests/unit/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/tests/unit/test_config.py b/staccato/tests/unit/test_config.py deleted file mode 100644 index 99dfa02..0000000 --- a/staccato/tests/unit/test_config.py +++ /dev/null @@ -1,29 +0,0 @@ -import json -import testtools - -from staccato.common import config -from staccato.tests import utils - - -class TestConfig(utils.TempFileCleanupBaseTest): - - def test_db_connection_default(self): - conf = config.get_config_object(args=[]) - self.assertEquals(conf.sql_connection, 'sqlite:///staccato.sqlite') - - def test_protocol_policy_retrieve_none(self): - conf = config.get_config_object(args=[]) - conf.protocol_policy = None - j = config.get_protocol_policy(conf) - self.assertEqual(j, {}) - - def test_protocol_policy_retrieve(self): - conf = config.get_config_object(args=[]) - p_file = self.get_tempfile() - policy = {"hello": "world"} - fptr = open(p_file, 'w') - fptr.write(json.dumps(policy)) - fptr.close() - conf.protocol_policy = p_file - j = config.get_protocol_policy(conf) - self.assertEqual(j, policy) diff --git a/staccato/tests/unit/test_protocol_loading.py b/staccato/tests/unit/test_protocol_loading.py deleted file mode 100644 index 7056851..0000000 --- a/staccato/tests/unit/test_protocol_loading.py +++ /dev/null @@ -1,102 +0,0 @@ -import urlparse -import testtools -from staccato.common import utils, exceptions -import staccato.protocols.file as file_protocol - - -class TestProtocolLoading(testtools.TestCase): - - def setUp(self): - super(TestProtocolLoading, self).setUp() - - def test_basic_load(self): - proto_name = "staccato.protocols.file.FileProtocol" - inst = utils.load_protocol_module(proto_name, {}) - self.assertTrue(isinstance(inst, file_protocol.FileProtocol)) - - def test_failed_load(self): - self.assertRaises(exceptions.StaccatoParameterError, - utils.load_protocol_module, - "notAModule", {}) - - def test_find_module_default(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - module_path = "just.some.thing" - - lookup_dict = { - 'file': [{'module': module_path}] - } - res = utils.find_protocol_module_name(lookup_dict, url_parts) - self.assertEqual(res, module_path) - - def test_find_module_wildcards(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - module_path = "just.some.thing" - - lookup_dict = { - 'file': [{'module': module_path, - 'netloc': '.*', - 'path': '.*'}] - } - res = utils.find_protocol_module_name(lookup_dict, url_parts) - self.assertEqual(res, module_path) - - def test_find_module_multiple_wildcards(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - bad_module_path = "just.some.bad.thing" - good_module_path = "just.some.bad.thing" - - lookup_dict = { - 'file': [{'module': bad_module_path, - 'netloc': '.*', - 'path': '/sorry/.*'}, - {'module': good_module_path}] - } - res = utils.find_protocol_module_name(lookup_dict, url_parts) - self.assertEqual(res, good_module_path) - - def test_find_module_wildcards_middle(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - module_path = "just.some.thing" - - lookup_dict = { - 'file': [{'module': module_path, - 'netloc': '.*host.com.*', - 'path': '.*'}] - } - res = utils.find_protocol_module_name(lookup_dict, url_parts) - self.assertEqual(res, module_path) - - def test_find_module_not_found(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - module_path = "just.some.thing" - - lookup_dict = { - 'file': [{'module': module_path, - 'netloc': '.*', - 'path': '/secure/path/only.*'}] - } - self.assertRaises(exceptions.StaccatoParameterError, - utils.find_protocol_module_name, - lookup_dict, - url_parts) - - def test_find_no_url_scheme(self): - url = "file://host.com:90//path/to/file" - url_parts = urlparse.urlparse(url) - module_path = "just.some.thing" - - lookup_dict = { - 'junk': [{'module': module_path, - 'netloc': '.*', - 'path': '/secure/path/only.*'}] - } - self.assertRaises(exceptions.StaccatoParameterError, - utils.find_protocol_module_name, - lookup_dict, - url_parts) diff --git a/staccato/tests/unit/test_statemachine.py b/staccato/tests/unit/test_statemachine.py deleted file mode 100644 index 44038cd..0000000 --- a/staccato/tests/unit/test_statemachine.py +++ /dev/null @@ -1,55 +0,0 @@ -import testtools - -import mox - -import staccato.xfer.events as xfer_events -import staccato.xfer.constants as constants - - -class TestEventsStateMachine(testtools.TestCase): - - def setUp(self): - super(TestEventsStateMachine, self).setUp() - self.mox = mox.Mox() - - def tearDown(self): - super(TestEventsStateMachine, self).tearDown() - self.mox.UnsetStubs() - - def test_printer(self): - # just make sure it works - xfer_events._print_state_machine() - - def test_state_new_to_running(self): - executor = self.mox.CreateMockAnything() - db = self.mox.CreateMockAnything() - xfer_request = self.mox.CreateMockAnything() - xfer_request.state = constants.States.STATE_NEW - xfer_request.id = "ID" - my_states = xfer_events.XferStateMachine(executor) - self.mox.StubOutWithMock(my_states, 'state_running_handler') - self.mox.StubOutWithMock(my_states, '_get_current_state') - self.mox.StubOutWithMock(my_states, '_state_changed') - - my_states._get_current_state( - db=db, - xfer_request=xfer_request).AndReturn(constants.States.STATE_NEW) - my_states._state_changed(constants.States.STATE_NEW, - constants.Events.EVENT_START, - constants.States.STATE_RUNNING, - db=db, xfer_request=xfer_request) - - # the function pointers and mox make the next stub not work - # my_states.state_running_handler( - # constants.States.STATE_NEW, - # constants.Events.EVENT_START, - # constants.States.STATE_RUNNING, - # db, - # xfer_request) - executor.execute(xfer_request.id, my_states) - - self.mox.ReplayAll() - my_states.event_occurred(constants.Events.EVENT_START, - xfer_request=xfer_request, - db=db) - self.mox.VerifyAll() diff --git a/staccato/tests/unit/test_utils.py b/staccato/tests/unit/test_utils.py deleted file mode 100644 index 82f3548..0000000 --- a/staccato/tests/unit/test_utils.py +++ /dev/null @@ -1,107 +0,0 @@ -import testtools - -import staccato.tests.utils as tests_utils -import staccato.common.utils as common_utils - -import staccato.xfer.utils as xfers_utils -import staccato.protocols.interface as proto_iface -import staccato.common.exceptions as exceptions -import staccato.common.config as config - - -class FakeXferRequest(object): - next_ndx = 0 - - -class FakeDB(object): - - def save_db_obj(self, obj): - pass - - -class TestXferCheckpointerSingleSync(testtools.TestCase): - - def setUp(self): - super(TestXferCheckpointerSingleSync, self).setUp() - - self.fake_xfer = FakeXferRequest() - self.checker = xfers_utils.XferCheckpointer( - self.fake_xfer, {}, FakeDB(), db_refresh_rate=0) - - def _run_blocks(self, blocks): - for start, end in blocks: - self.checker.update(start, end) - self.checker.sync({}) - - def test_non_continuous_zero(self): - blocks = [(10, 20), (30, 40)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 0) - - def test_join_simple(self): - blocks = [(0, 20), (20, 40)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 40) - - def test_non_continuous(self): - blocks = [(0, 5), (10, 20)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 5) - - def test_join_single(self): - blocks = [(0, 20)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 20) - - def test_join_overlap(self): - blocks = [(0, 20), (10, 30)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 30) - - def test_join_included(self): - blocks = [(0, 20), (10, 15)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 20) - - def test_join_large_later(self): - blocks = [(10, 20), (30, 40), (0, 100)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 100) - - def test_join_out_of_order(self): - blocks = [(30, 40), (0, 10), (20, 30), (10, 25)] - self._run_blocks(blocks) - self.assertEqual(self.fake_xfer.next_ndx, 40) - - -class TestBasicUtils(tests_utils.TempFileCleanupBaseTest): - - def test_empty_interface(self): - blank_interface = proto_iface.BaseProtocolInterface() - kwargs = { - 'url_parts': None, - 'writer': None, - 'monitor': None, - 'source_opts': None, - 'start': 0, - 'end': None, - } - - interface_funcs = [blank_interface.get_reader, - blank_interface.get_writer, - blank_interface.new_read, - blank_interface.new_write] - - for func in interface_funcs: - self.assertRaises( - exceptions.StaccatoNotImplementedException, - func, - **kwargs) - - def test_load_paste_app_error(self): - conf = config.get_config_object(args=[]) - p_file = self.get_tempfile() - self.assertRaises( - RuntimeError, - common_utils.load_paste_app, - 'notthere', p_file, conf) diff --git a/staccato/tests/unit/v1/__init__.py b/staccato/tests/unit/v1/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/tests/unit/v1/test_api.py b/staccato/tests/unit/v1/test_api.py deleted file mode 100644 index 1b3a2b5..0000000 --- a/staccato/tests/unit/v1/test_api.py +++ /dev/null @@ -1,200 +0,0 @@ -import json -import mox -import uuid - -import webob.exc - -from staccato.tests import utils -import staccato.api.v1.xfer as v1_xfer -import staccato.db.models as db_models -import staccato.xfer.constants as constants -import staccato.common.config as config - - -def _make_xfer_request(): - x = db_models.XferRequest() - x.id = str(uuid.uuid4()) - x.state = constants.States.STATE_RUNNING - x.srcurl = "http://%s.com/path" % str(uuid.uuid4()) - x.dsturl = "file://%s.com/path" % str(uuid.uuid4()) - x.start_ndx = 0 - x.end_ndx = 150 - x.source_opts = {'somevalue': str(uuid.uuid4())} - x.dest_opts = {'destvalue': str(uuid.uuid4())} - return x - - -class TestDeserializer(utils.TempFileCleanupBaseTest): - - def test_new_transfer_good_required(self): - xd = v1_xfer.XferDeserializer() - body_json = {"source_url": "file://", "destination_url": "file:///"} - body = json.dumps(body_json) - results = xd.newtransfer(body) - self.assertEqual(results, body_json) - - def test_new_transfer_good_options(self): - xd = v1_xfer.XferDeserializer() - body_json = {"source_url": "file://", - "destination_url": "file:///", - "source_options": {'hello': 'world'}, - "start_offset": 10, - "end_offset": 100, - "destination_options": {}} - body = json.dumps(body_json) - results = xd.newtransfer(body) - self.assertEqual(results, body_json) - - def test_new_transfer_missing_required(self): - xd = v1_xfer.XferDeserializer() - body_json = {"source_url": "file://"} - body = json.dumps(body_json) - self.assertRaises(webob.exc.HTTPBadRequest, - xd.newtransfer, - body) - - def test_new_transfer_bad_option(self): - xd = v1_xfer.XferDeserializer() - body_json = {"source_url": "file://", - "destination_url": "file:///", - "not_good": 10}, - body = json.dumps(body_json) - self.assertRaises(webob.exc.HTTPBadRequest, - xd.newtransfer, - body) - - def test_default(self): - xd = v1_xfer.XferDeserializer() - results = xd.default('{}') - self.assertEqual(results, {'body': {}}) - - def test_xferaction(self): - xd = v1_xfer.XferDeserializer() - body_json = {"xferaction": "cancel"} - body = json.dumps(body_json) - results = xd.xferaction(body) - self.assertEqual(results, body_json) - - def test_bad_xferaction(self): - xd = v1_xfer.XferDeserializer() - body_json = {"xferaction": "notreal"} - body = json.dumps(body_json) - self.assertRaises(webob.exc.HTTPBadRequest, - xd.xferaction, - body) - - -class TestSerializer(utils.TempFileCleanupBaseTest): - - def setUp(self): - super(TestSerializer, self).setUp() - self.serializer = v1_xfer.XferSerializer() - - def _check_xfer(self, xfer, d): - self.assertEqual(xfer.id, d['id']) - self.assertEqual(xfer.srcurl, d['source_url']) - self.assertEqual(xfer.dsturl, d['destination_url']) - self.assertEqual(xfer.state, d['state']) - self.assertEqual(xfer.start_ndx, d['start_offset']) - self.assertEqual(xfer.end_ndx, d['end_offset']) - self.assertEqual(xfer.source_opts, d['source_options']) - self.assertEqual(xfer.dest_opts, d['destination_options']) - - def test_default(self): - x = _make_xfer_request() - ds = self.serializer.default(x) - d = json.loads(ds) - self._check_xfer(x, d) - - def test_list(self): - x1 = _make_xfer_request() - x2 = _make_xfer_request() - x3 = _make_xfer_request() - x4 = _make_xfer_request() - xfer_list = [x1, x2, x3, x4] - lookup = {} - for x in xfer_list: - lookup[x.id] = x - result_str = self.serializer.list(xfer_list) - result_list = json.loads(result_str) - for x_d in result_list: - x = lookup[x_d['id']] - self._check_xfer(x, x_d) - - -class TestController(utils.TempFileCleanupBaseTest): - - def setUp(self): - super(TestController, self).setUp() - self.mox = mox.Mox() - self.sm = self.mox.CreateMockAnything() - self.db = self.mox.CreateMockAnything() - self.request = self.mox.CreateMockAnything() - self.conf = config.get_config_object(args=[]) - self.controller = v1_xfer.XferController(self.db, self.sm, self.conf) - - def tearDown(self): - super(TestController, self).tearDown() - self.mox.UnsetStubs() - - def test_status(self): - xfer = _make_xfer_request() - - self.db.lookup_xfer_request_by_id( - xfer.id, owner='admin').AndReturn(xfer) - - self.mox.ReplayAll() - result = self.controller.status(self.request, xfer.id, 'admin') - self.mox.VerifyAll() - self.assertEqual(result, xfer) - - def test_status(self): - xfer = _make_xfer_request() - - self.db.lookup_xfer_request_by_id( - xfer.id, owner='admin').AndReturn(xfer) - - self.mox.ReplayAll() - result = self.controller.status(self.request, xfer.id, 'admin') - self.mox.VerifyAll() - self.assertEqual(result, xfer) - - def test_delete(self): - xfer = _make_xfer_request() - - self.db.lookup_xfer_request_by_id( - xfer.id, owner='admin').AndReturn(xfer) - self.sm.event_occurred(constants.Events.EVENT_DELETE, - xfer_request=xfer, - db=self.db) - self.mox.ReplayAll() - self.controller.delete(self.request, xfer.id, 'admin') - self.mox.VerifyAll() - - def test_new_request(self): - xfer = _make_xfer_request() - xfer.srcurl = "http://someplace.com" - xfer.dsturl = "file://path/to/no/where" - - self.mox.StubOutWithMock(config, 'get_protocol_policy') - - config.get_protocol_policy(self.conf).AndReturn( - { - "file": [{"module": "staccato.protocols.file.FileProtocol"}], - "http": [{"module": "staccato.protocols.http.HttpProtocol"}] - }) - - self.db.get_new_xfer('admin', - xfer.srcurl, - xfer.dsturl, - "staccato.protocols.http.HttpProtocol", - "staccato.protocols.file.FileProtocol", - start_ndx=0, - end_ndx=None, - source_opts={}, - dest_opts={}).AndReturn(xfer) - self.mox.ReplayAll() - res = self.controller.newtransfer(self.request, xfer.srcurl, - xfer.dsturl, 'admin') - self.mox.VerifyAll() - self.assertEqual(res, xfer) diff --git a/staccato/tests/utils.py b/staccato/tests/utils.py deleted file mode 100644 index 970c69b..0000000 --- a/staccato/tests/utils.py +++ /dev/null @@ -1,109 +0,0 @@ -import tempfile -import testtools -import os -import json -import webob - -import staccato.db as db - -TEST_CONF = """ -[DEFAULT] - -sql_connection = %(sql_connection)s -db_auto_create = True -log_level = DEBUG -protocol_policy = %(protocol_policy)s -""" - -FILE_ONLY_PROTOCOL = { - "file": [{"module": "staccato.protocols.file.FileProtocol"}] -} - - -class BaseTestCase(testtools.TestCase): - pass - - -class TempFileCleanupBaseTest(BaseTestCase): - - def setUp(self): - super(TempFileCleanupBaseTest, self).setUp() - self.files_to_delete = [] - - def make_db(self, conf): - return db.StaccatoDB(conf) - - def make_confile(self, d, protocol_policy=None): - conf_file = self.get_tempfile() - - if protocol_policy is not None: - protocol_policy_file = self.get_tempfile() - f = open(protocol_policy_file, 'w') - json.dump(protocol_policy, f) - f.close() - d.update({'protocol_policy': protocol_policy_file}) - - out_conf = TEST_CONF % d - fout = open(conf_file, 'w') - fout.write(out_conf) - fout.close() - - return conf_file - - def tearDown(self): - super(TempFileCleanupBaseTest, self).tearDown() - for f in self.files_to_delete: - try: - pass - os.remove(f) - except: - pass - - def get_tempfile(self): - fname = tempfile.mkstemp()[1] - self.files_to_delete.append(fname) - return fname - - def get_tempdir(self): - return tempfile.mkdtemp() - - -class Httplib2WsgiAdapter(object): - def __init__(self, app): - self.app = app - - def request(self, uri, method="GET", body=None, headers=None): - req = webob.Request.blank(uri, method=method, headers=headers) - req.body = body - resp = req.get_response(self.app) - return Httplib2WebobResponse(resp), resp.body - - -class Httplib2WebobResponse(object): - def __init__(self, webob_resp): - self.webob_resp = webob_resp - - @property - def status(self): - return self.webob_resp.status_code - - def __getitem__(self, key): - return self.webob_resp.headers[key] - - def get(self, key): - return self.webob_resp.headers[key] - - -class HttplibWsgiAdapter(object): - def __init__(self, app): - self.app = app - self.req = None - - def request(self, method, url, body=None, headers={}): - self.req = webob.Request.blank(url, method=method, headers=headers) - self.req.body = body - - def getresponse(self): - response = self.req.get_response(self.app) - return FakeHTTPResponse(response.status_code, response.headers, - response.body) diff --git a/staccato/version.py b/staccato/version.py deleted file mode 100644 index 47628a8..0000000 --- a/staccato/version.py +++ /dev/null @@ -1,20 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2012 OpenStack Foundation -# -# 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. - - -from staccato.openstack.common import version as common_version - -version_info = common_version.VersionInfo('staccato') diff --git a/staccato/xfer/__init__.py b/staccato/xfer/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/staccato/xfer/constants.py b/staccato/xfer/constants.py deleted file mode 100644 index 6f542fa..0000000 --- a/staccato/xfer/constants.py +++ /dev/null @@ -1,26 +0,0 @@ -class Events: - EVENT_NEW = "EVENT_NEW" - EVENT_START = "EVENT_START" - EVENT_ERROR = "EVENT_ERROR" - EVENT_COMPLETE = "EVENT_COMPLETE" - EVENT_CANCEL = "EVENT_CANCEL" - EVENT_DELETE = "EVENT_DELETE" - - -class States: - STATE_NEW = "STATE_NEW" - STATE_RUNNING = "STATE_RUNNING" - STATE_CANCELING = "STATE_CANCELING" - STATE_CANCELED = "STATE_CANCELED" - STATE_ERRORING = "STATE_ERRORING" - STATE_ERROR = "STATE_ERROR" - STATE_COMPLETE = "STATE_COMPLETE" - STATE_DELETED = "STATE_DELETED" - - -def is_state_done_running(state): - done_states = [States.STATE_CANCELED, - States.STATE_ERROR, - States.STATE_COMPLETE, - States.STATE_DELETED] - return state in done_states diff --git a/staccato/xfer/events.py b/staccato/xfer/events.py deleted file mode 100644 index 2380979..0000000 --- a/staccato/xfer/events.py +++ /dev/null @@ -1,126 +0,0 @@ -""" -This file describes events that can happen on a request structure -""" -from staccato.common import state_machine -from staccato.xfer import constants - - -class XferStateMachine(state_machine.StateMachine): - - def __init__(self, executor): - super(XferStateMachine, self).__init__() - self.map_states() - self.executor = executor - - def _state_changed(self, current_state, event, new_state, **kwvals): - xfer_request = kwvals['xfer_request'] - db = kwvals['db'] - xfer_request.state = new_state - db.save_db_obj(xfer_request) - - def _get_current_state(self, **kwvals): - xfer_request = kwvals['xfer_request'] - db = kwvals['db'] - xfer_request = db.lookup_xfer_request_by_id(xfer_request.id) - return xfer_request.state - - def state_noop_handler( - self, - current_state, - event, - new_state, - db, - xfer_request, - **kwvals): - """ - This handler just allows for the DB change. - """ - pass - - def state_running_handler( - self, - current_state, - event, - new_state, - db, - xfer_request, - **kwvals): - self.executor.execute(xfer_request.id, self) - - def state_delete_handler( - self, - current_state, - event, - new_state, - db, - xfer_request, - **kwvals): - db.delete_db_obj(xfer_request) - - def map_states(self): - self.set_state_func(constants.States.STATE_NEW, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_RUNNING, - self.state_running_handler) - self.set_state_func(constants.States.STATE_CANCELING, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_CANCELED, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_ERRORING, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_ERROR, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_COMPLETE, - self.state_noop_handler) - self.set_state_func(constants.States.STATE_DELETED, - self.state_delete_handler) - - # setup the state machine - self.set_mapping(constants.States.STATE_NEW, - constants.Events.EVENT_START, - constants.States.STATE_RUNNING) - self.set_mapping(constants.States.STATE_NEW, - constants.Events.EVENT_CANCEL, - constants.States.STATE_CANCELED) - self.set_mapping(constants.States.STATE_NEW, - constants.Events.EVENT_DELETE, - constants.States.STATE_DELETED) - - self.set_mapping(constants.States.STATE_CANCELED, - constants.Events.EVENT_DELETE, - constants.States.STATE_DELETED) - - self.set_mapping(constants.States.STATE_CANCELING, - constants.Events.EVENT_COMPLETE, - constants.States.STATE_COMPLETE) - - self.set_mapping(constants.States.STATE_RUNNING, - constants.Events.EVENT_COMPLETE, - constants.States.STATE_COMPLETE) - self.set_mapping(constants.States.STATE_RUNNING, - constants.Events.EVENT_CANCEL, - constants.States.STATE_CANCELING) - self.set_mapping(constants.States.STATE_RUNNING, - constants.Events.EVENT_ERROR, - constants.States.STATE_ERRORING) - - self.set_mapping(constants.States.STATE_ERRORING, - constants.Events.EVENT_COMPLETE, - constants.States.STATE_ERROR) - - self.set_mapping(constants.States.STATE_COMPLETE, - constants.Events.EVENT_DELETE, - constants.States.STATE_DELETED) - - self.set_mapping(constants.States.STATE_ERROR, - constants.Events.EVENT_START, - constants.States.STATE_RUNNING) - - -def _print_state_machine(): - """ - This function is here to create a state machine diagram of the actual - code - """ - my_states = XferStateMachine(None) - my_states.mapping_to_digraph() diff --git a/staccato/xfer/executor.py b/staccato/xfer/executor.py deleted file mode 100644 index 5ff49ae..0000000 --- a/staccato/xfer/executor.py +++ /dev/null @@ -1,78 +0,0 @@ -import threading -import urlparse - -from staccato import db -from staccato.common import utils -from staccato.xfer import constants -import staccato.xfer.utils as xfer_utils - - -def do_transfer(CONF, xfer_id, state_machine): - """ - This function does a transfer. It will create its own DB. This should be - run in its own thread. - """ - db_con = db.StaccatoDB(CONF) - try: - request = db_con.lookup_xfer_request_by_id(xfer_id) - - checkpointer = xfer_utils.XferCheckpointer(request, {}, db_con) - monitor = xfer_utils.XferReadMonitor(db_con, request.id) - - src_module = utils.load_protocol_module(request.src_module_name, CONF) - dst_module = utils.load_protocol_module(request.dst_module_name, CONF) - - dsturl_parts = urlparse.urlparse(request.dsturl) - writer = dst_module.get_writer(dsturl_parts, - request.dest_opts, - checkpointer=checkpointer) - - # it is up to the reader/writer to put on the bw limits - srcurl_parts = urlparse.urlparse(request.srcurl) - reader = src_module.get_reader(srcurl_parts, - writer, - monitor, - request.source_opts, - request.next_ndx, - request.end_ndx) - - reader.process() - except Exception, ex: - state_machine.event_occurred(constants.Events.EVENT_ERROR, - exception=ex, - conf=CONF, - xfer_request=request, - db=db_con) - raise - finally: - state_machine.event_occurred(constants.Events.EVENT_COMPLETE, - conf=CONF, - xfer_request=request, - db=db_con) - - -class SimpleThreadExecutor(object): - - def __init__(self, conf): - self.conf = conf - self.threads = [] - - def execute(self, xfer_id, state_machine): - thread = threading.Thread(target=self, args=(xfer_id, state_machine)) - thread.daemon = True - self.threads.append(thread) - thread.start() - - def __call__(self, xfer_id, state_machine): - do_transfer(self.conf, xfer_id, state_machine) - - def cleanup(self): - for t in self.threads[:]: - if not t.is_alive(): - t.join() - self.threads.pop(t) - - def shutdown(self): - for t in self.threads: - t.join() - self.threads = [] diff --git a/staccato/xfer/utils.py b/staccato/xfer/utils.py deleted file mode 100644 index 2ce4ebf..0000000 --- a/staccato/xfer/utils.py +++ /dev/null @@ -1,129 +0,0 @@ -import datetime -import time - -import staccato.xfer.constants as constants -import staccato.common.exceptions as exceptions - - -def _merge_one(blocks): - - if not blocks: - return blocks.copy() - merge = True - while merge: - new = {} - merge = False - keys = sorted(blocks.keys()) - ndx = 0 - current_key = keys[ndx] - new[current_key] = blocks[current_key] - ndx = ndx + 1 - - while ndx < len(keys): - next_key = keys[ndx] - start_i = current_key - start_j = next_key - - end_i = blocks[start_i] - end_j = blocks[start_j] - - if end_i >= start_j: - merge = True - new[start_i] = max(end_i, end_j) - #ndx = ndx + 1 - else: - new[start_j] = end_j - current_key = next_key - - ndx = ndx + 1 - blocks = new - return new - - -class XferDBUpdater(object): - - def __init__(self, db_refresh_rate=5): - self.db_refresh_rate = db_refresh_rate - self._set_time() - - def _set_time(self): - self.next_time = datetime.datetime.now() +\ - datetime.timedelta(seconds=self.db_refresh_rate) - - def _check_db_ready(self): - n = datetime.datetime.now() - if n > self.next_time: - self._set_time() - self._do_db_operation() - - -class XferReadMonitor(XferDBUpdater): - - def __init__(self, db, xfer_id, db_refresh_rate=5): - super(XferReadMonitor, self).__init__(db_refresh_rate=db_refresh_rate) - self.db = db - self.done = True # TODO base this on xfer_request - self.xfer_id = xfer_id - self._do_db_operation() - - def _do_db_operation(self): - self.request = self.db.lookup_xfer_request_by_id(self.xfer_id) - - def is_done(self): - self._check_db_ready() - return self.request.state != constants.States.STATE_RUNNING - - -class XferCheckpointer(XferDBUpdater): - """ - This class is used by protocol plugins to keep track of the progress of - a transfer. With each write the plugin can call update() and the blocks - will be tracked. When the protocol plugin has safely synced some data - to disk it can call sync(). Each call to sync may cause a write to the - database. - - This class will help write side connections keep track fo their workload - """ - def __init__(self, xfer_request, protocol_doc, db, db_refresh_rate=5): - """ - :param xfer_id: The transfer ID to be tracked. - :protocol doc: protocol specific information for tracking. This - should be a dict - """ - super(XferCheckpointer, self).__init__(db_refresh_rate=db_refresh_rate) - self.blocks = {} - self.db = db - self.protocol_doc = protocol_doc - self.xfer_request = xfer_request - self.update(0, 0) - - def update(self, block_start, block_end): - """ - :param block_start: the start of the block. - :param block_end: the end of the block. - """ - if block_end < block_start: - raise exceptions.StaccatoParameterError() - - if block_start in self.blocks: - self.blocks[block_start] = max(self.blocks[block_start], block_end) - else: - self.blocks[block_start] = block_end - - time.sleep(0.1) - self.blocks = _merge_one(self.blocks) - - def _do_db_operation(self): - keys = sorted(self.blocks.keys()) - self.xfer_request.next_ndx = self.blocks[keys[0]] - self.db.save_db_obj(self.xfer_request) - - def sync(self, protocol_doc): - """ - :param protocol_doc: A update to the protocol specific information - sent in by the protocol module. This will be - merged with the last dict sent in. - """ - # take the first from the list and only sync that far. - self.protocol_doc.update(protocol_doc) - self._check_db_ready() diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 336f601..0000000 --- a/test-requirements.txt +++ /dev/null @@ -1,29 +0,0 @@ -# Packages needed for dev testing -distribute>=0.6.24 - -# Install bounded pep8/pyflakes first, then let flake8 install -pep8==1.4.5 -pyflakes==0.7.2 -flake8==2.0 -hacking>=0.5.3,<0.6 - -# For translations processing -Babel - -# Needed for testing -coverage -fixtures>=0.3.12 -mox -nose -nose-exclude -openstack.nose_plugin>=0.7 -nosehtmloutput>=0.0.3 -sphinx>=1.1.2 -requests -testtools>=0.9.22 - -# Optional packages that should be installed when testing -MySQL-python -psycopg2 -pysendfile==2.0.0 -xattr>=0.6.0 diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py deleted file mode 100644 index 42a44e8..0000000 --- a/tools/install_venv_common.py +++ /dev/null @@ -1,222 +0,0 @@ -# vim: tabstop=4 shiftwidth=4 softtabstop=4 - -# Copyright 2013 OpenStack Foundation -# Copyright 2013 IBM Corp. -# -# 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. - -"""Provides methods needed by installation script for OpenStack development -virtual environments. - -Since this script is used to bootstrap a virtualenv from the system's Python -environment, it should be kept strictly compatible with Python 2.6. - -Synced in from openstack-common -""" - -from __future__ import print_function - -import optparse -import os -import subprocess -import sys - - -class InstallVenv(object): - - def __init__(self, root, venv, pip_requires, test_requires, py_version, - project): - self.root = root - self.venv = venv - self.pip_requires = pip_requires - self.test_requires = test_requires - self.py_version = py_version - self.project = project - - def die(self, message, *args): - print(message % args, file=sys.stderr) - sys.exit(1) - - def check_python_version(self): - if sys.version_info < (2, 6): - self.die("Need Python Version >= 2.6") - - def run_command_with_code(self, cmd, redirect_output=True, - check_exit_code=True): - """Runs a command in an out-of-process shell. - - Returns the output of that command. Working directory is self.root. - """ - if redirect_output: - stdout = subprocess.PIPE - else: - stdout = None - - proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) - output = proc.communicate()[0] - if check_exit_code and proc.returncode != 0: - self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) - return (output, proc.returncode) - - def run_command(self, cmd, redirect_output=True, check_exit_code=True): - return self.run_command_with_code(cmd, redirect_output, - check_exit_code)[0] - - def get_distro(self): - if (os.path.exists('/etc/fedora-release') or - os.path.exists('/etc/redhat-release')): - return Fedora(self.root, self.venv, self.pip_requires, - self.test_requires, self.py_version, self.project) - else: - return Distro(self.root, self.venv, self.pip_requires, - self.test_requires, self.py_version, self.project) - - def check_dependencies(self): - self.get_distro().install_virtualenv() - - def create_virtualenv(self, no_site_packages=True): - """Creates the virtual environment and installs PIP. - - Creates the virtual environment and installs PIP only into the - virtual environment. - """ - if not os.path.isdir(self.venv): - print('Creating venv...', end=' ') - if no_site_packages: - self.run_command(['virtualenv', '-q', '--no-site-packages', - self.venv]) - else: - self.run_command(['virtualenv', '-q', self.venv]) - print('done.') - print('Installing pip in venv...', end=' ') - if not self.run_command(['tools/with_venv.sh', 'easy_install', - 'pip>1.0']).strip(): - self.die("Failed to install pip.") - print('done.') - else: - print("venv already exists...") - pass - - def pip_install(self, *args): - self.run_command(['tools/with_venv.sh', - 'pip', 'install', '--upgrade'] + list(args), - redirect_output=False) - - def install_dependencies(self): - print('Installing dependencies with pip (this can take a while)...') - - # First things first, make sure our venv has the latest pip and - # distribute. - # NOTE: we keep pip at version 1.1 since the most recent version causes - # the .venv creation to fail. See: - # https://bugs.launchpad.net/nova/+bug/1047120 - self.pip_install('pip==1.1') - self.pip_install('distribute') - - # Install greenlet by hand - just listing it in the requires file does - # not - # get it installed in the right order - self.pip_install('greenlet') - - self.pip_install('-r', self.pip_requires) - self.pip_install('-r', self.test_requires) - - def post_process(self): - self.get_distro().post_process() - - def parse_args(self, argv): - """Parses command-line arguments.""" - parser = optparse.OptionParser() - parser.add_option('-n', '--no-site-packages', - action='store_true', - help="Do not inherit packages from global Python " - "install") - return parser.parse_args(argv[1:])[0] - - -class Distro(InstallVenv): - - def check_cmd(self, cmd): - return bool(self.run_command(['which', cmd], - check_exit_code=False).strip()) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if self.check_cmd('easy_install'): - print('Installing virtualenv via easy_install...', end=' ') - if self.run_command(['easy_install', 'virtualenv']): - print('Succeeded') - return - else: - print('Failed') - - self.die('ERROR: virtualenv not found.\n\n%s development' - ' requires virtualenv, please install it using your' - ' favorite package management tool' % self.project) - - def post_process(self): - """Any distribution-specific post-processing gets done here. - - In particular, this is useful for applying patches to code inside - the venv. - """ - pass - - -class Fedora(Distro): - """This covers all Fedora-based distributions. - - Includes: Fedora, RHEL, CentOS, Scientific Linux - """ - - def check_pkg(self, pkg): - return self.run_command_with_code(['rpm', '-q', pkg], - check_exit_code=False)[1] == 0 - - def apply_patch(self, originalfile, patchfile): - self.run_command(['patch', '-N', originalfile, patchfile], - check_exit_code=False) - - def install_virtualenv(self): - if self.check_cmd('virtualenv'): - return - - if not self.check_pkg('python-virtualenv'): - self.die("Please install 'python-virtualenv'.") - - super(Fedora, self).install_virtualenv() - - def post_process(self): - """Workaround for a bug in eventlet. - - This currently affects RHEL6.1, but the fix can safely be - applied to all RHEL and Fedora distributions. - - This can be removed when the fix is applied upstream. - - Nova: https://bugs.launchpad.net/nova/+bug/884915 - Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 - RHEL: https://bugzilla.redhat.com/958868 - """ - - # Install "patch" program if it's not there - if not self.check_pkg('patch'): - self.die("Please install 'patch'.") - - # Apply the eventlet patch - self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, - 'site-packages', - 'eventlet/green/subprocess.py'), - 'contrib/redhat-eventlet.patch') diff --git a/tools/make_state_machine.sh b/tools/make_state_machine.sh deleted file mode 100755 index ac3a1d3..0000000 --- a/tools/make_state_machine.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -source /home/jbresnah/Dev/OpenStack/openstackVE/bin/activate -f=`mktemp` -python -c 'import staccato.xfer.events as e;e._print_state_machine()' > $f -#dot -T png -o $1 $f -dot -T pdf -o $1 $f -rm $f -echo "Made $1" diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e4ef12b..0000000 --- a/tox.ini +++ /dev/null @@ -1,32 +0,0 @@ -[tox] -envlist = py26,py27,pep8 - -[testenv] -setenv = VIRTUAL_ENV={envdir} - NOSE_WITH_OPENSTACK=1 - NOSE_OPENSTACK_COLOR=1 - NOSE_OPENSTACK_RED=0.05 - NOSE_OPENSTACK_YELLOW=0.025 - NOSE_OPENSTACK_SHOW_ELAPSED=1 - NOSE_OPENSTACK_STDOUT=1 -deps = -r{toxinidir}/requirements.txt - -r{toxinidir}/test-requirements.txt -commands = nosetests {posargs} - -[tox:jenkins] -downloadcache = ~/cache/pip - -[testenv:pep8] -commands = - flake8 - -[testenv:cover] -setenv = NOSE_WITH_COVERAGE=1 - -[testenv:venv] -commands = {posargs} - -[flake8] -ignore = E125,E126,E711,E712,F,H -builtins = _ -exclude = .venv,.git,.tox,dist,doc,etc,*glance/locale*,*openstack/common*,*lib/python*,*egg,build