From 7fd6112605d2ab5c75f5d9b2d91ba706b9d439bc Mon Sep 17 00:00:00 2001 From: Fabio Verboso Date: Tue, 15 Mar 2016 11:31:08 +0100 Subject: [PATCH] Applied cookiecutter template Change-Id: Id860d5c4c97ae02c361c06e407a2e11cb3379783 --- .gitreview | 4 + CONTRIBUTING.rst | 17 + HACKING.rst | 4 + LICENSE | 30 +- MANIFEST.in | 6 + README.rst | 19 + babel.cfg | 2 + doc/source/conf.py | 75 + doc/source/contributing.rst | 4 + doc/source/index.rst | 25 + doc/source/installation.rst | 12 + doc/source/readme.rst | 1 + doc/source/usage.rst | 7 + iotronic/__init__.py | 7 +- iotronic/api/__init__.py | 4 +- iotronic/api/app.py | 20 +- iotronic/api/config.py | 4 +- iotronic/api/controllers/base.py | 4 +- iotronic/api/controllers/v1/__init__.py | 53 +- iotronic/api/controllers/v1/__old/__init__.py | 207 -- iotronic/api/controllers/v1/__old/chassis.py | 270 -- .../api/controllers/v1/__old/collection.py | 48 - iotronic/api/controllers/v1/__old/driver.py | 210 -- iotronic/api/controllers/v1/__old/node.py | 1104 --------- iotronic/api/controllers/v1/__old/port.py | 396 --- iotronic/api/controllers/v1/__old/state.py | 34 - iotronic/api/controllers/v1/__old/types.py | 239 -- iotronic/api/controllers/v1/__old/utils.py | 107 - iotronic/api/controllers/v1/collection.py | 4 +- iotronic/api/controllers/v1/location.py | 33 +- iotronic/api/controllers/v1/node.py | 132 +- iotronic/api/controllers/v1/utils.py | 2 + iotronic/api/hooks.py | 8 +- iotronic/api/middleware/auth_token.py | 1 + iotronic/api/middleware/parsable_error.py | 3 +- iotronic/common/config.py | 2 +- iotronic/common/config_generator/generator.py | 3 +- iotronic/common/disk_partitioner.py | 26 +- iotronic/common/exception.py | 4 +- iotronic/common/fsm.py | 2 + .../glance_service/base_image_service.py | 1 - iotronic/common/image_service.py | 5 +- iotronic/common/images.py | 2 +- iotronic/common/paths.py | 8 +- iotronic/common/pxe_utils.py | 2 +- iotronic/common/rpc.py | 20 +- iotronic/common/service.py | 2 +- iotronic/common/states.py | 2 +- iotronic/common/utils.py | 2 +- iotronic/conductor/__old/manager.py | 2195 ----------------- iotronic/conductor/__old/task_manager.py | 362 --- iotronic/conductor/__old/utils.py | 160 -- iotronic/conductor/manager.py | 117 +- iotronic/conductor/rpcapi.py | 6 +- iotronic/conductor/task_manager.py | 15 +- iotronic/db/api.py | 23 +- .../21b331f883ef_add_provision_updated_at.py | 2 +- .../2581ebaf0cb2_initial_migration.py | 2 +- .../versions/3ae36a5f5131_add_logical_name.py | 2 +- .../4f399b21ae71_add_node_clean_step.py | 2 +- ...c57409b9_replace_nostate_with_available.py | 2 +- iotronic/db/sqlalchemy/api.py | 48 +- iotronic/db/sqlalchemy/models.py | 29 +- iotronic/objects/__init__.py | 26 +- iotronic/objects/__old/chassis.py | 186 -- iotronic/objects/__old/node.py | 272 -- iotronic/objects/__old/port.py | 217 -- iotronic/objects/base.py | 14 +- iotronic/objects/location.py | 20 +- iotronic/objects/node.py | 4 +- iotronic/objects/sessionwp.py | 26 +- iotronic/objects/utils.py | 1 + .../openstack/common/eventlet_backdoor.py | 3 +- iotronic/openstack/common/loopingcall.py | 2 + iotronic/openstack/common/periodic_task.py | 2 + iotronic/openstack/common/service.py | 3 + iotronic/openstack/common/threadgroup.py | 2 + iotronic/tests/__init__.py | 0 iotronic/tests/base.py | 23 + iotronic/tests/test_iotronic.py | 28 + iotronic/wamp/clientwamp.py | 108 +- iotronic/wamp/functions.py | 61 +- iotronic/wamp/rpcwampserver.py | 95 +- requirements.txt | 5 + setup.cfg | 46 + setup.py | 104 +- temp/clients_wamp_rw.py | 163 -- temp/test_autobahn_wamp_client.py | 147 -- temp/test_autobahn_ws_client.py | 107 - temp/test_autobahn_ws_server.py | 40 - temp/test_c.py | 46 - temp/test_pub.py | 97 - test-requirements.txt | 14 + tox.ini | 60 + utils/chat_wamp.py | 10 - utils/rpc_test.py | 20 - utils/wamp_rpc_server.py | 6 - 97 files changed, 924 insertions(+), 7176 deletions(-) create mode 100644 .gitreview create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel.cfg create mode 100755 doc/source/conf.py create mode 100644 doc/source/contributing.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/installation.rst create mode 100644 doc/source/readme.rst create mode 100644 doc/source/usage.rst delete mode 100644 iotronic/api/controllers/v1/__old/__init__.py delete mode 100644 iotronic/api/controllers/v1/__old/chassis.py delete mode 100644 iotronic/api/controllers/v1/__old/collection.py delete mode 100644 iotronic/api/controllers/v1/__old/driver.py delete mode 100644 iotronic/api/controllers/v1/__old/node.py delete mode 100644 iotronic/api/controllers/v1/__old/port.py delete mode 100644 iotronic/api/controllers/v1/__old/state.py delete mode 100644 iotronic/api/controllers/v1/__old/types.py delete mode 100644 iotronic/api/controllers/v1/__old/utils.py delete mode 100644 iotronic/conductor/__old/manager.py delete mode 100644 iotronic/conductor/__old/task_manager.py delete mode 100644 iotronic/conductor/__old/utils.py delete mode 100644 iotronic/objects/__old/chassis.py delete mode 100644 iotronic/objects/__old/node.py delete mode 100644 iotronic/objects/__old/port.py create mode 100644 iotronic/tests/__init__.py create mode 100644 iotronic/tests/base.py create mode 100644 iotronic/tests/test_iotronic.py create mode 100644 requirements.txt create mode 100644 setup.cfg delete mode 100644 temp/clients_wamp_rw.py delete mode 100644 temp/test_autobahn_wamp_client.py delete mode 100644 temp/test_autobahn_ws_client.py delete mode 100644 temp/test_autobahn_ws_server.py delete mode 100644 temp/test_c.py delete mode 100644 temp/test_pub.py create mode 100644 test-requirements.txt create mode 100644 tox.ini delete mode 100644 utils/chat_wamp.py delete mode 100644 utils/rpc_test.py delete mode 100644 utils/wamp_rpc_server.py diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..d07aba8 --- /dev/null +++ b/.gitreview @@ -0,0 +1,4 @@ +[gerrit] +host=review.openstack.org +port=29418 +project=openstack/iotronic.git diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..cebec9c --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/iotronic diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..f38f20a --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +iotronic Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE index e06d208..68c771a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,5 @@ -Apache License + + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -173,30 +174,3 @@ Apache License incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - 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/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c978a52 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..368d776 --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +=============================== +iotronic +=============================== + +IoTronic is an Internet of Things resource management service for OpenStack clouds. + +Please feel here a long description which must be at least 3 lines wrapped on +80 cols, so that distribution package maintainers can use it in their packages. +Note that this is a hard requirement. + +* Free software: Apache license +* Documentation: http://docs.openstack.org/developer/iotronic +* Source: http://git.openstack.org/cgit/openstack/iotronic +* Bugs: http://bugs.launchpad.net/iotronic + +Features +-------- + +* TODO diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..15cd6cb --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: **.py] + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100755 index 0000000..3966aca --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# 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 sys + +sys.path.insert(0, os.path.abspath('../..')) +# -- General configuration ---------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + #'sphinx.ext.intersphinx', + 'oslosphinx' +] + +# autodoc generation is a bit aggressive and a nuisance when doing heavy +# text edit cycles. +# execute "export SPHINX_DEBUG=1" in your terminal to disable + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'iotronic' +copyright = u'2013, OpenStack Foundation' + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +# html_theme_path = ["."] +# html_theme = '_theme' +# html_static_path = ['static'] + +# Output file base name for HTML help builder. +htmlhelp_basename = '%sdoc' % project + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', + '%s.tex' % project, + u'%s Documentation' % project, + u'OpenStack Foundation', 'manual'), +] + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst new file mode 100644 index 0000000..1728a61 --- /dev/null +++ b/doc/source/contributing.rst @@ -0,0 +1,4 @@ +============ +Contributing +============ +.. include:: ../../CONTRIBUTING.rst diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..64e45d0 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,25 @@ +.. iotronic documentation master file, created by + sphinx-quickstart on Tue Jul 9 22:26:36 2013. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to iotronic's documentation! +======================================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + readme + installation + usage + contributing + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..8b3cb64 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,12 @@ +============ +Installation +============ + +At the command line:: + + $ pip install iotronic + +Or, if you have virtualenvwrapper installed:: + + $ mkvirtualenv iotronic + $ pip install iotronic diff --git a/doc/source/readme.rst b/doc/source/readme.rst new file mode 100644 index 0000000..a6210d3 --- /dev/null +++ b/doc/source/readme.rst @@ -0,0 +1 @@ +.. include:: ../../README.rst diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..855bce0 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,7 @@ +======== +Usage +======== + +To use iotronic in a project:: + + import iotronic diff --git a/iotronic/__init__.py b/iotronic/__init__.py index c733b15..6f0cbd8 100644 --- a/iotronic/__init__.py +++ b/iotronic/__init__.py @@ -13,10 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import eventlet import os + os.environ['EVENTLET_NO_GREENDNS'] = 'yes' - -import eventlet - -eventlet.monkey_patch(os=False) \ No newline at end of file +eventlet.monkey_patch(os=False) diff --git a/iotronic/api/__init__.py b/iotronic/api/__init__.py index ad995d9..46a025c 100644 --- a/iotronic/api/__init__.py +++ b/iotronic/api/__init__.py @@ -27,7 +27,7 @@ API_SERVICE_OPTS = [ default=1000, help='The maximum number of items returned in a single ' 'response from a collection resource.'), - ] +] CONF = cfg.CONF @@ -35,4 +35,4 @@ CONF = cfg.CONF opt_group = cfg.OptGroup(name='api', title='Options for the iotronic-api service') CONF.register_group(opt_group) -CONF.register_opts(API_SERVICE_OPTS, opt_group) \ No newline at end of file +CONF.register_opts(API_SERVICE_OPTS, opt_group) diff --git a/iotronic/api/app.py b/iotronic/api/app.py index 929910c..44b024c 100644 --- a/iotronic/api/app.py +++ b/iotronic/api/app.py @@ -25,16 +25,19 @@ from iotronic.api import middleware api_opts = [ - cfg.StrOpt('auth_strategy', + cfg.StrOpt( + 'auth_strategy', default='keystone', help='Authentication strategy used by iotronic-api: one of "keystone" ' - 'or "noauth". "noauth" should not be used in a production ' - 'environment because all authentication will be disabled.'), - cfg.BoolOpt('pecan_debug', - default=False, - help=('Enable pecan debug mode. WARNING: this is insecure ' - 'and should not be used in a production environment.')), - ] + 'or "noauth". "noauth" should not be used in a production ' + 'environment because all authentication will be disabled.'), + cfg.BoolOpt( + 'pecan_debug', + default=False, + help=( + 'Enable pecan debug mode. WARNING: this is insecure ' + 'and should not be used in a production environment.')), +] CONF = cfg.CONF CONF.register_opts(api_opts) @@ -79,6 +82,7 @@ def setup_app(pecan_config=None, extra_hooks=None): class VersionSelectorApplication(object): + def __init__(self): pc = get_pecan_config() pc.app.enable_acl = (CONF.auth_strategy == 'keystone') diff --git a/iotronic/api/config.py b/iotronic/api/config.py index 525344d..cea055a 100644 --- a/iotronic/api/config.py +++ b/iotronic/api/config.py @@ -30,8 +30,8 @@ app = { 'acl_public_routes': [ '/', '/v1', - #'/v1/drivers/[a-z_]*/vendor_passthru/lookup', - #'/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat', + # '/v1/drivers/[a-z_]*/vendor_passthru/lookup', + # '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat', '/v1/nodes/[a-z0-9\-]', ], } diff --git a/iotronic/api/controllers/base.py b/iotronic/api/controllers/base.py index 7d6d79b..c8f2137 100644 --- a/iotronic/api/controllers/base.py +++ b/iotronic/api/controllers/base.py @@ -70,8 +70,8 @@ class Version(object): :param latest_version: version to use if latest is requested :raises: webob.HTTPNotAcceptable """ - (self.major, self.minor) = Version.parse_headers(headers, - default_version, latest_version) + (self.major, self.minor) = Version.parse_headers( + headers, default_version, latest_version) def __repr__(self): return '%s.%s' % (self.major, self.minor) diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py index 5e69d74..2d18e45 100644 --- a/iotronic/api/controllers/v1/__init__.py +++ b/iotronic/api/controllers/v1/__init__.py @@ -16,24 +16,23 @@ Version 1 of the Iotronic API """ +from iotronic.api.controllers import base +from iotronic.api.controllers import link +from iotronic.api.controllers.v1 import node +from iotronic.api import expose +from iotronic.common.i18n import _ import pecan from pecan import rest from webob import exc from wsme import types as wtypes -from iotronic.api.controllers import link -from iotronic.api.controllers.v1 import node ''' -#from iotronic.api.controllers.v1 import chassis -#from iotronic.api.controllers.v1 import driver +# from iotronic.api.controllers.v1 import chassis +# from iotronic.api.controllers.v1 import driver -#from iotronic.api.controllers.v1 import port +# from iotronic.api.controllers.v1 import port ''' -from iotronic.api.controllers import base -from iotronic.api import expose -from iotronic.common.i18n import _ - BASE_VERSION = 1 MIN_VER_STR = '1.0' @@ -56,7 +55,7 @@ class MediaType(base.APIBase): def __init__(self, base, type): self.base = base self.type = type -''' +''' class V1(base.APIBase): @@ -65,29 +64,29 @@ class V1(base.APIBase): id = wtypes.text """The ID of the version, also acts as the release number""" - #media_types = [MediaType] + # media_types = [MediaType] """An array of supported media types for this version""" - #links = [link.Link] + # links = [link.Link] """Links that point to a specific URL for this version and documentation""" - #chassis = [link.Link] + # chassis = [link.Link] """Links to the chassis resource""" nodes = [link.Link] """Links to the nodes resource""" - - #ports = [link.Link] + + # ports = [link.Link] """Links to the ports resource""" - #drivers = [link.Link] + # drivers = [link.Link] """Links to the drivers resource""" @staticmethod def convert(): v1 = V1() v1.id = "v1" - + v1.nodes = [link.Link.make_link('self', pecan.request.host_url, 'nodes', ''), link.Link.make_link('bookmark', @@ -95,7 +94,7 @@ class V1(base.APIBase): 'nodes', '', bookmark=True) ] - + ''' v1.links = [link.Link.make_link('self', pecan.request.host_url, 'v1', '', bookmark=True), @@ -105,10 +104,10 @@ class V1(base.APIBase): 'api-spec-v1.html', bookmark=True, type='text/html') ] - + v1.media_types = [MediaType('application/json', 'application/vnd.openstack.iotronic.v1+json')] - + v1.chassis = [link.Link.make_link('self', pecan.request.host_url, 'chassis', ''), link.Link.make_link('bookmark', @@ -138,11 +137,11 @@ class V1(base.APIBase): class Controller(rest.RestController): """Version 1 API controller root.""" - + nodes = node.NodesController() - #ports = port.PortsController() - #chassis = chassis.ChassisController() - #drivers = driver.DriversController() + # ports = port.PortsController() + # chassis = chassis.ChassisController() + # drivers = driver.DriversController() @expose.expose(V1) def get(self): @@ -159,8 +158,10 @@ class Controller(rest.RestController): raise exc.HTTPNotAcceptable(_( "Mutually exclusive versions requested. Version %(ver)s " "requested but not supported by this service. The supported " - "version range is: [%(min)s, %(max)s].") % {'ver': version, - 'min': MIN_VER_STR, 'max': MAX_VER_STR}, headers=headers) + "version range is: [%(min)s,%(max)s]." + ) % {'ver': version, 'min': MIN_VER_STR, + 'max': MAX_VER_STR}, + headers=headers) # ensure the minor version is within the supported range if version < MIN_VER or version > MAX_VER: raise exc.HTTPNotAcceptable(_( diff --git a/iotronic/api/controllers/v1/__old/__init__.py b/iotronic/api/controllers/v1/__old/__init__.py deleted file mode 100644 index 69a9940..0000000 --- a/iotronic/api/controllers/v1/__old/__init__.py +++ /dev/null @@ -1,207 +0,0 @@ -# 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. - -""" -Version 1 of the Iotronic API -""" - -import pecan -from pecan import rest -from webob import exc -from wsme import types as wtypes - - -from iotronic.api.controllers import base -from iotronic.api.controllers import link -#from iotronic.api.controllers.v1 import chassis -#from iotronic.api.controllers.v1 import driver -from iotronic.api.controllers.v1 import node -from iotronic.api.controllers.v1 import board -#from iotronic.api.controllers.v1 import port -from iotronic.api import expose -from iotronic.common.i18n import _ - - - -BASE_VERSION = 1 - -# NOTE(deva): v1.0 is reserved to indicate Juno's API, but is not presently -# supported by the API service. All changes between Juno and the -# point where we added microversioning are considered backwards- -# compatible, but are not specifically discoverable at this time. -# -# The v1.1 version indicates this "initial" version as being -# different from Juno (v1.0), and includes the following changes: -# -# 827db7fe: Add Node.maintenance_reason -# 68eed82b: Add API endpoint to set/unset the node maintenance mode -# bc973889: Add sync and async support for passthru methods -# e03f443b: Vendor endpoints to support different HTTP methods -# e69e5309: Make vendor methods discoverable via the Iotronic API -# edf532db: Add logic to store the config drive passed by Nova - -# v1.1: API at the point in time when microversioning support was added -MIN_VER_STR = '1.0' - -# v1.2: Renamed NOSTATE ("None") to AVAILABLE ("available") -# v1.3: Add node.driver_internal_info -# v1.4: Add MANAGEABLE state -# v1.5: Add logical node names -# v1.6: Add INSPECT* states -MAX_VER_STR = '1.0' - - -MIN_VER = base.Version({base.Version.string: MIN_VER_STR}, - MIN_VER_STR, MAX_VER_STR) -MAX_VER = base.Version({base.Version.string: MAX_VER_STR}, - MIN_VER_STR, MAX_VER_STR) - - -class MediaType(base.APIBase): - """A media type representation.""" - - base = wtypes.text - type = wtypes.text - - def __init__(self, base, type): - self.base = base - self.type = type - - -class V1(base.APIBase): - """The representation of the version 1 of the API.""" - - id = wtypes.text - """The ID of the version, also acts as the release number""" - - media_types = [MediaType] - """An array of supported media types for this version""" - - links = [link.Link] - """Links that point to a specific URL for this version and documentation""" - - #chassis = [link.Link] - """Links to the chassis resource""" - - nodes = [link.Link] - """Links to the nodes resource""" - - #ports = [link.Link] - """Links to the ports resource""" - - #drivers = [link.Link] - """Links to the drivers resource""" - - @staticmethod - def convert(): - v1 = V1() - v1.id = "v1" - - v1.links = [link.Link.make_link('self', pecan.request.host_url, - 'v1', '', bookmark=True), - link.Link.make_link('describedby', - 'http://docs.openstack.org', - 'developer/iotronic/dev', - 'api-spec-v1.html', - bookmark=True, type='text/html') - ] - - v1.media_types = [MediaType('application/json', - 'application/vnd.openstack.iotronic.v1+json')] - ''' - v1.chassis = [link.Link.make_link('self', pecan.request.host_url, - 'chassis', ''), - link.Link.make_link('bookmark', - pecan.request.host_url, - 'chassis', '', - bookmark=True) - ] - ''' - v1.nodes = [link.Link.make_link('self', pecan.request.host_url, - 'nodes', ''), - link.Link.make_link('bookmark', - pecan.request.host_url, - 'nodes', '', - bookmark=True) - ] - ''' - v1.ports = [link.Link.make_link('self', pecan.request.host_url, - 'ports', ''), - link.Link.make_link('bookmark', - pecan.request.host_url, - 'ports', '', - bookmark=True) - ] - v1.drivers = [link.Link.make_link('self', pecan.request.host_url, - 'drivers', ''), - link.Link.make_link('bookmark', - pecan.request.host_url, - 'drivers', '', - bookmark=True) - ] - ''' - return v1 - - -class Controller(rest.RestController): - """Version 1 API controller root.""" - - nodes = node.NodesController() - #ports = port.PortsController() - #chassis = chassis.ChassisController() - #drivers = driver.DriversController() - boards= board.BoardsController() - - @expose.expose(V1) - def get(self): - # NOTE: The reason why convert() it's being called for every - # request is because we need to get the host url from - # the request object to make the links. - return V1.convert() - - def _check_version(self, version, headers=None): - if headers is None: - headers = {} - # ensure that major version in the URL matches the header - if version.major != BASE_VERSION: - raise exc.HTTPNotAcceptable(_( - "Mutually exclusive versions requested. Version %(ver)s " - "requested but not supported by this service. The supported " - "version range is: [%(min)s, %(max)s].") % {'ver': version, - 'min': MIN_VER_STR, 'max': MAX_VER_STR}, headers=headers) - # ensure the minor version is within the supported range - if version < MIN_VER or version > MAX_VER: - raise exc.HTTPNotAcceptable(_( - "Version %(ver)s was requested but the minor version is not " - "supported by this service. The supported version range is: " - "[%(min)s, %(max)s].") % {'ver': version, 'min': MIN_VER_STR, - 'max': MAX_VER_STR}, headers=headers) - - @pecan.expose() - def _route(self, args): - v = base.Version(pecan.request.headers, MIN_VER_STR, MAX_VER_STR) - - # Always set the min and max headers - pecan.response.headers[base.Version.min_string] = MIN_VER_STR - pecan.response.headers[base.Version.max_string] = MAX_VER_STR - - # assert that requested version is supported - self._check_version(v, pecan.response.headers) - pecan.response.headers[base.Version.string] = str(v) - pecan.request.version = v - - return super(Controller, self)._route(args) - - -__all__ = (Controller) diff --git a/iotronic/api/controllers/v1/__old/chassis.py b/iotronic/api/controllers/v1/__old/chassis.py deleted file mode 100644 index 4c34121..0000000 --- a/iotronic/api/controllers/v1/__old/chassis.py +++ /dev/null @@ -1,270 +0,0 @@ -# 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 datetime - -import pecan -from pecan import rest -import wsme -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link -from iotronic.api.controllers.v1 import collection -from iotronic.api.controllers.v1 import node -from iotronic.api.controllers.v1 import types -from iotronic.api.controllers.v1 import utils as api_utils -from iotronic.api import expose -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic import objects - - -class ChassisPatchType(types.JsonPatchType): - pass - - -class Chassis(base.APIBase): - """API representation of a chassis. - - This class enforces type checking and value constraints, and converts - between the internal object model and the API representation of - a chassis. - """ - - uuid = types.uuid - """The UUID of the chassis""" - - description = wtypes.text - """The description of the chassis""" - - extra = {wtypes.text: types.jsontype} - """The metadata of the chassis""" - - links = wsme.wsattr([link.Link], readonly=True) - """A list containing a self link and associated chassis links""" - - nodes = wsme.wsattr([link.Link], readonly=True) - """Links to the collection of nodes contained in this chassis""" - - def __init__(self, **kwargs): - self.fields = [] - for field in objects.Chassis.fields: - # Skip fields we do not expose. - if not hasattr(self, field): - continue - self.fields.append(field) - setattr(self, field, kwargs.get(field, wtypes.Unset)) - - @staticmethod - def _convert_with_links(chassis, url, expand=True): - if not expand: - chassis.unset_fields_except(['uuid', 'description']) - else: - chassis.nodes = [link.Link.make_link('self', - url, - 'chassis', - chassis.uuid + "/nodes"), - link.Link.make_link('bookmark', - url, - 'chassis', - chassis.uuid + "/nodes", - bookmark=True) - ] - chassis.links = [link.Link.make_link('self', - url, - 'chassis', chassis.uuid), - link.Link.make_link('bookmark', - url, - 'chassis', chassis.uuid, - bookmark=True) - ] - return chassis - - @classmethod - def convert_with_links(cls, rpc_chassis, expand=True): - chassis = Chassis(**rpc_chassis.as_dict()) - return cls._convert_with_links(chassis, pecan.request.host_url, - expand) - - @classmethod - def sample(cls, expand=True): - time = datetime.datetime(2000, 1, 1, 12, 0, 0) - sample = cls(uuid='eaaca217-e7d8-47b4-bb41-3f99f20eed89', extra={}, - description='Sample chassis', created_at=time, - updated_at=time) - return cls._convert_with_links(sample, 'http://localhost:6385', - expand) - - -class ChassisCollection(collection.Collection): - """API representation of a collection of chassis.""" - - chassis = [Chassis] - """A list containing chassis objects""" - - def __init__(self, **kwargs): - self._type = 'chassis' - - @staticmethod - def convert_with_links(chassis, limit, url=None, expand=False, **kwargs): - collection = ChassisCollection() - collection.chassis = [Chassis.convert_with_links(ch, expand) - for ch in chassis] - url = url or None - collection.next = collection.get_next(limit, url=url, **kwargs) - return collection - - @classmethod - def sample(cls, expand=True): - sample = cls() - sample.chassis = [Chassis.sample(expand=False)] - return sample - - -class ChassisController(rest.RestController): - """REST controller for Chassis.""" - - nodes = node.NodesController() - """Expose nodes as a sub-element of chassis""" - - # Set the flag to indicate that the requests to this resource are - # coming from a top-level resource - nodes.from_chassis = True - - _custom_actions = { - 'detail': ['GET'], - } - - invalid_sort_key_list = ['extra'] - - def _get_chassis_collection(self, marker, limit, sort_key, sort_dir, - expand=False, resource_url=None): - limit = api_utils.validate_limit(limit) - sort_dir = api_utils.validate_sort_dir(sort_dir) - marker_obj = None - if marker: - marker_obj = objects.Chassis.get_by_uuid(pecan.request.context, - marker) - - if sort_key in self.invalid_sort_key_list: - raise exception.InvalidParameterValue(_( - "The sort_key value %(key)s is an invalid field for sorting") - % {'key': sort_key}) - - chassis = objects.Chassis.list(pecan.request.context, limit, - marker_obj, sort_key=sort_key, - sort_dir=sort_dir) - return ChassisCollection.convert_with_links(chassis, limit, - url=resource_url, - expand=expand, - sort_key=sort_key, - sort_dir=sort_dir) - - @expose.expose(ChassisCollection, types.uuid, - int, wtypes.text, wtypes.text) - def get_all(self, marker=None, limit=None, sort_key='id', sort_dir='asc'): - """Retrieve a list of chassis. - - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - return self._get_chassis_collection(marker, limit, sort_key, sort_dir) - - @expose.expose(ChassisCollection, types.uuid, int, - wtypes.text, wtypes.text) - def detail(self, marker=None, limit=None, sort_key='id', sort_dir='asc'): - """Retrieve a list of chassis with detail. - - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - # /detail should only work against collections - parent = pecan.request.path.split('/')[:-1][-1] - if parent != "chassis": - raise exception.HTTPNotFound - - expand = True - resource_url = '/'.join(['chassis', 'detail']) - return self._get_chassis_collection(marker, limit, sort_key, sort_dir, - expand, resource_url) - - @expose.expose(Chassis, types.uuid) - def get_one(self, chassis_uuid): - """Retrieve information about the given chassis. - - :param chassis_uuid: UUID of a chassis. - """ - rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context, - chassis_uuid) - return Chassis.convert_with_links(rpc_chassis) - - @expose.expose(Chassis, body=Chassis, status_code=201) - def post(self, chassis): - """Create a new chassis. - - :param chassis: a chassis within the request body. - """ - new_chassis = objects.Chassis(pecan.request.context, - **chassis.as_dict()) - new_chassis.create() - # Set the HTTP Location Header - pecan.response.location = link.build_url('chassis', new_chassis.uuid) - return Chassis.convert_with_links(new_chassis) - - @wsme.validate(types.uuid, [ChassisPatchType]) - @expose.expose(Chassis, types.uuid, body=[ChassisPatchType]) - def patch(self, chassis_uuid, patch): - """Update an existing chassis. - - :param chassis_uuid: UUID of a chassis. - :param patch: a json PATCH document to apply to this chassis. - """ - rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context, - chassis_uuid) - try: - chassis = Chassis(**api_utils.apply_jsonpatch( - rpc_chassis.as_dict(), patch)) - except api_utils.JSONPATCH_EXCEPTIONS as e: - raise exception.PatchError(patch=patch, reason=e) - - # Update only the fields that have changed - for field in objects.Chassis.fields: - try: - patch_val = getattr(chassis, field) - except AttributeError: - # Ignore fields that aren't exposed in the API - continue - if patch_val == wtypes.Unset: - patch_val = None - if rpc_chassis[field] != patch_val: - rpc_chassis[field] = patch_val - - rpc_chassis.save() - return Chassis.convert_with_links(rpc_chassis) - - @expose.expose(None, types.uuid, status_code=204) - def delete(self, chassis_uuid): - """Delete a chassis. - - :param chassis_uuid: UUID of a chassis. - """ - rpc_chassis = objects.Chassis.get_by_uuid(pecan.request.context, - chassis_uuid) - rpc_chassis.destroy() diff --git a/iotronic/api/controllers/v1/__old/collection.py b/iotronic/api/controllers/v1/__old/collection.py deleted file mode 100644 index d49337f..0000000 --- a/iotronic/api/controllers/v1/__old/collection.py +++ /dev/null @@ -1,48 +0,0 @@ -# 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 pecan -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link - - -class Collection(base.APIBase): - - next = wtypes.text - """A link to retrieve the next subset of the collection""" - - @property - def collection(self): - return getattr(self, self._type) - - def has_next(self, limit): - """Return whether collection has more items.""" - return len(self.collection) and len(self.collection) == limit - - def get_next(self, limit, url=None, **kwargs): - """Return a link to the next subset of the collection.""" - if not self.has_next(limit): - return wtypes.Unset - - resource_url = url or self._type - q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) - next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { - 'args': q_args, 'limit': limit, - 'marker': self.collection[-1].uuid} - - return link.Link.make_link('next', pecan.request.host_url, - resource_url, next_args).href diff --git a/iotronic/api/controllers/v1/__old/driver.py b/iotronic/api/controllers/v1/__old/driver.py deleted file mode 100644 index 3400890..0000000 --- a/iotronic/api/controllers/v1/__old/driver.py +++ /dev/null @@ -1,210 +0,0 @@ -# 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 pecan -from pecan import rest -import wsme -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link -from iotronic.api import expose -from iotronic.common import exception -from iotronic.common.i18n import _ - - -# Property information for drivers: -# key = driver name; -# value = dictionary of properties of that driver: -# key = property name. -# value = description of the property. -# NOTE(rloo). This is cached for the lifetime of the API service. If one or -# more conductor services are restarted with new driver versions, the API -# service should be restarted. -_DRIVER_PROPERTIES = {} - -# Vendor information for drivers: -# key = driver name; -# value = dictionary of vendor methods of that driver: -# key = method name. -# value = dictionary with the metadata of that method. -# NOTE(lucasagomes). This is cached for the lifetime of the API -# service. If one or more conductor services are restarted with new driver -# versions, the API service should be restarted. -_VENDOR_METHODS = {} - - -class Driver(base.APIBase): - """API representation of a driver.""" - - name = wtypes.text - """The name of the driver""" - - hosts = [wtypes.text] - """A list of active conductors that support this driver""" - - links = wsme.wsattr([link.Link], readonly=True) - """A list containing self and bookmark links""" - - @staticmethod - def convert_with_links(name, hosts): - driver = Driver() - driver.name = name - driver.hosts = hosts - driver.links = [ - link.Link.make_link('self', - pecan.request.host_url, - 'drivers', name), - link.Link.make_link('bookmark', - pecan.request.host_url, - 'drivers', name, - bookmark=True) - ] - return driver - - @classmethod - def sample(cls): - sample = cls(name="sample-driver", - hosts=["fake-host"]) - return sample - - -class DriverList(base.APIBase): - """API representation of a list of drivers.""" - - drivers = [Driver] - """A list containing drivers objects""" - - @staticmethod - def convert_with_links(drivers): - collection = DriverList() - collection.drivers = [ - Driver.convert_with_links(dname, list(drivers[dname])) - for dname in drivers] - return collection - - @classmethod - def sample(cls): - sample = cls() - sample.drivers = [Driver.sample()] - return sample - - -class DriverPassthruController(rest.RestController): - """REST controller for driver passthru. - - This controller allow vendors to expose cross-node functionality in the - Iotronic API. Iotronic will merely relay the message from here to the specified - driver, no introspection will be made in the message body. - """ - - _custom_actions = { - 'methods': ['GET'] - } - - @expose.expose(wtypes.text, wtypes.text) - def methods(self, driver_name): - """Retrieve information about vendor methods of the given driver. - - :param driver_name: name of the driver. - :returns: dictionary with : - entries. - :raises: DriverNotFound if the driver name is invalid or the - driver cannot be loaded. - """ - if driver_name not in _VENDOR_METHODS: - topic = pecan.request.rpcapi.get_topic_for_driver(driver_name) - ret = pecan.request.rpcapi.get_driver_vendor_passthru_methods( - pecan.request.context, driver_name, topic=topic) - _VENDOR_METHODS[driver_name] = ret - - return _VENDOR_METHODS[driver_name] - - @expose.expose(wtypes.text, wtypes.text, wtypes.text, - body=wtypes.text) - def _default(self, driver_name, method, data=None): - """Call a driver API extension. - - :param driver_name: name of the driver to call. - :param method: name of the method, to be passed to the vendor - implementation. - :param data: body of data to supply to the specified method. - """ - if not method: - raise wsme.exc.ClientSideError(_("Method not specified")) - - if data is None: - data = {} - - http_method = pecan.request.method.upper() - topic = pecan.request.rpcapi.get_topic_for_driver(driver_name) - ret, is_async = pecan.request.rpcapi.driver_vendor_passthru( - pecan.request.context, driver_name, method, - http_method, data, topic=topic) - status_code = 202 if is_async else 200 - return wsme.api.Response(ret, status_code=status_code) - - -class DriversController(rest.RestController): - """REST controller for Drivers.""" - - vendor_passthru = DriverPassthruController() - - _custom_actions = { - 'properties': ['GET'], - } - - @expose.expose(DriverList) - def get_all(self): - """Retrieve a list of drivers.""" - # FIXME(deva): formatting of the auto-generated REST API docs - # will break from a single-line doc string. - # This is a result of a bug in sphinxcontrib-pecanwsme - # https://github.com/dreamhost/sphinxcontrib-pecanwsme/issues/8 - driver_list = pecan.request.dbapi.get_active_driver_dict() - return DriverList.convert_with_links(driver_list) - - @expose.expose(Driver, wtypes.text) - def get_one(self, driver_name): - """Retrieve a single driver.""" - # NOTE(russell_h): There is no way to make this more efficient than - # retrieving a list of drivers using the current sqlalchemy schema, but - # this path must be exposed for Pecan to route any paths we might - # choose to expose below it. - - driver_dict = pecan.request.dbapi.get_active_driver_dict() - for name, hosts in driver_dict.items(): - if name == driver_name: - return Driver.convert_with_links(name, list(hosts)) - - raise exception.DriverNotFound(driver_name=driver_name) - - @expose.expose(wtypes.text, wtypes.text) - def properties(self, driver_name): - """Retrieve property information of the given driver. - - :param driver_name: name of the driver. - :returns: dictionary with : - entries. - :raises: DriverNotFound (HTTP 404) if the driver name is invalid or - the driver cannot be loaded. - """ - if driver_name not in _DRIVER_PROPERTIES: - topic = pecan.request.rpcapi.get_topic_for_driver(driver_name) - properties = pecan.request.rpcapi.get_driver_properties( - pecan.request.context, driver_name, topic=topic) - _DRIVER_PROPERTIES[driver_name] = properties - - return _DRIVER_PROPERTIES[driver_name] diff --git a/iotronic/api/controllers/v1/__old/node.py b/iotronic/api/controllers/v1/__old/node.py deleted file mode 100644 index 137f0ac..0000000 --- a/iotronic/api/controllers/v1/__old/node.py +++ /dev/null @@ -1,1104 +0,0 @@ -# Copyright 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. - -import ast -import datetime - -from oslo_config import cfg -from oslo_log import log -from oslo_utils import strutils -from oslo_utils import uuidutils -import pecan -from pecan import rest -import wsme -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link -from iotronic.api.controllers.v1 import collection -from iotronic.api.controllers.v1 import port -from iotronic.api.controllers.v1 import types -from iotronic.api.controllers.v1 import utils as api_utils -from iotronic.api import expose -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic.common import states as ir_states -from iotronic import objects - - -CONF = cfg.CONF -CONF.import_opt('heartbeat_timeout', 'iotronic.conductor.manager', - group='conductor') - -LOG = log.getLogger(__name__) - -# Vendor information for node's driver: -# key = driver name; -# value = dictionary of node vendor methods of that driver: -# key = method name. -# value = dictionary with the metadata of that method. -# NOTE(lucasagomes). This is cached for the lifetime of the API -# service. If one or more conductor services are restarted with new driver -# versions, the API service should be restarted. -_VENDOR_METHODS = {} - - -def hide_fields_in_newer_versions(obj): - # if requested version is < 1.3, hide driver_internal_info - if pecan.request.version.minor < 3: - obj.driver_internal_info = wsme.Unset - - if not api_utils.allow_node_logical_names(): - obj.name = wsme.Unset - - # if requested version is < 1.6, hide inspection_*_at fields - if pecan.request.version.minor < 6: - obj.inspection_finished_at = wsme.Unset - obj.inspection_started_at = wsme.Unset - - -def assert_juno_provision_state_name(obj): - # if requested version is < 1.2, convert AVAILABLE to the old NOSTATE - #if (pecan.request.version.minor < 2 and - # obj.provision_state == ir_states.AVAILABLE): - # obj.provision_state = ir_states.NOSTATE - pass - - -def check_allow_management_verbs(verb): - # v1.4 added the MANAGEABLE state and two verbs to move nodes into - # and out of that state. Reject requests to do this in older versions - if (pecan.request.version.minor < 4 and - verb in [ir_states.VERBS['manage'], ir_states.VERBS['provide']]): - raise exception.NotAcceptable() - if (pecan.request.version.minor < 6 and - verb == ir_states.VERBS['inspect']): - raise exception.NotAcceptable() - - -class NodePatchType(types.JsonPatchType): - - @staticmethod - def internal_attrs(): - defaults = types.JsonPatchType.internal_attrs() - # TODO(lucasagomes): Include maintenance once the endpoint - # v1/nodes//maintenance do more things than updating the DB. - return defaults + ['/console_enabled', '/last_error', - '/power_state', '/provision_state', '/reservation', - '/target_power_state', '/target_provision_state', - '/provision_updated_at', '/maintenance_reason', - '/driver_internal_info', '/inspection_finished_at', - '/inspection_started_at', ] - - @staticmethod - def mandatory_attrs(): - return ['/chassis_uuid', '/driver'] - - -class BootDeviceController(rest.RestController): - - _custom_actions = { - 'supported': ['GET'], - } - - def _get_boot_device(self, node_ident, supported=False): - """Get the current boot device or a list of supported devices. - - :param node_ident: the UUID or logical name of a node. - :param supported: Boolean value. If true return a list of - supported boot devices, if false return the - current boot device. Default: False. - :returns: The current boot device or a list of the supported - boot devices. - - """ - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - if supported: - return pecan.request.rpcapi.get_supported_boot_devices( - pecan.request.context, rpc_node.uuid, topic) - else: - return pecan.request.rpcapi.get_boot_device(pecan.request.context, - rpc_node.uuid, topic) - - @expose.expose(None, types.uuid_or_name, wtypes.text, types.boolean, - status_code=204) - def put(self, node_ident, boot_device, persistent=False): - """Set the boot device for a node. - - Set the boot device to use on next reboot of the node. - - :param node_ident: the UUID or logical name of a node. - :param boot_device: the boot device, one of - :mod:`iotronic.common.boot_devices`. - :param persistent: Boolean value. True if the boot device will - persist to all future boots, False if not. - Default: False. - - """ - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - pecan.request.rpcapi.set_boot_device(pecan.request.context, - rpc_node.uuid, - boot_device, - persistent=persistent, - topic=topic) - - @expose.expose(wtypes.text, types.uuid_or_name) - def get(self, node_ident): - """Get the current boot device for a node. - - :param node_ident: the UUID or logical name of a node. - :returns: a json object containing: - - :boot_device: the boot device, one of - :mod:`iotronic.common.boot_devices` or None if it is unknown. - :persistent: Whether the boot device will persist to all - future boots or not, None if it is unknown. - - """ - return self._get_boot_device(node_ident) - - @expose.expose(wtypes.text, types.uuid_or_name) - def supported(self, node_ident): - """Get a list of the supported boot devices. - - :param node_ident: the UUID or logical name of a node. - :returns: A json object with the list of supported boot - devices. - - """ - boot_devices = self._get_boot_device(node_ident, supported=True) - return {'supported_boot_devices': boot_devices} - - -class NodeManagementController(rest.RestController): - - boot_device = BootDeviceController() - """Expose boot_device as a sub-element of management""" - - -class ConsoleInfo(base.APIBase): - """API representation of the console information for a node.""" - - console_enabled = types.boolean - """The console state: if the console is enabled or not.""" - - console_info = {wtypes.text: types.jsontype} - """The console information. It typically includes the url to access the - console and the type of the application that hosts the console.""" - - @classmethod - def sample(cls): - console = {'type': 'shellinabox', 'url': 'http://:4201'} - return cls(console_enabled=True, console_info=console) - - -class NodeConsoleController(rest.RestController): - - @expose.expose(ConsoleInfo, types.uuid_or_name) - def get(self, node_ident): - """Get connection information about the console. - - :param node_ident: UUID or logical name of a node. - """ - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - try: - console = pecan.request.rpcapi.get_console_information( - pecan.request.context, rpc_node.uuid, topic) - console_state = True - except exception.NodeConsoleNotEnabled: - console = None - console_state = False - - return ConsoleInfo(console_enabled=console_state, console_info=console) - - @expose.expose(None, types.uuid_or_name, types.boolean, - status_code=202) - def put(self, node_ident, enabled): - """Start and stop the node console. - - :param node_ident: UUID or logical name of a node. - :param enabled: Boolean value; whether to enable or disable the - console. - """ - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - pecan.request.rpcapi.set_console_mode(pecan.request.context, - rpc_node.uuid, enabled, topic) - # Set the HTTP Location Header - url_args = '/'.join([node_ident, 'states', 'console']) - pecan.response.location = link.build_url('nodes', url_args) - - -class NodeStates(base.APIBase): - """API representation of the states of a node.""" - - console_enabled = types.boolean - """Indicates whether the console access is enabled or disabled on - the node.""" - - power_state = wtypes.text - """Represent the current (not transition) power state of the node""" - - provision_state = wtypes.text - """Represent the current (not transition) provision state of the node""" - - provision_updated_at = datetime.datetime - """The UTC date and time of the last provision state change""" - - target_power_state = wtypes.text - """The user modified desired power state of the node.""" - - target_provision_state = wtypes.text - """The user modified desired provision state of the node.""" - - last_error = wtypes.text - """Any error from the most recent (last) asynchronous transaction that - started but failed to finish.""" - - @staticmethod - def convert(rpc_node): - attr_list = ['console_enabled', 'last_error', 'power_state', - 'provision_state', 'target_power_state', - 'target_provision_state', 'provision_updated_at'] - states = NodeStates() - for attr in attr_list: - setattr(states, attr, getattr(rpc_node, attr)) - assert_juno_provision_state_name(states) - return states - - @classmethod - def sample(cls): - sample = cls(target_power_state=ir_states.POWER_ON, - target_provision_state=ir_states.ACTIVE, - last_error=None, - console_enabled=False, - provision_updated_at=None, - power_state=ir_states.POWER_ON, - provision_state=None) - return sample - - -class NodeStatesController(rest.RestController): - - _custom_actions = { - 'power': ['PUT'], - 'provision': ['PUT'], - } - - console = NodeConsoleController() - """Expose console as a sub-element of states""" - - @expose.expose(NodeStates, types.uuid_or_name) - def get(self, node_ident): - """List the states of the node. - - :param node_ident: the UUID or logical_name of a node. - """ - # NOTE(lucasagomes): All these state values come from the - # DB. Iotronic counts with a periodic task that verify the current - # power states of the nodes and update the DB accordingly. - rpc_node = api_utils.get_rpc_node(node_ident) - return NodeStates.convert(rpc_node) - - @expose.expose(None, types.uuid_or_name, wtypes.text, - status_code=202) - def power(self, node_ident, target): - """Set the power state of the node. - - :param node_ident: the UUID or logical name of a node. - :param target: The desired power state of the node. - :raises: ClientSideError (HTTP 409) if a power operation is - already in progress. - :raises: InvalidStateRequested (HTTP 400) if the requested target - state is not valid or if the node is in CLEANING state. - - """ - # TODO(lucasagomes): Test if it's able to transition to the - # target state from the current one - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - - if target not in [ir_states.POWER_ON, - ir_states.POWER_OFF, - ir_states.REBOOT]: - raise exception.InvalidStateRequested( - action=target, node=node_ident, - state=rpc_node.power_state) - - # Don't change power state for nodes in cleaning - elif rpc_node.provision_state == ir_states.CLEANING: - raise exception.InvalidStateRequested( - action=target, node=node_ident, - state=rpc_node.provision_state) - - pecan.request.rpcapi.change_node_power_state(pecan.request.context, - rpc_node.uuid, target, - topic) - # Set the HTTP Location Header - url_args = '/'.join([node_ident, 'states']) - pecan.response.location = link.build_url('nodes', url_args) - - @expose.expose(None, types.uuid_or_name, wtypes.text, - wtypes.text, status_code=202) - def provision(self, node_ident, target, configdrive=None): - """Asynchronous trigger the provisioning of the node. - - This will set the target provision state of the node, and a - background task will begin which actually applies the state - change. This call will return a 202 (Accepted) indicating the - request was accepted and is in progress; the client should - continue to GET the status of this node to observe the status - of the requested action. - - :param node_ident: UUID or logical name of a node. - :param target: The desired provision state of the node. - :param configdrive: Optional. A gzipped and base64 encoded - configdrive. Only valid when setting provision state - to "active". - :raises: NodeLocked (HTTP 409) if the node is currently locked. - :raises: ClientSideError (HTTP 409) if the node is already being - provisioned. - :raises: InvalidStateRequested (HTTP 400) if the requested transition - is not possible from the current state. - :raises: NotAcceptable (HTTP 406) if the API version specified does - not allow the requested state transition. - """ - check_allow_management_verbs(target) - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - - # Normally, we let the task manager recognize and deal with - # NodeLocked exceptions. However, that isn't done until the RPC calls - # below. In order to main backward compatibility with our API HTTP - # response codes, we have this check here to deal with cases where - # a node is already being operated on (DEPLOYING or such) and we - # want to continue returning 409. Without it, we'd return 400. - if rpc_node.reservation: - raise exception.NodeLocked(node=rpc_node.uuid, - host=rpc_node.reservation) - - if (target in (ir_states.ACTIVE, ir_states.REBUILD) - and rpc_node.maintenance): - raise exception.NodeInMaintenance(op=_('provisioning'), - node=rpc_node.uuid) - - m = ir_states.machine.copy() - m.initialize(rpc_node.provision_state) - if not m.is_valid_event(ir_states.VERBS.get(target, target)): - raise exception.InvalidStateRequested( - action=target, node=rpc_node.uuid, - state=rpc_node.provision_state) - - if configdrive and target != ir_states.ACTIVE: - msg = (_('Adding a config drive is only supported when setting ' - 'provision state to %s') % ir_states.ACTIVE) - raise wsme.exc.ClientSideError(msg, status_code=400) - - # Note that there is a race condition. The node state(s) could change - # by the time the RPC call is made and the TaskManager manager gets a - # lock. - if target == ir_states.ACTIVE: - pecan.request.rpcapi.do_node_deploy(pecan.request.context, - rpc_node.uuid, False, - configdrive, topic) - elif target == ir_states.REBUILD: - pecan.request.rpcapi.do_node_deploy(pecan.request.context, - rpc_node.uuid, True, - None, topic) - elif target == ir_states.DELETED: - pecan.request.rpcapi.do_node_tear_down( - pecan.request.context, rpc_node.uuid, topic) - elif target == ir_states.VERBS['inspect']: - pecan.request.rpcapi.inspect_hardware( - pecan.request.context, rpc_node.uuid, topic=topic) - elif target in ( - ir_states.VERBS['manage'], ir_states.VERBS['provide']): - pecan.request.rpcapi.do_provisioning_action( - pecan.request.context, rpc_node.uuid, target, topic) - else: - msg = (_('The requested action "%(action)s" could not be ' - 'understood.') % {'action': target}) - raise exception.InvalidStateRequested(message=msg) - - # Set the HTTP Location Header - url_args = '/'.join([node_ident, 'states']) - pecan.response.location = link.build_url('nodes', url_args) - - -class Node(base.APIBase): - """API representation of a bare metal node. - - This class enforces type checking and value constraints, and converts - between the internal object model and the API representation of a node. - """ - ''' - _chassis_uuid = None - - def _get_chassis_uuid(self): - return self._chassis_uuid - - def _set_chassis_uuid(self, value): - if value and self._chassis_uuid != value: - try: - chassis = objects.Chassis.get(pecan.request.context, value) - self._chassis_uuid = chassis.uuid - # NOTE(lucasagomes): Create the chassis_id attribute on-the-fly - # to satisfy the api -> rpc object - # conversion. - self.chassis_id = chassis.id - except exception.ChassisNotFound as e: - # Change error code because 404 (NotFound) is inappropriate - # response for a POST request to create a Port - e.code = 400 # BadRequest - raise e - elif value == wtypes.Unset: - self._chassis_uuid = wtypes.Unset - ''' - uuid = types.uuid - """Unique UUID for this node""" - - #instance_uuid = types.uuid - #"""The UUID of the instance in nova-compute""" - - name = wsme.wsattr(wtypes.text) - status = wsme.wsattr(wtypes.text) - """The logical name for this node""" - ''' - power_state = wsme.wsattr(wtypes.text, readonly=True) - """Represent the current (not transition) power state of the node""" - - target_power_state = wsme.wsattr(wtypes.text, readonly=True) - """The user modified desired power state of the node.""" - - last_error = wsme.wsattr(wtypes.text, readonly=True) - """Any error from the most recent (last) asynchronous transaction that - started but failed to finish.""" - - provision_state = wsme.wsattr(wtypes.text, readonly=True) - """Represent the current (not transition) provision state of the node""" - ''' - reservation = wsme.wsattr(wtypes.text, readonly=True) - """The hostname of the conductor that holds an exclusive lock on - the node.""" - ''' - provision_updated_at = datetime.datetime - """The UTC date and time of the last provision state change""" - - inspection_finished_at = datetime.datetime - """The UTC date and time when the last hardware inspection finished - successfully.""" - - inspection_started_at = datetime.datetime - """The UTC date and time when the hardware inspection was started""" - - maintenance = types.boolean - """Indicates whether the node is in maintenance mode.""" - - maintenance_reason = wsme.wsattr(wtypes.text, readonly=True) - """Indicates reason for putting a node in maintenance mode.""" - - target_provision_state = wsme.wsattr(wtypes.text, readonly=True) - """The user modified desired provision state of the node.""" - - console_enabled = types.boolean - """Indicates whether the console access is enabled or disabled on - the node.""" - - instance_info = {wtypes.text: types.jsontype} - """This node's instance info.""" - - driver = wsme.wsattr(wtypes.text, mandatory=True) - """The driver responsible for controlling the node""" - - driver_info = {wtypes.text: types.jsontype} - """This node's driver configuration""" - - driver_internal_info = wsme.wsattr({wtypes.text: types.jsontype}, - readonly=True) - """This driver's internal configuration""" - - extra = {wtypes.text: types.jsontype} - """This node's meta data""" - - # NOTE: properties should use a class to enforce required properties - # current list: arch, cpus, disk, ram, image - properties = {wtypes.text: types.jsontype} - """The physical characteristics of this node""" - - chassis_uuid = wsme.wsproperty(types.uuid, _get_chassis_uuid, - _set_chassis_uuid) - """The UUID of the chassis this node belongs""" - ''' - links = wsme.wsattr([link.Link], readonly=True) - """A list containing a self link and associated node links""" - ''' - ports = wsme.wsattr([link.Link], readonly=True) - """Links to the collection of ports on this node""" - ''' - # NOTE(deva): "conductor_affinity" shouldn't be presented on the - # API because it's an internal value. Don't add it here. - - def __init__(self, **kwargs): - self.fields = [] - fields = list(objects.Node.fields) - # NOTE(lucasagomes): chassis_uuid is not part of objects.Node.fields - # because it's an API-only attribute. - #fields.append('chassis_uuid') - for k in fields: - # Skip fields we do not expose. - if not hasattr(self, k): - continue - self.fields.append(k) - setattr(self, k, kwargs.get(k, wtypes.Unset)) - - # NOTE(lucasagomes): chassis_id is an attribute created on-the-fly - # by _set_chassis_uuid(), it needs to be present in the fields so - # that as_dict() will contain chassis_id field when converting it - # before saving it in the database. - #self.fields.append('chassis_id') - #setattr(self, 'chassis_uuid', kwargs.get('chassis_id', wtypes.Unset)) - - @staticmethod - def _convert_with_links(node, url, expand=True, show_password=True): - if not expand: - except_list = ['instance_uuid', 'maintenance', 'power_state', - 'provision_state', 'uuid', 'name'] - node.unset_fields_except(except_list) - ''' - else: - if not show_password: - node.driver_info = ast.literal_eval(strutils.mask_password( - node.driver_info, - "******")) - node.ports = [link.Link.make_link('self', url, 'nodes', - node.uuid + "/ports"), - link.Link.make_link('bookmark', url, 'nodes', - node.uuid + "/ports", - bookmark=True) - ] - - # NOTE(lucasagomes): The numeric ID should not be exposed to - # the user, it's internal only. - node.chassis_id = wtypes.Unset - ''' - node.links = [link.Link.make_link('self', url, 'nodes', - node.uuid), - link.Link.make_link('bookmark', url, 'nodes', - node.uuid, bookmark=True) - ] - return node - - @classmethod - def convert_with_links(cls, rpc_node, expand=True): - node = Node(**rpc_node.as_dict()) - assert_juno_provision_state_name(node) - hide_fields_in_newer_versions(node) - return cls._convert_with_links(node, pecan.request.host_url, - expand, - pecan.request.context.show_password) - - @classmethod - def sample(cls, expand=True): - time = datetime.datetime(2000, 1, 1, 12, 0, 0) - node_uuid = '1be26c0b-03f2-4d2e-ae87-c02d7f33c123' - instance_uuid = 'dcf1fbc5-93fc-4596-9395-b80572f6267b' - name = 'database16-dc02' - sample = cls(uuid=node_uuid, instance_uuid=instance_uuid, - name=name, power_state=ir_states.POWER_ON, - target_power_state=ir_states.NOSTATE, - last_error=None, provision_state=ir_states.ACTIVE, - target_provision_state=ir_states.NOSTATE, - reservation=None, driver='fake', driver_info={}, - driver_internal_info={}, extra={}, - properties={'memory_mb': '1024', 'local_gb': '10', - 'cpus': '1'}, updated_at=time, created_at=time, - provision_updated_at=time, instance_info={}, - maintenance=False, maintenance_reason=None, - inspection_finished_at=None, inspection_started_at=time, - console_enabled=False, clean_step='') - # NOTE(matty_dubs): The chassis_uuid getter() is based on the - # _chassis_uuid variable: - sample._chassis_uuid = 'edcad704-b2da-41d5-96d9-afd580ecfa12' - return cls._convert_with_links(sample, 'http://localhost:6385', expand) - - -class NodeCollection(collection.Collection): - """API representation of a collection of nodes.""" - - nodes = [Node] - """A list containing nodes objects""" - - def __init__(self, **kwargs): - self._type = 'nodes' - - @staticmethod - def convert_with_links(nodes, limit, url=None, expand=False, **kwargs): - collection = NodeCollection() - collection.nodes = [Node.convert_with_links(n, expand) for n in nodes] - collection.next = collection.get_next(limit, url=url, **kwargs) - return collection - - @classmethod - def sample(cls): - sample = cls() - node = Node.sample(expand=False) - sample.nodes = [node] - return sample - - -class NodeVendorPassthruController(rest.RestController): - """REST controller for VendorPassthru. - - This controller allow vendors to expose a custom functionality in - the Iotronic API. Iotronic will merely relay the message from here to the - appropriate driver, no introspection will be made in the message body. - """ - - _custom_actions = { - 'methods': ['GET'] - } - - @expose.expose(wtypes.text, types.uuid_or_name) - def methods(self, node_ident): - """Retrieve information about vendor methods of the given node. - - :param node_ident: UUID or logical name of a node. - :returns: dictionary with : - entries. - :raises: NodeNotFound if the node is not found. - """ - # Raise an exception if node is not found - rpc_node = api_utils.get_rpc_node(node_ident) - - if rpc_node.driver not in _VENDOR_METHODS: - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - ret = pecan.request.rpcapi.get_node_vendor_passthru_methods( - pecan.request.context, rpc_node.uuid, topic=topic) - _VENDOR_METHODS[rpc_node.driver] = ret - - return _VENDOR_METHODS[rpc_node.driver] - - @expose.expose(wtypes.text, types.uuid_or_name, wtypes.text, - body=wtypes.text) - def _default(self, node_ident, method, data=None): - """Call a vendor extension. - - :param node_ident: UUID or logical name of a node. - :param method: name of the method in vendor driver. - :param data: body of data to supply to the specified method. - """ - # Raise an exception if node is not found - rpc_node = api_utils.get_rpc_node(node_ident) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - - # Raise an exception if method is not specified - if not method: - raise wsme.exc.ClientSideError(_("Method not specified")) - - if data is None: - data = {} - - http_method = pecan.request.method.upper() - ret, is_async = pecan.request.rpcapi.vendor_passthru( - pecan.request.context, rpc_node.uuid, method, - http_method, data, topic) - status_code = 202 if is_async else 200 - return wsme.api.Response(ret, status_code=status_code) - - -class NodeMaintenanceController(rest.RestController): - - def _set_maintenance(self, node_ident, maintenance_mode, reason=None): - rpc_node = api_utils.get_rpc_node(node_ident) - rpc_node.maintenance = maintenance_mode - rpc_node.maintenance_reason = reason - - try: - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - except exception.NoValidHost as e: - e.code = 400 - raise e - pecan.request.rpcapi.update_node(pecan.request.context, - rpc_node, topic=topic) - - @expose.expose(None, types.uuid_or_name, wtypes.text, - status_code=202) - def put(self, node_ident, reason=None): - """Put the node in maintenance mode. - - :param node_ident: the UUID or logical_name of a node. - :param reason: Optional, the reason why it's in maintenance. - - """ - self._set_maintenance(node_ident, True, reason=reason) - - @expose.expose(None, types.uuid_or_name, status_code=202) - def delete(self, node_ident): - """Remove the node from maintenance mode. - - :param node_ident: the UUID or logical name of a node. - - """ - self._set_maintenance(node_ident, False) - - -class NodesController(rest.RestController): - """REST controller for Nodes.""" - - states = NodeStatesController() - """Expose the state controller action as a sub-element of nodes""" - - vendor_passthru = NodeVendorPassthruController() - """A resource used for vendors to expose a custom functionality in - the API""" - - ports = port.PortsController() - """Expose ports as a sub-element of nodes""" - - management = NodeManagementController() - """Expose management as a sub-element of nodes""" - - maintenance = NodeMaintenanceController() - """Expose maintenance as a sub-element of nodes""" - - # Set the flag to indicate that the requests to this resource are - # coming from a top-level resource - ports.from_nodes = True - - from_chassis = False - """A flag to indicate if the requests to this controller are coming - from the top-level resource Chassis""" - - _custom_actions = { - 'detail': ['GET'], - 'validate': ['GET'], - } - - invalid_sort_key_list = ['properties', 'driver_info', 'extra', - 'instance_info', 'driver_internal_info'] - - def _get_nodes_collection(self, chassis_uuid, instance_uuid, associated, - maintenance, marker, limit, sort_key, sort_dir, - expand=False, resource_url=None): - if self.from_chassis and not chassis_uuid: - raise exception.MissingParameterValue(_( - "Chassis id not specified.")) - - limit = api_utils.validate_limit(limit) - sort_dir = api_utils.validate_sort_dir(sort_dir) - - marker_obj = None - if marker: - marker_obj = objects.Node.get_by_uuid(pecan.request.context, - marker) - - if sort_key in self.invalid_sort_key_list: - raise exception.InvalidParameterValue(_( - "The sort_key value %(key)s is an invalid field for sorting") - % {'key': sort_key}) - - if instance_uuid: - nodes = self._get_nodes_by_instance(instance_uuid) - else: - filters = {} - if chassis_uuid: - filters['chassis_uuid'] = chassis_uuid - if associated is not None: - filters['associated'] = associated - if maintenance is not None: - filters['maintenance'] = maintenance - - nodes = objects.Node.list(pecan.request.context, limit, marker_obj, - sort_key=sort_key, sort_dir=sort_dir, - filters=filters) - - parameters = {'sort_key': sort_key, 'sort_dir': sort_dir} - if associated: - parameters['associated'] = associated - if maintenance: - parameters['maintenance'] = maintenance - return NodeCollection.convert_with_links(nodes, limit, - url=resource_url, - expand=expand, - **parameters) - - def _get_nodes_by_instance(self, instance_uuid): - """Retrieve a node by its instance uuid. - - It returns a list with the node, or an empty list if no node is found. - """ - try: - node = objects.Node.get_by_instance_uuid(pecan.request.context, - instance_uuid) - return [node] - except exception.InstanceNotFound: - return [] - - @expose.expose(NodeCollection, types.uuid, types.uuid, - types.boolean, types.boolean, types.uuid, int, wtypes.text, - wtypes.text) - def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None, - maintenance=None, marker=None, limit=None, sort_key='id', - sort_dir='asc'): - """Retrieve a list of nodes. - - :param chassis_uuid: Optional UUID of a chassis, to get only nodes for - that chassis. - :param instance_uuid: Optional UUID of an instance, to find the node - associated with that instance. - :param associated: Optional boolean whether to return a list of - associated or unassociated nodes. May be combined - with other parameters. - :param maintenance: Optional boolean value that indicates whether - to get nodes in maintenance mode ("True"), or not - in maintenance mode ("False"). - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - return self._get_nodes_collection(chassis_uuid, instance_uuid, - associated, maintenance, marker, - limit, sort_key, sort_dir) - - @expose.expose(NodeCollection, types.uuid, types.uuid, - types.boolean, types.boolean, types.uuid, int, wtypes.text, - wtypes.text) - def detail(self, chassis_uuid=None, instance_uuid=None, associated=None, - maintenance=None, marker=None, limit=None, sort_key='id', - sort_dir='asc'): - """Retrieve a list of nodes with detail. - - :param chassis_uuid: Optional UUID of a chassis, to get only nodes for - that chassis. - :param instance_uuid: Optional UUID of an instance, to find the node - associated with that instance. - :param associated: Optional boolean whether to return a list of - associated or unassociated nodes. May be combined - with other parameters. - :param maintenance: Optional boolean value that indicates whether - to get nodes in maintenance mode ("True"), or not - in maintenance mode ("False"). - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - # /detail should only work against collections - parent = pecan.request.path.split('/')[:-1][-1] - if parent != "nodes": - raise exception.HTTPNotFound - - expand = True - resource_url = '/'.join(['nodes', 'detail']) - return self._get_nodes_collection(chassis_uuid, instance_uuid, - associated, maintenance, marker, - limit, sort_key, sort_dir, expand, - resource_url) - - @expose.expose(wtypes.text, types.uuid_or_name, types.uuid) - def validate(self, node=None, node_uuid=None): - """Validate the driver interfaces, using the node's UUID or name. - - Note that the 'node_uuid' interface is deprecated in favour - of the 'node' interface - - :param node: UUID or name of a node. - :param node_uuid: UUID of a node. - """ - if node: - # We're invoking this interface using positional notation, or - # explicitly using 'node'. Try and determine which one. - if (not api_utils.allow_node_logical_names() and - not uuidutils.is_uuid_like(node)): - raise exception.NotAcceptable() - - rpc_node = api_utils.get_rpc_node(node_uuid or node) - - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - return pecan.request.rpcapi.validate_driver_interfaces( - pecan.request.context, rpc_node.uuid, topic) - - @expose.expose(Node, types.uuid_or_name) - def get_one(self, node_ident): - """Retrieve information about the given node. - - :param node_ident: UUID or logical name of a node. - """ - if self.from_chassis: - raise exception.OperationNotPermitted - - rpc_node = api_utils.get_rpc_node(node_ident) - return Node.convert_with_links(rpc_node) - - @expose.expose(Node, body=Node, status_code=201) - def post(self, node): - """Create a new node. - - :param node: a node within the request body. - """ - if self.from_chassis: - raise exception.OperationNotPermitted - - # NOTE(deva): get_topic_for checks if node.driver is in the hash ring - # and raises NoValidHost if it is not. - # We need to ensure that node has a UUID before it can - # be mapped onto the hash ring. - if not node.uuid: - node.uuid = uuidutils.generate_uuid() - - try: - pecan.request.rpcapi.get_topic_for(node) - except exception.NoValidHost as e: - # NOTE(deva): convert from 404 to 400 because client can see - # list of available drivers and shouldn't request - # one that doesn't exist. - e.code = 400 - raise e - - # Verify that if we're creating a new node with a 'name' set - # that it is a valid name - if node.name: - if not api_utils.allow_node_logical_names(): - raise exception.NotAcceptable() - if not api_utils.is_valid_node_name(node.name): - msg = _("Cannot create node with invalid name %(name)s") - raise wsme.exc.ClientSideError(msg % {'name': node.name}, - status_code=400) - - new_node = objects.Node(pecan.request.context, - **node.as_dict()) - new_node.create() - # Set the HTTP Location Header - pecan.response.location = link.build_url('nodes', new_node.uuid) - return Node.convert_with_links(new_node) - - @wsme.validate(types.uuid, [NodePatchType]) - @expose.expose(Node, types.uuid_or_name, body=[NodePatchType]) - def patch(self, node_ident, patch): - """Update an existing node. - - :param node_ident: UUID or logical name of a node. - :param patch: a json PATCH document to apply to this node. - """ - if self.from_chassis: - raise exception.OperationNotPermitted - - rpc_node = api_utils.get_rpc_node(node_ident) - - # Check if node is transitioning state, although nodes in some states - # can be updated. - if (rpc_node.provision_state == ir_states.CLEANING and - patch == [{'op': 'remove', 'path': '/instance_uuid'}]): - # Allow node.instance_uuid removal during cleaning, but not other - # operations. - # TODO(JoshNang) remove node.instance_uuid when removing - # instance_info and stop removing node.instance_uuid in the Nova - # Iotronic driver. Bug: 1436568 - LOG.debug('Removing instance uuid %(instance)s from node %(node)s', - {'instance': rpc_node.instance_uuid, - 'node': rpc_node.uuid}) - elif ((rpc_node.target_power_state or rpc_node.target_provision_state) - and rpc_node.provision_state not in - ir_states.UPDATE_ALLOWED_STATES): - msg = _("Node %s can not be updated while a state transition " - "is in progress.") - raise wsme.exc.ClientSideError(msg % node_ident, status_code=409) - - # Verify that if we're patching 'name' that it is a valid - name = api_utils.get_patch_value(patch, '/name') - if name: - if not api_utils.allow_node_logical_names(): - raise exception.NotAcceptable() - if not api_utils.is_valid_node_name(name): - msg = _("Node %(node)s: Cannot change name to invalid " - "name '%(name)s'") - raise wsme.exc.ClientSideError(msg % {'node': node_ident, - 'name': name}, - status_code=400) - - try: - node_dict = rpc_node.as_dict() - # NOTE(lucasagomes): - # 1) Remove chassis_id because it's an internal value and - # not present in the API object - # 2) Add chassis_uuid - node_dict['chassis_uuid'] = node_dict.pop('chassis_id', None) - node = Node(**api_utils.apply_jsonpatch(node_dict, patch)) - except api_utils.JSONPATCH_EXCEPTIONS as e: - raise exception.PatchError(patch=patch, reason=e) - - # Update only the fields that have changed - for field in objects.Node.fields: - try: - patch_val = getattr(node, field) - except AttributeError: - # Ignore fields that aren't exposed in the API - continue - if patch_val == wtypes.Unset: - patch_val = None - if rpc_node[field] != patch_val: - rpc_node[field] = patch_val - - # NOTE(deva): we calculate the rpc topic here in case node.driver - # has changed, so that update is sent to the - # new conductor, not the old one which may fail to - # load the new driver. - try: - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - except exception.NoValidHost as e: - # NOTE(deva): convert from 404 to 400 because client can see - # list of available drivers and shouldn't request - # one that doesn't exist. - e.code = 400 - raise e - - # NOTE(lucasagomes): If it's changing the driver and the console - # is enabled we prevent updating it because the new driver will - # not be able to stop a console started by the previous one. - delta = rpc_node.obj_what_changed() - if 'driver' in delta and rpc_node.console_enabled: - raise wsme.exc.ClientSideError( - _("Node %s can not update the driver while the console is " - "enabled. Please stop the console first.") % node_ident, - status_code=409) - - new_node = pecan.request.rpcapi.update_node( - pecan.request.context, rpc_node, topic) - - return Node.convert_with_links(new_node) - - @expose.expose(None, types.uuid_or_name, status_code=204) - def delete(self, node_ident): - """Delete a node. - - :param node_ident: UUID or logical name of a node. - """ - if self.from_chassis: - raise exception.OperationNotPermitted - - rpc_node = api_utils.get_rpc_node(node_ident) - - try: - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - except exception.NoValidHost as e: - e.code = 400 - raise e - - pecan.request.rpcapi.destroy_node(pecan.request.context, - rpc_node.uuid, topic) diff --git a/iotronic/api/controllers/v1/__old/port.py b/iotronic/api/controllers/v1/__old/port.py deleted file mode 100644 index 7483370..0000000 --- a/iotronic/api/controllers/v1/__old/port.py +++ /dev/null @@ -1,396 +0,0 @@ -# Copyright 2013 UnitedStack 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 datetime - -from oslo_utils import uuidutils -import pecan -from pecan import rest -import wsme -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link -from iotronic.api.controllers.v1 import collection -from iotronic.api.controllers.v1 import types -from iotronic.api.controllers.v1 import utils as api_utils -from iotronic.api import expose -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic import objects - - -class PortPatchType(types.JsonPatchType): - - @staticmethod - def mandatory_attrs(): - return ['/address', '/node_uuid'] - - -class Port(base.APIBase): - """API representation of a port. - - This class enforces type checking and value constraints, and converts - between the internal object model and the API representation of a port. - """ - - _node_uuid = None - - def _get_node_uuid(self): - return self._node_uuid - - def _set_node_uuid(self, value): - if value and self._node_uuid != value: - try: - # FIXME(comstud): One should only allow UUID here, but - # there seems to be a bug in that tests are passing an - # ID. See bug #1301046 for more details. - node = objects.Node.get(pecan.request.context, value) - self._node_uuid = node.uuid - # NOTE(lucasagomes): Create the node_id attribute on-the-fly - # to satisfy the api -> rpc object - # conversion. - self.node_id = node.id - except exception.NodeNotFound as e: - # Change error code because 404 (NotFound) is inappropriate - # response for a POST request to create a Port - e.code = 400 # BadRequest - raise e - elif value == wtypes.Unset: - self._node_uuid = wtypes.Unset - - uuid = types.uuid - """Unique UUID for this port""" - - address = wsme.wsattr(types.macaddress, mandatory=True) - """MAC Address for this port""" - - extra = {wtypes.text: types.jsontype} - """This port's meta data""" - - node_uuid = wsme.wsproperty(types.uuid, _get_node_uuid, _set_node_uuid, - mandatory=True) - """The UUID of the node this port belongs to""" - - links = wsme.wsattr([link.Link], readonly=True) - """A list containing a self link and associated port links""" - - def __init__(self, **kwargs): - self.fields = [] - fields = list(objects.Port.fields) - # NOTE(lucasagomes): node_uuid is not part of objects.Port.fields - # because it's an API-only attribute - fields.append('node_uuid') - for field in fields: - # Skip fields we do not expose. - if not hasattr(self, field): - continue - self.fields.append(field) - setattr(self, field, kwargs.get(field, wtypes.Unset)) - - # NOTE(lucasagomes): node_id is an attribute created on-the-fly - # by _set_node_uuid(), it needs to be present in the fields so - # that as_dict() will contain node_id field when converting it - # before saving it in the database. - self.fields.append('node_id') - setattr(self, 'node_uuid', kwargs.get('node_id', wtypes.Unset)) - - @staticmethod - def _convert_with_links(port, url, expand=True): - if not expand: - port.unset_fields_except(['uuid', 'address']) - - # never expose the node_id attribute - port.node_id = wtypes.Unset - - port.links = [link.Link.make_link('self', url, - 'ports', port.uuid), - link.Link.make_link('bookmark', url, - 'ports', port.uuid, - bookmark=True) - ] - return port - - @classmethod - def convert_with_links(cls, rpc_port, expand=True): - port = Port(**rpc_port.as_dict()) - return cls._convert_with_links(port, pecan.request.host_url, expand) - - @classmethod - def sample(cls, expand=True): - sample = cls(uuid='27e3153e-d5bf-4b7e-b517-fb518e17f34c', - address='fe:54:00:77:07:d9', - extra={'foo': 'bar'}, - created_at=datetime.datetime.utcnow(), - updated_at=datetime.datetime.utcnow()) - # NOTE(lucasagomes): node_uuid getter() method look at the - # _node_uuid variable - sample._node_uuid = '7ae81bb3-dec3-4289-8d6c-da80bd8001ae' - return cls._convert_with_links(sample, 'http://localhost:6385', expand) - - -class PortCollection(collection.Collection): - """API representation of a collection of ports.""" - - ports = [Port] - """A list containing ports objects""" - - def __init__(self, **kwargs): - self._type = 'ports' - - @staticmethod - def convert_with_links(rpc_ports, limit, url=None, expand=False, **kwargs): - collection = PortCollection() - collection.ports = [Port.convert_with_links(p, expand) - for p in rpc_ports] - collection.next = collection.get_next(limit, url=url, **kwargs) - return collection - - @classmethod - def sample(cls): - sample = cls() - sample.ports = [Port.sample(expand=False)] - return sample - - -class PortsController(rest.RestController): - """REST controller for Ports.""" - - from_nodes = False - """A flag to indicate if the requests to this controller are coming - from the top-level resource Nodes.""" - - _custom_actions = { - 'detail': ['GET'], - } - - invalid_sort_key_list = ['extra'] - - def _get_ports_collection(self, node_ident, address, marker, limit, - sort_key, sort_dir, expand=False, - resource_url=None): - if self.from_nodes and not node_ident: - raise exception.MissingParameterValue(_( - "Node identifier not specified.")) - - limit = api_utils.validate_limit(limit) - sort_dir = api_utils.validate_sort_dir(sort_dir) - - marker_obj = None - if marker: - marker_obj = objects.Port.get_by_uuid(pecan.request.context, - marker) - - if sort_key in self.invalid_sort_key_list: - raise exception.InvalidParameterValue(_( - "The sort_key value %(key)s is an invalid field for sorting" - ) % {'key': sort_key}) - - if node_ident: - # FIXME(comstud): Since all we need is the node ID, we can - # make this more efficient by only querying - # for that column. This will get cleaned up - # as we move to the object interface. - node = api_utils.get_rpc_node(node_ident) - ports = objects.Port.list_by_node_id(pecan.request.context, - node.id, limit, marker_obj, - sort_key=sort_key, - sort_dir=sort_dir) - elif address: - ports = self._get_ports_by_address(address) - else: - ports = objects.Port.list(pecan.request.context, limit, - marker_obj, sort_key=sort_key, - sort_dir=sort_dir) - - return PortCollection.convert_with_links(ports, limit, - url=resource_url, - expand=expand, - sort_key=sort_key, - sort_dir=sort_dir) - - def _get_ports_by_address(self, address): - """Retrieve a port by its address. - - :param address: MAC address of a port, to get the port which has - this MAC address. - :returns: a list with the port, or an empty list if no port is found. - - """ - try: - port = objects.Port.get_by_address(pecan.request.context, address) - return [port] - except exception.PortNotFound: - return [] - - @expose.expose(PortCollection, types.uuid_or_name, types.uuid, - types.macaddress, types.uuid, int, wtypes.text, - wtypes.text) - def get_all(self, node=None, node_uuid=None, address=None, marker=None, - limit=None, sort_key='id', sort_dir='asc'): - """Retrieve a list of ports. - - Note that the 'node_uuid' interface is deprecated in favour - of the 'node' interface - - :param node: UUID or name of a node, to get only ports for that - node. - :param node_uuid: UUID of a node, to get only ports for that - node. - :param address: MAC address of a port, to get the port which has - this MAC address. - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - if not node_uuid and node: - # We're invoking this interface using positional notation, or - # explicitly using 'node'. Try and determine which one. - # Make sure only one interface, node or node_uuid is used - if (not api_utils.allow_node_logical_names() and - not uuidutils.is_uuid_like(node)): - raise exception.NotAcceptable() - - return self._get_ports_collection(node_uuid or node, address, marker, - limit, sort_key, sort_dir) - - @expose.expose(PortCollection, types.uuid_or_name, types.uuid, - types.macaddress, types.uuid, int, wtypes.text, - wtypes.text) - def detail(self, node=None, node_uuid=None, address=None, marker=None, - limit=None, sort_key='id', sort_dir='asc'): - """Retrieve a list of ports with detail. - - Note that the 'node_uuid' interface is deprecated in favour - of the 'node' interface - - :param node: UUID or name of a node, to get only ports for that - node. - :param node_uuid: UUID of a node, to get only ports for that - node. - :param address: MAC address of a port, to get the port which has - this MAC address. - :param marker: pagination marker for large data sets. - :param limit: maximum number of resources to return in a single result. - :param sort_key: column to sort results by. Default: id. - :param sort_dir: direction to sort. "asc" or "desc". Default: asc. - """ - if not node_uuid and node: - # We're invoking this interface using positional notation, or - # explicitly using 'node'. Try and determine which one. - # Make sure only one interface, node or node_uuid is used - if (not api_utils.allow_node_logical_names() and - not uuidutils.is_uuid_like(node)): - raise exception.NotAcceptable() - - # NOTE(lucasagomes): /detail should only work against collections - parent = pecan.request.path.split('/')[:-1][-1] - if parent != "ports": - raise exception.HTTPNotFound - - expand = True - resource_url = '/'.join(['ports', 'detail']) - return self._get_ports_collection(node_uuid or node, address, marker, - limit, sort_key, sort_dir, expand, - resource_url) - - @expose.expose(Port, types.uuid) - def get_one(self, port_uuid): - """Retrieve information about the given port. - - :param port_uuid: UUID of a port. - """ - if self.from_nodes: - raise exception.OperationNotPermitted - - rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid) - return Port.convert_with_links(rpc_port) - - @expose.expose(Port, body=Port, status_code=201) - def post(self, port): - """Create a new port. - - :param port: a port within the request body. - """ - if self.from_nodes: - raise exception.OperationNotPermitted - - new_port = objects.Port(pecan.request.context, - **port.as_dict()) - new_port.create() - # Set the HTTP Location Header - pecan.response.location = link.build_url('ports', new_port.uuid) - return Port.convert_with_links(new_port) - - @wsme.validate(types.uuid, [PortPatchType]) - @expose.expose(Port, types.uuid, body=[PortPatchType]) - def patch(self, port_uuid, patch): - """Update an existing port. - - :param port_uuid: UUID of a port. - :param patch: a json PATCH document to apply to this port. - """ - if self.from_nodes: - raise exception.OperationNotPermitted - - rpc_port = objects.Port.get_by_uuid(pecan.request.context, port_uuid) - try: - port_dict = rpc_port.as_dict() - # NOTE(lucasagomes): - # 1) Remove node_id because it's an internal value and - # not present in the API object - # 2) Add node_uuid - port_dict['node_uuid'] = port_dict.pop('node_id', None) - port = Port(**api_utils.apply_jsonpatch(port_dict, patch)) - except api_utils.JSONPATCH_EXCEPTIONS as e: - raise exception.PatchError(patch=patch, reason=e) - - # Update only the fields that have changed - for field in objects.Port.fields: - try: - patch_val = getattr(port, field) - except AttributeError: - # Ignore fields that aren't exposed in the API - continue - if patch_val == wtypes.Unset: - patch_val = None - if rpc_port[field] != patch_val: - rpc_port[field] = patch_val - - rpc_node = objects.Node.get_by_id(pecan.request.context, - rpc_port.node_id) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - - new_port = pecan.request.rpcapi.update_port( - pecan.request.context, rpc_port, topic) - - return Port.convert_with_links(new_port) - - @expose.expose(None, types.uuid, status_code=204) - def delete(self, port_uuid): - """Delete a port. - - :param port_uuid: UUID of a port. - """ - if self.from_nodes: - raise exception.OperationNotPermitted - rpc_port = objects.Port.get_by_uuid(pecan.request.context, - port_uuid) - rpc_node = objects.Node.get_by_id(pecan.request.context, - rpc_port.node_id) - topic = pecan.request.rpcapi.get_topic_for(rpc_node) - pecan.request.rpcapi.destroy_port(pecan.request.context, - rpc_port, topic) diff --git a/iotronic/api/controllers/v1/__old/state.py b/iotronic/api/controllers/v1/__old/state.py deleted file mode 100644 index c3843bf..0000000 --- a/iotronic/api/controllers/v1/__old/state.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -from wsme import types as wtypes - -from iotronic.api.controllers import base -from iotronic.api.controllers import link - - -class State(base.APIBase): - - current = wtypes.text - """The current state""" - - target = wtypes.text - """The user modified desired state""" - - available = [wtypes.text] - """A list of available states it is able to transition to""" - - links = [link.Link] - """A list containing a self link and associated state links""" diff --git a/iotronic/api/controllers/v1/__old/types.py b/iotronic/api/controllers/v1/__old/types.py deleted file mode 100644 index f435c61..0000000 --- a/iotronic/api/controllers/v1/__old/types.py +++ /dev/null @@ -1,239 +0,0 @@ -# coding: utf-8 -# -# 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 json - -from oslo_utils import strutils -from oslo_utils import uuidutils -import six -import wsme -from wsme import types as wtypes - -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic.common import utils - - -class MacAddressType(wtypes.UserType): - """A simple MAC address type.""" - - basetype = wtypes.text - name = 'macaddress' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - @staticmethod - def validate(value): - return utils.validate_and_normalize_mac(value) - - @staticmethod - def frombasetype(value): - if value is None: - return None - return MacAddressType.validate(value) - - -class UuidOrNameType(wtypes.UserType): - """A simple UUID or logical name type.""" - - basetype = wtypes.text - name = 'uuid_or_name' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - @staticmethod - def validate(value): - if not (uuidutils.is_uuid_like(value) - or utils.is_hostname_safe(value)): - raise exception.InvalidUuidOrName(name=value) - return value - - @staticmethod - def frombasetype(value): - if value is None: - return None - return UuidOrNameType.validate(value) - - -class NameType(wtypes.UserType): - """A simple logical name type.""" - - basetype = wtypes.text - name = 'name' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - @staticmethod - def validate(value): - if not utils.is_hostname_safe(value): - raise exception.InvalidName(name=value) - return value - - @staticmethod - def frombasetype(value): - if value is None: - return None - return NameType.validate(value) - - -class UuidType(wtypes.UserType): - """A simple UUID type.""" - - basetype = wtypes.text - name = 'uuid' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - @staticmethod - def validate(value): - if not uuidutils.is_uuid_like(value): - raise exception.InvalidUUID(uuid=value) - return value - - @staticmethod - def frombasetype(value): - if value is None: - return None - return UuidType.validate(value) - - -class BooleanType(wtypes.UserType): - """A simple boolean type.""" - - basetype = wtypes.text - name = 'boolean' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - @staticmethod - def validate(value): - try: - return strutils.bool_from_string(value, strict=True) - except ValueError as e: - # raise Invalid to return 400 (BadRequest) in the API - raise exception.Invalid(e) - - @staticmethod - def frombasetype(value): - if value is None: - return None - return BooleanType.validate(value) - - -class JsonType(wtypes.UserType): - """A simple JSON type.""" - - basetype = wtypes.text - name = 'json' - # FIXME(lucasagomes): When used with wsexpose decorator WSME will try - # to get the name of the type by accessing it's __name__ attribute. - # Remove this __name__ attribute once it's fixed in WSME. - # https://bugs.launchpad.net/wsme/+bug/1265590 - __name__ = name - - def __str__(self): - # These are the json serializable native types - return ' | '.join(map(str, (wtypes.text, six.integer_types, float, - BooleanType, list, dict, None))) - - @staticmethod - def validate(value): - try: - json.dumps(value) - except TypeError: - raise exception.Invalid(_('%s is not JSON serializable') % value) - else: - return value - - @staticmethod - def frombasetype(value): - return JsonType.validate(value) - - -macaddress = MacAddressType() -uuid_or_name = UuidOrNameType() -name = NameType() -uuid = UuidType() -boolean = BooleanType() -# Can't call it 'json' because that's the name of the stdlib module -jsontype = JsonType() - - -class JsonPatchType(wtypes.Base): - """A complex type that represents a single json-patch operation.""" - - path = wtypes.wsattr(wtypes.StringType(pattern='^(/[\w-]+)+$'), - mandatory=True) - op = wtypes.wsattr(wtypes.Enum(str, 'add', 'replace', 'remove'), - mandatory=True) - value = wsme.wsattr(jsontype, default=wtypes.Unset) - - @staticmethod - def internal_attrs(): - """Returns a list of internal attributes. - - Internal attributes can't be added, replaced or removed. This - method may be overwritten by derived class. - - """ - return ['/created_at', '/id', '/links', '/updated_at', '/uuid'] - - @staticmethod - def mandatory_attrs(): - """Retruns a list of mandatory attributes. - - Mandatory attributes can't be removed from the document. This - method should be overwritten by derived class. - - """ - return [] - - @staticmethod - def validate(patch): - _path = '/' + patch.path.split('/')[1] - if _path in patch.internal_attrs(): - msg = _("'%s' is an internal attribute and can not be updated") - raise wsme.exc.ClientSideError(msg % patch.path) - - if patch.path in patch.mandatory_attrs() and patch.op == 'remove': - msg = _("'%s' is a mandatory attribute and can not be removed") - raise wsme.exc.ClientSideError(msg % patch.path) - - if patch.op != 'remove': - if patch.value is wsme.Unset: - msg = _("'add' and 'replace' operations needs value") - raise wsme.exc.ClientSideError(msg) - - ret = {'path': patch.path, 'op': patch.op} - if patch.value is not wsme.Unset: - ret['value'] = patch.value - return ret diff --git a/iotronic/api/controllers/v1/__old/utils.py b/iotronic/api/controllers/v1/__old/utils.py deleted file mode 100644 index 9d3336e..0000000 --- a/iotronic/api/controllers/v1/__old/utils.py +++ /dev/null @@ -1,107 +0,0 @@ -# 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 jsonpatch -from oslo_config import cfg -from oslo_utils import uuidutils -import pecan -import wsme - -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic.common import utils -from iotronic import objects - - -CONF = cfg.CONF - - -JSONPATCH_EXCEPTIONS = (jsonpatch.JsonPatchException, - jsonpatch.JsonPointerException, - KeyError) - - -def validate_limit(limit): - if limit is None: - return CONF.api.max_limit - - if limit <= 0: - raise wsme.exc.ClientSideError(_("Limit must be positive")) - - return min(CONF.api.max_limit, limit) - - -def validate_sort_dir(sort_dir): - if sort_dir not in ['asc', 'desc']: - raise wsme.exc.ClientSideError(_("Invalid sort direction: %s. " - "Acceptable values are " - "'asc' or 'desc'") % sort_dir) - return sort_dir - - -def apply_jsonpatch(doc, patch): - for p in patch: - if p['op'] == 'add' and p['path'].count('/') == 1: - if p['path'].lstrip('/') not in doc: - msg = _('Adding a new attribute (%s) to the root of ' - ' the resource is not allowed') - raise wsme.exc.ClientSideError(msg % p['path']) - return jsonpatch.apply_patch(doc, jsonpatch.JsonPatch(patch)) - - -def get_patch_value(patch, path): - for p in patch: - if p['path'] == path: - return p['value'] - - -def allow_node_logical_names(): - # v1.5 added logical name aliases - return pecan.request.version.minor >= 5 - - -def get_rpc_node(node_ident): - """Get the RPC node from the node uuid or logical name. - - :param node_ident: the UUID or logical name of a node. - - :returns: The RPC Node. - :raises: InvalidUuidOrName if the name or uuid provided is not valid. - :raises: NodeNotFound if the node is not found. - """ - # Check to see if the node_ident is a valid UUID. If it is, treat it - # as a UUID. - if uuidutils.is_uuid_like(node_ident): - return objects.Node.get_by_uuid(pecan.request.context, node_ident) - - # We can refer to nodes by their name, if the client supports it - if allow_node_logical_names(): - if utils.is_hostname_safe(node_ident): - return objects.Node.get_by_name(pecan.request.context, node_ident) - raise exception.InvalidUuidOrName(name=node_ident) - - # Ensure we raise the same exception as we did for the Juno release - raise exception.NodeNotFound(node=node_ident) - - -def is_valid_node_name(name): - """Determine if the provided name is a valid node name. - - Check to see that the provided node name is valid, and isn't a UUID. - - :param: name: the node name to check. - :returns: True if the name is valid, False otherwise. - """ - return utils.is_hostname_safe(name) and (not uuidutils.is_uuid_like(name)) diff --git a/iotronic/api/controllers/v1/collection.py b/iotronic/api/controllers/v1/collection.py index d49337f..747af6e 100644 --- a/iotronic/api/controllers/v1/collection.py +++ b/iotronic/api/controllers/v1/collection.py @@ -41,8 +41,8 @@ class Collection(base.APIBase): resource_url = url or self._type q_args = ''.join(['%s=%s&' % (key, kwargs[key]) for key in kwargs]) next_args = '?%(args)slimit=%(limit)d&marker=%(marker)s' % { - 'args': q_args, 'limit': limit, - 'marker': self.collection[-1].uuid} + 'args': q_args, 'limit': limit, + 'marker': self.collection[-1].uuid} return link.Link.make_link('next', pecan.request.host_url, resource_url, next_args).href diff --git a/iotronic/api/controllers/v1/location.py b/iotronic/api/controllers/v1/location.py index 58c2809..c244eec 100644 --- a/iotronic/api/controllers/v1/location.py +++ b/iotronic/api/controllers/v1/location.py @@ -1,14 +1,27 @@ -import pecan -from wsme import types as wtypes -import wsme +# 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 iotronic.api.controllers import base from iotronic import objects +import wsme +from wsme import types as wtypes + class Location(base.APIBase): """API representation of a location. + """ - + longitude = wsme.wsattr(wtypes.text) latitude = wsme.wsattr(wtypes.text) altitude = wsme.wsattr(wtypes.text) @@ -26,12 +39,12 @@ class Location(base.APIBase): @staticmethod def convert_with_list(list): - list_locations=[] + list_locations = [] for l in list: list_locations.append(Location(**l.as_dict())) return list_locations -''' +''' class LocationCollection(collection.Collection): """API representation of a collection of locations.""" @@ -42,11 +55,11 @@ class LocationCollection(collection.Collection): self._type = 'locations' @staticmethod - def convert_with_locates(locations, limit, url=None, expand=False, **kwargs): + def convert_with_locates(locations, + limit, url=None, expand=False, **kwargs): collection = LocationCollection() - collection.locations = [Location.convert_with_locates(n, expand) for n in locations] + collection.locations = [Location.convert_with_locates(n, expand) + for n in locations] collection.next = collection.get_next(limit, url=url, **kwargs) return collection ''' - - diff --git a/iotronic/api/controllers/v1/node.py b/iotronic/api/controllers/v1/node.py index 1874323..b27b5c2 100644 --- a/iotronic/api/controllers/v1/node.py +++ b/iotronic/api/controllers/v1/node.py @@ -1,49 +1,64 @@ -from pecan import rest -from iotronic.api import expose -from wsme import types as wtypes -from iotronic import objects -from iotronic.api.controllers.v1 import types +# 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 iotronic.api.controllers import base from iotronic.api.controllers.v1 import collection from iotronic.api.controllers.v1 import location as loc +from iotronic.api.controllers.v1 import types from iotronic.api.controllers.v1 import utils as api_utils -from iotronic.api.controllers import base +from iotronic.api import expose from iotronic.common import exception -import wsme +from iotronic import objects import pecan -import code +from pecan import rest +import wsme +from wsme import types as wtypes class Node(base.APIBase): """API representation of a node. + """ uuid = types.uuid code = wsme.wsattr(wtypes.text) status = wsme.wsattr(wtypes.text) - name= wsme.wsattr(wtypes.text) - device= wsme.wsattr(wtypes.text) - session= wsme.wsattr(wtypes.text) - mobile=types.boolean - location=wsme.wsattr([loc.Location]) - extra=types.jsontype + name = wsme.wsattr(wtypes.text) + device = wsme.wsattr(wtypes.text) + session = wsme.wsattr(wtypes.text) + mobile = types.boolean + location = wsme.wsattr([loc.Location]) + extra = types.jsontype @staticmethod def _convert_with_locates(node, url, expand=True, show_password=True): - + try: - session=objects.SessionWP({}).get_session_by_node_uuid(node.uuid,valid=True) - node.session=session.session_id - except: + session = objects.SessionWP( + {}).get_session_by_node_uuid( + node.uuid, valid=True) + node.session = session.session_id + except Exception: pass - + if not expand: - except_list = ['name', 'code', 'status','uuid','session'] + except_list = ['name', 'code', 'status', 'uuid', 'session'] node.unset_fields_except(except_list) return node - - list_loc=objects.Location({}).list_by_node_id({},node.id) - node.location=loc.Location.convert_with_list(list_loc) - + + list_loc = objects.Location({}).list_by_node_id({}, node.id) + node.location = loc.Location.convert_with_list(list_loc) + ''' else: if not show_password: @@ -66,14 +81,14 @@ class Node(base.APIBase): ] ''' return node - + @classmethod def convert_with_locates(cls, rpc_node, expand=True): node = Node(**rpc_node.as_dict()) - node.id=rpc_node.id + node.id = rpc_node.id return cls._convert_with_locates(node, pecan.request.host_url, - expand, - pecan.request.context.show_password) + expand, + pecan.request.context.show_password) def __init__(self, **kwargs): self.fields = [] @@ -84,10 +99,11 @@ class Node(base.APIBase): continue self.fields.append(k) setattr(self, k, kwargs.get(k, wtypes.Unset)) - + + class NodeCollection(collection.Collection): """API representation of a collection of nodes.""" - + nodes = [Node] """A list containing nodes objects""" @@ -97,10 +113,13 @@ class NodeCollection(collection.Collection): @staticmethod def convert_with_locates(nodes, limit, url=None, expand=False, **kwargs): collection = NodeCollection() - collection.nodes = [Node.convert_with_locates(n, expand) for n in nodes] + collection.nodes = [ + Node.convert_with_locates( + n, expand) for n in nodes] collection.next = collection.get_next(limit, url=url, **kwargs) return collection - + + class NodesController(rest.RestController): invalid_sort_key_list = ['properties'] @@ -137,7 +156,7 @@ class NodesController(rest.RestController): nodes = objects.Node.list(pecan.request.context, limit, marker_obj, sort_key=sort_key, sort_dir=sort_dir, filters=filters) - + parameters = {'sort_key': sort_key, 'sort_dir': sort_dir} ''' if associated: @@ -146,10 +165,10 @@ class NodesController(rest.RestController): parameters['maintenance'] = maintenance ''' return NodeCollection.convert_with_locates(nodes, limit, - url=resource_url, - expand=expand, - **parameters) - + url=resource_url, + expand=expand, + **parameters) + @expose.expose(NodeCollection, types.uuid, types.uuid, types.boolean, types.boolean, types.uuid, int, wtypes.text, wtypes.text) def get_all(self, chassis_uuid=None, instance_uuid=None, associated=None, @@ -176,19 +195,17 @@ class NodesController(rest.RestController): associated, maintenance, marker, limit, sort_key, sort_dir) - - - @expose.expose(Node,types.uuid_or_name) - def get(self,node_ident): + @expose.expose(Node, types.uuid_or_name) + def get(self, node_ident): """Retrieve information about the given node. :param node_ident: UUID or logical name of a node. """ rpc_node = api_utils.get_rpc_node(node_ident) node = Node(**rpc_node.as_dict()) - node.id=rpc_node.id + node.id = rpc_node.id return Node.convert_with_locates(node) - + @expose.expose(None, types.uuid_or_name, status_code=204) def delete(self, node_ident): """Delete a node. @@ -205,11 +222,11 @@ class NodesController(rest.RestController): pecan.request.rpcapi.destroy_node(pecan.request.context, rpc_node.uuid, topic) - + @expose.expose(Node, body=Node, status_code=201) - def post(self,Node): + def post(self, Node): """Create a new Node. - + :param Node: a Node within the request body. """ if not Node.name: @@ -218,25 +235,24 @@ class NodesController(rest.RestController): if not Node.code: raise exception.MissingParameterValue( _("Code is not specified.")) - if not Node.location: + if not Node.location: raise exception.MissingParameterValue( _("Location is not specified.")) - + if Node.name: if not api_utils.is_valid_node_name(Node.name): msg = _("Cannot create node with invalid name %(name)s") raise wsme.exc.ClientSideError(msg % {'name': Node.name}, - status_code=400) - + status_code=400) + new_Node = objects.Node(pecan.request.context, **Node.as_dict()) new_Node.create() - - new_Location=objects.Location(pecan.request.context, - **Node.location[0].as_dict()) - new_Location.node_id=new_Node.id - new_Location.create() - - #pecan.response.location = link.build_url('Nodes', new_Node.uuid) - return Node.convert_with_locates(new_Node) + new_Location = objects.Location(pecan.request.context, + **Node.location[0].as_dict()) + new_Location.node_id = new_Node.id + new_Location.create() + + # pecan.response.location = link.build_url('Nodes', new_Node.uuid) + return Node.convert_with_locates(new_Node) diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py index 3514a53..9d3336e 100644 --- a/iotronic/api/controllers/v1/utils.py +++ b/iotronic/api/controllers/v1/utils.py @@ -71,6 +71,7 @@ def allow_node_logical_names(): # v1.5 added logical name aliases return pecan.request.version.minor >= 5 + def get_rpc_node(node_ident): """Get the RPC node from the node uuid or logical name. @@ -94,6 +95,7 @@ def get_rpc_node(node_ident): # Ensure we raise the same exception as we did for the Juno release raise exception.NodeNotFound(node=node_ident) + def is_valid_node_name(name): """Determine if the provided name is a valid node name. diff --git a/iotronic/api/hooks.py b/iotronic/api/hooks.py index ef3a147..5659721 100644 --- a/iotronic/api/hooks.py +++ b/iotronic/api/hooks.py @@ -23,7 +23,6 @@ from iotronic.common import policy from iotronic.conductor import rpcapi -from iotronic.db import api as dbapi class ConfigHook(hooks.PecanHook): @@ -37,8 +36,8 @@ class DBHook(hooks.PecanHook): """Attach the dbapi object to the request so controllers can get to it.""" def before(self, state): - - #state.request.dbapi = dbapi.get_instance() + + # state.request.dbapi = dbapi.get_instance() pass @@ -62,6 +61,7 @@ class ContextHook(hooks.PecanHook): or admin substring. Otherwise it is set to False. """ + def __init__(self, public_api_routes): self.public_api_routes = public_api_routes super(ContextHook, self).__init__() @@ -112,6 +112,7 @@ class TrustedCallHook(hooks.PecanHook): tenant, domain or other administrative unit. """ + def before(self, state): ctx = state.request.context if ctx.is_public_api: @@ -132,6 +133,7 @@ class NoExceptionTracebackHook(hooks.PecanHook): # 'on_error' never fired for wsme+pecan pair. wsme @wsexpose decorator # catches and handles all the errors, so 'on_error' dedicated for unhandled # exceptions never fired. + def after(self, state): # Omit empty body. Some errors may not have body at this level yet. if not state.response.body: diff --git a/iotronic/api/middleware/auth_token.py b/iotronic/api/middleware/auth_token.py index 5cb3934..9c21ab1 100644 --- a/iotronic/api/middleware/auth_token.py +++ b/iotronic/api/middleware/auth_token.py @@ -31,6 +31,7 @@ class AuthTokenMiddleware(auth_token.AuthProtocol): for public routes in the API. """ + def __init__(self, app, conf, public_api_routes=[]): # TODO(mrda): Remove .xml and ensure that doesn't result in a # 401 Authentication Required instead of 404 Not Found diff --git a/iotronic/api/middleware/parsable_error.py b/iotronic/api/middleware/parsable_error.py index ae898a9..e02a164 100644 --- a/iotronic/api/middleware/parsable_error.py +++ b/iotronic/api/middleware/parsable_error.py @@ -35,6 +35,7 @@ LOG = log.getLogger(__name__) class ParsableErrorMiddleware(object): """Replace error body with something the client can parse.""" + def __init__(self, app): self.app = app @@ -69,7 +70,7 @@ class ParsableErrorMiddleware(object): if (state['status_code'] // 100) not in (2, 3): req = webob.Request(environ) if (req.accept.best_match(['application/json', 'application/xml']) - == 'application/xml'): + == 'application/xml'): try: # simple check xml is valid body = [et.ElementTree.tostring( diff --git a/iotronic/common/config.py b/iotronic/common/config.py index 38d6e5c..d018df5 100644 --- a/iotronic/common/config.py +++ b/iotronic/common/config.py @@ -26,6 +26,6 @@ def parse_args(argv, default_config_files=None): cfg.CONF(argv[1:], project='iotronic', version=version.version_info.release_string(), - #version='2015.7', + # version='2015.7', default_config_files=default_config_files) rpc.init(cfg.CONF) diff --git a/iotronic/common/config_generator/generator.py b/iotronic/common/config_generator/generator.py index ccf073d..3f433fe 100644 --- a/iotronic/common/config_generator/generator.py +++ b/iotronic/common/config_generator/generator.py @@ -122,8 +122,7 @@ def generate(argv): opt_list.append((ext.name, opts)) for pkg_name in pkg_names: - mods = mods_by_pkg.get(pkg_name) - mods.sort() + mods = sorted(mods_by_pkg.get(pkg_name)) for mod_str in mods: if mod_str.endswith('.__init__'): mod_str = mod_str[:mod_str.rfind(".")] diff --git a/iotronic/common/disk_partitioner.py b/iotronic/common/disk_partitioner.py index d255a4b..67055fe 100644 --- a/iotronic/common/disk_partitioner.py +++ b/iotronic/common/disk_partitioner.py @@ -26,18 +26,20 @@ from iotronic.common import utils from iotronic.openstack.common import loopingcall opts = [ - cfg.IntOpt('check_device_interval', - default=1, - help='After Iotronic has completed creating the partition table, ' - 'it continues to check for activity on the attached iSCSI ' - 'device status at this interval prior to copying the image' - ' to the node, in seconds'), - cfg.IntOpt('check_device_max_retries', - default=20, - help='The maximum number of times to check that the device is ' - 'not accessed by another process. If the device is still ' - 'busy after that, the disk partitioning will be treated as' - ' having failed.'), + cfg.IntOpt( + 'check_device_interval', + default=1, + help='After Iotronic has completed creating the partition table, ' + 'it continues to check for activity on the attached iSCSI ' + 'device status at this interval prior to copying the image' + ' to the node, in seconds'), + cfg.IntOpt( + 'check_device_max_retries', + default=20, + help='The maximum number of times to check that the device is ' + 'not accessed by another process. If the device is still ' + 'busy after that, the disk partitioning will be treated as' + ' having failed.'), ] CONF = cfg.CONF diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py index 7aa78a1..edfe3d1 100644 --- a/iotronic/common/exception.py +++ b/iotronic/common/exception.py @@ -159,12 +159,15 @@ class InstanceAssociated(Conflict): message = _("Instance %(instance_uuid)s is already associated with a node," " it cannot be associated with this other node %(node)s") + class DuplicateName(Conflict): message = _("A node with name %(name)s already exists.") + class DuplicateCode(Conflict): message = _("A node with code %(code)s already exists.") + class InvalidUUID(Invalid): message = _("Expected a uuid but received %(uuid)s.") @@ -573,4 +576,3 @@ class PathNotFound(IotronicException): class DirectoryNotWritable(IotronicException): message = _("Directory %(dir)s is not writable.") - diff --git a/iotronic/common/fsm.py b/iotronic/common/fsm.py index 16b202c..7d2f8d9 100644 --- a/iotronic/common/fsm.py +++ b/iotronic/common/fsm.py @@ -32,6 +32,7 @@ from iotronic.common.i18n import _ class _Jump(object): """A FSM transition tracks this data while jumping.""" + def __init__(self, name, on_enter, on_exit): self.name = name self.on_enter = on_enter @@ -44,6 +45,7 @@ class FSM(object): This class models a state machine, and expects an outside caller to manually trigger the state changes one at a time by invoking process_event """ + def __init__(self, start_state=None): self._transitions = {} self._states = OrderedDict() diff --git a/iotronic/common/glance_service/base_image_service.py b/iotronic/common/glance_service/base_image_service.py index 44377c8..1ab5792 100644 --- a/iotronic/common/glance_service/base_image_service.py +++ b/iotronic/common/glance_service/base_image_service.py @@ -240,7 +240,6 @@ class BaseImageService(object): @check_image_service def _update(self, image_id, image_meta, data=None, method='update', purge_props=False): - """Modify the given image with the new data. :param image_id: The opaque image identifier. diff --git a/iotronic/common/image_service.py b/iotronic/common/image_service.py index 5b70812..48450a5 100644 --- a/iotronic/common/image_service.py +++ b/iotronic/common/image_service.py @@ -84,8 +84,9 @@ def import_versioned_module(version, submodule=None): def GlanceImageService(client=None, version=1, context=None): module = import_versioned_module(version, 'image_service') service_class = getattr(module, 'GlanceImageService') - if (context is not None and CONF.glance.auth_strategy == 'keystone' - and not context.auth_token): + if (context is not None + and CONF.glance.auth_strategy == 'keystone' + and not context.auth_token): context.auth_token = keystone.get_admin_auth_token() return service_class(client, version, context) diff --git a/iotronic/common/images.py b/iotronic/common/images.py index 4a67840..5dec885 100644 --- a/iotronic/common/images.py +++ b/iotronic/common/images.py @@ -512,7 +512,7 @@ def is_whole_disk_image(ctx, instance_info): else: # Non glance image ref if (not instance_info.get('kernel') and - not instance_info.get('ramdisk')): + not instance_info.get('ramdisk')): is_whole_disk_image = True return is_whole_disk_image diff --git a/iotronic/common/paths.py b/iotronic/common/paths.py index 20b1db7..53e5edf 100644 --- a/iotronic/common/paths.py +++ b/iotronic/common/paths.py @@ -23,12 +23,10 @@ path_opts = [ cfg.StrOpt('pybasedir', default=os.path.abspath(os.path.join(os.path.dirname(__file__), '../')), - help='Directory where the iotronic python module is installed.'), - cfg.StrOpt('bindir', - default='$pybasedir/bin', + help='Directory where the iotronic module is installed.'), + cfg.StrOpt('bindir', default='$pybasedir/bin', help='Directory where iotronic binaries are installed.'), - cfg.StrOpt('state_path', - default='$pybasedir', + cfg.StrOpt('state_path', default='$pybasedir', help="Top-level directory for maintaining iotronic's state."), ] diff --git a/iotronic/common/pxe_utils.py b/iotronic/common/pxe_utils.py index 9aa9798..3113723 100644 --- a/iotronic/common/pxe_utils.py +++ b/iotronic/common/pxe_utils.py @@ -237,7 +237,7 @@ def clean_up_pxe_config(task): # see: https://bugs.launchpad.net/iotronic/+bug/1441710 if CONF.pxe.ipxe_enabled: utils.unlink_without_raise(_get_pxe_mac_path(mac, - delimiter='')) + delimiter='')) utils.rmtree_without_raise(os.path.join(get_root_dir(), task.node.uuid)) diff --git a/iotronic/common/rpc.py b/iotronic/common/rpc.py index 20ec8e1..3be54cb 100644 --- a/iotronic/common/rpc.py +++ b/iotronic/common/rpc.py @@ -13,6 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_config import cfg +import oslo_messaging as messaging +from oslo_serialization import jsonutils + +from iotronic.common import context as iotronic_context +from iotronic.common import exception + __all__ = [ 'init', 'cleanup', @@ -27,16 +34,8 @@ __all__ = [ 'TRANSPORT_ALIASES', ] -from oslo_config import cfg -import oslo_messaging as messaging -from oslo_serialization import jsonutils - -from iotronic.common import context as iotronic_context -from iotronic.common import exception - - CONF = cfg.CONF -#print CONF.transport_url +# print CONF.transport_url TRANSPORT = None NOTIFIER = None @@ -92,6 +91,7 @@ def get_allowed_exmods(): class JsonPayloadSerializer(messaging.NoOpSerializer): + @staticmethod def serialize_entity(context, entity): return jsonutils.to_primitive(entity, convert_instances=True) @@ -120,7 +120,7 @@ class RequestContextSerializer(messaging.Serializer): def get_transport_url(url_str=None): - #LOG.info('yoooooooooooo') + # LOG.info('yoooooooooooo') return messaging.TransportURL.parse(CONF, url_str, TRANSPORT_ALIASES) diff --git a/iotronic/common/service.py b/iotronic/common/service.py index cab51b2..a1c74a4 100644 --- a/iotronic/common/service.py +++ b/iotronic/common/service.py @@ -117,7 +117,7 @@ class RPCService(service.Service): def prepare_service(argv=[]): log.register_options(cfg.CONF) - + log.set_defaults(default_log_levels=['amqp=WARN', 'amqplib=WARN', 'qpid.messagregister_optionsing=INFO', diff --git a/iotronic/common/states.py b/iotronic/common/states.py index fa1cb4c..51d0340 100644 --- a/iotronic/common/states.py +++ b/iotronic/common/states.py @@ -153,7 +153,7 @@ INSPECTFAIL = 'inspect failed' UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, CLEANFAIL) """Transitional states in which we allow updating a node.""" -### NEW +# NEW OPERATIVE = 'operative' MAINTENANCE = 'maintenance' diff --git a/iotronic/common/utils.py b/iotronic/common/utils.py index 1fb268a..a6ff21c 100644 --- a/iotronic/common/utils.py +++ b/iotronic/common/utils.py @@ -257,7 +257,7 @@ def is_valid_cidr(address): ip_segment = address.split('/') if (len(ip_segment) <= 1 or - ip_segment[1] == ''): + ip_segment[1] == ''): return False return True diff --git a/iotronic/conductor/__old/manager.py b/iotronic/conductor/__old/manager.py deleted file mode 100644 index 050c365..0000000 --- a/iotronic/conductor/__old/manager.py +++ /dev/null @@ -1,2195 +0,0 @@ -# coding=utf-8 - -# Copyright 2013 Hewlett-Packard Development Company, L.P. -# Copyright 2013 International Business Machines 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. -"""Conduct all activity related to bare-metal deployments. - -A single instance of :py:class:`iotronic.iotconductor.manager.ConductorManager` is -created within the *iotronic-conductor* process, and is responsible for -performing all actions on bare metal resources (Chassis, Nodes, and Ports). -Commands are received via RPCs. The conductor service also performs periodic -tasks, eg. to monitor the status of active deployments. - -Drivers are loaded via entrypoints by the -:py:class:`iotronic.common.driver_factory` class. Each driver is instantiated -only once, when the ConductorManager service starts. In this way, a single -ConductorManager may use multiple drivers, and manage heterogeneous hardware. - -When multiple :py:class:`ConductorManager` are run on different hosts, they are -all active and cooperatively manage all nodes in the deployment. Nodes are -locked by each conductor when performing actions which change the state of that -node; these locks are represented by the -:py:class:`iotronic.iotconductor.task_manager.TaskManager` class. - -A :py:class:`iotronic.common.hash_ring.HashRing` is used to distribute nodes -across the set of active conductors which support each node's driver. -Rebalancing this ring can trigger various actions by each conductor, such as -building or tearing down the TFTP environment for a node, notifying Neutron of -a change, etc. -""" -''' -import collections -import datetime -import inspect -import tempfile - - -from iotronic.common import dhcp_factory - - -from iotronic.common.glance_service import service_utils as glance_utils - -from iotronic.common import images -from iotronic.common import rpc -from iotronic.common import states -from iotronic.common import swift -from iotronic.iotconductor import task_manager -from iotronic.iotconductor import utils - -from iotronic import objects -from iotronic.openstack.common import periodic_task -''' -import threading -import eventlet -from eventlet import greenpool - -from iotronic.db import api as dbapi -from oslo_config import cfg -from oslo_db import exception as db_exception -from oslo_concurrency import lockutils -from oslo_config import cfg -from oslo_db import exception as db_exception -from oslo_log import log -import oslo_messaging as messaging -from oslo_utils import excutils -from oslo_utils import uuidutils - -from iotronic.common import hash_ring as hash -from iotronic.common.i18n import _ -from iotronic.common.i18n import _LC -from iotronic.common.i18n import _LE -from iotronic.common.i18n import _LI -from iotronic.common.i18n import _LW -#from iotronic.common import driver_factory - -from iotronic.conductor import task_manager - -from iotronic.openstack.common import periodic_task -from iotronic.common import exception - -MANAGER_TOPIC = 'iotronic.conductor_manager' -WORKER_SPAWN_lOCK = "conductor_worker_spawn" - -LOG = log.getLogger(__name__) - -conductor_opts = [ - cfg.StrOpt('api_url', - help=('URL of Iotronic API service. If not set iotronic can ' - 'get the current value from the keystone service ' - 'catalog.')), - cfg.IntOpt('heartbeat_interval', - default=10, - help='Seconds between conductor heart beats.'), - cfg.IntOpt('heartbeat_timeout', - default=60, - help='Maximum time (in seconds) since the last check-in ' - 'of a conductor. A conductor is considered inactive ' - 'when this time has been exceeded.'), - cfg.IntOpt('sync_power_state_interval', - default=60, - help='Interval between syncing the node power state to the ' - 'database, in seconds.'), - cfg.IntOpt('check_provision_state_interval', - default=60, - help='Interval between checks of provision timeouts, ' - 'in seconds.'), - cfg.IntOpt('deploy_callback_timeout', - default=1800, - help='Timeout (seconds) to wait for a callback from ' - 'a deploy ramdisk. Set to 0 to disable timeout.'), - cfg.BoolOpt('force_power_state_during_sync', - default=True, - help='During sync_power_state, should the hardware power ' - 'state be set to the state recorded in the database ' - '(True) or should the database be updated based on ' - 'the hardware state (False).'), - cfg.IntOpt('power_state_sync_max_retries', - default=3, - help='During sync_power_state failures, limit the ' - 'number of times Iotronic should try syncing the ' - 'hardware node power state with the node power state ' - 'in DB'), - cfg.IntOpt('periodic_max_workers', - default=8, - help='Maximum number of worker threads that can be started ' - 'simultaneously by a periodic task. Should be less ' - 'than RPC thread pool size.'), - cfg.IntOpt('workers_pool_size', - default=100, - help='The size of the workers greenthread pool.'), - cfg.IntOpt('node_locked_retry_attempts', - default=3, - help='Number of attempts to grab a node lock.'), - cfg.IntOpt('node_locked_retry_interval', - default=1, - help='Seconds to sleep between node lock attempts.'), - cfg.BoolOpt('send_sensor_data', - default=False, - help='Enable sending sensor data message via the ' - 'notification bus'), - cfg.IntOpt('send_sensor_data_interval', - default=600, - help='Seconds between conductor sending sensor data message' - ' to ceilometer via the notification bus.'), - cfg.ListOpt('send_sensor_data_types', - default=['ALL'], - help='List of comma separated meter types which need to be' - ' sent to Ceilometer. The default value, "ALL", is a ' - 'special value meaning send all the sensor data.'), - cfg.IntOpt('sync_local_state_interval', - default=180, - help='When conductors join or leave the cluster, existing ' - 'conductors may need to update any persistent ' - 'local state as nodes are moved around the cluster. ' - 'This option controls how often, in seconds, each ' - 'conductor will check for nodes that it should ' - '"take over". Set it to a negative value to disable ' - 'the check entirely.'), - cfg.BoolOpt('configdrive_use_swift', - default=False, - help='Whether to upload the config drive to Swift.'), - cfg.StrOpt('configdrive_swift_container', - default='iotronic_configdrive_container', - help='Name of the Swift container to store config drive ' - 'data. Used when configdrive_use_swift is True.'), - cfg.IntOpt('inspect_timeout', - default=1800, - help='Timeout (seconds) for waiting for node inspection. ' - '0 - unlimited.'), - cfg.BoolOpt('clean_nodes', - default=True, - help='Cleaning is a configurable set of steps, such as ' - 'erasing disk drives, that are performed on the node ' - 'to ensure it is in a baseline state and ready to be ' - 'deployed to. ' - 'This is done after instance deletion, and during ' - 'the transition from a "managed" to "available" ' - 'state. When enabled, the particular steps ' - 'performed to clean a node depend on which driver ' - 'that node is managed by; see the individual ' - 'driver\'s documentation for details. ' - 'NOTE: The introduction of the cleaning operation ' - 'causes instance deletion to take significantly ' - 'longer. In an environment where all tenants are ' - 'trusted (eg, because there is only one tenant), ' - 'this option could be safely disabled.'), -] -CONF = cfg.CONF -CONF.register_opts(conductor_opts, 'conductor') - -CLEANING_INTERFACE_PRIORITY = { - # When two clean steps have the same priority, their order is determined - # by which interface is implementing the clean step. The clean step of the - # interface with the highest value here, will be executed first in that - # case. - 'power': 3, - 'management': 2, - 'deploy': 1 -} - - -class ConductorManager(periodic_task.PeriodicTasks): - """Iotronic Conductor manager main class.""" - - # NOTE(rloo): This must be in sync with rpcapi.ConductorAPI's. - RPC_API_VERSION = '1.0' - - target = messaging.Target(version=RPC_API_VERSION) - - def __init__(self, host, topic): - super(ConductorManager, self).__init__() - if not host: - host = CONF.host - self.host = host - self.topic = topic - #self.power_state_sync_count = collections.defaultdict(int) - #self.notifier = rpc.get_notifier() - ''' - def _get_driver(self, driver_name): - """Get the driver. - - :param driver_name: name of the driver. - :returns: the driver; an instance of a class which implements - :class:`iotronic.drivers.base.BaseDriver`. - :raises: DriverNotFound if the driver is not loaded. - - """ - try: - return self._driver_factory[driver_name].obj - except KeyError: - raise exception.DriverNotFound(driver_name=driver_name) - ''' - def init_host(self): - self.dbapi = dbapi.get_instance() - - self._keepalive_evt = threading.Event() - """Event for the keepalive thread.""" - - self._worker_pool = greenpool.GreenPool( - size=CONF.conductor.workers_pool_size) - """GreenPool of background workers for performing tasks async.""" - - self.ring_manager = hash.HashRingManager() - """Consistent hash ring which maps drivers to conductors.""" - - # NOTE(deva): instantiating DriverFactory may raise DriverLoadError - # or DriverNotFound - #self._driver_factory = driver_factory.DriverFactory() - #"""Driver factory loads all enabled drivers.""" - - #self.drivers = self._driver_factory.names - """List of driver names which this conductor supports.""" - ''' - if not self.drivers: - msg = _LE("Conductor %s cannot be started because no drivers " - "were loaded. This could be because no drivers were " - "specified in 'enabled_drivers' config option.") - LOG.error(msg, self.host) - raise exception.NoDriversLoaded(conductor=self.host) - - # Collect driver-specific periodic tasks - for driver_obj in driver_factory.drivers().values(): - self._collect_periodic_tasks(driver_obj) - for iface_name in (driver_obj.core_interfaces + - driver_obj.standard_interfaces + - ['vendor']): - iface = getattr(driver_obj, iface_name, None) - if iface: - self._collect_periodic_tasks(iface) - ''' - # clear all locks held by this conductor before registering - self.dbapi.clear_node_reservations_for_conductor(self.host) - try: - # Register this conductor with the cluster - cdr = self.dbapi.register_conductor({'hostname': self.host,'drivers': ['fake']}) - except exception.ConductorAlreadyRegistered: - # This conductor was already registered and did not shut down - # properly, so log a warning and update the record. - LOG.warn(_LW("A conductor with hostname %(hostname)s " - "was previously registered. Updating registration"), - {'hostname': self.host}) - cdr = self.dbapi.register_conductor({'hostname': self.host, - 'drivers': self.drivers}, - update_existing=True) - self.conductor = cdr - - # Spawn a dedicated greenthread for the keepalive - try: - self._spawn_worker(self._conductor_service_record_keepalive) - LOG.info(_LI('Successfully started conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - except exception.NoFreeConductorWorker: - with excutils.save_and_reraise_exception(): - LOG.critical(_LC('Failed to start keepalive')) - self.del_host() - - def _collect_periodic_tasks(self, obj): - for n, method in inspect.getmembers(obj, inspect.ismethod): - if getattr(method, '_periodic_enabled', False): - self.add_periodic_task(method) - - def del_host(self, deregister=True): - self._keepalive_evt.set() - if deregister: - try: - # Inform the cluster that this conductor is shutting down. - # Note that rebalancing will not occur immediately, but when - # the periodic sync takes place. - self.dbapi.unregister_conductor(self.host) - LOG.info(_LI('Successfully stopped conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - except exception.ConductorNotFound: - pass - else: - LOG.info(_LI('Not deregistering conductor with hostname ' - '%(hostname)s.'), - {'hostname': self.host}) - # Waiting here to give workers the chance to finish. This has the - # benefit of releasing locks workers placed on nodes, as well as - # having work complete normally. - self._worker_pool.waitall() - - def periodic_tasks(self, context, raise_on_error=False): - """Periodic tasks are run at pre-specified interval.""" - return self.run_periodic_tasks(context, raise_on_error=raise_on_error) - - @lockutils.synchronized(WORKER_SPAWN_lOCK, 'iotronic-') - def _spawn_worker(self, func, *args, **kwargs): - - """Create a greenthread to run func(*args, **kwargs). - - Spawns a greenthread if there are free slots in pool, otherwise raises - exception. Execution control returns immediately to the caller. - - :returns: GreenThread object. - :raises: NoFreeConductorWorker if worker pool is currently full. - - """ - if self._worker_pool.free(): - return self._worker_pool.spawn(func, *args, **kwargs) - else: - raise exception.NoFreeConductorWorker() - - def _conductor_service_record_keepalive(self): - while not self._keepalive_evt.is_set(): - try: - self.dbapi.touch_conductor(self.host) - except db_exception.DBConnectionError: - LOG.warning(_LW('Conductor could not connect to database ' - 'while heartbeating.')) - self._keepalive_evt.wait(CONF.conductor.heartbeat_interval) - - @messaging.expected_exceptions(exception.InvalidParameterValue, - exception.MissingParameterValue, - exception.NodeLocked) - def update_node(self, context, node_obj): - """Update a node with the supplied data. - - This method is the main "hub" for PUT and PATCH requests in the API. - It ensures that the requested change is safe to perform, - validates the parameters with the node's driver, if necessary. - - :param context: an admin context - :param node_obj: a changed (but not saved) node object. - - """ - node_id = node_obj.uuid - LOG.debug("RPC update_node called for node %s." % node_id) - - # NOTE(jroll) clear maintenance_reason if node.update sets - # maintenance to False for backwards compatibility, for tools - # not using the maintenance endpoint. - delta = node_obj.obj_what_changed() - if 'maintenance' in delta and not node_obj.maintenance: - node_obj.maintenance_reason = None - - driver_name = node_obj.driver if 'driver' in delta else None - with task_manager.acquire(context, node_id, shared=False, - driver_name=driver_name): - node_obj.save() - - return node_obj - - @messaging.expected_exceptions(exception.InvalidParameterValue, - exception.MissingParameterValue, - exception.NoFreeConductorWorker, - exception.NodeLocked) - def change_node_power_state(self, context, node_id, new_state): - """RPC method to encapsulate changes to a node's state. - - Perform actions such as power on, power off. The validation is - performed synchronously, and if successful, the power action is - updated in the background (asynchronously). Once the power action - is finished and successful, it updates the power_state for the - node with the new power state. - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :param new_state: the desired power state of the node. - :raises: NoFreeConductorWorker when there is no free worker to start - async task. - - """ - LOG.debug("RPC change_node_power_state called for node %(node)s. " - "The desired new state is %(state)s." - % {'node': node_id, 'state': new_state}) - - with task_manager.acquire(context, node_id, shared=False) as task: - task.driver.power.validate(task) - # Set the target_power_state and clear any last_error, since we're - # starting a new operation. This will expose to other processes - # and clients that work is in progress. - if new_state == states.REBOOT: - task.node.target_power_state = states.POWER_ON - else: - task.node.target_power_state = new_state - task.node.last_error = None - task.node.save() - task.set_spawn_error_hook(power_state_error_handler, - task.node, task.node.power_state) - task.spawn_after(self._spawn_worker, utils.node_power_action, - task, new_state) - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.InvalidParameterValue, - exception.UnsupportedDriverExtension, - exception.MissingParameterValue) - def vendor_passthru(self, context, node_id, driver_method, - http_method, info): - """RPC method to encapsulate vendor action. - - Synchronously validate driver specific info or get driver status, - and if successful invokes the vendor method. If the method mode - is 'async' the conductor will start background worker to perform - vendor action. - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :param driver_method: the name of the vendor method. - :param http_method: the HTTP method used for the request. - :param info: vendor method args. - :raises: InvalidParameterValue if supplied info is not valid. - :raises: MissingParameterValue if missing supplied info - :raises: UnsupportedDriverExtension if current driver does not have - vendor interface or method is unsupported. - :raises: NoFreeConductorWorker when there is no free worker to start - async task. - :raises: NodeLocked if node is locked by another conductor. - :returns: A tuple containing the response of the invoked method - and a boolean value indicating whether the method was - invoked asynchronously (True) or synchronously (False). - If invoked asynchronously the response field will be - always None. - """ - LOG.debug("RPC vendor_passthru called for node %s." % node_id) - # NOTE(max_lobur): Even though not all vendor_passthru calls may - # require an exclusive lock, we need to do so to guarantee that the - # state doesn't unexpectedly change between doing a vendor.validate - # and vendor.vendor_passthru. - with task_manager.acquire(context, node_id, shared=False) as task: - if not getattr(task.driver, 'vendor', None): - raise exception.UnsupportedDriverExtension( - driver=task.node.driver, - extension='vendor interface') - - vendor_iface = task.driver.vendor - - # NOTE(lucasagomes): Before the vendor_passthru() method was - # a self-contained method and each driver implemented their own - # version of it, now we have a common mechanism that drivers - # should use to expose their vendor methods. If a driver still - # have their own vendor_passthru() method we call it to be - # backward compat. This code should be removed once L opens. - if hasattr(vendor_iface, 'vendor_passthru'): - LOG.warning(_LW("Drivers implementing their own version " - "of vendor_passthru() has been deprecated. " - "Please update the code to use the " - "@passthru decorator.")) - vendor_iface.validate(task, method=driver_method, - **info) - task.spawn_after(self._spawn_worker, - vendor_iface.vendor_passthru, task, - method=driver_method, **info) - # NodeVendorPassthru was always async - return (None, True) - - try: - vendor_opts = vendor_iface.vendor_routes[driver_method] - vendor_func = vendor_opts['func'] - except KeyError: - raise exception.InvalidParameterValue( - _('No handler for method %s') % driver_method) - - http_method = http_method.upper() - if http_method not in vendor_opts['http_methods']: - raise exception.InvalidParameterValue( - _('The method %(method)s does not support HTTP %(http)s') % - {'method': driver_method, 'http': http_method}) - - vendor_iface.validate(task, method=driver_method, - http_method=http_method, **info) - - # Inform the vendor method which HTTP method it was invoked with - info['http_method'] = http_method - - # Invoke the vendor method accordingly with the mode - is_async = vendor_opts['async'] - ret = None - if is_async: - task.spawn_after(self._spawn_worker, vendor_func, task, **info) - else: - ret = vendor_func(task, **info) - - return (ret, is_async) - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.InvalidParameterValue, - exception.MissingParameterValue, - exception.UnsupportedDriverExtension, - exception.DriverNotFound) - def driver_vendor_passthru(self, context, driver_name, driver_method, - http_method, info): - """Handle top-level vendor actions. - - RPC method which handles driver-level vendor passthru calls. These - calls don't require a node UUID and are executed on a random - conductor with the specified driver. If the method mode is - async the conductor will start background worker to perform - vendor action. - - :param context: an admin context. - :param driver_name: name of the driver on which to call the method. - :param driver_method: name of the vendor method, for use by the driver. - :param http_method: the HTTP method used for the request. - :param info: user-supplied data to pass through to the driver. - :raises: MissingParameterValue if missing supplied info - :raises: InvalidParameterValue if supplied info is not valid. - :raises: UnsupportedDriverExtension if current driver does not have - vendor interface, if the vendor interface does not implement - driver-level vendor passthru or if the passthru method is - unsupported. - :raises: DriverNotFound if the supplied driver is not loaded. - :raises: NoFreeConductorWorker when there is no free worker to start - async task. - :returns: A tuple containing the response of the invoked method - and a boolean value indicating whether the method was - invoked asynchronously (True) or synchronously (False). - If invoked asynchronously the response field will be - always None. - """ - # Any locking in a top-level vendor action will need to be done by the - # implementation, as there is little we could reasonably lock on here. - LOG.debug("RPC driver_vendor_passthru for driver %s." % driver_name) - driver = self._get_driver(driver_name) - if not getattr(driver, 'vendor', None): - raise exception.UnsupportedDriverExtension( - driver=driver_name, - extension='vendor interface') - - # NOTE(lucasagomes): Before the driver_vendor_passthru() - # method was a self-contained method and each driver implemented - # their own version of it, now we have a common mechanism that - # drivers should use to expose their vendor methods. If a driver - # still have their own driver_vendor_passthru() method we call - # it to be backward compat. This code should be removed - # once L opens. - if hasattr(driver.vendor, 'driver_vendor_passthru'): - LOG.warning(_LW("Drivers implementing their own version " - "of driver_vendor_passthru() has been " - "deprecated. Please update the code to use " - "the @driver_passthru decorator.")) - - driver.vendor.driver_validate(method=driver_method, **info) - ret = driver.vendor.driver_vendor_passthru( - context, method=driver_method, **info) - # DriverVendorPassthru was always sync - return (ret, False) - - try: - vendor_opts = driver.vendor.driver_routes[driver_method] - vendor_func = vendor_opts['func'] - except KeyError: - raise exception.InvalidParameterValue( - _('No handler for method %s') % driver_method) - - http_method = http_method.upper() - if http_method not in vendor_opts['http_methods']: - raise exception.InvalidParameterValue( - _('The method %(method)s does not support HTTP %(http)s') % - {'method': driver_method, 'http': http_method}) - - # Inform the vendor method which HTTP method it was invoked with - info['http_method'] = http_method - - # Invoke the vendor method accordingly with the mode - is_async = vendor_opts['async'] - ret = None - driver.vendor.driver_validate(method=driver_method, **info) - - if is_async: - self._spawn_worker(vendor_func, context, **info) - else: - ret = vendor_func(context, **info) - - return (ret, is_async) - - @messaging.expected_exceptions(exception.UnsupportedDriverExtension) - def get_node_vendor_passthru_methods(self, context, node_id): - """Retrieve information about vendor methods of the given node. - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :returns: dictionary of : entries. - - """ - LOG.debug("RPC get_node_vendor_passthru_methods called for node %s" - % node_id) - with task_manager.acquire(context, node_id, shared=True) as task: - if not getattr(task.driver, 'vendor', None): - raise exception.UnsupportedDriverExtension( - driver=task.node.driver, - extension='vendor interface') - - return get_vendor_passthru_metadata( - task.driver.vendor.vendor_routes) - - @messaging.expected_exceptions(exception.UnsupportedDriverExtension, - exception.DriverNotFound) - def get_driver_vendor_passthru_methods(self, context, driver_name): - """Retrieve information about vendor methods of the given driver. - - :param context: an admin context. - :param driver_name: name of the driver. - :returns: dictionary of : entries. - - """ - # Any locking in a top-level vendor action will need to be done by the - # implementation, as there is little we could reasonably lock on here. - LOG.debug("RPC get_driver_vendor_passthru_methods for driver %s" - % driver_name) - driver = self._get_driver(driver_name) - if not getattr(driver, 'vendor', None): - raise exception.UnsupportedDriverExtension( - driver=driver_name, - extension='vendor interface') - - return get_vendor_passthru_metadata(driver.vendor.driver_routes) - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.NodeInMaintenance, - exception.InstanceDeployFailure, - exception.InvalidStateRequested) - def do_node_deploy(self, context, node_id, rebuild=False, - configdrive=None): - """RPC method to initiate deployment to a node. - - Initiate the deployment of a node. Validations are done - synchronously and the actual deploy work is performed in - background (asynchronously). - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :param rebuild: True if this is a rebuild request. A rebuild will - recreate the instance on the same node, overwriting - all disk. The ephemeral partition, if it exists, can - optionally be preserved. - :param configdrive: Optional. A gzipped and base64 encoded configdrive. - :raises: InstanceDeployFailure - :raises: NodeInMaintenance if the node is in maintenance mode. - :raises: NoFreeConductorWorker when there is no free worker to start - async task. - :raises: InvalidStateRequested when the requested state is not a valid - target from the current state. - - """ - LOG.debug("RPC do_node_deploy called for node %s." % node_id) - - # NOTE(comstud): If the _sync_power_states() periodic task happens - # to have locked this node, we'll fail to acquire the lock. The - # client should perhaps retry in this case unless we decide we - # want to add retries or extra synchronization here. - with task_manager.acquire(context, node_id, shared=False) as task: - node = task.node - if node.maintenance: - raise exception.NodeInMaintenance(op=_('provisioning'), - node=node.uuid) - - if rebuild: - event = 'rebuild' - - # Note(gilliard) Clear these to force the driver to - # check whether they have been changed in glance - # NOTE(vdrok): If image_source is not from Glance we should - # not clear kernel and ramdisk as they're input manually - if glance_utils.is_glance_image( - node.instance_info.get('image_source')): - instance_info = node.instance_info - instance_info.pop('kernel', None) - instance_info.pop('ramdisk', None) - node.instance_info = instance_info - else: - event = 'deploy' - - driver_internal_info = node.driver_internal_info - # Infer the image type to make sure the deploy driver - # validates only the necessary variables for different - # image types. - # NOTE(sirushtim): The iwdi variable can be None. It's up to - # the deploy driver to validate this. - iwdi = images.is_whole_disk_image(context, node.instance_info) - driver_internal_info['is_whole_disk_image'] = iwdi - node.driver_internal_info = driver_internal_info - node.save() - - try: - task.driver.power.validate(task) - task.driver.deploy.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue) as e: - raise exception.InstanceDeployFailure(_( - "RPC do_node_deploy failed to validate deploy or " - "power info. Error: %(msg)s") % {'msg': e}) - - LOG.debug("do_node_deploy Calling event: %(event)s for node: " - "%(node)s", {'event': event, 'node': node.uuid}) - try: - task.process_event(event, - callback=self._spawn_worker, - call_args=(do_node_deploy, task, - self.conductor.id, - configdrive), - err_handler=provisioning_error_handler) - except exception.InvalidState: - raise exception.InvalidStateRequested( - action=event, node=task.node.uuid, - state=task.node.provision_state) - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.InstanceDeployFailure, - exception.InvalidStateRequested) - def do_node_tear_down(self, context, node_id): - """RPC method to tear down an existing node deployment. - - Validate driver specific information synchronously, and then - spawn a background worker to tear down the node asynchronously. - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :raises: InstanceDeployFailure - :raises: NoFreeConductorWorker when there is no free worker to start - async task - :raises: InvalidStateRequested when the requested state is not a valid - target from the current state. - - """ - LOG.debug("RPC do_node_tear_down called for node %s." % node_id) - - with task_manager.acquire(context, node_id, shared=False) as task: - try: - # NOTE(ghe): Valid power driver values are needed to perform - # a tear-down. Deploy info is useful to purge the cache but not - # required for this method. - task.driver.power.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue) as e: - raise exception.InstanceDeployFailure(_( - "Failed to validate power driver interface. " - "Can not delete instance. Error: %(msg)s") % {'msg': e}) - - try: - task.process_event('delete', - callback=self._spawn_worker, - call_args=(self._do_node_tear_down, task), - err_handler=provisioning_error_handler) - except exception.InvalidState: - raise exception.InvalidStateRequested( - action='delete', node=task.node.uuid, - state=task.node.provision_state) - - def _do_node_tear_down(self, task): - """Internal RPC method to tear down an existing node deployment.""" - node = task.node - try: - task.driver.deploy.clean_up(task) - task.driver.deploy.tear_down(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - LOG.exception(_LE('Error in tear_down of node %(node)s: ' - '%(err)s'), - {'node': node.uuid, 'err': e}) - node.last_error = _("Failed to tear down. Error: %s") % e - task.process_event('error') - else: - # NOTE(deva): When tear_down finishes, the deletion is done, - # cleaning will start next - LOG.info(_LI('Successfully unprovisioned node %(node)s with ' - 'instance %(instance)s.'), - {'node': node.uuid, 'instance': node.instance_uuid}) - finally: - # NOTE(deva): there is no need to unset conductor_affinity - # because it is a reference to the most recent conductor which - # deployed a node, and does not limit any future actions. - # But we do need to clear the instance_info - node.instance_info = {} - node.save() - - # Begin cleaning - try: - task.process_event('clean') - except exception.InvalidState: - raise exception.InvalidStateRequested( - action='clean', node=node.uuid, - state=node.provision_state) - self._do_node_clean(task) - - def continue_node_clean(self, context, node_id): - """RPC method to continue cleaning a node. - - This is useful for cleaning tasks that are async. When they complete, - they call back via RPC, a new worker and lock are set up, and cleaning - continues. This can also be used to resume cleaning on take_over. - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :raises: InvalidStateRequested if the node is not in CLEANING state - :raises: NoFreeConductorWorker when there is no free worker to start - async task - :raises: NodeLocked if node is locked by another conductor. - :raises: NodeNotFound if the node no longer appears in the database - - """ - LOG.debug("RPC continue_node_clean called for node %s.", node_id) - - with task_manager.acquire(context, node_id, shared=False) as task: - if task.node.provision_state != states.CLEANING: - raise exception.InvalidStateRequested(_( - 'Cannot continue cleaning on %(node)s, node is in ' - '%(state)s state, should be %(clean_state)s') % - {'node': task.node.uuid, - 'state': task.node.provision_state, - 'clean_state': states.CLEANING}) - task.set_spawn_error_hook(cleaning_error_handler, task.node, - 'Failed to run next clean step') - task.spawn_after( - self._spawn_worker, - self._do_next_clean_step, - task, - task.node.driver_internal_info.get('clean_steps', []), - task.node.clean_step) - - def _do_node_clean(self, task): - """Internal RPC method to perform automated cleaning of a node.""" - node = task.node - LOG.debug('Starting cleaning for node %s', node.uuid) - - if not CONF.conductor.clean_nodes: - # Skip cleaning, move to AVAILABLE. - node.clean_step = None - node.save() - - task.process_event('done') - LOG.info(_LI('Cleaning is disabled, node %s has been successfully ' - 'moved to AVAILABLE state.'), node.uuid) - return - - try: - # NOTE(ghe): Valid power driver values are needed to perform - # a cleaning. - task.driver.power.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue) as e: - msg = (_('Failed to validate power driver interface. ' - 'Can not clean node %(node)s. Error: %(msg)s') % - {'node': node.uuid, 'msg': e}) - return cleaning_error_handler(task, msg) - - # Allow the deploy driver to set up the ramdisk again (necessary for - # IPA cleaning/zapping) - try: - prepare_result = task.driver.deploy.prepare_cleaning(task) - except Exception as e: - msg = (_('Failed to prepare node %(node)s for cleaning: %(e)s') - % {'node': node.uuid, 'e': e}) - LOG.exception(msg) - return cleaning_error_handler(task, msg) - if prepare_result == states.CLEANING: - # Prepare is asynchronous, the deploy driver will need to - # set node.driver_internal_info['clean_steps'] and - # node.clean_step and then make an RPC call to - # continue_node_cleaning to start cleaning. - return - - set_node_cleaning_steps(task) - self._do_next_clean_step( - task, - node.driver_internal_info.get('clean_steps', []), - node.clean_step) - - def _do_next_clean_step(self, task, steps, last_step): - """Start executing cleaning/zapping steps from the last step (if any). - - :param task: a TaskManager instance with an exclusive lock - :param steps: The complete list of steps that need to be executed - on the node - :param last_step: The last step that was executed. {} will start - from the beginning - """ - node = task.node - # Trim already executed steps - if last_step: - try: - # Trim off last_step (now finished) and all previous steps. - steps = steps[steps.index(last_step) + 1:] - except ValueError: - msg = (_('Node %(node)s got an invalid last step for ' - '%(state)s: %(step)s.') % - {'node': node.uuid, 'step': last_step, - 'state': node.provision_state}) - LOG.exception(msg) - return cleaning_error_handler(task, msg) - - LOG.info(_LI('Executing %(state)s on node %(node)s, remaining steps: ' - '%(steps)s'), {'node': node.uuid, 'steps': steps, - 'state': node.provision_state}) - # Execute each step until we hit an async step or run out of steps - for step in steps: - # Save which step we're about to start so we can restart - # if necessary - node.clean_step = step - node.save() - interface = getattr(task.driver, step.get('interface')) - LOG.info(_LI('Executing %(step)s on node %(node)s'), - {'step': step, 'node': node.uuid}) - try: - result = interface.execute_clean_step(task, step) - except Exception as e: - msg = (_('Node %(node)s failed step %(step)s: ' - '%(exc)s') % - {'node': node.uuid, 'exc': e, - 'step': node.clean_step}) - LOG.exception(msg) - cleaning_error_handler(task, msg) - return - - # Check if the step is done or not. The step should return - # states.CLEANING if the step is still being executed, or - # None if the step is done. - if result == states.CLEANING: - # Kill this worker, the async step will make an RPC call to - # continue_node_clean to continue cleaning - LOG.info(_LI('Clean step %(step)s on node %(node)s being ' - 'executed asynchronously, waiting for driver.') % - {'node': node.uuid, 'step': step}) - return - elif result is not None: - msg = (_('While executing step %(step)s on node ' - '%(node)s, step returned invalid value: %(val)s') - % {'step': step, 'node': node.uuid, 'val': result}) - LOG.error(msg) - return cleaning_error_handler(task, msg) - LOG.info(_LI('Node %(node)s finished clean step %(step)s'), - {'node': node.uuid, 'step': step}) - - # Clear clean_step - node.clean_step = None - driver_internal_info = node.driver_internal_info - driver_internal_info['clean_steps'] = None - node.driver_internal_info = driver_internal_info - try: - task.driver.deploy.tear_down_cleaning(task) - except Exception as e: - msg = (_('Failed to tear down from cleaning for node %s') - % node.uuid) - LOG.exception(msg) - return cleaning_error_handler(task, msg, tear_down_cleaning=False) - - LOG.info(_LI('Node %s cleaning complete'), node.uuid) - task.process_event('done') - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.InvalidParameterValue, - exception.MissingParameterValue, - exception.InvalidStateRequested) - def do_provisioning_action(self, context, node_id, action): - """RPC method to initiate certain provisioning state transitions. - - Initiate a provisioning state change through the state machine, - rather than through an RPC call to do_node_deploy / do_node_tear_down - - :param context: an admin context. - :param node_id: the id or uuid of a node. - :param action: an action. One of iotronic.common.states.VERBS - :raises: InvalidParameterValue - :raises: InvalidStateRequested - :raises: NoFreeConductorWorker - - """ - with task_manager.acquire(context, node_id, shared=False) as task: - if (action == states.VERBS['provide'] and - task.node.provision_state == states.MANAGEABLE): - task.process_event('provide', - callback=self._spawn_worker, - call_args=(self._do_node_clean, task), - err_handler=provisioning_error_handler) - else: - try: - task.process_event(action) - except exception.InvalidState: - raise exception.InvalidStateRequested( - action=action, node=task.node.uuid, - state=task.node.provision_state) - - @periodic_task.periodic_task( - spacing=CONF.conductor.sync_power_state_interval) - def _sync_power_states(self, context): - """Periodic task to sync power states for the nodes. - - Attempt to grab a lock and sync only if the following - conditions are met: - - 1) Node is mapped to this conductor. - 2) Node is not in maintenance mode. - 3) Node is not in DEPLOYWAIT provision state. - 4) Node doesn't have a reservation - - NOTE: Grabbing a lock here can cause other methods to fail to - grab it. We want to avoid trying to grab a lock while a - node is in the DEPLOYWAIT state so we don't unnecessarily - cause a deploy callback to fail. There's not much we can do - here to avoid failing a brand new deploy to a node that we've - locked here, though. - """ - # FIXME(comstud): Since our initial state checks are outside - # of the lock (to try to avoid the lock), some checks are - # repeated after grabbing the lock so we can unlock quickly. - # The node mapping is not re-checked because it doesn't much - # matter if things happened to re-balance. - # - # This is inefficient and racey. We end up with calling DB API's - # get_node() twice (once here, and once in acquire(). Ideally we - # add a way to pass constraints to task_manager.acquire() - # (through to its DB API call) so that we can eliminate our call - # and first set of checks below. - - filters = {'reserved': False, 'maintenance': False} - node_iter = self.iter_nodes(fields=['id'], filters=filters) - for (node_uuid, driver, node_id) in node_iter: - try: - # NOTE(deva): we should not acquire a lock on a node in - # DEPLOYWAIT, as this could cause an error within - # a deploy ramdisk POSTing back at the same time. - # TODO(deva): refactor this check, because it needs to be done - # in every periodic task, not just this one. - node = objects.Node.get_by_id(context, node_id) - if (node.provision_state == states.DEPLOYWAIT or - node.maintenance or node.reservation is not None): - continue - - with task_manager.acquire(context, node_uuid) as task: - if (task.node.provision_state == states.DEPLOYWAIT or - task.node.maintenance): - continue - count = do_sync_power_state( - task, self.power_state_sync_count[node_uuid]) - if count: - self.power_state_sync_count[node_uuid] = count - else: - # don't bloat the dict with non-failing nodes - del self.power_state_sync_count[node_uuid] - except exception.NodeNotFound: - LOG.info(_LI("During sync_power_state, node %(node)s was not " - "found and presumed deleted by another process."), - {'node': node_uuid}) - except exception.NodeLocked: - LOG.info(_LI("During sync_power_state, node %(node)s was " - "already locked by another process. Skip."), - {'node': node_uuid}) - finally: - # Yield on every iteration - eventlet.sleep(0) - - @periodic_task.periodic_task( - spacing=CONF.conductor.check_provision_state_interval) - def _check_deploy_timeouts(self, context): - """Periodically checks whether a deploy RPC call has timed out. - - If a deploy call has timed out, the deploy failed and we clean up. - - :param context: request context. - """ - callback_timeout = CONF.conductor.deploy_callback_timeout - if not callback_timeout: - return - - filters = {'reserved': False, - 'provision_state': states.DEPLOYWAIT, - 'maintenance': False, - 'provisioned_before': callback_timeout} - sort_key = 'provision_updated_at' - callback_method = utils.cleanup_after_timeout - err_handler = provisioning_error_handler - self._fail_if_in_state(context, filters, states.DEPLOYWAIT, - sort_key, callback_method, err_handler) - - def _do_takeover(self, task): - """Take over this node. - - Prepares a node for takeover by this conductor, performs the takeover, - and changes the conductor associated with the node. The node with the - new conductor affiliation is saved to the DB. - - :param task: a TaskManager instance - """ - LOG.debug(('Conductor %(cdr)s taking over node %(node)s'), - {'cdr': self.host, 'node': task.node.uuid}) - task.driver.deploy.prepare(task) - task.driver.deploy.take_over(task) - # NOTE(lucasagomes): Set the ID of the new conductor managing - # this node - task.node.conductor_affinity = self.conductor.id - task.node.save() - - @periodic_task.periodic_task( - spacing=CONF.conductor.sync_local_state_interval) - def _sync_local_state(self, context): - """Perform any actions necessary to sync local state. - - This is called periodically to refresh the conductor's copy of the - consistent hash ring. If any mappings have changed, this method then - determines which, if any, nodes need to be "taken over". - The ensuing actions could include preparing a PXE environment, - updating the DHCP server, and so on. - """ - self.ring_manager.reset() - filters = {'reserved': False, - 'maintenance': False, - 'provision_state': states.ACTIVE} - node_iter = self.iter_nodes(fields=['id', 'conductor_affinity'], - filters=filters) - - workers_count = 0 - for node_uuid, driver, node_id, conductor_affinity in node_iter: - if conductor_affinity == self.conductor.id: - continue - - # Node is mapped here, but not updated by this conductor last - try: - with task_manager.acquire(context, node_uuid) as task: - # NOTE(deva): now that we have the lock, check again to - # avoid racing with deletes and other state changes - node = task.node - if (node.maintenance or - node.conductor_affinity == self.conductor.id or - node.provision_state != states.ACTIVE): - continue - - task.spawn_after(self._spawn_worker, - self._do_takeover, task) - - except exception.NoFreeConductorWorker: - break - except (exception.NodeLocked, exception.NodeNotFound): - continue - workers_count += 1 - if workers_count == CONF.conductor.periodic_max_workers: - break - - def _mapped_to_this_conductor(self, node_uuid, driver): - """Check that node is mapped to this conductor. - - Note that because mappings are eventually consistent, it is possible - for two conductors to simultaneously believe that a node is mapped to - them. Any operation that depends on exclusive control of a node should - take out a lock. - """ - try: - ring = self.ring_manager[driver] - except exception.DriverNotFound: - return False - - return self.host in ring.get_hosts(node_uuid) - - def iter_nodes(self, fields=None, **kwargs): - """Iterate over nodes mapped to this conductor. - - Requests node set from and filters out nodes that are not - mapped to this conductor. - - Yields tuples (node_uuid, driver, ...) where ... is derived from - fields argument, e.g.: fields=None means yielding ('uuid', 'driver'), - fields=['foo'] means yielding ('uuid', 'driver', 'foo'). - - :param fields: list of fields to fetch in addition to uuid and driver - :param kwargs: additional arguments to pass to dbapi when looking for - nodes - :return: generator yielding tuples of requested fields - """ - columns = ['uuid', 'driver'] + list(fields or ()) - node_list = self.dbapi.get_nodeinfo_list(columns=columns, **kwargs) - for result in node_list: - if self._mapped_to_this_conductor(*result[:2]): - yield result - - @messaging.expected_exceptions(exception.NodeLocked) - def validate_driver_interfaces(self, context, node_id): - """Validate the `core` and `standardized` interfaces for drivers. - - :param context: request context. - :param node_id: node id or uuid. - :returns: a dictionary containing the results of each - interface validation. - - """ - LOG.debug('RPC validate_driver_interfaces called for node %s.', - node_id) - ret_dict = {} - with task_manager.acquire(context, node_id, shared=True) as task: - # NOTE(sirushtim): the is_whole_disk_image variable is needed by - # deploy drivers for doing their validate(). Since the deploy - # isn't being done yet and the driver information could change in - # the meantime, we don't know if the is_whole_disk_image value will - # change or not. It isn't saved to the DB, but only used with this - # node instance for the current validations. - iwdi = images.is_whole_disk_image(context, - task.node.instance_info) - task.node.driver_internal_info['is_whole_disk_image'] = iwdi - for iface_name in (task.driver.core_interfaces + - task.driver.standard_interfaces): - iface = getattr(task.driver, iface_name, None) - result = reason = None - if iface: - try: - iface.validate(task) - result = True - except (exception.InvalidParameterValue, - exception.UnsupportedDriverExtension, - exception.MissingParameterValue) as e: - result = False - reason = str(e) - else: - reason = _('not supported') - - ret_dict[iface_name] = {} - ret_dict[iface_name]['result'] = result - if reason is not None: - ret_dict[iface_name]['reason'] = reason - return ret_dict - - @messaging.expected_exceptions(exception.NodeLocked, - exception.NodeAssociated, - exception.InvalidState) - def destroy_node(self, context, node_id): - """Delete a node. - - :param context: request context. - :param node_id: node id or uuid. - :raises: NodeLocked if node is locked by another conductor. - :raises: NodeAssociated if the node contains an instance - associated with it. - :raises: InvalidState if the node is in the wrong provision - state to perform deletion. - - """ - ''' - node.destroy() - LOG.info(_LI('Successfully deleted node %(node)s.'), - {'node': node.uuid}) - ''' - with task_manager.acquire(context, node_id) as task: - node = task.node - node.destroy() - LOG.info(_LI('Successfully deleted node %(node)s.'), - {'node': node.uuid}) - #if node.instance_uuid is not None: - # raise exception.NodeAssociated(node=node.uuid, - # instance=node.instance_uuid) - - # TODO(lucasagomes): We should add ENROLLED once it's part of our - # state machine - # NOTE(lucasagomes): For the *FAIL states we users should - # move it to a safe state prior to deletion. This is because we - # should try to avoid deleting a node in a dirty/whacky state, - # e.g: A node in DEPLOYFAIL, if deleted without passing through - # tear down/cleaning may leave data from the previous tenant - # in the disk. So nodes in *FAIL states should first be moved to: - # CLEANFAIL -> MANAGEABLE - # INSPECTIONFAIL -> MANAGEABLE - # DEPLOYFAIL -> DELETING - # ZAPFAIL -> MANAGEABLE (in the future) - ''' - valid_states = (states.AVAILABLE, states.NOSTATE, - states.MANAGEABLE) - if node.provision_state not in valid_states: - msg = (_('Can not delete node "%(node)s" while it is in ' - 'provision state "%(state)s". Valid provision states ' - 'to perform deletion are: "%(valid_states)s"') % - {'node': node.uuid, 'state': node.provision_state, - 'valid_states': valid_states}) - raise exception.InvalidState(msg) - if node.console_enabled: - try: - task.driver.console.stop_console(task) - except Exception as err: - LOG.error(_LE('Failed to stop console while deleting ' - 'the node %(node)s: %(err)s.'), - {'node': node.uuid, 'err': err}) - node.destroy() - LOG.info(_LI('Successfully deleted node %(node)s.'), - {'node': node.uuid}) - ''' - - @messaging.expected_exceptions(exception.NodeLocked, - exception.NodeNotFound) - def destroy_port(self, context, port): - """Delete a port. - - :param context: request context. - :param port: port object - :raises: NodeLocked if node is locked by another conductor. - :raises: NodeNotFound if the node associated with the port does not - exist. - - """ - LOG.debug('RPC destroy_port called for port %(port)s', - {'port': port.uuid}) - with task_manager.acquire(context, port.node_id) as task: - port.destroy() - LOG.info(_LI('Successfully deleted port %(port)s. ' - 'The node associated with the port was ' - '%(node)s'), - {'port': port.uuid, 'node': task.node.uuid}) - - @messaging.expected_exceptions(exception.NodeLocked, - exception.UnsupportedDriverExtension, - exception.NodeConsoleNotEnabled, - exception.InvalidParameterValue, - exception.MissingParameterValue) - def get_console_information(self, context, node_id): - """Get connection information about the console. - - :param context: request context. - :param node_id: node id or uuid. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support console. - :raises: NodeConsoleNotEnabled if the console is not enabled. - :raises: InvalidParameterValue when the wrong driver info is specified. - :raises: MissingParameterValue if missing supplied info. - """ - LOG.debug('RPC get_console_information called for node %s' % node_id) - - with task_manager.acquire(context, node_id, shared=True) as task: - node = task.node - - if not getattr(task.driver, 'console', None): - raise exception.UnsupportedDriverExtension(driver=node.driver, - extension='console') - if not node.console_enabled: - raise exception.NodeConsoleNotEnabled(node=node_id) - - task.driver.console.validate(task) - return task.driver.console.get_console(task) - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.UnsupportedDriverExtension, - exception.InvalidParameterValue, - exception.MissingParameterValue) - def set_console_mode(self, context, node_id, enabled): - """Enable/Disable the console. - - Validate driver specific information synchronously, and then - spawn a background worker to set console mode asynchronously. - - :param context: request context. - :param node_id: node id or uuid. - :param enabled: Boolean value; whether the console is enabled or - disabled. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support console. - :raises: InvalidParameterValue when the wrong driver info is specified. - :raises: MissingParameterValue if missing supplied info. - :raises: NoFreeConductorWorker when there is no free worker to start - async task - """ - LOG.debug('RPC set_console_mode called for node %(node)s with ' - 'enabled %(enabled)s' % {'node': node_id, - 'enabled': enabled}) - - with task_manager.acquire(context, node_id, shared=False) as task: - node = task.node - if not getattr(task.driver, 'console', None): - raise exception.UnsupportedDriverExtension(driver=node.driver, - extension='console') - - task.driver.console.validate(task) - - if enabled == node.console_enabled: - op = _('enabled') if enabled else _('disabled') - LOG.info(_LI("No console action was triggered because the " - "console is already %s"), op) - task.release_resources() - else: - node.last_error = None - node.save() - task.spawn_after(self._spawn_worker, - self._set_console_mode, task, enabled) - - def _set_console_mode(self, task, enabled): - """Internal method to set console mode on a node.""" - node = task.node - try: - if enabled: - task.driver.console.start_console(task) - # TODO(deva): We should be updating conductor_affinity here - # but there is no support for console sessions in - # take_over() right now. - else: - task.driver.console.stop_console(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - op = _('enabling') if enabled else _('disabling') - msg = (_('Error %(op)s the console on node %(node)s. ' - 'Reason: %(error)s') % {'op': op, - 'node': node.uuid, - 'error': e}) - node.last_error = msg - else: - node.console_enabled = enabled - node.last_error = None - finally: - node.save() - - @messaging.expected_exceptions(exception.NodeLocked, - exception.FailedToUpdateMacOnPort, - exception.MACAlreadyExists) - def update_port(self, context, port_obj): - """Update a port. - - :param context: request context. - :param port_obj: a changed (but not saved) port object. - :raises: DHCPLoadError if the dhcp_provider cannot be loaded. - :raises: FailedToUpdateMacOnPort if MAC address changed and update - failed. - :raises: MACAlreadyExists if the update is setting a MAC which is - registered on another port already. - """ - port_uuid = port_obj.uuid - LOG.debug("RPC update_port called for port %s.", port_uuid) - - with task_manager.acquire(context, port_obj.node_id) as task: - node = task.node - if 'address' in port_obj.obj_what_changed(): - vif = port_obj.extra.get('vif_port_id') - if vif: - api = dhcp_factory.DHCPFactory() - api.provider.update_port_address(vif, port_obj.address, - token=context.auth_token) - # Log warning if there is no vif_port_id and an instance - # is associated with the node. - elif node.instance_uuid: - LOG.warning(_LW( - "No VIF found for instance %(instance)s " - "port %(port)s when attempting to update port MAC " - "address."), - {'port': port_uuid, 'instance': node.instance_uuid}) - - port_obj.save() - - return port_obj - - @messaging.expected_exceptions(exception.DriverNotFound) - def get_driver_properties(self, context, driver_name): - """Get the properties of the driver. - - :param context: request context. - :param driver_name: name of the driver. - :returns: a dictionary with : - entries. - :raises: DriverNotFound if the driver is not loaded. - - """ - LOG.debug("RPC get_driver_properties called for driver %s.", - driver_name) - driver = self._get_driver(driver_name) - return driver.get_properties() - - @periodic_task.periodic_task( - spacing=CONF.conductor.send_sensor_data_interval) - def _send_sensor_data(self, context): - """Periodically sends sensor data to Ceilometer.""" - # do nothing if send_sensor_data option is False - if not CONF.conductor.send_sensor_data: - return - - filters = {'associated': True} - node_iter = self.iter_nodes(fields=['instance_uuid'], - filters=filters) - - for (node_uuid, driver, instance_uuid) in node_iter: - # populate the message which will be sent to ceilometer - message = {'message_id': uuidutils.generate_uuid(), - 'instance_uuid': instance_uuid, - 'node_uuid': node_uuid, - 'timestamp': datetime.datetime.utcnow(), - 'event_type': 'hardware.ipmi.metrics.update'} - - try: - with task_manager.acquire(context, - node_uuid, - shared=True) as task: - task.driver.management.validate(task) - sensors_data = task.driver.management.get_sensors_data( - task) - except NotImplementedError: - LOG.warn(_LW( - 'get_sensors_data is not implemented for driver' - ' %(driver)s, node_uuid is %(node)s'), - {'node': node_uuid, 'driver': driver}) - except exception.FailedToParseSensorData as fps: - LOG.warn(_LW( - "During get_sensors_data, could not parse " - "sensor data for node %(node)s. Error: %(err)s."), - {'node': node_uuid, 'err': str(fps)}) - except exception.FailedToGetSensorData as fgs: - LOG.warn(_LW( - "During get_sensors_data, could not get " - "sensor data for node %(node)s. Error: %(err)s."), - {'node': node_uuid, 'err': str(fgs)}) - except exception.NodeNotFound: - LOG.warn(_LW( - "During send_sensor_data, node %(node)s was not " - "found and presumed deleted by another process."), - {'node': node_uuid}) - except Exception as e: - LOG.warn(_LW( - "Failed to get sensor data for node %(node)s. " - "Error: %(error)s"), {'node': node_uuid, 'error': str(e)}) - else: - message['payload'] = ( - self._filter_out_unsupported_types(sensors_data)) - if message['payload']: - self.notifier.info(context, "hardware.ipmi.metrics", - message) - finally: - # Yield on every iteration - eventlet.sleep(0) - - def _filter_out_unsupported_types(self, sensors_data): - """Filters out sensor data types that aren't specified in the config. - - Removes sensor data types that aren't specified in - CONF.conductor.send_sensor_data_types. - - :param sensors_data: dict containing sensor types and the associated - data - :returns: dict with unsupported sensor types removed - """ - allowed = set(x.lower() for x in CONF.conductor.send_sensor_data_types) - - if 'all' in allowed: - return sensors_data - - return dict((sensor_type, sensor_value) for (sensor_type, sensor_value) - in sensors_data.items() if sensor_type.lower() in allowed) - - @messaging.expected_exceptions(exception.NodeLocked, - exception.UnsupportedDriverExtension, - exception.InvalidParameterValue, - exception.MissingParameterValue) - def set_boot_device(self, context, node_id, device, persistent=False): - """Set the boot device for a node. - - Set the boot device to use on next reboot of the node. - - :param context: request context. - :param node_id: node id or uuid. - :param device: the boot device, one of - :mod:`iotronic.common.boot_devices`. - :param persistent: Whether to set next-boot, or make the change - permanent. Default: False. - :raises: NodeLocked if node is locked by another conductor. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support management. - :raises: InvalidParameterValue when the wrong driver info is - specified or an invalid boot device is specified. - :raises: MissingParameterValue if missing supplied info. - """ - LOG.debug('RPC set_boot_device called for node %(node)s with ' - 'device %(device)s', {'node': node_id, 'device': device}) - with task_manager.acquire(context, node_id) as task: - node = task.node - if not getattr(task.driver, 'management', None): - raise exception.UnsupportedDriverExtension( - driver=node.driver, extension='management') - task.driver.management.validate(task) - task.driver.management.set_boot_device(task, device, - persistent=persistent) - - @messaging.expected_exceptions(exception.NodeLocked, - exception.UnsupportedDriverExtension, - exception.InvalidParameterValue, - exception.MissingParameterValue) - def get_boot_device(self, context, node_id): - """Get the current boot device. - - Returns the current boot device of a node. - - :param context: request context. - :param node_id: node id or uuid. - :raises: NodeLocked if node is locked by another conductor. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support management. - :raises: InvalidParameterValue when the wrong driver info is - specified. - :raises: MissingParameterValue if missing supplied info. - :returns: a dictionary containing: - - :boot_device: the boot device, one of - :mod:`iotronic.common.boot_devices` or None if it is unknown. - :persistent: Whether the boot device will persist to all - future boots or not, None if it is unknown. - - """ - LOG.debug('RPC get_boot_device called for node %s', node_id) - with task_manager.acquire(context, node_id) as task: - if not getattr(task.driver, 'management', None): - raise exception.UnsupportedDriverExtension( - driver=task.node.driver, extension='management') - task.driver.management.validate(task) - return task.driver.management.get_boot_device(task) - - @messaging.expected_exceptions(exception.NodeLocked, - exception.UnsupportedDriverExtension, - exception.InvalidParameterValue, - exception.MissingParameterValue) - def get_supported_boot_devices(self, context, node_id): - """Get the list of supported devices. - - Returns the list of supported boot devices of a node. - - :param context: request context. - :param node_id: node id or uuid. - :raises: NodeLocked if node is locked by another conductor. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support management. - :raises: InvalidParameterValue when the wrong driver info is - specified. - :raises: MissingParameterValue if missing supplied info. - :returns: A list with the supported boot devices defined - in :mod:`iotronic.common.boot_devices`. - - """ - LOG.debug('RPC get_supported_boot_devices called for node %s', node_id) - with task_manager.acquire(context, node_id, shared=True) as task: - if not getattr(task.driver, 'management', None): - raise exception.UnsupportedDriverExtension( - driver=task.node.driver, extension='management') - return task.driver.management.get_supported_boot_devices() - - @messaging.expected_exceptions(exception.NoFreeConductorWorker, - exception.NodeLocked, - exception.HardwareInspectionFailure, - exception.InvalidStateRequested, - exception.UnsupportedDriverExtension) - def inspect_hardware(self, context, node_id): - """Inspect hardware to obtain hardware properties. - - Initiate the inspection of a node. Validations are done - synchronously and the actual inspection work is performed in - background (asynchronously). - - :param context: request context. - :param node_id: node id or uuid. - :raises: NodeLocked if node is locked by another conductor. - :raises: UnsupportedDriverExtension if the node's driver doesn't - support inspect. - :raises: NoFreeConductorWorker when there is no free worker to start - async task - :raises: HardwareInspectionFailure when unable to get - essential scheduling properties from hardware. - :raises: InvalidStateRequested if 'inspect' is not a - valid action to do in the current state. - - """ - LOG.debug('RPC inspect_hardware called for node %s', node_id) - with task_manager.acquire(context, node_id, shared=False) as task: - if not getattr(task.driver, 'inspect', None): - raise exception.UnsupportedDriverExtension( - driver=task.node.driver, extension='inspect') - - try: - task.driver.power.validate(task) - task.driver.inspect.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue) as e: - error = (_("RPC inspect_hardware failed to validate " - "inspection or power info. Error: %(msg)s") - % {'msg': e}) - raise exception.HardwareInspectionFailure(error=error) - - try: - task.process_event('inspect', - callback=self._spawn_worker, - call_args=(_do_inspect_hardware, task), - err_handler=provisioning_error_handler) - - except exception.InvalidState: - raise exception.InvalidStateRequested( - action='inspect', node=task.node.uuid, - state=task.node.provision_state) - - @periodic_task.periodic_task( - spacing=CONF.conductor.check_provision_state_interval) - def _check_inspect_timeouts(self, context): - """Periodically checks inspect_timeout and fails upon reaching it. - - :param: context: request context - - """ - callback_timeout = CONF.conductor.inspect_timeout - if not callback_timeout: - return - - filters = {'reserved': False, - 'provision_state': states.INSPECTING, - 'inspection_started_before': callback_timeout} - sort_key = 'inspection_started_at' - last_error = _("timeout reached while inspecting the node") - self._fail_if_in_state(context, filters, states.INSPECTING, - sort_key, last_error=last_error) - - def _fail_if_in_state(self, context, filters, provision_state, - sort_key, callback_method=None, - err_handler=None, last_error=None): - """Fail nodes that are in specified state. - - Retrieves nodes that satisfy the criteria in 'filters'. - If any of these nodes is in 'provision_state', it has failed - in whatever provisioning activity it was currently doing. - That failure is processed here. - - :param: context: request context - :param: filters: criteria (as a dictionary) to get the desired - list of nodes that satisfy the filter constraints. - For example, if filters['provisioned_before'] = 60, - this would process nodes whose provision_updated_at - field value was 60 or more seconds before 'now'. - :param: provision_state: provision_state that the node is in, - for the provisioning activity to have failed. - :param: sort_key: the nodes are sorted based on this key. - :param: callback_method: the callback method to be invoked in a - spawned thread, for a failed node. This - method must take a :class:`TaskManager` as - the first (and only required) parameter. - :param: err_handler: for a failed node, the error handler to invoke - if an error occurs trying to spawn an thread - to do the callback_method. - :param: last_error: the error message to be updated in node.last_error - - """ - node_iter = self.iter_nodes(filters=filters, - sort_key=sort_key, - sort_dir='asc') - - workers_count = 0 - for node_uuid, driver in node_iter: - try: - with task_manager.acquire(context, node_uuid) as task: - if (task.node.maintenance or - task.node.provision_state != provision_state): - continue - - # timeout has been reached - process the event 'fail' - if callback_method: - task.process_event('fail', - callback=self._spawn_worker, - call_args=(callback_method, task), - err_handler=err_handler) - else: - task.node.last_error = last_error - task.process_event('fail') - except exception.NoFreeConductorWorker: - break - except (exception.NodeLocked, exception.NodeNotFound): - continue - workers_count += 1 - if workers_count >= CONF.conductor.periodic_max_workers: - break - - -def get_vendor_passthru_metadata(route_dict): - d = {} - for method, metadata in route_dict.items(): - # 'func' is the vendor method reference, ignore it - d[method] = {k: metadata[k] for k in metadata if k != 'func'} - return d - - -def power_state_error_handler(e, node, power_state): - """Set the node's power states if error occurs. - - This hook gets called upon an execption being raised when spawning - the worker thread to change the power state of a node. - - :param e: the exception object that was raised. - :param node: an Iotronic node object. - :param power_state: the power state to set on the node. - - """ - if isinstance(e, exception.NoFreeConductorWorker): - node.power_state = power_state - node.target_power_state = states.NOSTATE - node.last_error = (_("No free conductor workers available")) - node.save() - LOG.warning(_LW("No free conductor workers available to perform " - "an action on node %(node)s, setting node's " - "power state back to %(power_state)s."), - {'node': node.uuid, 'power_state': power_state}) - - -def provisioning_error_handler(e, node, provision_state, - target_provision_state): - """Set the node's provisioning states if error occurs. - - This hook gets called upon an exception being raised when spawning - the worker to do the deployment or tear down of a node. - - :param e: the exception object that was raised. - :param node: an Iotronic node object. - :param provision_state: the provision state to be set on - the node. - :param target_provision_state: the target provision state to be - set on the node. - - """ - if isinstance(e, exception.NoFreeConductorWorker): - # NOTE(deva): there is no need to clear conductor_affinity - # because it isn't updated on a failed deploy - node.provision_state = provision_state - node.target_provision_state = target_provision_state - node.last_error = (_("No free conductor workers available")) - node.save() - LOG.warning(_LW("No free conductor workers available to perform " - "an action on node %(node)s, setting node's " - "provision_state back to %(prov_state)s and " - "target_provision_state to %(tgt_prov_state)s."), - {'node': node.uuid, 'prov_state': provision_state, - 'tgt_prov_state': target_provision_state}) - - -def _get_configdrive_obj_name(node): - """Generate the object name for the config drive.""" - return 'configdrive-%s' % node.uuid - - -def _store_configdrive(node, configdrive): - """Handle the storage of the config drive. - - If configured, the config drive data are uploaded to Swift. The Node's - instance_info is updated to include either the temporary Swift URL - from the upload, or if no upload, the actual config drive data. - - :param node: an Iotronic node object. - :param configdrive: A gzipped and base64 encoded configdrive. - :raises: SwiftOperationError if an error occur when uploading the - config drive to Swift. - - """ - if CONF.conductor.configdrive_use_swift: - # NOTE(lucasagomes): No reason to use a different timeout than - # the one used for deploying the node - timeout = CONF.conductor.deploy_callback_timeout - container = CONF.conductor.configdrive_swift_container - object_name = _get_configdrive_obj_name(node) - - object_headers = {'X-Delete-After': timeout} - - with tempfile.NamedTemporaryFile() as fileobj: - fileobj.write(configdrive) - fileobj.flush() - - swift_api = swift.SwiftAPI() - swift_api.create_object(container, object_name, fileobj.name, - object_headers=object_headers) - configdrive = swift_api.get_temp_url(container, object_name, - timeout) - - i_info = node.instance_info - i_info['configdrive'] = configdrive - node.instance_info = i_info - - -def do_node_deploy(task, conductor_id, configdrive=None): - """Prepare the environment and deploy a node.""" - node = task.node - - def handle_failure(e, task, logmsg, errmsg): - # NOTE(deva): there is no need to clear conductor_affinity - task.process_event('fail') - args = {'node': task.node.uuid, 'err': e} - LOG.warning(logmsg, args) - node.last_error = errmsg % e - - try: - try: - if configdrive: - _store_configdrive(node, configdrive) - except exception.SwiftOperationError as e: - with excutils.save_and_reraise_exception(): - handle_failure( - e, task, - _LW('Error while uploading the configdrive for ' - '%(node)s to Swift'), - _('Failed to upload the configdrive to Swift. ' - 'Error: %s')) - - try: - task.driver.deploy.prepare(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - handle_failure( - e, task, - _LW('Error while preparing to deploy to node %(node)s: ' - '%(err)s'), - _("Failed to prepare to deploy. Error: %s")) - - try: - new_state = task.driver.deploy.deploy(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - handle_failure( - e, task, - _LW('Error in deploy of node %(node)s: %(err)s'), - _("Failed to deploy. Error: %s")) - - # Update conductor_affinity to reference this conductor's ID - # since there may be local persistent state - node.conductor_affinity = conductor_id - - # NOTE(deva): Some drivers may return states.DEPLOYWAIT - # eg. if they are waiting for a callback - if new_state == states.DEPLOYDONE: - task.process_event('done') - LOG.info(_LI('Successfully deployed node %(node)s with ' - 'instance %(instance)s.'), - {'node': node.uuid, 'instance': node.instance_uuid}) - elif new_state == states.DEPLOYWAIT: - task.process_event('wait') - else: - LOG.error(_LE('Unexpected state %(state)s returned while ' - 'deploying node %(node)s.'), - {'state': new_state, 'node': node.uuid}) - finally: - node.save() - - -def handle_sync_power_state_max_retries_exceeded(task, - actual_power_state): - """Handles power state sync exceeding the max retries. - - When synchronizing the power state between a node and the DB has exceeded - the maximum number of retries, change the DB power state to be the actual - node power state and place the node in maintenance. - - :param task: a TaskManager instance with an exclusive lock - :param actual_power_state: the actual power state of the node; a power - state from iotronic.common.states - """ - node = task.node - msg = (_("During sync_power_state, max retries exceeded " - "for node %(node)s, node state %(actual)s " - "does not match expected state '%(state)s'. " - "Updating DB state to '%(actual)s' " - "Switching node to maintenance mode.") % - {'node': node.uuid, 'actual': actual_power_state, - 'state': node.power_state}) - node.power_state = actual_power_state - node.last_error = msg - node.maintenance = True - node.maintenance_reason = msg - node.save() - LOG.error(msg) - - -def do_sync_power_state(task, count): - """Sync the power state for this node, incrementing the counter on failure. - - When the limit of power_state_sync_max_retries is reached, the node is put - into maintenance mode and the error recorded. - - :param task: a TaskManager instance with an exclusive lock - :param count: number of times this node has previously failed a sync - :returns: Count of failed attempts. - On success, the counter is set to 0. - On failure, the count is incremented by one - """ - node = task.node - power_state = None - count += 1 - - max_retries = CONF.conductor.power_state_sync_max_retries - # If power driver info can not be validated, and node has no prior state, - # do not attempt to sync the node's power state. - if node.power_state is None: - try: - task.driver.power.validate(task) - except (exception.InvalidParameterValue, - exception.MissingParameterValue): - return 0 - - try: - # The driver may raise an exception, or may return ERROR. - # Handle both the same way. - power_state = task.driver.power.get_power_state(task) - if power_state == states.ERROR: - raise exception.PowerStateFailure( - _("Power driver returned ERROR state " - "while trying to sync power state.")) - except Exception as e: - # Stop if any exception is raised when getting the power state - if count > max_retries: - handle_sync_power_state_max_retries_exceeded(task, power_state) - else: - LOG.warning(_LW("During sync_power_state, could not get power " - "state for node %(node)s, attempt %(attempt)s of " - "%(retries)s. Error: %(err)s."), - {'node': node.uuid, 'attempt': count, - 'retries': max_retries, 'err': e}) - return count - else: - # If node has no prior state AND we successfully got a state, - # simply record that. - if node.power_state is None: - LOG.info(_LI("During sync_power_state, node %(node)s has no " - "previous known state. Recording current state " - "'%(state)s'."), - {'node': node.uuid, 'state': power_state}) - node.power_state = power_state - node.save() - return 0 - - # If the node is now in the expected state, reset the counter - # otherwise, if we've exceeded the retry limit, stop here - if node.power_state == power_state: - return 0 - else: - if count > max_retries: - handle_sync_power_state_max_retries_exceeded(task, power_state) - return count - - if CONF.conductor.force_power_state_during_sync: - LOG.warning(_LW("During sync_power_state, node %(node)s state " - "'%(actual)s' does not match expected state. " - "Changing hardware state to '%(state)s'."), - {'node': node.uuid, 'actual': power_state, - 'state': node.power_state}) - try: - # node_power_action will update the node record - # so don't do that again here. - utils.node_power_action(task, node.power_state) - except Exception as e: - LOG.error(_LE( - "Failed to change power state of node %(node)s " - "to '%(state)s', attempt %(attempt)s of %(retries)s."), - {'node': node.uuid, - 'state': node.power_state, - 'attempt': count, - 'retries': max_retries}) - else: - LOG.warning(_LW("During sync_power_state, node %(node)s state " - "does not match expected state '%(state)s'. " - "Updating recorded state to '%(actual)s'."), - {'node': node.uuid, 'actual': power_state, - 'state': node.power_state}) - node.power_state = power_state - node.save() - - return count - - -def _do_inspect_hardware(task): - """Initiates inspection. - - :param: task: a TaskManager instance with an exclusive lock - on its node. - :raises: HardwareInspectionFailure if driver doesn't - return the state as states.MANAGEABLE or - states.INSPECTING. - - """ - node = task.node - - def handle_failure(e): - node.last_error = e - task.process_event('fail') - LOG.error(_LE("Failed to inspect node %(node)s: %(err)s"), - {'node': node.uuid, 'err': e}) - - try: - new_state = task.driver.inspect.inspect_hardware(task) - - except Exception as e: - with excutils.save_and_reraise_exception(): - error = str(e) - handle_failure(error) - - if new_state == states.MANAGEABLE: - task.process_event('done') - LOG.info(_LI('Successfully inspected node %(node)s') - % {'node': node.uuid}) - elif new_state != states.INSPECTING: - error = (_("During inspection, driver returned unexpected " - "state %(state)s") % {'state': new_state}) - handle_failure(error) - raise exception.HardwareInspectionFailure(error=error) - - -def cleaning_error_handler(task, msg, tear_down_cleaning=True): - """Put a failed node in CLEANFAIL or ZAPFAIL and maintenance.""" - # Reset clean step, msg should include current step - if task.node.provision_state == states.CLEANING: - task.node.clean_step = {} - task.node.last_error = msg - task.node.maintenance = True - task.node.maintenance_reason = msg - task.node.save() - if tear_down_cleaning: - try: - task.driver.deploy.tear_down_cleaning(task) - except Exception as e: - msg = (_LE('Failed to tear down cleaning on node %(uuid)s, ' - 'reason: %(err)s'), {'err': e, 'uuid': task.node.uuid}) - LOG.exception(msg) - - task.process_event('fail') - - -def _step_key(step): - """Sort by priority, then interface priority in event of tie. - - :param step: cleaning step dict to get priority for. - """ - return (step.get('priority'), - CLEANING_INTERFACE_PRIORITY[step.get('interface')]) - - -def _get_cleaning_steps(task, enabled=False): - """Get sorted cleaning steps for task.node - - :param task: A TaskManager object - :param enabled: If True, returns only enabled (priority > 0) steps. If - False, returns all clean steps. - :returns: A list of clean steps dictionaries, sorted with largest priority - as the first item - """ - # Iterate interfaces and get clean steps from each - steps = list() - for interface in CLEANING_INTERFACE_PRIORITY: - interface = getattr(task.driver, interface) - if interface: - interface_steps = [x for x in interface.get_clean_steps(task) - if not enabled or x['priority'] > 0] - steps.extend(interface_steps) - # Sort the steps from higher priority to lower priority - return sorted(steps, key=_step_key, reverse=True) - - -def set_node_cleaning_steps(task): - """Get the list of clean steps, save them to the node.""" - # Get the prioritized steps, store them. - node = task.node - driver_internal_info = node.driver_internal_info - driver_internal_info['clean_steps'] = _get_cleaning_steps(task, - enabled=True) - node.driver_internal_info = driver_internal_info - node.clean_step = {} - node.save() diff --git a/iotronic/conductor/__old/task_manager.py b/iotronic/conductor/__old/task_manager.py deleted file mode 100644 index 4e6724b..0000000 --- a/iotronic/conductor/__old/task_manager.py +++ /dev/null @@ -1,362 +0,0 @@ -# coding=utf-8 - -# Copyright 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. - -""" -A context manager to perform a series of tasks on a set of resources. - -:class:`TaskManager` is a context manager, created on-demand to allow -synchronized access to a node and its resources. - -The :class:`TaskManager` will, by default, acquire an exclusive lock on -a node for the duration that the TaskManager instance exists. You may -create a TaskManager instance without locking by passing "shared=True" -when creating it, but certain operations on the resources held by such -an instance of TaskManager will not be possible. Requiring this exclusive -lock guards against parallel operations interfering with each other. - -A shared lock is useful when performing non-interfering operations, -such as validating the driver interfaces. - -An exclusive lock is stored in the database to coordinate between -:class:`iotronic.iotconductor.manager` instances, that are typically deployed on -different hosts. - -:class:`TaskManager` methods, as well as driver methods, may be decorated to -determine whether their invocation requires an exclusive lock. - -The TaskManager instance exposes certain node resources and properties as -attributes that you may access: - - task.context - The context passed to TaskManager() - task.shared - False if Node is locked, True if it is not locked. (The - 'shared' kwarg arg of TaskManager()) - task.node - The Node object - task.ports - Ports belonging to the Node - task.driver - The Driver for the Node, or the Driver based on the - 'driver_name' kwarg of TaskManager(). - -Example usage: - -:: - - with task_manager.acquire(context, node_id) as task: - task.driver.power.power_on(task.node) - -If you need to execute task-requiring code in a background thread, the -TaskManager instance provides an interface to handle this for you, making -sure to release resources when the thread finishes (successfully or if -an exception occurs). Common use of this is within the Manager like so: - -:: - - with task_manager.acquire(context, node_id) as task: - - task.spawn_after(self._spawn_worker, - utils.node_power_action, task, new_state) - -All exceptions that occur in the current GreenThread as part of the -spawn handling are re-raised. You can specify a hook to execute custom -code when such exceptions occur. For example, the hook is a more elegant -solution than wrapping the "with task_manager.acquire()" with a -try..exception block. (Note that this hook does not handle exceptions -raised in the background thread.): - -:: - - def on_error(e): - if isinstance(e, Exception): - ... - - with task_manager.acquire(context, node_id) as task: - - task.set_spawn_error_hook(on_error) - task.spawn_after(self._spawn_worker, - utils.node_power_action, task, new_state) - -""" - -import functools - -from oslo_config import cfg -from oslo_log import log as logging -from oslo_utils import excutils -import retrying - -from iotronic.common import driver_factory -from iotronic.common import exception -from iotronic.common.i18n import _LW -from iotronic.common import states -from iotronic import objects - -LOG = logging.getLogger(__name__) - -CONF = cfg.CONF - - -def require_exclusive_lock(f): - """Decorator to require an exclusive lock. - - Decorated functions must take a :class:`TaskManager` as the first - parameter. Decorated class methods should take a :class:`TaskManager` - as the first parameter after "self". - - """ - @functools.wraps(f) - def wrapper(*args, **kwargs): - task = args[0] if isinstance(args[0], TaskManager) else args[1] - if task.shared: - raise exception.ExclusiveLockRequired() - return f(*args, **kwargs) - return wrapper - - -def acquire(context, node_id, shared=False, driver_name=None): - """Shortcut for acquiring a lock on a Node. - - :param context: Request context. - :param node_id: ID or UUID of node to lock. - :param shared: Boolean indicating whether to take a shared or exclusive - lock. Default: False. - :param driver_name: Name of Driver. Default: None. - :returns: An instance of :class:`TaskManager`. - - """ - return TaskManager(context, node_id, shared=shared, - driver_name=driver_name) - - -class TaskManager(object): - """Context manager for tasks. - - This class wraps the locking, driver loading, and acquisition - of related resources (eg, Node and Ports) when beginning a unit of work. - - """ - - def __init__(self, context, node_id, shared=False, driver_name=None): - """Create a new TaskManager. - - Acquire a lock on a node. The lock can be either shared or - exclusive. Shared locks may be used for read-only or - non-disruptive actions only, and must be considerate to what - other threads may be doing on the same node at the same time. - - :param context: request context - :param node_id: ID or UUID of node to lock. - :param shared: Boolean indicating whether to take a shared or exclusive - lock. Default: False. - :param driver_name: The name of the driver to load, if different - from the Node's current driver. - :raises: DriverNotFound - :raises: NodeNotFound - :raises: NodeLocked - - """ - - self._spawn_method = None - self._on_error_method = None - - self.context = context - self.node = None - self.shared = shared - - self.fsm = states.machine.copy() - - # NodeLocked exceptions can be annoying. Let's try to alleviate - # some of that pain by retrying our lock attempts. The retrying - # module expects a wait_fixed value in milliseconds. - @retrying.retry( - retry_on_exception=lambda e: isinstance(e, exception.NodeLocked), - stop_max_attempt_number=CONF.conductor.node_locked_retry_attempts, - wait_fixed=CONF.conductor.node_locked_retry_interval * 1000) - def reserve_node(): - LOG.debug("Attempting to reserve node %(node)s", - {'node': node_id}) - self.node = objects.Node.reserve(context, CONF.host, node_id) - - try: - if not self.shared: - reserve_node() - else: - self.node = objects.Node.get(context, node_id) - #self.ports = objects.Port.list_by_node_id(context, self.node.id) - #self.driver = driver_factory.get_driver(driver_name or - # self.node.driver) - - # NOTE(deva): this handles the Juno-era NOSTATE state - # and should be deleted after Kilo is released - ''' - if self.node.provision_state is states.NOSTATE: - self.node.provision_state = states.AVAILABLE - self.node.save() - - self.fsm.initialize(self.node.provision_state) - ''' - except Exception: - with excutils.save_and_reraise_exception(): - self.release_resources() - - def spawn_after(self, _spawn_method, *args, **kwargs): - """Call this to spawn a thread to complete the task. - - The specified method will be called when the TaskManager instance - exits. - - :param _spawn_method: a method that returns a GreenThread object - :param args: args passed to the method. - :param kwargs: additional kwargs passed to the method. - - """ - self._spawn_method = _spawn_method - self._spawn_args = args - self._spawn_kwargs = kwargs - - def set_spawn_error_hook(self, _on_error_method, *args, **kwargs): - """Create a hook to handle exceptions when spawning a task. - - Create a hook that gets called upon an exception being raised - from spawning a background thread to do a task. - - :param _on_error_method: a callable object, it's first parameter - should accept the Exception object that was raised. - :param args: additional args passed to the callable object. - :param kwargs: additional kwargs passed to the callable object. - - """ - self._on_error_method = _on_error_method - self._on_error_args = args - self._on_error_kwargs = kwargs - - def release_resources(self): - """Unlock a node and release resources. - - If an exclusive lock is held, unlock the node. Reset attributes - to make it clear that this instance of TaskManager should no - longer be accessed. - """ - - if not self.shared: - try: - if self.node: - objects.Node.release(self.context, CONF.host, self.node.id) - except exception.NodeNotFound: - # squelch the exception if the node was deleted - # within the task's context. - pass - self.node = None - self.driver = None - self.ports = None - self.fsm = None - - def _thread_release_resources(self, t): - """Thread.link() callback to release resources.""" - self.release_resources() - - def process_event(self, event, callback=None, call_args=None, - call_kwargs=None, err_handler=None): - """Process the given event for the task's current state. - - :param event: the name of the event to process - :param callback: optional callback to invoke upon event transition - :param call_args: optional \*args to pass to the callback method - :param call_kwargs: optional \**kwargs to pass to the callback method - :param err_handler: optional error handler to invoke if the - callback fails, eg. because there are no workers available - (err_handler should accept arguments node, prev_prov_state, and - prev_target_state) - :raises: InvalidState if the event is not allowed by the associated - state machine - """ - # Advance the state model for the given event. Note that this doesn't - # alter the node in any way. This may raise InvalidState, if this event - # is not allowed in the current state. - self.fsm.process_event(event) - - # stash current states in the error handler if callback is set, - # in case we fail to get a worker from the pool - if err_handler and callback: - self.set_spawn_error_hook(err_handler, self.node, - self.node.provision_state, - self.node.target_provision_state) - - self.node.provision_state = self.fsm.current_state - self.node.target_provision_state = self.fsm.target_state - - # set up the async worker - if callback: - # clear the error if we're going to start work in a callback - self.node.last_error = None - if call_args is None: - call_args = () - if call_kwargs is None: - call_kwargs = {} - self.spawn_after(callback, *call_args, **call_kwargs) - - # publish the state transition by saving the Node - self.node.save() - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type is None and self._spawn_method is not None: - # Spawn a worker to complete the task - # The linked callback below will be called whenever: - # - background task finished with no errors. - # - background task has crashed with exception. - # - callback was added after the background task has - # finished or crashed. While eventlet currently doesn't - # schedule the new thread until the current thread blocks - # for some reason, this is true. - # All of the above are asserted in tests such that we'll - # catch if eventlet ever changes this behavior. - thread = None - try: - thread = self._spawn_method(*self._spawn_args, - **self._spawn_kwargs) - - # NOTE(comstud): Trying to use a lambda here causes - # the callback to not occur for some reason. This - # also makes it easier to test. - thread.link(self._thread_release_resources) - # Don't unlock! The unlock will occur when the - # thread finshes. - return - except Exception as e: - with excutils.save_and_reraise_exception(): - try: - # Execute the on_error hook if set - if self._on_error_method: - self._on_error_method(e, *self._on_error_args, - **self._on_error_kwargs) - except Exception: - LOG.warning(_LW("Task's on_error hook failed to " - "call %(method)s on node %(node)s"), - {'method': self._on_error_method.__name__, - 'node': self.node.uuid}) - - if thread is not None: - # This means the link() failed for some - # reason. Nuke the thread. - thread.cancel() - self.release_resources() - self.release_resources() diff --git a/iotronic/conductor/__old/utils.py b/iotronic/conductor/__old/utils.py deleted file mode 100644 index 31a36ab..0000000 --- a/iotronic/conductor/__old/utils.py +++ /dev/null @@ -1,160 +0,0 @@ -# coding=utf-8 - -# 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_log import log -from oslo_utils import excutils - -from iotronic.common import exception -from iotronic.common.i18n import _ -from iotronic.common.i18n import _LI -from iotronic.common.i18n import _LW -from iotronic.common import states -from iotronic.conductor import task_manager - -LOG = log.getLogger(__name__) - - -@task_manager.require_exclusive_lock -def node_set_boot_device(task, device, persistent=False): - """Set the boot device for a node. - - :param task: a TaskManager instance. - :param device: Boot device. Values are vendor-specific. - :param persistent: Whether to set next-boot, or make the change - permanent. Default: False. - :raises: InvalidParameterValue if the validation of the - ManagementInterface fails. - - """ - if getattr(task.driver, 'management', None): - task.driver.management.validate(task) - task.driver.management.set_boot_device(task, - device=device, - persistent=persistent) - - -@task_manager.require_exclusive_lock -def node_power_action(task, new_state): - """Change power state or reset for a node. - - Perform the requested power action if the transition is required. - - :param task: a TaskManager instance containing the node to act on. - :param new_state: Any power state from iotronic.common.states. If the - state is 'REBOOT' then a reboot will be attempted, otherwise - the node power state is directly set to 'state'. - :raises: InvalidParameterValue when the wrong state is specified - or the wrong driver info is specified. - :raises: other exceptions by the node's power driver if something - wrong occurred during the power action. - - """ - node = task.node - target_state = states.POWER_ON if new_state == states.REBOOT else new_state - - if new_state != states.REBOOT: - try: - curr_state = task.driver.power.get_power_state(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - node['last_error'] = _( - "Failed to change power state to '%(target)s'. " - "Error: %(error)s") % {'target': new_state, 'error': e} - node['target_power_state'] = states.NOSTATE - node.save() - - if curr_state == new_state: - # Neither the iotronic service nor the hardware has erred. The - # node is, for some reason, already in the requested state, - # though we don't know why. eg, perhaps the user previously - # requested the node POWER_ON, the network delayed those IPMI - # packets, and they are trying again -- but the node finally - # responds to the first request, and so the second request - # gets to this check and stops. - # This isn't an error, so we'll clear last_error field - # (from previous operation), log a warning, and return. - node['last_error'] = None - # NOTE(dtantsur): under rare conditions we can get out of sync here - node['power_state'] = new_state - node['target_power_state'] = states.NOSTATE - node.save() - LOG.warn(_LW("Not going to change_node_power_state because " - "current state = requested state = '%(state)s'."), - {'state': curr_state}) - return - - if curr_state == states.ERROR: - # be optimistic and continue action - LOG.warn(_LW("Driver returns ERROR power state for node %s."), - node.uuid) - - # Set the target_power_state and clear any last_error, if we're - # starting a new operation. This will expose to other processes - # and clients that work is in progress. - if node['target_power_state'] != target_state: - node['target_power_state'] = target_state - node['last_error'] = None - node.save() - - # take power action - try: - if new_state != states.REBOOT: - task.driver.power.set_power_state(task, new_state) - else: - task.driver.power.reboot(task) - except Exception as e: - with excutils.save_and_reraise_exception(): - node['last_error'] = _( - "Failed to change power state to '%(target)s'. " - "Error: %(error)s") % {'target': target_state, 'error': e} - else: - # success! - node['power_state'] = target_state - LOG.info(_LI('Successfully set node %(node)s power state to ' - '%(state)s.'), - {'node': node.uuid, 'state': target_state}) - finally: - node['target_power_state'] = states.NOSTATE - node.save() - - -@task_manager.require_exclusive_lock -def cleanup_after_timeout(task): - """Cleanup deploy task after timeout. - - :param task: a TaskManager instance. - """ - node = task.node - msg = (_('Timeout reached while waiting for callback for node %s') - % node.uuid) - node.last_error = msg - LOG.error(msg) - node.save() - - error_msg = _('Cleanup failed for node %(node)s after deploy timeout: ' - ' %(error)s') - try: - task.driver.deploy.clean_up(task) - except exception.IotronicException as e: - msg = error_msg % {'node': node.uuid, 'error': e} - LOG.error(msg) - node.last_error = msg - node.save() - except Exception as e: - msg = error_msg % {'node': node.uuid, 'error': e} - LOG.error(msg) - node.last_error = _('Deploy timed out, but an unhandled exception was ' - 'encountered while aborting. More info may be ' - 'found in the log file.') - node.save() diff --git a/iotronic/conductor/manager.py b/iotronic/conductor/manager.py index 72a4d46..ff40883 100644 --- a/iotronic/conductor/manager.py +++ b/iotronic/conductor/manager.py @@ -1,5 +1,4 @@ # coding=utf-8 -from time import sleep # Copyright 2013 Hewlett-Packard Development Company, L.P. # Copyright 2013 International Business Machines Corporation @@ -18,8 +17,8 @@ from time import sleep # under the License. """Conduct all activity related to bare-metal deployments. -A single instance of :py:class:`iotronic.iotconductor.manager.ConductorManager` is -created within the *iotronic-conductor* process, and is responsible for +A single instance of :py:class:`iotronic.iotconductor.manager.ConductorManager` +is created within the *iotronic-conductor* process, and is responsible for performing all actions on bare metal resources (Chassis, Nodes, and Ports). Commands are received via RPCs. The conductor service also performs periodic tasks, eg. to monitor the status of active deployments. @@ -41,36 +40,38 @@ Rebalancing this ring can trigger various actions by each conductor, such as building or tearing down the TFTP environment for a node, notifying Neutron of a change, etc. """ -''' -import collections + import datetime +import eventlet +from eventlet import greenpool import inspect import tempfile +from iotronic.conductor import utils +from iotronic.db import api as dbapi from iotronic.common import dhcp_factory - - +from iotronic.common import exception from iotronic.common.glance_service import service_utils as glance_utils - +from iotronic.common import hash_ring as hash from iotronic.common import images -from iotronic.common import rpc + +from iotronic.common.i18n import _ +from iotronic.common.i18n import _LC +from iotronic.common.i18n import _LE +from iotronic.common.i18n import _LI +from iotronic.common.i18n import _LW +# from iotronic.common import driver_factory + + from iotronic.common import states from iotronic.common import swift -from iotronic.iotconductor import task_manager -from iotronic.iotconductor import utils - +from iotronic.conductor import task_manager +from iotronic import objects from iotronic.openstack.common import periodic_task -''' -import threading -import eventlet -from eventlet import greenpool -from iotronic.db import api as dbapi -from oslo_config import cfg -from oslo_db import exception as db_exception from oslo_concurrency import lockutils from oslo_config import cfg from oslo_db import exception as db_exception @@ -78,21 +79,20 @@ from oslo_log import log import oslo_messaging as messaging from oslo_utils import excutils from oslo_utils import uuidutils -from iotronic.conductor import utils -from iotronic import objects -from iotronic.common import hash_ring as hash -from iotronic.common.i18n import _ -from iotronic.common.i18n import _LC -from iotronic.common.i18n import _LE -from iotronic.common.i18n import _LI -from iotronic.common.i18n import _LW -#from iotronic.common import driver_factory +import threading -from iotronic.conductor import task_manager +''' +import collections + +from iotronic.common import rpc from iotronic.common import states + +from iotronic.iotconductor import task_manager +from iotronic.iotconductor import utils + from iotronic.openstack.common import periodic_task -from iotronic.common import exception +''' MANAGER_TOPIC = 'iotronic.conductor_manager' WORKER_SPAWN_lOCK = "conductor_worker_spawn" @@ -602,10 +602,9 @@ def set_node_cleaning_steps(task): node.driver_internal_info = driver_internal_info node.clean_step = {} node.save() - - - -##################### NEW + + +# NEW class ConductorManager(periodic_task.PeriodicTasks): @@ -623,8 +622,8 @@ class ConductorManager(periodic_task.PeriodicTasks): self.host = host self.topic = topic self.drivers = ['fake'] - #self.power_state_sync_count = collections.defaultdict(int) - #self.notifier = rpc.get_notifier() + # self.power_state_sync_count = collections.defaultdict(int) + # self.notifier = rpc.get_notifier() ''' def _get_driver(self, driver_name): """Get the driver. @@ -640,6 +639,7 @@ class ConductorManager(periodic_task.PeriodicTasks): except KeyError: raise exception.DriverNotFound(driver_name=driver_name) ''' + def init_host(self): self.dbapi = dbapi.get_instance() @@ -655,10 +655,10 @@ class ConductorManager(periodic_task.PeriodicTasks): # NOTE(deva): instantiating DriverFactory may raise DriverLoadError # or DriverNotFound - #self._driver_factory = driver_factory.DriverFactory() - #"""Driver factory loads all enabled drivers.""" - - #self.drivers = self._driver_factory.names + # self._driver_factory = driver_factory.DriverFactory() + # """Driver factory loads all enabled drivers.""" + + # self.drivers = self._driver_factory.names """List of driver names which this conductor supports.""" ''' if not self.drivers: @@ -678,20 +678,21 @@ class ConductorManager(periodic_task.PeriodicTasks): if iface: self._collect_periodic_tasks(iface) ''' - + # clear all locks held by this conductor before registering - #self.dbapi.clear_node_reservations_for_conductor(self.host) + # self.dbapi.clear_node_reservations_for_conductor(self.host) try: # Register this conductor with the cluster - cdr = self.dbapi.register_conductor({'hostname': self.host,'drivers': ['fake']}) + cdr = self.dbapi.register_conductor( + {'hostname': self.host, 'drivers': ['fake']}) except exception.ConductorAlreadyRegistered: # This conductor was already registered and did not shut down # properly, so log a warning and update the record. LOG.warn(_LW("A conductor with hostname %(hostname)s " "was previously registered. Updating registration"), {'hostname': self.host}) - - #TO BE CHANGED + + # TO BE CHANGED cdr = self.dbapi.register_conductor({'hostname': self.host, 'drivers': self.drivers}, update_existing=True) @@ -707,11 +708,9 @@ class ConductorManager(periodic_task.PeriodicTasks): with excutils.save_and_reraise_exception(): LOG.critical(_LC('Failed to start keepalive')) self.del_host() - - from iotronic.wamp.rpcwampserver import RPC_Wamp_Server - RPC_Wamp_Server() - + # from iotronic.wamp.rpcwampserver import RPC_Wamp_Server + # RPC_Wamp_Server() def _collect_periodic_tasks(self, obj): for n, method in inspect.getmembers(obj, inspect.ismethod): @@ -746,7 +745,6 @@ class ConductorManager(periodic_task.PeriodicTasks): @lockutils.synchronized(WORKER_SPAWN_lOCK, 'iotronic-') def _spawn_worker(self, func, *args, **kwargs): - """Create a greenthread to run func(*args, **kwargs). Spawns a greenthread if there are free slots in pool, otherwise raises @@ -1434,7 +1432,7 @@ class ConductorManager(periodic_task.PeriodicTasks): action=action, node=task.node.uuid, state=task.node.provision_state) - #@periodic_task.periodic_task(spacing=CONF.conductor.sync_power_state_interval) + # @periodic_task.periodic_task(spacing=CONF.conductor.sync_power_state_interval) def _sync_power_states(self, context): """Periodic task to sync power states for the nodes. @@ -1502,7 +1500,7 @@ class ConductorManager(periodic_task.PeriodicTasks): # Yield on every iteration eventlet.sleep(0) - #@periodic_task.periodic_task(spacing=CONF.conductor.check_provision_state_interval) + # @periodic_task.periodic_task(spacing=CONF.conductor.check_provision_state_interval) def _check_deploy_timeouts(self, context): """Periodically checks whether a deploy RPC call has timed out. @@ -1542,7 +1540,7 @@ class ConductorManager(periodic_task.PeriodicTasks): task.node.conductor_affinity = self.conductor.id task.node.save() - #@periodic_task.periodic_task(spacing=CONF.conductor.sync_local_state_interval) + # @periodic_task.periodic_task(spacing=CONF.conductor.sync_local_state_interval) def _sync_local_state(self, context): """Perform any actions necessary to sync local state. @@ -1616,7 +1614,7 @@ class ConductorManager(periodic_task.PeriodicTasks): nodes :return: generator yielding tuples of requested fields """ - columns = ['uuid',] + list(fields or ()) + columns = ['uuid', ] + list(fields or ()) node_list = self.dbapi.get_nodeinfo_list(columns=columns, **kwargs) for result in node_list: if self._mapped_to_this_conductor(*result[:2]): @@ -1667,9 +1665,6 @@ class ConductorManager(periodic_task.PeriodicTasks): ret_dict[iface_name]['reason'] = reason return ret_dict - - - @messaging.expected_exceptions(exception.NodeLocked, exception.NodeAssociated, exception.InvalidState) @@ -1691,7 +1686,7 @@ class ConductorManager(periodic_task.PeriodicTasks): node.destroy() LOG.info(_LI('Successfully deleted node %(node)s.'), {'node': node.uuid}) - #if node.instance_uuid is not None: + # if node.instance_uuid is not None: # raise exception.NodeAssociated(node=node.uuid, # instance=node.instance_uuid) @@ -1904,7 +1899,7 @@ class ConductorManager(periodic_task.PeriodicTasks): driver = self._get_driver(driver_name) return driver.get_properties() - #@periodic_task.periodic_task(spacing=CONF.conductor.send_sensor_data_interval) + # @periodic_task.periodic_task(spacing=CONF.conductor.send_sensor_data_interval) def _send_sensor_data(self, context): """Periodically sends sensor data to Ceilometer.""" # do nothing if send_sensor_data option is False @@ -2128,7 +2123,7 @@ class ConductorManager(periodic_task.PeriodicTasks): action='inspect', node=task.node.uuid, state=task.node.provision_state) - #@periodic_task.periodic_task(spacing=CONF.conductor.check_provision_state_interval) + # @periodic_task.periodic_task(spacing=CONF.conductor.check_provision_state_interval) def _check_inspect_timeouts(self, context): """Periodically checks inspect_timeout and fails upon reaching it. @@ -2185,7 +2180,7 @@ class ConductorManager(periodic_task.PeriodicTasks): try: with task_manager.acquire(context, node_uuid) as task: if (task.node.maintenance or - task.node.provision_state != provision_state): + task.node.provision_state != provision_state): continue # timeout has been reached - process the event 'fail' diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py index 3f3864f..986877f 100644 --- a/iotronic/conductor/rpcapi.py +++ b/iotronic/conductor/rpcapi.py @@ -22,9 +22,7 @@ import random import oslo_messaging as messaging -from iotronic.common import exception from iotronic.common import hash_ring -from iotronic.common.i18n import _ from iotronic.common import rpc from iotronic.conductor import manager from iotronic.objects import base as objects_base @@ -78,9 +76,7 @@ class ConductorAPI(object): 'driver %s.') % node.driver) raise exception.NoValidHost(reason=reason) ''' - - pass - + def get_topic_for_driver(self, driver_name): """Get RPC topic name for a conductor supporting the given driver. diff --git a/iotronic/conductor/task_manager.py b/iotronic/conductor/task_manager.py index 1f31f3d..1149a3c 100644 --- a/iotronic/conductor/task_manager.py +++ b/iotronic/conductor/task_manager.py @@ -32,8 +32,8 @@ A shared lock is useful when performing non-interfering operations, such as validating the driver interfaces. An exclusive lock is stored in the database to coordinate between -:class:`iotronic.iotconductor.manager` instances, that are typically deployed on -different hosts. +:class:`iotronic.iotconductor.manager` instances, +that are typically deployed on different hosts. :class:`TaskManager` methods, as well as driver methods, may be decorated to determine whether their invocation requires an exclusive lock. @@ -101,7 +101,6 @@ from oslo_log import log as logging from oslo_utils import excutils import retrying -from iotronic.common import driver_factory from iotronic.common import exception from iotronic.common.i18n import _LW from iotronic.common import states @@ -176,7 +175,7 @@ class TaskManager(object): self._on_error_method = None self.context = context - #self.node = None + # self.node = None self.node = None self.shared = shared @@ -201,8 +200,8 @@ class TaskManager(object): else: """ self.node = objects.Node.get(context, node_id) - #self.ports = objects.Port.list_by_node_id(context, self.node.id) - #self.driver = driver_factory.get_driver(driver_name or + # self.ports = objects.Port.list_by_node_id(context, self.node.id) + # self.driver = driver_factory.get_driver(driver_name or # self.node.driver) # NOTE(deva): this handles the Juno-era NOSTATE state @@ -256,7 +255,7 @@ class TaskManager(object): to make it clear that this instance of TaskManager should no longer be accessed. """ - pass #don't need it at the moment + pass # don't need it at the moment """ if not self.shared: try: @@ -357,7 +356,7 @@ class TaskManager(object): LOG.warning(_LW("Task's on_error hook failed to " "call %(method)s on node %(node)s"), {'method': self._on_error_method.__name__, - 'node': self.node.uuid}) + 'node': self.node.uuid}) if thread is not None: # This means the link() failed for some diff --git a/iotronic/db/api.py b/iotronic/db/api.py index 739b89b..a55249e 100644 --- a/iotronic/db/api.py +++ b/iotronic/db/api.py @@ -177,7 +177,7 @@ class Connection(object): :returns: A node. """ ''' - + @abc.abstractmethod def get_node_by_code(self, instance): """Return a node. @@ -343,7 +343,7 @@ class Connection(object): :param chassis_id: The id or the uuid of a chassis. """ ''' - + @abc.abstractmethod def register_conductor(self, values, update_existing=False): """Register an active conductor with the cluster. @@ -405,9 +405,7 @@ class Connection(object): """ - - -###################### NEW ############################# +# ##################### NEW ############################# @abc.abstractmethod def create_session(self, values): @@ -415,7 +413,7 @@ class Connection(object): :param values: session_id. """ - + @abc.abstractmethod def update_session(self, session_id, values): """Update properties of an session. @@ -423,15 +421,15 @@ class Connection(object): :param session_id: The id of a session. :param values: Dict of values to update. :returns: A session. - """ - + """ + @abc.abstractmethod def create_location(self, values): """Create a new location. :param values: Dict of values. - """ - + """ + @abc.abstractmethod def update_location(self, location_id, values): """Update properties of an location. @@ -440,8 +438,7 @@ class Connection(object): :param values: Dict of values to update. :returns: A location. """ - - + @abc.abstractmethod def destroy_location(self, location_id): """Destroy an location. @@ -451,7 +448,7 @@ class Connection(object): @abc.abstractmethod def get_locations_by_node_id(self, node_id, limit=None, marker=None, - sort_key=None, sort_dir=None): + sort_key=None, sort_dir=None): """List all the locations for a given node. :param node_id: The integer node ID. diff --git a/iotronic/db/sqlalchemy/alembic/versions/21b331f883ef_add_provision_updated_at.py b/iotronic/db/sqlalchemy/alembic/versions/21b331f883ef_add_provision_updated_at.py index 12eb08f..6ec38a0 100644 --- a/iotronic/db/sqlalchemy/alembic/versions/21b331f883ef_add_provision_updated_at.py +++ b/iotronic/db/sqlalchemy/alembic/versions/21b331f883ef_add_provision_updated_at.py @@ -28,7 +28,7 @@ import sqlalchemy as sa def upgrade(): op.add_column('nodes', sa.Column('provision_updated_at', sa.DateTime(), - nullable=True)) + nullable=True)) def downgrade(): diff --git a/iotronic/db/sqlalchemy/alembic/versions/2581ebaf0cb2_initial_migration.py b/iotronic/db/sqlalchemy/alembic/versions/2581ebaf0cb2_initial_migration.py index 868b8db..3d2e41f 100644 --- a/iotronic/db/sqlalchemy/alembic/versions/2581ebaf0cb2_initial_migration.py +++ b/iotronic/db/sqlalchemy/alembic/versions/2581ebaf0cb2_initial_migration.py @@ -103,4 +103,4 @@ def upgrade(): def downgrade(): raise NotImplementedError(('Downgrade from initial migration is' - ' unsupported.')) + ' unsupported.')) diff --git a/iotronic/db/sqlalchemy/alembic/versions/3ae36a5f5131_add_logical_name.py b/iotronic/db/sqlalchemy/alembic/versions/3ae36a5f5131_add_logical_name.py index 58d58bd..af862d5 100644 --- a/iotronic/db/sqlalchemy/alembic/versions/3ae36a5f5131_add_logical_name.py +++ b/iotronic/db/sqlalchemy/alembic/versions/3ae36a5f5131_add_logical_name.py @@ -28,7 +28,7 @@ import sqlalchemy as sa def upgrade(): op.add_column('nodes', sa.Column('name', sa.String(length=63), - nullable=True)) + nullable=True)) op.create_unique_constraint('uniq_nodes0name', 'nodes', ['name']) diff --git a/iotronic/db/sqlalchemy/alembic/versions/4f399b21ae71_add_node_clean_step.py b/iotronic/db/sqlalchemy/alembic/versions/4f399b21ae71_add_node_clean_step.py index a8c5798..083aaa0 100644 --- a/iotronic/db/sqlalchemy/alembic/versions/4f399b21ae71_add_node_clean_step.py +++ b/iotronic/db/sqlalchemy/alembic/versions/4f399b21ae71_add_node_clean_step.py @@ -28,7 +28,7 @@ import sqlalchemy as sa def upgrade(): op.add_column('nodes', sa.Column('clean_step', sa.Text(), - nullable=True)) + nullable=True)) def downgrade(): diff --git a/iotronic/db/sqlalchemy/alembic/versions/5674c57409b9_replace_nostate_with_available.py b/iotronic/db/sqlalchemy/alembic/versions/5674c57409b9_replace_nostate_with_available.py index 28f5987..b71b7e8 100644 --- a/iotronic/db/sqlalchemy/alembic/versions/5674c57409b9_replace_nostate_with_available.py +++ b/iotronic/db/sqlalchemy/alembic/versions/5674c57409b9_replace_nostate_with_available.py @@ -41,7 +41,7 @@ AVAILABLE = 'available' def upgrade(): op.execute( node.update().where( - node.c.provision_state == None).values( + node.c.provision_state is None).values( {'provision_state': op.inline_literal(AVAILABLE)})) diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py index cc3e7b0..b39b988 100644 --- a/iotronic/db/sqlalchemy/api.py +++ b/iotronic/db/sqlalchemy/api.py @@ -149,8 +149,9 @@ def _paginate_query(model, limit=None, marker=None, sort_key=None, % {'key': sort_key}) return query.all() -#### NEW - +# NEW + + def add_location_filter_by_node(query, value): if strutils.is_int_like(value): return query.filter_by(node_id=value) @@ -159,6 +160,7 @@ def add_location_filter_by_node(query, value): models.Location.node_id == models.Node.id) return query.filter(models.Node.uuid == value) + class Connection(api.Connection): """SqlAlchemy connection.""" @@ -176,9 +178,9 @@ class Connection(api.Connection): query = query.filter_by(chassis_id=chassis_obj.id) if 'associated' in filters: if filters['associated']: - query = query.filter(models.Node.instance_uuid != None) + query = query.filter(models.Node.instance_uuid is not None) else: - query = query.filter(models.Node.instance_uuid == None) + query = query.filter(models.Node.instance_uuid is None) """ if 'reserved' in filters: if filters['reserved']: @@ -264,12 +266,13 @@ class Connection(api.Connection): except NoResultFound: raise exception.NodeNotFound(node_id) """ + def create_node(self, values): # ensure defaults are present for new nodes if 'uuid' not in values: values['uuid'] = uuidutils.generate_uuid() if 'status' not in values: - values['status'] = states.OPERATIVE + values['status'] = states.OPERATIVE node = models.Node() node.update(values) @@ -301,7 +304,7 @@ class Connection(api.Connection): return query.one() except NoResultFound: raise exception.NodeNotFound(node=node_name) - + def get_node_by_code(self, node_code): query = model_query(models.Node).filter_by(code=node_code) try: @@ -323,8 +326,9 @@ class Connection(api.Connection): return result ''' + def destroy_node(self, node_id): - + session = get_session() with session.begin(): query = model_query(models.Node, session=session) @@ -338,9 +342,10 @@ class Connection(api.Connection): # required for deleting all ports, attached to the node. if uuidutils.is_uuid_like(node_id): node_id = node_ref['id'] - + location_query = model_query(models.Location, session=session) - location_query = add_location_filter_by_node(location_query, node_id) + location_query = add_location_filter_by_node( + location_query, node_id) location_query.delete() query.delete() @@ -366,7 +371,7 @@ class Connection(api.Connection): query.delete() """ - + def update_node(self, node_id, values): # NOTE(dtantsur): this can lead to very strange errors if 'uuid' in values: @@ -560,6 +565,7 @@ class Connection(api.Connection): if count != 1: raise exception.ChassisNotFound(chassis=chassis_id) """ + def register_conductor(self, values, update_existing=False): session = get_session() with session.begin(): @@ -641,8 +647,7 @@ class Connection(api.Connection): return d2c - -###################### NEW ############################# +# ##################### NEW ############################# def create_session(self, values): session = models.SessionWP() session.update(values) @@ -667,7 +672,7 @@ class Connection(api.Connection): location.update(values) location.save() return location - + def update_location(self, location_id, values): # NOTE(dtantsur): this can lead to very strange errors session = get_session() @@ -680,7 +685,7 @@ class Connection(api.Connection): except NoResultFound: raise exception.LocationNotFound(location=location_id) return ref - + def destroy_location(self, location_id): session = get_session() with session.begin(): @@ -689,24 +694,27 @@ class Connection(api.Connection): count = query.delete() if count == 0: raise exception.LocationNotFound(location=location_id) - + def get_locations_by_node_id(self, node_id, limit=None, marker=None, - sort_key=None, sort_dir=None): + sort_key=None, sort_dir=None): query = model_query(models.Location) query = query.filter_by(node_id=node_id) return _paginate_query(models.Location, limit, marker, sort_key, sort_dir, query) - + def get_session_by_node_uuid(self, node_uuid, valid): - query = model_query(models.SessionWP).filter_by(node_uuid=node_uuid).filter_by(valid=valid) + query = model_query( + models.SessionWP).filter_by( + node_uuid=node_uuid).filter_by( + valid=valid) try: return query.one() except NoResultFound: return None - + def get_session_by_session_id(self, session_id): query = model_query(models.SessionWP).filter_by(session_id=session_id) try: return query.one() except NoResultFound: - return None \ No newline at end of file + return None diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py index 7db313b..f417bd3 100644 --- a/iotronic/db/sqlalchemy/models.py +++ b/iotronic/db/sqlalchemy/models.py @@ -24,12 +24,13 @@ from oslo_config import cfg from oslo_db import options as db_options from oslo_db.sqlalchemy import models import six.moves.urllib.parse as urlparse -from sqlalchemy import Boolean, Column, DateTime +from sqlalchemy import Boolean +from sqlalchemy import Column from sqlalchemy import ForeignKey, Integer -from sqlalchemy import schema, String, Text from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import schema +from sqlalchemy import String from sqlalchemy.types import TypeDecorator, TEXT - from iotronic.common import paths @@ -39,7 +40,8 @@ sql_opts = [ help='MySQL engine to use.') ] -_DEFAULT_SQL_CONNECTION = 'sqlite:///' + paths.state_path_def('iotronic.sqlite') +_DEFAULT_SQL_CONNECTION = 'sqlite:///' + \ + paths.state_path_def('iotronic.sqlite') cfg.CONF.register_opts(sql_opts, 'database') @@ -89,7 +91,7 @@ class JSONEncodedList(JsonEncodedType): class IotronicBase(models.TimestampMixin, - models.ModelBase): + models.ModelBase): metadata = None @@ -142,7 +144,7 @@ class Node(Base): """Represents a Node.""" __tablename__ = 'nodes' - + __table_args__ = ( schema.UniqueConstraint('uuid', name='uniq_nodes0uuid'), schema.UniqueConstraint('code', name='uniq_nodes0code'), @@ -155,8 +157,7 @@ class Node(Base): device = Column(String(255)) session = Column(String(255), nullable=True) mobile = Column(Boolean, default=False) - #location = Column(JSONEncodedDict) - extra = Column(JSONEncodedDict) + extra = Column(JSONEncodedDict) """ __tablename__ = 'nodes' ''' @@ -212,6 +213,7 @@ class Node(Base): #extra = Column(JSONEncodedDict) """ + class Location(Base): """Represents a location of a node.""" @@ -224,13 +226,18 @@ class Location(Base): altitude = Column(String(18), nullable=True) node_id = Column(Integer, ForeignKey('nodes.id')) + class SessionWP(Base): """Represents a session of a node.""" __tablename__ = 'sessions' __table_args__ = ( - schema.UniqueConstraint('session_id', name='uniq_session_id0session_id'), - schema.UniqueConstraint('node_uuid', name='uniq_node_uuid0node_uuid'), + schema.UniqueConstraint( + 'session_id', + name='uniq_session_id0session_id'), + schema.UniqueConstraint( + 'node_uuid', + name='uniq_node_uuid0node_uuid'), table_args()) id = Column(Integer, primary_key=True) valid = Column(Boolean, default=True) @@ -238,6 +245,7 @@ class SessionWP(Base): node_uuid = Column(String(36)) node_id = Column(Integer, ForeignKey('nodes.id')) + class Port(Base): """Represents a network port of a bare metal node.""" @@ -251,4 +259,3 @@ class Port(Base): address = Column(String(18)) node_id = Column(Integer, ForeignKey('nodes.id'), nullable=True) extra = Column(JSONEncodedDict) - diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py index 1e66db4..921d1ae 100644 --- a/iotronic/objects/__init__.py +++ b/iotronic/objects/__init__.py @@ -12,26 +12,26 @@ # License for the specific language governing permissions and limitations # under the License. -#from iotronic.objects import chassis +# from iotronic.objects import chassis from iotronic.objects import conductor -from iotronic.objects import node from iotronic.objects import location +from iotronic.objects import node from iotronic.objects import sessionwp -#from iotronic.objects import port +# from iotronic.objects import port -#Chassis = chassis.Chassis +# Chassis = chassis.Chassis Conductor = conductor.Conductor Node = node.Node Location = location.Location -SessionWP=sessionwp.SessionWP -#Port = port.Port +SessionWP = sessionwp.SessionWP +# Port = port.Port __all__ = ( - #Chassis, - Conductor, - Node, - Location, - SessionWP, - #Port - ) + # Chassis, + Conductor, + Node, + Location, + SessionWP, + # Port +) diff --git a/iotronic/objects/__old/chassis.py b/iotronic/objects/__old/chassis.py deleted file mode 100644 index a131e49..0000000 --- a/iotronic/objects/__old/chassis.py +++ /dev/null @@ -1,186 +0,0 @@ -# coding=utf-8 -# -# -# 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_utils import strutils -from oslo_utils import uuidutils - -from iotronic.common import exception -from iotronic.db import api as dbapi -from iotronic.objects import base -from iotronic.objects import utils as obj_utils - - -class Chassis(base.IotronicObject): - # Version 1.0: Initial version - # Version 1.1: Add get() and get_by_id() and make get_by_uuid() - # only work with a uuid - # Version 1.2: Add create() and destroy() - # Version 1.3: Add list() - VERSION = '1.3' - - dbapi = dbapi.get_instance() - - fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'extra': obj_utils.dict_or_none, - 'description': obj_utils.str_or_none, - } - - @staticmethod - def _from_db_object(chassis, db_chassis): - """Converts a database entity to a formal :class:`Chassis` object. - - :param chassis: An object of :class:`Chassis`. - :param db_chassis: A DB model of a chassis. - :return: a :class:`Chassis` object. - """ - for field in chassis.fields: - chassis[field] = db_chassis[field] - - chassis.obj_reset_changes() - return chassis - - @base.remotable_classmethod - def get(cls, context, chassis_id): - """Find a chassis based on its id or uuid and return a Chassis object. - - :param chassis_id: the id *or* uuid of a chassis. - :returns: a :class:`Chassis` object. - """ - if strutils.is_int_like(chassis_id): - return cls.get_by_id(context, chassis_id) - elif uuidutils.is_uuid_like(chassis_id): - return cls.get_by_uuid(context, chassis_id) - else: - raise exception.InvalidIdentity(identity=chassis_id) - - @base.remotable_classmethod - def get_by_id(cls, context, chassis_id): - """Find a chassis based on its integer id and return a Chassis object. - - :param chassis_id: the id of a chassis. - :returns: a :class:`Chassis` object. - """ - db_chassis = cls.dbapi.get_chassis_by_id(chassis_id) - chassis = Chassis._from_db_object(cls(context), db_chassis) - return chassis - - @base.remotable_classmethod - def get_by_uuid(cls, context, uuid): - """Find a chassis based on uuid and return a :class:`Chassis` object. - - :param uuid: the uuid of a chassis. - :param context: Security context - :returns: a :class:`Chassis` object. - """ - db_chassis = cls.dbapi.get_chassis_by_uuid(uuid) - chassis = Chassis._from_db_object(cls(context), db_chassis) - return chassis - - @base.remotable_classmethod - def list(cls, context, limit=None, marker=None, - sort_key=None, sort_dir=None): - """Return a list of Chassis objects. - - :param context: Security context. - :param limit: maximum number of resources to return in a single result. - :param marker: pagination marker for large data sets. - :param sort_key: column to sort results by. - :param sort_dir: direction to sort. "asc" or "desc". - :returns: a list of :class:`Chassis` object. - - """ - db_chassis = cls.dbapi.get_chassis_list(limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) - return [Chassis._from_db_object(cls(context), obj) - for obj in db_chassis] - - @base.remotable - def create(self, context=None): - """Create a Chassis record in the DB. - - Column-wise updates will be made based on the result of - self.what_changed(). If target_power_state is provided, - it will be checked against the in-database copy of the - chassis before updates are made. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Chassis(context) - - """ - values = self.obj_get_changes() - db_chassis = self.dbapi.create_chassis(values) - self._from_db_object(self, db_chassis) - - @base.remotable - def destroy(self, context=None): - """Delete the Chassis from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Chassis(context) - """ - self.dbapi.destroy_chassis(self.uuid) - self.obj_reset_changes() - - @base.remotable - def save(self, context=None): - """Save updates to this Chassis. - - Updates will be made column by column based on the result - of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Chassis(context) - """ - updates = self.obj_get_changes() - self.dbapi.update_chassis(self.uuid, updates) - - self.obj_reset_changes() - - @base.remotable - def refresh(self, context=None): - """Loads and applies updates for this Chassis. - - Loads a :class:`Chassis` with the same uuid from the database and - checks for updated attributes. Updates are applied from - the loaded chassis column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Chassis(context) - """ - current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] diff --git a/iotronic/objects/__old/node.py b/iotronic/objects/__old/node.py deleted file mode 100644 index bb857a8..0000000 --- a/iotronic/objects/__old/node.py +++ /dev/null @@ -1,272 +0,0 @@ -# coding=utf-8 -# -# -# 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_utils import strutils -from oslo_utils import uuidutils - -from iotronic.common import exception -from iotronic.db import api as db_api -from iotronic.objects import base -from iotronic.objects import utils as obj_utils - - -class Node(base.IotronicObject): - # Version 1.0: Initial version - VERSION = '1.0' - - dbapi = db_api.get_instance() - - fields = { - 'id': int, - - 'uuid': obj_utils.str_or_none, - 'name': obj_utils.str_or_none, - 'status': obj_utils.str_or_none, - #'chassis_id': obj_utils.int_or_none, - #'instance_uuid': obj_utils.str_or_none, - - #'driver': obj_utils.str_or_none, - #'driver_info': obj_utils.dict_or_none, - #'driver_internal_info': obj_utils.dict_or_none, - - # A clean step dictionary, indicating the current clean step - # being executed, or None, indicating cleaning is not in progress - # or has not yet started. - #'clean_step': obj_utils.dict_or_none, - - #'instance_info': obj_utils.dict_or_none, - #'properties': obj_utils.dict_or_none, - 'reservation': obj_utils.str_or_none, - # a reference to the id of the conductor service, not its hostname, - # that has most recently performed some action which could require - # local state to be maintained (eg, built a PXE config) - #'conductor_affinity': obj_utils.int_or_none, - - # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR - #'power_state': obj_utils.str_or_none, - - # Set to one of states.POWER_ON|POWER_OFF when a power operation - # starts, and set to NOSTATE when the operation finishes - # (successfully or unsuccessfully). - #'target_power_state': obj_utils.str_or_none, - - #'provision_state': obj_utils.str_or_none, - #'provision_updated_at': obj_utils.datetime_or_str_or_none, - #'target_provision_state': obj_utils.str_or_none, - - #'maintenance': bool, - #'maintenance_reason': obj_utils.str_or_none, - #'console_enabled': bool, - - # Any error from the most recent (last) asynchronous transaction - # that started but failed to finish. - #'last_error': obj_utils.str_or_none, - - #'inspection_finished_at': obj_utils.datetime_or_str_or_none, - #'inspection_started_at': obj_utils.datetime_or_str_or_none, - - #'extra': obj_utils.dict_or_none, - } - - @staticmethod - def _from_db_object(node, db_node): - """Converts a database entity to a formal object.""" - for field in node.fields: - node[field] = db_node[field] - node.obj_reset_changes() - return node - - @base.remotable_classmethod - def get(cls, context, node_id): - """Find a node based on its id or uuid and return a Node object. - - :param node_id: the id *or* uuid of a node. - :returns: a :class:`Node` object. - """ - if strutils.is_int_like(node_id): - return cls.get_by_id(context, node_id) - elif uuidutils.is_uuid_like(node_id): - return cls.get_by_uuid(context, node_id) - else: - raise exception.InvalidIdentity(identity=node_id) - - @base.remotable_classmethod - def get_by_id(cls, context, node_id): - """Find a node based on its integer id and return a Node object. - - :param node_id: the id of a node. - :returns: a :class:`Node` object. - """ - db_node = cls.dbapi.get_node_by_id(node_id) - node = Node._from_db_object(cls(context), db_node) - return node - - @base.remotable_classmethod - def get_by_uuid(cls, context, uuid): - """Find a node based on uuid and return a Node object. - - :param uuid: the uuid of a node. - :returns: a :class:`Node` object. - """ - db_node = cls.dbapi.get_node_by_uuid(uuid) - node = Node._from_db_object(cls(context), db_node) - return node - - @base.remotable_classmethod - def get_by_name(cls, context, name): - """Find a node based on name and return a Node object. - - :param name: the logical name of a node. - :returns: a :class:`Node` object. - """ - db_node = cls.dbapi.get_node_by_name(name) - node = Node._from_db_object(cls(context), db_node) - return node - - @base.remotable_classmethod - def get_by_instance_uuid(cls, context, instance_uuid): - """Find a node based on the instance uuid and return a Node object. - - :param uuid: the uuid of the instance. - :returns: a :class:`Node` object. - """ - db_node = cls.dbapi.get_node_by_instance(instance_uuid) - node = Node._from_db_object(cls(context), db_node) - return node - - @base.remotable_classmethod - def list(cls, context, limit=None, marker=None, sort_key=None, - sort_dir=None, filters=None): - """Return a list of Node objects. - - :param context: Security context. - :param limit: maximum number of resources to return in a single result. - :param marker: pagination marker for large data sets. - :param sort_key: column to sort results by. - :param sort_dir: direction to sort. "asc" or "desc". - :param filters: Filters to apply. - :returns: a list of :class:`Node` object. - - """ - db_nodes = cls.dbapi.get_node_list(filters=filters, limit=limit, - marker=marker, sort_key=sort_key, - sort_dir=sort_dir) - return [Node._from_db_object(cls(context), obj) for obj in db_nodes] - - @base.remotable_classmethod - def reserve(cls, context, tag, node_id): - """Get and reserve a node. - - To prevent other ManagerServices from manipulating the given - Node while a Task is performed, mark it reserved by this host. - - :param context: Security context. - :param tag: A string uniquely identifying the reservation holder. - :param node_id: A node id or uuid. - :raises: NodeNotFound if the node is not found. - :returns: a :class:`Node` object. - - """ - db_node = cls.dbapi.reserve_node(tag, node_id) - node = Node._from_db_object(cls(context), db_node) - return node - - @base.remotable_classmethod - def release(cls, context, tag, node_id): - """Release the reservation on a node. - - :param context: Security context. - :param tag: A string uniquely identifying the reservation holder. - :param node_id: A node id or uuid. - :raises: NodeNotFound if the node is not found. - - """ - cls.dbapi.release_node(tag, node_id) - - @base.remotable - def create(self, context=None): - """Create a Node record in the DB. - - Column-wise updates will be made based on the result of - self.what_changed(). If target_power_state is provided, - it will be checked against the in-database copy of the - node before updates are made. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Node(context) - - """ - values = self.obj_get_changes() - db_node = self.dbapi.create_node(values) - self._from_db_object(self, db_node) - - @base.remotable - def destroy(self, context=None): - """Delete the Node from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Node(context) - """ - self.dbapi.destroy_node(self.uuid) - self.obj_reset_changes() - - @base.remotable - def save(self, context=None): - """Save updates to this Node. - - Column-wise updates will be made based on the result of - self.what_changed(). If target_power_state is provided, - it will be checked against the in-database copy of the - node before updates are made. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Node(context) - """ - updates = self.obj_get_changes() - if 'driver' in updates and 'driver_internal_info' not in updates: - # Clean driver_internal_info when changes driver - self.driver_internal_info = {} - updates = self.obj_get_changes() - self.dbapi.update_node(self.uuid, updates) - self.obj_reset_changes() - - @base.remotable - def refresh(self, context=None): - """Refresh the object by re-fetching from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Node(context) - """ - current = self.__class__.get_by_uuid(self._context, self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] diff --git a/iotronic/objects/__old/port.py b/iotronic/objects/__old/port.py deleted file mode 100644 index 4b00374..0000000 --- a/iotronic/objects/__old/port.py +++ /dev/null @@ -1,217 +0,0 @@ -# coding=utf-8 -# -# -# 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_utils import strutils -from oslo_utils import uuidutils - -from iotronic.common import exception -from iotronic.common import utils -from iotronic.db import api as dbapi -from iotronic.objects import base -from iotronic.objects import utils as obj_utils - - -class Port(base.IotronicObject): - # Version 1.0: Initial version - # Version 1.1: Add get() and get_by_id() and get_by_address() and - # make get_by_uuid() only work with a uuid - # Version 1.2: Add create() and destroy() - # Version 1.3: Add list() - # Version 1.4: Add list_by_node_id() - VERSION = '1.4' - - dbapi = dbapi.get_instance() - - fields = { - 'id': int, - 'uuid': obj_utils.str_or_none, - 'node_id': obj_utils.int_or_none, - 'address': obj_utils.str_or_none, - 'extra': obj_utils.dict_or_none, - } - - @staticmethod - def _from_db_object(port, db_port): - """Converts a database entity to a formal object.""" - for field in port.fields: - port[field] = db_port[field] - - port.obj_reset_changes() - return port - - @staticmethod - def _from_db_object_list(db_objects, cls, context): - """Converts a list of database entities to a list of formal objects.""" - return [Port._from_db_object(cls(context), obj) for obj in db_objects] - - @base.remotable_classmethod - def get(cls, context, port_id): - """Find a port based on its id or uuid and return a Port object. - - :param port_id: the id *or* uuid of a port. - :returns: a :class:`Port` object. - """ - if strutils.is_int_like(port_id): - return cls.get_by_id(context, port_id) - elif uuidutils.is_uuid_like(port_id): - return cls.get_by_uuid(context, port_id) - elif utils.is_valid_mac(port_id): - return cls.get_by_address(context, port_id) - else: - raise exception.InvalidIdentity(identity=port_id) - - @base.remotable_classmethod - def get_by_id(cls, context, port_id): - """Find a port based on its integer id and return a Port object. - - :param port_id: the id of a port. - :returns: a :class:`Port` object. - """ - db_port = cls.dbapi.get_port_by_id(port_id) - port = Port._from_db_object(cls(context), db_port) - return port - - @base.remotable_classmethod - def get_by_uuid(cls, context, uuid): - """Find a port based on uuid and return a :class:`Port` object. - - :param uuid: the uuid of a port. - :param context: Security context - :returns: a :class:`Port` object. - """ - db_port = cls.dbapi.get_port_by_uuid(uuid) - port = Port._from_db_object(cls(context), db_port) - return port - - @base.remotable_classmethod - def get_by_address(cls, context, address): - """Find a port based on address and return a :class:`Port` object. - - :param address: the address of a port. - :param context: Security context - :returns: a :class:`Port` object. - """ - db_port = cls.dbapi.get_port_by_address(address) - port = Port._from_db_object(cls(context), db_port) - return port - - @base.remotable_classmethod - def list(cls, context, limit=None, marker=None, - sort_key=None, sort_dir=None): - """Return a list of Port objects. - - :param context: Security context. - :param limit: maximum number of resources to return in a single result. - :param marker: pagination marker for large data sets. - :param sort_key: column to sort results by. - :param sort_dir: direction to sort. "asc" or "desc". - :returns: a list of :class:`Port` object. - - """ - db_ports = cls.dbapi.get_port_list(limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) - return Port._from_db_object_list(db_ports, cls, context) - - @base.remotable_classmethod - def list_by_node_id(cls, context, node_id, limit=None, marker=None, - sort_key=None, sort_dir=None): - """Return a list of Port objects associated with a given node ID. - - :param context: Security context. - :param node_id: the ID of the node. - :param limit: maximum number of resources to return in a single result. - :param marker: pagination marker for large data sets. - :param sort_key: column to sort results by. - :param sort_dir: direction to sort. "asc" or "desc". - :returns: a list of :class:`Port` object. - - """ - db_ports = cls.dbapi.get_ports_by_node_id(node_id, limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) - return Port._from_db_object_list(db_ports, cls, context) - - @base.remotable - def create(self, context=None): - """Create a Port record in the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Port(context) - - """ - values = self.obj_get_changes() - db_port = self.dbapi.create_port(values) - self._from_db_object(self, db_port) - - @base.remotable - def destroy(self, context=None): - """Delete the Port from the DB. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Port(context) - """ - self.dbapi.destroy_port(self.uuid) - self.obj_reset_changes() - - @base.remotable - def save(self, context=None): - """Save updates to this Port. - - Updates will be made column by column based on the result - of self.what_changed(). - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Port(context) - """ - updates = self.obj_get_changes() - self.dbapi.update_port(self.uuid, updates) - - self.obj_reset_changes() - - @base.remotable - def refresh(self, context=None): - """Loads updates for this Port. - - Loads a port with the same uuid from the database and - checks for updated attributes. Updates are applied from - the loaded port column by column, if there are any updates. - - :param context: Security context. NOTE: This should only - be used internally by the indirection_api. - Unfortunately, RPC requires context as the first - argument, even though we don't use it. - A context should be set when instantiating the - object, e.g.: Port(context) - """ - current = self.__class__.get_by_uuid(self._context, uuid=self.uuid) - for field in self.fields: - if (hasattr(self, base.get_attrname(field)) and - self[field] != current[field]): - self[field] = current[field] diff --git a/iotronic/objects/base.py b/iotronic/objects/base.py index fe6d58a..3804bb1 100644 --- a/iotronic/objects/base.py +++ b/iotronic/objects/base.py @@ -120,6 +120,7 @@ def remotable_classmethod(fn): # "orphaned" and remotable methods cannot be called. def remotable(fn): """Decorator for remotable object methods.""" + def wrapper(self, *args, **kwargs): ctxt = self._context try: @@ -514,7 +515,7 @@ class ObjectListBase(object): objects = [] for entity in value: obj = IotronicObject.obj_from_primitive(entity, - context=self._context) + context=self._context) objects.append(obj) return objects @@ -539,9 +540,10 @@ class IotronicObjectSerializer(messaging.NoOpSerializer): """A IotronicObject-aware Serializer. This implements the Oslo Serializer interface and provides the - ability to serialize and deserialize IotronicObject entities. Any service - that needs to accept or return IotronicObjects as arguments or result values - should pass this to its RpcProxy and RpcDispatcher objects. + ability to serialize and deserialize IotronicObject entities. + Any service that needs to accept or return IotronicObjects as + arguments or result values should pass this to its RpcProxy + and RpcDispatcher objects. """ def _process_iterable(self, context, action_fn, values): @@ -582,8 +584,8 @@ class IotronicObjectSerializer(messaging.NoOpSerializer): def obj_to_primitive(obj): """Recursively turn an object into a python primitive. - An IotronicObject becomes a dict, and anything that implements ObjectListBase - becomes a list. + An IotronicObject becomes a dict, and anything that implements + ObjectListBase becomes a list. """ if isinstance(obj, ObjectListBase): return [obj_to_primitive(x) for x in obj] diff --git a/iotronic/objects/location.py b/iotronic/objects/location.py index 70b761d..c3d8729 100644 --- a/iotronic/objects/location.py +++ b/iotronic/objects/location.py @@ -17,7 +17,6 @@ from oslo_utils import strutils from oslo_utils import uuidutils from iotronic.common import exception -from iotronic.common import utils from iotronic.db import api as dbapi from iotronic.objects import base from iotronic.objects import utils as obj_utils @@ -48,7 +47,10 @@ class Location(base.IotronicObject): @staticmethod def _from_db_object_list(db_objects, cls, context): """Converts a list of database entities to a list of formal objects.""" - return [Location._from_db_object(cls(context), obj) for obj in db_objects] + return [ + Location._from_db_object( + cls(context), + obj) for obj in db_objects] @base.remotable_classmethod def get(cls, context, location_id): @@ -101,9 +103,9 @@ class Location(base.IotronicObject): """ db_locations = cls.dbapi.get_location_list(limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) return Location._from_db_object_list(db_locations, cls, context) @base.remotable_classmethod @@ -121,13 +123,13 @@ class Location(base.IotronicObject): """ db_locations = cls.dbapi.get_locations_by_node_id(node_id, limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) return Location._from_db_object_list(db_locations, cls, context) @base.remotable - def create(self,context=None): + def create(self, context=None): """Create a Location record in the DB. :param context: Security context. NOTE: This should only diff --git a/iotronic/objects/node.py b/iotronic/objects/node.py index 86afe28..a6e4524 100644 --- a/iotronic/objects/node.py +++ b/iotronic/objects/node.py @@ -104,8 +104,8 @@ class Node(base.IotronicObject): """ db_node = cls.dbapi.get_node_by_name(name) node = Node._from_db_object(cls(context), db_node) - return node - + return node + @base.remotable_classmethod def get_by_instance_uuid(cls, context, instance_uuid): """Find a node based on the instance uuid and return a Node object. diff --git a/iotronic/objects/sessionwp.py b/iotronic/objects/sessionwp.py index 684c470..b69f6fe 100644 --- a/iotronic/objects/sessionwp.py +++ b/iotronic/objects/sessionwp.py @@ -17,7 +17,6 @@ from oslo_utils import strutils from oslo_utils import uuidutils from iotronic.common import exception -from iotronic.common import utils from iotronic.db import api as dbapi from iotronic.objects import base from iotronic.objects import utils as obj_utils @@ -48,7 +47,10 @@ class SessionWP(base.IotronicObject): @staticmethod def _from_db_object_list(db_objects, cls, context): """Converts a list of database entities to a list of formal objects.""" - return [SessionWP._from_db_object(cls(context), obj) for obj in db_objects] + return [ + SessionWP._from_db_object( + cls(context), + obj) for obj in db_objects] @base.remotable_classmethod def get(cls, context, session_id): @@ -74,7 +76,7 @@ class SessionWP(base.IotronicObject): db_session = cls.dbapi.get_session_by_id(ses_id) session = SessionWP._from_db_object(cls(context), db_session) return session - + @base.remotable_classmethod def get_by_session_id(cls, context, session_id): """Find a session based on its integer id and return a SessionWP object. @@ -87,17 +89,17 @@ class SessionWP(base.IotronicObject): return session @base.remotable_classmethod - def get_session_by_node_uuid(cls,node_uuid,valid=True, context=None ): + def get_session_by_node_uuid(cls, node_uuid, valid=True, context=None): """Find a session based on uuid and return a :class:`SessionWP` object. :param node_uuid: the uuid of a node. :param context: Security context :returns: a :class:`SessionWP` object. """ - db_session = cls.dbapi.get_session_by_node_uuid(node_uuid,valid) + db_session = cls.dbapi.get_session_by_node_uuid(node_uuid, valid) session = SessionWP._from_db_object(cls(context), db_session) - return session - + return session + @base.remotable_classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None): @@ -112,9 +114,9 @@ class SessionWP(base.IotronicObject): """ db_sessions = cls.dbapi.get_session_list(limit=limit, - marker=marker, - sort_key=sort_key, - sort_dir=sort_dir) + marker=marker, + sort_key=sort_key, + sort_dir=sort_dir) return SessionWP._from_db_object_list(db_sessions, cls, context) ''' @@ -138,9 +140,9 @@ class SessionWP(base.IotronicObject): sort_dir=sort_dir) return SessionWP._from_db_object_list(db_sessions, cls, context) ''' - + @base.remotable - def create(self,context=None): + def create(self, context=None): """Create a SessionWP record in the DB. :param context: Security context. NOTE: This should only diff --git a/iotronic/objects/utils.py b/iotronic/objects/utils.py index 9d21c64..bc57c3d 100644 --- a/iotronic/objects/utils.py +++ b/iotronic/objects/utils.py @@ -109,6 +109,7 @@ def nested_object_or_none(objclass): def dt_serializer(name): """Return a datetime serializer for a named attribute.""" + def serializer(self, name=name): if getattr(self, name) is not None: return timeutils.isotime(getattr(self, name)) diff --git a/iotronic/openstack/common/eventlet_backdoor.py b/iotronic/openstack/common/eventlet_backdoor.py index 381b861..3c80291 100644 --- a/iotronic/openstack/common/eventlet_backdoor.py +++ b/iotronic/openstack/common/eventlet_backdoor.py @@ -56,6 +56,7 @@ def list_opts(): class EventletBackdoorConfigValueError(Exception): + def __init__(self, port_range, help_msg, ex): msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. ' '%(help)s' % @@ -108,7 +109,7 @@ def _listen(host, start_port, end_port, listen_func): return listen_func((host, try_port)) except socket.error as exc: if (exc.errno != errno.EADDRINUSE or - try_port >= end_port): + try_port >= end_port): raise try_port += 1 diff --git a/iotronic/openstack/common/loopingcall.py b/iotronic/openstack/common/loopingcall.py index 5e9d3b0..fa09d18 100644 --- a/iotronic/openstack/common/loopingcall.py +++ b/iotronic/openstack/common/loopingcall.py @@ -29,6 +29,7 @@ LOG = logging.getLogger(__name__) # NOTE(zyluo): This lambda function was declared to avoid mocking collisions # with time.time() called in the standard logging module # during unittests. + _ts = lambda: time.time() @@ -50,6 +51,7 @@ class LoopingCallDone(Exception): class LoopingCallBase(object): + def __init__(self, f=None, *args, **kw): self.args = args self.kw = kw diff --git a/iotronic/openstack/common/periodic_task.py b/iotronic/openstack/common/periodic_task.py index 31474a8..77b4ef8 100644 --- a/iotronic/openstack/common/periodic_task.py +++ b/iotronic/openstack/common/periodic_task.py @@ -107,6 +107,7 @@ def periodic_task(*args, **kwargs): class _PeriodicTasksMeta(type): + def _add_periodic_task(cls, task): """Add a periodic task to the list of periodic tasks. @@ -183,6 +184,7 @@ def _nearest_boundary(last_run, spacing): @six.add_metaclass(_PeriodicTasksMeta) class PeriodicTasks(object): + def __init__(self): super(PeriodicTasks, self).__init__() self._periodic_last_run = {} diff --git a/iotronic/openstack/common/service.py b/iotronic/openstack/common/service.py index 7615496..88fbb50 100644 --- a/iotronic/openstack/common/service.py +++ b/iotronic/openstack/common/service.py @@ -143,12 +143,14 @@ class Launcher(object): 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 _set_signals_handler(signal.SIG_DFL) @@ -191,6 +193,7 @@ class ServiceLauncher(Launcher): class ServiceWrapper(object): + def __init__(self, service, workers): self.service = service self.workers = workers diff --git a/iotronic/openstack/common/threadgroup.py b/iotronic/openstack/common/threadgroup.py index f36f279..8de29bb 100644 --- a/iotronic/openstack/common/threadgroup.py +++ b/iotronic/openstack/common/threadgroup.py @@ -36,6 +36,7 @@ class Thread(object): :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) @@ -57,6 +58,7 @@ class ThreadGroup(object): 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 = [] diff --git a/iotronic/tests/__init__.py b/iotronic/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic/tests/base.py b/iotronic/tests/base.py new file mode 100644 index 0000000..1c30cdb --- /dev/null +++ b/iotronic/tests/base.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +# Copyright 2010-2011 OpenStack Foundation +# Copyright (c) 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. + +from oslotest import base + + +class TestCase(base.BaseTestCase): + + """Test case base class for all unit tests.""" diff --git a/iotronic/tests/test_iotronic.py b/iotronic/tests/test_iotronic.py new file mode 100644 index 0000000..d4e03b0 --- /dev/null +++ b/iotronic/tests/test_iotronic.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- + +# 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. + +""" +test_iotronic +---------------------------------- + +Tests for `iotronic` module. +""" + +from iotronic.tests import base + + +class TestIotronic(base.TestCase): + + def test_something(self): + pass diff --git a/iotronic/wamp/clientwamp.py b/iotronic/wamp/clientwamp.py index cad86c9..4c80e38 100644 --- a/iotronic/wamp/clientwamp.py +++ b/iotronic/wamp/clientwamp.py @@ -1,82 +1,108 @@ +# 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 autobahn.twisted.util import sleep from autobahn.twisted.wamp import ApplicationRunner from autobahn.twisted.wamp import ApplicationSession -from twisted.internet.defer import inlineCallbacks import multiprocessing -from autobahn.twisted.util import sleep +from twisted.internet.defer import inlineCallbacks + + +msg_queue = None -msg_queue=None - class Publisher(ApplicationSession): def onJoin(self, details): print("Publisher session ready") - + + class Subscriber(ApplicationSession): + @inlineCallbacks def onJoin(self, details): print("Subscriber session ready") self.topic_reader = self.config.extra['topic'] def manage_msg(*args): - print args + print (args) try: yield self.subscribe(manage_msg, self.topic_reader) print("subscribed to topic") except Exception as e: print("could not subscribe to topic: {0}".format(e)) - + global msg_queue while True: if not msg_queue.empty(): - msg=msg_queue.get() + msg = msg_queue.get() self.publish(msg['topic'], msg['message']) yield sleep(0.01) - -class PublisherClient: - def __init__(self,ip,port,realm): - self.ip=unicode(ip) - self.port=unicode(port) - self.realm=unicode(realm) - self._url = "ws://"+self.ip+":"+self.port+"/ws" - self.runner = ApplicationRunner(url=unicode(self._url), realm=self.realm, - #debug=True, debug_wamp=True, debug_app=True - ) + + +class PublisherClient(object): + + def __init__(self, ip, port, realm): + self.ip = unicode(ip) + self.port = unicode(port) + self.realm = unicode(realm) + self._url = "ws://" + self.ip + ":" + self.port + "/ws" + self.runner = ApplicationRunner( + url=unicode(self._url), + realm=self.realm, + # debug=True, debug_wamp=True, + # debug_app=True + ) def start(self): # Pass start_reactor=False to all runner.run() calls self.runner.run(Publisher, start_reactor=False) - -class SubscriberClient: - def __init__(self,ip,port,realm,topic): - self.ip=unicode(ip) - self.port=unicode(port) - self.realm=unicode(realm) - self.topic=unicode(topic) - self._url = "ws://"+self.ip+":"+self.port+"/ws" - self.runner = ApplicationRunner(url=unicode(self._url), realm=self.realm, extra={'topic':self.topic} - #debug=True, debug_wamp=True, debug_app=True - ) - + + +class SubscriberClient(object): + + def __init__(self, ip, port, realm, topic): + self.ip = unicode(ip) + self.port = unicode(port) + self.realm = unicode(realm) + self.topic = unicode(topic) + self._url = "ws://" + self.ip + ":" + self.port + "/ws" + self.runner = ApplicationRunner( + url=unicode(self._url), + realm=self.realm, + # debug=True, debug_wamp=True, + # debug_app=True + ) + def start(self): # Pass start_reactor=False to all runner.run() calls - self.runner.run(Subscriber, start_reactor=False) + self.runner.run(Subscriber, start_reactor=False) -class ClientWamp: - - def __init__(self,ip,port,realm,topic='board.connection'): - server = SubscriberClient(ip,port,realm,topic) - sendMessage = PublisherClient(ip,port,realm) + +class ClientWamp(object): + + def __init__(self, ip, port, realm, topic='board.connection'): + server = SubscriberClient(ip, port, realm, topic) + sendMessage = PublisherClient(ip, port, realm) server.start() sendMessage.start() - + from twisted.internet import reactor global msg_queue msg_queue = multiprocessing.Queue() multi = multiprocessing.Process(target=reactor.run, args=()) multi.start() - - def send(self,topic,msg): - full_msg={'topic':unicode(topic),'message':unicode(msg)} - msg_queue.put(full_msg) \ No newline at end of file + + def send(self, topic, msg): + full_msg = {'topic': unicode(topic), 'message': unicode(msg)} + msg_queue.put(full_msg) diff --git a/iotronic/wamp/functions.py b/iotronic/wamp/functions.py index 7f5967a..df1b13d 100644 --- a/iotronic/wamp/functions.py +++ b/iotronic/wamp/functions.py @@ -1,19 +1,30 @@ -from iotronic import objects -from oslo_utils import uuidutils -import pecan -from oslo_log import log +# 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 iotronic.common import exception +from iotronic import objects +from oslo_log import log LOG = log.getLogger(__name__) + def leave_function(session_id): - LOG.debug('Node with %s disconnectd',session_id) + LOG.debug('Node with %s disconnectd', session_id) try: - old_session=objects.SessionWP({}).get_by_session_id({},session_id) - old_session.valid=False + old_session = objects.SessionWP({}).get_by_session_id({}, session_id) + old_session.valid = False old_session.save() LOG.debug('Session %s deleted', session_id) - except: + except Exception: LOG.debug('Error in deleting session %s', session_id) @@ -21,25 +32,31 @@ def test(): LOG.debug('hello') return u'hello!' -def registration(code_node,session_num): - LOG.debug('Receved registration from %s with session %s',code_node, session_num) - response='' + +def registration(code_node, session_num): + LOG.debug( + 'Receved registration from %s with session %s', + code_node, + session_num) + response = '' try: node = objects.Node.get_by_code({}, code_node) - except: + except Exception: response = exception.NodeNotFound(node=code_node) try: - old_session=objects.SessionWP({}).get_session_by_node_uuid(node.uuid,valid=True) - old_session.valid=False + old_session = objects.SessionWP( + {}).get_session_by_node_uuid( + node.uuid, valid=True) + old_session.valid = False old_session.save() - except: + except Exception: LOG.debug('valid session for %s Not found', node.uuid) - - session=objects.SessionWP({}) - session.node_id=node.id - session.node_uuid=node.uuid - session.session_id=session_num + + session = objects.SessionWP({}) + session.node_id = node.id + session.node_uuid = node.uuid + session.session_id = session_num session.create() session.save() - - return unicode(response) \ No newline at end of file + + return unicode(response) diff --git a/iotronic/wamp/rpcwampserver.py b/iotronic/wamp/rpcwampserver.py index fc704d3..5dd0ba9 100644 --- a/iotronic/wamp/rpcwampserver.py +++ b/iotronic/wamp/rpcwampserver.py @@ -1,32 +1,42 @@ -from twisted.internet.defer import inlineCallbacks -from autobahn.twisted.wamp import ApplicationSession, ApplicationRunner +# 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 autobahn.twisted.wamp import ApplicationRunner +from autobahn.twisted.wamp import ApplicationSession import multiprocessing -from twisted.internet import reactor -from oslo_log import log from oslo_config import cfg +from oslo_log import log +from twisted.internet.defer import inlineCallbacks +from twisted.internet import reactor + LOG = log.getLogger(__name__) wamp_opts = [ - cfg.StrOpt('wamp_ip', - default='127.0.0.1', - help=('URL of wamp broker')), - cfg.IntOpt('wamp_port', - default=8181, - help='port wamp broker'), - cfg.StrOpt('wamp_realm', - default='s4t', - help=('realm broker')), + cfg.StrOpt('wamp_ip', default='127.0.0.1', help='URL of wamp broker'), + cfg.IntOpt('wamp_port', default=8181, help='port wamp broker'), + cfg.StrOpt('wamp_realm', default='s4t', help='realm broker') ] + CONF = cfg.CONF CONF.register_opts(wamp_opts, 'wamp') + class RPCWampManager(ApplicationSession): - + def __init__(self, config=None): ApplicationSession.__init__(self, config) LOG.info("RPC wamp manager created") - + ''' #unused methods def onConnect(self): @@ -40,11 +50,11 @@ class RPCWampManager(ApplicationSession): print("session left") import os, signal os.kill(multi.pid, signal.SIGKILL) - + def onDisconnect(self): print("transport disconnected") ''' - + @inlineCallbacks def onJoin(self, details): LOG.info('RPC Wamp Session ready') @@ -54,35 +64,38 @@ class RPCWampManager(ApplicationSession): try: yield self.register(fun.test, u'stack4things.test') yield self.register(fun.registration, u'stack4things.register') - + LOG.info("Procedures registered") except Exception as e: print("could not register procedure: {0}".format(e)) - -class RPCWampServer: - def __init__(self,ip,port,realm): - self.ip=unicode(ip) - self.port=unicode(port) - self.realm=unicode(realm) - self._url = "ws://"+self.ip+":"+self.port+"/ws" - self.runner = ApplicationRunner(url=unicode(self._url), realm=self.realm, - #debug=True, debug_wamp=True, debug_app=True - ) + + +class RPCWampServer(object): + + def __init__(self, ip, port, realm): + self.ip = unicode(ip) + self.port = unicode(port) + self.realm = unicode(realm) + self._url = "ws://" + self.ip + ":" + self.port + "/ws" + self.runner = ApplicationRunner( + url=unicode(self._url), + realm=self.realm, + # debug=True, debug_wamp=True, + # debug_app=True + ) def start(self): # Pass start_reactor=False to all runner.run() calls self.runner.run(RPCWampManager, start_reactor=False) - - -class RPC_Wamp_Server: - - def __init__(self): - self.ip=unicode(CONF.wamp.wamp_ip) - self.port=unicode(CONF.wamp.wamp_port) - self.realm=unicode(CONF.wamp.wamp_realm) - server = RPCWampServer(self.ip,self.port,self.realm) - server.start() - multi = multiprocessing.Process(target=reactor.run,args=()) - multi.start() - + +class RPC_Wamp_Server(object): + + def __init__(self): + self.ip = unicode(CONF.wamp.wamp_ip) + self.port = unicode(CONF.wamp.wamp_port) + self.realm = unicode(CONF.wamp.wamp_realm) + server = RPCWampServer(self.ip, self.port, self.realm) + server.start() + multi = multiprocessing.Process(target=reactor.run, args=()) + multi.start() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fb8b92b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +pbr>=1.6 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3f540f4 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,46 @@ +[metadata] +name = iotronic +summary = IoTronic is an Internet of Things resource management service for OpenStack clouds. +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 :: 3 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + +[files] +packages = + iotronic + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_sphinx] +upload-dir = doc/build/html + +[compile_catalog] +directory = iotronic/locale +domain = iotronic + +[update_catalog] +domain = iotronic +output_dir = iotronic/locale +input_file = iotronic/locale/iotronic.pot + +[extract_messages] +keywords = _ gettext ngettext l_ lazy_gettext +mapping_file = babel.cfg +output_file = iotronic/locale/iotronic.pot diff --git a/setup.py b/setup.py index 9bd5b68..056c16c 100644 --- a/setup.py +++ b/setup.py @@ -1,81 +1,29 @@ -import os -from setuptools import setup, find_packages +# Copyright (c) 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. -def read(fname): - return open(os.path.join(os.path.dirname(__file__), fname)).read() +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools -setup( - name = "iotronic", - #packages = ["cwProbe", "plugins"], - packages = find_packages(), - version = "0.1", - description = "iot", - author = "", - author_email = "", - url = "", - download_url = "", - keywords = ["iotronic", "iot", "s4t"], - classifiers = [ - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Development Status :: 4 - Beta", - "Environment :: Other Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: GNU General Public License (GPL)", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - license='GPL', - platforms=['Any'], - #provides=['plugins',], - +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass - dependency_links = [ - - ], - - - - entry_points={ - #'cwprobe.plugins.monitors': [ - #'mycheck = plugins.cwpl_mycheckpoint:Cwpl_MyCheckPoint', - #'cpu = plugins.cwpl_cpu:Cwpl_Cpu', - #'awstats = plugins.cwpl_awstats:Cwpl_Awstat', - #'test = plugins.cwpl_test:Cwpl_Test', - #], - }, - - install_requires=[ - #'setuptools', - #'greenlet', - #'httplib2', - #'stevedore', - #'psutil', - #'qpid-python==0.20', - #'pyyamllib', - #'pyloglib', - #'cwconfparser', - #'MySQL-python', - ], - - - include_package_data = True, - - data_files=[ - ('/usr/bin', ['bin/iotronic-conductor']), - ('/usr/bin', ['bin/iotronic']), - ], - - - #package_data = { - # '': ['scripts/etc/init.d/cwProbe', 'scripts/usr/bin/cwProbe'], - #}, - - - #options = {'bdist_rpm':{'post_install' : 'scripts/post_install'}, - - zip_safe=False, - #long_description=read('README.txt') - - -) +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/temp/clients_wamp_rw.py b/temp/clients_wamp_rw.py deleted file mode 100644 index 766c069..0000000 --- a/temp/clients_wamp_rw.py +++ /dev/null @@ -1,163 +0,0 @@ -######################################################################################### -## -## The MIT License (MIT) -## -## Copyright (c) 2014 Andrea Rocco Lotronto -## -## Permission is hereby granted, free of charge, to any person obtaining a copy -## of this software and associated documentation files (the "Software"), to deal -## in the Software without restriction, including without limitation the rights -## to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -## copies of the Software, and to permit persons to whom the Software is -## furnished to do so, subject to the following conditions: -## -## The above copyright notice and this permission notice shall be included in all -## copies or substantial portions of the Software. -## -## THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -## IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -## FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -## AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -## LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -## OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -## SOFTWARE. -######################################################################################## - -from autobahn.twisted.wamp import ApplicationSession -from autobahn.twisted.wamp import ApplicationSessionFactory - -from autobahn.twisted.websocket import WampWebSocketClientFactory - -from autobahn.wamp.types import ComponentConfig - -from twisted.internet.defer import inlineCallbacks -from twisted.internet import reactor -from twisted.internet.endpoints import clientFromString -from twisted.python import log - -import threading -import time -import sys -log.startLogging(sys.stdout) - - -##Global Variable for saving client writer session -sessio_writer=None - - -## WAMP Application Class for Writer Client ## -class AutobahnClientWriter(ApplicationSession): - - @inlineCallbacks - def onJoin(self, details): - - global sessio_writer - sessio_writer = self - yield log.msg('Client Writer Connected') -###################################################### - -## WAMP Application Class for Reader Client ## -class AutobahnClientReader(ApplicationSession): - - @inlineCallbacks - def onJoin(self, details): - - log.msg('Client Reader Connected') - - self.topic_reader = self.config.extra['topicReader'] - - def onMessage(*args): - #DEBUG Message - log.msg('I receives',args) - ##New Class Parser for MSG - - try: - yield self.subscribe(onMessage, self.topic_reader) - print ("Subscribed to topic: "+self.topic_reader) - - except Exception as e: - print("could not subscribe to topic:" +self.topic_reader) -###################################################### - -## Principal class for inizialating and starting clients WAMP -class WampClient(): - - def __init__(self, topicRead='board.connection'):#Sistemare - - self._topicRead = None - self._debug = False - self._debug_wamp = False - self._debug_app = False - - self._factoryWriter = None - self._factoryReader = None - - self._realm = None - self._url = None - - self._extra = {'topicReader': topicRead} - - def connect(self, ip, port, realm): - - self._realm = realm - self._url = 'ws://'+ip+':'+'/ws' - self._reactor_thread = None - - self._session_factoryWriter = None - self._session_factoryReader = None - - cfgReader = ComponentConfig(self._realm, self._extra) - cfgWriter = ComponentConfig(self._realm, self._extra) - - self._session_factoryReader = ApplicationSessionFactory(cfgReader) - self._session_factoryReader.session = AutobahnClientReader - - self._session_factoryWriter = ApplicationSessionFactory(cfgWriter) - self._session_factoryWriter.session = AutobahnClientWriter - - - self._factoryReader = WampWebSocketClientFactory(self._session_factoryReader, url = self._url, - debug = self._debug, debug_wamp = self._debug_wamp) - - self._factoryWriter = WampWebSocketClientFactory(self._session_factoryWriter, url = self._url, - debug = self._debug, debug_wamp = self._debug_wamp) - - self._reactor_thread = threading.Thread(target=reactor.run, args=(False,)) - self._reactor_thread.daemon = True - - endpoint_descriptor = 'tcp:'+ip+':'+port - - self._clientReader = clientFromString(reactor, endpoint_descriptor) - self._clientReader.connect(self._factoryReader) - - self._clientWriter = clientFromString(reactor, endpoint_descriptor) - self._clientWriter.connect(self._factoryWriter) - - self._reactor_thread.start() - - return self -################################################################################## - - -## Utility Class to wite on a specific topic ## -def writeToTopic(topic, message): - global sessio_writer - sessio_writer.publish(topic,message) -####################################################### - -#####Config paramiters#### -ipWamp = '172.17.3.139' -portWamp ='8181' -realmWAMP = 's4t' -##Topic Scrittura; Msg -########################## - - -if __name__ == '__main__': - - client = WampClient() - test = client.connect(ipWamp, portWamp, realmWAMP) - - while True: - time.sleep(2) - writeToTopic('board.connection', 'MEEEEEEEEEEEEEE') diff --git a/temp/test_autobahn_wamp_client.py b/temp/test_autobahn_wamp_client.py deleted file mode 100644 index a07363b..0000000 --- a/temp/test_autobahn_wamp_client.py +++ /dev/null @@ -1,147 +0,0 @@ -from autobahn.twisted.websocket import WampWebSocketClientFactory -from autobahn.twisted.websocket import WampWebSocketClientProtocol - -from autobahn.twisted.websocket import WebSocketClientProtocol -from autobahn.twisted.websocket import WebSocketClientFactory -from autobahn.twisted.websocket import connectWS - -from autobahn.twisted.wamp import ApplicationSessionFactory - -from twisted.internet import reactor - -#from twisted.python import log -#import sys -#log.startLogging(sys.stdout) -import threading -import Queue - -# ----- twisted ---------- -class MyAppComponent(ApplicationSession): - - def onJoin(self, details): - if not self.factory._myAppSession: - self.factory._myAppSession = self - -def onLeave(self, details): - if self.factory._myAppSession == self: - self.factory._myAppSession = None - -#------------------------------------------------------- -class _WampClientProtocol(WampWebSocketClientProtocol): - def __init__(self, factory): - self.factory = factory - - def onOpen(self): - #log.msg("Client connected") - self.factory.protocol_instance = self - self.factory.base_client._connected_event.set() -#-------------------------------------------------------- - -class _WampClientFactory(WampWebSocketClientFactory): - def __init__(self, factory, *args, **kwargs): - WampWebSocketClientFactory.__init__(self, factory, *args, **kwargs) - self.protocol_instance = None - self.base_client = None - - def buildProtocol(self, addr): - return _WampClientProtocol(self) -#------------------------------------------------------------ - -''' -class _WebSocketClientProtocol(WebSocketClientProtocol): - def __init__(self, factory): - self.factory = factory - - def onOpen(self): - #log.debug("Client connected") - self.factory.protocol_instance = self - self.factory.base_client._connected_event.set() - -class _WebSocketClientFactory(WebSocketClientFactory): - def __init__(self, *args, **kwargs): - WebSocketClientFactory.__init__(self, *args, **kwargs) - self.protocol_instance = None - self.base_client = None - - def buildProtocol(self, addr): - return _WebSocketClientProtocol(self) -''' -# ------ end twisted ------- - -class BaseWBClient(object): - - def __init__(self, websocket_settings): - #self.settings = websocket_settings - # instance to be set by the own factory - self.factory = None - # this event will be triggered on onOpen() - self._connected_event = threading.Event() - # queue to hold not yet dispatched messages - self._send_queue = Queue.Queue() - self._reactor_thread = None - - self.session_factory = ApplicationSessionFactory() - - def connect(self): - - #log.msg("Connecting to 172.17.3.139:8181") - self.factory = _WampClientFactory(self.session_factory, - "ws://172.17.3.139:8181/ws", - debug_wamp=True) - self.factory.base_client = self - - c = connectWS(self.factory) - - self._reactor_thread = threading.Thread(target=reactor.run, - args=(False,)) - self._reactor_thread.daemon = True - self._reactor_thread.start() - - def send_message(self, body): - if not self._check_connection(): - return - #log.msg("Queing send") - self._send_queue.put(body) - reactor.callFromThread(self._dispatch) - - def _check_connection(self): - if not self._connected_event.wait(timeout=10): - #log.err("Unable to connect to server") - self.close() - return False - return True - - def _dispatch(self): - #log.msg("Dispatching") - while True: - try: - body = self._send_queue.get(block=False) - except Queue.Empty: - break - self.factory.protocol_instance.sendMessage(body) - - def close(self): - reactor.callFromThread(reactor.stop) - -import time -def Ppippo(coda): - while True: - coda.send_message('YOOOOOOOO') - time.sleep(5) - -if __name__ == '__main__': - - ws_setting = {'host':'172.17.3.139', 'port':8080} - - client = BaseWBClient(ws_setting) - - t1 = threading.Thread(client.connect()) - #t11 = threading.Thread(Ppippo(client)) - #t11.start() - t1.start() - - #client.connect() - #client.send_message('pippo') - - - \ No newline at end of file diff --git a/temp/test_autobahn_ws_client.py b/temp/test_autobahn_ws_client.py deleted file mode 100644 index 508540d..0000000 --- a/temp/test_autobahn_ws_client.py +++ /dev/null @@ -1,107 +0,0 @@ -from autobahn.twisted.websocket import WebSocketClientProtocol -from autobahn.twisted.websocket import WebSocketClientFactory -from autobahn.twisted.websocket import connectWS - -from twisted.internet import reactor - -from twisted.python import log -import sys -log.startLogging(sys.stdout) -import threading -import Queue - -# ----- twisted ---------- -class _WebSocketClientProtocol(WebSocketClientProtocol): - def __init__(self, factory): - self.factory = factory - - def onOpen(self): - #log.debug("Client connected") - self.factory.protocol_instance = self - self.factory.base_client._connected_event.set() - -class _WebSocketClientFactory(WebSocketClientFactory): - def __init__(self, *args, **kwargs): - WebSocketClientFactory.__init__(self, *args, **kwargs) - self.protocol_instance = None - self.base_client = None - - def buildProtocol(self, addr): - return _WebSocketClientProtocol(self) -# ------ end twisted ------- - -class BaseWBClient(object): - - def __init__(self, websocket_settings): - #self.settings = websocket_settings - # instance to be set by the own factory - self.factory = None - # this event will be triggered on onOpen() - self._connected_event = threading.Event() - # queue to hold not yet dispatched messages - self._send_queue = Queue.Queue() - self._reactor_thread = None - - def connect(self): - - log.msg("Connecting to 172.17.3.139:8282") - self.factory = _WebSocketClientFactory( - "ws://172.17.3.139:8282", - debug=True) - self.factory.base_client = self - - c = connectWS(self.factory) - - self._reactor_thread = threading.Thread(target=reactor.run, - args=(False,)) - self._reactor_thread.daemon = True - self._reactor_thread.start() - - def send_message(self, body): - if not self._check_connection(): - return - log.msg("Queing send") - self._send_queue.put(body) - reactor.callFromThread(self._dispatch) - - def _check_connection(self): - if not self._connected_event.wait(timeout=10): - log.err("Unable to connect to server") - self.close() - return False - return True - - def _dispatch(self): - log.msg("Dispatching") - while True: - try: - body = self._send_queue.get(block=False) - except Queue.Empty: - break - self.factory.protocol_instance.sendMessage(body) - - def close(self): - reactor.callFromThread(reactor.stop) - -import time -def Ppippo(coda): - while True: - coda.send_message('YOOOOOOOO') - time.sleep(5) - -if __name__ == '__main__': - - ws_setting = {'host':'172.17.3.139', 'port':8080} - - client = BaseWBClient(ws_setting) - - t1 = threading.Thread(client.connect()) - t11 = threading.Thread(Ppippo(client)) - t11.start() - t1.start() - - #client.connect() - #client.send_message('pippo') - - - \ No newline at end of file diff --git a/temp/test_autobahn_ws_server.py b/temp/test_autobahn_ws_server.py deleted file mode 100644 index c58f4ee..0000000 --- a/temp/test_autobahn_ws_server.py +++ /dev/null @@ -1,40 +0,0 @@ -from autobahn.twisted.websocket import WebSocketServerProtocol, \ - WebSocketServerFactory - - -class MyServerProtocol(WebSocketServerProtocol): - - def onConnect(self, request): - print("Client connecting: {0}".format(request.peer)) - - def onOpen(self): - print("WebSocket connection open.") - - def onMessage(self, payload, isBinary): - if isBinary: - print("Binary message received: {0} bytes".format(len(payload))) - else: - print("Text message received: {0}".format(payload.decode('utf8'))) - - ## echo back message verbatim - self.sendMessage(payload, isBinary) - - def onClose(self, wasClean, code, reason): - print("WebSocket connection closed: {0}".format(reason)) - - - -if __name__ == '__main__': - - import sys - - from twisted.python import log - from twisted.internet import reactor - - log.startLogging(sys.stdout) - - factory = WebSocketServerFactory("ws://172.17.3.139:8282", debug = False) - factory.protocol = MyServerProtocol - - reactor.listenTCP(8282, factory) - reactor.run() \ No newline at end of file diff --git a/temp/test_c.py b/temp/test_c.py deleted file mode 100644 index a87d013..0000000 --- a/temp/test_c.py +++ /dev/null @@ -1,46 +0,0 @@ -from autobahn.twisted.websocket import WebSocketClientProtocol, \ - WebSocketClientFactory - - - -class MyClientProtocol(WebSocketClientProtocol): - - def onConnect(self, response): - print("Server connected: {0}".format(response.peer)) - - def onOpen(self): - print("WebSocket connection open.") - - def hello(): - self.sendMessage(u"Hello, world!".encode('utf8')) - self.sendMessage(b"\x00\x01\x03\x04", isBinary = True) - self.factory.reactor.callLater(1, hello) - - ## start sending messages every second .. - hello() - - def onMessage(self, payload, isBinary): - if isBinary: - print("Binary message received: {0} bytes".format(len(payload))) - else: - print("Text message received: {0}".format(payload.decode('utf8'))) - - def onClose(self, wasClean, code, reason): - print("WebSocket connection closed: {0}".format(reason)) - - - -if __name__ == '__main__': - - import sys - - from twisted.python import log - from twisted.internet import reactor - - log.startLogging(sys.stdout) - - factory = WebSocketClientFactory("ws://localhost:9000", debug = False) - factory.protocol = MyClientProtocol - - reactor.connectTCP("localhost", 9000, factory) - reactor.run() \ No newline at end of file diff --git a/temp/test_pub.py b/temp/test_pub.py deleted file mode 100644 index 02df4df..0000000 --- a/temp/test_pub.py +++ /dev/null @@ -1,97 +0,0 @@ - -wampAddress = 'ws://172.17.3.139:8181/ws' -wampRealm = 's4t' - -#from threading import Thread - -from autobahn.twisted.wamp import ApplicationRunner -from autobahn.twisted.wamp import ApplicationSession -from twisted.internet.defer import inlineCallbacks - -#import per test -from twisted.internet.defer import DeferredQueue -from twisted.internet import threads - -#Classe autobahn per ka gestione della comunicazione con i dispositivi remoti -class AutobahnMRS(ApplicationSession): - @inlineCallbacks - def onJoin(self, details): - print("Sessio attached [Connect to WAMP Router] Sub") - - def onMessage(*args): - print args - - try: - yield self.subscribe(onMessage, 'test') - print ("Subscribed to topic: test") - - except Exception as e: - print("Exception:" +e) - - -#Classe autobahn per la gestione della comunicazione interna -class AutobahnIM(ApplicationSession): - - @inlineCallbacks - def onJoin(self, details): - print("Sessio attached [Connect to WAMP Router] Pub") - - try: - yield self.publish('test','YOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO') - print ("Publish to topic: test") - - except Exception as e: - print("Exception:" +e) - - - -#Classe per la gestione della comunicazioni con i dispositivi remoti -class ManageRemoteSystem: - def __init__(self): - self.runner = ApplicationRunner(url= wampAddress, realm = wampRealm) - - def start(self): - self.runner.run(AutobahnMRS, start_reactor=False); - - -#Classe per la gestione della comunicazione interna al ManageRemoteSystem -class InternalMessages: - def __init__(self): - self.runner = ApplicationRunner(url= wampAddress, realm = wampRealm) - - def start(self): - self.runner.run(AutobahnIM, start_reactor=False); - -#Classe principale per il servizio iotronic -#class S4tServer: - -def something(): - count = 0 - while True: - print('something:', count) - yield sleep(1) - count+=1 - -if __name__ == '__main__': - - #import multiprocessing - - server = ManageRemoteSystem() - #sendMessage = InternalMessages() - server.start() - #sendMessage.start() - - from twisted.internet import reactor - reactor.run() - #thread1 = Thread(target = reactor.run()) - #thread2 = Thread(target = something()) - - #thread2.start() - #thread1.start() - - #thread1.daemon = True - #thread2.daemon = True - - #thread2.join() - #thread1.join() - diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..52a2353 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,14 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. + +hacking>=0.10.2,<0.11 # Apache-2.0 + +coverage>=3.6 # Apache-2.0 +python-subunit>=0.0.18 # Apache-2.0/BSD +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 # BSD +oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0 +oslotest>=1.10.0 # Apache-2.0 +testrepository>=0.0.18 # Apache-2.0/BSD +testscenarios>=0.4 # Apache-2.0/BSD +testtools>=1.4.0 # MIT diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..81bc719 --- /dev/null +++ b/tox.ini @@ -0,0 +1,60 @@ +[tox] +minversion = 2.0 +envlist = py34-constraints,py27-constraints,pypy-constraints,pep8-constraints +skipsdist = True + +[testenv] +usedevelop = True +install_command = + constraints: {[testenv:common-constraints]install_command} + pip install -U {opts} {packages} +setenv = + VIRTUAL_ENV={envdir} +deps = -r{toxinidir}/test-requirements.txt +# commands = python setup.py test --slowest --testr-args='{posargs}' + +[testenv:common-constraints] +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:pep8-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:venv-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = {posargs} + +[testenv:cover] +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:cover-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py test --coverage --testr-args='{posargs}' + +[testenv:docs] +commands = python setup.py build_sphinx + +[testenv:docs-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = python setup.py build_sphinx + +[testenv:debug] +commands = oslo_debug_helper {posargs} + +[testenv:debug-constraints] +install_command = {[testenv:common-constraints]install_command} +commands = oslo_debug_helper {posargs} + +[flake8] +# E123, E125 skipped as they are invalid PEP-8. + +show-source = True +ignore = E123,E125 +builtins = _ +exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build diff --git a/utils/chat_wamp.py b/utils/chat_wamp.py deleted file mode 100644 index c4b6878..0000000 --- a/utils/chat_wamp.py +++ /dev/null @@ -1,10 +0,0 @@ -from iotronic.wamp.clientwamp import ClientWamp -from sys import stdin -import inspect - -c=ClientWamp('localhost','8181','s4t') -c.send('board.connection','Hello from the chat wamp!') -print 'USING',inspect.getfile(c.__class__) -while True: - userinput = stdin.readline() - c.send('board.connection',str(userinput)) \ No newline at end of file diff --git a/utils/rpc_test.py b/utils/rpc_test.py deleted file mode 100644 index 0004143..0000000 --- a/utils/rpc_test.py +++ /dev/null @@ -1,20 +0,0 @@ -from autobahn.twisted.wamp import ApplicationSession -from twisted.internet.defer import inlineCallbacks -from autobahn.twisted.wamp import ApplicationRunner - - -class RPCCaller(ApplicationSession): - @inlineCallbacks - def onJoin(self, details): - print("session ready") - try: - #stack4things.iotronic.conductor.function - res = yield self.call(u'stack4things.conductor.rpc.test',) - print("call result: {}".format(res)) - except Exception as e: - print("call error: {0}".format(e)) - -runner = ApplicationRunner(url=u"ws://localhost:8181/ws", realm=u"s4t") -runner.run(RPCCaller) - - diff --git a/utils/wamp_rpc_server.py b/utils/wamp_rpc_server.py deleted file mode 100644 index f645125..0000000 --- a/utils/wamp_rpc_server.py +++ /dev/null @@ -1,6 +0,0 @@ -from iotronic.wamp.rpcwampserver import RPC_Wamp_Server -#NB USE THE IP ADDRESS INSTEAD OF THE HOSTNAME -c=RPC_Wamp_Server('127.0.0.1','8181','s4t') - - -