start
This commit is contained in:
		
							
								
								
									
										49
									
								
								bin/iotronic-conductor
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										49
									
								
								bin/iotronic-conductor
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
#!/usr/bin/env python
 | 
			
		||||
 | 
			
		||||
# Copyright 2011 OpenStack LLC.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Iotronic Conductor
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
 | 
			
		||||
from iotronic.common import service as iotronic_service
 | 
			
		||||
from iotronic.openstack.common import service
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
	
 | 
			
		||||
	iotronic_service.prepare_service(sys.argv)
 | 
			
		||||
	mgr = iotronic_service.RPCService(CONF.host,
 | 
			
		||||
                                    'iotronic.conductor.manager',
 | 
			
		||||
                                    'ConductorManager')
 | 
			
		||||
	
 | 
			
		||||
	launcher = service.launch(mgr)
 | 
			
		||||
	launcher.wait()
 | 
			
		||||
	'''
 | 
			
		||||
    try:
 | 
			
		||||
        Conductor()
 | 
			
		||||
	pass
 | 
			
		||||
    except RuntimeError, e:
 | 
			
		||||
        sys.exit("ERROR: %s" % e)
 | 
			
		||||
	'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										4
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										4
									
								
								build.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
python setup.py build; python setup.py install; systemctl restart httpd;
 | 
			
		||||
rm -rf build
 | 
			
		||||
rm -rf iotronic.egg-info 
 | 
			
		||||
rm -rf dist
 | 
			
		||||
							
								
								
									
										38
									
								
								etc/apache2/iotronic.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								etc/apache2/iotronic.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
# This is an example Apache2 configuration file for using the
 | 
			
		||||
# Ironic API through mod_wsgi.  This version assumes you are
 | 
			
		||||
# running devstack to configure the software.
 | 
			
		||||
 | 
			
		||||
Listen 1288
 | 
			
		||||
 | 
			
		||||
<VirtualHost *:1288>
 | 
			
		||||
    WSGIDaemonProcess iotronic 
 | 
			
		||||
#user=root group=root threads=10 display-name=%{GROUP}
 | 
			
		||||
    WSGIScriptAlias / /etc/iotronic/app.wsgi
 | 
			
		||||
 | 
			
		||||
    #SetEnv APACHE_RUN_USER stack
 | 
			
		||||
    #SetEnv APACHE_RUN_GROUP stack
 | 
			
		||||
    WSGIProcessGroup iotronic
 | 
			
		||||
 | 
			
		||||
    ErrorLog /var/log/httpd/iotronic_error.log
 | 
			
		||||
    LogLevel debug
 | 
			
		||||
    CustomLog /var/log/httpd/iotronic_access.log combined
 | 
			
		||||
 | 
			
		||||
    <Directory /etc/iotronic>
 | 
			
		||||
        WSGIProcessGroup iotronic
 | 
			
		||||
        WSGIApplicationGroup %{GLOBAL}
 | 
			
		||||
        AllowOverride All
 | 
			
		||||
        Require all granted
 | 
			
		||||
    </Directory>
 | 
			
		||||
</VirtualHost>
 | 
			
		||||
							
								
								
									
										29
									
								
								etc/iotronic/app.wsgi
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								etc/iotronic/app.wsgi
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
# -*- mode: python -*-
 | 
			
		||||
# -*- encoding: 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 iotronic.api import app
 | 
			
		||||
from iotronic.common import service
 | 
			
		||||
 | 
			
		||||
from oslo import i18n
 | 
			
		||||
#from oslo_config import cfg
 | 
			
		||||
#cfg.CONF(project='iotronic')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
i18n.install('iotronic')
 | 
			
		||||
service.prepare_service([])
 | 
			
		||||
 | 
			
		||||
application = app.VersionSelectorApplication()
 | 
			
		||||
'''
 | 
			
		||||
							
								
								
									
										25
									
								
								etc/iotronic/iotronic.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								etc/iotronic/iotronic.conf
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,25 @@
 | 
			
		||||
[DEFAULT]
 | 
			
		||||
transport_url=rabbit://root:0penstack@iotctrl:5672/
 | 
			
		||||
debug=True
 | 
			
		||||
verbose=False
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Options defined in ironic.api.app
 | 
			
		||||
#  
 | 
			
		||||
 | 
			
		||||
# Authentication strategy used by ironic-api: one of
 | 
			
		||||
# "keystone" or "noauth". "noauth" should not be used in a
 | 
			
		||||
# production environment because all authentication will be
 | 
			
		||||
# disabled. (string value)
 | 
			
		||||
auth_strategy=noauth
 | 
			
		||||
 | 
			
		||||
# Enable pecan debug mode. WARNING: this is insecure and
 | 
			
		||||
# should not be used in a production environment. (boolean
 | 
			
		||||
# value)
 | 
			
		||||
#pecan_debug=false
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
[database]
 | 
			
		||||
connection = mysql://iotronic:0penstack@localhost/iotronic
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1616
									
								
								etc/iotronic/iotronic.conf_old
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1616
									
								
								etc/iotronic/iotronic.conf_old
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										5
									
								
								etc/iotronic/policy.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								etc/iotronic/policy.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
    "admin_api": "role:admin or role:administrator",
 | 
			
		||||
    "show_password": "!",
 | 
			
		||||
    "default": "rule:admin_api"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								infopackages
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								infopackages
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
yum install mariadb mariadb-server MySQL-python
 | 
			
		||||
yum install rabbitmq-server
 | 
			
		||||
yum install httpd mod_wsgi memcached python-memcached
 | 
			
		||||
yum install gcc python-devel pip
 | 
			
		||||
pip install eventlet
 | 
			
		||||
yum install python-oslo-config
 | 
			
		||||
pip install pecan
 | 
			
		||||
pip install keystonemiddleware
 | 
			
		||||
yum install python-oslo-log
 | 
			
		||||
yum install python-oslo-concurrency
 | 
			
		||||
pip install paramiko
 | 
			
		||||
yum install python-oslo-policy
 | 
			
		||||
yum install python-wsme
 | 
			
		||||
yum install python-oslo-policy
 | 
			
		||||
yum install python-oslo-messaging
 | 
			
		||||
yum install python-oslo-db
 | 
			
		||||
pip install jsonpatch
 | 
			
		||||
							
								
								
									
										22
									
								
								iotronic/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								iotronic/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
# Copyright (c) 2014 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 os
 | 
			
		||||
 | 
			
		||||
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
 | 
			
		||||
 | 
			
		||||
import eventlet
 | 
			
		||||
 | 
			
		||||
eventlet.monkey_patch(os=False)
 | 
			
		||||
							
								
								
									
										38
									
								
								iotronic/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								iotronic/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
API_SERVICE_OPTS = [
 | 
			
		||||
    cfg.StrOpt('host_ip',
 | 
			
		||||
               default='0.0.0.0',
 | 
			
		||||
               help='The IP address on which iotronic-api listens.'),
 | 
			
		||||
    cfg.IntOpt('port',
 | 
			
		||||
               default=1288,
 | 
			
		||||
               help='The TCP port on which iotronic-api listens.'),
 | 
			
		||||
    cfg.IntOpt('max_limit',
 | 
			
		||||
               default=1000,
 | 
			
		||||
               help='The maximum number of items returned in a single '
 | 
			
		||||
                    'response from a collection resource.'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										34
									
								
								iotronic/api/acl.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								iotronic/api/acl.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
 | 
			
		||||
#
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
"""Access Control Lists (ACL's) control access the API server."""
 | 
			
		||||
 | 
			
		||||
from iotronic.api.middleware import auth_token
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def install(app, conf, public_routes):
 | 
			
		||||
    """Install ACL check on application.
 | 
			
		||||
 | 
			
		||||
    :param app: A WSGI applicatin.
 | 
			
		||||
    :param conf: Settings. Dict'ified and passed to keystonemiddleware
 | 
			
		||||
    :param public_routes: The list of the routes which will be allowed to
 | 
			
		||||
                          access without authentication.
 | 
			
		||||
    :return: The same WSGI application with ACL installed.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return auth_token.AuthTokenMiddleware(app,
 | 
			
		||||
                                          conf=dict(conf),
 | 
			
		||||
                                          public_api_routes=public_routes)
 | 
			
		||||
							
								
								
									
										88
									
								
								iotronic/api/app.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								iotronic/api/app.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,88 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import pecan
 | 
			
		||||
 | 
			
		||||
from iotronic.api import acl
 | 
			
		||||
from iotronic.api import config
 | 
			
		||||
from iotronic.api import hooks
 | 
			
		||||
from iotronic.api import middleware
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api_opts = [
 | 
			
		||||
    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.')),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(api_opts)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_pecan_config():
 | 
			
		||||
    # Set up the pecan configuration
 | 
			
		||||
    filename = config.__file__.replace('.pyc', '.py')
 | 
			
		||||
    return pecan.configuration.conf_from_file(filename)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup_app(pecan_config=None, extra_hooks=None):
 | 
			
		||||
    app_hooks = [hooks.ConfigHook(),
 | 
			
		||||
                 hooks.DBHook(),
 | 
			
		||||
                 hooks.ContextHook(pecan_config.app.acl_public_routes),
 | 
			
		||||
                 hooks.RPCHook(),
 | 
			
		||||
                 hooks.NoExceptionTracebackHook()]
 | 
			
		||||
    if extra_hooks:
 | 
			
		||||
        app_hooks.extend(extra_hooks)
 | 
			
		||||
 | 
			
		||||
    if not pecan_config:
 | 
			
		||||
        pecan_config = get_pecan_config()
 | 
			
		||||
 | 
			
		||||
    if pecan_config.app.enable_acl:
 | 
			
		||||
        app_hooks.append(hooks.TrustedCallHook())
 | 
			
		||||
 | 
			
		||||
    pecan.configuration.set_config(dict(pecan_config), overwrite=True)
 | 
			
		||||
 | 
			
		||||
    app = pecan.make_app(
 | 
			
		||||
        pecan_config.app.root,
 | 
			
		||||
        static_root=pecan_config.app.static_root,
 | 
			
		||||
        debug=CONF.pecan_debug,
 | 
			
		||||
        force_canonical=getattr(pecan_config.app, 'force_canonical', True),
 | 
			
		||||
        hooks=app_hooks,
 | 
			
		||||
        wrap_app=middleware.ParsableErrorMiddleware,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if pecan_config.app.enable_acl:
 | 
			
		||||
        return acl.install(app, cfg.CONF, pecan_config.app.acl_public_routes)
 | 
			
		||||
 | 
			
		||||
    return app
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VersionSelectorApplication(object):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        pc = get_pecan_config()
 | 
			
		||||
        pc.app.enable_acl = (CONF.auth_strategy == 'keystone')
 | 
			
		||||
        self.v1 = setup_app(pecan_config=pc)
 | 
			
		||||
 | 
			
		||||
    def __call__(self, environ, start_response):
 | 
			
		||||
        return self.v1(environ, start_response)
 | 
			
		||||
							
								
								
									
										43
									
								
								iotronic/api/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								iotronic/api/config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
# Server Specific Configurations
 | 
			
		||||
# See https://pecan.readthedocs.org/en/latest/configuration.html#server-configuration # noqa
 | 
			
		||||
server = {
 | 
			
		||||
    'port': '1288',
 | 
			
		||||
    'host': '0.0.0.0'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Pecan Application Configurations
 | 
			
		||||
# See https://pecan.readthedocs.org/en/latest/configuration.html#application-configuration # noqa
 | 
			
		||||
app = {
 | 
			
		||||
    'root': 'iotronic.api.controllers.root.RootController',
 | 
			
		||||
    'modules': ['iotronic.api'],
 | 
			
		||||
    'static_root': '%(confdir)s/public',
 | 
			
		||||
    'debug': True,
 | 
			
		||||
    'enable_acl': True,
 | 
			
		||||
    'acl_public_routes': [
 | 
			
		||||
        '/',
 | 
			
		||||
        '/v1',
 | 
			
		||||
        #'/v1/drivers/[a-z_]*/vendor_passthru/lookup',
 | 
			
		||||
        '/v1/nodes/[a-z0-9\-]+/vendor_passthru/heartbeat',
 | 
			
		||||
        '/v1/boards/[a-z0-9\-]',
 | 
			
		||||
    ],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# WSME Configurations
 | 
			
		||||
# See https://wsme.readthedocs.org/en/latest/integrate.html#configuration
 | 
			
		||||
wsme = {
 | 
			
		||||
    'debug': False,
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/api/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/api/controllers/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										114
									
								
								iotronic/api/controllers/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								iotronic/api/controllers/base.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
# 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 webob import exc
 | 
			
		||||
import wsme
 | 
			
		||||
from wsme import types as wtypes
 | 
			
		||||
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class APIBase(wtypes.Base):
 | 
			
		||||
 | 
			
		||||
    created_at = wsme.wsattr(datetime.datetime, readonly=True)
 | 
			
		||||
    """The time in UTC at which the object is created"""
 | 
			
		||||
 | 
			
		||||
    updated_at = wsme.wsattr(datetime.datetime, readonly=True)
 | 
			
		||||
    """The time in UTC at which the object is updated"""
 | 
			
		||||
 | 
			
		||||
    def as_dict(self):
 | 
			
		||||
        """Render this object as a dict of its fields."""
 | 
			
		||||
        return dict((k, getattr(self, k))
 | 
			
		||||
                    for k in self.fields
 | 
			
		||||
                    if hasattr(self, k) and
 | 
			
		||||
                    getattr(self, k) != wsme.Unset)
 | 
			
		||||
 | 
			
		||||
    def unset_fields_except(self, except_list=None):
 | 
			
		||||
        """Unset fields so they don't appear in the message body.
 | 
			
		||||
 | 
			
		||||
        :param except_list: A list of fields that won't be touched.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        if except_list is None:
 | 
			
		||||
            except_list = []
 | 
			
		||||
 | 
			
		||||
        for k in self.as_dict():
 | 
			
		||||
            if k not in except_list:
 | 
			
		||||
                setattr(self, k, wsme.Unset)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Version(object):
 | 
			
		||||
    """API Version object."""
 | 
			
		||||
 | 
			
		||||
    string = 'X-OpenStack-Iotronic-API-Version'
 | 
			
		||||
    """HTTP Header string carrying the requested version"""
 | 
			
		||||
 | 
			
		||||
    min_string = 'X-OpenStack-Iotronic-API-Minimum-Version'
 | 
			
		||||
    """HTTP response header"""
 | 
			
		||||
 | 
			
		||||
    max_string = 'X-OpenStack-Iotronic-API-Maximum-Version'
 | 
			
		||||
    """HTTP response header"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, headers, default_version, latest_version):
 | 
			
		||||
        """Create an API Version object from the supplied headers.
 | 
			
		||||
 | 
			
		||||
        :param headers: webob headers
 | 
			
		||||
        :param default_version: version to use if not specified in headers
 | 
			
		||||
        :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)
 | 
			
		||||
 | 
			
		||||
    def __repr__(self):
 | 
			
		||||
        return '%s.%s' % (self.major, self.minor)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def parse_headers(headers, default_version, latest_version):
 | 
			
		||||
        """Determine the API version requested based on the headers supplied.
 | 
			
		||||
 | 
			
		||||
        :param headers: webob headers
 | 
			
		||||
        :param default_version: version to use if not specified in headers
 | 
			
		||||
        :param latest_version: version to use if latest is requested
 | 
			
		||||
        :returns: a tupe of (major, minor) version numbers
 | 
			
		||||
        :raises: webob.HTTPNotAcceptable
 | 
			
		||||
        """
 | 
			
		||||
        version_str = headers.get(Version.string, default_version)
 | 
			
		||||
 | 
			
		||||
        if version_str.lower() == 'latest':
 | 
			
		||||
            parse_str = latest_version
 | 
			
		||||
        else:
 | 
			
		||||
            parse_str = version_str
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            version = tuple(int(i) for i in parse_str.split('.'))
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            version = ()
 | 
			
		||||
 | 
			
		||||
        if len(version) != 2:
 | 
			
		||||
            raise exc.HTTPNotAcceptable(_(
 | 
			
		||||
                "Invalid value for %s header") % Version.string)
 | 
			
		||||
        return version
 | 
			
		||||
 | 
			
		||||
    def __lt__(a, b):
 | 
			
		||||
        if (a.major == b.major and a.minor < b.minor):
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def __gt__(a, b):
 | 
			
		||||
        if (a.major == b.major and a.minor > b.minor):
 | 
			
		||||
            return True
 | 
			
		||||
        return False
 | 
			
		||||
							
								
								
									
										58
									
								
								iotronic/api/controllers/link.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								iotronic/api/controllers/link.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
# 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def build_url(resource, resource_args, bookmark=False, base_url=None):
 | 
			
		||||
    if base_url is None:
 | 
			
		||||
        base_url = pecan.request.host_url
 | 
			
		||||
 | 
			
		||||
    template = '%(url)s/%(res)s' if bookmark else '%(url)s/v1/%(res)s'
 | 
			
		||||
    # FIXME(lucasagomes): I'm getting a 404 when doing a GET on
 | 
			
		||||
    # a nested resource that the URL ends with a  '/'.
 | 
			
		||||
    # https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs
 | 
			
		||||
    template += '%(args)s' if resource_args.startswith('?') else '/%(args)s'
 | 
			
		||||
    return template % {'url': base_url, 'res': resource, 'args': resource_args}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Link(base.APIBase):
 | 
			
		||||
    """A link representation."""
 | 
			
		||||
 | 
			
		||||
    href = wtypes.text
 | 
			
		||||
    """The url of a link."""
 | 
			
		||||
 | 
			
		||||
    rel = wtypes.text
 | 
			
		||||
    """The name of a link."""
 | 
			
		||||
 | 
			
		||||
    type = wtypes.text
 | 
			
		||||
    """Indicates the type of document/link."""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def make_link(rel_name, url, resource, resource_args,
 | 
			
		||||
                  bookmark=False, type=wtypes.Unset):
 | 
			
		||||
        href = build_url(resource, resource_args,
 | 
			
		||||
                         bookmark=bookmark, base_url=url)
 | 
			
		||||
        return Link(href=href, rel=rel_name, type=type)
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def sample(cls):
 | 
			
		||||
        sample = cls(href="http://localhost:6385/chassis/"
 | 
			
		||||
                          "eaaca217-e7d8-47b4-bb41-3f99f20eed89",
 | 
			
		||||
                     rel="bookmark")
 | 
			
		||||
        return sample
 | 
			
		||||
							
								
								
									
										97
									
								
								iotronic/api/controllers/root.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								iotronic/api/controllers/root.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
 | 
			
		||||
#
 | 
			
		||||
# 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
 | 
			
		||||
from wsme import types as wtypes
 | 
			
		||||
 | 
			
		||||
from iotronic.api.controllers import base
 | 
			
		||||
from iotronic.api.controllers import link
 | 
			
		||||
from iotronic.api.controllers import v1
 | 
			
		||||
from iotronic.api import expose
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Version(base.APIBase):
 | 
			
		||||
    """An API version representation."""
 | 
			
		||||
 | 
			
		||||
    id = wtypes.text
 | 
			
		||||
    """The ID of the version, also acts as the release number"""
 | 
			
		||||
 | 
			
		||||
    links = [link.Link]
 | 
			
		||||
    """A Link that point to a specific version of the API"""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def convert(id):
 | 
			
		||||
        version = Version()
 | 
			
		||||
        version.id = id
 | 
			
		||||
        version.links = [link.Link.make_link('self', pecan.request.host_url,
 | 
			
		||||
                                             id, '', bookmark=True)]
 | 
			
		||||
        return version
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Root(base.APIBase):
 | 
			
		||||
 | 
			
		||||
    name = wtypes.text
 | 
			
		||||
    """The name of the API"""
 | 
			
		||||
 | 
			
		||||
    description = wtypes.text
 | 
			
		||||
    """Some information about this API"""
 | 
			
		||||
 | 
			
		||||
    versions = [Version]
 | 
			
		||||
    """Links to all the versions available in this API"""
 | 
			
		||||
 | 
			
		||||
    default_version = Version
 | 
			
		||||
    """A link to the default version of the API"""
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def convert():
 | 
			
		||||
        root = Root()
 | 
			
		||||
        root.name = "OpenStack Iotronic API"
 | 
			
		||||
        root.description = ("Iotronic is an OpenStack project which aims to "
 | 
			
		||||
                            "provision baremetal machines.")
 | 
			
		||||
        root.versions = [Version.convert('v1')]
 | 
			
		||||
        root.default_version = Version.convert('v1')
 | 
			
		||||
        return root
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RootController(rest.RestController):
 | 
			
		||||
 | 
			
		||||
    _versions = ['v1']
 | 
			
		||||
    """All supported API versions"""
 | 
			
		||||
 | 
			
		||||
    _default_version = 'v1'
 | 
			
		||||
    """The default API version"""
 | 
			
		||||
 | 
			
		||||
    v1 = v1.Controller()
 | 
			
		||||
 | 
			
		||||
    @expose.expose(Root)
 | 
			
		||||
    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 Root.convert()
 | 
			
		||||
 | 
			
		||||
    @pecan.expose()
 | 
			
		||||
    def _route(self, args):
 | 
			
		||||
        """Overrides the default routing behavior.
 | 
			
		||||
 | 
			
		||||
        It redirects the request to the default version of the iotronic API
 | 
			
		||||
        if the version number is not specified in the url.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if args[0] and args[0] not in self._versions:
 | 
			
		||||
            args = [self._default_version] + args
 | 
			
		||||
        return super(RootController, self)._route(args)
 | 
			
		||||
							
								
								
									
										208
									
								
								iotronic/api/controllers/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										208
									
								
								iotronic/api/controllers/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,208 @@
 | 
			
		||||
# 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 link
 | 
			
		||||
from iotronic.api.controllers.v1 import board
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#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 port
 | 
			
		||||
from iotronic.api.controllers.v1 import board
 | 
			
		||||
'''
 | 
			
		||||
 | 
			
		||||
from iotronic.api.controllers import base
 | 
			
		||||
from iotronic.api import expose
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
 | 
			
		||||
BASE_VERSION = 1
 | 
			
		||||
 | 
			
		||||
MIN_VER_STR = '1.0'
 | 
			
		||||
 | 
			
		||||
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"""
 | 
			
		||||
    
 | 
			
		||||
    boards = [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.boards = [link.Link.make_link('self', pecan.request.host_url,
 | 
			
		||||
                                'nodes', ''),
 | 
			
		||||
            link.Link.make_link('bookmark',
 | 
			
		||||
                                pecan.request.host_url,
 | 
			
		||||
                                'nodes', '',
 | 
			
		||||
                                bookmark=True)
 | 
			
		||||
            ]
 | 
			
		||||
        
 | 
			
		||||
        '''
 | 
			
		||||
        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."""
 | 
			
		||||
    
 | 
			
		||||
    boards = board.BoardsController()
 | 
			
		||||
    #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)
 | 
			
		||||
							
								
								
									
										207
									
								
								iotronic/api/controllers/v1/__old/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										207
									
								
								iotronic/api/controllers/v1/__old/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,207 @@
 | 
			
		||||
# 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)
 | 
			
		||||
							
								
								
									
										270
									
								
								iotronic/api/controllers/v1/__old/chassis.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								iotronic/api/controllers/v1/__old/chassis.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,270 @@
 | 
			
		||||
# 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()
 | 
			
		||||
							
								
								
									
										48
									
								
								iotronic/api/controllers/v1/__old/collection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								iotronic/api/controllers/v1/__old/collection.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										210
									
								
								iotronic/api/controllers/v1/__old/driver.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								iotronic/api/controllers/v1/__old/driver.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
			
		||||
# 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 <vendor method name>:<method metadata>
 | 
			
		||||
                  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 <property name>:<property description>
 | 
			
		||||
                  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]
 | 
			
		||||
							
								
								
									
										1104
									
								
								iotronic/api/controllers/v1/__old/node.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1104
									
								
								iotronic/api/controllers/v1/__old/node.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										396
									
								
								iotronic/api/controllers/v1/__old/port.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										396
									
								
								iotronic/api/controllers/v1/__old/port.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,396 @@
 | 
			
		||||
# 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)
 | 
			
		||||
							
								
								
									
										34
									
								
								iotronic/api/controllers/v1/__old/state.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								iotronic/api/controllers/v1/__old/state.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
# 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"""
 | 
			
		||||
							
								
								
									
										239
									
								
								iotronic/api/controllers/v1/__old/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								iotronic/api/controllers/v1/__old/types.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										107
									
								
								iotronic/api/controllers/v1/__old/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								iotronic/api/controllers/v1/__old/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,107 @@
 | 
			
		||||
# 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))
 | 
			
		||||
							
								
								
									
										238
									
								
								iotronic/api/controllers/v1/board.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								iotronic/api/controllers/v1/board.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,238 @@
 | 
			
		||||
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
 | 
			
		||||
from iotronic.api.controllers.v1 import collection
 | 
			
		||||
from iotronic.api.controllers.v1 import utils as api_utils
 | 
			
		||||
from iotronic.api.controllers import base
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
import wsme
 | 
			
		||||
import pecan
 | 
			
		||||
from pecan import rest
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Board(base.APIBase):
 | 
			
		||||
    """API representation of a board.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    uuid = types.uuid
 | 
			
		||||
    name = wsme.wsattr(wtypes.text)
 | 
			
		||||
    status = wsme.wsattr(wtypes.text)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _convert_with_links(board, url, expand=True, show_password=True):
 | 
			
		||||
        '''
 | 
			
		||||
        if not expand:
 | 
			
		||||
            except_list = ['instance_uuid', 'maintenance', 'power_state',
 | 
			
		||||
                           'provision_state', 'uuid', 'name']
 | 
			
		||||
            board.unset_fields_except(except_list)
 | 
			
		||||
        else:
 | 
			
		||||
            if not show_password:
 | 
			
		||||
                board.driver_info = ast.literal_eval(strutils.mask_password(
 | 
			
		||||
                                                    board.driver_info,
 | 
			
		||||
                                                    "******"))
 | 
			
		||||
            board.ports = [link.Link.make_link('self', url, 'boards',
 | 
			
		||||
                                              board.uuid + "/ports"),
 | 
			
		||||
                          link.Link.make_link('bookmark', url, 'boards',
 | 
			
		||||
                                              board.uuid + "/ports",
 | 
			
		||||
                                              bookmark=True)
 | 
			
		||||
                          ]
 | 
			
		||||
 | 
			
		||||
        board.chassis_id = wtypes.Unset
 | 
			
		||||
        '''
 | 
			
		||||
        '''
 | 
			
		||||
        board.links = [link.Link.make_link('self', url, 'boards',
 | 
			
		||||
                                          board.uuid),
 | 
			
		||||
                      link.Link.make_link('bookmark', url, 'boards',
 | 
			
		||||
                                          board.uuid, bookmark=True)
 | 
			
		||||
                      ]
 | 
			
		||||
        '''
 | 
			
		||||
        return board
 | 
			
		||||
    
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def convert_with_links(cls, rpc_board, expand=True):
 | 
			
		||||
        board = Board(**rpc_board.as_dict())
 | 
			
		||||
        return cls._convert_with_links(board, pecan.request.host_url,
 | 
			
		||||
                                       expand,
 | 
			
		||||
                                       pecan.request.context.show_password)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self.fields = []
 | 
			
		||||
        fields = list(objects.Board.fields)
 | 
			
		||||
        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))
 | 
			
		||||
            
 | 
			
		||||
class BoardCollection(collection.Collection):
 | 
			
		||||
    """API representation of a collection of boards."""
 | 
			
		||||
 | 
			
		||||
    boards = [Board]
 | 
			
		||||
    """A list containing boards objects"""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        self._type = 'boards'
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def convert_with_links(boards, limit, url=None, expand=False, **kwargs):
 | 
			
		||||
        collection = BoardCollection()
 | 
			
		||||
        collection.boards = [Board.convert_with_links(n, expand) for n in boards]
 | 
			
		||||
        collection.next = collection.get_next(limit, url=url, **kwargs)
 | 
			
		||||
        return collection
 | 
			
		||||
    
 | 
			
		||||
class BoardsController(rest.RestController):
 | 
			
		||||
 | 
			
		||||
    invalid_sort_key_list = ['properties']
 | 
			
		||||
 | 
			
		||||
    def _get_boards_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.Board.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:
 | 
			
		||||
            boards = self._get_boards_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
 | 
			
		||||
 | 
			
		||||
            boards = objects.Board.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 BoardCollection.convert_with_links(boards, limit,
 | 
			
		||||
                                                 url=resource_url,
 | 
			
		||||
                                                 expand=expand,
 | 
			
		||||
                                                 **parameters)
 | 
			
		||||
    
 | 
			
		||||
    @expose.expose(BoardCollection, 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 boards.
 | 
			
		||||
 | 
			
		||||
        :param chassis_uuid: Optional UUID of a chassis, to get only boards for
 | 
			
		||||
                           that chassis.
 | 
			
		||||
        :param instance_uuid: Optional UUID of an instance, to find the board
 | 
			
		||||
                              associated with that instance.
 | 
			
		||||
        :param associated: Optional boolean whether to return a list of
 | 
			
		||||
                           associated or unassociated boards. May be combined
 | 
			
		||||
                           with other parameters.
 | 
			
		||||
        :param maintenance: Optional boolean value that indicates whether
 | 
			
		||||
                            to get boards 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_boards_collection(chassis_uuid, instance_uuid,
 | 
			
		||||
                                          associated, maintenance, marker,
 | 
			
		||||
                                          limit, sort_key, sort_dir)
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    
 | 
			
		||||
    @expose.expose(Board,types.uuid_or_name)    
 | 
			
		||||
    def get(self,board_ident):
 | 
			
		||||
        """Retrieve information about the given board.
 | 
			
		||||
 | 
			
		||||
        :param node_ident: UUID or logical name of a board.
 | 
			
		||||
        """
 | 
			
		||||
        rpc_board = api_utils.get_rpc_board(board_ident)
 | 
			
		||||
        board = Board(**rpc_board.as_dict())
 | 
			
		||||
        return board
 | 
			
		||||
    
 | 
			
		||||
    @expose.expose(None, types.uuid_or_name, status_code=204)
 | 
			
		||||
    def delete(self, board_ident):
 | 
			
		||||
        """Delete a board.
 | 
			
		||||
 | 
			
		||||
        :param board_ident: UUID or logical name of a board.
 | 
			
		||||
        """
 | 
			
		||||
        rpc_board = api_utils.get_rpc_board(board_ident)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            topic = pecan.request.rpcapi.get_topic_for(rpc_board)
 | 
			
		||||
        except exception.NoValidHost as e:
 | 
			
		||||
            e.code = 400
 | 
			
		||||
            raise e
 | 
			
		||||
 | 
			
		||||
        pecan.request.rpcapi.destroy_board(pecan.request.context,
 | 
			
		||||
                                          rpc_board.uuid, topic)
 | 
			
		||||
        
 | 
			
		||||
    #@expose.expose(Board, body=Board, status_code=201)
 | 
			
		||||
    #def post(self, Board):
 | 
			
		||||
    @expose.expose(Board, status_code=201)
 | 
			
		||||
    def post(self):
 | 
			
		||||
        """Create a new Board.
 | 
			
		||||
 | 
			
		||||
        :param Board: a Board within the request body.
 | 
			
		||||
        """
 | 
			
		||||
        '''
 | 
			
		||||
        if not Board.uuid:
 | 
			
		||||
            Board.uuid = uuidutils.generate_uuid()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            pecan.request.rpcapi.get_topic_for(Board)
 | 
			
		||||
        except exception.NoValidHost as e:
 | 
			
		||||
            e.code = 400
 | 
			
		||||
            raise e
 | 
			
		||||
 | 
			
		||||
        if Board.name:
 | 
			
		||||
            if not api_utils.allow_Board_logical_names():
 | 
			
		||||
                raise exception.NotAcceptable()
 | 
			
		||||
            if not api_utils.is_valid_Board_name(Board.name):
 | 
			
		||||
                msg = _("Cannot create Board with invalid name %(name)s")
 | 
			
		||||
                raise wsme.exc.ClientSideError(msg % {'name': Board.name},
 | 
			
		||||
                                               status_code=400)
 | 
			
		||||
        '''
 | 
			
		||||
        #new_Board = objects.Board(pecan.request.context,
 | 
			
		||||
        #                        **Board.as_dict())
 | 
			
		||||
        
 | 
			
		||||
        #new_Board = objects.Board(pecan.request.context,
 | 
			
		||||
        #                        **Board.as_dict())
 | 
			
		||||
        #rpc_board = api_utils.get_rpc_board('a9a86ab8-ad45-455e-86c3-d8f7d892ec9d')
 | 
			
		||||
        
 | 
			
		||||
        """{'status': u'1', 'uuid': u'a9a86ab8-ad45-455e-86c3-d8f7d892ec9d', 
 | 
			
		||||
        'created_at': datetime.datetime(2015, 1, 30, 16, 56, tzinfo=<iso8601.iso8601.Utc object at 0x7f5b81e0dd90>), 
 | 
			
		||||
        'updated_at': None, 
 | 
			
		||||
        'reservation': None, 'id': 106, 'name': u'provaaaa'}
 | 
			
		||||
        """
 | 
			
		||||
        b="{'status': '1', 'uuid': 'a9a86ab8-ad45-455e-86c3-d8f7d892ec9d', 'name': 'provaaaa'}"
 | 
			
		||||
        board = Board(**b.as_dict())
 | 
			
		||||
        board.uuid = uuidutils.generate_uuid()
 | 
			
		||||
        
 | 
			
		||||
        new_Board = objects.Board(pecan.request.context,
 | 
			
		||||
                                **board.as_dict())
 | 
			
		||||
        new_Board.create()
 | 
			
		||||
        #pecan.response.location = link.build_url('Boards', new_Board.uuid)
 | 
			
		||||
        return Board.convert_with_links(new_Board)
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								iotronic/api/controllers/v1/collection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								iotronic/api/controllers/v1/collection.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										239
									
								
								iotronic/api/controllers/v1/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								iotronic/api/controllers/v1/types.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
# 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
 | 
			
		||||
							
								
								
									
										131
									
								
								iotronic/api/controllers/v1/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								iotronic/api/controllers/v1/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,131 @@
 | 
			
		||||
# 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))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#################### NEW
 | 
			
		||||
 | 
			
		||||
def get_rpc_board(board_ident):
 | 
			
		||||
    """Get the RPC board from the board uuid or logical name.
 | 
			
		||||
 | 
			
		||||
    :param board_ident: the UUID or logical name of a board.
 | 
			
		||||
 | 
			
		||||
    :returns: The RPC Board.
 | 
			
		||||
    :raises: InvalidUuidOrName if the name or uuid provided is not valid.
 | 
			
		||||
    :raises: BoardNotFound if the board is not found.
 | 
			
		||||
    """
 | 
			
		||||
    # Check to see if the board_ident is a valid UUID.  If it is, treat it
 | 
			
		||||
    # as a UUID.
 | 
			
		||||
    if uuidutils.is_uuid_like(board_ident):
 | 
			
		||||
        return objects.Board.get_by_uuid(pecan.request.context, board_ident)
 | 
			
		||||
 | 
			
		||||
    # We can refer to boards by their name, if the client supports it
 | 
			
		||||
    if allow_board_logical_names():
 | 
			
		||||
        if utils.is_hostname_safe(board_ident):
 | 
			
		||||
            return objects.Board.get_by_name(pecan.request.context, board_ident)
 | 
			
		||||
        raise exception.InvalidUuidOrName(name=board_ident)
 | 
			
		||||
 | 
			
		||||
    # Ensure we raise the same exception as we did for the Juno release
 | 
			
		||||
    raise exception.BoardNotFound(board=board_ident)
 | 
			
		||||
							
								
								
									
										24
									
								
								iotronic/api/expose.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								iotronic/api/expose.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2015 Rackspace, 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 wsmeext.pecan as wsme_pecan
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def expose(*args, **kwargs):
 | 
			
		||||
    """Ensure that only JSON, and not XML, is supported."""
 | 
			
		||||
    if 'rest_content_types' not in kwargs:
 | 
			
		||||
        kwargs['rest_content_types'] = ('json',)
 | 
			
		||||
    return wsme_pecan.wsexpose(*args, **kwargs)
 | 
			
		||||
							
								
								
									
										159
									
								
								iotronic/api/hooks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								iotronic/api/hooks.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,159 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
# not use this file except in compliance with the License. You may obtain
 | 
			
		||||
# a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
# License for the specific language governing permissions and limitations
 | 
			
		||||
# under the License.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from pecan import hooks
 | 
			
		||||
from webob import exc
 | 
			
		||||
 | 
			
		||||
from iotronic.common import context
 | 
			
		||||
from iotronic.common import policy
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from iotronic.conductor import rpcapi
 | 
			
		||||
from iotronic.db import api as dbapi
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConfigHook(hooks.PecanHook):
 | 
			
		||||
    """Attach the config object to the request so controllers can get to it."""
 | 
			
		||||
 | 
			
		||||
    def before(self, state):
 | 
			
		||||
        state.request.cfg = cfg.CONF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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()
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ContextHook(hooks.PecanHook):
 | 
			
		||||
    """Configures a request context and attaches it to the request.
 | 
			
		||||
 | 
			
		||||
    The following HTTP request headers are used:
 | 
			
		||||
 | 
			
		||||
    X-User-Id or X-User:
 | 
			
		||||
        Used for context.user_id.
 | 
			
		||||
 | 
			
		||||
    X-Tenant-Id or X-Tenant:
 | 
			
		||||
        Used for context.tenant.
 | 
			
		||||
 | 
			
		||||
    X-Auth-Token:
 | 
			
		||||
        Used for context.auth_token.
 | 
			
		||||
 | 
			
		||||
    X-Roles:
 | 
			
		||||
        Used for setting context.is_admin flag to either True or False.
 | 
			
		||||
        The flag is set to True, if X-Roles contains either an administrator
 | 
			
		||||
        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__()
 | 
			
		||||
 | 
			
		||||
    def before(self, state):
 | 
			
		||||
        headers = state.request.headers
 | 
			
		||||
 | 
			
		||||
        # Do not pass any token with context for noauth mode
 | 
			
		||||
        auth_token = (None if cfg.CONF.auth_strategy == 'noauth' else
 | 
			
		||||
                      headers.get('X-Auth-Token'))
 | 
			
		||||
 | 
			
		||||
        creds = {
 | 
			
		||||
            'user': headers.get('X-User') or headers.get('X-User-Id'),
 | 
			
		||||
            'tenant': headers.get('X-Tenant') or headers.get('X-Tenant-Id'),
 | 
			
		||||
            'domain_id': headers.get('X-User-Domain-Id'),
 | 
			
		||||
            'domain_name': headers.get('X-User-Domain-Name'),
 | 
			
		||||
            'auth_token': auth_token,
 | 
			
		||||
            'roles': headers.get('X-Roles', '').split(','),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # NOTE(adam_g): We also check the previous 'admin' rule to ensure
 | 
			
		||||
        # compat with default juno policy.json.  This double check may be
 | 
			
		||||
        # removed in L.
 | 
			
		||||
        is_admin = (policy.enforce('admin_api', creds, creds) or
 | 
			
		||||
                    policy.enforce('admin', creds, creds))
 | 
			
		||||
        is_public_api = state.request.environ.get('is_public_api', False)
 | 
			
		||||
        show_password = policy.enforce('show_password', creds, creds)
 | 
			
		||||
 | 
			
		||||
        state.request.context = context.RequestContext(
 | 
			
		||||
            is_admin=is_admin,
 | 
			
		||||
            is_public_api=is_public_api,
 | 
			
		||||
            show_password=show_password,
 | 
			
		||||
            **creds)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RPCHook(hooks.PecanHook):
 | 
			
		||||
    """Attach the rpcapi object to the request so controllers can get to it."""
 | 
			
		||||
 | 
			
		||||
    def before(self, state):
 | 
			
		||||
        state.request.rpcapi = rpcapi.ConductorAPI()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TrustedCallHook(hooks.PecanHook):
 | 
			
		||||
    """Verify that the user has admin rights.
 | 
			
		||||
 | 
			
		||||
    Checks whether the API call is performed against a public
 | 
			
		||||
    resource or the user has admin privileges in the appropriate
 | 
			
		||||
    tenant, domain or other administrative unit.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    def before(self, state):
 | 
			
		||||
        ctx = state.request.context
 | 
			
		||||
        if ctx.is_public_api:
 | 
			
		||||
            return
 | 
			
		||||
        policy.enforce('admin_api', ctx.to_dict(), ctx.to_dict(),
 | 
			
		||||
                       do_raise=True, exc=exc.HTTPForbidden)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoExceptionTracebackHook(hooks.PecanHook):
 | 
			
		||||
    """Workaround rpc.common: deserialize_remote_exception.
 | 
			
		||||
 | 
			
		||||
    deserialize_remote_exception builds rpc exception traceback into error
 | 
			
		||||
    message which is then sent to the client. Such behavior is a security
 | 
			
		||||
    concern so this hook is aimed to cut-off traceback from the error message.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    # NOTE(max_lobur): 'after' hook used instead of 'on_error' because
 | 
			
		||||
    # '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:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Do nothing if there is no error.
 | 
			
		||||
        if 200 <= state.response.status_int < 400:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        json_body = state.response.json
 | 
			
		||||
        # Do not remove traceback when server in debug mode (except 'Server'
 | 
			
		||||
        # errors when 'debuginfo' will be used for traces).
 | 
			
		||||
        if cfg.CONF.debug and json_body.get('faultcode') != 'Server':
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        faultstring = json_body.get('faultstring')
 | 
			
		||||
        traceback_marker = 'Traceback (most recent call last):'
 | 
			
		||||
        if faultstring and traceback_marker in faultstring:
 | 
			
		||||
            # Cut-off traceback.
 | 
			
		||||
            faultstring = faultstring.split(traceback_marker, 1)[0]
 | 
			
		||||
            # Remove trailing newlines and spaces if any.
 | 
			
		||||
            json_body['faultstring'] = faultstring.rstrip()
 | 
			
		||||
            # Replace the whole json. Cannot change original one beacause it's
 | 
			
		||||
            # generated on the fly.
 | 
			
		||||
            state.response.json = json_body
 | 
			
		||||
							
								
								
									
										23
									
								
								iotronic/api/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								iotronic/api/middleware/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
# -*- encoding: 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 iotronic.api.middleware import auth_token
 | 
			
		||||
from iotronic.api.middleware import parsable_error
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ParsableErrorMiddleware = parsable_error.ParsableErrorMiddleware
 | 
			
		||||
AuthTokenMiddleware = auth_token.AuthTokenMiddleware
 | 
			
		||||
 | 
			
		||||
__all__ = (ParsableErrorMiddleware,
 | 
			
		||||
           AuthTokenMiddleware)
 | 
			
		||||
							
								
								
									
										62
									
								
								iotronic/api/middleware/auth_token.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								iotronic/api/middleware/auth_token.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
# -*- encoding: 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 re
 | 
			
		||||
 | 
			
		||||
from keystonemiddleware import auth_token
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common import utils
 | 
			
		||||
 | 
			
		||||
LOG = log.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AuthTokenMiddleware(auth_token.AuthProtocol):
 | 
			
		||||
    """A wrapper on Keystone auth_token middleware.
 | 
			
		||||
 | 
			
		||||
    Does not perform verification of authentication tokens
 | 
			
		||||
    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
 | 
			
		||||
        route_pattern_tpl = '%s(\.json|\.xml)?$'
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.public_api_routes = [re.compile(route_pattern_tpl % route_tpl)
 | 
			
		||||
                                      for route_tpl in public_api_routes]
 | 
			
		||||
        except re.error as e:
 | 
			
		||||
            msg = _('Cannot compile public API routes: %s') % e
 | 
			
		||||
 | 
			
		||||
            LOG.error(msg)
 | 
			
		||||
            raise exception.ConfigInvalid(error_msg=msg)
 | 
			
		||||
 | 
			
		||||
        super(AuthTokenMiddleware, self).__init__(app, conf)
 | 
			
		||||
 | 
			
		||||
    def __call__(self, env, start_response):
 | 
			
		||||
        path = utils.safe_rstrip(env.get('PATH_INFO'), '/')
 | 
			
		||||
 | 
			
		||||
        # The information whether the API call is being performed against the
 | 
			
		||||
        # public API is required for some other components. Saving it to the
 | 
			
		||||
        # WSGI environment is reasonable thereby.
 | 
			
		||||
        env['is_public_api'] = any(map(lambda pattern: re.match(pattern, path),
 | 
			
		||||
                                       self.public_api_routes))
 | 
			
		||||
 | 
			
		||||
        if env['is_public_api']:
 | 
			
		||||
            return self._app(env, start_response)
 | 
			
		||||
 | 
			
		||||
        return super(AuthTokenMiddleware, self).__call__(env, start_response)
 | 
			
		||||
							
								
								
									
										94
									
								
								iotronic/api/middleware/parsable_error.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								iotronic/api/middleware/parsable_error.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright © 2012 New Dream Network, LLC (DreamHost)
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
# not use this file except in compliance with the License. You may obtain
 | 
			
		||||
# a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#      http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
# License for the specific language governing permissions and limitations
 | 
			
		||||
# under the License.
 | 
			
		||||
"""
 | 
			
		||||
Middleware to replace the plain text message body of an error
 | 
			
		||||
response with one formatted so the client can parse it.
 | 
			
		||||
 | 
			
		||||
Based on pecan.middleware.errordocument
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
from xml import etree as et
 | 
			
		||||
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
import six
 | 
			
		||||
import webob
 | 
			
		||||
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
 | 
			
		||||
LOG = log.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ParsableErrorMiddleware(object):
 | 
			
		||||
    """Replace error body with something the client can parse."""
 | 
			
		||||
    def __init__(self, app):
 | 
			
		||||
        self.app = app
 | 
			
		||||
 | 
			
		||||
    def __call__(self, environ, start_response):
 | 
			
		||||
        # Request for this state, modified by replace_start_response()
 | 
			
		||||
        # and used when an error is being reported.
 | 
			
		||||
        state = {}
 | 
			
		||||
 | 
			
		||||
        def replacement_start_response(status, headers, exc_info=None):
 | 
			
		||||
            """Overrides the default response to make errors parsable."""
 | 
			
		||||
            try:
 | 
			
		||||
                status_code = int(status.split(' ')[0])
 | 
			
		||||
                state['status_code'] = status_code
 | 
			
		||||
            except (ValueError, TypeError):  # pragma: nocover
 | 
			
		||||
                raise Exception(_(
 | 
			
		||||
                    'ErrorDocumentMiddleware received an invalid '
 | 
			
		||||
                    'status %s') % status)
 | 
			
		||||
            else:
 | 
			
		||||
                if (state['status_code'] // 100) not in (2, 3):
 | 
			
		||||
                    # Remove some headers so we can replace them later
 | 
			
		||||
                    # when we have the full error message and can
 | 
			
		||||
                    # compute the length.
 | 
			
		||||
                    headers = [(h, v)
 | 
			
		||||
                               for (h, v) in headers
 | 
			
		||||
                               if h not in ('Content-Length', 'Content-Type')
 | 
			
		||||
                               ]
 | 
			
		||||
                # Save the headers in case we need to modify them.
 | 
			
		||||
                state['headers'] = headers
 | 
			
		||||
                return start_response(status, headers, exc_info)
 | 
			
		||||
 | 
			
		||||
        app_iter = self.app(environ, replacement_start_response)
 | 
			
		||||
        if (state['status_code'] // 100) not in (2, 3):
 | 
			
		||||
            req = webob.Request(environ)
 | 
			
		||||
            if (req.accept.best_match(['application/json', 'application/xml'])
 | 
			
		||||
                == 'application/xml'):
 | 
			
		||||
                try:
 | 
			
		||||
                    # simple check xml is valid
 | 
			
		||||
                    body = [et.ElementTree.tostring(
 | 
			
		||||
                            et.ElementTree.fromstring('<error_message>'
 | 
			
		||||
                                                      + '\n'.join(app_iter)
 | 
			
		||||
                                                      + '</error_message>'))]
 | 
			
		||||
                except et.ElementTree.ParseError as err:
 | 
			
		||||
                    LOG.error(_LE('Error parsing HTTP response: %s'), err)
 | 
			
		||||
                    body = ['<error_message>%s' % state['status_code']
 | 
			
		||||
                            + '</error_message>']
 | 
			
		||||
                state['headers'].append(('Content-Type', 'application/xml'))
 | 
			
		||||
            else:
 | 
			
		||||
                if six.PY3:
 | 
			
		||||
                    app_iter = [i.decode('utf-8') for i in app_iter]
 | 
			
		||||
                body = [json.dumps({'error_message': '\n'.join(app_iter)})]
 | 
			
		||||
                if six.PY3:
 | 
			
		||||
                    body = [item.encode('utf-8') for item in body]
 | 
			
		||||
                state['headers'].append(('Content-Type', 'application/json'))
 | 
			
		||||
            state['headers'].append(('Content-Length', str(len(body[0]))))
 | 
			
		||||
        else:
 | 
			
		||||
            body = app_iter
 | 
			
		||||
        return body
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/common/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										42
									
								
								iotronic/common/boot_devices.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								iotronic/common/boot_devices.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
# Copyright 2014 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Mapping of boot devices used when requesting the system to boot
 | 
			
		||||
from an alternate device.
 | 
			
		||||
 | 
			
		||||
The options presented were based on the IPMItool chassis
 | 
			
		||||
bootdev command. You can find the documentation at:
 | 
			
		||||
http://linux.die.net/man/1/ipmitool
 | 
			
		||||
 | 
			
		||||
NOTE: This module does not include all the options from ipmitool because
 | 
			
		||||
they don't make sense in the limited context of Iotronic right now.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
PXE = 'pxe'
 | 
			
		||||
"Boot from PXE boot"
 | 
			
		||||
 | 
			
		||||
DISK = 'disk'
 | 
			
		||||
"Boot from default Hard-drive"
 | 
			
		||||
 | 
			
		||||
CDROM = 'cdrom'
 | 
			
		||||
"Boot from CD/DVD"
 | 
			
		||||
 | 
			
		||||
BIOS = 'bios'
 | 
			
		||||
"Boot into BIOS setup"
 | 
			
		||||
 | 
			
		||||
SAFE = 'safe'
 | 
			
		||||
"Boot from default Hard-drive, request Safe Mode"
 | 
			
		||||
							
								
								
									
										31
									
								
								iotronic/common/config.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								iotronic/common/config.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
# Copyright 2012 Red Hat, Inc.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
 | 
			
		||||
from iotronic.common import rpc
 | 
			
		||||
from iotronic import version
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_args(argv, default_config_files=None):
 | 
			
		||||
    rpc.set_defaults(control_exchange='iotronic')
 | 
			
		||||
    cfg.CONF(argv[1:],
 | 
			
		||||
             project='iotronic',
 | 
			
		||||
             version=version.version_info.release_string(),
 | 
			
		||||
             #version='2015.7',
 | 
			
		||||
             default_config_files=default_config_files)
 | 
			
		||||
    rpc.init(cfg.CONF)
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/common/config_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/common/config_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										333
									
								
								iotronic/common/config_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										333
									
								
								iotronic/common/config_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,333 @@
 | 
			
		||||
# Copyright 2012 SINA Corporation
 | 
			
		||||
# Copyright 2014 Cisco Systems, 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.
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
"""Extracts OpenStack config option info from module(s)."""
 | 
			
		||||
 | 
			
		||||
# NOTE(GheRivero): Copied from oslo_incubator before getting removed in
 | 
			
		||||
#  Change-Id: If15b77d31a8c615aad8fca30f6dd9928da2d08bb
 | 
			
		||||
 | 
			
		||||
from __future__ import print_function
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import imp
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
import socket
 | 
			
		||||
import sys
 | 
			
		||||
import textwrap
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import oslo_i18n
 | 
			
		||||
from oslo_utils import importutils
 | 
			
		||||
import six
 | 
			
		||||
import stevedore.named
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
oslo_i18n.install('iotronic')
 | 
			
		||||
 | 
			
		||||
STROPT = "StrOpt"
 | 
			
		||||
BOOLOPT = "BoolOpt"
 | 
			
		||||
INTOPT = "IntOpt"
 | 
			
		||||
FLOATOPT = "FloatOpt"
 | 
			
		||||
LISTOPT = "ListOpt"
 | 
			
		||||
DICTOPT = "DictOpt"
 | 
			
		||||
MULTISTROPT = "MultiStrOpt"
 | 
			
		||||
 | 
			
		||||
OPT_TYPES = {
 | 
			
		||||
    STROPT: 'string value',
 | 
			
		||||
    BOOLOPT: 'boolean value',
 | 
			
		||||
    INTOPT: 'integer value',
 | 
			
		||||
    FLOATOPT: 'floating point value',
 | 
			
		||||
    LISTOPT: 'list value',
 | 
			
		||||
    DICTOPT: 'dict value',
 | 
			
		||||
    MULTISTROPT: 'multi valued',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
OPTION_REGEX = re.compile(r"(%s)" % "|".join([STROPT, BOOLOPT, INTOPT,
 | 
			
		||||
                                              FLOATOPT, LISTOPT, DICTOPT,
 | 
			
		||||
                                              MULTISTROPT]))
 | 
			
		||||
 | 
			
		||||
PY_EXT = ".py"
 | 
			
		||||
BASEDIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
 | 
			
		||||
                                       "../../../../"))
 | 
			
		||||
WORDWRAP_WIDTH = 60
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def raise_extension_exception(extmanager, ep, err):
 | 
			
		||||
    raise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate(argv):
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description='generate sample configuration file',
 | 
			
		||||
    )
 | 
			
		||||
    parser.add_argument('-m', dest='modules', action='append')
 | 
			
		||||
    parser.add_argument('-l', dest='libraries', action='append')
 | 
			
		||||
    parser.add_argument('srcfiles', nargs='*')
 | 
			
		||||
    parsed_args = parser.parse_args(argv)
 | 
			
		||||
 | 
			
		||||
    mods_by_pkg = dict()
 | 
			
		||||
    for filepath in parsed_args.srcfiles:
 | 
			
		||||
        pkg_name = filepath.split(os.sep)[1]
 | 
			
		||||
        mod_str = '.'.join(['.'.join(filepath.split(os.sep)[:-1]),
 | 
			
		||||
                            os.path.basename(filepath).split('.')[0]])
 | 
			
		||||
        mods_by_pkg.setdefault(pkg_name, list()).append(mod_str)
 | 
			
		||||
    # NOTE(lzyeval): place top level modules before packages
 | 
			
		||||
    pkg_names = sorted(pkg for pkg in mods_by_pkg if pkg.endswith(PY_EXT))
 | 
			
		||||
    ext_names = sorted(pkg for pkg in mods_by_pkg if pkg not in pkg_names)
 | 
			
		||||
    pkg_names.extend(ext_names)
 | 
			
		||||
 | 
			
		||||
    # opts_by_group is a mapping of group name to an options list
 | 
			
		||||
    # The options list is a list of (module, options) tuples
 | 
			
		||||
    opts_by_group = {'DEFAULT': []}
 | 
			
		||||
 | 
			
		||||
    if parsed_args.modules:
 | 
			
		||||
        for module_name in parsed_args.modules:
 | 
			
		||||
            module = _import_module(module_name)
 | 
			
		||||
            if module:
 | 
			
		||||
                for group, opts in _list_opts(module):
 | 
			
		||||
                    opts_by_group.setdefault(group, []).append((module_name,
 | 
			
		||||
                                                                opts))
 | 
			
		||||
 | 
			
		||||
    # Look for entry points defined in libraries (or applications) for
 | 
			
		||||
    # option discovery, and include their return values in the output.
 | 
			
		||||
    #
 | 
			
		||||
    # Each entry point should be a function returning an iterable
 | 
			
		||||
    # of pairs with the group name (or None for the default group)
 | 
			
		||||
    # and the list of Opt instances for that group.
 | 
			
		||||
    if parsed_args.libraries:
 | 
			
		||||
        loader = stevedore.named.NamedExtensionManager(
 | 
			
		||||
            'oslo.config.opts',
 | 
			
		||||
            names=list(set(parsed_args.libraries)),
 | 
			
		||||
            invoke_on_load=False,
 | 
			
		||||
            on_load_failure_callback=raise_extension_exception
 | 
			
		||||
        )
 | 
			
		||||
        for ext in loader:
 | 
			
		||||
            for group, opts in ext.plugin():
 | 
			
		||||
                opt_list = opts_by_group.setdefault(group or 'DEFAULT', [])
 | 
			
		||||
                opt_list.append((ext.name, opts))
 | 
			
		||||
 | 
			
		||||
    for pkg_name in pkg_names:
 | 
			
		||||
        mods = mods_by_pkg.get(pkg_name)
 | 
			
		||||
        mods.sort()
 | 
			
		||||
        for mod_str in mods:
 | 
			
		||||
            if mod_str.endswith('.__init__'):
 | 
			
		||||
                mod_str = mod_str[:mod_str.rfind(".")]
 | 
			
		||||
 | 
			
		||||
            mod_obj = _import_module(mod_str)
 | 
			
		||||
            if not mod_obj:
 | 
			
		||||
                raise RuntimeError("Unable to import module %s" % mod_str)
 | 
			
		||||
 | 
			
		||||
            for group, opts in _list_opts(mod_obj):
 | 
			
		||||
                opts_by_group.setdefault(group, []).append((mod_str, opts))
 | 
			
		||||
 | 
			
		||||
    print_group_opts('DEFAULT', opts_by_group.pop('DEFAULT', []))
 | 
			
		||||
    for group in sorted(opts_by_group.keys()):
 | 
			
		||||
        print_group_opts(group, opts_by_group[group])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _import_module(mod_str):
 | 
			
		||||
    try:
 | 
			
		||||
        if mod_str.startswith('bin.'):
 | 
			
		||||
            imp.load_source(mod_str[4:], os.path.join('bin', mod_str[4:]))
 | 
			
		||||
            return sys.modules[mod_str[4:]]
 | 
			
		||||
        else:
 | 
			
		||||
            return importutils.import_module(mod_str)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        sys.stderr.write("Error importing module %s: %s\n" % (mod_str, str(e)))
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_in_group(opt, group):
 | 
			
		||||
    """Check if opt is in group."""
 | 
			
		||||
    for value in group._opts.values():
 | 
			
		||||
        # NOTE(llu): Temporary workaround for bug #1262148, wait until
 | 
			
		||||
        # newly released oslo.config support '==' operator.
 | 
			
		||||
        if not(value['opt'] != opt):
 | 
			
		||||
            return True
 | 
			
		||||
    return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _guess_groups(opt):
 | 
			
		||||
    # is it in the DEFAULT group?
 | 
			
		||||
    if _is_in_group(opt, cfg.CONF):
 | 
			
		||||
        return 'DEFAULT'
 | 
			
		||||
 | 
			
		||||
    # what other groups is it in?
 | 
			
		||||
    for value in cfg.CONF.values():
 | 
			
		||||
        if isinstance(value, cfg.CONF.GroupAttr):
 | 
			
		||||
            if _is_in_group(opt, value._group):
 | 
			
		||||
                return value._group.name
 | 
			
		||||
 | 
			
		||||
    raise RuntimeError(
 | 
			
		||||
        "Unable to find group for option %s, "
 | 
			
		||||
        "maybe it's defined twice in the same group?"
 | 
			
		||||
        % opt.name
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _list_opts(obj):
 | 
			
		||||
    def is_opt(o):
 | 
			
		||||
        return (isinstance(o, cfg.Opt) and
 | 
			
		||||
                not isinstance(o, cfg.SubCommandOpt))
 | 
			
		||||
 | 
			
		||||
    opts = list()
 | 
			
		||||
 | 
			
		||||
    if 'list_opts' in dir(obj):
 | 
			
		||||
        group_opts = getattr(obj, 'list_opts')()
 | 
			
		||||
        # NOTE(GheRivero): Options without a defined group,
 | 
			
		||||
        # must be registered to the DEFAULT section
 | 
			
		||||
        fixed_list = []
 | 
			
		||||
        for section, opts in group_opts:
 | 
			
		||||
            if not section:
 | 
			
		||||
                section = 'DEFAULT'
 | 
			
		||||
            fixed_list.append((section, opts))
 | 
			
		||||
        return fixed_list
 | 
			
		||||
 | 
			
		||||
    for attr_str in dir(obj):
 | 
			
		||||
        attr_obj = getattr(obj, attr_str)
 | 
			
		||||
        if is_opt(attr_obj):
 | 
			
		||||
            opts.append(attr_obj)
 | 
			
		||||
        elif (isinstance(attr_obj, list) and
 | 
			
		||||
              all(map(lambda x: is_opt(x), attr_obj))):
 | 
			
		||||
            opts.extend(attr_obj)
 | 
			
		||||
 | 
			
		||||
    ret = {}
 | 
			
		||||
    for opt in opts:
 | 
			
		||||
        ret.setdefault(_guess_groups(opt), []).append(opt)
 | 
			
		||||
    return ret.items()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def print_group_opts(group, opts_by_module):
 | 
			
		||||
    print("[%s]" % group)
 | 
			
		||||
    print('')
 | 
			
		||||
    for mod, opts in opts_by_module:
 | 
			
		||||
        print('#')
 | 
			
		||||
        print('# Options defined in %s' % mod)
 | 
			
		||||
        print('#')
 | 
			
		||||
        print('')
 | 
			
		||||
        for opt in opts:
 | 
			
		||||
            _print_opt(opt)
 | 
			
		||||
        print('')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_my_ip():
 | 
			
		||||
    try:
 | 
			
		||||
        csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
 | 
			
		||||
        csock.connect(('8.8.8.8', 80))
 | 
			
		||||
        (addr, port) = csock.getsockname()
 | 
			
		||||
        csock.close()
 | 
			
		||||
        return addr
 | 
			
		||||
    except socket.error:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _sanitize_default(name, value):
 | 
			
		||||
    """Set up a reasonably sensible default for pybasedir, my_ip and host."""
 | 
			
		||||
    hostname = socket.gethostname()
 | 
			
		||||
    fqdn = socket.getfqdn()
 | 
			
		||||
    if value.startswith(sys.prefix):
 | 
			
		||||
        # NOTE(jd) Don't use os.path.join, because it is likely to think the
 | 
			
		||||
        # second part is an absolute pathname and therefore drop the first
 | 
			
		||||
        # part.
 | 
			
		||||
        value = os.path.normpath("/usr/" + value[len(sys.prefix):])
 | 
			
		||||
    elif value.startswith(BASEDIR):
 | 
			
		||||
        return value.replace(BASEDIR, '/usr/lib/python/site-packages')
 | 
			
		||||
    elif BASEDIR in value:
 | 
			
		||||
        return value.replace(BASEDIR, '')
 | 
			
		||||
    elif value == _get_my_ip():
 | 
			
		||||
        return '10.0.0.1'
 | 
			
		||||
    elif value in (hostname, fqdn):
 | 
			
		||||
        if 'host' in name:
 | 
			
		||||
            return 'iotronic'
 | 
			
		||||
    elif value.endswith(hostname):
 | 
			
		||||
        return value.replace(hostname, 'iotronic')
 | 
			
		||||
    elif value.endswith(fqdn):
 | 
			
		||||
        return value.replace(fqdn, 'iotronic')
 | 
			
		||||
    elif value.strip() != value:
 | 
			
		||||
        return '"%s"' % value
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _print_opt(opt):
 | 
			
		||||
    opt_name, opt_default, opt_help = opt.dest, opt.default, opt.help
 | 
			
		||||
    if not opt_help:
 | 
			
		||||
        sys.stderr.write('WARNING: "%s" is missing help string.\n' % opt_name)
 | 
			
		||||
        opt_help = ""
 | 
			
		||||
    try:
 | 
			
		||||
        opt_type = OPTION_REGEX.search(str(type(opt))).group(0)
 | 
			
		||||
    except (ValueError, AttributeError) as err:
 | 
			
		||||
        sys.stderr.write("%s\n" % str(err))
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
    opt_help = u'%s (%s)' % (opt_help,
 | 
			
		||||
                             OPT_TYPES[opt_type])
 | 
			
		||||
    print('#', "\n# ".join(textwrap.wrap(opt_help, WORDWRAP_WIDTH)))
 | 
			
		||||
    if opt.deprecated_opts:
 | 
			
		||||
        for deprecated_opt in opt.deprecated_opts:
 | 
			
		||||
            if deprecated_opt.name:
 | 
			
		||||
                deprecated_group = (deprecated_opt.group if
 | 
			
		||||
                                    deprecated_opt.group else "DEFAULT")
 | 
			
		||||
                print('# Deprecated group/name - [%s]/%s' %
 | 
			
		||||
                      (deprecated_group,
 | 
			
		||||
                       deprecated_opt.name))
 | 
			
		||||
    try:
 | 
			
		||||
        if opt_default is None:
 | 
			
		||||
            print('#%s=<None>' % opt_name)
 | 
			
		||||
        else:
 | 
			
		||||
            _print_type(opt_type, opt_name, opt_default)
 | 
			
		||||
        print('')
 | 
			
		||||
    except Exception:
 | 
			
		||||
        sys.stderr.write('Error in option "%s"\n' % opt_name)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _print_type(opt_type, opt_name, opt_default):
 | 
			
		||||
    if opt_type == STROPT:
 | 
			
		||||
        assert(isinstance(opt_default, six.string_types))
 | 
			
		||||
        print('#%s=%s' % (opt_name, _sanitize_default(opt_name,
 | 
			
		||||
                                                      opt_default)))
 | 
			
		||||
    elif opt_type == BOOLOPT:
 | 
			
		||||
        assert(isinstance(opt_default, bool))
 | 
			
		||||
        print('#%s=%s' % (opt_name, str(opt_default).lower()))
 | 
			
		||||
    elif opt_type == INTOPT:
 | 
			
		||||
        assert(isinstance(opt_default, int) and
 | 
			
		||||
               not isinstance(opt_default, bool))
 | 
			
		||||
        print('#%s=%s' % (opt_name, opt_default))
 | 
			
		||||
    elif opt_type == FLOATOPT:
 | 
			
		||||
        assert(isinstance(opt_default, float))
 | 
			
		||||
        print('#%s=%s' % (opt_name, opt_default))
 | 
			
		||||
    elif opt_type == LISTOPT:
 | 
			
		||||
        assert(isinstance(opt_default, list))
 | 
			
		||||
        print('#%s=%s' % (opt_name, ','.join(opt_default)))
 | 
			
		||||
    elif opt_type == DICTOPT:
 | 
			
		||||
        assert(isinstance(opt_default, dict))
 | 
			
		||||
        opt_default_strlist = [str(key) + ':' + str(value)
 | 
			
		||||
                               for (key, value) in opt_default.items()]
 | 
			
		||||
        print('#%s=%s' % (opt_name, ','.join(opt_default_strlist)))
 | 
			
		||||
    elif opt_type == MULTISTROPT:
 | 
			
		||||
        assert(isinstance(opt_default, list))
 | 
			
		||||
        if not opt_default:
 | 
			
		||||
            opt_default = ['']
 | 
			
		||||
        for default in opt_default:
 | 
			
		||||
            print('#%s=%s' % (opt_name, default))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    generate(sys.argv[1:])
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										67
									
								
								iotronic/common/context.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								iotronic/common/context.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
# -*- encoding: 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_context import context
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RequestContext(context.RequestContext):
 | 
			
		||||
    """Extends security contexts from the OpenStack common library."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self, auth_token=None, domain_id=None, domain_name=None,
 | 
			
		||||
                 user=None, tenant=None, is_admin=False, is_public_api=False,
 | 
			
		||||
                 read_only=False, show_deleted=False, request_id=None,
 | 
			
		||||
                 roles=None, show_password=True):
 | 
			
		||||
        """Stores several additional request parameters:
 | 
			
		||||
 | 
			
		||||
        :param domain_id: The ID of the domain.
 | 
			
		||||
        :param domain_name: The name of the domain.
 | 
			
		||||
        :param is_public_api: Specifies whether the request should be processed
 | 
			
		||||
                              without authentication.
 | 
			
		||||
        :param roles: List of user's roles if any.
 | 
			
		||||
        :param show_password: Specifies whether passwords should be masked
 | 
			
		||||
                              before sending back to API call.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.is_public_api = is_public_api
 | 
			
		||||
        self.domain_id = domain_id
 | 
			
		||||
        self.domain_name = domain_name
 | 
			
		||||
        self.roles = roles or []
 | 
			
		||||
        self.show_password = show_password
 | 
			
		||||
 | 
			
		||||
        super(RequestContext, self).__init__(auth_token=auth_token,
 | 
			
		||||
                                             user=user, tenant=tenant,
 | 
			
		||||
                                             is_admin=is_admin,
 | 
			
		||||
                                             read_only=read_only,
 | 
			
		||||
                                             show_deleted=show_deleted,
 | 
			
		||||
                                             request_id=request_id)
 | 
			
		||||
 | 
			
		||||
    def to_dict(self):
 | 
			
		||||
        return {'auth_token': self.auth_token,
 | 
			
		||||
                'user': self.user,
 | 
			
		||||
                'tenant': self.tenant,
 | 
			
		||||
                'is_admin': self.is_admin,
 | 
			
		||||
                'read_only': self.read_only,
 | 
			
		||||
                'show_deleted': self.show_deleted,
 | 
			
		||||
                'request_id': self.request_id,
 | 
			
		||||
                'domain_id': self.domain_id,
 | 
			
		||||
                'roles': self.roles,
 | 
			
		||||
                'domain_name': self.domain_name,
 | 
			
		||||
                'show_password': self.show_password,
 | 
			
		||||
                'is_public_api': self.is_public_api}
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def from_dict(cls, values):
 | 
			
		||||
        values.pop('user', None)
 | 
			
		||||
        values.pop('tenant', None)
 | 
			
		||||
        return cls(**values)
 | 
			
		||||
							
								
								
									
										100
									
								
								iotronic/common/dhcp_factory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								iotronic/common/dhcp_factory.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,100 @@
 | 
			
		||||
# Copyright 2014 Rackspace, Inc.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from oslo_concurrency import lockutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import stevedore
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
dhcp_provider_opts = [
 | 
			
		||||
    cfg.StrOpt('dhcp_provider',
 | 
			
		||||
               default='neutron',
 | 
			
		||||
               help='DHCP provider to use. "neutron" uses Neutron, and '
 | 
			
		||||
               '"none" uses a no-op provider.'
 | 
			
		||||
               ),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(dhcp_provider_opts, group='dhcp')
 | 
			
		||||
 | 
			
		||||
_dhcp_provider = None
 | 
			
		||||
 | 
			
		||||
EM_SEMAPHORE = 'dhcp_provider'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DHCPFactory(object):
 | 
			
		||||
 | 
			
		||||
    # NOTE(lucasagomes): Instantiate a stevedore.driver.DriverManager
 | 
			
		||||
    #                    only once, the first time DHCPFactory.__init__
 | 
			
		||||
    #                    is called.
 | 
			
		||||
    _dhcp_provider = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        if not DHCPFactory._dhcp_provider:
 | 
			
		||||
            DHCPFactory._set_dhcp_provider(**kwargs)
 | 
			
		||||
 | 
			
		||||
    # NOTE(lucasagomes): Use lockutils to avoid a potential race in eventlet
 | 
			
		||||
    #                    that might try to create two dhcp factories.
 | 
			
		||||
    @classmethod
 | 
			
		||||
    @lockutils.synchronized(EM_SEMAPHORE, 'iotronic-')
 | 
			
		||||
    def _set_dhcp_provider(cls, **kwargs):
 | 
			
		||||
        """Initialize the dhcp provider
 | 
			
		||||
 | 
			
		||||
        :raises: DHCPLoadError if the dhcp_provider cannot be loaded.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        # NOTE(lucasagomes): In case multiple greenthreads queue up on
 | 
			
		||||
        #                    this lock before _dhcp_provider is initialized,
 | 
			
		||||
        #                    prevent creation of multiple DriverManager.
 | 
			
		||||
        if cls._dhcp_provider:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        dhcp_provider_name = CONF.dhcp.dhcp_provider
 | 
			
		||||
        try:
 | 
			
		||||
            _extension_manager = stevedore.driver.DriverManager(
 | 
			
		||||
                'iotronic.dhcp',
 | 
			
		||||
                dhcp_provider_name,
 | 
			
		||||
                invoke_kwds=kwargs,
 | 
			
		||||
                invoke_on_load=True)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            raise exception.DHCPLoadError(
 | 
			
		||||
                dhcp_provider_name=dhcp_provider_name, reason=e
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        cls._dhcp_provider = _extension_manager.driver
 | 
			
		||||
 | 
			
		||||
    def update_dhcp(self, task, dhcp_opts, ports=None):
 | 
			
		||||
        """Send or update the DHCP BOOT options for this node.
 | 
			
		||||
 | 
			
		||||
        :param task: A TaskManager instance.
 | 
			
		||||
        :param dhcp_opts: this will be a list of dicts, e.g.
 | 
			
		||||
 | 
			
		||||
                          ::
 | 
			
		||||
 | 
			
		||||
                           [{'opt_name': 'bootfile-name',
 | 
			
		||||
                             'opt_value': 'pxelinux.0'},
 | 
			
		||||
                            {'opt_name': 'server-ip-address',
 | 
			
		||||
                             'opt_value': '123.123.123.456'},
 | 
			
		||||
                            {'opt_name': 'tftp-server',
 | 
			
		||||
                             'opt_value': '123.123.123.123'}]
 | 
			
		||||
        :param ports: a list of Neutron port dicts to update DHCP options on.
 | 
			
		||||
            If None, will get the list of ports from the Iotronic port objects.
 | 
			
		||||
        """
 | 
			
		||||
        self.provider.update_dhcp_opts(task, dhcp_opts, ports)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def provider(self):
 | 
			
		||||
        return self._dhcp_provider
 | 
			
		||||
							
								
								
									
										211
									
								
								iotronic/common/disk_partitioner.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										211
									
								
								iotronic/common/disk_partitioner.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,211 @@
 | 
			
		||||
# Copyright 2014 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 re
 | 
			
		||||
 | 
			
		||||
from oslo_concurrency import processutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common.i18n import _LW
 | 
			
		||||
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.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
opt_group = cfg.OptGroup(name='disk_partitioner',
 | 
			
		||||
                         title='Options for the disk partitioner')
 | 
			
		||||
CONF.register_group(opt_group)
 | 
			
		||||
CONF.register_opts(opts, opt_group)
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DiskPartitioner(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, device, disk_label='msdos', alignment='optimal'):
 | 
			
		||||
        """A convenient wrapper around the parted tool.
 | 
			
		||||
 | 
			
		||||
        :param device: The device path.
 | 
			
		||||
        :param disk_label: The type of the partition table. Valid types are:
 | 
			
		||||
                           "bsd", "dvh", "gpt", "loop", "mac", "msdos",
 | 
			
		||||
                           "pc98", or "sun".
 | 
			
		||||
        :param alignment: Set alignment for newly created partitions.
 | 
			
		||||
                          Valid types are: none, cylinder, minimal and
 | 
			
		||||
                          optimal.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self._device = device
 | 
			
		||||
        self._disk_label = disk_label
 | 
			
		||||
        self._alignment = alignment
 | 
			
		||||
        self._partitions = []
 | 
			
		||||
        self._fuser_pids_re = re.compile(r'((\d)+\s*)+')
 | 
			
		||||
 | 
			
		||||
    def _exec(self, *args):
 | 
			
		||||
        # NOTE(lucasagomes): utils.execute() is already a wrapper on top
 | 
			
		||||
        #                    of processutils.execute() which raises specific
 | 
			
		||||
        #                    exceptions. It also logs any failure so we don't
 | 
			
		||||
        #                    need to log it again here.
 | 
			
		||||
        utils.execute('parted', '-a', self._alignment, '-s', self._device,
 | 
			
		||||
                      '--', 'unit', 'MiB', *args, check_exit_code=[0],
 | 
			
		||||
                      run_as_root=True)
 | 
			
		||||
 | 
			
		||||
    def add_partition(self, size, part_type='primary', fs_type='',
 | 
			
		||||
                      bootable=False):
 | 
			
		||||
        """Add a partition.
 | 
			
		||||
 | 
			
		||||
        :param size: The size of the partition in MiB.
 | 
			
		||||
        :param part_type: The type of the partition. Valid values are:
 | 
			
		||||
                          primary, logical, or extended.
 | 
			
		||||
        :param fs_type: The filesystem type. Valid types are: ext2, fat32,
 | 
			
		||||
                        fat16, HFS, linux-swap, NTFS, reiserfs, ufs.
 | 
			
		||||
                        If blank (''), it will create a Linux native
 | 
			
		||||
                        partition (83).
 | 
			
		||||
        :param bootable: Boolean value; whether the partition is bootable
 | 
			
		||||
                         or not.
 | 
			
		||||
        :returns: The partition number.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self._partitions.append({'size': size,
 | 
			
		||||
                                 'type': part_type,
 | 
			
		||||
                                 'fs_type': fs_type,
 | 
			
		||||
                                 'bootable': bootable})
 | 
			
		||||
        return len(self._partitions)
 | 
			
		||||
 | 
			
		||||
    def get_partitions(self):
 | 
			
		||||
        """Get the partitioning layout.
 | 
			
		||||
 | 
			
		||||
        :returns: An iterator with the partition number and the
 | 
			
		||||
                  partition layout.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        return enumerate(self._partitions, 1)
 | 
			
		||||
 | 
			
		||||
    def _wait_for_disk_to_become_available(self, retries, max_retries, pids,
 | 
			
		||||
                                           stderr):
 | 
			
		||||
        retries[0] += 1
 | 
			
		||||
        if retries[0] > max_retries:
 | 
			
		||||
            raise loopingcall.LoopingCallDone()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # NOTE(ifarkas): fuser returns a non-zero return code if none of
 | 
			
		||||
            #                the specified files is accessed
 | 
			
		||||
            out, err = utils.execute('fuser', self._device,
 | 
			
		||||
                                     check_exit_code=[0, 1], run_as_root=True)
 | 
			
		||||
 | 
			
		||||
            if not out and not err:
 | 
			
		||||
                raise loopingcall.LoopingCallDone()
 | 
			
		||||
            else:
 | 
			
		||||
                if err:
 | 
			
		||||
                    stderr[0] = err
 | 
			
		||||
                if out:
 | 
			
		||||
                    pids_match = re.search(self._fuser_pids_re, out)
 | 
			
		||||
                    pids[0] = pids_match.group()
 | 
			
		||||
        except processutils.ProcessExecutionError as exc:
 | 
			
		||||
            LOG.warning(_LW('Failed to check the device %(device)s with fuser:'
 | 
			
		||||
                            ' %(err)s'), {'device': self._device, 'err': exc})
 | 
			
		||||
 | 
			
		||||
    def commit(self):
 | 
			
		||||
        """Write to the disk."""
 | 
			
		||||
        LOG.debug("Committing partitions to disk.")
 | 
			
		||||
        cmd_args = ['mklabel', self._disk_label]
 | 
			
		||||
        # NOTE(lucasagomes): Lead in with 1MiB to allow room for the
 | 
			
		||||
        #                    partition table itself.
 | 
			
		||||
        start = 1
 | 
			
		||||
        for num, part in self.get_partitions():
 | 
			
		||||
            end = start + part['size']
 | 
			
		||||
            cmd_args.extend(['mkpart', part['type'], part['fs_type'],
 | 
			
		||||
                             str(start), str(end)])
 | 
			
		||||
            if part['bootable']:
 | 
			
		||||
                cmd_args.extend(['set', str(num), 'boot', 'on'])
 | 
			
		||||
            start = end
 | 
			
		||||
 | 
			
		||||
        self._exec(*cmd_args)
 | 
			
		||||
 | 
			
		||||
        retries = [0]
 | 
			
		||||
        pids = ['']
 | 
			
		||||
        fuser_err = ['']
 | 
			
		||||
        interval = CONF.disk_partitioner.check_device_interval
 | 
			
		||||
        max_retries = CONF.disk_partitioner.check_device_max_retries
 | 
			
		||||
 | 
			
		||||
        timer = loopingcall.FixedIntervalLoopingCall(
 | 
			
		||||
            self._wait_for_disk_to_become_available,
 | 
			
		||||
            retries, max_retries, pids, fuser_err)
 | 
			
		||||
        timer.start(interval=interval).wait()
 | 
			
		||||
 | 
			
		||||
        if retries[0] > max_retries:
 | 
			
		||||
            if pids[0]:
 | 
			
		||||
                raise exception.InstanceDeployFailure(
 | 
			
		||||
                    _('Disk partitioning failed on device %(device)s. '
 | 
			
		||||
                      'Processes with the following PIDs are holding it: '
 | 
			
		||||
                      '%(pids)s. Time out waiting for completion.')
 | 
			
		||||
                    % {'device': self._device, 'pids': pids[0]})
 | 
			
		||||
            else:
 | 
			
		||||
                raise exception.InstanceDeployFailure(
 | 
			
		||||
                    _('Disk partitioning failed on device %(device)s. Fuser '
 | 
			
		||||
                      'exited with "%(fuser_err)s". Time out waiting for '
 | 
			
		||||
                      'completion.')
 | 
			
		||||
                    % {'device': self._device, 'fuser_err': fuser_err[0]})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_PARTED_PRINT_RE = re.compile(r"^(\d+):([\d\.]+)MiB:"
 | 
			
		||||
                              "([\d\.]+)MiB:([\d\.]+)MiB:(\w*)::(\w*)")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def list_partitions(device):
 | 
			
		||||
    """Get partitions information from given device.
 | 
			
		||||
 | 
			
		||||
    :param device: The device path.
 | 
			
		||||
    :returns: list of dictionaries (one per partition) with keys:
 | 
			
		||||
              number, start, end, size (in MiB), filesystem, flags
 | 
			
		||||
    """
 | 
			
		||||
    output = utils.execute(
 | 
			
		||||
        'parted', '-s', '-m', device, 'unit', 'MiB', 'print',
 | 
			
		||||
        use_standard_locale=True, run_as_root=True)[0]
 | 
			
		||||
    if isinstance(output, bytes):
 | 
			
		||||
        output = output.decode("utf-8")
 | 
			
		||||
    lines = [line for line in output.split('\n') if line.strip()][2:]
 | 
			
		||||
    # Example of line: 1:1.00MiB:501MiB:500MiB:ext4::boot
 | 
			
		||||
    fields = ('number', 'start', 'end', 'size', 'filesystem', 'flags')
 | 
			
		||||
    result = []
 | 
			
		||||
    for line in lines:
 | 
			
		||||
        match = _PARTED_PRINT_RE.match(line)
 | 
			
		||||
        if match is None:
 | 
			
		||||
            LOG.warn(_LW("Partition information from parted for device "
 | 
			
		||||
                         "%(device)s does not match "
 | 
			
		||||
                         "expected format: %(line)s"),
 | 
			
		||||
                     dict(device=device, line=line))
 | 
			
		||||
            continue
 | 
			
		||||
        # Cast int fields to ints (some are floats and we round them down)
 | 
			
		||||
        groups = [int(float(x)) if i < 4 else x
 | 
			
		||||
                  for i, x in enumerate(match.groups())]
 | 
			
		||||
        result.append(dict(zip(fields, groups)))
 | 
			
		||||
    return result
 | 
			
		||||
							
								
								
									
										144
									
								
								iotronic/common/driver_factory.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								iotronic/common/driver_factory.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
# 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 oslo_concurrency import lockutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
from stevedore import dispatch
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _LI
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
LOG = log.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
driver_opts = [
 | 
			
		||||
    cfg.ListOpt('enabled_drivers',
 | 
			
		||||
                default=['pxe_ipmitool'],
 | 
			
		||||
                help='Specify the list of drivers to load during service '
 | 
			
		||||
                     'initialization. Missing drivers, or drivers which '
 | 
			
		||||
                     'fail to initialize, will prevent the conductor '
 | 
			
		||||
                     'service from starting. The option default is a '
 | 
			
		||||
                     'recommended set of production-oriented drivers. A '
 | 
			
		||||
                     'complete list of drivers present on your system may '
 | 
			
		||||
                     'be found by enumerating the "iotronic.drivers" '
 | 
			
		||||
                     'entrypoint. An example may be found in the '
 | 
			
		||||
                     'developer documentation online.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(driver_opts)
 | 
			
		||||
 | 
			
		||||
EM_SEMAPHORE = 'extension_manager'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_driver(driver_name):
 | 
			
		||||
    """Simple method to get a ref to an instance of a driver.
 | 
			
		||||
 | 
			
		||||
    Driver loading is handled by the DriverFactory class. This method
 | 
			
		||||
    conveniently wraps that class and returns the actual driver object.
 | 
			
		||||
 | 
			
		||||
    :param driver_name: the name of the driver class to load
 | 
			
		||||
    :returns: An instance of a class which implements
 | 
			
		||||
              iotronic.drivers.base.BaseDriver
 | 
			
		||||
    :raises: DriverNotFound if the requested driver_name could not be
 | 
			
		||||
             found in the "iotronic.drivers" namespace.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        factory = DriverFactory()
 | 
			
		||||
        return factory[driver_name].obj
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        raise exception.DriverNotFound(driver_name=driver_name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def drivers():
 | 
			
		||||
    """Get all drivers as a dict name -> driver object."""
 | 
			
		||||
    factory = DriverFactory()
 | 
			
		||||
    return {name: factory[name].obj for name in factory.names}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DriverFactory(object):
 | 
			
		||||
    """Discover, load and manage the drivers available."""
 | 
			
		||||
 | 
			
		||||
    # NOTE(deva): loading the _extension_manager as a class member will break
 | 
			
		||||
    #             stevedore when it loads a driver, because the driver will
 | 
			
		||||
    #             import this file (and thus instantiate another factory).
 | 
			
		||||
    #             Instead, we instantiate a NameDispatchExtensionManager only
 | 
			
		||||
    #             once, the first time DriverFactory.__init__ is called.
 | 
			
		||||
    _extension_manager = None
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        if not DriverFactory._extension_manager:
 | 
			
		||||
            DriverFactory._init_extension_manager()
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, name):
 | 
			
		||||
        return self._extension_manager[name]
 | 
			
		||||
 | 
			
		||||
    # NOTE(deva): Use lockutils to avoid a potential race in eventlet
 | 
			
		||||
    #             that might try to create two driver factories.
 | 
			
		||||
    @classmethod
 | 
			
		||||
    @lockutils.synchronized(EM_SEMAPHORE, 'iotronic-')
 | 
			
		||||
    def _init_extension_manager(cls):
 | 
			
		||||
        # NOTE(deva): In case multiple greenthreads queue up on this lock
 | 
			
		||||
        #             before _extension_manager is initialized, prevent
 | 
			
		||||
        #             creation of multiple NameDispatchExtensionManagers.
 | 
			
		||||
        if cls._extension_manager:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # NOTE(deva): Drivers raise "DriverLoadError" if they are unable to be
 | 
			
		||||
        #             loaded, eg. due to missing external dependencies.
 | 
			
		||||
        #             We capture that exception, and, only if it is for an
 | 
			
		||||
        #             enabled driver, raise it from here. If enabled driver
 | 
			
		||||
        #             raises other exception type, it is wrapped in
 | 
			
		||||
        #             "DriverLoadError", providing the name of the driver that
 | 
			
		||||
        #             caused it, and raised. If the exception is for a
 | 
			
		||||
        #             non-enabled driver, we suppress it.
 | 
			
		||||
        def _catch_driver_not_found(mgr, ep, exc):
 | 
			
		||||
            # NOTE(deva): stevedore loads plugins *before* evaluating
 | 
			
		||||
            #             _check_func, so we need to check here, too.
 | 
			
		||||
            if ep.name in CONF.enabled_drivers:
 | 
			
		||||
                if not isinstance(exc, exception.DriverLoadError):
 | 
			
		||||
                    raise exception.DriverLoadError(driver=ep.name, reason=exc)
 | 
			
		||||
                raise exc
 | 
			
		||||
 | 
			
		||||
        def _check_func(ext):
 | 
			
		||||
            return ext.name in CONF.enabled_drivers
 | 
			
		||||
 | 
			
		||||
        cls._extension_manager = (
 | 
			
		||||
            dispatch.NameDispatchExtensionManager(
 | 
			
		||||
                'iotronic.drivers',
 | 
			
		||||
                _check_func,
 | 
			
		||||
                invoke_on_load=True,
 | 
			
		||||
                on_load_failure_callback=_catch_driver_not_found))
 | 
			
		||||
 | 
			
		||||
        # NOTE(deva): if we were unable to load any configured driver, perhaps
 | 
			
		||||
        #             because it is not present on the system, raise an error.
 | 
			
		||||
        if (sorted(CONF.enabled_drivers) !=
 | 
			
		||||
                sorted(cls._extension_manager.names())):
 | 
			
		||||
            found = cls._extension_manager.names()
 | 
			
		||||
            names = [n for n in CONF.enabled_drivers if n not in found]
 | 
			
		||||
            # just in case more than one could not be found ...
 | 
			
		||||
            names = ', '.join(names)
 | 
			
		||||
            raise exception.DriverNotFound(driver_name=names)
 | 
			
		||||
 | 
			
		||||
        LOG.info(_LI("Loaded the following drivers: %s"),
 | 
			
		||||
                 cls._extension_manager.names())
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def names(self):
 | 
			
		||||
        """The list of driver names available."""
 | 
			
		||||
        return self._extension_manager.names()
 | 
			
		||||
							
								
								
									
										589
									
								
								iotronic/common/exception.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										589
									
								
								iotronic/common/exception.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,589 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""Iotronic base exception handling.
 | 
			
		||||
 | 
			
		||||
Includes decorator for re-raising Iotronic-type exceptions.
 | 
			
		||||
 | 
			
		||||
SHOULD include dedicated exception logging.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
exc_log_opts = [
 | 
			
		||||
    cfg.BoolOpt('fatal_exception_format_errors',
 | 
			
		||||
                default=False,
 | 
			
		||||
                help='Used if there is a formatting error when generating an '
 | 
			
		||||
                     'exception message (a programming error). If True, '
 | 
			
		||||
                     'raise an exception; if False, use the unformatted '
 | 
			
		||||
                     'message.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(exc_log_opts)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _cleanse_dict(original):
 | 
			
		||||
    """Strip all admin_password, new_pass, rescue_pass keys from a dict."""
 | 
			
		||||
    return dict((k, v) for k, v in original.iteritems() if "_pass" not in k)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IotronicException(Exception):
 | 
			
		||||
    """Base Iotronic Exception
 | 
			
		||||
 | 
			
		||||
    To correctly use this class, inherit from it and define
 | 
			
		||||
    a 'message' property. That message will get printf'd
 | 
			
		||||
    with the keyword arguments provided to the constructor.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    message = _("An unknown exception occurred.")
 | 
			
		||||
    code = 500
 | 
			
		||||
    headers = {}
 | 
			
		||||
    safe = False
 | 
			
		||||
 | 
			
		||||
    def __init__(self, message=None, **kwargs):
 | 
			
		||||
        self.kwargs = kwargs
 | 
			
		||||
 | 
			
		||||
        if 'code' not in self.kwargs:
 | 
			
		||||
            try:
 | 
			
		||||
                self.kwargs['code'] = self.code
 | 
			
		||||
            except AttributeError:
 | 
			
		||||
                pass
 | 
			
		||||
 | 
			
		||||
        if not message:
 | 
			
		||||
            try:
 | 
			
		||||
                message = self.message % kwargs
 | 
			
		||||
 | 
			
		||||
            except Exception as e:
 | 
			
		||||
                # kwargs doesn't match a variable in the message
 | 
			
		||||
                # log the issue and the kwargs
 | 
			
		||||
                LOG.exception(_LE('Exception in string format operation'))
 | 
			
		||||
                for name, value in kwargs.items():
 | 
			
		||||
                    LOG.error("%s: %s" % (name, value))
 | 
			
		||||
 | 
			
		||||
                if CONF.fatal_exception_format_errors:
 | 
			
		||||
                    raise e
 | 
			
		||||
                else:
 | 
			
		||||
                    # at least get the core message out if something happened
 | 
			
		||||
                    message = self.message
 | 
			
		||||
 | 
			
		||||
        super(IotronicException, self).__init__(message)
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        """Encode to utf-8 then wsme api can consume it as well."""
 | 
			
		||||
        if not six.PY3:
 | 
			
		||||
            return unicode(self.args[0]).encode('utf-8')
 | 
			
		||||
 | 
			
		||||
        return self.args[0]
 | 
			
		||||
 | 
			
		||||
    def format_message(self):
 | 
			
		||||
        if self.__class__.__name__.endswith('_Remote'):
 | 
			
		||||
            return self.args[0]
 | 
			
		||||
        else:
 | 
			
		||||
            return six.text_type(self)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotAuthorized(IotronicException):
 | 
			
		||||
    message = _("Not authorized.")
 | 
			
		||||
    code = 403
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OperationNotPermitted(NotAuthorized):
 | 
			
		||||
    message = _("Operation not permitted.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Invalid(IotronicException):
 | 
			
		||||
    message = _("Unacceptable parameters.")
 | 
			
		||||
    code = 400
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Conflict(IotronicException):
 | 
			
		||||
    message = _('Conflict.')
 | 
			
		||||
    code = 409
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TemporaryFailure(IotronicException):
 | 
			
		||||
    message = _("Resource temporarily unavailable, please retry.")
 | 
			
		||||
    code = 503
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotAcceptable(IotronicException):
 | 
			
		||||
    # TODO(deva): We need to set response headers in the API for this exception
 | 
			
		||||
    message = _("Request not acceptable.")
 | 
			
		||||
    code = 406
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidState(Conflict):
 | 
			
		||||
    message = _("Invalid resource state.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeAlreadyExists(Conflict):
 | 
			
		||||
    message = _("A node with UUID %(uuid)s already exists.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MACAlreadyExists(Conflict):
 | 
			
		||||
    message = _("A port with MAC address %(mac)s already exists.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChassisAlreadyExists(Conflict):
 | 
			
		||||
    message = _("A chassis with UUID %(uuid)s already exists.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PortAlreadyExists(Conflict):
 | 
			
		||||
    message = _("A port with UUID %(uuid)s already exists.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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 InvalidUUID(Invalid):
 | 
			
		||||
    message = _("Expected a uuid but received %(uuid)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidUuidOrName(Invalid):
 | 
			
		||||
    message = _("Expected a logical name or uuid but received %(name)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidName(Invalid):
 | 
			
		||||
    message = _("Expected a logical name but received %(name)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidIdentity(Invalid):
 | 
			
		||||
    message = _("Expected an uuid or int but received %(identity)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidMAC(Invalid):
 | 
			
		||||
    message = _("Expected a MAC address but received %(mac)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidStateRequested(Invalid):
 | 
			
		||||
    message = _('The requested action "%(action)s" can not be performed '
 | 
			
		||||
                'on node "%(node)s" while it is in state "%(state)s".')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PatchError(Invalid):
 | 
			
		||||
    message = _("Couldn't apply patch '%(patch)s'. Reason: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstanceDeployFailure(IotronicException):
 | 
			
		||||
    message = _("Failed to deploy instance: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageUnacceptable(IotronicException):
 | 
			
		||||
    message = _("Image %(image_id)s is unacceptable: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageConvertFailed(IotronicException):
 | 
			
		||||
    message = _("Image %(image_id)s is unacceptable: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Cannot be templated as the error syntax varies.
 | 
			
		||||
# msg needs to be constructed when raised.
 | 
			
		||||
class InvalidParameterValue(Invalid):
 | 
			
		||||
    message = _("%(err)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MissingParameterValue(InvalidParameterValue):
 | 
			
		||||
    message = _("%(err)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Duplicate(IotronicException):
 | 
			
		||||
    message = _("Resource already exists.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NotFound(IotronicException):
 | 
			
		||||
    message = _("Resource could not be found.")
 | 
			
		||||
    code = 404
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DHCPLoadError(IotronicException):
 | 
			
		||||
    message = _("Failed to load DHCP provider %(dhcp_provider_name)s, "
 | 
			
		||||
                "reason: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DriverNotFound(NotFound):
 | 
			
		||||
    message = _("Could not find the following driver(s): %(driver_name)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageNotFound(NotFound):
 | 
			
		||||
    message = _("Image %(image_id)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoValidHost(NotFound):
 | 
			
		||||
    message = _("No valid host was found. Reason: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InstanceNotFound(NotFound):
 | 
			
		||||
    message = _("Instance %(instance)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeNotFound(NotFound):
 | 
			
		||||
    message = _("Node %(node)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeAssociated(InvalidState):
 | 
			
		||||
    message = _("Node %(node)s is associated with instance %(instance)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PortNotFound(NotFound):
 | 
			
		||||
    message = _("Port %(port)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FailedToUpdateDHCPOptOnPort(IotronicException):
 | 
			
		||||
    message = _("Update DHCP options on port: %(port_id)s failed.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FailedToGetIPAddressOnPort(IotronicException):
 | 
			
		||||
    message = _("Retrieve IP address on port: %(port_id)s failed.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidIPv4Address(IotronicException):
 | 
			
		||||
    message = _("Invalid IPv4 address %(ip_address)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FailedToUpdateMacOnPort(IotronicException):
 | 
			
		||||
    message = _("Update MAC address on port: %(port_id)s failed.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChassisNotFound(NotFound):
 | 
			
		||||
    message = _("Chassis %(chassis)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoDriversLoaded(IotronicException):
 | 
			
		||||
    message = _("Conductor %(conductor)s cannot be started "
 | 
			
		||||
                "because no drivers were loaded.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConductorNotFound(NotFound):
 | 
			
		||||
    message = _("Conductor %(conductor)s could not be found.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConductorAlreadyRegistered(IotronicException):
 | 
			
		||||
    message = _("Conductor %(conductor)s already registered.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PowerStateFailure(InvalidState):
 | 
			
		||||
    message = _("Failed to set node power state to %(pstate)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExclusiveLockRequired(NotAuthorized):
 | 
			
		||||
    message = _("An exclusive lock is required, "
 | 
			
		||||
                "but the current context has a shared lock.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeMaintenanceFailure(Invalid):
 | 
			
		||||
    message = _("Failed to toggle maintenance-mode flag "
 | 
			
		||||
                "for node %(node)s: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeConsoleNotEnabled(Invalid):
 | 
			
		||||
    message = _("Console access is not enabled on node %(node)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeInMaintenance(Invalid):
 | 
			
		||||
    message = _("The %(op)s operation can't be performed on node "
 | 
			
		||||
                "%(node)s because it's in maintenance mode.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ChassisNotEmpty(Invalid):
 | 
			
		||||
    message = _("Cannot complete the requested action because chassis "
 | 
			
		||||
                "%(chassis)s contains nodes.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IPMIFailure(IotronicException):
 | 
			
		||||
    message = _("IPMI call failed: %(cmd)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AMTConnectFailure(IotronicException):
 | 
			
		||||
    message = _("Failed to connect to AMT service.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class AMTFailure(IotronicException):
 | 
			
		||||
    message = _("AMT call failed: %(cmd)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MSFTOCSClientApiException(IotronicException):
 | 
			
		||||
    message = _("MSFT OCS call failed.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SSHConnectFailed(IotronicException):
 | 
			
		||||
    message = _("Failed to establish SSH connection to host %(host)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SSHCommandFailed(IotronicException):
 | 
			
		||||
    message = _("Failed to execute command via SSH: %(cmd)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnsupportedObjectError(IotronicException):
 | 
			
		||||
    message = _('Unsupported object type %(objtype)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class OrphanedObjectError(IotronicException):
 | 
			
		||||
    message = _('Cannot call %(method)s on orphaned %(objtype)s object')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UnsupportedDriverExtension(Invalid):
 | 
			
		||||
    message = _('Driver %(driver)s does not support %(extension)s '
 | 
			
		||||
                '(disabled or not implemented).')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IncompatibleObjectVersion(IotronicException):
 | 
			
		||||
    message = _('Version %(objver)s of %(objname)s is not supported')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GlanceConnectionFailed(IotronicException):
 | 
			
		||||
    message = _("Connection to glance host %(host)s:%(port)s failed: "
 | 
			
		||||
                "%(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageNotAuthorized(NotAuthorized):
 | 
			
		||||
    message = _("Not authorized for image %(image_id)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidImageRef(Invalid):
 | 
			
		||||
    message = _("Invalid image href %(image_href)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageRefValidationFailed(IotronicException):
 | 
			
		||||
    message = _("Validation of image href %(image_href)s failed, "
 | 
			
		||||
                "reason: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageDownloadFailed(IotronicException):
 | 
			
		||||
    message = _("Failed to download image %(image_href)s, reason: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KeystoneUnauthorized(IotronicException):
 | 
			
		||||
    message = _("Not authorized in Keystone.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class KeystoneFailure(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CatalogNotFound(IotronicException):
 | 
			
		||||
    message = _("Service type %(service_type)s with endpoint type "
 | 
			
		||||
                "%(endpoint_type)s not found in keystone service catalog.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ServiceUnavailable(IotronicException):
 | 
			
		||||
    message = _("Connection failed")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Forbidden(IotronicException):
 | 
			
		||||
    message = _("Requested OpenStack Images API is forbidden")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BadRequest(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidEndpoint(IotronicException):
 | 
			
		||||
    message = _("The provided endpoint is invalid")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CommunicationError(IotronicException):
 | 
			
		||||
    message = _("Unable to communicate with the server.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HTTPForbidden(Forbidden):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Unauthorized(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HTTPNotFound(NotFound):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConfigNotFound(IotronicException):
 | 
			
		||||
    message = _("Could not find config at %(path)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeLocked(Conflict):
 | 
			
		||||
    message = _("Node %(node)s is locked by host %(host)s, please retry "
 | 
			
		||||
                "after the current operation is completed.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeNotLocked(Invalid):
 | 
			
		||||
    message = _("Node %(node)s found not to be locked on release")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoFreeConductorWorker(TemporaryFailure):
 | 
			
		||||
    message = _('Requested action cannot be performed due to lack of free '
 | 
			
		||||
                'conductor workers.')
 | 
			
		||||
    code = 503  # Service Unavailable (temporary).
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VendorPassthruException(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConfigInvalid(IotronicException):
 | 
			
		||||
    message = _("Invalid configuration file. %(error_msg)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DriverLoadError(IotronicException):
 | 
			
		||||
    message = _("Driver %(driver)s could not be loaded. Reason: %(reason)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConsoleError(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NoConsolePid(ConsoleError):
 | 
			
		||||
    message = _("Could not find pid in pid file %(pid_path)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConsoleSubprocessFailed(ConsoleError):
 | 
			
		||||
    message = _("Console subprocess failed to start. %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordFileFailedToCreate(IotronicException):
 | 
			
		||||
    message = _("Failed to create the password file. %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IBootOperationError(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IloOperationError(IotronicException):
 | 
			
		||||
    message = _("%(operation)s failed, error: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IloOperationNotSupported(IotronicException):
 | 
			
		||||
    message = _("%(operation)s not supported. error: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracRequestFailed(IotronicException):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracClientError(DracRequestFailed):
 | 
			
		||||
    message = _('DRAC client failed. '
 | 
			
		||||
                'Last error (cURL error code): %(last_error)s, '
 | 
			
		||||
                'fault string: "%(fault_string)s" '
 | 
			
		||||
                'response_code: %(response_code)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracOperationFailed(DracRequestFailed):
 | 
			
		||||
    message = _('DRAC operation failed. Message: %(message)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracUnexpectedReturnValue(DracRequestFailed):
 | 
			
		||||
    message = _('DRAC operation yielded return value %(actual_return_value)s '
 | 
			
		||||
                'that is neither error nor expected %(expected_return_value)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracPendingConfigJobExists(IotronicException):
 | 
			
		||||
    message = _('Another job with ID %(job_id)s is already created  '
 | 
			
		||||
                'to configure %(target)s. Wait until existing job '
 | 
			
		||||
                'is completed or is canceled')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DracInvalidFilterDialect(IotronicException):
 | 
			
		||||
    message = _('Invalid filter dialect \'%(invalid_filter)s\'. '
 | 
			
		||||
                'Supported options are %(supported)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FailedToGetSensorData(IotronicException):
 | 
			
		||||
    message = _("Failed to get sensor data for node %(node)s. "
 | 
			
		||||
                "Error: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FailedToParseSensorData(IotronicException):
 | 
			
		||||
    message = _("Failed to parse sensor data for node %(node)s. "
 | 
			
		||||
                "Error: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InsufficientDiskSpace(IotronicException):
 | 
			
		||||
    message = _("Disk volume where '%(path)s' is located doesn't have "
 | 
			
		||||
                "enough disk space. Required %(required)d MiB, "
 | 
			
		||||
                "only %(actual)d MiB available space present.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ImageCreationFailed(IotronicException):
 | 
			
		||||
    message = _('Creating %(image_type)s image failed: %(error)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SwiftOperationError(IotronicException):
 | 
			
		||||
    message = _("Swift operation '%(operation)s' failed: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SNMPFailure(IotronicException):
 | 
			
		||||
    message = _("SNMP operation '%(operation)s' failed: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FileSystemNotSupported(IotronicException):
 | 
			
		||||
    message = _("Failed to create a file system. "
 | 
			
		||||
                "File system %(fs)s is not supported.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class IRMCOperationError(IotronicException):
 | 
			
		||||
    message = _('iRMC %(operation)s failed. Reason: %(error)s')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class VirtualBoxOperationFailed(IotronicException):
 | 
			
		||||
    message = _("VirtualBox operation '%(operation)s' failed. "
 | 
			
		||||
                "Error: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HardwareInspectionFailure(IotronicException):
 | 
			
		||||
    message = _("Failed to inspect hardware. Reason: %(error)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class NodeCleaningFailure(IotronicException):
 | 
			
		||||
    message = _("Failed to clean node %(node)s: %(reason)s")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PathNotFound(IotronicException):
 | 
			
		||||
    message = _("Path %(dir)s does not exist.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DirectoryNotWritable(IotronicException):
 | 
			
		||||
    message = _("Directory %(dir)s is not writable.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#################### new
 | 
			
		||||
class BoardNotFound(NotFound):
 | 
			
		||||
    message = _("Board %(board)s could not be found.")
 | 
			
		||||
    
 | 
			
		||||
class BoardLocked(Conflict):
 | 
			
		||||
    message = _("Board %(board)s is locked by host %(host)s, please retry "
 | 
			
		||||
                "after the current operation is completed.")
 | 
			
		||||
 | 
			
		||||
class BoardAssociated(InvalidState):
 | 
			
		||||
    message = _("Board %(board)s is associated with instance %(instance)s.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										239
									
								
								iotronic/common/fsm.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								iotronic/common/fsm.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,239 @@
 | 
			
		||||
# -*- coding: utf-8 -*-
 | 
			
		||||
 | 
			
		||||
#    Copyright (C) 2014 Yahoo! 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.
 | 
			
		||||
"""State machine modelling, copied from TaskFlow project.
 | 
			
		||||
 | 
			
		||||
This work will be turned into a library.
 | 
			
		||||
See https://github.com/harlowja/automaton
 | 
			
		||||
 | 
			
		||||
This is being used in the implementation of:
 | 
			
		||||
http://specs.openstack.org/openstack/iotronic-specs/specs/kilo/new-iotronic-state-machine.html
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from collections import OrderedDict  # noqa
 | 
			
		||||
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception as excp
 | 
			
		||||
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
 | 
			
		||||
        self.on_exit = on_exit
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FSM(object):
 | 
			
		||||
    """A finite state machine.
 | 
			
		||||
 | 
			
		||||
    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()
 | 
			
		||||
        self._start_state = start_state
 | 
			
		||||
        self._target_state = None
 | 
			
		||||
        # Note that _current is a _Jump instance
 | 
			
		||||
        self._current = None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def start_state(self):
 | 
			
		||||
        return self._start_state
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def current_state(self):
 | 
			
		||||
        if self._current is not None:
 | 
			
		||||
            return self._current.name
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def target_state(self):
 | 
			
		||||
        return self._target_state
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def terminated(self):
 | 
			
		||||
        """Returns whether the state machine is in a terminal state."""
 | 
			
		||||
        if self._current is None:
 | 
			
		||||
            return False
 | 
			
		||||
        return self._states[self._current.name]['terminal']
 | 
			
		||||
 | 
			
		||||
    def add_state(self, state, on_enter=None, on_exit=None,
 | 
			
		||||
                  target=None, terminal=None, stable=False):
 | 
			
		||||
        """Adds a given state to the state machine.
 | 
			
		||||
 | 
			
		||||
        The on_enter and on_exit callbacks, if provided will be expected to
 | 
			
		||||
        take two positional parameters, these being the state being exited (for
 | 
			
		||||
        on_exit) or the state being entered (for on_enter) and a second
 | 
			
		||||
        parameter which is the event that is being processed that caused the
 | 
			
		||||
        state transition.
 | 
			
		||||
 | 
			
		||||
        :param stable: Use this to specify that this state is a stable/passive
 | 
			
		||||
                       state. A state must have been previously defined as
 | 
			
		||||
                       'stable' before it can be used as a 'target'
 | 
			
		||||
        :param target: The target state for 'state' to go to.  Before a state
 | 
			
		||||
                       can be used as a target it must have been previously
 | 
			
		||||
                       added and specified as 'stable'
 | 
			
		||||
        """
 | 
			
		||||
        if state in self._states:
 | 
			
		||||
            raise excp.Duplicate(_("State '%s' already defined") % state)
 | 
			
		||||
        if on_enter is not None:
 | 
			
		||||
            if not six.callable(on_enter):
 | 
			
		||||
                raise ValueError(_("On enter callback must be callable"))
 | 
			
		||||
        if on_exit is not None:
 | 
			
		||||
            if not six.callable(on_exit):
 | 
			
		||||
                raise ValueError(_("On exit callback must be callable"))
 | 
			
		||||
        if target is not None and target not in self._states:
 | 
			
		||||
            raise excp.InvalidState(_("Target state '%s' does not exist")
 | 
			
		||||
                                    % target)
 | 
			
		||||
        if target is not None and not self._states[target]['stable']:
 | 
			
		||||
            raise excp.InvalidState(
 | 
			
		||||
                _("Target state '%s' is not a 'stable' state") % target)
 | 
			
		||||
 | 
			
		||||
        self._states[state] = {
 | 
			
		||||
            'terminal': bool(terminal),
 | 
			
		||||
            'reactions': {},
 | 
			
		||||
            'on_enter': on_enter,
 | 
			
		||||
            'on_exit': on_exit,
 | 
			
		||||
            'target': target,
 | 
			
		||||
            'stable': stable,
 | 
			
		||||
        }
 | 
			
		||||
        self._transitions[state] = OrderedDict()
 | 
			
		||||
 | 
			
		||||
    def add_transition(self, start, end, event):
 | 
			
		||||
        """Adds an allowed transition from start -> end for the given event."""
 | 
			
		||||
        if start not in self._states:
 | 
			
		||||
            raise excp.NotFound(
 | 
			
		||||
                _("Can not add a transition on event '%(event)s' that "
 | 
			
		||||
                  "starts in a undefined state '%(state)s'")
 | 
			
		||||
                % {'event': event, 'state': start})
 | 
			
		||||
        if end not in self._states:
 | 
			
		||||
            raise excp.NotFound(
 | 
			
		||||
                _("Can not add a transition on event '%(event)s' that "
 | 
			
		||||
                  "ends in a undefined state '%(state)s'")
 | 
			
		||||
                % {'event': event, 'state': end})
 | 
			
		||||
        self._transitions[start][event] = _Jump(end,
 | 
			
		||||
                                                self._states[end]['on_enter'],
 | 
			
		||||
                                                self._states[start]['on_exit'])
 | 
			
		||||
 | 
			
		||||
    def process_event(self, event):
 | 
			
		||||
        """Trigger a state change in response to the provided event."""
 | 
			
		||||
        current = self._current
 | 
			
		||||
        if current is None:
 | 
			
		||||
            raise excp.InvalidState(_("Can only process events after"
 | 
			
		||||
                                      " being initialized (not before)"))
 | 
			
		||||
        if self._states[current.name]['terminal']:
 | 
			
		||||
            raise excp.InvalidState(
 | 
			
		||||
                _("Can not transition from terminal "
 | 
			
		||||
                  "state '%(state)s' on event '%(event)s'")
 | 
			
		||||
                % {'state': current.name, 'event': event})
 | 
			
		||||
        if event not in self._transitions[current.name]:
 | 
			
		||||
            raise excp.InvalidState(
 | 
			
		||||
                _("Can not transition from state '%(state)s' on "
 | 
			
		||||
                  "event '%(event)s' (no defined transition)")
 | 
			
		||||
                % {'state': current.name, 'event': event})
 | 
			
		||||
        replacement = self._transitions[current.name][event]
 | 
			
		||||
        if current.on_exit is not None:
 | 
			
		||||
            current.on_exit(current.name, event)
 | 
			
		||||
        if replacement.on_enter is not None:
 | 
			
		||||
            replacement.on_enter(replacement.name, event)
 | 
			
		||||
        self._current = replacement
 | 
			
		||||
 | 
			
		||||
        # clear _target if we've reached it
 | 
			
		||||
        if (self._target_state is not None and
 | 
			
		||||
                self._target_state == replacement.name):
 | 
			
		||||
            self._target_state = None
 | 
			
		||||
        # if new state has a different target, update the target
 | 
			
		||||
        if self._states[replacement.name]['target'] is not None:
 | 
			
		||||
            self._target_state = self._states[replacement.name]['target']
 | 
			
		||||
 | 
			
		||||
    def is_valid_event(self, event):
 | 
			
		||||
        """Check whether the event is actionable in the current state."""
 | 
			
		||||
        current = self._current
 | 
			
		||||
        if current is None:
 | 
			
		||||
            return False
 | 
			
		||||
        if self._states[current.name]['terminal']:
 | 
			
		||||
            return False
 | 
			
		||||
        if event not in self._transitions[current.name]:
 | 
			
		||||
            return False
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def initialize(self, state=None):
 | 
			
		||||
        """Sets up the state machine.
 | 
			
		||||
 | 
			
		||||
        sets the current state to the specified state, or start_state
 | 
			
		||||
        if no state was specified..
 | 
			
		||||
        """
 | 
			
		||||
        if state is None:
 | 
			
		||||
            state = self._start_state
 | 
			
		||||
        if state not in self._states:
 | 
			
		||||
            raise excp.NotFound(_("Can not start from an undefined"
 | 
			
		||||
                                  " state '%s'") % (state))
 | 
			
		||||
        if self._states[state]['terminal']:
 | 
			
		||||
            raise excp.InvalidState(_("Can not start from a terminal"
 | 
			
		||||
                                      " state '%s'") % (state))
 | 
			
		||||
        self._current = _Jump(state, None, None)
 | 
			
		||||
        self._target_state = self._states[state]['target']
 | 
			
		||||
 | 
			
		||||
    def copy(self, shallow=False):
 | 
			
		||||
        """Copies the current state machine (shallow or deep).
 | 
			
		||||
 | 
			
		||||
        NOTE(harlowja): the copy will be left in an *uninitialized* state.
 | 
			
		||||
 | 
			
		||||
        NOTE(harlowja): when a shallow copy is requested the copy will share
 | 
			
		||||
                        the same transition table and state table as the
 | 
			
		||||
                        source; this can be advantageous if you have a machine
 | 
			
		||||
                        and transitions + states that is defined somewhere
 | 
			
		||||
                        and want to use copies to run with (the copies have
 | 
			
		||||
                        the current state that is different between machines).
 | 
			
		||||
        """
 | 
			
		||||
        c = FSM(self.start_state)
 | 
			
		||||
        if not shallow:
 | 
			
		||||
            for state, data in six.iteritems(self._states):
 | 
			
		||||
                copied_data = data.copy()
 | 
			
		||||
                copied_data['reactions'] = copied_data['reactions'].copy()
 | 
			
		||||
                c._states[state] = copied_data
 | 
			
		||||
            for state, data in six.iteritems(self._transitions):
 | 
			
		||||
                c._transitions[state] = data.copy()
 | 
			
		||||
        else:
 | 
			
		||||
            c._transitions = self._transitions
 | 
			
		||||
            c._states = self._states
 | 
			
		||||
        return c
 | 
			
		||||
 | 
			
		||||
    def __contains__(self, state):
 | 
			
		||||
        """Returns if this state exists in the machines known states."""
 | 
			
		||||
        return state in self._states
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def states(self):
 | 
			
		||||
        """Returns a list of the state names."""
 | 
			
		||||
        return list(six.iterkeys(self._states))
 | 
			
		||||
 | 
			
		||||
    def __iter__(self):
 | 
			
		||||
        """Iterates over (start, event, end) transition tuples."""
 | 
			
		||||
        for state in six.iterkeys(self._states):
 | 
			
		||||
            for event, target in six.iteritems(self._transitions[state]):
 | 
			
		||||
                yield (state, event, target.name)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def events(self):
 | 
			
		||||
        """Returns how many events exist."""
 | 
			
		||||
        c = 0
 | 
			
		||||
        for state in six.iterkeys(self._states):
 | 
			
		||||
            c += len(self._transitions[state])
 | 
			
		||||
        return c
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/common/glance_service/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/common/glance_service/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										288
									
								
								iotronic/common/glance_service/base_image_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								iotronic/common/glance_service/base_image_service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,288 @@
 | 
			
		||||
# Copyright 2010 OpenStack Foundation
 | 
			
		||||
# 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 functools
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
 | 
			
		||||
from glanceclient import client
 | 
			
		||||
from glanceclient import exc as glance_exc
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import sendfile
 | 
			
		||||
import six
 | 
			
		||||
import six.moves.urllib.parse as urlparse
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.glance_service import service_utils
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _translate_image_exception(image_id, exc_value):
 | 
			
		||||
    if isinstance(exc_value, (glance_exc.Forbidden,
 | 
			
		||||
                              glance_exc.Unauthorized)):
 | 
			
		||||
        return exception.ImageNotAuthorized(image_id=image_id)
 | 
			
		||||
    if isinstance(exc_value, glance_exc.NotFound):
 | 
			
		||||
        return exception.ImageNotFound(image_id=image_id)
 | 
			
		||||
    if isinstance(exc_value, glance_exc.BadRequest):
 | 
			
		||||
        return exception.Invalid(exc_value)
 | 
			
		||||
    return exc_value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _translate_plain_exception(exc_value):
 | 
			
		||||
    if isinstance(exc_value, (glance_exc.Forbidden,
 | 
			
		||||
                              glance_exc.Unauthorized)):
 | 
			
		||||
        return exception.NotAuthorized(exc_value)
 | 
			
		||||
    if isinstance(exc_value, glance_exc.NotFound):
 | 
			
		||||
        return exception.NotFound(exc_value)
 | 
			
		||||
    if isinstance(exc_value, glance_exc.BadRequest):
 | 
			
		||||
        return exception.Invalid(exc_value)
 | 
			
		||||
    return exc_value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_image_service(func):
 | 
			
		||||
    """Creates a glance client if doesn't exists and calls the function."""
 | 
			
		||||
    @functools.wraps(func)
 | 
			
		||||
    def wrapper(self, *args, **kwargs):
 | 
			
		||||
        """Wrapper around methods calls.
 | 
			
		||||
 | 
			
		||||
        :param image_href: href that describes the location of an image
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if self.client:
 | 
			
		||||
            return func(self, *args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        image_href = kwargs.get('image_href')
 | 
			
		||||
        (image_id, self.glance_host,
 | 
			
		||||
         self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href)
 | 
			
		||||
 | 
			
		||||
        if use_ssl:
 | 
			
		||||
            scheme = 'https'
 | 
			
		||||
        else:
 | 
			
		||||
            scheme = 'http'
 | 
			
		||||
        params = {}
 | 
			
		||||
        params['insecure'] = CONF.glance.glance_api_insecure
 | 
			
		||||
        if CONF.glance.auth_strategy == 'keystone':
 | 
			
		||||
            params['token'] = self.context.auth_token
 | 
			
		||||
        endpoint = '%s://%s:%s' % (scheme, self.glance_host, self.glance_port)
 | 
			
		||||
        self.client = client.Client(self.version,
 | 
			
		||||
                                    endpoint, **params)
 | 
			
		||||
        return func(self, *args, **kwargs)
 | 
			
		||||
    return wrapper
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BaseImageService(object):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, client=None, version=1, context=None):
 | 
			
		||||
        self.client = client
 | 
			
		||||
        self.version = version
 | 
			
		||||
        self.context = context
 | 
			
		||||
 | 
			
		||||
    def call(self, method, *args, **kwargs):
 | 
			
		||||
        """Call a glance client method.
 | 
			
		||||
 | 
			
		||||
        If we get a connection error,
 | 
			
		||||
        retry the request according to CONF.glance_num_retries.
 | 
			
		||||
 | 
			
		||||
        :param context: The request context, for access checks.
 | 
			
		||||
        :param version: The requested API version.v
 | 
			
		||||
        :param method: The method requested to be called.
 | 
			
		||||
        :param args: A list of positional arguments for the method called
 | 
			
		||||
        :param kwargs: A dict of keyword arguments for the method called
 | 
			
		||||
 | 
			
		||||
        :raises: GlanceConnectionFailed
 | 
			
		||||
        """
 | 
			
		||||
        retry_excs = (glance_exc.ServiceUnavailable,
 | 
			
		||||
                      glance_exc.InvalidEndpoint,
 | 
			
		||||
                      glance_exc.CommunicationError)
 | 
			
		||||
        image_excs = (glance_exc.Forbidden,
 | 
			
		||||
                      glance_exc.Unauthorized,
 | 
			
		||||
                      glance_exc.NotFound,
 | 
			
		||||
                      glance_exc.BadRequest)
 | 
			
		||||
        num_attempts = 1 + CONF.glance.glance_num_retries
 | 
			
		||||
 | 
			
		||||
        for attempt in range(1, num_attempts + 1):
 | 
			
		||||
            try:
 | 
			
		||||
                return getattr(self.client.images, method)(*args, **kwargs)
 | 
			
		||||
            except retry_excs as e:
 | 
			
		||||
                host = self.glance_host
 | 
			
		||||
                port = self.glance_port
 | 
			
		||||
                error_msg = _LE("Error contacting glance server "
 | 
			
		||||
                                "'%(host)s:%(port)s' for '%(method)s', attempt"
 | 
			
		||||
                                " %(attempt)s of %(num_attempts)s failed.")
 | 
			
		||||
                LOG.exception(error_msg, {'host': host,
 | 
			
		||||
                                          'port': port,
 | 
			
		||||
                                          'num_attempts': num_attempts,
 | 
			
		||||
                                          'attempt': attempt,
 | 
			
		||||
                                          'method': method})
 | 
			
		||||
                if attempt == num_attempts:
 | 
			
		||||
                    raise exception.GlanceConnectionFailed(host=host,
 | 
			
		||||
                                                           port=port,
 | 
			
		||||
                                                           reason=str(e))
 | 
			
		||||
                time.sleep(1)
 | 
			
		||||
            except image_excs as e:
 | 
			
		||||
                exc_type, exc_value, exc_trace = sys.exc_info()
 | 
			
		||||
                if method == 'list':
 | 
			
		||||
                    new_exc = _translate_plain_exception(
 | 
			
		||||
                        exc_value)
 | 
			
		||||
                else:
 | 
			
		||||
                    new_exc = _translate_image_exception(
 | 
			
		||||
                        args[0], exc_value)
 | 
			
		||||
                six.reraise(type(new_exc), new_exc, exc_trace)
 | 
			
		||||
 | 
			
		||||
    @check_image_service
 | 
			
		||||
    def _detail(self, method='list', **kwargs):
 | 
			
		||||
        """Calls out to Glance for a list of detailed image information.
 | 
			
		||||
 | 
			
		||||
        :returns: A list of dicts containing image metadata.
 | 
			
		||||
        """
 | 
			
		||||
        LOG.debug("Getting a full list of images metadata from glance.")
 | 
			
		||||
        params = service_utils.extract_query_params(kwargs, self.version)
 | 
			
		||||
 | 
			
		||||
        images = self.call(method, **params)
 | 
			
		||||
 | 
			
		||||
        _images = []
 | 
			
		||||
        for image in images:
 | 
			
		||||
            if service_utils.is_image_available(self.context, image):
 | 
			
		||||
                _images.append(service_utils.translate_from_glance(image))
 | 
			
		||||
 | 
			
		||||
        return _images
 | 
			
		||||
 | 
			
		||||
    @check_image_service
 | 
			
		||||
    def _show(self, image_href, method='get'):
 | 
			
		||||
        """Returns a dict with image data for the given opaque image id.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
        :returns: A dict containing image metadata.
 | 
			
		||||
 | 
			
		||||
        :raises: ImageNotFound
 | 
			
		||||
        """
 | 
			
		||||
        LOG.debug("Getting image metadata from glance. Image: %s"
 | 
			
		||||
                  % image_href)
 | 
			
		||||
        (image_id, self.glance_host,
 | 
			
		||||
         self.glance_port, use_ssl) = service_utils.parse_image_ref(image_href)
 | 
			
		||||
 | 
			
		||||
        image = self.call(method, image_id)
 | 
			
		||||
 | 
			
		||||
        if not service_utils.is_image_available(self.context, image):
 | 
			
		||||
            raise exception.ImageNotFound(image_id=image_id)
 | 
			
		||||
 | 
			
		||||
        base_image_meta = service_utils.translate_from_glance(image)
 | 
			
		||||
        return base_image_meta
 | 
			
		||||
 | 
			
		||||
    @check_image_service
 | 
			
		||||
    def _download(self, image_id, data=None, method='data'):
 | 
			
		||||
        """Calls out to Glance for data and writes data.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
        :param data: (Optional) File object to write data to.
 | 
			
		||||
        """
 | 
			
		||||
        (image_id, self.glance_host,
 | 
			
		||||
         self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
 | 
			
		||||
 | 
			
		||||
        if (self.version == 2 and
 | 
			
		||||
                'file' in CONF.glance.allowed_direct_url_schemes):
 | 
			
		||||
 | 
			
		||||
            location = self._get_location(image_id)
 | 
			
		||||
            url = urlparse.urlparse(location)
 | 
			
		||||
            if url.scheme == "file":
 | 
			
		||||
                with open(url.path, "r") as f:
 | 
			
		||||
                    filesize = os.path.getsize(f.name)
 | 
			
		||||
                    sendfile.sendfile(data.fileno(), f.fileno(), 0, filesize)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        image_chunks = self.call(method, image_id)
 | 
			
		||||
 | 
			
		||||
        if data is None:
 | 
			
		||||
            return image_chunks
 | 
			
		||||
        else:
 | 
			
		||||
            for chunk in image_chunks:
 | 
			
		||||
                data.write(chunk)
 | 
			
		||||
 | 
			
		||||
    @check_image_service
 | 
			
		||||
    def _create(self, image_meta, data=None, method='create'):
 | 
			
		||||
        """Store the image data and return the new image object.
 | 
			
		||||
 | 
			
		||||
        :param image_meta: A dict containing image metadata
 | 
			
		||||
        :param data: (Optional) File object to create image from.
 | 
			
		||||
        :returns: dict -- New created image metadata
 | 
			
		||||
        """
 | 
			
		||||
        sent_service_image_meta = service_utils.translate_to_glance(image_meta)
 | 
			
		||||
 | 
			
		||||
        # TODO(ghe): Allow copy-from or location headers Bug #1199532
 | 
			
		||||
 | 
			
		||||
        if data:
 | 
			
		||||
            sent_service_image_meta['data'] = data
 | 
			
		||||
 | 
			
		||||
        recv_service_image_meta = self.call(method, **sent_service_image_meta)
 | 
			
		||||
 | 
			
		||||
        return service_utils.translate_from_glance(recv_service_image_meta)
 | 
			
		||||
 | 
			
		||||
    @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.
 | 
			
		||||
        :param data: (Optional) File object to update data from.
 | 
			
		||||
        :param purge_props: (Optional=False) Purge existing properties.
 | 
			
		||||
        :returns: dict -- New created image metadata
 | 
			
		||||
        """
 | 
			
		||||
        (image_id, self.glance_host,
 | 
			
		||||
         self.glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
 | 
			
		||||
        if image_meta:
 | 
			
		||||
            image_meta = service_utils.translate_to_glance(image_meta)
 | 
			
		||||
        else:
 | 
			
		||||
            image_meta = {}
 | 
			
		||||
        if self.version == 1:
 | 
			
		||||
            image_meta['purge_props'] = purge_props
 | 
			
		||||
            if data:
 | 
			
		||||
                image_meta['data'] = data
 | 
			
		||||
 | 
			
		||||
        # NOTE(bcwaldon): id is not an editable field, but it is likely to be
 | 
			
		||||
        # passed in by calling code. Let's be nice and ignore it.
 | 
			
		||||
        image_meta.pop('id', None)
 | 
			
		||||
 | 
			
		||||
        image_meta = self.call(method, image_id, **image_meta)
 | 
			
		||||
 | 
			
		||||
        if self.version == 2 and data:
 | 
			
		||||
            self.call('upload', image_id, data)
 | 
			
		||||
            image_meta = self._show(image_id)
 | 
			
		||||
 | 
			
		||||
        return image_meta
 | 
			
		||||
 | 
			
		||||
    @check_image_service
 | 
			
		||||
    def _delete(self, image_id, method='delete'):
 | 
			
		||||
        """Delete the given image.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
 | 
			
		||||
        :raises: ImageNotFound if the image does not exist.
 | 
			
		||||
        :raises: NotAuthorized if the user is not an owner.
 | 
			
		||||
        :raises: ImageNotAuthorized if the user is not authorized.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        (image_id, glance_host,
 | 
			
		||||
         glance_port, use_ssl) = service_utils.parse_image_ref(image_id)
 | 
			
		||||
 | 
			
		||||
        self.call(method, image_id)
 | 
			
		||||
							
								
								
									
										81
									
								
								iotronic/common/glance_service/service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								iotronic/common/glance_service/service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
# 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 abc
 | 
			
		||||
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@six.add_metaclass(abc.ABCMeta)
 | 
			
		||||
class ImageService(object):
 | 
			
		||||
    """Provides storage and retrieval of disk image objects within Glance."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Constructor."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def detail(self):
 | 
			
		||||
        """Calls out to Glance for a list of detailed image information."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def show(self, image_id):
 | 
			
		||||
        """Returns a dict with image data for the given opaque image id.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
        :returns: A dict containing image metadata.
 | 
			
		||||
 | 
			
		||||
        :raises: ImageNotFound
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def download(self, image_id, data=None):
 | 
			
		||||
        """Calls out to Glance for data and writes data.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
        :param data: (Optional) File object to write data to.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def create(self, image_meta, data=None):
 | 
			
		||||
        """Store the image data and return the new image object.
 | 
			
		||||
 | 
			
		||||
        :param image_meta: A dict containing image metadata
 | 
			
		||||
        :param data: (Optional) File object to create image from.
 | 
			
		||||
        :returns: dict -- New created image metadata
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def update(self, image_id,
 | 
			
		||||
               image_meta, data=None, purge_props=False):
 | 
			
		||||
        """Modify the given image with the new data.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
        :param data: (Optional) File object to update data from.
 | 
			
		||||
        :param purge_props: (Optional=True) Purge existing properties.
 | 
			
		||||
        :returns: dict -- New created image metadata
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def delete(self, image_id):
 | 
			
		||||
        """Delete the given image.
 | 
			
		||||
 | 
			
		||||
        :param image_id: The opaque image identifier.
 | 
			
		||||
 | 
			
		||||
        :raises: ImageNotFound if the image does not exist.
 | 
			
		||||
        :raises: NotAuthorized if the user is not an owner.
 | 
			
		||||
        :raises: ImageNotAuthorized if the user is not authorized.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
							
								
								
									
										247
									
								
								iotronic/common/glance_service/service_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										247
									
								
								iotronic/common/glance_service/service_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,247 @@
 | 
			
		||||
# Copyright 2012 OpenStack Foundation
 | 
			
		||||
# 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 copy
 | 
			
		||||
import itertools
 | 
			
		||||
import logging
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_serialization import jsonutils
 | 
			
		||||
from oslo_utils import timeutils
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
import six
 | 
			
		||||
import six.moves.urllib.parse as urlparse
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
_GLANCE_API_SERVER = None
 | 
			
		||||
""" iterator that cycles (indefinitely) over glance API servers. """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_glance_url():
 | 
			
		||||
    """Generate the URL to glance."""
 | 
			
		||||
    return "%s://%s:%d" % (CONF.glance.glance_protocol,
 | 
			
		||||
                           CONF.glance.glance_host,
 | 
			
		||||
                           CONF.glance.glance_port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_image_url(image_ref):
 | 
			
		||||
    """Generate an image URL from an image_ref."""
 | 
			
		||||
    return "%s/images/%s" % (generate_glance_url(), image_ref)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _extract_attributes(image):
 | 
			
		||||
    IMAGE_ATTRIBUTES = ['size', 'disk_format', 'owner',
 | 
			
		||||
                        'container_format', 'checksum', 'id',
 | 
			
		||||
                        'name', 'created_at', 'updated_at',
 | 
			
		||||
                        'deleted_at', 'deleted', 'status',
 | 
			
		||||
                        'min_disk', 'min_ram', 'is_public']
 | 
			
		||||
 | 
			
		||||
    IMAGE_ATTRIBUTES_V2 = ['tags', 'visibility', 'protected',
 | 
			
		||||
                           'file', 'schema']
 | 
			
		||||
 | 
			
		||||
    output = {}
 | 
			
		||||
    for attr in IMAGE_ATTRIBUTES:
 | 
			
		||||
        output[attr] = getattr(image, attr, None)
 | 
			
		||||
 | 
			
		||||
    output['properties'] = getattr(image, 'properties', {})
 | 
			
		||||
 | 
			
		||||
    if hasattr(image, 'schema') and 'v2' in image['schema']:
 | 
			
		||||
        IMAGE_ATTRIBUTES = IMAGE_ATTRIBUTES + IMAGE_ATTRIBUTES_V2
 | 
			
		||||
        for attr in IMAGE_ATTRIBUTES_V2:
 | 
			
		||||
            output[attr] = getattr(image, attr, None)
 | 
			
		||||
        output['schema'] = image['schema']
 | 
			
		||||
 | 
			
		||||
        for image_property in set(image.keys()) - set(IMAGE_ATTRIBUTES):
 | 
			
		||||
            output['properties'][image_property] = image[image_property]
 | 
			
		||||
 | 
			
		||||
    return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _convert_timestamps_to_datetimes(image_meta):
 | 
			
		||||
    """Returns image with timestamp fields converted to datetime objects."""
 | 
			
		||||
    for attr in ['created_at', 'updated_at', 'deleted_at']:
 | 
			
		||||
        if image_meta.get(attr):
 | 
			
		||||
            image_meta[attr] = timeutils.parse_isotime(image_meta[attr])
 | 
			
		||||
    return image_meta
 | 
			
		||||
 | 
			
		||||
_CONVERT_PROPS = ('block_device_mapping', 'mappings')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _convert(metadata, method):
 | 
			
		||||
    metadata = copy.deepcopy(metadata)
 | 
			
		||||
    properties = metadata.get('properties')
 | 
			
		||||
    if properties:
 | 
			
		||||
        for attr in _CONVERT_PROPS:
 | 
			
		||||
            if attr in properties:
 | 
			
		||||
                prop = properties[attr]
 | 
			
		||||
                if method == 'from':
 | 
			
		||||
                    if isinstance(prop, six.string_types):
 | 
			
		||||
                        properties[attr] = jsonutils.loads(prop)
 | 
			
		||||
                if method == 'to':
 | 
			
		||||
                    if not isinstance(prop, six.string_types):
 | 
			
		||||
                        properties[attr] = jsonutils.dumps(prop)
 | 
			
		||||
    return metadata
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _remove_read_only(image_meta):
 | 
			
		||||
    IMAGE_ATTRIBUTES = ['status', 'updated_at', 'created_at', 'deleted_at']
 | 
			
		||||
    output = copy.deepcopy(image_meta)
 | 
			
		||||
    for attr in IMAGE_ATTRIBUTES:
 | 
			
		||||
        if attr in output:
 | 
			
		||||
            del output[attr]
 | 
			
		||||
    return output
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_api_server_iterator():
 | 
			
		||||
    """Return iterator over shuffled API servers.
 | 
			
		||||
 | 
			
		||||
    Shuffle a list of CONF.glance.glance_api_servers and return an iterator
 | 
			
		||||
    that will cycle through the list, looping around to the beginning if
 | 
			
		||||
    necessary.
 | 
			
		||||
 | 
			
		||||
    If CONF.glance.glance_api_servers isn't set, we fall back to using this
 | 
			
		||||
    as the server: CONF.glance.glance_host:CONF.glance.glance_port.
 | 
			
		||||
 | 
			
		||||
    :returns: iterator that cycles (indefinitely) over shuffled glance API
 | 
			
		||||
              servers. The iterator returns tuples of (host, port, use_ssl).
 | 
			
		||||
    """
 | 
			
		||||
    api_servers = []
 | 
			
		||||
 | 
			
		||||
    configured_servers = (CONF.glance.glance_api_servers or
 | 
			
		||||
                          ['%s:%s' % (CONF.glance.glance_host,
 | 
			
		||||
                                      CONF.glance.glance_port)])
 | 
			
		||||
    for api_server in configured_servers:
 | 
			
		||||
        if '//' not in api_server:
 | 
			
		||||
            api_server = '%s://%s' % (CONF.glance.glance_protocol, api_server)
 | 
			
		||||
        url = urlparse.urlparse(api_server)
 | 
			
		||||
        port = url.port or 80
 | 
			
		||||
        host = url.netloc.split(':', 1)[0]
 | 
			
		||||
        use_ssl = (url.scheme == 'https')
 | 
			
		||||
        api_servers.append((host, port, use_ssl))
 | 
			
		||||
    random.shuffle(api_servers)
 | 
			
		||||
    return itertools.cycle(api_servers)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_api_server():
 | 
			
		||||
    """Return a Glance API server.
 | 
			
		||||
 | 
			
		||||
    :returns: for an API server, the tuple (host-or-IP, port, use_ssl), where
 | 
			
		||||
        use_ssl is True to use the 'https' scheme, and False to use 'http'.
 | 
			
		||||
    """
 | 
			
		||||
    global _GLANCE_API_SERVER
 | 
			
		||||
 | 
			
		||||
    if not _GLANCE_API_SERVER:
 | 
			
		||||
        _GLANCE_API_SERVER = _get_api_server_iterator()
 | 
			
		||||
    return six.next(_GLANCE_API_SERVER)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_image_ref(image_href):
 | 
			
		||||
    """Parse an image href into composite parts.
 | 
			
		||||
 | 
			
		||||
    :param image_href: href of an image
 | 
			
		||||
    :returns: a tuple of the form (image_id, host, port, use_ssl)
 | 
			
		||||
 | 
			
		||||
    :raises ValueError
 | 
			
		||||
    """
 | 
			
		||||
    if '/' not in str(image_href):
 | 
			
		||||
        image_id = image_href
 | 
			
		||||
        (glance_host, glance_port, use_ssl) = _get_api_server()
 | 
			
		||||
        return (image_id, glance_host, glance_port, use_ssl)
 | 
			
		||||
    else:
 | 
			
		||||
        try:
 | 
			
		||||
            url = urlparse.urlparse(image_href)
 | 
			
		||||
            if url.scheme == 'glance':
 | 
			
		||||
                (glance_host, glance_port, use_ssl) = _get_api_server()
 | 
			
		||||
                image_id = image_href.split('/')[-1]
 | 
			
		||||
            else:
 | 
			
		||||
                glance_port = url.port or 80
 | 
			
		||||
                glance_host = url.netloc.split(':', 1)[0]
 | 
			
		||||
                image_id = url.path.split('/')[-1]
 | 
			
		||||
                use_ssl = (url.scheme == 'https')
 | 
			
		||||
            return (image_id, glance_host, glance_port, use_ssl)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            raise exception.InvalidImageRef(image_href=image_href)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def extract_query_params(params, version):
 | 
			
		||||
    _params = {}
 | 
			
		||||
    accepted_params = ('filters', 'marker', 'limit',
 | 
			
		||||
                       'sort_key', 'sort_dir')
 | 
			
		||||
    for param in accepted_params:
 | 
			
		||||
        if params.get(param):
 | 
			
		||||
            _params[param] = params.get(param)
 | 
			
		||||
    # ensure filters is a dict
 | 
			
		||||
    _params.setdefault('filters', {})
 | 
			
		||||
 | 
			
		||||
    # NOTE(vish): don't filter out private images
 | 
			
		||||
    # NOTE(ghe): in v2, not passing any visibility doesn't filter prvate images
 | 
			
		||||
    if version == 1:
 | 
			
		||||
        _params['filters'].setdefault('is_public', 'none')
 | 
			
		||||
 | 
			
		||||
    return _params
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def translate_to_glance(image_meta):
 | 
			
		||||
    image_meta = _convert(image_meta, 'to')
 | 
			
		||||
    image_meta = _remove_read_only(image_meta)
 | 
			
		||||
    return image_meta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def translate_from_glance(image):
 | 
			
		||||
    image_meta = _extract_attributes(image)
 | 
			
		||||
    image_meta = _convert_timestamps_to_datetimes(image_meta)
 | 
			
		||||
    image_meta = _convert(image_meta, 'from')
 | 
			
		||||
    return image_meta
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_image_available(context, image):
 | 
			
		||||
    """Check image availability.
 | 
			
		||||
 | 
			
		||||
    This check is needed in case Nova and Glance are deployed
 | 
			
		||||
    without authentication turned on.
 | 
			
		||||
    """
 | 
			
		||||
    # The presence of an auth token implies this is an authenticated
 | 
			
		||||
    # request and we need not handle the noauth use-case.
 | 
			
		||||
    if hasattr(context, 'auth_token') and context.auth_token:
 | 
			
		||||
        return True
 | 
			
		||||
    if image.is_public or context.is_admin:
 | 
			
		||||
        return True
 | 
			
		||||
    properties = image.properties
 | 
			
		||||
    if context.project_id and ('owner_id' in properties):
 | 
			
		||||
        return str(properties['owner_id']) == str(context.project_id)
 | 
			
		||||
 | 
			
		||||
    if context.project_id and ('project_id' in properties):
 | 
			
		||||
        return str(properties['project_id']) == str(context.project_id)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        user_id = properties['user_id']
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return str(user_id) == str(context.user_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_glance_image(image_href):
 | 
			
		||||
    if not isinstance(image_href, six.string_types):
 | 
			
		||||
        return False
 | 
			
		||||
    return (image_href.startswith('glance://') or
 | 
			
		||||
            uuidutils.is_uuid_like(image_href))
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/common/glance_service/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/common/glance_service/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										41
									
								
								iotronic/common/glance_service/v1/image_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								iotronic/common/glance_service/v1/image_service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,41 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from iotronic.common.glance_service import base_image_service
 | 
			
		||||
from iotronic.common.glance_service import service
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GlanceImageService(base_image_service.BaseImageService,
 | 
			
		||||
                         service.ImageService):
 | 
			
		||||
 | 
			
		||||
    def detail(self, **kwargs):
 | 
			
		||||
        return self._detail(method='list', **kwargs)
 | 
			
		||||
 | 
			
		||||
    def show(self, image_id):
 | 
			
		||||
        return self._show(image_id, method='get')
 | 
			
		||||
 | 
			
		||||
    def download(self, image_id, data=None):
 | 
			
		||||
        return self._download(image_id, method='data', data=data)
 | 
			
		||||
 | 
			
		||||
    def create(self, image_meta, data=None):
 | 
			
		||||
        return self._create(image_meta, method='create', data=data)
 | 
			
		||||
 | 
			
		||||
    def update(self, image_id, image_meta, data=None, purge_props=False):
 | 
			
		||||
        return self._update(image_id, image_meta, data=data, method='update',
 | 
			
		||||
                            purge_props=purge_props)
 | 
			
		||||
 | 
			
		||||
    def delete(self, image_id):
 | 
			
		||||
        return self._delete(image_id, method='delete')
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/common/glance_service/v2/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/common/glance_service/v2/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										231
									
								
								iotronic/common/glance_service/v2/image_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										231
									
								
								iotronic/common/glance_service/v2/image_service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,231 @@
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_utils import uuidutils
 | 
			
		||||
from swiftclient import utils as swift_utils
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception as exc
 | 
			
		||||
from iotronic.common.glance_service import base_image_service
 | 
			
		||||
from iotronic.common.glance_service import service
 | 
			
		||||
from iotronic.common.glance_service import service_utils
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
glance_opts = [
 | 
			
		||||
    cfg.ListOpt('allowed_direct_url_schemes',
 | 
			
		||||
                default=[],
 | 
			
		||||
                help='A list of URL schemes that can be downloaded directly '
 | 
			
		||||
                'via the direct_url.  Currently supported schemes: '
 | 
			
		||||
                '[file].'),
 | 
			
		||||
    # To upload this key to Swift:
 | 
			
		||||
    # swift post -m Temp-Url-Key:correcthorsebatterystaple
 | 
			
		||||
    cfg.StrOpt('swift_temp_url_key',
 | 
			
		||||
               help='The secret token given to Swift to allow temporary URL '
 | 
			
		||||
                    'downloads. Required for temporary URLs.',
 | 
			
		||||
               secret=True),
 | 
			
		||||
    cfg.IntOpt('swift_temp_url_duration',
 | 
			
		||||
               default=1200,
 | 
			
		||||
               help='The length of time in seconds that the temporary URL '
 | 
			
		||||
                    'will be valid for. Defaults to 20 minutes. If some '
 | 
			
		||||
                    'deploys get a 401 response code when trying to download '
 | 
			
		||||
                    'from the temporary URL, try raising this duration.'),
 | 
			
		||||
    cfg.StrOpt('swift_endpoint_url',
 | 
			
		||||
               help='The "endpoint" (scheme, hostname, optional port) for '
 | 
			
		||||
                    'the Swift URL of the form '
 | 
			
		||||
                    '"endpoint_url/api_version/account/container/object_id". '
 | 
			
		||||
                    'Do not include trailing "/". '
 | 
			
		||||
                    'For example, use "https://swift.example.com". '
 | 
			
		||||
                    'Required for temporary URLs.'),
 | 
			
		||||
    cfg.StrOpt('swift_api_version',
 | 
			
		||||
               default='v1',
 | 
			
		||||
               help='The Swift API version to create a temporary URL for. '
 | 
			
		||||
                    'Defaults to "v1". Swift temporary URL format: '
 | 
			
		||||
                    '"endpoint_url/api_version/account/container/object_id"'),
 | 
			
		||||
    cfg.StrOpt('swift_account',
 | 
			
		||||
               help='The account that Glance uses to communicate with '
 | 
			
		||||
                    'Swift. The format is "AUTH_uuid". "uuid" is the '
 | 
			
		||||
                    'UUID for the account configured in the glance-api.conf. '
 | 
			
		||||
                    'Required for temporary URLs. For example: '
 | 
			
		||||
                    '"AUTH_a422b2-91f3-2f46-74b7-d7c9e8958f5d30". '
 | 
			
		||||
                    'Swift temporary URL format: '
 | 
			
		||||
                    '"endpoint_url/api_version/account/container/object_id"'),
 | 
			
		||||
    cfg.StrOpt('swift_container',
 | 
			
		||||
               default='glance',
 | 
			
		||||
               help='The Swift container Glance is configured to store its '
 | 
			
		||||
                    'images in. Defaults to "glance", which is the default '
 | 
			
		||||
                    'in glance-api.conf. '
 | 
			
		||||
                    'Swift temporary URL format: '
 | 
			
		||||
                    '"endpoint_url/api_version/account/container/object_id"'),
 | 
			
		||||
    cfg.IntOpt('swift_store_multiple_containers_seed',
 | 
			
		||||
               default=0,
 | 
			
		||||
               help='This should match a config by the same name in the '
 | 
			
		||||
                    'Glance configuration file. When set to 0, a '
 | 
			
		||||
                    'single-tenant store will only use one '
 | 
			
		||||
                    'container to store all images. When set to an integer '
 | 
			
		||||
                    'value between 1 and 32, a single-tenant store will use '
 | 
			
		||||
                    'multiple containers to store images, and this value '
 | 
			
		||||
                    'will determine how many containers are created.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(glance_opts, group='glance')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class GlanceImageService(base_image_service.BaseImageService,
 | 
			
		||||
                         service.ImageService):
 | 
			
		||||
 | 
			
		||||
    def detail(self, **kwargs):
 | 
			
		||||
        return self._detail(method='list', **kwargs)
 | 
			
		||||
 | 
			
		||||
    def show(self, image_id):
 | 
			
		||||
        return self._show(image_id, method='get')
 | 
			
		||||
 | 
			
		||||
    def download(self, image_id, data=None):
 | 
			
		||||
        return self._download(image_id, method='data', data=data)
 | 
			
		||||
 | 
			
		||||
    def create(self, image_meta, data=None):
 | 
			
		||||
        image_id = self._create(image_meta, method='create', data=None)['id']
 | 
			
		||||
        return self.update(image_id, None, data)
 | 
			
		||||
 | 
			
		||||
    def update(self, image_id, image_meta, data=None, purge_props=False):
 | 
			
		||||
        # NOTE(ghe): purge_props not working until bug 1206472 solved
 | 
			
		||||
        return self._update(image_id, image_meta, data, method='update',
 | 
			
		||||
                            purge_props=False)
 | 
			
		||||
 | 
			
		||||
    def delete(self, image_id):
 | 
			
		||||
        return self._delete(image_id, method='delete')
 | 
			
		||||
 | 
			
		||||
    def swift_temp_url(self, image_info):
 | 
			
		||||
        """Generate a no-auth Swift temporary URL.
 | 
			
		||||
 | 
			
		||||
        This function will generate the temporary Swift URL using the image
 | 
			
		||||
        id from Glance and the config options: 'swift_endpoint_url',
 | 
			
		||||
        'swift_api_version', 'swift_account' and 'swift_container'.
 | 
			
		||||
        The temporary URL will be valid for 'swift_temp_url_duration' seconds.
 | 
			
		||||
        This allows Iotronic to download a Glance image without passing around
 | 
			
		||||
        an auth_token.
 | 
			
		||||
 | 
			
		||||
        :param image_info: The return from a GET request to Glance for a
 | 
			
		||||
            certain image_id. Should be a dictionary, with keys like 'name' and
 | 
			
		||||
            'checksum'. See
 | 
			
		||||
            http://docs.openstack.org/developer/glance/glanceapi.html for
 | 
			
		||||
            examples.
 | 
			
		||||
        :returns: A signed Swift URL from which an image can be downloaded,
 | 
			
		||||
            without authentication.
 | 
			
		||||
 | 
			
		||||
        :raises: InvalidParameterValue if Swift config options are not set
 | 
			
		||||
            correctly.
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is not set.
 | 
			
		||||
        :raises: ImageUnacceptable if the image info from Glance does not
 | 
			
		||||
            have a image ID.
 | 
			
		||||
        """
 | 
			
		||||
        self._validate_temp_url_config()
 | 
			
		||||
 | 
			
		||||
        if ('id' not in image_info or not
 | 
			
		||||
                uuidutils.is_uuid_like(image_info['id'])):
 | 
			
		||||
            raise exc.ImageUnacceptable(_(
 | 
			
		||||
                'The given image info does not have a valid image id: %s')
 | 
			
		||||
                % image_info)
 | 
			
		||||
 | 
			
		||||
        url_fragments = {
 | 
			
		||||
            'endpoint_url': CONF.glance.swift_endpoint_url,
 | 
			
		||||
            'api_version': CONF.glance.swift_api_version,
 | 
			
		||||
            'account': CONF.glance.swift_account,
 | 
			
		||||
            'container': self._get_swift_container(image_info['id']),
 | 
			
		||||
            'object_id': image_info['id']
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        template = '/{api_version}/{account}/{container}/{object_id}'
 | 
			
		||||
        url_path = template.format(**url_fragments)
 | 
			
		||||
        path = swift_utils.generate_temp_url(
 | 
			
		||||
            path=url_path,
 | 
			
		||||
            seconds=CONF.glance.swift_temp_url_duration,
 | 
			
		||||
            key=CONF.glance.swift_temp_url_key,
 | 
			
		||||
            method='GET')
 | 
			
		||||
 | 
			
		||||
        return '{endpoint_url}{url_path}'.format(
 | 
			
		||||
            endpoint_url=url_fragments['endpoint_url'], url_path=path)
 | 
			
		||||
 | 
			
		||||
    def _validate_temp_url_config(self):
 | 
			
		||||
        """Validate the required settings for a temporary URL."""
 | 
			
		||||
        if not CONF.glance.swift_temp_url_key:
 | 
			
		||||
            raise exc.MissingParameterValue(_(
 | 
			
		||||
                'Swift temporary URLs require a shared secret to be created. '
 | 
			
		||||
                'You must provide "swift_temp_url_key" as a config option.'))
 | 
			
		||||
        if not CONF.glance.swift_endpoint_url:
 | 
			
		||||
            raise exc.MissingParameterValue(_(
 | 
			
		||||
                'Swift temporary URLs require a Swift endpoint URL. '
 | 
			
		||||
                'You must provide "swift_endpoint_url" as a config option.'))
 | 
			
		||||
        if not CONF.glance.swift_account:
 | 
			
		||||
            raise exc.MissingParameterValue(_(
 | 
			
		||||
                'Swift temporary URLs require a Swift account string. '
 | 
			
		||||
                'You must provide "swift_account" as a config option.'))
 | 
			
		||||
        if CONF.glance.swift_temp_url_duration < 0:
 | 
			
		||||
            raise exc.InvalidParameterValue(_(
 | 
			
		||||
                '"swift_temp_url_duration" must be a positive integer.'))
 | 
			
		||||
        seed_num_chars = CONF.glance.swift_store_multiple_containers_seed
 | 
			
		||||
        if (seed_num_chars is None or seed_num_chars < 0
 | 
			
		||||
                or seed_num_chars > 32):
 | 
			
		||||
            raise exc.InvalidParameterValue(_(
 | 
			
		||||
                "An integer value between 0 and 32 is required for"
 | 
			
		||||
                " swift_store_multiple_containers_seed."))
 | 
			
		||||
 | 
			
		||||
    def _get_swift_container(self, image_id):
 | 
			
		||||
        """Get the Swift container the image is stored in.
 | 
			
		||||
 | 
			
		||||
        Code based on: https://github.com/openstack/glance_store/blob/3cd690b3
 | 
			
		||||
        7dc9d935445aca0998e8aec34a3e3530/glance_store/
 | 
			
		||||
        _drivers/swift/store.py#L725
 | 
			
		||||
 | 
			
		||||
        Returns appropriate container name depending upon value of
 | 
			
		||||
        ``swift_store_multiple_containers_seed``. In single-container mode,
 | 
			
		||||
        which is a seed value of 0, simply returns ``swift_container``.
 | 
			
		||||
        In multiple-container mode, returns ``swift_container`` as the
 | 
			
		||||
        prefix plus a suffix determined by the multiple container seed
 | 
			
		||||
 | 
			
		||||
        examples:
 | 
			
		||||
            single-container mode:  'glance'
 | 
			
		||||
            multiple-container mode: 'glance_3a1' for image uuid 3A1xxxxxxx...
 | 
			
		||||
 | 
			
		||||
        :param image_id: UUID of image
 | 
			
		||||
        :returns: The name of the swift container the image is stored in
 | 
			
		||||
        """
 | 
			
		||||
        seed_num_chars = CONF.glance.swift_store_multiple_containers_seed
 | 
			
		||||
 | 
			
		||||
        if seed_num_chars > 0:
 | 
			
		||||
            image_id = str(image_id).lower()
 | 
			
		||||
 | 
			
		||||
            num_dashes = image_id[:seed_num_chars].count('-')
 | 
			
		||||
            num_chars = seed_num_chars + num_dashes
 | 
			
		||||
            name_suffix = image_id[:num_chars]
 | 
			
		||||
            new_container_name = (CONF.glance.swift_container +
 | 
			
		||||
                                  '_' + name_suffix)
 | 
			
		||||
            return new_container_name
 | 
			
		||||
        else:
 | 
			
		||||
            return CONF.glance.swift_container
 | 
			
		||||
 | 
			
		||||
    def _get_location(self, image_id):
 | 
			
		||||
        """Get storage URL.
 | 
			
		||||
 | 
			
		||||
        Returns the direct url representing the backend storage location,
 | 
			
		||||
        or None if this attribute is not shown by Glance.
 | 
			
		||||
        """
 | 
			
		||||
        image_meta = self.call('get', image_id)
 | 
			
		||||
 | 
			
		||||
        if not service_utils.is_image_available(self.context, image_meta):
 | 
			
		||||
            raise exc.ImageNotFound(image_id=image_id)
 | 
			
		||||
 | 
			
		||||
        return getattr(image_meta, 'direct_url', None)
 | 
			
		||||
							
								
								
									
										8
									
								
								iotronic/common/grub_conf.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								iotronic/common/grub_conf.template
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
set default=0
 | 
			
		||||
set timeout=5
 | 
			
		||||
set hidden_timeout_quiet=false
 | 
			
		||||
 | 
			
		||||
menuentry "boot_partition" {
 | 
			
		||||
linuxefi {{ linux }} {{ kernel_params }} --
 | 
			
		||||
initrdefi {{ initrd }}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										200
									
								
								iotronic/common/hash_ring.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										200
									
								
								iotronic/common/hash_ring.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,200 @@
 | 
			
		||||
# 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 bisect
 | 
			
		||||
import hashlib
 | 
			
		||||
import threading
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.db import api as dbapi
 | 
			
		||||
 | 
			
		||||
hash_opts = [
 | 
			
		||||
    cfg.IntOpt('hash_partition_exponent',
 | 
			
		||||
               default=5,
 | 
			
		||||
               help='Exponent to determine number of hash partitions to use '
 | 
			
		||||
                    'when distributing load across conductors. Larger values '
 | 
			
		||||
                    'will result in more even distribution of load and less '
 | 
			
		||||
                    'load when rebalancing the ring, but more memory usage. '
 | 
			
		||||
                    'Number of partitions per conductor is '
 | 
			
		||||
                    '(2^hash_partition_exponent). This determines the '
 | 
			
		||||
                    'granularity of rebalancing: given 10 hosts, and an '
 | 
			
		||||
                    'exponent of the 2, there are 40 partitions in the ring.'
 | 
			
		||||
                    'A few thousand partitions should make rebalancing '
 | 
			
		||||
                    'smooth in most cases. The default is suitable for up to '
 | 
			
		||||
                    'a few hundred conductors. Too many partitions has a CPU '
 | 
			
		||||
                    'impact.'),
 | 
			
		||||
    cfg.IntOpt('hash_distribution_replicas',
 | 
			
		||||
               default=1,
 | 
			
		||||
               help='[Experimental Feature] '
 | 
			
		||||
                    'Number of hosts to map onto each hash partition. '
 | 
			
		||||
                    'Setting this to more than one will cause additional '
 | 
			
		||||
                    'conductor services to prepare deployment environments '
 | 
			
		||||
                    'and potentially allow the Iotronic cluster to recover '
 | 
			
		||||
                    'more quickly if a conductor instance is terminated.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(hash_opts)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HashRing(object):
 | 
			
		||||
    """A stable hash ring.
 | 
			
		||||
 | 
			
		||||
    We map item N to a host Y based on the closest lower hash:
 | 
			
		||||
 | 
			
		||||
    - hash(item) -> partition
 | 
			
		||||
    - hash(host) -> divider
 | 
			
		||||
    - closest lower divider is the host to use
 | 
			
		||||
    - we hash each host many times to spread load more finely
 | 
			
		||||
      as otherwise adding a host gets (on average) 50% of the load of
 | 
			
		||||
      just one other host assigned to it.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hosts, replicas=None):
 | 
			
		||||
        """Create a new hash ring across the specified hosts.
 | 
			
		||||
 | 
			
		||||
        :param hosts: an iterable of hosts which will be mapped.
 | 
			
		||||
        :param replicas: number of hosts to map to each hash partition,
 | 
			
		||||
                         or len(hosts), which ever is lesser.
 | 
			
		||||
                         Default: CONF.hash_distribution_replicas
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        if replicas is None:
 | 
			
		||||
            replicas = CONF.hash_distribution_replicas
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.hosts = set(hosts)
 | 
			
		||||
            self.replicas = replicas if replicas <= len(hosts) else len(hosts)
 | 
			
		||||
        except TypeError:
 | 
			
		||||
            raise exception.Invalid(
 | 
			
		||||
                _("Invalid hosts supplied when building HashRing."))
 | 
			
		||||
 | 
			
		||||
        self._host_hashes = {}
 | 
			
		||||
        for host in hosts:
 | 
			
		||||
            key = str(host).encode('utf8')
 | 
			
		||||
            key_hash = hashlib.md5(key)
 | 
			
		||||
            for p in range(2 ** CONF.hash_partition_exponent):
 | 
			
		||||
                key_hash.update(key)
 | 
			
		||||
                hashed_key = self._hash2int(key_hash)
 | 
			
		||||
                self._host_hashes[hashed_key] = host
 | 
			
		||||
        # Gather the (possibly colliding) resulting hashes into a bisectable
 | 
			
		||||
        # list.
 | 
			
		||||
        self._partitions = sorted(self._host_hashes.keys())
 | 
			
		||||
 | 
			
		||||
    def _hash2int(self, key_hash):
 | 
			
		||||
        """Convert the given hash's digest to a numerical value for the ring.
 | 
			
		||||
 | 
			
		||||
        :returns: An integer equivalent value of the digest.
 | 
			
		||||
        """
 | 
			
		||||
        return int(key_hash.hexdigest(), 16)
 | 
			
		||||
 | 
			
		||||
    def _get_partition(self, data):
 | 
			
		||||
        try:
 | 
			
		||||
            if six.PY3 and data is not None:
 | 
			
		||||
                data = data.encode('utf-8')
 | 
			
		||||
            key_hash = hashlib.md5(data)
 | 
			
		||||
            hashed_key = self._hash2int(key_hash)
 | 
			
		||||
            position = bisect.bisect(self._partitions, hashed_key)
 | 
			
		||||
            return position if position < len(self._partitions) else 0
 | 
			
		||||
        except TypeError:
 | 
			
		||||
            raise exception.Invalid(
 | 
			
		||||
                _("Invalid data supplied to HashRing.get_hosts."))
 | 
			
		||||
 | 
			
		||||
    def get_hosts(self, data, ignore_hosts=None):
 | 
			
		||||
        """Get the list of hosts which the supplied data maps onto.
 | 
			
		||||
 | 
			
		||||
        :param data: A string identifier to be mapped across the ring.
 | 
			
		||||
        :param ignore_hosts: A list of hosts to skip when performing the hash.
 | 
			
		||||
                             Useful to temporarily skip down hosts without
 | 
			
		||||
                             performing a full rebalance.
 | 
			
		||||
                             Default: None.
 | 
			
		||||
        :returns: a list of hosts.
 | 
			
		||||
                  The length of this list depends on the number of replicas
 | 
			
		||||
                  this `HashRing` was created with. It may be less than this
 | 
			
		||||
                  if ignore_hosts is not None.
 | 
			
		||||
        """
 | 
			
		||||
        hosts = []
 | 
			
		||||
        if ignore_hosts is None:
 | 
			
		||||
            ignore_hosts = set()
 | 
			
		||||
        else:
 | 
			
		||||
            ignore_hosts = set(ignore_hosts)
 | 
			
		||||
            ignore_hosts.intersection_update(self.hosts)
 | 
			
		||||
        partition = self._get_partition(data)
 | 
			
		||||
        for replica in range(0, self.replicas):
 | 
			
		||||
            if len(hosts) + len(ignore_hosts) == len(self.hosts):
 | 
			
		||||
                # prevent infinite loop - cannot allocate more fallbacks.
 | 
			
		||||
                break
 | 
			
		||||
            # Linear probing: partition N, then N+1 etc.
 | 
			
		||||
            host = self._get_host(partition)
 | 
			
		||||
            while host in hosts or host in ignore_hosts:
 | 
			
		||||
                partition += 1
 | 
			
		||||
                if partition >= len(self._partitions):
 | 
			
		||||
                    partition = 0
 | 
			
		||||
                host = self._get_host(partition)
 | 
			
		||||
            hosts.append(host)
 | 
			
		||||
        return hosts
 | 
			
		||||
 | 
			
		||||
    def _get_host(self, partition):
 | 
			
		||||
        """Find what host is serving a partition.
 | 
			
		||||
 | 
			
		||||
        :param partition: The index of the partition in the partition map.
 | 
			
		||||
            e.g. 0 is the first partition, 1 is the second.
 | 
			
		||||
        :return: The host object the ring was constructed with.
 | 
			
		||||
        """
 | 
			
		||||
        return self._host_hashes[self._partitions[partition]]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HashRingManager(object):
 | 
			
		||||
    _hash_rings = None
 | 
			
		||||
    _lock = threading.Lock()
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.dbapi = dbapi.get_instance()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ring(self):
 | 
			
		||||
        # Hot path, no lock
 | 
			
		||||
        if self._hash_rings is not None:
 | 
			
		||||
            return self._hash_rings
 | 
			
		||||
 | 
			
		||||
        with self._lock:
 | 
			
		||||
            if self._hash_rings is None:
 | 
			
		||||
                rings = self._load_hash_rings()
 | 
			
		||||
                self.__class__._hash_rings = rings
 | 
			
		||||
            return self._hash_rings
 | 
			
		||||
 | 
			
		||||
    def _load_hash_rings(self):
 | 
			
		||||
        rings = {}
 | 
			
		||||
        d2c = self.dbapi.get_active_driver_dict()
 | 
			
		||||
 | 
			
		||||
        for driver_name, hosts in d2c.items():
 | 
			
		||||
            rings[driver_name] = HashRing(hosts)
 | 
			
		||||
        return rings
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def reset(cls):
 | 
			
		||||
        with cls._lock:
 | 
			
		||||
            cls._hash_rings = None
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, driver_name):
 | 
			
		||||
        try:
 | 
			
		||||
            return self.ring[driver_name]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise exception.DriverNotFound(
 | 
			
		||||
                _("The driver '%s' is unknown.") % driver_name)
 | 
			
		||||
							
								
								
									
										31
									
								
								iotronic/common/i18n.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								iotronic/common/i18n.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
# Copyright (c) 2014 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 oslo_i18n as i18n
 | 
			
		||||
 | 
			
		||||
_translators = i18n.TranslatorFactory(domain='iotronic')
 | 
			
		||||
 | 
			
		||||
# The primary translation function using the well-known name "_"
 | 
			
		||||
_ = _translators.primary
 | 
			
		||||
 | 
			
		||||
# Translators for log levels.
 | 
			
		||||
#
 | 
			
		||||
# The abbreviated names are meant to reflect the usual use of a short
 | 
			
		||||
# name like '_'. The "L" is for "log" and the other letter comes from
 | 
			
		||||
# the level.
 | 
			
		||||
_LI = _translators.log_info
 | 
			
		||||
_LW = _translators.log_warning
 | 
			
		||||
_LE = _translators.log_error
 | 
			
		||||
_LC = _translators.log_critical
 | 
			
		||||
							
								
								
									
										294
									
								
								iotronic/common/image_service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								iotronic/common/image_service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
# Copyright 2010 OpenStack Foundation
 | 
			
		||||
# 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 abc
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
from oslo_utils import importutils
 | 
			
		||||
import requests
 | 
			
		||||
import sendfile
 | 
			
		||||
import six
 | 
			
		||||
import six.moves.urllib.parse as urlparse
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common import keystone
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
IMAGE_CHUNK_SIZE = 1024 * 1024  # 1mb
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
# Import this opt early so that it is available when registering
 | 
			
		||||
# glance_opts below.
 | 
			
		||||
CONF.import_opt('my_ip', 'iotronic.netconf')
 | 
			
		||||
 | 
			
		||||
glance_opts = [
 | 
			
		||||
    cfg.StrOpt('glance_host',
 | 
			
		||||
               default='$my_ip',
 | 
			
		||||
               help='Default glance hostname or IP address.'),
 | 
			
		||||
    cfg.IntOpt('glance_port',
 | 
			
		||||
               default=9292,
 | 
			
		||||
               help='Default glance port.'),
 | 
			
		||||
    cfg.StrOpt('glance_protocol',
 | 
			
		||||
               default='http',
 | 
			
		||||
               help='Default protocol to use when connecting to glance. '
 | 
			
		||||
               'Set to https for SSL.'),
 | 
			
		||||
    cfg.ListOpt('glance_api_servers',
 | 
			
		||||
                help='A list of the glance api servers available to iotronic. '
 | 
			
		||||
                'Prefix with https:// for SSL-based glance API servers. '
 | 
			
		||||
                'Format is [hostname|IP]:port.'),
 | 
			
		||||
    cfg.BoolOpt('glance_api_insecure',
 | 
			
		||||
                default=False,
 | 
			
		||||
                help='Allow to perform insecure SSL (https) requests to '
 | 
			
		||||
                     'glance.'),
 | 
			
		||||
    cfg.IntOpt('glance_num_retries',
 | 
			
		||||
               default=0,
 | 
			
		||||
               help='Number of retries when downloading an image from '
 | 
			
		||||
                    'glance.'),
 | 
			
		||||
    cfg.StrOpt('auth_strategy',
 | 
			
		||||
               default='keystone',
 | 
			
		||||
               help='Authentication strategy to use when connecting to '
 | 
			
		||||
                    'glance. Only "keystone" and "noauth" are currently '
 | 
			
		||||
                    'supported by iotronic.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF.register_opts(glance_opts, group='glance')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def import_versioned_module(version, submodule=None):
 | 
			
		||||
    module = 'iotronic.common.glance_service.v%s' % version
 | 
			
		||||
    if submodule:
 | 
			
		||||
        module = '.'.join((module, submodule))
 | 
			
		||||
    return importutils.try_import(module)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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):
 | 
			
		||||
        context.auth_token = keystone.get_admin_auth_token()
 | 
			
		||||
    return service_class(client, version, context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@six.add_metaclass(abc.ABCMeta)
 | 
			
		||||
class BaseImageService(object):
 | 
			
		||||
    """Provides retrieval of disk images."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def validate_href(self, image_href):
 | 
			
		||||
        """Validate image reference.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed.
 | 
			
		||||
        :returns: Information needed to further operate with an image.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def download(self, image_href, image_file):
 | 
			
		||||
        """Downloads image to specified location.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :param image_file: File object to write data to.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed.
 | 
			
		||||
        :raises: exception.ImageDownloadFailed.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def show(self, image_href):
 | 
			
		||||
        """Get dictionary of image properties.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed.
 | 
			
		||||
        :returns: dictionary of image properties.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HttpImageService(BaseImageService):
 | 
			
		||||
    """Provides retrieval of disk images using HTTP."""
 | 
			
		||||
 | 
			
		||||
    def validate_href(self, image_href):
 | 
			
		||||
        """Validate HTTP image reference.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if HEAD request failed or
 | 
			
		||||
            returned response code not equal to 200.
 | 
			
		||||
        :returns: Response to HEAD request.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            response = requests.head(image_href)
 | 
			
		||||
            if response.status_code != 200:
 | 
			
		||||
                raise exception.ImageRefValidationFailed(
 | 
			
		||||
                    image_href=image_href,
 | 
			
		||||
                    reason=_("Got HTTP code %s instead of 200 in response to "
 | 
			
		||||
                             "HEAD request.") % response.status_code)
 | 
			
		||||
        except requests.RequestException as e:
 | 
			
		||||
            raise exception.ImageRefValidationFailed(image_href=image_href,
 | 
			
		||||
                                                     reason=e)
 | 
			
		||||
        return response
 | 
			
		||||
 | 
			
		||||
    def download(self, image_href, image_file):
 | 
			
		||||
        """Downloads image to specified location.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :param image_file: File object to write data to.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if GET request returned
 | 
			
		||||
            response code not equal to 200.
 | 
			
		||||
        :raises: exception.ImageDownloadFailed if:
 | 
			
		||||
            * IOError happened during file write;
 | 
			
		||||
            * GET request failed.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            response = requests.get(image_href, stream=True)
 | 
			
		||||
            if response.status_code != 200:
 | 
			
		||||
                raise exception.ImageRefValidationFailed(
 | 
			
		||||
                    image_href=image_href,
 | 
			
		||||
                    reason=_("Got HTTP code %s instead of 200 in response to "
 | 
			
		||||
                             "GET request.") % response.status_code)
 | 
			
		||||
            with response.raw as input_img:
 | 
			
		||||
                shutil.copyfileobj(input_img, image_file, IMAGE_CHUNK_SIZE)
 | 
			
		||||
        except (requests.RequestException, IOError) as e:
 | 
			
		||||
            raise exception.ImageDownloadFailed(image_href=image_href,
 | 
			
		||||
                                                reason=e)
 | 
			
		||||
 | 
			
		||||
    def show(self, image_href):
 | 
			
		||||
        """Get dictionary of image properties.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if:
 | 
			
		||||
            * HEAD request failed;
 | 
			
		||||
            * HEAD request returned response code not equal to 200;
 | 
			
		||||
            * Content-Length header not found in response to HEAD request.
 | 
			
		||||
        :returns: dictionary of image properties.
 | 
			
		||||
        """
 | 
			
		||||
        response = self.validate_href(image_href)
 | 
			
		||||
        image_size = response.headers.get('Content-Length')
 | 
			
		||||
        if image_size is None:
 | 
			
		||||
            raise exception.ImageRefValidationFailed(
 | 
			
		||||
                image_href=image_href,
 | 
			
		||||
                reason=_("Cannot determine image size as there is no "
 | 
			
		||||
                         "Content-Length header specified in response "
 | 
			
		||||
                         "to HEAD request."))
 | 
			
		||||
        return {
 | 
			
		||||
            'size': int(image_size),
 | 
			
		||||
            'properties': {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class FileImageService(BaseImageService):
 | 
			
		||||
    """Provides retrieval of disk images available locally on the conductor."""
 | 
			
		||||
 | 
			
		||||
    def validate_href(self, image_href):
 | 
			
		||||
        """Validate local image reference.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if source image file
 | 
			
		||||
            doesn't exist.
 | 
			
		||||
        :returns: Path to image file if it exists.
 | 
			
		||||
        """
 | 
			
		||||
        image_path = urlparse.urlparse(image_href).path
 | 
			
		||||
        if not os.path.isfile(image_path):
 | 
			
		||||
            raise exception.ImageRefValidationFailed(
 | 
			
		||||
                image_href=image_href,
 | 
			
		||||
                reason=_("Specified image file not found."))
 | 
			
		||||
        return image_path
 | 
			
		||||
 | 
			
		||||
    def download(self, image_href, image_file):
 | 
			
		||||
        """Downloads image to specified location.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :param image_file: File object to write data to.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if source image file
 | 
			
		||||
            doesn't exist.
 | 
			
		||||
        :raises: exception.ImageDownloadFailed if exceptions were raised while
 | 
			
		||||
            writing to file or creating hard link.
 | 
			
		||||
        """
 | 
			
		||||
        source_image_path = self.validate_href(image_href)
 | 
			
		||||
        dest_image_path = image_file.name
 | 
			
		||||
        local_device = os.stat(dest_image_path).st_dev
 | 
			
		||||
        try:
 | 
			
		||||
            # We should have read and write access to source file to create
 | 
			
		||||
            # hard link to it.
 | 
			
		||||
            if (local_device == os.stat(source_image_path).st_dev and
 | 
			
		||||
                    os.access(source_image_path, os.R_OK | os.W_OK)):
 | 
			
		||||
                image_file.close()
 | 
			
		||||
                os.remove(dest_image_path)
 | 
			
		||||
                os.link(source_image_path, dest_image_path)
 | 
			
		||||
            else:
 | 
			
		||||
                filesize = os.path.getsize(source_image_path)
 | 
			
		||||
                with open(source_image_path, 'rb') as input_img:
 | 
			
		||||
                    sendfile.sendfile(image_file.fileno(), input_img.fileno(),
 | 
			
		||||
                                      0, filesize)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            raise exception.ImageDownloadFailed(image_href=image_href,
 | 
			
		||||
                                                reason=e)
 | 
			
		||||
 | 
			
		||||
    def show(self, image_href):
 | 
			
		||||
        """Get dictionary of image properties.
 | 
			
		||||
 | 
			
		||||
        :param image_href: Image reference.
 | 
			
		||||
        :raises: exception.ImageRefValidationFailed if image file specified
 | 
			
		||||
            doesn't exist.
 | 
			
		||||
        :returns: dictionary of image properties.
 | 
			
		||||
        """
 | 
			
		||||
        source_image_path = self.validate_href(image_href)
 | 
			
		||||
        return {
 | 
			
		||||
            'size': os.path.getsize(source_image_path),
 | 
			
		||||
            'properties': {}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
protocol_mapping = {
 | 
			
		||||
    'http': HttpImageService,
 | 
			
		||||
    'https': HttpImageService,
 | 
			
		||||
    'file': FileImageService,
 | 
			
		||||
    'glance': GlanceImageService,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_image_service(image_href, client=None, version=1, context=None):
 | 
			
		||||
    """Get image service instance to download the image.
 | 
			
		||||
 | 
			
		||||
    :param image_href: String containing href to get image service for.
 | 
			
		||||
    :param client: Glance client to be used for download, used only if
 | 
			
		||||
        image_href is Glance href.
 | 
			
		||||
    :param version: Version of Glance API to use, used only if image_href is
 | 
			
		||||
        Glance href.
 | 
			
		||||
    :param context: request context, used only if image_href is Glance href.
 | 
			
		||||
    :raises: exception.ImageRefValidationFailed if no image service can
 | 
			
		||||
        handle specified href.
 | 
			
		||||
    :returns: Instance of an image service class that is able to download
 | 
			
		||||
        specified image.
 | 
			
		||||
    """
 | 
			
		||||
    scheme = urlparse.urlparse(image_href).scheme.lower()
 | 
			
		||||
    try:
 | 
			
		||||
        cls = protocol_mapping[scheme or 'glance']
 | 
			
		||||
    except KeyError:
 | 
			
		||||
        raise exception.ImageRefValidationFailed(
 | 
			
		||||
            image_href=image_href,
 | 
			
		||||
            reason=_('Image download protocol '
 | 
			
		||||
                     '%s is not supported.') % scheme
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if cls == GlanceImageService:
 | 
			
		||||
        return cls(client, version, context)
 | 
			
		||||
    return cls()
 | 
			
		||||
							
								
								
									
										577
									
								
								iotronic/common/images.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										577
									
								
								iotronic/common/images.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,577 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
# Copyright (c) 2010 Citrix Systems, Inc.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Handling of VM disk images.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
 | 
			
		||||
import jinja2
 | 
			
		||||
from oslo_concurrency import processutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.glance_service import service_utils as glance_utils
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
from iotronic.common import image_service as service
 | 
			
		||||
from iotronic.common import paths
 | 
			
		||||
from iotronic.common import utils
 | 
			
		||||
from iotronic.openstack.common import fileutils
 | 
			
		||||
from iotronic.openstack.common import imageutils
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
image_opts = [
 | 
			
		||||
    cfg.BoolOpt('force_raw_images',
 | 
			
		||||
                default=True,
 | 
			
		||||
                help='If True, convert backing images to "raw" disk image '
 | 
			
		||||
                     'format.'),
 | 
			
		||||
    cfg.StrOpt('isolinux_bin',
 | 
			
		||||
               default='/usr/lib/syslinux/isolinux.bin',
 | 
			
		||||
               help='Path to isolinux binary file.'),
 | 
			
		||||
    cfg.StrOpt('isolinux_config_template',
 | 
			
		||||
               default=paths.basedir_def('common/isolinux_config.template'),
 | 
			
		||||
               help='Template file for isolinux configuration file.'),
 | 
			
		||||
    cfg.StrOpt('grub_config_template',
 | 
			
		||||
               default=paths.basedir_def('common/grub_conf.template'),
 | 
			
		||||
               help='Template file for grub configuration file.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(image_opts)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _create_root_fs(root_directory, files_info):
 | 
			
		||||
    """Creates a filesystem root in given directory.
 | 
			
		||||
 | 
			
		||||
    Given a mapping of absolute path of files to their relative paths
 | 
			
		||||
    within the filesystem, this method copies the files to their
 | 
			
		||||
    destination.
 | 
			
		||||
 | 
			
		||||
    :param root_directory: the filesystem root directory.
 | 
			
		||||
    :param files_info: A dict containing absolute path of file to be copied
 | 
			
		||||
        -> relative path within the vfat image. For example,
 | 
			
		||||
        {
 | 
			
		||||
         '/absolute/path/to/file' -> 'relative/path/within/root'
 | 
			
		||||
         ...
 | 
			
		||||
        }
 | 
			
		||||
    :raises: OSError, if creation of any directory failed.
 | 
			
		||||
    :raises: IOError, if copying any of the files failed.
 | 
			
		||||
    """
 | 
			
		||||
    for src_file, path in files_info.items():
 | 
			
		||||
        target_file = os.path.join(root_directory, path)
 | 
			
		||||
        dirname = os.path.dirname(target_file)
 | 
			
		||||
        if not os.path.exists(dirname):
 | 
			
		||||
            os.makedirs(dirname)
 | 
			
		||||
 | 
			
		||||
        shutil.copyfile(src_file, target_file)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _umount_without_raise(mount_dir):
 | 
			
		||||
    """Helper method to umount without raise."""
 | 
			
		||||
    try:
 | 
			
		||||
        utils.umount(mount_dir)
 | 
			
		||||
    except processutils.ProcessExecutionError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_vfat_image(output_file, files_info=None, parameters=None,
 | 
			
		||||
                      parameters_file='parameters.txt', fs_size_kib=100):
 | 
			
		||||
    """Creates the fat fs image on the desired file.
 | 
			
		||||
 | 
			
		||||
    This method copies the given files to a root directory (optional),
 | 
			
		||||
    writes the parameters specified to the parameters file within the
 | 
			
		||||
    root directory (optional), and then creates a vfat image of the root
 | 
			
		||||
    directory.
 | 
			
		||||
 | 
			
		||||
    :param output_file: The path to the file where the fat fs image needs
 | 
			
		||||
        to be created.
 | 
			
		||||
    :param files_info: A dict containing absolute path of file to be copied
 | 
			
		||||
        -> relative path within the vfat image. For example,
 | 
			
		||||
        {
 | 
			
		||||
         '/absolute/path/to/file' -> 'relative/path/within/root'
 | 
			
		||||
         ...
 | 
			
		||||
        }
 | 
			
		||||
    :param parameters: A dict containing key-value pairs of parameters.
 | 
			
		||||
    :param parameters_file: The filename for the parameters file.
 | 
			
		||||
    :param fs_size_kib: size of the vfat filesystem in KiB.
 | 
			
		||||
    :raises: ImageCreationFailed, if image creation failed while doing any
 | 
			
		||||
        of filesystem manipulation activities like creating dirs, mounting,
 | 
			
		||||
        creating filesystem, copying files, etc.
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        utils.dd('/dev/zero', output_file, 'count=1', "bs=%dKiB" % fs_size_kib)
 | 
			
		||||
    except processutils.ProcessExecutionError as e:
 | 
			
		||||
        raise exception.ImageCreationFailed(image_type='vfat', error=e)
 | 
			
		||||
 | 
			
		||||
    with utils.tempdir() as tmpdir:
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # The label helps ramdisks to find the partition containing
 | 
			
		||||
            # the parameters (by using /dev/disk/by-label/ir-vfd-dev).
 | 
			
		||||
            # NOTE: FAT filesystem label can be up to 11 characters long.
 | 
			
		||||
            utils.mkfs('vfat', output_file, label="ir-vfd-dev")
 | 
			
		||||
            utils.mount(output_file, tmpdir, '-o', 'umask=0')
 | 
			
		||||
        except processutils.ProcessExecutionError as e:
 | 
			
		||||
            raise exception.ImageCreationFailed(image_type='vfat', error=e)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if files_info:
 | 
			
		||||
                _create_root_fs(tmpdir, files_info)
 | 
			
		||||
 | 
			
		||||
            if parameters:
 | 
			
		||||
                parameters_file = os.path.join(tmpdir, parameters_file)
 | 
			
		||||
                params_list = ['%(key)s=%(val)s' % {'key': k, 'val': v}
 | 
			
		||||
                               for k, v in parameters.items()]
 | 
			
		||||
                file_contents = '\n'.join(params_list)
 | 
			
		||||
                utils.write_to_file(parameters_file, file_contents)
 | 
			
		||||
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            LOG.exception(_LE("vfat image creation failed. Error: %s"), e)
 | 
			
		||||
            raise exception.ImageCreationFailed(image_type='vfat', error=e)
 | 
			
		||||
 | 
			
		||||
        finally:
 | 
			
		||||
            try:
 | 
			
		||||
                utils.umount(tmpdir)
 | 
			
		||||
            except processutils.ProcessExecutionError as e:
 | 
			
		||||
                raise exception.ImageCreationFailed(image_type='vfat', error=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _generate_cfg(kernel_params, template, options):
 | 
			
		||||
    """Generates a isolinux or grub configuration file.
 | 
			
		||||
 | 
			
		||||
    Given a given a list of strings containing kernel parameters, this method
 | 
			
		||||
    returns the kernel cmdline string.
 | 
			
		||||
    :param kernel_params: a list of strings(each element being a string like
 | 
			
		||||
        'K=V' or 'K' or combination of them like 'K1=V1 K2 K3=V3') to be added
 | 
			
		||||
        as the kernel cmdline.
 | 
			
		||||
    :param template: the path of the config template file.
 | 
			
		||||
    :param options: a dictionary of keywords which need to be replaced in
 | 
			
		||||
                    template file to generate a proper config file.
 | 
			
		||||
    :returns: a string containing the contents of the isolinux configuration
 | 
			
		||||
        file.
 | 
			
		||||
    """
 | 
			
		||||
    if not kernel_params:
 | 
			
		||||
        kernel_params = []
 | 
			
		||||
    kernel_params_str = ' '.join(kernel_params)
 | 
			
		||||
 | 
			
		||||
    tmpl_path, tmpl_file = os.path.split(template)
 | 
			
		||||
    env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
 | 
			
		||||
    template = env.get_template(tmpl_file)
 | 
			
		||||
 | 
			
		||||
    options.update({'kernel_params': kernel_params_str})
 | 
			
		||||
 | 
			
		||||
    cfg = template.render(options)
 | 
			
		||||
    return cfg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_isolinux_image_for_bios(output_file, kernel, ramdisk,
 | 
			
		||||
                                   kernel_params=None):
 | 
			
		||||
    """Creates an isolinux image on the specified file.
 | 
			
		||||
 | 
			
		||||
    Copies the provided kernel, ramdisk to a directory, generates the isolinux
 | 
			
		||||
    configuration file using the kernel parameters provided, and then generates
 | 
			
		||||
    a bootable ISO image.
 | 
			
		||||
 | 
			
		||||
    :param output_file: the path to the file where the iso image needs to be
 | 
			
		||||
        created.
 | 
			
		||||
    :param kernel: the kernel to use.
 | 
			
		||||
    :param ramdisk: the ramdisk to use.
 | 
			
		||||
    :param kernel_params: a list of strings(each element being a string like
 | 
			
		||||
        'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added
 | 
			
		||||
        as the kernel cmdline.
 | 
			
		||||
    :raises: ImageCreationFailed, if image creation failed while copying files
 | 
			
		||||
        or while running command to generate iso.
 | 
			
		||||
    """
 | 
			
		||||
    ISOLINUX_BIN = 'isolinux/isolinux.bin'
 | 
			
		||||
    ISOLINUX_CFG = 'isolinux/isolinux.cfg'
 | 
			
		||||
 | 
			
		||||
    options = {'kernel': '/vmlinuz', 'ramdisk': '/initrd'}
 | 
			
		||||
 | 
			
		||||
    with utils.tempdir() as tmpdir:
 | 
			
		||||
        files_info = {
 | 
			
		||||
            kernel: 'vmlinuz',
 | 
			
		||||
            ramdisk: 'initrd',
 | 
			
		||||
            CONF.isolinux_bin: ISOLINUX_BIN,
 | 
			
		||||
        }
 | 
			
		||||
        try:
 | 
			
		||||
            _create_root_fs(tmpdir, files_info)
 | 
			
		||||
        except (OSError, IOError) as e:
 | 
			
		||||
            LOG.exception(_LE("Creating the filesystem root failed."))
 | 
			
		||||
            raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
 | 
			
		||||
        cfg = _generate_cfg(kernel_params,
 | 
			
		||||
                            CONF.isolinux_config_template, options)
 | 
			
		||||
 | 
			
		||||
        isolinux_cfg = os.path.join(tmpdir, ISOLINUX_CFG)
 | 
			
		||||
        utils.write_to_file(isolinux_cfg, cfg)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            utils.execute('mkisofs', '-r', '-V', "VMEDIA_BOOT_ISO",
 | 
			
		||||
                          '-cache-inodes', '-J', '-l', '-no-emul-boot',
 | 
			
		||||
                          '-boot-load-size', '4', '-boot-info-table',
 | 
			
		||||
                          '-b', ISOLINUX_BIN, '-o', output_file, tmpdir)
 | 
			
		||||
        except processutils.ProcessExecutionError as e:
 | 
			
		||||
            LOG.exception(_LE("Creating ISO image failed."))
 | 
			
		||||
            raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_isolinux_image_for_uefi(output_file, deploy_iso, kernel, ramdisk,
 | 
			
		||||
                                   kernel_params=None):
 | 
			
		||||
    """Creates an isolinux image on the specified file.
 | 
			
		||||
 | 
			
		||||
    Copies the provided kernel, ramdisk, efiboot.img to a directory, creates
 | 
			
		||||
    the path for grub config file, generates the isolinux configuration file
 | 
			
		||||
    using the kernel parameters provided, generates the grub configuration
 | 
			
		||||
    file using kernel parameters and then generates a bootable ISO image
 | 
			
		||||
    for uefi.
 | 
			
		||||
 | 
			
		||||
    :param output_file: the path to the file where the iso image needs to be
 | 
			
		||||
        created.
 | 
			
		||||
    :param deploy_iso: deploy iso used to initiate the deploy.
 | 
			
		||||
    :param kernel: the kernel to use.
 | 
			
		||||
    :param ramdisk: the ramdisk to use.
 | 
			
		||||
    :param kernel_params: a list of strings(each element being a string like
 | 
			
		||||
        'K=V' or 'K' or combination of them like 'K1=V1,K2,...') to be added
 | 
			
		||||
        as the kernel cmdline.
 | 
			
		||||
    :raises: ImageCreationFailed, if image creation failed while copying files
 | 
			
		||||
        or while running command to generate iso.
 | 
			
		||||
    """
 | 
			
		||||
    ISOLINUX_BIN = 'isolinux/isolinux.bin'
 | 
			
		||||
    ISOLINUX_CFG = 'isolinux/isolinux.cfg'
 | 
			
		||||
 | 
			
		||||
    isolinux_options = {'kernel': '/vmlinuz', 'ramdisk': '/initrd'}
 | 
			
		||||
    grub_options = {'linux': '/vmlinuz', 'initrd': '/initrd'}
 | 
			
		||||
 | 
			
		||||
    with utils.tempdir() as tmpdir:
 | 
			
		||||
        files_info = {
 | 
			
		||||
            kernel: 'vmlinuz',
 | 
			
		||||
            ramdisk: 'initrd',
 | 
			
		||||
            CONF.isolinux_bin: ISOLINUX_BIN,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Open the deploy iso used to initiate deploy and copy the
 | 
			
		||||
        # efiboot.img i.e. boot loader to the current temporary
 | 
			
		||||
        # directory.
 | 
			
		||||
        with utils.tempdir() as mountdir:
 | 
			
		||||
            uefi_path_info, e_img_rel_path, grub_rel_path = (
 | 
			
		||||
                _mount_deploy_iso(deploy_iso, mountdir))
 | 
			
		||||
 | 
			
		||||
            # if either of these variables are not initialized then the
 | 
			
		||||
            # uefi efiboot.img cannot be created.
 | 
			
		||||
            files_info.update(uefi_path_info)
 | 
			
		||||
            try:
 | 
			
		||||
                _create_root_fs(tmpdir, files_info)
 | 
			
		||||
            except (OSError, IOError) as e:
 | 
			
		||||
                LOG.exception(_LE("Creating the filesystem root failed."))
 | 
			
		||||
                raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
            finally:
 | 
			
		||||
                _umount_without_raise(mountdir)
 | 
			
		||||
 | 
			
		||||
        cfg = _generate_cfg(kernel_params,
 | 
			
		||||
                            CONF.isolinux_config_template, isolinux_options)
 | 
			
		||||
 | 
			
		||||
        isolinux_cfg = os.path.join(tmpdir, ISOLINUX_CFG)
 | 
			
		||||
        utils.write_to_file(isolinux_cfg, cfg)
 | 
			
		||||
 | 
			
		||||
        # Generate and copy grub config file.
 | 
			
		||||
        grub_cfg = os.path.join(tmpdir, grub_rel_path)
 | 
			
		||||
        grub_conf = _generate_cfg(kernel_params,
 | 
			
		||||
                                  CONF.grub_config_template, grub_options)
 | 
			
		||||
        utils.write_to_file(grub_cfg, grub_conf)
 | 
			
		||||
 | 
			
		||||
        # Create the boot_iso.
 | 
			
		||||
        try:
 | 
			
		||||
            utils.execute('mkisofs', '-r', '-V', "VMEDIA_BOOT_ISO",
 | 
			
		||||
                          '-cache-inodes', '-J', '-l', '-no-emul-boot',
 | 
			
		||||
                          '-boot-load-size', '4', '-boot-info-table',
 | 
			
		||||
                          '-b', ISOLINUX_BIN, '-eltorito-alt-boot',
 | 
			
		||||
                          '-e', e_img_rel_path, '-no-emul-boot',
 | 
			
		||||
                          '-o', output_file, tmpdir)
 | 
			
		||||
        except processutils.ProcessExecutionError as e:
 | 
			
		||||
            LOG.exception(_LE("Creating ISO image failed."))
 | 
			
		||||
            raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def qemu_img_info(path):
 | 
			
		||||
    """Return an object containing the parsed output from qemu-img info."""
 | 
			
		||||
    if not os.path.exists(path):
 | 
			
		||||
        return imageutils.QemuImgInfo()
 | 
			
		||||
 | 
			
		||||
    out, err = utils.execute('env', 'LC_ALL=C', 'LANG=C',
 | 
			
		||||
                             'qemu-img', 'info', path)
 | 
			
		||||
    return imageutils.QemuImgInfo(out)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_image(source, dest, out_format, run_as_root=False):
 | 
			
		||||
    """Convert image to other format."""
 | 
			
		||||
    cmd = ('qemu-img', 'convert', '-O', out_format, source, dest)
 | 
			
		||||
    utils.execute(*cmd, run_as_root=run_as_root)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def fetch(context, image_href, path, image_service=None, force_raw=False):
 | 
			
		||||
    # TODO(vish): Improve context handling and add owner and auth data
 | 
			
		||||
    #             when it is added to glance.  Right now there is no
 | 
			
		||||
    #             auth checking in glance, so we assume that access was
 | 
			
		||||
    #             checked before we got here.
 | 
			
		||||
    if not image_service:
 | 
			
		||||
        image_service = service.get_image_service(image_href,
 | 
			
		||||
                                                  context=context)
 | 
			
		||||
        LOG.debug("Using %(image_service)s to download image %(image_href)s." %
 | 
			
		||||
                  {'image_service': image_service.__class__,
 | 
			
		||||
                   'image_href': image_href})
 | 
			
		||||
 | 
			
		||||
    with fileutils.remove_path_on_error(path):
 | 
			
		||||
        with open(path, "wb") as image_file:
 | 
			
		||||
            image_service.download(image_href, image_file)
 | 
			
		||||
 | 
			
		||||
    if force_raw:
 | 
			
		||||
        image_to_raw(image_href, path, "%s.part" % path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def image_to_raw(image_href, path, path_tmp):
 | 
			
		||||
    with fileutils.remove_path_on_error(path_tmp):
 | 
			
		||||
        data = qemu_img_info(path_tmp)
 | 
			
		||||
 | 
			
		||||
        fmt = data.file_format
 | 
			
		||||
        if fmt is None:
 | 
			
		||||
            raise exception.ImageUnacceptable(
 | 
			
		||||
                reason=_("'qemu-img info' parsing failed."),
 | 
			
		||||
                image_id=image_href)
 | 
			
		||||
 | 
			
		||||
        backing_file = data.backing_file
 | 
			
		||||
        if backing_file is not None:
 | 
			
		||||
            raise exception.ImageUnacceptable(
 | 
			
		||||
                image_id=image_href,
 | 
			
		||||
                reason=_("fmt=%(fmt)s backed by: %(backing_file)s") %
 | 
			
		||||
                {'fmt': fmt, 'backing_file': backing_file})
 | 
			
		||||
 | 
			
		||||
        if fmt != "raw":
 | 
			
		||||
            staged = "%s.converted" % path
 | 
			
		||||
            LOG.debug("%(image)s was %(format)s, converting to raw" %
 | 
			
		||||
                      {'image': image_href, 'format': fmt})
 | 
			
		||||
            with fileutils.remove_path_on_error(staged):
 | 
			
		||||
                convert_image(path_tmp, staged, 'raw')
 | 
			
		||||
                os.unlink(path_tmp)
 | 
			
		||||
 | 
			
		||||
                data = qemu_img_info(staged)
 | 
			
		||||
                if data.file_format != "raw":
 | 
			
		||||
                    raise exception.ImageConvertFailed(
 | 
			
		||||
                        image_id=image_href,
 | 
			
		||||
                        reason=_("Converted to raw, but format is "
 | 
			
		||||
                                 "now %s") % data.file_format)
 | 
			
		||||
 | 
			
		||||
                os.rename(staged, path)
 | 
			
		||||
        else:
 | 
			
		||||
            os.rename(path_tmp, path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def download_size(context, image_href, image_service=None):
 | 
			
		||||
    if not image_service:
 | 
			
		||||
        image_service = service.get_image_service(image_href, context=context)
 | 
			
		||||
    return image_service.show(image_href)['size']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def converted_size(path):
 | 
			
		||||
    """Get size of converted raw image.
 | 
			
		||||
 | 
			
		||||
    The size of image converted to raw format can be growing up to the virtual
 | 
			
		||||
    size of the image.
 | 
			
		||||
 | 
			
		||||
    :param path: path to the image file.
 | 
			
		||||
    :returns: virtual size of the image or 0 if conversion not needed.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    data = qemu_img_info(path)
 | 
			
		||||
    return data.virtual_size
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_image_properties(context, image_href, properties="all"):
 | 
			
		||||
    """Returns the values of several properties of an image
 | 
			
		||||
 | 
			
		||||
    :param context: context
 | 
			
		||||
    :param image_href: href of the image
 | 
			
		||||
    :param properties: the properties whose values are required.
 | 
			
		||||
        This argument is optional, default value is "all", so if not specified
 | 
			
		||||
        all properties will be returned.
 | 
			
		||||
    :returns: a dict of the values of the properties. A property not on the
 | 
			
		||||
        glance metadata will have a value of None.
 | 
			
		||||
    """
 | 
			
		||||
    img_service = service.get_image_service(image_href, context=context)
 | 
			
		||||
    iproperties = img_service.show(image_href)['properties']
 | 
			
		||||
 | 
			
		||||
    if properties == "all":
 | 
			
		||||
        return iproperties
 | 
			
		||||
 | 
			
		||||
    return {p: iproperties.get(p) for p in properties}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_temp_url_for_glance_image(context, image_uuid):
 | 
			
		||||
    """Returns the tmp url for a glance image.
 | 
			
		||||
 | 
			
		||||
    :param context: context
 | 
			
		||||
    :param image_uuid: the UUID of the image in glance
 | 
			
		||||
    :returns: the tmp url for the glance image.
 | 
			
		||||
    """
 | 
			
		||||
    # Glance API version 2 is required for getting direct_url of the image.
 | 
			
		||||
    glance_service = service.GlanceImageService(version=2, context=context)
 | 
			
		||||
    image_properties = glance_service.show(image_uuid)
 | 
			
		||||
    LOG.debug('Got image info: %(info)s for image %(image_uuid)s.',
 | 
			
		||||
              {'info': image_properties, 'image_uuid': image_uuid})
 | 
			
		||||
    return glance_service.swift_temp_url(image_properties)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_boot_iso(context, output_filename, kernel_href,
 | 
			
		||||
                    ramdisk_href, deploy_iso_uuid, root_uuid=None,
 | 
			
		||||
                    kernel_params=None, boot_mode=None):
 | 
			
		||||
    """Creates a bootable ISO image for a node.
 | 
			
		||||
 | 
			
		||||
    Given the hrefs for kernel, ramdisk, root partition's UUID and
 | 
			
		||||
    kernel cmdline arguments, this method fetches the kernel and ramdisk,
 | 
			
		||||
    and builds a bootable ISO image that can be used to boot up the
 | 
			
		||||
    baremetal node.
 | 
			
		||||
 | 
			
		||||
    :param context: context
 | 
			
		||||
    :param output_filename: the absolute path of the output ISO file
 | 
			
		||||
    :param kernel_href: URL or glance uuid of the kernel to use
 | 
			
		||||
    :param ramdisk_href: URL or glance uuid of the ramdisk to use
 | 
			
		||||
    :param deploy_iso_uuid: URL or glance uuid of the deploy iso used
 | 
			
		||||
    :param root_uuid: uuid of the root filesystem (optional)
 | 
			
		||||
    :param kernel_params: a string containing whitespace separated values
 | 
			
		||||
        kernel cmdline arguments of the form K=V or K (optional).
 | 
			
		||||
    :boot_mode: the boot mode in which the deploy is to happen.
 | 
			
		||||
    :raises: ImageCreationFailed, if creating boot ISO failed.
 | 
			
		||||
    """
 | 
			
		||||
    with utils.tempdir() as tmpdir:
 | 
			
		||||
        kernel_path = os.path.join(tmpdir, kernel_href.split('/')[-1])
 | 
			
		||||
        ramdisk_path = os.path.join(tmpdir, ramdisk_href.split('/')[-1])
 | 
			
		||||
        fetch(context, kernel_href, kernel_path)
 | 
			
		||||
        fetch(context, ramdisk_href, ramdisk_path)
 | 
			
		||||
 | 
			
		||||
        params = []
 | 
			
		||||
        if root_uuid:
 | 
			
		||||
            params.append('root=UUID=%s' % root_uuid)
 | 
			
		||||
        if kernel_params:
 | 
			
		||||
            params.append(kernel_params)
 | 
			
		||||
 | 
			
		||||
        if boot_mode == 'uefi':
 | 
			
		||||
            deploy_iso = os.path.join(tmpdir, deploy_iso_uuid)
 | 
			
		||||
            fetch(context, deploy_iso_uuid, deploy_iso)
 | 
			
		||||
            create_isolinux_image_for_uefi(output_filename,
 | 
			
		||||
                                           deploy_iso,
 | 
			
		||||
                                           kernel_path,
 | 
			
		||||
                                           ramdisk_path,
 | 
			
		||||
                                           params)
 | 
			
		||||
        else:
 | 
			
		||||
            create_isolinux_image_for_bios(output_filename,
 | 
			
		||||
                                           kernel_path,
 | 
			
		||||
                                           ramdisk_path,
 | 
			
		||||
                                           params)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_whole_disk_image(ctx, instance_info):
 | 
			
		||||
    """Find out if the image is a partition image or a whole disk image.
 | 
			
		||||
 | 
			
		||||
    :param ctx: an admin context
 | 
			
		||||
    :param instance_info: a node's instance info dict
 | 
			
		||||
 | 
			
		||||
    :returns True for whole disk images and False for partition images
 | 
			
		||||
        and None on no image_source or Error.
 | 
			
		||||
    """
 | 
			
		||||
    image_source = instance_info.get('image_source')
 | 
			
		||||
    if not image_source:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    is_whole_disk_image = False
 | 
			
		||||
    if glance_utils.is_glance_image(image_source):
 | 
			
		||||
        try:
 | 
			
		||||
            iproperties = get_image_properties(ctx, image_source)
 | 
			
		||||
        except Exception:
 | 
			
		||||
            return
 | 
			
		||||
        is_whole_disk_image = (not iproperties.get('kernel_id') and
 | 
			
		||||
                               not iproperties.get('ramdisk_id'))
 | 
			
		||||
    else:
 | 
			
		||||
        # Non glance image ref
 | 
			
		||||
        if (not instance_info.get('kernel') and
 | 
			
		||||
            not instance_info.get('ramdisk')):
 | 
			
		||||
            is_whole_disk_image = True
 | 
			
		||||
 | 
			
		||||
    return is_whole_disk_image
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _mount_deploy_iso(deploy_iso, mountdir):
 | 
			
		||||
    """This function opens up the deploy iso used for deploy.
 | 
			
		||||
 | 
			
		||||
    :param: deploy_iso: path to the deploy iso where its
 | 
			
		||||
                        contents are fetched to.
 | 
			
		||||
    :raises: ImageCreationFailed if mount fails.
 | 
			
		||||
    :returns: a tuple consisting of - 1. a dictionary containing
 | 
			
		||||
                                         the values as required
 | 
			
		||||
                                         by create_isolinux_image,
 | 
			
		||||
                                      2. efiboot.img relative path, and
 | 
			
		||||
                                      3. grub.cfg relative path.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    e_img_rel_path = None
 | 
			
		||||
    e_img_path = None
 | 
			
		||||
    grub_rel_path = None
 | 
			
		||||
    grub_path = None
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        utils.mount(deploy_iso, mountdir, '-o', 'loop')
 | 
			
		||||
    except processutils.ProcessExecutionError as e:
 | 
			
		||||
        LOG.exception(_LE("mounting the deploy iso failed."))
 | 
			
		||||
        raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        for (dir, subdir, files) in os.walk(mountdir):
 | 
			
		||||
            if 'efiboot.img' in files:
 | 
			
		||||
                e_img_path = os.path.join(dir, 'efiboot.img')
 | 
			
		||||
                e_img_rel_path = os.path.relpath(e_img_path,
 | 
			
		||||
                                                 mountdir)
 | 
			
		||||
            if 'grub.cfg' in files:
 | 
			
		||||
                grub_path = os.path.join(dir, 'grub.cfg')
 | 
			
		||||
                grub_rel_path = os.path.relpath(grub_path,
 | 
			
		||||
                                                mountdir)
 | 
			
		||||
    except (OSError, IOError) as e:
 | 
			
		||||
        LOG.exception(_LE("examining the deploy iso failed."))
 | 
			
		||||
        _umount_without_raise(mountdir)
 | 
			
		||||
        raise exception.ImageCreationFailed(image_type='iso', error=e)
 | 
			
		||||
 | 
			
		||||
    # check if the variables are assigned some values or not during
 | 
			
		||||
    # walk of the mountdir.
 | 
			
		||||
    if not (e_img_path and e_img_rel_path and grub_path and grub_rel_path):
 | 
			
		||||
        error = (_("Deploy iso didn't contain efiboot.img or grub.cfg"))
 | 
			
		||||
        _umount_without_raise(mountdir)
 | 
			
		||||
        raise exception.ImageCreationFailed(image_type='iso', error=error)
 | 
			
		||||
 | 
			
		||||
    uefi_path_info = {e_img_path: e_img_rel_path,
 | 
			
		||||
                      grub_path: grub_rel_path}
 | 
			
		||||
 | 
			
		||||
    # Returning a tuple as it makes the code simpler and clean.
 | 
			
		||||
    # uefi_path_info: is needed by the caller for _create_root_fs to create
 | 
			
		||||
    # appropriate directory structures for uefi boot iso.
 | 
			
		||||
    # grub_rel_path: is needed to copy the new grub.cfg generated using
 | 
			
		||||
    # generate_cfg() to the same directory path structure where it was
 | 
			
		||||
    # present in deploy iso. This path varies for different OS vendors.
 | 
			
		||||
    # e_img_rel_path: is required by mkisofs to generate boot iso.
 | 
			
		||||
    return uefi_path_info, e_img_rel_path, grub_rel_path
 | 
			
		||||
							
								
								
									
										5
									
								
								iotronic/common/isolinux_config.template
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								iotronic/common/isolinux_config.template
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
default boot
 | 
			
		||||
 | 
			
		||||
label boot
 | 
			
		||||
kernel {{ kernel }}
 | 
			
		||||
append initrd={{ ramdisk }} text {{ kernel_params }} --
 | 
			
		||||
							
								
								
									
										139
									
								
								iotronic/common/keystone.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								iotronic/common/keystone.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
# 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 keystoneclient import exceptions as ksexception
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from six.moves.urllib import parse
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
 | 
			
		||||
keystone_opts = [
 | 
			
		||||
    cfg.StrOpt('region_name',
 | 
			
		||||
               help='The region used for getting endpoints of OpenStack'
 | 
			
		||||
                    'services.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF.register_opts(keystone_opts, group='keystone')
 | 
			
		||||
CONF.import_group('keystone_authtoken', 'keystonemiddleware.auth_token')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_apiv3(auth_url, auth_version):
 | 
			
		||||
    """Checks if V3 version of API is being used or not.
 | 
			
		||||
 | 
			
		||||
    This method inspects auth_url and auth_version, and checks whether V3
 | 
			
		||||
    version of the API is being used or not.
 | 
			
		||||
 | 
			
		||||
    :param auth_url: a http or https url to be inspected (like
 | 
			
		||||
        'http://127.0.0.1:9898/').
 | 
			
		||||
    :param auth_version: a string containing the version (like 'v2', 'v3.0')
 | 
			
		||||
    :returns: True if V3 of the API is being used.
 | 
			
		||||
    """
 | 
			
		||||
    return auth_version == 'v3.0' or '/v3' in parse.urlparse(auth_url).path
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_ksclient(token=None):
 | 
			
		||||
    auth_url = CONF.keystone_authtoken.auth_uri
 | 
			
		||||
    if not auth_url:
 | 
			
		||||
        raise exception.KeystoneFailure(_('Keystone API endpoint is missing'))
 | 
			
		||||
 | 
			
		||||
    auth_version = CONF.keystone_authtoken.auth_version
 | 
			
		||||
    api_v3 = _is_apiv3(auth_url, auth_version)
 | 
			
		||||
 | 
			
		||||
    if api_v3:
 | 
			
		||||
        from keystoneclient.v3 import client
 | 
			
		||||
    else:
 | 
			
		||||
        from keystoneclient.v2_0 import client
 | 
			
		||||
 | 
			
		||||
    auth_url = get_keystone_url(auth_url, auth_version)
 | 
			
		||||
    try:
 | 
			
		||||
        if token:
 | 
			
		||||
            return client.Client(token=token, auth_url=auth_url)
 | 
			
		||||
        else:
 | 
			
		||||
            return client.Client(
 | 
			
		||||
                username=CONF.keystone_authtoken.admin_user,
 | 
			
		||||
                password=CONF.keystone_authtoken.admin_password,
 | 
			
		||||
                tenant_name=CONF.keystone_authtoken.admin_tenant_name,
 | 
			
		||||
                region_name=CONF.keystone.region_name,
 | 
			
		||||
                auth_url=auth_url)
 | 
			
		||||
    except ksexception.Unauthorized:
 | 
			
		||||
        raise exception.KeystoneUnauthorized()
 | 
			
		||||
    except ksexception.AuthorizationFailure as err:
 | 
			
		||||
        raise exception.KeystoneFailure(_('Could not authorize in Keystone:'
 | 
			
		||||
                                          ' %s') % err)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_keystone_url(auth_url, auth_version):
 | 
			
		||||
    """Gives an http/https url to contact keystone.
 | 
			
		||||
 | 
			
		||||
    Given an auth_url and auth_version, this method generates the url in
 | 
			
		||||
    which keystone can be reached.
 | 
			
		||||
 | 
			
		||||
    :param auth_url: a http or https url to be inspected (like
 | 
			
		||||
        'http://127.0.0.1:9898/').
 | 
			
		||||
    :param auth_version: a string containing the version (like v2, v3.0, etc)
 | 
			
		||||
    :returns: a string containing the keystone url
 | 
			
		||||
    """
 | 
			
		||||
    api_v3 = _is_apiv3(auth_url, auth_version)
 | 
			
		||||
    api_version = 'v3' if api_v3 else 'v2.0'
 | 
			
		||||
    # NOTE(lucasagomes): Get rid of the trailing '/' otherwise urljoin()
 | 
			
		||||
    #   fails to override the version in the URL
 | 
			
		||||
    return parse.urljoin(auth_url.rstrip('/'), api_version)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_service_url(service_type='baremetal', endpoint_type='internal'):
 | 
			
		||||
    """Wrapper for get service url from keystone service catalog.
 | 
			
		||||
 | 
			
		||||
    Given a service_type and an endpoint_type, this method queries keystone
 | 
			
		||||
    service catalog and provides the url for the desired endpoint.
 | 
			
		||||
 | 
			
		||||
    :param service_type: the keystone service for which url is required.
 | 
			
		||||
    :param endpoint_type: the type of endpoint for the service.
 | 
			
		||||
    :returns: an http/https url for the desired endpoint.
 | 
			
		||||
    """
 | 
			
		||||
    ksclient = _get_ksclient()
 | 
			
		||||
 | 
			
		||||
    if not ksclient.has_service_catalog():
 | 
			
		||||
        raise exception.KeystoneFailure(_('No Keystone service catalog '
 | 
			
		||||
                                          'loaded'))
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        endpoint = ksclient.service_catalog.url_for(
 | 
			
		||||
            service_type=service_type,
 | 
			
		||||
            endpoint_type=endpoint_type,
 | 
			
		||||
            region_name=CONF.keystone.region_name)
 | 
			
		||||
 | 
			
		||||
    except ksexception.EndpointNotFound:
 | 
			
		||||
        raise exception.CatalogNotFound(service_type=service_type,
 | 
			
		||||
                                        endpoint_type=endpoint_type)
 | 
			
		||||
 | 
			
		||||
    return endpoint
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_admin_auth_token():
 | 
			
		||||
    """Get an admin auth_token from the Keystone."""
 | 
			
		||||
    ksclient = _get_ksclient()
 | 
			
		||||
    return ksclient.auth_token
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def token_expires_soon(token, duration=None):
 | 
			
		||||
    """Determines if token expiration is about to occur.
 | 
			
		||||
 | 
			
		||||
    :param duration: time interval in seconds
 | 
			
		||||
    :returns: boolean : true if expiration is within the given duration
 | 
			
		||||
    """
 | 
			
		||||
    ksclient = _get_ksclient(token=token)
 | 
			
		||||
    return ksclient.auth_ref.will_expire_soon(stale_duration=duration)
 | 
			
		||||
							
								
								
									
										30
									
								
								iotronic/common/network.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								iotronic/common/network.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
# Copyright 2014 Rackspace, Inc.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_node_vif_ids(task):
 | 
			
		||||
    """Get all VIF ids for a node.
 | 
			
		||||
 | 
			
		||||
    This function does not handle multi node operations.
 | 
			
		||||
 | 
			
		||||
    :param task: a TaskManager instance.
 | 
			
		||||
    :returns: A dict of the Node's port UUIDs and their associated VIFs
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    port_vifs = {}
 | 
			
		||||
    for port in task.ports:
 | 
			
		||||
        vif = port.extra.get('vif_port_id')
 | 
			
		||||
        if vif:
 | 
			
		||||
            port_vifs[port.uuid] = vif
 | 
			
		||||
    return port_vifs
 | 
			
		||||
							
								
								
									
										66
									
								
								iotronic/common/paths.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								iotronic/common/paths.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
# Copyright 2012 Red Hat, Inc.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
 | 
			
		||||
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 iotronic binaries are installed.'),
 | 
			
		||||
    cfg.StrOpt('state_path',
 | 
			
		||||
               default='$pybasedir',
 | 
			
		||||
               help="Top-level directory for maintaining iotronic's state."),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(path_opts)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def basedir_def(*args):
 | 
			
		||||
    """Return an uninterpolated path relative to $pybasedir."""
 | 
			
		||||
    return os.path.join('$pybasedir', *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def bindir_def(*args):
 | 
			
		||||
    """Return an uninterpolated path relative to $bindir."""
 | 
			
		||||
    return os.path.join('$bindir', *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def state_path_def(*args):
 | 
			
		||||
    """Return an uninterpolated path relative to $state_path."""
 | 
			
		||||
    return os.path.join('$state_path', *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def basedir_rel(*args):
 | 
			
		||||
    """Return a path relative to $pybasedir."""
 | 
			
		||||
    return os.path.join(CONF.pybasedir, *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def bindir_rel(*args):
 | 
			
		||||
    """Return a path relative to $bindir."""
 | 
			
		||||
    return os.path.join(CONF.bindir, *args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def state_path_rel(*args):
 | 
			
		||||
    """Return a path relative to $state_path."""
 | 
			
		||||
    return os.path.join(CONF.state_path, *args)
 | 
			
		||||
							
								
								
									
										68
									
								
								iotronic/common/policy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								iotronic/common/policy.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,68 @@
 | 
			
		||||
# Copyright (c) 2011 OpenStack Foundation
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""Policy Engine For Iotronic."""
 | 
			
		||||
 | 
			
		||||
from oslo_concurrency import lockutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_policy import policy
 | 
			
		||||
 | 
			
		||||
_ENFORCER = None
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@lockutils.synchronized('policy_enforcer', 'iotronic-')
 | 
			
		||||
def init_enforcer(policy_file=None, rules=None,
 | 
			
		||||
                  default_rule=None, use_conf=True):
 | 
			
		||||
    """Synchronously initializes the policy enforcer
 | 
			
		||||
 | 
			
		||||
       :param policy_file: Custom policy file to use, if none is specified,
 | 
			
		||||
                           `CONF.policy_file` will be used.
 | 
			
		||||
       :param rules: Default dictionary / Rules to use. It will be
 | 
			
		||||
                     considered just in the first instantiation.
 | 
			
		||||
       :param default_rule: Default rule to use, CONF.default_rule will
 | 
			
		||||
                            be used if none is specified.
 | 
			
		||||
       :param use_conf: Whether to load rules from config file.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    global _ENFORCER
 | 
			
		||||
 | 
			
		||||
    if _ENFORCER:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    _ENFORCER = policy.Enforcer(CONF, policy_file=policy_file,
 | 
			
		||||
                                rules=rules,
 | 
			
		||||
                                default_rule=default_rule,
 | 
			
		||||
                                use_conf=use_conf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_enforcer():
 | 
			
		||||
    """Provides access to the single instance of Policy enforcer."""
 | 
			
		||||
 | 
			
		||||
    if not _ENFORCER:
 | 
			
		||||
        init_enforcer()
 | 
			
		||||
 | 
			
		||||
    return _ENFORCER
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def enforce(rule, target, creds, do_raise=False, exc=None, *args, **kwargs):
 | 
			
		||||
    """A shortcut for policy.Enforcer.enforce()
 | 
			
		||||
 | 
			
		||||
    Checks authorization of a rule against the target and credentials.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    enforcer = get_enforcer()
 | 
			
		||||
    return enforcer.enforce(rule, target, creds, do_raise=do_raise,
 | 
			
		||||
                            exc=exc, *args, **kwargs)
 | 
			
		||||
							
								
								
									
										285
									
								
								iotronic/common/pxe_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								iotronic/common/pxe_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,285 @@
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2014 Rackspace, 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 os
 | 
			
		||||
 | 
			
		||||
import jinja2
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
 | 
			
		||||
from iotronic.common import dhcp_factory
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common import utils
 | 
			
		||||
from iotronic.drivers.modules import deploy_utils
 | 
			
		||||
from iotronic.drivers import utils as driver_utils
 | 
			
		||||
from iotronic.openstack.common import fileutils
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
PXE_CFG_DIR_NAME = 'pxelinux.cfg'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_root_dir():
 | 
			
		||||
    """Returns the directory where the config files and images will live."""
 | 
			
		||||
    if CONF.pxe.ipxe_enabled:
 | 
			
		||||
        return CONF.pxe.http_root
 | 
			
		||||
    else:
 | 
			
		||||
        return CONF.pxe.tftp_root
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _ensure_config_dirs_exist(node_uuid):
 | 
			
		||||
    """Ensure that the node's and PXE configuration directories exist.
 | 
			
		||||
 | 
			
		||||
    :param node_uuid: the UUID of the node.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    root_dir = get_root_dir()
 | 
			
		||||
    fileutils.ensure_tree(os.path.join(root_dir, node_uuid))
 | 
			
		||||
    fileutils.ensure_tree(os.path.join(root_dir, PXE_CFG_DIR_NAME))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _build_pxe_config(pxe_options, template):
 | 
			
		||||
    """Build the PXE boot configuration file.
 | 
			
		||||
 | 
			
		||||
    This method builds the PXE boot configuration file by rendering the
 | 
			
		||||
    template with the given parameters.
 | 
			
		||||
 | 
			
		||||
    :param pxe_options: A dict of values to set on the configuration file.
 | 
			
		||||
    :param template: The PXE configuration template.
 | 
			
		||||
    :returns: A formatted string with the file content.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    tmpl_path, tmpl_file = os.path.split(template)
 | 
			
		||||
    env = jinja2.Environment(loader=jinja2.FileSystemLoader(tmpl_path))
 | 
			
		||||
    template = env.get_template(tmpl_file)
 | 
			
		||||
    return template.render({'pxe_options': pxe_options,
 | 
			
		||||
                            'ROOT': '{{ ROOT }}',
 | 
			
		||||
                            'DISK_IDENTIFIER': '{{ DISK_IDENTIFIER }}',
 | 
			
		||||
                            })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _link_mac_pxe_configs(task):
 | 
			
		||||
    """Link each MAC address with the PXE configuration file.
 | 
			
		||||
 | 
			
		||||
    :param task: A TaskManager instance.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def create_link(mac_path):
 | 
			
		||||
        utils.unlink_without_raise(mac_path)
 | 
			
		||||
        utils.create_link_without_raise(pxe_config_file_path, mac_path)
 | 
			
		||||
 | 
			
		||||
    pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
 | 
			
		||||
    for mac in driver_utils.get_node_mac_addresses(task):
 | 
			
		||||
        create_link(_get_pxe_mac_path(mac))
 | 
			
		||||
        # TODO(lucasagomes): Backward compatibility with :hexraw,
 | 
			
		||||
        # to be removed in M.
 | 
			
		||||
        # see: https://bugs.launchpad.net/iotronic/+bug/1441710
 | 
			
		||||
        if CONF.pxe.ipxe_enabled:
 | 
			
		||||
            create_link(_get_pxe_mac_path(mac, delimiter=''))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _link_ip_address_pxe_configs(task):
 | 
			
		||||
    """Link each IP address with the PXE configuration file.
 | 
			
		||||
 | 
			
		||||
    :param task: A TaskManager instance.
 | 
			
		||||
    :raises: FailedToGetIPAddressOnPort
 | 
			
		||||
    :raises: InvalidIPv4Address
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
 | 
			
		||||
 | 
			
		||||
    api = dhcp_factory.DHCPFactory().provider
 | 
			
		||||
    ip_addrs = api.get_ip_addresses(task)
 | 
			
		||||
    if not ip_addrs:
 | 
			
		||||
        raise exception.FailedToGetIPAddressOnPort(_(
 | 
			
		||||
            "Failed to get IP address for any port on node %s.") %
 | 
			
		||||
            task.node.uuid)
 | 
			
		||||
    for port_ip_address in ip_addrs:
 | 
			
		||||
        ip_address_path = _get_pxe_ip_address_path(port_ip_address)
 | 
			
		||||
        utils.unlink_without_raise(ip_address_path)
 | 
			
		||||
        utils.create_link_without_raise(pxe_config_file_path,
 | 
			
		||||
                                        ip_address_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_pxe_mac_path(mac, delimiter=None):
 | 
			
		||||
    """Convert a MAC address into a PXE config file name.
 | 
			
		||||
 | 
			
		||||
    :param mac: A MAC address string in the format xx:xx:xx:xx:xx:xx.
 | 
			
		||||
    :param delimiter: The MAC address delimiter. Defaults to dash ('-').
 | 
			
		||||
    :returns: the path to the config file.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if delimiter is None:
 | 
			
		||||
        delimiter = '-'
 | 
			
		||||
 | 
			
		||||
    mac_file_name = mac.replace(':', delimiter).lower()
 | 
			
		||||
    if not CONF.pxe.ipxe_enabled:
 | 
			
		||||
        mac_file_name = '01-' + mac_file_name
 | 
			
		||||
 | 
			
		||||
    return os.path.join(get_root_dir(), PXE_CFG_DIR_NAME, mac_file_name)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_pxe_ip_address_path(ip_address):
 | 
			
		||||
    """Convert an ipv4 address into a PXE config file name.
 | 
			
		||||
 | 
			
		||||
    :param ip_address: A valid IPv4 address string in the format 'n.n.n.n'.
 | 
			
		||||
    :returns: the path to the config file.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    ip = ip_address.split('.')
 | 
			
		||||
    hex_ip = '{0:02X}{1:02X}{2:02X}{3:02X}'.format(*map(int, ip))
 | 
			
		||||
 | 
			
		||||
    return os.path.join(
 | 
			
		||||
        CONF.pxe.tftp_root, hex_ip + ".conf"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_deploy_kr_info(node_uuid, driver_info):
 | 
			
		||||
    """Get href and tftp path for deploy kernel and ramdisk.
 | 
			
		||||
 | 
			
		||||
    Note: driver_info should be validated outside of this method.
 | 
			
		||||
    """
 | 
			
		||||
    root_dir = get_root_dir()
 | 
			
		||||
    image_info = {}
 | 
			
		||||
    for label in ('deploy_kernel', 'deploy_ramdisk'):
 | 
			
		||||
        image_info[label] = (
 | 
			
		||||
            str(driver_info[label]),
 | 
			
		||||
            os.path.join(root_dir, node_uuid, label)
 | 
			
		||||
        )
 | 
			
		||||
    return image_info
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_pxe_config_file_path(node_uuid):
 | 
			
		||||
    """Generate the path for the node's PXE configuration file.
 | 
			
		||||
 | 
			
		||||
    :param node_uuid: the UUID of the node.
 | 
			
		||||
    :returns: The path to the node's PXE configuration file.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    return os.path.join(get_root_dir(), node_uuid, 'config')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_pxe_config(task, pxe_options, template=None):
 | 
			
		||||
    """Generate PXE configuration file and MAC address links for it.
 | 
			
		||||
 | 
			
		||||
    This method will generate the PXE configuration file for the task's
 | 
			
		||||
    node under a directory named with the UUID of that node. For each
 | 
			
		||||
    MAC address (port) of that node, a symlink for the configuration file
 | 
			
		||||
    will be created under the PXE configuration directory, so regardless
 | 
			
		||||
    of which port boots first they'll get the same PXE configuration.
 | 
			
		||||
 | 
			
		||||
    :param task: A TaskManager instance.
 | 
			
		||||
    :param pxe_options: A dictionary with the PXE configuration
 | 
			
		||||
        parameters.
 | 
			
		||||
    :param template: The PXE configuration template. If no template is
 | 
			
		||||
        given the CONF.pxe.pxe_config_template will be used.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    LOG.debug("Building PXE config for node %s", task.node.uuid)
 | 
			
		||||
 | 
			
		||||
    if template is None:
 | 
			
		||||
        template = CONF.pxe.pxe_config_template
 | 
			
		||||
 | 
			
		||||
    _ensure_config_dirs_exist(task.node.uuid)
 | 
			
		||||
 | 
			
		||||
    pxe_config_file_path = get_pxe_config_file_path(task.node.uuid)
 | 
			
		||||
    pxe_config = _build_pxe_config(pxe_options, template)
 | 
			
		||||
    utils.write_to_file(pxe_config_file_path, pxe_config)
 | 
			
		||||
 | 
			
		||||
    if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
 | 
			
		||||
        _link_ip_address_pxe_configs(task)
 | 
			
		||||
    else:
 | 
			
		||||
        _link_mac_pxe_configs(task)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clean_up_pxe_config(task):
 | 
			
		||||
    """Clean up the TFTP environment for the task's node.
 | 
			
		||||
 | 
			
		||||
    :param task: A TaskManager instance.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    LOG.debug("Cleaning up PXE config for node %s", task.node.uuid)
 | 
			
		||||
 | 
			
		||||
    if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
 | 
			
		||||
        api = dhcp_factory.DHCPFactory().provider
 | 
			
		||||
        ip_addresses = api.get_ip_addresses(task)
 | 
			
		||||
        if not ip_addresses:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        for port_ip_address in ip_addresses:
 | 
			
		||||
            try:
 | 
			
		||||
                ip_address_path = _get_pxe_ip_address_path(port_ip_address)
 | 
			
		||||
            except exception.InvalidIPv4Address:
 | 
			
		||||
                continue
 | 
			
		||||
            utils.unlink_without_raise(ip_address_path)
 | 
			
		||||
    else:
 | 
			
		||||
        for mac in driver_utils.get_node_mac_addresses(task):
 | 
			
		||||
            utils.unlink_without_raise(_get_pxe_mac_path(mac))
 | 
			
		||||
            # TODO(lucasagomes): Backward compatibility with :hexraw,
 | 
			
		||||
            # to be removed in M.
 | 
			
		||||
            # see: https://bugs.launchpad.net/iotronic/+bug/1441710
 | 
			
		||||
            if CONF.pxe.ipxe_enabled:
 | 
			
		||||
                utils.unlink_without_raise(_get_pxe_mac_path(mac,
 | 
			
		||||
                                           delimiter=''))
 | 
			
		||||
 | 
			
		||||
    utils.rmtree_without_raise(os.path.join(get_root_dir(),
 | 
			
		||||
                                            task.node.uuid))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dhcp_options_for_instance(task):
 | 
			
		||||
    """Retrieves the DHCP PXE boot options.
 | 
			
		||||
 | 
			
		||||
    :param task: A TaskManager instance.
 | 
			
		||||
    """
 | 
			
		||||
    dhcp_opts = []
 | 
			
		||||
    if CONF.pxe.ipxe_enabled:
 | 
			
		||||
        script_name = os.path.basename(CONF.pxe.ipxe_boot_script)
 | 
			
		||||
        ipxe_script_url = '/'.join([CONF.pxe.http_url, script_name])
 | 
			
		||||
        dhcp_provider_name = dhcp_factory.CONF.dhcp.dhcp_provider
 | 
			
		||||
        # if the request comes from dumb firmware send them the iPXE
 | 
			
		||||
        # boot image.
 | 
			
		||||
        if dhcp_provider_name == 'neutron':
 | 
			
		||||
            # Neutron use dnsmasq as default DHCP agent, add extra config
 | 
			
		||||
            # to neutron "dhcp-match=set:ipxe,175" and use below option
 | 
			
		||||
            dhcp_opts.append({'opt_name': 'tag:!ipxe,bootfile-name',
 | 
			
		||||
                              'opt_value': CONF.pxe.pxe_bootfile_name})
 | 
			
		||||
            dhcp_opts.append({'opt_name': 'tag:ipxe,bootfile-name',
 | 
			
		||||
                              'opt_value': ipxe_script_url})
 | 
			
		||||
        else:
 | 
			
		||||
            # !175 == non-iPXE.
 | 
			
		||||
            # http://ipxe.org/howto/dhcpd#ipxe-specific_options
 | 
			
		||||
            dhcp_opts.append({'opt_name': '!175,bootfile-name',
 | 
			
		||||
                              'opt_value': CONF.pxe.pxe_bootfile_name})
 | 
			
		||||
            dhcp_opts.append({'opt_name': 'bootfile-name',
 | 
			
		||||
                              'opt_value': ipxe_script_url})
 | 
			
		||||
    else:
 | 
			
		||||
        if deploy_utils.get_boot_mode_for_deploy(task.node) == 'uefi':
 | 
			
		||||
            boot_file = CONF.pxe.uefi_pxe_bootfile_name
 | 
			
		||||
        else:
 | 
			
		||||
            boot_file = CONF.pxe.pxe_bootfile_name
 | 
			
		||||
 | 
			
		||||
        dhcp_opts.append({'opt_name': 'bootfile-name',
 | 
			
		||||
                          'opt_value': boot_file})
 | 
			
		||||
 | 
			
		||||
    dhcp_opts.append({'opt_name': 'server-ip-address',
 | 
			
		||||
                      'opt_value': CONF.pxe.tftp_server})
 | 
			
		||||
    dhcp_opts.append({'opt_name': 'tftp-server',
 | 
			
		||||
                      'opt_value': CONF.pxe.tftp_server})
 | 
			
		||||
    return dhcp_opts
 | 
			
		||||
							
								
								
									
										150
									
								
								iotronic/common/rpc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								iotronic/common/rpc.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,150 @@
 | 
			
		||||
# Copyright 2014 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.
 | 
			
		||||
 | 
			
		||||
__all__ = [
 | 
			
		||||
    'init',
 | 
			
		||||
    'cleanup',
 | 
			
		||||
    'set_defaults',
 | 
			
		||||
    'add_extra_exmods',
 | 
			
		||||
    'clear_extra_exmods',
 | 
			
		||||
    'get_allowed_exmods',
 | 
			
		||||
    'RequestContextSerializer',
 | 
			
		||||
    'get_client',
 | 
			
		||||
    'get_server',
 | 
			
		||||
    'get_notifier',
 | 
			
		||||
    '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
 | 
			
		||||
TRANSPORT = None
 | 
			
		||||
NOTIFIER = None
 | 
			
		||||
 | 
			
		||||
ALLOWED_EXMODS = [
 | 
			
		||||
    exception.__name__,
 | 
			
		||||
]
 | 
			
		||||
EXTRA_EXMODS = []
 | 
			
		||||
 | 
			
		||||
# NOTE(lucasagomes): The iotronic.openstack.common.rpc entries are for
 | 
			
		||||
# backwards compat with IceHouse rpc_backend configuration values.
 | 
			
		||||
TRANSPORT_ALIASES = {
 | 
			
		||||
    'iotronic.openstack.common.rpc.impl_kombu': 'rabbit',
 | 
			
		||||
    'iotronic.openstack.common.rpc.impl_qpid': 'qpid',
 | 
			
		||||
    'iotronic.openstack.common.rpc.impl_zmq': 'zmq',
 | 
			
		||||
    'iotronic.rpc.impl_kombu': 'rabbit',
 | 
			
		||||
    'iotronic.rpc.impl_qpid': 'qpid',
 | 
			
		||||
    'iotronic.rpc.impl_zmq': 'zmq',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def init(conf):
 | 
			
		||||
    global TRANSPORT, NOTIFIER
 | 
			
		||||
    exmods = get_allowed_exmods()
 | 
			
		||||
    TRANSPORT = messaging.get_transport(conf,
 | 
			
		||||
                                        allowed_remote_exmods=exmods,
 | 
			
		||||
                                        aliases=TRANSPORT_ALIASES)
 | 
			
		||||
    serializer = RequestContextSerializer(JsonPayloadSerializer())
 | 
			
		||||
    NOTIFIER = messaging.Notifier(TRANSPORT, serializer=serializer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def cleanup():
 | 
			
		||||
    global TRANSPORT, NOTIFIER
 | 
			
		||||
    assert TRANSPORT is not None
 | 
			
		||||
    assert NOTIFIER is not None
 | 
			
		||||
    TRANSPORT.cleanup()
 | 
			
		||||
    TRANSPORT = NOTIFIER = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def set_defaults(control_exchange):
 | 
			
		||||
    messaging.set_transport_defaults(control_exchange)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_extra_exmods(*args):
 | 
			
		||||
    EXTRA_EXMODS.extend(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clear_extra_exmods():
 | 
			
		||||
    del EXTRA_EXMODS[:]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_allowed_exmods():
 | 
			
		||||
    return ALLOWED_EXMODS + EXTRA_EXMODS
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JsonPayloadSerializer(messaging.NoOpSerializer):
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def serialize_entity(context, entity):
 | 
			
		||||
        return jsonutils.to_primitive(entity, convert_instances=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RequestContextSerializer(messaging.Serializer):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, base):
 | 
			
		||||
        self._base = base
 | 
			
		||||
 | 
			
		||||
    def serialize_entity(self, context, entity):
 | 
			
		||||
        if not self._base:
 | 
			
		||||
            return entity
 | 
			
		||||
        return self._base.serialize_entity(context, entity)
 | 
			
		||||
 | 
			
		||||
    def deserialize_entity(self, context, entity):
 | 
			
		||||
        if not self._base:
 | 
			
		||||
            return entity
 | 
			
		||||
        return self._base.deserialize_entity(context, entity)
 | 
			
		||||
 | 
			
		||||
    def serialize_context(self, context):
 | 
			
		||||
        return context.to_dict()
 | 
			
		||||
 | 
			
		||||
    def deserialize_context(self, context):
 | 
			
		||||
        return iotronic_context.RequestContext.from_dict(context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_transport_url(url_str=None):
 | 
			
		||||
    #LOG.info('yoooooooooooo')
 | 
			
		||||
    return messaging.TransportURL.parse(CONF, url_str, TRANSPORT_ALIASES)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_client(target, version_cap=None, serializer=None):
 | 
			
		||||
    assert TRANSPORT is not None
 | 
			
		||||
    serializer = RequestContextSerializer(serializer)
 | 
			
		||||
    return messaging.RPCClient(TRANSPORT,
 | 
			
		||||
                               target,
 | 
			
		||||
                               version_cap=version_cap,
 | 
			
		||||
                               serializer=serializer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_server(target, endpoints, serializer=None):
 | 
			
		||||
    assert TRANSPORT is not None
 | 
			
		||||
    serializer = RequestContextSerializer(serializer)
 | 
			
		||||
    return messaging.get_rpc_server(TRANSPORT,
 | 
			
		||||
                                    target,
 | 
			
		||||
                                    endpoints,
 | 
			
		||||
                                    executor='eventlet',
 | 
			
		||||
                                    serializer=serializer)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_notifier(service=None, host=None, publisher_id=None):
 | 
			
		||||
    assert NOTIFIER is not None
 | 
			
		||||
    if not publisher_id:
 | 
			
		||||
        publisher_id = "%s.%s" % (service, host or CONF.host)
 | 
			
		||||
    return NOTIFIER.prepare(publisher_id=publisher_id)
 | 
			
		||||
							
								
								
									
										53
									
								
								iotronic/common/safe_utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								iotronic/common/safe_utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# Copyright 2011 Justin Santa Barbara
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""Utilities and helper functions that won't produce circular imports."""
 | 
			
		||||
 | 
			
		||||
import inspect
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def getcallargs(function, *args, **kwargs):
 | 
			
		||||
    """This is a simplified inspect.getcallargs (2.7+).
 | 
			
		||||
 | 
			
		||||
    It should be replaced when python >= 2.7 is standard.
 | 
			
		||||
    """
 | 
			
		||||
    keyed_args = {}
 | 
			
		||||
    argnames, varargs, keywords, defaults = inspect.getargspec(function)
 | 
			
		||||
 | 
			
		||||
    keyed_args.update(kwargs)
 | 
			
		||||
 | 
			
		||||
    # NOTE(alaski) the implicit 'self' or 'cls' argument shows up in
 | 
			
		||||
    # argnames but not in args or kwargs.  Uses 'in' rather than '==' because
 | 
			
		||||
    # some tests use 'self2'.
 | 
			
		||||
    if 'self' in argnames[0] or 'cls' == argnames[0]:
 | 
			
		||||
        # The function may not actually be a method or have __self__.
 | 
			
		||||
        # Typically seen when it's stubbed with mox.
 | 
			
		||||
        if inspect.ismethod(function) and hasattr(function, '__self__'):
 | 
			
		||||
            keyed_args[argnames[0]] = function.__self__
 | 
			
		||||
        else:
 | 
			
		||||
            keyed_args[argnames[0]] = None
 | 
			
		||||
 | 
			
		||||
    remaining_argnames = filter(lambda x: x not in keyed_args, argnames)
 | 
			
		||||
    keyed_args.update(dict(zip(remaining_argnames, args)))
 | 
			
		||||
 | 
			
		||||
    if defaults:
 | 
			
		||||
        num_defaults = len(defaults)
 | 
			
		||||
        for argname, value in zip(argnames[-num_defaults:], defaults):
 | 
			
		||||
            if argname not in keyed_args:
 | 
			
		||||
                keyed_args[argname] = value
 | 
			
		||||
 | 
			
		||||
    return keyed_args
 | 
			
		||||
							
								
								
									
										138
									
								
								iotronic/common/service.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								iotronic/common/service.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright © 2012 eNovance <licensing@enovance.com>
 | 
			
		||||
#
 | 
			
		||||
# 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 signal
 | 
			
		||||
import socket
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_context import context
 | 
			
		||||
from oslo_log import log
 | 
			
		||||
import oslo_messaging as messaging
 | 
			
		||||
from oslo_utils import importutils
 | 
			
		||||
 | 
			
		||||
from iotronic.common import config
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
from iotronic.common.i18n import _LI
 | 
			
		||||
from iotronic.common import rpc
 | 
			
		||||
from iotronic.objects import base as objects_base
 | 
			
		||||
from iotronic.openstack.common import service
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
service_opts = [
 | 
			
		||||
    cfg.IntOpt('periodic_interval',
 | 
			
		||||
               default=60,
 | 
			
		||||
               help='Seconds between running periodic tasks.'),
 | 
			
		||||
    cfg.StrOpt('host',
 | 
			
		||||
               default=socket.getfqdn(),
 | 
			
		||||
               help='Name of this node.  This can be an opaque identifier.  '
 | 
			
		||||
               'It is not necessarily a hostname, FQDN, or IP address. '
 | 
			
		||||
               'However, the node name must be valid within '
 | 
			
		||||
               'an AMQP key, and if using ZeroMQ, a valid '
 | 
			
		||||
               'hostname, FQDN, or IP address.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
cfg.CONF.register_opts(service_opts)
 | 
			
		||||
 | 
			
		||||
LOG = log.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RPCService(service.Service):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, host, manager_module, manager_class):
 | 
			
		||||
        super(RPCService, self).__init__()
 | 
			
		||||
        self.host = host
 | 
			
		||||
        manager_module = importutils.try_import(manager_module)
 | 
			
		||||
        manager_class = getattr(manager_module, manager_class)
 | 
			
		||||
        self.manager = manager_class(host, manager_module.MANAGER_TOPIC)
 | 
			
		||||
        self.topic = self.manager.topic
 | 
			
		||||
        self.rpcserver = None
 | 
			
		||||
        self.deregister = True
 | 
			
		||||
 | 
			
		||||
    def start(self):
 | 
			
		||||
        super(RPCService, self).start()
 | 
			
		||||
        admin_context = context.RequestContext('admin', 'admin', is_admin=True)
 | 
			
		||||
 | 
			
		||||
        target = messaging.Target(topic=self.topic, server=self.host)
 | 
			
		||||
        endpoints = [self.manager]
 | 
			
		||||
        serializer = objects_base.IotronicObjectSerializer()
 | 
			
		||||
        self.rpcserver = rpc.get_server(target, endpoints, serializer)
 | 
			
		||||
        self.rpcserver.start()
 | 
			
		||||
 | 
			
		||||
        self.handle_signal()
 | 
			
		||||
        self.manager.init_host()
 | 
			
		||||
        self.tg.add_dynamic_timer(
 | 
			
		||||
            self.manager.periodic_tasks,
 | 
			
		||||
            periodic_interval_max=cfg.CONF.periodic_interval,
 | 
			
		||||
            context=admin_context)
 | 
			
		||||
 | 
			
		||||
        LOG.info(_LI('Created RPC server for service %(service)s on host '
 | 
			
		||||
                     '%(host)s.'),
 | 
			
		||||
                 {'service': self.topic, 'host': self.host})
 | 
			
		||||
 | 
			
		||||
    def stop(self):
 | 
			
		||||
        try:
 | 
			
		||||
            self.rpcserver.stop()
 | 
			
		||||
            self.rpcserver.wait()
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            LOG.exception(_LE('Service error occurred when stopping the '
 | 
			
		||||
                              'RPC server. Error: %s'), e)
 | 
			
		||||
        try:
 | 
			
		||||
            self.manager.del_host(deregister=self.deregister)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            LOG.exception(_LE('Service error occurred when cleaning up '
 | 
			
		||||
                              'the RPC manager. Error: %s'), e)
 | 
			
		||||
 | 
			
		||||
        super(RPCService, self).stop(graceful=True)
 | 
			
		||||
        LOG.info(_LI('Stopped RPC server for service %(service)s on host '
 | 
			
		||||
                     '%(host)s.'),
 | 
			
		||||
                 {'service': self.topic, 'host': self.host})
 | 
			
		||||
 | 
			
		||||
    def _handle_signal(self, signo, frame):
 | 
			
		||||
        LOG.info(_LI('Got signal SIGUSR1. Not deregistering on next shutdown '
 | 
			
		||||
                     'of service %(service)s on host %(host)s.'),
 | 
			
		||||
                 {'service': self.topic, 'host': self.host})
 | 
			
		||||
        self.deregister = False
 | 
			
		||||
 | 
			
		||||
    def handle_signal(self):
 | 
			
		||||
        """Add a signal handler for SIGUSR1.
 | 
			
		||||
 | 
			
		||||
        The handler ensures that the manager is not deregistered when it is
 | 
			
		||||
        shutdown.
 | 
			
		||||
        """
 | 
			
		||||
        signal.signal(signal.SIGUSR1, self._handle_signal)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def prepare_service(argv=[]):
 | 
			
		||||
    log.register_options(cfg.CONF)
 | 
			
		||||
    
 | 
			
		||||
    log.set_defaults(default_log_levels=['amqp=WARN',
 | 
			
		||||
                                         'amqplib=WARN',
 | 
			
		||||
                                         'qpid.messagregister_optionsing=INFO',
 | 
			
		||||
                                         'oslo.messaging=INFO',
 | 
			
		||||
                                         'sqlalchemy=WARN',
 | 
			
		||||
                                         'keystoneclient=INFO',
 | 
			
		||||
                                         'stevedore=INFO',
 | 
			
		||||
                                         'eventlet.wsgi.server=WARN',
 | 
			
		||||
                                         'iso8601=WARN',
 | 
			
		||||
                                         'paramiko=WARN',
 | 
			
		||||
                                         'requests=WARN',
 | 
			
		||||
                                         'neutronclient=WARN',
 | 
			
		||||
                                         'glanceclient=WARN',
 | 
			
		||||
                                         'iotronic.openstack.common=WARN',
 | 
			
		||||
                                         'urllib3.connectionpool=WARN',
 | 
			
		||||
                                         ])
 | 
			
		||||
    config.parse_args(argv)
 | 
			
		||||
    log.setup(cfg.CONF, 'iotronic')
 | 
			
		||||
							
								
								
									
										298
									
								
								iotronic/common/states.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								iotronic/common/states.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,298 @@
 | 
			
		||||
# Copyright (c) 2012 NTT DOCOMO, INC.
 | 
			
		||||
# Copyright 2010 OpenStack Foundation
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Mapping of bare metal node states.
 | 
			
		||||
 | 
			
		||||
Setting the node `power_state` is handled by the conductor's power
 | 
			
		||||
synchronization thread. Based on the power state retrieved from the driver
 | 
			
		||||
for the node, the state is set to POWER_ON or POWER_OFF, accordingly.
 | 
			
		||||
Should this fail, the `power_state` value is left unchanged, and the node
 | 
			
		||||
is placed into maintenance mode.
 | 
			
		||||
 | 
			
		||||
The `power_state` can also be set manually via the API. A failure to change
 | 
			
		||||
the state leaves the current state unchanged. The node is NOT placed into
 | 
			
		||||
maintenance mode in this case.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
 | 
			
		||||
from iotronic.common import fsm
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
#####################
 | 
			
		||||
# Provisioning states
 | 
			
		||||
#####################
 | 
			
		||||
 | 
			
		||||
# TODO(deva): add add'l state mappings here
 | 
			
		||||
VERBS = {
 | 
			
		||||
    'active': 'deploy',
 | 
			
		||||
    'deleted': 'delete',
 | 
			
		||||
    'manage': 'manage',
 | 
			
		||||
    'provide': 'provide',
 | 
			
		||||
    'inspect': 'inspect',
 | 
			
		||||
}
 | 
			
		||||
""" Mapping of state-changing events that are PUT to the REST API
 | 
			
		||||
 | 
			
		||||
This is a mapping of target states which are PUT to the API, eg,
 | 
			
		||||
    PUT /v1/node/states/provision {'target': 'active'}
 | 
			
		||||
 | 
			
		||||
The dict format is:
 | 
			
		||||
    {target string used by the API: internal verb}
 | 
			
		||||
 | 
			
		||||
This provides a reference set of supported actions, and in the future
 | 
			
		||||
may be used to support renaming these actions.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
NOSTATE = None
 | 
			
		||||
""" No state information.
 | 
			
		||||
 | 
			
		||||
This state is used with power_state to represent a lack of knowledge of
 | 
			
		||||
power state, and in target_*_state fields when there is no target.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
MANAGEABLE = 'manageable'
 | 
			
		||||
""" Node is in a manageable state.
 | 
			
		||||
 | 
			
		||||
This state indicates that Iotronic has verified, at least once, that it had
 | 
			
		||||
sufficient information to manage the hardware. While in this state, the node
 | 
			
		||||
is not available for provisioning (it must be in the AVAILABLE state for that).
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
AVAILABLE = 'available'
 | 
			
		||||
""" Node is available for use and scheduling.
 | 
			
		||||
 | 
			
		||||
This state is replacing the NOSTATE state used prior to Kilo.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
ACTIVE = 'active'
 | 
			
		||||
""" Node is successfully deployed and associated with an instance. """
 | 
			
		||||
 | 
			
		||||
DEPLOYWAIT = 'wait call-back'
 | 
			
		||||
""" Node is waiting to be deployed.
 | 
			
		||||
 | 
			
		||||
This will be the node `provision_state` while the node is waiting for
 | 
			
		||||
the driver to finish deployment.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
DEPLOYING = 'deploying'
 | 
			
		||||
""" Node is ready to receive a deploy request, or is currently being deployed.
 | 
			
		||||
 | 
			
		||||
A node will have its `provision_state` set to DEPLOYING briefly before it
 | 
			
		||||
receives its initial deploy request. It will also move to this state from
 | 
			
		||||
DEPLOYWAIT after the callback is triggered and deployment is continued
 | 
			
		||||
(disk partitioning and image copying).
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
DEPLOYFAIL = 'deploy failed'
 | 
			
		||||
""" Node deployment failed. """
 | 
			
		||||
 | 
			
		||||
DEPLOYDONE = 'deploy complete'
 | 
			
		||||
""" Node was successfully deployed.
 | 
			
		||||
 | 
			
		||||
This is mainly a target provision state used during deployment. A successfully
 | 
			
		||||
deployed node should go to ACTIVE status.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
DELETING = 'deleting'
 | 
			
		||||
""" Node is actively being torn down. """
 | 
			
		||||
 | 
			
		||||
DELETED = 'deleted'
 | 
			
		||||
""" Node tear down was successful.
 | 
			
		||||
 | 
			
		||||
In Juno, target_provision_state was set to this value during node tear down.
 | 
			
		||||
 | 
			
		||||
In Kilo, this will be a transitory value of provision_state, and never
 | 
			
		||||
represented in target_provision_state.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
CLEANING = 'cleaning'
 | 
			
		||||
""" Node is being automatically cleaned to prepare it for provisioning. """
 | 
			
		||||
 | 
			
		||||
CLEANFAIL = 'clean failed'
 | 
			
		||||
""" Node failed cleaning. This requires operator intervention to resolve. """
 | 
			
		||||
 | 
			
		||||
ERROR = 'error'
 | 
			
		||||
""" An error occurred during node processing.
 | 
			
		||||
 | 
			
		||||
The `last_error` attribute of the node details should contain an error message.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
REBUILD = 'rebuild'
 | 
			
		||||
""" Node is to be rebuilt.
 | 
			
		||||
 | 
			
		||||
This is not used as a state, but rather as a "verb" when changing the node's
 | 
			
		||||
provision_state via the REST API.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
INSPECTING = 'inspecting'
 | 
			
		||||
""" Node is under inspection.
 | 
			
		||||
 | 
			
		||||
This is the provision state used when inspection is started. A successfully
 | 
			
		||||
inspected node shall transition to MANAGEABLE status.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
INSPECTFAIL = 'inspect failed'
 | 
			
		||||
""" Node inspection failed. """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
UPDATE_ALLOWED_STATES = (DEPLOYFAIL, INSPECTING, INSPECTFAIL, CLEANFAIL)
 | 
			
		||||
"""Transitional states in which we allow updating a node."""
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
##############
 | 
			
		||||
# Power states
 | 
			
		||||
##############
 | 
			
		||||
 | 
			
		||||
POWER_ON = 'power on'
 | 
			
		||||
""" Node is powered on. """
 | 
			
		||||
 | 
			
		||||
POWER_OFF = 'power off'
 | 
			
		||||
""" Node is powered off. """
 | 
			
		||||
 | 
			
		||||
REBOOT = 'rebooting'
 | 
			
		||||
""" Node is rebooting. """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#####################
 | 
			
		||||
# State machine model
 | 
			
		||||
#####################
 | 
			
		||||
def on_exit(old_state, event):
 | 
			
		||||
    """Used to log when a state is exited."""
 | 
			
		||||
    LOG.debug("Exiting old state '%s' in response to event '%s'",
 | 
			
		||||
              old_state, event)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_enter(new_state, event):
 | 
			
		||||
    """Used to log when entering a state."""
 | 
			
		||||
    LOG.debug("Entering new state '%s' in response to event '%s'",
 | 
			
		||||
              new_state, event)
 | 
			
		||||
 | 
			
		||||
watchers = {}
 | 
			
		||||
watchers['on_exit'] = on_exit
 | 
			
		||||
watchers['on_enter'] = on_enter
 | 
			
		||||
 | 
			
		||||
machine = fsm.FSM()
 | 
			
		||||
 | 
			
		||||
# Add stable states
 | 
			
		||||
machine.add_state(MANAGEABLE, stable=True, **watchers)
 | 
			
		||||
machine.add_state(AVAILABLE, stable=True, **watchers)
 | 
			
		||||
machine.add_state(ACTIVE, stable=True, **watchers)
 | 
			
		||||
machine.add_state(ERROR, stable=True, **watchers)
 | 
			
		||||
 | 
			
		||||
# Add deploy* states
 | 
			
		||||
# NOTE(deva): Juno shows a target_provision_state of DEPLOYDONE
 | 
			
		||||
#             this is changed in Kilo to ACTIVE
 | 
			
		||||
machine.add_state(DEPLOYING, target=ACTIVE, **watchers)
 | 
			
		||||
machine.add_state(DEPLOYWAIT, target=ACTIVE, **watchers)
 | 
			
		||||
machine.add_state(DEPLOYFAIL, target=ACTIVE, **watchers)
 | 
			
		||||
 | 
			
		||||
# Add clean* states
 | 
			
		||||
machine.add_state(CLEANING, target=AVAILABLE, **watchers)
 | 
			
		||||
machine.add_state(CLEANFAIL, target=AVAILABLE, **watchers)
 | 
			
		||||
 | 
			
		||||
# Add delete* states
 | 
			
		||||
machine.add_state(DELETING, target=AVAILABLE, **watchers)
 | 
			
		||||
 | 
			
		||||
# From AVAILABLE, a deployment may be started
 | 
			
		||||
machine.add_transition(AVAILABLE, DEPLOYING, 'deploy')
 | 
			
		||||
 | 
			
		||||
# Add inspect* states.
 | 
			
		||||
machine.add_state(INSPECTING, target=MANAGEABLE, **watchers)
 | 
			
		||||
machine.add_state(INSPECTFAIL, target=MANAGEABLE, **watchers)
 | 
			
		||||
 | 
			
		||||
# A deployment may fail
 | 
			
		||||
machine.add_transition(DEPLOYING, DEPLOYFAIL, 'fail')
 | 
			
		||||
 | 
			
		||||
# A failed deployment may be retried
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_deploy()
 | 
			
		||||
machine.add_transition(DEPLOYFAIL, DEPLOYING, 'rebuild')
 | 
			
		||||
# NOTE(deva): Juno allows a client to send "active" to initiate a rebuild
 | 
			
		||||
machine.add_transition(DEPLOYFAIL, DEPLOYING, 'deploy')
 | 
			
		||||
 | 
			
		||||
# A deployment may also wait on external callbacks
 | 
			
		||||
machine.add_transition(DEPLOYING, DEPLOYWAIT, 'wait')
 | 
			
		||||
machine.add_transition(DEPLOYWAIT, DEPLOYING, 'resume')
 | 
			
		||||
 | 
			
		||||
# A deployment waiting on callback may time out
 | 
			
		||||
machine.add_transition(DEPLOYWAIT, DEPLOYFAIL, 'fail')
 | 
			
		||||
 | 
			
		||||
# A deployment may complete
 | 
			
		||||
machine.add_transition(DEPLOYING, ACTIVE, 'done')
 | 
			
		||||
 | 
			
		||||
# An active instance may be re-deployed
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_deploy()
 | 
			
		||||
machine.add_transition(ACTIVE, DEPLOYING, 'rebuild')
 | 
			
		||||
 | 
			
		||||
# An active instance may be deleted
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_tear_down()
 | 
			
		||||
machine.add_transition(ACTIVE, DELETING, 'delete')
 | 
			
		||||
 | 
			
		||||
# While a deployment is waiting, it may be deleted
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_tear_down()
 | 
			
		||||
machine.add_transition(DEPLOYWAIT, DELETING, 'delete')
 | 
			
		||||
 | 
			
		||||
# A failed deployment may also be deleted
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_tear_down()
 | 
			
		||||
machine.add_transition(DEPLOYFAIL, DELETING, 'delete')
 | 
			
		||||
 | 
			
		||||
# This state can also transition to error
 | 
			
		||||
machine.add_transition(DELETING, ERROR, 'error')
 | 
			
		||||
 | 
			
		||||
# When finished deleting, a node will begin cleaning
 | 
			
		||||
machine.add_transition(DELETING, CLEANING, 'clean')
 | 
			
		||||
 | 
			
		||||
# If cleaning succeeds, it becomes available for scheduling
 | 
			
		||||
machine.add_transition(CLEANING, AVAILABLE, 'done')
 | 
			
		||||
 | 
			
		||||
# If cleaning fails, wait for operator intervention
 | 
			
		||||
machine.add_transition(CLEANING, CLEANFAIL, 'fail')
 | 
			
		||||
 | 
			
		||||
# An operator may want to move a CLEANFAIL node to MANAGEABLE, to perform
 | 
			
		||||
# other actions like zapping
 | 
			
		||||
machine.add_transition(CLEANFAIL, MANAGEABLE, 'manage')
 | 
			
		||||
 | 
			
		||||
# From MANAGEABLE, a node may move to available after going through cleaning
 | 
			
		||||
machine.add_transition(MANAGEABLE, CLEANING, 'provide')
 | 
			
		||||
 | 
			
		||||
# From AVAILABLE, a node may be made unavailable by managing it
 | 
			
		||||
machine.add_transition(AVAILABLE, MANAGEABLE, 'manage')
 | 
			
		||||
 | 
			
		||||
# An errored instance can be rebuilt
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_deploy()
 | 
			
		||||
machine.add_transition(ERROR, DEPLOYING, 'rebuild')
 | 
			
		||||
# or deleted
 | 
			
		||||
# iotronic/conductor/manager.py:do_node_tear_down()
 | 
			
		||||
machine.add_transition(ERROR, DELETING, 'delete')
 | 
			
		||||
 | 
			
		||||
# Added transitions for inspection.
 | 
			
		||||
# Initiate inspection.
 | 
			
		||||
machine.add_transition(MANAGEABLE, INSPECTING, 'inspect')
 | 
			
		||||
 | 
			
		||||
# iotronic/conductor/manager.py:inspect_hardware().
 | 
			
		||||
machine.add_transition(INSPECTING, MANAGEABLE, 'done')
 | 
			
		||||
 | 
			
		||||
# Inspection may fail.
 | 
			
		||||
machine.add_transition(INSPECTING, INSPECTFAIL, 'fail')
 | 
			
		||||
 | 
			
		||||
# Move the node to manageable state for any other
 | 
			
		||||
# action.
 | 
			
		||||
machine.add_transition(INSPECTFAIL, MANAGEABLE, 'manage')
 | 
			
		||||
 | 
			
		||||
# Reinitiate the inspect after inspectfail.
 | 
			
		||||
machine.add_transition(INSPECTFAIL, INSPECTING, 'inspect')
 | 
			
		||||
							
								
								
									
										191
									
								
								iotronic/common/swift.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								iotronic/common/swift.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,191 @@
 | 
			
		||||
#
 | 
			
		||||
# Copyright 2014 OpenStack Foundation
 | 
			
		||||
# All Rights Reserved
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
from six.moves.urllib import parse
 | 
			
		||||
from swiftclient import client as swift_client
 | 
			
		||||
from swiftclient import exceptions as swift_exceptions
 | 
			
		||||
from swiftclient import utils as swift_utils
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common import keystone
 | 
			
		||||
 | 
			
		||||
swift_opts = [
 | 
			
		||||
    cfg.IntOpt('swift_max_retries',
 | 
			
		||||
               default=2,
 | 
			
		||||
               help='Maximum number of times to retry a Swift request, '
 | 
			
		||||
                    'before failing.')
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(swift_opts, group='swift')
 | 
			
		||||
 | 
			
		||||
CONF.import_opt('admin_user', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('admin_tenant_name', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('admin_password', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('auth_uri', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('auth_version', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('insecure', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
CONF.import_opt('cafile', 'keystonemiddleware.auth_token',
 | 
			
		||||
                group='keystone_authtoken')
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SwiftAPI(object):
 | 
			
		||||
    """API for communicating with Swift."""
 | 
			
		||||
 | 
			
		||||
    def __init__(self,
 | 
			
		||||
                 user=CONF.keystone_authtoken.admin_user,
 | 
			
		||||
                 tenant_name=CONF.keystone_authtoken.admin_tenant_name,
 | 
			
		||||
                 key=CONF.keystone_authtoken.admin_password,
 | 
			
		||||
                 auth_url=CONF.keystone_authtoken.auth_uri,
 | 
			
		||||
                 auth_version=CONF.keystone_authtoken.auth_version):
 | 
			
		||||
        """Constructor for creating a SwiftAPI object.
 | 
			
		||||
 | 
			
		||||
        :param user: the name of the user for Swift account
 | 
			
		||||
        :param tenant_name: the name of the tenant for Swift account
 | 
			
		||||
        :param key: the 'password' or key to authenticate with
 | 
			
		||||
        :param auth_url: the url for authentication
 | 
			
		||||
        :param auth_version: the version of api to use for authentication
 | 
			
		||||
        """
 | 
			
		||||
        auth_url = keystone.get_keystone_url(auth_url, auth_version)
 | 
			
		||||
        params = {'retries': CONF.swift.swift_max_retries,
 | 
			
		||||
                  'insecure': CONF.keystone_authtoken.insecure,
 | 
			
		||||
                  'cacert': CONF.keystone_authtoken.cafile,
 | 
			
		||||
                  'user': user,
 | 
			
		||||
                  'tenant_name': tenant_name,
 | 
			
		||||
                  'key': key,
 | 
			
		||||
                  'authurl': auth_url,
 | 
			
		||||
                  'auth_version': auth_version}
 | 
			
		||||
 | 
			
		||||
        self.connection = swift_client.Connection(**params)
 | 
			
		||||
 | 
			
		||||
    def create_object(self, container, object, filename,
 | 
			
		||||
                      object_headers=None):
 | 
			
		||||
        """Uploads a given file to Swift.
 | 
			
		||||
 | 
			
		||||
        :param container: The name of the container for the object.
 | 
			
		||||
        :param object: The name of the object in Swift
 | 
			
		||||
        :param filename: The file to upload, as the object data
 | 
			
		||||
        :param object_headers: the headers for the object to pass to Swift
 | 
			
		||||
        :returns: The Swift UUID of the object
 | 
			
		||||
        :raises: SwiftOperationError, if any operation with Swift fails.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            self.connection.put_container(container)
 | 
			
		||||
        except swift_exceptions.ClientException as e:
 | 
			
		||||
            operation = _("put container")
 | 
			
		||||
            raise exception.SwiftOperationError(operation=operation, error=e)
 | 
			
		||||
 | 
			
		||||
        with open(filename, "r") as fileobj:
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                obj_uuid = self.connection.put_object(container,
 | 
			
		||||
                                                      object,
 | 
			
		||||
                                                      fileobj,
 | 
			
		||||
                                                      headers=object_headers)
 | 
			
		||||
            except swift_exceptions.ClientException as e:
 | 
			
		||||
                operation = _("put object")
 | 
			
		||||
                raise exception.SwiftOperationError(operation=operation,
 | 
			
		||||
                                                    error=e)
 | 
			
		||||
 | 
			
		||||
        return obj_uuid
 | 
			
		||||
 | 
			
		||||
    def get_temp_url(self, container, object, timeout):
 | 
			
		||||
        """Returns the temp url for the given Swift object.
 | 
			
		||||
 | 
			
		||||
        :param container: The name of the container in which Swift object
 | 
			
		||||
            is placed.
 | 
			
		||||
        :param object: The name of the Swift object.
 | 
			
		||||
        :param timeout: The timeout in seconds after which the generated url
 | 
			
		||||
            should expire.
 | 
			
		||||
        :returns: The temp url for the object.
 | 
			
		||||
        :raises: SwiftOperationError, if any operation with Swift fails.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            account_info = self.connection.head_account()
 | 
			
		||||
        except swift_exceptions.ClientException as e:
 | 
			
		||||
            operation = _("head account")
 | 
			
		||||
            raise exception.SwiftOperationError(operation=operation,
 | 
			
		||||
                                                error=e)
 | 
			
		||||
 | 
			
		||||
        storage_url, token = self.connection.get_auth()
 | 
			
		||||
        parse_result = parse.urlparse(storage_url)
 | 
			
		||||
        swift_object_path = '/'.join((parse_result.path, container, object))
 | 
			
		||||
        temp_url_key = account_info['x-account-meta-temp-url-key']
 | 
			
		||||
        url_path = swift_utils.generate_temp_url(swift_object_path, timeout,
 | 
			
		||||
                                                 temp_url_key, 'GET')
 | 
			
		||||
        return parse.urlunparse((parse_result.scheme,
 | 
			
		||||
                                 parse_result.netloc,
 | 
			
		||||
                                 url_path,
 | 
			
		||||
                                 None,
 | 
			
		||||
                                 None,
 | 
			
		||||
                                 None))
 | 
			
		||||
 | 
			
		||||
    def delete_object(self, container, object):
 | 
			
		||||
        """Deletes the given Swift object.
 | 
			
		||||
 | 
			
		||||
        :param container: The name of the container in which Swift object
 | 
			
		||||
            is placed.
 | 
			
		||||
        :param object: The name of the object in Swift to be deleted.
 | 
			
		||||
        :raises: SwiftOperationError, if operation with Swift fails.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            self.connection.delete_object(container, object)
 | 
			
		||||
        except swift_exceptions.ClientException as e:
 | 
			
		||||
            operation = _("delete object")
 | 
			
		||||
            raise exception.SwiftOperationError(operation=operation, error=e)
 | 
			
		||||
 | 
			
		||||
    def head_object(self, container, object):
 | 
			
		||||
        """Retrieves the information about the given Swift object.
 | 
			
		||||
 | 
			
		||||
        :param container: The name of the container in which Swift object
 | 
			
		||||
            is placed.
 | 
			
		||||
        :param object: The name of the object in Swift
 | 
			
		||||
        :returns: The information about the object as returned by
 | 
			
		||||
            Swift client's head_object call.
 | 
			
		||||
        :raises: SwiftOperationError, if operation with Swift fails.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            return self.connection.head_object(container, object)
 | 
			
		||||
        except swift_exceptions.ClientException as e:
 | 
			
		||||
            operation = _("head object")
 | 
			
		||||
            raise exception.SwiftOperationError(operation=operation, error=e)
 | 
			
		||||
 | 
			
		||||
    def update_object_meta(self, container, object, object_headers):
 | 
			
		||||
        """Update the metadata of a given Swift object.
 | 
			
		||||
 | 
			
		||||
        :param container: The name of the container in which Swift object
 | 
			
		||||
            is placed.
 | 
			
		||||
        :param object: The name of the object in Swift
 | 
			
		||||
        :param object_headers: the headers for the object to pass to Swift
 | 
			
		||||
        :raises: SwiftOperationError, if operation with Swift fails.
 | 
			
		||||
        """
 | 
			
		||||
        try:
 | 
			
		||||
            self.connection.post_object(container, object, object_headers)
 | 
			
		||||
        except swift_exceptions.ClientException as e:
 | 
			
		||||
            operation = _("post object")
 | 
			
		||||
            raise exception.SwiftOperationError(operation=operation, error=e)
 | 
			
		||||
							
								
								
									
										599
									
								
								iotronic/common/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										599
									
								
								iotronic/common/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,599 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# Copyright 2011 Justin Santa Barbara
 | 
			
		||||
# Copyright (c) 2012 NTT DOCOMO, 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.
 | 
			
		||||
 | 
			
		||||
"""Utilities and helper functions."""
 | 
			
		||||
 | 
			
		||||
import contextlib
 | 
			
		||||
import errno
 | 
			
		||||
import hashlib
 | 
			
		||||
import os
 | 
			
		||||
import random
 | 
			
		||||
import re
 | 
			
		||||
import shutil
 | 
			
		||||
import tempfile
 | 
			
		||||
 | 
			
		||||
import netaddr
 | 
			
		||||
from oslo_concurrency import processutils
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_log import log as logging
 | 
			
		||||
from oslo_utils import excutils
 | 
			
		||||
import paramiko
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
from iotronic.common import exception
 | 
			
		||||
from iotronic.common.i18n import _
 | 
			
		||||
from iotronic.common.i18n import _LE
 | 
			
		||||
from iotronic.common.i18n import _LW
 | 
			
		||||
 | 
			
		||||
utils_opts = [
 | 
			
		||||
    cfg.StrOpt('rootwrap_config',
 | 
			
		||||
               default="/etc/iotronic/rootwrap.conf",
 | 
			
		||||
               help='Path to the rootwrap configuration file to use for '
 | 
			
		||||
                    'running commands as root.'),
 | 
			
		||||
    cfg.StrOpt('tempdir',
 | 
			
		||||
               help='Explicitly specify the temporary working directory.'),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
CONF = cfg.CONF
 | 
			
		||||
CONF.register_opts(utils_opts)
 | 
			
		||||
 | 
			
		||||
LOG = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _get_root_helper():
 | 
			
		||||
    return 'sudo iotronic-rootwrap %s' % CONF.rootwrap_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def execute(*cmd, **kwargs):
 | 
			
		||||
    """Convenience wrapper around oslo's execute() method.
 | 
			
		||||
 | 
			
		||||
    :param cmd: Passed to processutils.execute.
 | 
			
		||||
    :param use_standard_locale: True | False. Defaults to False. If set to
 | 
			
		||||
                                True, execute command with standard locale
 | 
			
		||||
                                added to environment variables.
 | 
			
		||||
    :returns: (stdout, stderr) from process execution
 | 
			
		||||
    :raises: UnknownArgumentError
 | 
			
		||||
    :raises: ProcessExecutionError
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    use_standard_locale = kwargs.pop('use_standard_locale', False)
 | 
			
		||||
    if use_standard_locale:
 | 
			
		||||
        env = kwargs.pop('env_variables', os.environ.copy())
 | 
			
		||||
        env['LC_ALL'] = 'C'
 | 
			
		||||
        kwargs['env_variables'] = env
 | 
			
		||||
    if kwargs.get('run_as_root') and 'root_helper' not in kwargs:
 | 
			
		||||
        kwargs['root_helper'] = _get_root_helper()
 | 
			
		||||
    result = processutils.execute(*cmd, **kwargs)
 | 
			
		||||
    LOG.debug('Execution completed, command line is "%s"',
 | 
			
		||||
              ' '.join(map(str, cmd)))
 | 
			
		||||
    LOG.debug('Command stdout is: "%s"' % result[0])
 | 
			
		||||
    LOG.debug('Command stderr is: "%s"' % result[1])
 | 
			
		||||
    return result
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def trycmd(*args, **kwargs):
 | 
			
		||||
    """Convenience wrapper around oslo's trycmd() method."""
 | 
			
		||||
    if kwargs.get('run_as_root') and 'root_helper' not in kwargs:
 | 
			
		||||
        kwargs['root_helper'] = _get_root_helper()
 | 
			
		||||
    return processutils.trycmd(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def ssh_connect(connection):
 | 
			
		||||
    """Method to connect to a remote system using ssh protocol.
 | 
			
		||||
 | 
			
		||||
    :param connection: a dict of connection parameters.
 | 
			
		||||
    :returns: paramiko.SSHClient -- an active ssh connection.
 | 
			
		||||
    :raises: SSHConnectFailed
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    try:
 | 
			
		||||
        ssh = paramiko.SSHClient()
 | 
			
		||||
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
 | 
			
		||||
        key_contents = connection.get('key_contents')
 | 
			
		||||
        if key_contents:
 | 
			
		||||
            data = six.moves.StringIO(key_contents)
 | 
			
		||||
            if "BEGIN RSA PRIVATE" in key_contents:
 | 
			
		||||
                pkey = paramiko.RSAKey.from_private_key(data)
 | 
			
		||||
            elif "BEGIN DSA PRIVATE" in key_contents:
 | 
			
		||||
                pkey = paramiko.DSSKey.from_private_key(data)
 | 
			
		||||
            else:
 | 
			
		||||
                # Can't include the key contents - secure material.
 | 
			
		||||
                raise ValueError(_("Invalid private key"))
 | 
			
		||||
        else:
 | 
			
		||||
            pkey = None
 | 
			
		||||
        ssh.connect(connection.get('host'),
 | 
			
		||||
                    username=connection.get('username'),
 | 
			
		||||
                    password=connection.get('password'),
 | 
			
		||||
                    port=connection.get('port', 22),
 | 
			
		||||
                    pkey=pkey,
 | 
			
		||||
                    key_filename=connection.get('key_filename'),
 | 
			
		||||
                    timeout=connection.get('timeout', 10))
 | 
			
		||||
 | 
			
		||||
        # send TCP keepalive packets every 20 seconds
 | 
			
		||||
        ssh.get_transport().set_keepalive(20)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        LOG.debug("SSH connect failed: %s" % e)
 | 
			
		||||
        raise exception.SSHConnectFailed(host=connection.get('host'))
 | 
			
		||||
 | 
			
		||||
    return ssh
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_uid(topic, size=8):
 | 
			
		||||
    characters = '01234567890abcdefghijklmnopqrstuvwxyz'
 | 
			
		||||
    choices = [random.choice(characters) for _x in range(size)]
 | 
			
		||||
    return '%s-%s' % (topic, ''.join(choices))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def random_alnum(size=32):
 | 
			
		||||
    characters = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
 | 
			
		||||
    return ''.join(random.choice(characters) for _ in range(size))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def delete_if_exists(pathname):
 | 
			
		||||
    """delete a file, but ignore file not found error."""
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        os.unlink(pathname)
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        if e.errno == errno.ENOENT:
 | 
			
		||||
            return
 | 
			
		||||
        else:
 | 
			
		||||
            raise
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_boolstr(val):
 | 
			
		||||
    """Check if the provided string is a valid bool string or not."""
 | 
			
		||||
    boolstrs = ('true', 'false', 'yes', 'no', 'y', 'n', '1', '0')
 | 
			
		||||
    return str(val).lower() in boolstrs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_mac(address):
 | 
			
		||||
    """Verify the format of a MAC address.
 | 
			
		||||
 | 
			
		||||
    Check if a MAC address is valid and contains six octets. Accepts
 | 
			
		||||
    colon-separated format only.
 | 
			
		||||
 | 
			
		||||
    :param address: MAC address to be validated.
 | 
			
		||||
    :returns: True if valid. False if not.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    m = "[0-9a-f]{2}(:[0-9a-f]{2}){5}$"
 | 
			
		||||
    return (isinstance(address, six.string_types) and
 | 
			
		||||
            re.match(m, address.lower()))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_hostname_safe(hostname):
 | 
			
		||||
    """Determine if the supplied hostname is RFC compliant.
 | 
			
		||||
 | 
			
		||||
    Check that the supplied hostname conforms to:
 | 
			
		||||
        * http://en.wikipedia.org/wiki/Hostname
 | 
			
		||||
        * http://tools.ietf.org/html/rfc952
 | 
			
		||||
        * http://tools.ietf.org/html/rfc1123
 | 
			
		||||
    Allowing for hostnames, and hostnames + domains.
 | 
			
		||||
 | 
			
		||||
    :param hostname: The hostname to be validated.
 | 
			
		||||
    :returns: True if valid. False if not.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(hostname, six.string_types) or len(hostname) > 255:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    # Periods on the end of a hostname are ok, but complicates the
 | 
			
		||||
    # regex so we'll do this manually
 | 
			
		||||
    if hostname.endswith('.'):
 | 
			
		||||
        hostname = hostname[:-1]
 | 
			
		||||
 | 
			
		||||
    host = '[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?'
 | 
			
		||||
    domain = '[a-z0-9\-_]{0,62}[a-z0-9]'
 | 
			
		||||
 | 
			
		||||
    m = '^' + host + '(\.' + domain + ')*$'
 | 
			
		||||
 | 
			
		||||
    return re.match(m, hostname) is not None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_and_normalize_mac(address):
 | 
			
		||||
    """Validate a MAC address and return normalized form.
 | 
			
		||||
 | 
			
		||||
    Checks whether the supplied MAC address is formally correct and
 | 
			
		||||
    normalize it to all lower case.
 | 
			
		||||
 | 
			
		||||
    :param address: MAC address to be validated and normalized.
 | 
			
		||||
    :returns: Normalized and validated MAC address.
 | 
			
		||||
    :raises: InvalidMAC If the MAC address is not valid.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if not is_valid_mac(address):
 | 
			
		||||
        raise exception.InvalidMAC(mac=address)
 | 
			
		||||
    return address.lower()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_ipv6_cidr(address):
 | 
			
		||||
    try:
 | 
			
		||||
        str(netaddr.IPNetwork(address, version=6).cidr)
 | 
			
		||||
        return True
 | 
			
		||||
    except Exception:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_shortened_ipv6(address):
 | 
			
		||||
    addr = netaddr.IPAddress(address, version=6)
 | 
			
		||||
    return str(addr.ipv6())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_shortened_ipv6_cidr(address):
 | 
			
		||||
    net = netaddr.IPNetwork(address, version=6)
 | 
			
		||||
    return str(net.cidr)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_valid_cidr(address):
 | 
			
		||||
    """Check if the provided ipv4 or ipv6 address is a valid CIDR address."""
 | 
			
		||||
    try:
 | 
			
		||||
        # Validate the correct CIDR Address
 | 
			
		||||
        netaddr.IPNetwork(address)
 | 
			
		||||
    except netaddr.core.AddrFormatError:
 | 
			
		||||
        return False
 | 
			
		||||
    except UnboundLocalError:
 | 
			
		||||
        # NOTE(MotoKen): work around bug in netaddr 0.7.5 (see detail in
 | 
			
		||||
        # https://github.com/drkjam/netaddr/issues/2)
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    # Prior validation partially verify /xx part
 | 
			
		||||
    # Verify it here
 | 
			
		||||
    ip_segment = address.split('/')
 | 
			
		||||
 | 
			
		||||
    if (len(ip_segment) <= 1 or
 | 
			
		||||
        ip_segment[1] == ''):
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    return True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_ip_version(network):
 | 
			
		||||
    """Returns the IP version of a network (IPv4 or IPv6).
 | 
			
		||||
 | 
			
		||||
    :raises: AddrFormatError if invalid network.
 | 
			
		||||
    """
 | 
			
		||||
    if netaddr.IPNetwork(network).version == 6:
 | 
			
		||||
        return "IPv6"
 | 
			
		||||
    elif netaddr.IPNetwork(network).version == 4:
 | 
			
		||||
        return "IPv4"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def convert_to_list_dict(lst, label):
 | 
			
		||||
    """Convert a value or list into a list of dicts."""
 | 
			
		||||
    if not lst:
 | 
			
		||||
        return None
 | 
			
		||||
    if not isinstance(lst, list):
 | 
			
		||||
        lst = [lst]
 | 
			
		||||
    return [{label: x} for x in lst]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def sanitize_hostname(hostname):
 | 
			
		||||
    """Return a hostname which conforms to RFC-952 and RFC-1123 specs."""
 | 
			
		||||
    if isinstance(hostname, six.text_type):
 | 
			
		||||
        hostname = hostname.encode('latin-1', 'ignore')
 | 
			
		||||
 | 
			
		||||
    hostname = re.sub(b'[ _]', b'-', hostname)
 | 
			
		||||
    hostname = re.sub(b'[^\w.-]+', b'', hostname)
 | 
			
		||||
    hostname = hostname.lower()
 | 
			
		||||
    hostname = hostname.strip(b'.-')
 | 
			
		||||
 | 
			
		||||
    return hostname
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def read_cached_file(filename, cache_info, reload_func=None):
 | 
			
		||||
    """Read from a file if it has been modified.
 | 
			
		||||
 | 
			
		||||
    :param cache_info: dictionary to hold opaque cache.
 | 
			
		||||
    :param reload_func: optional function to be called with data when
 | 
			
		||||
                        file is reloaded due to a modification.
 | 
			
		||||
 | 
			
		||||
    :returns: data from file
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    mtime = os.path.getmtime(filename)
 | 
			
		||||
    if not cache_info or mtime != cache_info.get('mtime'):
 | 
			
		||||
        LOG.debug("Reloading cached file %s" % filename)
 | 
			
		||||
        with open(filename) as fap:
 | 
			
		||||
            cache_info['data'] = fap.read()
 | 
			
		||||
        cache_info['mtime'] = mtime
 | 
			
		||||
        if reload_func:
 | 
			
		||||
            reload_func(cache_info['data'])
 | 
			
		||||
    return cache_info['data']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def file_open(*args, **kwargs):
 | 
			
		||||
    """Open file
 | 
			
		||||
 | 
			
		||||
    see built-in file() documentation for more details
 | 
			
		||||
 | 
			
		||||
    Note: The reason this is kept in a separate module is to easily
 | 
			
		||||
          be able to provide a stub module that doesn't alter system
 | 
			
		||||
          state at all (for unit tests)
 | 
			
		||||
    """
 | 
			
		||||
    return file(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def hash_file(file_like_object):
 | 
			
		||||
    """Generate a hash for the contents of a file."""
 | 
			
		||||
    checksum = hashlib.sha1()
 | 
			
		||||
    for chunk in iter(lambda: file_like_object.read(32768), b''):
 | 
			
		||||
        checksum.update(chunk)
 | 
			
		||||
    return checksum.hexdigest()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@contextlib.contextmanager
 | 
			
		||||
def temporary_mutation(obj, **kwargs):
 | 
			
		||||
    """Temporarily change object attribute.
 | 
			
		||||
 | 
			
		||||
    Temporarily set the attr on a particular object to a given value then
 | 
			
		||||
    revert when finished.
 | 
			
		||||
 | 
			
		||||
    One use of this is to temporarily set the read_deleted flag on a context
 | 
			
		||||
    object:
 | 
			
		||||
 | 
			
		||||
        with temporary_mutation(context, read_deleted="yes"):
 | 
			
		||||
            do_something_that_needed_deleted_objects()
 | 
			
		||||
    """
 | 
			
		||||
    def is_dict_like(thing):
 | 
			
		||||
        return hasattr(thing, 'has_key')
 | 
			
		||||
 | 
			
		||||
    def get(thing, attr, default):
 | 
			
		||||
        if is_dict_like(thing):
 | 
			
		||||
            return thing.get(attr, default)
 | 
			
		||||
        else:
 | 
			
		||||
            return getattr(thing, attr, default)
 | 
			
		||||
 | 
			
		||||
    def set_value(thing, attr, val):
 | 
			
		||||
        if is_dict_like(thing):
 | 
			
		||||
            thing[attr] = val
 | 
			
		||||
        else:
 | 
			
		||||
            setattr(thing, attr, val)
 | 
			
		||||
 | 
			
		||||
    def delete(thing, attr):
 | 
			
		||||
        if is_dict_like(thing):
 | 
			
		||||
            del thing[attr]
 | 
			
		||||
        else:
 | 
			
		||||
            delattr(thing, attr)
 | 
			
		||||
 | 
			
		||||
    NOT_PRESENT = object()
 | 
			
		||||
 | 
			
		||||
    old_values = {}
 | 
			
		||||
    for attr, new_value in kwargs.items():
 | 
			
		||||
        old_values[attr] = get(obj, attr, NOT_PRESENT)
 | 
			
		||||
        set_value(obj, attr, new_value)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        yield
 | 
			
		||||
    finally:
 | 
			
		||||
        for attr, old_value in old_values.items():
 | 
			
		||||
            if old_value is NOT_PRESENT:
 | 
			
		||||
                delete(obj, attr)
 | 
			
		||||
            else:
 | 
			
		||||
                set_value(obj, attr, old_value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@contextlib.contextmanager
 | 
			
		||||
def tempdir(**kwargs):
 | 
			
		||||
    tempfile.tempdir = CONF.tempdir
 | 
			
		||||
    tmpdir = tempfile.mkdtemp(**kwargs)
 | 
			
		||||
    try:
 | 
			
		||||
        yield tmpdir
 | 
			
		||||
    finally:
 | 
			
		||||
        try:
 | 
			
		||||
            shutil.rmtree(tmpdir)
 | 
			
		||||
        except OSError as e:
 | 
			
		||||
            LOG.error(_LE('Could not remove tmpdir: %s'), e)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mkfs(fs, path, label=None):
 | 
			
		||||
    """Format a file or block device
 | 
			
		||||
 | 
			
		||||
    :param fs: Filesystem type (examples include 'swap', 'ext3', 'ext4'
 | 
			
		||||
               'btrfs', etc.)
 | 
			
		||||
    :param path: Path to file or block device to format
 | 
			
		||||
    :param label: Volume label to use
 | 
			
		||||
    """
 | 
			
		||||
    if fs == 'swap':
 | 
			
		||||
        args = ['mkswap']
 | 
			
		||||
    else:
 | 
			
		||||
        args = ['mkfs', '-t', fs]
 | 
			
		||||
    # add -F to force no interactive execute on non-block device.
 | 
			
		||||
    if fs in ('ext3', 'ext4'):
 | 
			
		||||
        args.extend(['-F'])
 | 
			
		||||
    if label:
 | 
			
		||||
        if fs in ('msdos', 'vfat'):
 | 
			
		||||
            label_opt = '-n'
 | 
			
		||||
        else:
 | 
			
		||||
            label_opt = '-L'
 | 
			
		||||
        args.extend([label_opt, label])
 | 
			
		||||
    args.append(path)
 | 
			
		||||
    try:
 | 
			
		||||
        execute(*args, run_as_root=True, use_standard_locale=True)
 | 
			
		||||
    except processutils.ProcessExecutionError as e:
 | 
			
		||||
        with excutils.save_and_reraise_exception() as ctx:
 | 
			
		||||
            if os.strerror(errno.ENOENT) in e.stderr:
 | 
			
		||||
                ctx.reraise = False
 | 
			
		||||
                LOG.exception(_LE('Failed to make file system. '
 | 
			
		||||
                                  'File system %s is not supported.'), fs)
 | 
			
		||||
                raise exception.FileSystemNotSupported(fs=fs)
 | 
			
		||||
            else:
 | 
			
		||||
                LOG.exception(_LE('Failed to create a file system '
 | 
			
		||||
                                  'in %(path)s. Error: %(error)s'),
 | 
			
		||||
                              {'path': path, 'error': e})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def unlink_without_raise(path):
 | 
			
		||||
    try:
 | 
			
		||||
        os.unlink(path)
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        if e.errno == errno.ENOENT:
 | 
			
		||||
            return
 | 
			
		||||
        else:
 | 
			
		||||
            LOG.warn(_LW("Failed to unlink %(path)s, error: %(e)s"),
 | 
			
		||||
                     {'path': path, 'e': e})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def rmtree_without_raise(path):
 | 
			
		||||
    try:
 | 
			
		||||
        if os.path.isdir(path):
 | 
			
		||||
            shutil.rmtree(path)
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        LOG.warn(_LW("Failed to remove dir %(path)s, error: %(e)s"),
 | 
			
		||||
                 {'path': path, 'e': e})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_to_file(path, contents):
 | 
			
		||||
    with open(path, 'w') as f:
 | 
			
		||||
        f.write(contents)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_link_without_raise(source, link):
 | 
			
		||||
    try:
 | 
			
		||||
        os.symlink(source, link)
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        if e.errno == errno.EEXIST:
 | 
			
		||||
            return
 | 
			
		||||
        else:
 | 
			
		||||
            LOG.warn(_LW("Failed to create symlink from %(source)s to %(link)s"
 | 
			
		||||
                         ", error: %(e)s"),
 | 
			
		||||
                     {'source': source, 'link': link, 'e': e})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def safe_rstrip(value, chars=None):
 | 
			
		||||
    """Removes trailing characters from a string if that does not make it empty
 | 
			
		||||
 | 
			
		||||
    :param value: A string value that will be stripped.
 | 
			
		||||
    :param chars: Characters to remove.
 | 
			
		||||
    :return: Stripped value.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    if not isinstance(value, six.string_types):
 | 
			
		||||
        LOG.warn(_LW("Failed to remove trailing character. Returning original "
 | 
			
		||||
                     "object. Supplied object is not a string: %s,"), value)
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    return value.rstrip(chars) or value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mount(src, dest, *args):
 | 
			
		||||
    """Mounts a device/image file on specified location.
 | 
			
		||||
 | 
			
		||||
    :param src: the path to the source file for mounting
 | 
			
		||||
    :param dest: the path where it needs to be mounted.
 | 
			
		||||
    :param args: a tuple containing the arguments to be
 | 
			
		||||
        passed to mount command.
 | 
			
		||||
    :raises: processutils.ProcessExecutionError if it failed
 | 
			
		||||
        to run the process.
 | 
			
		||||
    """
 | 
			
		||||
    args = ('mount', ) + args + (src, dest)
 | 
			
		||||
    execute(*args, run_as_root=True, check_exit_code=[0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def umount(loc, *args):
 | 
			
		||||
    """Umounts a mounted location.
 | 
			
		||||
 | 
			
		||||
    :param loc: the path to be unmounted.
 | 
			
		||||
    :param args: a tuple containing the argumnets to be
 | 
			
		||||
        passed to the umount command.
 | 
			
		||||
    :raises: processutils.ProcessExecutionError if it failed
 | 
			
		||||
        to run the process.
 | 
			
		||||
    """
 | 
			
		||||
    args = ('umount', ) + args + (loc, )
 | 
			
		||||
    execute(*args, run_as_root=True, check_exit_code=[0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def dd(src, dst, *args):
 | 
			
		||||
    """Execute dd from src to dst.
 | 
			
		||||
 | 
			
		||||
    :param src: the input file for dd command.
 | 
			
		||||
    :param dst: the output file for dd command.
 | 
			
		||||
    :param args: a tuple containing the arguments to be
 | 
			
		||||
        passed to dd command.
 | 
			
		||||
    :raises: processutils.ProcessExecutionError if it failed
 | 
			
		||||
        to run the process.
 | 
			
		||||
    """
 | 
			
		||||
    LOG.debug("Starting dd process.")
 | 
			
		||||
    execute('dd', 'if=%s' % src, 'of=%s' % dst, *args,
 | 
			
		||||
            run_as_root=True, check_exit_code=[0])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def is_http_url(url):
 | 
			
		||||
    url = url.lower()
 | 
			
		||||
    return url.startswith('http://') or url.startswith('https://')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_dir(directory_to_check=None, required_space=1):
 | 
			
		||||
    """Check a directory is usable.
 | 
			
		||||
 | 
			
		||||
    This function can be used by drivers to check that directories
 | 
			
		||||
    they need to write to are usable. This should be called from the
 | 
			
		||||
    drivers init function. This function checks that the directory
 | 
			
		||||
    exists and then calls check_dir_writable and check_dir_free_space.
 | 
			
		||||
    If directory_to_check is not provided the default is to use the
 | 
			
		||||
    temp directory.
 | 
			
		||||
 | 
			
		||||
    :param directory_to_check: the directory to check.
 | 
			
		||||
    :param required_space: amount of space to check for in MiB.
 | 
			
		||||
    :raises: PathNotFound if directory can not be found
 | 
			
		||||
    :raises: DirectoryNotWritable if user is unable to write to the
 | 
			
		||||
             directory
 | 
			
		||||
    :raises InsufficientDiskSpace: if free space is < required space
 | 
			
		||||
    """
 | 
			
		||||
    # check if directory_to_check is passed in, if not set to tempdir
 | 
			
		||||
    if directory_to_check is None:
 | 
			
		||||
        directory_to_check = (tempfile.gettempdir() if CONF.tempdir
 | 
			
		||||
                              is None else CONF.tempdir)
 | 
			
		||||
 | 
			
		||||
    LOG.debug("checking directory: %s", directory_to_check)
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(directory_to_check):
 | 
			
		||||
        raise exception.PathNotFound(dir=directory_to_check)
 | 
			
		||||
 | 
			
		||||
    _check_dir_writable(directory_to_check)
 | 
			
		||||
    _check_dir_free_space(directory_to_check, required_space)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_dir_writable(chk_dir):
 | 
			
		||||
    """Check that the chk_dir is able to be written to.
 | 
			
		||||
 | 
			
		||||
    :param chk_dir: Directory to check
 | 
			
		||||
    :raises: DirectoryNotWritable if user is unable to write to the
 | 
			
		||||
             directory
 | 
			
		||||
    """
 | 
			
		||||
    is_writable = os.access(chk_dir, os.W_OK)
 | 
			
		||||
    if not is_writable:
 | 
			
		||||
        raise exception.DirectoryNotWritable(dir=chk_dir)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_dir_free_space(chk_dir, required_space=1):
 | 
			
		||||
    """Check that directory has some free space.
 | 
			
		||||
 | 
			
		||||
    :param chk_dir: Directory to check
 | 
			
		||||
    :param required_space: amount of space to check for in MiB.
 | 
			
		||||
    :raises InsufficientDiskSpace: if free space is < required space
 | 
			
		||||
    """
 | 
			
		||||
    # check that we have some free space
 | 
			
		||||
    stat = os.statvfs(chk_dir)
 | 
			
		||||
    # get dir free space in MiB.
 | 
			
		||||
    free_space = float(stat.f_bsize * stat.f_bavail) / 1024 / 1024
 | 
			
		||||
    # check for at least required_space MiB free
 | 
			
		||||
    if free_space < required_space:
 | 
			
		||||
        raise exception.InsufficientDiskSpace(path=chk_dir,
 | 
			
		||||
                                              required=required_space,
 | 
			
		||||
                                              actual=free_space)
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/conductor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/conductor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										2195
									
								
								iotronic/conductor/__old/manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2195
									
								
								iotronic/conductor/__old/manager.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										362
									
								
								iotronic/conductor/__old/task_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										362
									
								
								iotronic/conductor/__old/task_manager.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,362 @@
 | 
			
		||||
# 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:
 | 
			
		||||
        <do some work>
 | 
			
		||||
        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:
 | 
			
		||||
        <do some work>
 | 
			
		||||
        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()
 | 
			
		||||
							
								
								
									
										160
									
								
								iotronic/conductor/__old/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								iotronic/conductor/__old/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
# 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()
 | 
			
		||||
							
								
								
									
										2269
									
								
								iotronic/conductor/manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2269
									
								
								iotronic/conductor/manager.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										519
									
								
								iotronic/conductor/rpcapi.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								iotronic/conductor/rpcapi.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,519 @@
 | 
			
		||||
# 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.
 | 
			
		||||
"""
 | 
			
		||||
Client side of the conductor RPC API.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ConductorAPI(object):
 | 
			
		||||
    """Client side of the conductor RPC API.
 | 
			
		||||
 | 
			
		||||
    API version history:
 | 
			
		||||
    |    1.0 - Initial version.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    # NOTE(rloo): This must be in sync with manager.ConductorManager's.
 | 
			
		||||
    RPC_API_VERSION = '1.0'
 | 
			
		||||
 | 
			
		||||
    def __init__(self, topic=None):
 | 
			
		||||
        super(ConductorAPI, self).__init__()
 | 
			
		||||
        self.topic = topic
 | 
			
		||||
        if self.topic is None:
 | 
			
		||||
            self.topic = manager.MANAGER_TOPIC
 | 
			
		||||
 | 
			
		||||
        target = messaging.Target(topic=self.topic,
 | 
			
		||||
                                  version='1.0')
 | 
			
		||||
        serializer = objects_base.IotronicObjectSerializer()
 | 
			
		||||
        self.client = rpc.get_client(target,
 | 
			
		||||
                                     version_cap=self.RPC_API_VERSION,
 | 
			
		||||
                                     serializer=serializer)
 | 
			
		||||
        # NOTE(deva): this is going to be buggy
 | 
			
		||||
        self.ring_manager = hash_ring.HashRingManager()
 | 
			
		||||
 | 
			
		||||
    def get_topic_for(self, node):
 | 
			
		||||
        """Get the RPC topic for the conductor service the node is mapped to.
 | 
			
		||||
 | 
			
		||||
        :param node: a node object.
 | 
			
		||||
        :returns: an RPC topic string.
 | 
			
		||||
        :raises: NoValidHost
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        '''
 | 
			
		||||
        self.ring_manager.reset()
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            ring = self.ring_manager[node.driver]
 | 
			
		||||
            dest = ring.get_hosts(node.uuid)
 | 
			
		||||
            return self.topic + "." + dest[0]
 | 
			
		||||
        except exception.DriverNotFound:
 | 
			
		||||
            reason = (_('No conductor service registered which supports '
 | 
			
		||||
                        '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.
 | 
			
		||||
 | 
			
		||||
        The topic is used to route messages to the conductor supporting
 | 
			
		||||
        the specified driver. A conductor is selected at random from the
 | 
			
		||||
        set of qualified conductors.
 | 
			
		||||
 | 
			
		||||
        :param driver_name: the name of the driver to route to.
 | 
			
		||||
        :returns: an RPC topic string.
 | 
			
		||||
        :raises: DriverNotFound
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        self.ring_manager.reset()
 | 
			
		||||
 | 
			
		||||
        hash_ring = self.ring_manager[driver_name]
 | 
			
		||||
        host = random.choice(list(hash_ring.hosts))
 | 
			
		||||
        return self.topic + "." + host
 | 
			
		||||
 | 
			
		||||
    def update_node(self, context, node_obj, topic=None):
 | 
			
		||||
        """Synchronously, have a conductor update the node's information.
 | 
			
		||||
 | 
			
		||||
        Update the node's information in the database and return a node object.
 | 
			
		||||
        The conductor will lock the node while it validates the supplied
 | 
			
		||||
        information. If driver_info is passed, it will be validated by
 | 
			
		||||
        the core drivers. If instance_uuid is passed, it will be set or unset
 | 
			
		||||
        only if the node is properly configured.
 | 
			
		||||
 | 
			
		||||
        Note that power_state should not be passed via this method.
 | 
			
		||||
        Use change_node_power_state for initiating driver actions.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_obj: a changed (but not saved) node object.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: updated node object, including all fields.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.1')
 | 
			
		||||
        return cctxt.call(context, 'update_node', node_obj=node_obj)
 | 
			
		||||
 | 
			
		||||
    def change_node_power_state(self, context, node_id, new_state, topic=None):
 | 
			
		||||
        """Change a node's power state.
 | 
			
		||||
 | 
			
		||||
        Synchronously, acquire lock and start the conductor background task
 | 
			
		||||
        to change power state of a node.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param new_state: one of iotronic.common.states power state values
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                 async task.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.6')
 | 
			
		||||
        return cctxt.call(context, 'change_node_power_state', node_id=node_id,
 | 
			
		||||
                          new_state=new_state)
 | 
			
		||||
 | 
			
		||||
    def vendor_passthru(self, context, node_id, driver_method, http_method,
 | 
			
		||||
                        info, topic=None):
 | 
			
		||||
        """Receive requests for vendor-specific actions.
 | 
			
		||||
 | 
			
		||||
        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: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param driver_method: name of method for driver.
 | 
			
		||||
        :param http_method: the HTTP method used for the request.
 | 
			
		||||
        :param info: info for node driver.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: InvalidParameterValue if supplied info is not valid.
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is missing
 | 
			
		||||
        :raises: UnsupportedDriverExtension if current driver does not have
 | 
			
		||||
                 vendor interface.
 | 
			
		||||
        :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.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.20')
 | 
			
		||||
        return cctxt.call(context, 'vendor_passthru', node_id=node_id,
 | 
			
		||||
                          driver_method=driver_method,
 | 
			
		||||
                          http_method=http_method,
 | 
			
		||||
                          info=info)
 | 
			
		||||
 | 
			
		||||
    def driver_vendor_passthru(self, context, driver_name, driver_method,
 | 
			
		||||
                               http_method, info, topic=None):
 | 
			
		||||
        """Pass vendor-specific calls which don't specify a node to a driver.
 | 
			
		||||
 | 
			
		||||
        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: request 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: data to pass through to the driver.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: InvalidParameterValue for parameter errors.
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is missing
 | 
			
		||||
        :raises: UnsupportedDriverExtension if the driver doesn't have a vendor
 | 
			
		||||
                 interface, or if the vendor interface does not support the
 | 
			
		||||
                 specified driver_method.
 | 
			
		||||
        :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.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.20')
 | 
			
		||||
        return cctxt.call(context, 'driver_vendor_passthru',
 | 
			
		||||
                          driver_name=driver_name,
 | 
			
		||||
                          driver_method=driver_method,
 | 
			
		||||
                          http_method=http_method,
 | 
			
		||||
                          info=info)
 | 
			
		||||
 | 
			
		||||
    def get_node_vendor_passthru_methods(self, context, node_id, topic=None):
 | 
			
		||||
        """Retrieve information about vendor methods of the given node.
 | 
			
		||||
 | 
			
		||||
        :param context: an admin context.
 | 
			
		||||
        :param node_id: the id or uuid of a node.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: dictionary of <method name>:<method metadata> entries.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.21')
 | 
			
		||||
        return cctxt.call(context, 'get_node_vendor_passthru_methods',
 | 
			
		||||
                          node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def get_driver_vendor_passthru_methods(self, context, driver_name,
 | 
			
		||||
                                           topic=None):
 | 
			
		||||
        """Retrieve information about vendor methods of the given driver.
 | 
			
		||||
 | 
			
		||||
        :param context: an admin context.
 | 
			
		||||
        :param driver_name: name of the driver.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: dictionary of <method name>:<method metadata> entries.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.21')
 | 
			
		||||
        return cctxt.call(context, 'get_driver_vendor_passthru_methods',
 | 
			
		||||
                          driver_name=driver_name)
 | 
			
		||||
 | 
			
		||||
    def do_node_deploy(self, context, node_id, rebuild, configdrive,
 | 
			
		||||
                       topic=None):
 | 
			
		||||
        """Signal to conductor service to perform a deployment.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param rebuild: True if this is a rebuild request.
 | 
			
		||||
        :param configdrive: A gzipped and base64 encoded configdrive.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: InstanceDeployFailure
 | 
			
		||||
        :raises: InvalidParameterValue if validation fails
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is missing
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                 async task.
 | 
			
		||||
 | 
			
		||||
        The node must already be configured and in the appropriate
 | 
			
		||||
        undeployed state before this method is called.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.22')
 | 
			
		||||
        return cctxt.call(context, 'do_node_deploy', node_id=node_id,
 | 
			
		||||
                          rebuild=rebuild, configdrive=configdrive)
 | 
			
		||||
 | 
			
		||||
    def do_node_tear_down(self, context, node_id, topic=None):
 | 
			
		||||
        """Signal to conductor service to tear down a deployment.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: InstanceDeployFailure
 | 
			
		||||
        :raises: InvalidParameterValue if validation fails
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is missing
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                 async task.
 | 
			
		||||
 | 
			
		||||
        The node must already be configured and in the appropriate
 | 
			
		||||
        deployed state before this method is called.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.6')
 | 
			
		||||
        return cctxt.call(context, 'do_node_tear_down', node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def do_provisioning_action(self, context, node_id, action, topic=None):
 | 
			
		||||
        """Signal to conductor service to perform the given action on a node.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param action: an action. One of iotronic.common.states.VERBS
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: InvalidParameterValue
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                async task.
 | 
			
		||||
        :raises: InvalidStateRequested if the requested action can not
 | 
			
		||||
                 be performed.
 | 
			
		||||
 | 
			
		||||
        This encapsulates some provisioning actions in a single call.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.23')
 | 
			
		||||
        return cctxt.call(context, 'do_provisioning_action',
 | 
			
		||||
                          node_id=node_id, action=action)
 | 
			
		||||
 | 
			
		||||
    def continue_node_clean(self, context, node_id, topic=None):
 | 
			
		||||
        """Signal to conductor service to start the next cleaning action.
 | 
			
		||||
 | 
			
		||||
        NOTE(JoshNang) this is an RPC cast, there will be no response or
 | 
			
		||||
        exception raised by the conductor for this RPC.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.27')
 | 
			
		||||
        return cctxt.cast(context, 'continue_node_clean',
 | 
			
		||||
                          node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def validate_driver_interfaces(self, context, node_id, topic=None):
 | 
			
		||||
        """Validate the `core` and `standardized` interfaces for drivers.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: a dictionary containing the results of each
 | 
			
		||||
                  interface validation.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.5')
 | 
			
		||||
        return cctxt.call(context, 'validate_driver_interfaces',
 | 
			
		||||
                          node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def destroy_node(self, context, node_id, topic=None):
 | 
			
		||||
        """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.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.9')
 | 
			
		||||
        return cctxt.call(context, 'destroy_node', node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def get_console_information(self, context, node_id, topic=None):
 | 
			
		||||
        """Get connection information about the console.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: UnsupportedDriverExtension if the node's driver doesn't
 | 
			
		||||
                 support console.
 | 
			
		||||
        :raises: InvalidParameterValue when the wrong driver info is specified.
 | 
			
		||||
        :raises: MissingParameterValue if a required parameter is missing
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.11')
 | 
			
		||||
        return cctxt.call(context, 'get_console_information', node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def set_console_mode(self, context, node_id, enabled, topic=None):
 | 
			
		||||
        """Enable/Disable the console.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :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 a required parameter is missing
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                 async task.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.11')
 | 
			
		||||
        return cctxt.call(context, 'set_console_mode', node_id=node_id,
 | 
			
		||||
                          enabled=enabled)
 | 
			
		||||
 | 
			
		||||
    def update_port(self, context, port_obj, topic=None):
 | 
			
		||||
        """Synchronously, have a conductor update the port's information.
 | 
			
		||||
 | 
			
		||||
        Update the port's information in the database and return a port object.
 | 
			
		||||
        The conductor will lock related node and trigger specific driver
 | 
			
		||||
        actions if they are needed.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param port_obj: a changed (but not saved) port object.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: updated port object, including all fields.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.13')
 | 
			
		||||
        return cctxt.call(context, 'update_port', port_obj=port_obj)
 | 
			
		||||
 | 
			
		||||
    def get_driver_properties(self, context, driver_name, topic=None):
 | 
			
		||||
        """Get the properties of the driver.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param driver_name: name of the driver.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :returns: a dictionary with <property name>:<property description>
 | 
			
		||||
                  entries.
 | 
			
		||||
        :raises: DriverNotFound.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.16')
 | 
			
		||||
        return cctxt.call(context, 'get_driver_properties',
 | 
			
		||||
                          driver_name=driver_name)
 | 
			
		||||
 | 
			
		||||
    def set_boot_device(self, context, node_id, device, persistent=False,
 | 
			
		||||
                        topic=None):
 | 
			
		||||
        """Set the boot device for a node.
 | 
			
		||||
 | 
			
		||||
        Set the boot device to use on next reboot of the node. Be aware
 | 
			
		||||
        that not all drivers support this.
 | 
			
		||||
 | 
			
		||||
        :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.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
 | 
			
		||||
        return cctxt.call(context, 'set_boot_device', node_id=node_id,
 | 
			
		||||
                          device=device, persistent=persistent)
 | 
			
		||||
 | 
			
		||||
    def get_boot_device(self, context, node_id, topic=None):
 | 
			
		||||
        """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.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
 | 
			
		||||
        return cctxt.call(context, 'get_boot_device', node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def get_supported_boot_devices(self, context, node_id, topic=None):
 | 
			
		||||
        """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`.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.17')
 | 
			
		||||
        return cctxt.call(context, 'get_supported_boot_devices',
 | 
			
		||||
                          node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def inspect_hardware(self, context, node_id, topic=None):
 | 
			
		||||
        """Signals the conductor service to perform hardware introspection.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param node_id: node id or uuid.
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: NodeLocked if node is locked by another conductor.
 | 
			
		||||
        :raises: HardwareInspectionFailure
 | 
			
		||||
        :raises: NoFreeConductorWorker when there is no free worker to start
 | 
			
		||||
                 async task.
 | 
			
		||||
        :raises: UnsupportedDriverExtension if the node's driver doesn't
 | 
			
		||||
                 support inspection.
 | 
			
		||||
        :raises: InvalidStateRequested if 'inspect' is not a valid
 | 
			
		||||
                 action to do in the current state.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.24')
 | 
			
		||||
        return cctxt.call(context, 'inspect_hardware', node_id=node_id)
 | 
			
		||||
 | 
			
		||||
    def destroy_port(self, context, port, topic=None):
 | 
			
		||||
        """Delete a port.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param port: port object
 | 
			
		||||
        :param topic: RPC topic. Defaults to self.topic.
 | 
			
		||||
        :raises: NodeLocked if node is locked by another conductor.
 | 
			
		||||
        :raises: NodeNotFound if the node associated with the port does not
 | 
			
		||||
                 exist.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.25')
 | 
			
		||||
        return cctxt.call(context, 'destroy_port', port=port)
 | 
			
		||||
 | 
			
		||||
######################### NEW
 | 
			
		||||
 | 
			
		||||
    def destroy_board(self, context, board_id, topic=None):
 | 
			
		||||
        """Delete a board.
 | 
			
		||||
 | 
			
		||||
        :param context: request context.
 | 
			
		||||
        :param board_id: board id or uuid.
 | 
			
		||||
        :raises: BoardLocked if board is locked by another conductor.
 | 
			
		||||
        :raises: BoardAssociated if the board contains an instance
 | 
			
		||||
            associated with it.
 | 
			
		||||
        :raises: InvalidState if the board is in the wrong provision
 | 
			
		||||
            state to perform deletion.
 | 
			
		||||
        """
 | 
			
		||||
        cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
 | 
			
		||||
        return cctxt.call(context, 'destroy_board', board_id=board_id)
 | 
			
		||||
							
								
								
									
										363
									
								
								iotronic/conductor/task_manager.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										363
									
								
								iotronic/conductor/task_manager.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,363 @@
 | 
			
		||||
# 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 board and its resources.
 | 
			
		||||
 | 
			
		||||
The :class:`TaskManager` will, by default, acquire an exclusive lock on
 | 
			
		||||
a board 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 board resources and properties as
 | 
			
		||||
attributes that you may access:
 | 
			
		||||
 | 
			
		||||
    task.context
 | 
			
		||||
        The context passed to TaskManager()
 | 
			
		||||
    task.shared
 | 
			
		||||
        False if Board is locked, True if it is not locked. (The
 | 
			
		||||
        'shared' kwarg arg of TaskManager())
 | 
			
		||||
    task.board
 | 
			
		||||
        The Board object
 | 
			
		||||
    task.ports
 | 
			
		||||
        Ports belonging to the Board
 | 
			
		||||
    task.driver
 | 
			
		||||
        The Driver for the Board, or the Driver based on the
 | 
			
		||||
        'driver_name' kwarg of TaskManager().
 | 
			
		||||
 | 
			
		||||
Example usage:
 | 
			
		||||
 | 
			
		||||
::
 | 
			
		||||
 | 
			
		||||
    with task_manager.acquire(context, board_id) as task:
 | 
			
		||||
        task.driver.power.power_on(task.board)
 | 
			
		||||
 | 
			
		||||
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, board_id) as task:
 | 
			
		||||
        <do some work>
 | 
			
		||||
        task.spawn_after(self._spawn_worker,
 | 
			
		||||
                         utils.board_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, board_id) as task:
 | 
			
		||||
        <do some work>
 | 
			
		||||
        task.set_spawn_error_hook(on_error)
 | 
			
		||||
        task.spawn_after(self._spawn_worker,
 | 
			
		||||
                         utils.board_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, board_id, shared=False, driver_name=None):
 | 
			
		||||
    """Shortcut for acquiring a lock on a Board.
 | 
			
		||||
 | 
			
		||||
    :param context: Request context.
 | 
			
		||||
    :param board_id: ID or UUID of board 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, board_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, Board and Ports) when beginning a unit of work.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(self, context, board_id, shared=False, driver_name=None):
 | 
			
		||||
        """Create a new TaskManager.
 | 
			
		||||
 | 
			
		||||
        Acquire a lock on a board. 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 board at the same time.
 | 
			
		||||
 | 
			
		||||
        :param context: request context
 | 
			
		||||
        :param board_id: ID or UUID of board 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 Board's current driver.
 | 
			
		||||
        :raises: DriverNotFound
 | 
			
		||||
        :raises: BoardNotFound
 | 
			
		||||
        :raises: BoardLocked
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        self._spawn_method = None
 | 
			
		||||
        self._on_error_method = None
 | 
			
		||||
 | 
			
		||||
        self.context = context
 | 
			
		||||
        #self.board = None
 | 
			
		||||
        self.board = None
 | 
			
		||||
        self.shared = shared
 | 
			
		||||
 | 
			
		||||
        self.fsm = states.machine.copy()
 | 
			
		||||
 | 
			
		||||
        # BoardLocked 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.BoardLocked),
 | 
			
		||||
            stop_max_attempt_number=CONF.conductor.board_locked_retry_attempts,
 | 
			
		||||
            wait_fixed=CONF.conductor.board_locked_retry_interval * 1000)
 | 
			
		||||
        def reserve_board():
 | 
			
		||||
            LOG.debug("Attempting to reserve board %(board)s",
 | 
			
		||||
                      {'board': board_id})
 | 
			
		||||
            self.board = objects.Board.reserve(context, CONF.host, board_id)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            if not self.shared:
 | 
			
		||||
                reserve_board()
 | 
			
		||||
            else:
 | 
			
		||||
                self.board = objects.Board.get(context, board_id)
 | 
			
		||||
            #self.ports = objects.Port.list_by_board_id(context, self.board.id)
 | 
			
		||||
            #self.driver = driver_factory.get_driver(driver_name or
 | 
			
		||||
            #                                       self.board.driver)
 | 
			
		||||
 | 
			
		||||
            # NOTE(deva): this handles the Juno-era NOSTATE state
 | 
			
		||||
            #             and should be deleted after Kilo is released
 | 
			
		||||
            '''
 | 
			
		||||
            if self.board.provision_state is states.NOSTATE:
 | 
			
		||||
                self.board.provision_state = states.AVAILABLE
 | 
			
		||||
                self.board.save()
 | 
			
		||||
 | 
			
		||||
            self.fsm.initialize(self.board.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 board and release resources.
 | 
			
		||||
 | 
			
		||||
        If an exclusive lock is held, unlock the board. Reset attributes
 | 
			
		||||
        to make it clear that this instance of TaskManager should no
 | 
			
		||||
        longer be accessed.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
        if not self.shared:
 | 
			
		||||
            try:
 | 
			
		||||
                if self.board:
 | 
			
		||||
                    objects.Board.release(self.context, CONF.host, self.board.id)
 | 
			
		||||
            except exception.BoardNotFound:
 | 
			
		||||
                # squelch the exception if the board was deleted
 | 
			
		||||
                # within the task's context.
 | 
			
		||||
                pass
 | 
			
		||||
        self.board = 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 board, 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 board 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.board,
 | 
			
		||||
                                      self.board.provision_state,
 | 
			
		||||
                                      self.board.target_provision_state)
 | 
			
		||||
 | 
			
		||||
        self.board.provision_state = self.fsm.current_state
 | 
			
		||||
        self.board.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.board.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 Board
 | 
			
		||||
        self.board.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 board %(board)s"),
 | 
			
		||||
                                    {'method': self._on_error_method.__name__,
 | 
			
		||||
                                    'board': self.board.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()
 | 
			
		||||
							
								
								
									
										160
									
								
								iotronic/conductor/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										160
									
								
								iotronic/conductor/utils.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,160 @@
 | 
			
		||||
# 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()
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/db/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/db/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										488
									
								
								iotronic/db/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										488
									
								
								iotronic/db/api.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,488 @@
 | 
			
		||||
# -*- encoding: utf-8 -*-
 | 
			
		||||
#
 | 
			
		||||
# Copyright 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.
 | 
			
		||||
"""
 | 
			
		||||
Base classes for storage engines
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import abc
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from oslo_db import api as db_api
 | 
			
		||||
import six
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_BACKEND_MAPPING = {'sqlalchemy': 'iotronic.db.sqlalchemy.api'}
 | 
			
		||||
IMPL = db_api.DBAPI.from_config(cfg.CONF, backend_mapping=_BACKEND_MAPPING,
 | 
			
		||||
                                lazy=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_instance():
 | 
			
		||||
    """Return a DB API instance."""
 | 
			
		||||
    return IMPL
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@six.add_metaclass(abc.ABCMeta)
 | 
			
		||||
class Connection(object):
 | 
			
		||||
    """Base class for storage system connections."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        """Constructor."""
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_nodeinfo_list(self, columns=None, filters=None, limit=None,
 | 
			
		||||
                          marker=None, sort_key=None, sort_dir=None):
 | 
			
		||||
        """Get specific columns for matching nodes.
 | 
			
		||||
 | 
			
		||||
        Return a list of the specified columns for all nodes that match the
 | 
			
		||||
        specified filters.
 | 
			
		||||
 | 
			
		||||
        :param columns: List of column names to return.
 | 
			
		||||
                        Defaults to 'id' column when columns == None.
 | 
			
		||||
        :param filters: Filters to apply. Defaults to None.
 | 
			
		||||
 | 
			
		||||
                        :associated: True | False
 | 
			
		||||
                        :reserved: True | False
 | 
			
		||||
                        :maintenance: True | False
 | 
			
		||||
                        :chassis_uuid: uuid of chassis
 | 
			
		||||
                        :driver: driver's name
 | 
			
		||||
                        :provision_state: provision state of node
 | 
			
		||||
                        :provisioned_before:
 | 
			
		||||
                            nodes with provision_updated_at field before this
 | 
			
		||||
                            interval in seconds
 | 
			
		||||
        :param limit: Maximum number of nodes to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted.
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted.
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        :returns: A list of tuples of the specified columns.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_node_list(self, filters=None, limit=None, marker=None,
 | 
			
		||||
                      sort_key=None, sort_dir=None):
 | 
			
		||||
        """Return a list of nodes.
 | 
			
		||||
 | 
			
		||||
        :param filters: Filters to apply. Defaults to None.
 | 
			
		||||
 | 
			
		||||
                        :associated: True | False
 | 
			
		||||
                        :reserved: True | False
 | 
			
		||||
                        :maintenance: True | False
 | 
			
		||||
                        :chassis_uuid: uuid of chassis
 | 
			
		||||
                        :driver: driver's name
 | 
			
		||||
                        :provision_state: provision state of node
 | 
			
		||||
                        :provisioned_before:
 | 
			
		||||
                            nodes with provision_updated_at field before this
 | 
			
		||||
                            interval in seconds
 | 
			
		||||
        :param limit: Maximum number of nodes to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted.
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted.
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def reserve_node(self, tag, node_id):
 | 
			
		||||
        """Reserve a node.
 | 
			
		||||
 | 
			
		||||
        To prevent other ManagerServices from manipulating the given
 | 
			
		||||
        Node while a Task is performed, mark it reserved by this host.
 | 
			
		||||
 | 
			
		||||
        :param tag: A string uniquely identifying the reservation holder.
 | 
			
		||||
        :param node_id: A node id or uuid.
 | 
			
		||||
        :returns: A Node object.
 | 
			
		||||
        :raises: NodeNotFound if the node is not found.
 | 
			
		||||
        :raises: NodeLocked if the node is already reserved.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def release_node(self, tag, node_id):
 | 
			
		||||
        """Release the reservation on a node.
 | 
			
		||||
 | 
			
		||||
        :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.
 | 
			
		||||
        :raises: NodeLocked if the node is reserved by another host.
 | 
			
		||||
        :raises: NodeNotLocked if the node was found to not have a
 | 
			
		||||
                 reservation at all.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def create_node(self, values):
 | 
			
		||||
        """Create a new node.
 | 
			
		||||
 | 
			
		||||
        :param values: A dict containing several items used to identify
 | 
			
		||||
                       and track the node, and several dicts which are passed
 | 
			
		||||
                       into the Drivers when managing this node. For example:
 | 
			
		||||
 | 
			
		||||
                       ::
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                         'uuid': uuidutils.generate_uuid(),
 | 
			
		||||
                         'instance_uuid': None,
 | 
			
		||||
                         'power_state': states.POWER_OFF,
 | 
			
		||||
                         'provision_state': states.AVAILABLE,
 | 
			
		||||
                         'driver': 'pxe_ipmitool',
 | 
			
		||||
                         'driver_info': { ... },
 | 
			
		||||
                         'properties': { ... },
 | 
			
		||||
                         'extra': { ... },
 | 
			
		||||
                        }
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_node_by_id(self, node_id):
 | 
			
		||||
        """Return a node.
 | 
			
		||||
 | 
			
		||||
        :param node_id: The id of a node.
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_node_by_uuid(self, node_uuid):
 | 
			
		||||
        """Return a node.
 | 
			
		||||
 | 
			
		||||
        :param node_uuid: The uuid of a node.
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_node_by_name(self, node_name):
 | 
			
		||||
        """Return a node.
 | 
			
		||||
 | 
			
		||||
        :param node_name: The logical name of a node.
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_node_by_instance(self, instance):
 | 
			
		||||
        """Return a node.
 | 
			
		||||
 | 
			
		||||
        :param instance: The instance name or uuid to search for.
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def destroy_node(self, node_id):
 | 
			
		||||
        """Destroy a node and all associated interfaces.
 | 
			
		||||
 | 
			
		||||
        :param node_id: The id or uuid of a node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def update_node(self, node_id, values):
 | 
			
		||||
        """Update properties of a node.
 | 
			
		||||
 | 
			
		||||
        :param node_id: The id or uuid of a node.
 | 
			
		||||
        :param values: Dict of values to update.
 | 
			
		||||
                       May be a partial list, eg. when setting the
 | 
			
		||||
                       properties for a driver. For example:
 | 
			
		||||
 | 
			
		||||
                       ::
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                         'driver_info':
 | 
			
		||||
                             {
 | 
			
		||||
                              'my-field-1': val1,
 | 
			
		||||
                              'my-field-2': val2,
 | 
			
		||||
                             }
 | 
			
		||||
                        }
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        :raises: NodeAssociated
 | 
			
		||||
        :raises: NodeNotFound
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_port_by_id(self, port_id):
 | 
			
		||||
        """Return a network port representation.
 | 
			
		||||
 | 
			
		||||
        :param port_id: The id of a port.
 | 
			
		||||
        :returns: A port.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_port_by_uuid(self, port_uuid):
 | 
			
		||||
        """Return a network port representation.
 | 
			
		||||
 | 
			
		||||
        :param port_uuid: The uuid of a port.
 | 
			
		||||
        :returns: A port.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_port_by_address(self, address):
 | 
			
		||||
        """Return a network port representation.
 | 
			
		||||
 | 
			
		||||
        :param address: The MAC address of a port.
 | 
			
		||||
        :returns: A port.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_port_list(self, limit=None, marker=None,
 | 
			
		||||
                      sort_key=None, sort_dir=None):
 | 
			
		||||
        """Return a list of ports.
 | 
			
		||||
 | 
			
		||||
        :param limit: Maximum number of ports to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted.
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted.
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_ports_by_node_id(self, node_id, limit=None, marker=None,
 | 
			
		||||
                             sort_key=None, sort_dir=None):
 | 
			
		||||
        """List all the ports for a given node.
 | 
			
		||||
 | 
			
		||||
        :param node_id: The integer node ID.
 | 
			
		||||
        :param limit: Maximum number of ports to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        :returns: A list of ports.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def create_port(self, values):
 | 
			
		||||
        """Create a new port.
 | 
			
		||||
 | 
			
		||||
        :param values: Dict of values.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def update_port(self, port_id, values):
 | 
			
		||||
        """Update properties of an port.
 | 
			
		||||
 | 
			
		||||
        :param port_id: The id or MAC of a port.
 | 
			
		||||
        :param values: Dict of values to update.
 | 
			
		||||
        :returns: A port.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def destroy_port(self, port_id):
 | 
			
		||||
        """Destroy an port.
 | 
			
		||||
 | 
			
		||||
        :param port_id: The id or MAC of a port.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def create_chassis(self, values):
 | 
			
		||||
        """Create a new chassis.
 | 
			
		||||
 | 
			
		||||
        :param values: Dict of values.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_chassis_by_id(self, chassis_id):
 | 
			
		||||
        """Return a chassis representation.
 | 
			
		||||
 | 
			
		||||
        :param chassis_id: The id of a chassis.
 | 
			
		||||
        :returns: A chassis.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_chassis_by_uuid(self, chassis_uuid):
 | 
			
		||||
        """Return a chassis representation.
 | 
			
		||||
 | 
			
		||||
        :param chassis_uuid: The uuid of a chassis.
 | 
			
		||||
        :returns: A chassis.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_chassis_list(self, limit=None, marker=None,
 | 
			
		||||
                         sort_key=None, sort_dir=None):
 | 
			
		||||
        """Return a list of chassis.
 | 
			
		||||
 | 
			
		||||
        :param limit: Maximum number of chassis to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted.
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted.
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def update_chassis(self, chassis_id, values):
 | 
			
		||||
        """Update properties of an chassis.
 | 
			
		||||
 | 
			
		||||
        :param chassis_id: The id or the uuid of a chassis.
 | 
			
		||||
        :param values: Dict of values to update.
 | 
			
		||||
        :returns: A chassis.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def destroy_chassis(self, chassis_id):
 | 
			
		||||
        """Destroy a chassis.
 | 
			
		||||
 | 
			
		||||
        :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.
 | 
			
		||||
 | 
			
		||||
        :param values: A dict of values which must contain the following:
 | 
			
		||||
 | 
			
		||||
                       ::
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                         'hostname': the unique hostname which identifies
 | 
			
		||||
                                     this Conductor service.
 | 
			
		||||
                         'drivers': a list of supported drivers.
 | 
			
		||||
                        }
 | 
			
		||||
        :param update_existing: When false, registration will raise an
 | 
			
		||||
                                exception when a conflicting online record
 | 
			
		||||
                                is found. When true, will overwrite the
 | 
			
		||||
                                existing record. Default: False.
 | 
			
		||||
        :returns: A conductor.
 | 
			
		||||
        :raises: ConductorAlreadyRegistered
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_conductor(self, hostname):
 | 
			
		||||
        """Retrieve a conductor's service record from the database.
 | 
			
		||||
 | 
			
		||||
        :param hostname: The hostname of the conductor service.
 | 
			
		||||
        :returns: A conductor.
 | 
			
		||||
        :raises: ConductorNotFound
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def unregister_conductor(self, hostname):
 | 
			
		||||
        """Remove this conductor from the service registry immediately.
 | 
			
		||||
 | 
			
		||||
        :param hostname: The hostname of this conductor service.
 | 
			
		||||
        :raises: ConductorNotFound
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def touch_conductor(self, hostname):
 | 
			
		||||
        """Mark a conductor as active by updating its 'updated_at' property.
 | 
			
		||||
 | 
			
		||||
        :param hostname: The hostname of this conductor service.
 | 
			
		||||
        :raises: ConductorNotFound
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_active_driver_dict(self, interval):
 | 
			
		||||
        """Retrieve drivers for the registered and active conductors.
 | 
			
		||||
 | 
			
		||||
        :param interval: Seconds since last check-in of a conductor.
 | 
			
		||||
        :returns: A dict which maps driver names to the set of hosts
 | 
			
		||||
                  which support them. For example:
 | 
			
		||||
 | 
			
		||||
                  ::
 | 
			
		||||
 | 
			
		||||
                    {driverA: set([host1, host2]),
 | 
			
		||||
                     driverB: set([host2, host3])}
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
###################### NEW #############################
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_board_by_uuid(self, node_uuid):
 | 
			
		||||
        """Return a node.
 | 
			
		||||
 | 
			
		||||
        :param node_uuid: The uuid of a node.
 | 
			
		||||
        :returns: A node.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def get_board_list(self, filters=None, limit=None, marker=None,
 | 
			
		||||
                      sort_key=None, sort_dir=None):
 | 
			
		||||
        """Return a list of nodes.
 | 
			
		||||
 | 
			
		||||
        :param filters: Filters to apply. Defaults to None.
 | 
			
		||||
 | 
			
		||||
                        :associated: True | False
 | 
			
		||||
                        :reserved: True | False
 | 
			
		||||
                        :maintenance: True | False
 | 
			
		||||
                        :chassis_uuid: uuid of chassis
 | 
			
		||||
                        :driver: driver's name
 | 
			
		||||
                        :provision_state: provision state of node
 | 
			
		||||
                        :provisioned_before:
 | 
			
		||||
                            nodes with provision_updated_at field before this
 | 
			
		||||
                            interval in seconds
 | 
			
		||||
        :param limit: Maximum number of nodes to return.
 | 
			
		||||
        :param marker: the last item of the previous page; we return the next
 | 
			
		||||
                       result set.
 | 
			
		||||
        :param sort_key: Attribute by which results should be sorted.
 | 
			
		||||
        :param sort_dir: direction in which results should be sorted.
 | 
			
		||||
                         (asc, desc)
 | 
			
		||||
        """
 | 
			
		||||
        
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def reserve_board(self, tag, board_id):
 | 
			
		||||
        """Reserve a board.
 | 
			
		||||
 | 
			
		||||
        To prevent other ManagerServices from manipulating the given
 | 
			
		||||
        Board while a Task is performed, mark it reserved by this host.
 | 
			
		||||
 | 
			
		||||
        :param tag: A string uniquely identifying the reservation holder.
 | 
			
		||||
        :param board_id: A board id or uuid.
 | 
			
		||||
        :returns: A Board object.
 | 
			
		||||
        :raises: BoardNotFound if the board is not found.
 | 
			
		||||
        :raises: BoardLocked if the board is already reserved.
 | 
			
		||||
        """
 | 
			
		||||
        
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def release_board(self, tag, board_id):
 | 
			
		||||
        """Release the reservation on a board.
 | 
			
		||||
 | 
			
		||||
        :param tag: A string uniquely identifying the reservation holder.
 | 
			
		||||
        :param board_id: A board id or uuid.
 | 
			
		||||
        :raises: BoardNotFound if the board is not found.
 | 
			
		||||
        :raises: BoardLocked if the board is reserved by another host.
 | 
			
		||||
        :raises: BoardNotLocked if the board was found to not have a
 | 
			
		||||
                 reservation at all.
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def destroy_board(self, board_id):
 | 
			
		||||
        """Destroy a board and all associated interfaces.
 | 
			
		||||
 | 
			
		||||
        :param board_id: The id or uuid of a board.
 | 
			
		||||
        """
 | 
			
		||||
        
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def create_board(self, values):
 | 
			
		||||
        """Create a new board.
 | 
			
		||||
 | 
			
		||||
        :param values: A dict containing several items used to identify
 | 
			
		||||
                       and track the board, and several dicts which are passed
 | 
			
		||||
                       into the Drivers when managing this board. For example:
 | 
			
		||||
 | 
			
		||||
                       ::
 | 
			
		||||
 | 
			
		||||
                        {
 | 
			
		||||
                         'uuid': uuidutils.generate_uuid(),
 | 
			
		||||
                         'instance_uuid': None,
 | 
			
		||||
                         'power_state': states.POWER_OFF,
 | 
			
		||||
                         'provision_state': states.AVAILABLE,
 | 
			
		||||
                         'driver': 'pxe_ipmitool',
 | 
			
		||||
                         'driver_info': { ... },
 | 
			
		||||
                         'properties': { ... },
 | 
			
		||||
                         'extra': { ... },
 | 
			
		||||
                        }
 | 
			
		||||
        :returns: A board.
 | 
			
		||||
        """
 | 
			
		||||
							
								
								
									
										56
									
								
								iotronic/db/migration.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								iotronic/db/migration.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
# Copyright 2010 United States Government as represented by the
 | 
			
		||||
# Administrator of the National Aeronautics and Space Administration.
 | 
			
		||||
# All Rights Reserved.
 | 
			
		||||
#
 | 
			
		||||
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 | 
			
		||||
#    not use this file except in compliance with the License. You may obtain
 | 
			
		||||
#    a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#         http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
#    Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 | 
			
		||||
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 | 
			
		||||
#    License for the specific language governing permissions and limitations
 | 
			
		||||
#    under the License.
 | 
			
		||||
 | 
			
		||||
"""Database setup and migration commands."""
 | 
			
		||||
 | 
			
		||||
from oslo_config import cfg
 | 
			
		||||
from stevedore import driver
 | 
			
		||||
 | 
			
		||||
_IMPL = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_backend():
 | 
			
		||||
    global _IMPL
 | 
			
		||||
    if not _IMPL:
 | 
			
		||||
        cfg.CONF.import_opt('backend', 'oslo_db.options', group='database')
 | 
			
		||||
        _IMPL = driver.DriverManager("iotronic.database.migration_backend",
 | 
			
		||||
                                     cfg.CONF.database.backend).driver
 | 
			
		||||
    return _IMPL
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade(version=None):
 | 
			
		||||
    """Migrate the database to `version` or the most recent version."""
 | 
			
		||||
    return get_backend().upgrade(version)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade(version=None):
 | 
			
		||||
    return get_backend().downgrade(version)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def version():
 | 
			
		||||
    return get_backend().version()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def stamp(version):
 | 
			
		||||
    return get_backend().stamp(version)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def revision(message, autogenerate):
 | 
			
		||||
    return get_backend().revision(message, autogenerate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def create_schema():
 | 
			
		||||
    return get_backend().create_schema()
 | 
			
		||||
							
								
								
									
										0
									
								
								iotronic/db/sqlalchemy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								iotronic/db/sqlalchemy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										54
									
								
								iotronic/db/sqlalchemy/alembic.ini
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								iotronic/db/sqlalchemy/alembic.ini
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,54 @@
 | 
			
		||||
# A generic, single database configuration.
 | 
			
		||||
 | 
			
		||||
[alembic]
 | 
			
		||||
# path to migration scripts
 | 
			
		||||
script_location = %(here)s/alembic
 | 
			
		||||
 | 
			
		||||
# template used to generate migration files
 | 
			
		||||
# file_template = %%(rev)s_%%(slug)s
 | 
			
		||||
 | 
			
		||||
# max length of characters to apply to the
 | 
			
		||||
# "slug" field
 | 
			
		||||
#truncate_slug_length = 40
 | 
			
		||||
 | 
			
		||||
# set to 'true' to run the environment during
 | 
			
		||||
# the 'revision' command, regardless of autogenerate
 | 
			
		||||
# revision_environment = false
 | 
			
		||||
 | 
			
		||||
#sqlalchemy.url = driver://user:pass@localhost/dbname
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Logging configuration
 | 
			
		||||
[loggers]
 | 
			
		||||
keys = root,sqlalchemy,alembic
 | 
			
		||||
 | 
			
		||||
[handlers]
 | 
			
		||||
keys = console
 | 
			
		||||
 | 
			
		||||
[formatters]
 | 
			
		||||
keys = generic
 | 
			
		||||
 | 
			
		||||
[logger_root]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers = console
 | 
			
		||||
qualname =
 | 
			
		||||
 | 
			
		||||
[logger_sqlalchemy]
 | 
			
		||||
level = WARN
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = sqlalchemy.engine
 | 
			
		||||
 | 
			
		||||
[logger_alembic]
 | 
			
		||||
level = INFO
 | 
			
		||||
handlers =
 | 
			
		||||
qualname = alembic
 | 
			
		||||
 | 
			
		||||
[handler_console]
 | 
			
		||||
class = StreamHandler
 | 
			
		||||
args = (sys.stderr,)
 | 
			
		||||
level = NOTSET
 | 
			
		||||
formatter = generic
 | 
			
		||||
 | 
			
		||||
[formatter_generic]
 | 
			
		||||
format = %(levelname)-5.5s [%(name)s] %(message)s
 | 
			
		||||
datefmt = %H:%M:%S
 | 
			
		||||
							
								
								
									
										16
									
								
								iotronic/db/sqlalchemy/alembic/README
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								iotronic/db/sqlalchemy/alembic/README
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
Please see https://alembic.readthedocs.org/en/latest/index.html for general documentation
 | 
			
		||||
 | 
			
		||||
To create alembic migrations use:
 | 
			
		||||
$ iotronic-dbsync revision --message --autogenerate
 | 
			
		||||
 | 
			
		||||
Stamp db with most recent migration version, without actually running migrations
 | 
			
		||||
$ iotronic-dbsync stamp --revision head
 | 
			
		||||
 | 
			
		||||
Upgrade can be performed by:
 | 
			
		||||
$ iotronic-dbsync - for backward compatibility
 | 
			
		||||
$ iotronic-dbsync upgrade
 | 
			
		||||
# iotronic-dbsync upgrade --revision head
 | 
			
		||||
 | 
			
		||||
Downgrading db:
 | 
			
		||||
$ iotronic-dbsync downgrade
 | 
			
		||||
$ iotronic-dbsync downgrade --revision base
 | 
			
		||||
							
								
								
									
										61
									
								
								iotronic/db/sqlalchemy/alembic/env.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								iotronic/db/sqlalchemy/alembic/env.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,61 @@
 | 
			
		||||
#    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 logging import config as log_config
 | 
			
		||||
 | 
			
		||||
from alembic import context
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
    # NOTE(whaom): This is to register the DB2 alembic code which
 | 
			
		||||
    # is an optional runtime dependency.
 | 
			
		||||
    from ibm_db_alembic.ibm_db import IbmDbImpl  # noqa
 | 
			
		||||
except ImportError:
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
from iotronic.db.sqlalchemy import api as sqla_api
 | 
			
		||||
from iotronic.db.sqlalchemy import models
 | 
			
		||||
 | 
			
		||||
# this is the Alembic Config object, which provides
 | 
			
		||||
# access to the values within the .ini file in use.
 | 
			
		||||
config = context.config
 | 
			
		||||
 | 
			
		||||
# Interpret the config file for Python logging.
 | 
			
		||||
# This line sets up loggers basically.
 | 
			
		||||
log_config.fileConfig(config.config_file_name)
 | 
			
		||||
 | 
			
		||||
# add your model's MetaData object here
 | 
			
		||||
# for 'autogenerate' support
 | 
			
		||||
# from myapp import mymodel
 | 
			
		||||
target_metadata = models.Base.metadata
 | 
			
		||||
 | 
			
		||||
# other values from the config, defined by the needs of env.py,
 | 
			
		||||
# can be acquired:
 | 
			
		||||
# my_important_option = config.get_main_option("my_important_option")
 | 
			
		||||
# ... etc.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_migrations_online():
 | 
			
		||||
    """Run migrations in 'online' mode.
 | 
			
		||||
 | 
			
		||||
    In this scenario we need to create an Engine
 | 
			
		||||
    and associate a connection with the context.
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
    engine = sqla_api.get_engine()
 | 
			
		||||
    with engine.connect() as connection:
 | 
			
		||||
        context.configure(connection=connection,
 | 
			
		||||
                          target_metadata=target_metadata)
 | 
			
		||||
        with context.begin_transaction():
 | 
			
		||||
            context.run_migrations()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
run_migrations_online()
 | 
			
		||||
							
								
								
									
										22
									
								
								iotronic/db/sqlalchemy/alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								iotronic/db/sqlalchemy/alembic/script.py.mako
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,22 @@
 | 
			
		||||
"""${message}
 | 
			
		||||
 | 
			
		||||
Revision ID: ${up_revision}
 | 
			
		||||
Revises: ${down_revision}
 | 
			
		||||
Create Date: ${create_date}
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = ${repr(up_revision)}
 | 
			
		||||
down_revision = ${repr(down_revision)}
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
${imports if imports else ""}
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    ${upgrades if upgrades else "pass"}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    ${downgrades if downgrades else "pass"}
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""add inspection_started_at and inspection_finished_at
 | 
			
		||||
 | 
			
		||||
Revision ID: 1e1d5ace7dc6
 | 
			
		||||
Revises: 3ae36a5f5131
 | 
			
		||||
Create Date: 2015-02-26 10:46:46.861927
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '1e1d5ace7dc6'
 | 
			
		||||
down_revision = '3ae36a5f5131'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('inspection_started_at',
 | 
			
		||||
                                     sa.DateTime(),
 | 
			
		||||
                                     nullable=True))
 | 
			
		||||
    op.add_column('nodes', sa.Column('inspection_finished_at',
 | 
			
		||||
                                     sa.DateTime(),
 | 
			
		||||
                                     nullable=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_column('nodes', 'inspection_started_at')
 | 
			
		||||
    op.drop_column('nodes', 'inspection_finished_at')
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""Add provision_updated_at
 | 
			
		||||
 | 
			
		||||
Revision ID: 21b331f883ef
 | 
			
		||||
Revises: 2581ebaf0cb2
 | 
			
		||||
Create Date: 2014-02-19 13:45:30.150632
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '21b331f883ef'
 | 
			
		||||
down_revision = '2581ebaf0cb2'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('provision_updated_at', sa.DateTime(),
 | 
			
		||||
                  nullable=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_column('nodes', 'provision_updated_at')
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""Add Node.maintenance_reason
 | 
			
		||||
 | 
			
		||||
Revision ID: 242cc6a923b3
 | 
			
		||||
Revises: 487deb87cc9d
 | 
			
		||||
Create Date: 2014-10-15 23:00:43.164061
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '242cc6a923b3'
 | 
			
		||||
down_revision = '487deb87cc9d'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('maintenance_reason',
 | 
			
		||||
                                     sa.Text(),
 | 
			
		||||
                                     nullable=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_column('nodes', 'maintenance_reason')
 | 
			
		||||
@@ -0,0 +1,106 @@
 | 
			
		||||
#
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""initial migration
 | 
			
		||||
 | 
			
		||||
Revision ID: 2581ebaf0cb2
 | 
			
		||||
Revises: None
 | 
			
		||||
Create Date: 2014-01-17 12:14:07.754448
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '2581ebaf0cb2'
 | 
			
		||||
down_revision = None
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    # commands auto generated by Alembic - please adjust!
 | 
			
		||||
    op.create_table(
 | 
			
		||||
        'conductors',
 | 
			
		||||
        sa.Column('created_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('updated_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('id', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('hostname', sa.String(length=255), nullable=False),
 | 
			
		||||
        sa.Column('drivers', sa.Text(), nullable=True),
 | 
			
		||||
        sa.PrimaryKeyConstraint('id'),
 | 
			
		||||
        sa.UniqueConstraint('hostname', name='uniq_conductors0hostname'),
 | 
			
		||||
        mysql_ENGINE='InnoDB',
 | 
			
		||||
        mysql_DEFAULT_CHARSET='UTF8'
 | 
			
		||||
    )
 | 
			
		||||
    op.create_table(
 | 
			
		||||
        'chassis',
 | 
			
		||||
        sa.Column('created_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('updated_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('id', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('uuid', sa.String(length=36), nullable=True),
 | 
			
		||||
        sa.Column('extra', sa.Text(), nullable=True),
 | 
			
		||||
        sa.Column('description', sa.String(length=255), nullable=True),
 | 
			
		||||
        sa.PrimaryKeyConstraint('id'),
 | 
			
		||||
        sa.UniqueConstraint('uuid', name='uniq_chassis0uuid'),
 | 
			
		||||
        mysql_ENGINE='InnoDB',
 | 
			
		||||
        mysql_DEFAULT_CHARSET='UTF8'
 | 
			
		||||
    )
 | 
			
		||||
    op.create_table(
 | 
			
		||||
        'nodes',
 | 
			
		||||
        sa.Column('created_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('updated_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('id', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('uuid', sa.String(length=36), nullable=True),
 | 
			
		||||
        sa.Column('instance_uuid', sa.String(length=36), nullable=True),
 | 
			
		||||
        sa.Column('chassis_id', sa.Integer(), nullable=True),
 | 
			
		||||
        sa.Column('power_state', sa.String(length=15), nullable=True),
 | 
			
		||||
        sa.Column('target_power_state', sa.String(length=15), nullable=True),
 | 
			
		||||
        sa.Column('provision_state', sa.String(length=15), nullable=True),
 | 
			
		||||
        sa.Column('target_provision_state', sa.String(length=15),
 | 
			
		||||
                  nullable=True),
 | 
			
		||||
        sa.Column('last_error', sa.Text(), nullable=True),
 | 
			
		||||
        sa.Column('properties', sa.Text(), nullable=True),
 | 
			
		||||
        sa.Column('driver', sa.String(length=15), nullable=True),
 | 
			
		||||
        sa.Column('driver_info', sa.Text(), nullable=True),
 | 
			
		||||
        sa.Column('reservation', sa.String(length=255), nullable=True),
 | 
			
		||||
        sa.Column('maintenance', sa.Boolean(), nullable=True),
 | 
			
		||||
        sa.Column('extra', sa.Text(), nullable=True),
 | 
			
		||||
        sa.ForeignKeyConstraint(['chassis_id'], ['chassis.id'], ),
 | 
			
		||||
        sa.PrimaryKeyConstraint('id'),
 | 
			
		||||
        sa.UniqueConstraint('uuid', name='uniq_nodes0uuid'),
 | 
			
		||||
        mysql_ENGINE='InnoDB',
 | 
			
		||||
        mysql_DEFAULT_CHARSET='UTF8'
 | 
			
		||||
    )
 | 
			
		||||
    op.create_index('node_instance_uuid', 'nodes', ['instance_uuid'],
 | 
			
		||||
                    unique=False)
 | 
			
		||||
    op.create_table(
 | 
			
		||||
        'ports',
 | 
			
		||||
        sa.Column('created_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('updated_at', sa.DateTime(), nullable=True),
 | 
			
		||||
        sa.Column('id', sa.Integer(), nullable=False),
 | 
			
		||||
        sa.Column('uuid', sa.String(length=36), nullable=True),
 | 
			
		||||
        sa.Column('address', sa.String(length=18), nullable=True),
 | 
			
		||||
        sa.Column('node_id', sa.Integer(), nullable=True),
 | 
			
		||||
        sa.Column('extra', sa.Text(), nullable=True),
 | 
			
		||||
        sa.ForeignKeyConstraint(['node_id'], ['nodes.id'], ),
 | 
			
		||||
        sa.PrimaryKeyConstraint('id'),
 | 
			
		||||
        sa.UniqueConstraint('address', name='uniq_ports0address'),
 | 
			
		||||
        sa.UniqueConstraint('uuid', name='uniq_ports0uuid'),
 | 
			
		||||
        mysql_ENGINE='InnoDB',
 | 
			
		||||
        mysql_DEFAULT_CHARSET='UTF8'
 | 
			
		||||
    )
 | 
			
		||||
    # end Alembic commands
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    raise NotImplementedError(('Downgrade from initial migration is'
 | 
			
		||||
                              ' unsupported.'))
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""increase-node-name-length
 | 
			
		||||
 | 
			
		||||
Revision ID: 2fb93ffd2af1
 | 
			
		||||
Revises: 4f399b21ae71
 | 
			
		||||
Create Date: 2015-03-18 17:08:11.470791
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '2fb93ffd2af1'
 | 
			
		||||
down_revision = '4f399b21ae71'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
from sqlalchemy.dialects import mysql
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.alter_column('nodes', 'name',
 | 
			
		||||
                    existing_type=mysql.VARCHAR(length=63),
 | 
			
		||||
                    type_=sa.String(length=255),
 | 
			
		||||
                    existing_nullable=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.alter_column('nodes', 'name',
 | 
			
		||||
                    existing_type=sa.String(length=255),
 | 
			
		||||
                    type_=mysql.VARCHAR(length=63),
 | 
			
		||||
                    existing_nullable=True)
 | 
			
		||||
@@ -0,0 +1,40 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""Add Node instance info
 | 
			
		||||
 | 
			
		||||
Revision ID: 31baaf680d2b
 | 
			
		||||
Revises: 3cb628139ea4
 | 
			
		||||
Create Date: 2014-03-05 21:09:32.372463
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '31baaf680d2b'
 | 
			
		||||
down_revision = '3cb628139ea4'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    # commands auto generated by Alembic - please adjust
 | 
			
		||||
    op.add_column('nodes', sa.Column('instance_info',
 | 
			
		||||
                                     sa.Text(),
 | 
			
		||||
                                     nullable=True))
 | 
			
		||||
    # end Alembic commands
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    # commands auto generated by Alembic - please adjust
 | 
			
		||||
    op.drop_column('nodes', 'instance_info')
 | 
			
		||||
    # end Alembic commands
 | 
			
		||||
@@ -0,0 +1,37 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""add_logical_name
 | 
			
		||||
 | 
			
		||||
Revision ID: 3ae36a5f5131
 | 
			
		||||
Revises: bb59b63f55a
 | 
			
		||||
Create Date: 2014-12-10 14:27:26.323540
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '3ae36a5f5131'
 | 
			
		||||
down_revision = 'bb59b63f55a'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('name', sa.String(length=63),
 | 
			
		||||
                  nullable=True))
 | 
			
		||||
    op.create_unique_constraint('uniq_nodes0name', 'nodes', ['name'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_constraint('uniq_nodes0name', 'nodes', type_='unique')
 | 
			
		||||
    op.drop_column('nodes', 'name')
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
# Copyright 2014 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.
 | 
			
		||||
 | 
			
		||||
"""add unique constraint to instance_uuid
 | 
			
		||||
 | 
			
		||||
Revision ID: 3bea56f25597
 | 
			
		||||
Revises: 31baaf680d2b
 | 
			
		||||
Create Date: 2014-06-05 11:45:07.046670
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '3bea56f25597'
 | 
			
		||||
down_revision = '31baaf680d2b'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.create_unique_constraint("uniq_nodes0instance_uuid", "nodes",
 | 
			
		||||
                                ["instance_uuid"])
 | 
			
		||||
    op.drop_index('node_instance_uuid', 'nodes')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_constraint("uniq_nodes0instance_uuid", "nodes", type_='unique')
 | 
			
		||||
    op.create_index('node_instance_uuid', 'nodes', ['instance_uuid'])
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""Nodes add console enabled
 | 
			
		||||
 | 
			
		||||
Revision ID: 3cb628139ea4
 | 
			
		||||
Revises: 21b331f883ef
 | 
			
		||||
Create Date: 2014-02-26 11:24:11.318023
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '3cb628139ea4'
 | 
			
		||||
down_revision = '21b331f883ef'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('console_enabled', sa.Boolean))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_column('nodes', 'console_enabled')
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""add conductor_affinity and online
 | 
			
		||||
 | 
			
		||||
Revision ID: 487deb87cc9d
 | 
			
		||||
Revises: 3bea56f25597
 | 
			
		||||
Create Date: 2014-09-26 16:16:30.988900
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '487deb87cc9d'
 | 
			
		||||
down_revision = '3bea56f25597'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column(
 | 
			
		||||
        'conductors',
 | 
			
		||||
        sa.Column('online', sa.Boolean(), default=True))
 | 
			
		||||
    op.add_column(
 | 
			
		||||
        'nodes',
 | 
			
		||||
        sa.Column('conductor_affinity', sa.Integer(),
 | 
			
		||||
                  sa.ForeignKey('conductors.id',
 | 
			
		||||
                                name='nodes_conductor_affinity_fk'),
 | 
			
		||||
                  nullable=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_constraint('nodes_conductor_affinity_fk', 'nodes',
 | 
			
		||||
                       type_='foreignkey')
 | 
			
		||||
    op.drop_column('nodes', 'conductor_affinity')
 | 
			
		||||
    op.drop_column('conductors', 'online')
 | 
			
		||||
@@ -0,0 +1,35 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""Add node.clean_step
 | 
			
		||||
 | 
			
		||||
Revision ID: 4f399b21ae71
 | 
			
		||||
Revises: 1e1d5ace7dc6
 | 
			
		||||
Create Date: 2015-02-18 01:21:46.062311
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '4f399b21ae71'
 | 
			
		||||
down_revision = '1e1d5ace7dc6'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
import sqlalchemy as sa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.add_column('nodes', sa.Column('clean_step', sa.Text(),
 | 
			
		||||
                  nullable=True))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.drop_column('nodes', 'clean_step')
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
#    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.
 | 
			
		||||
 | 
			
		||||
"""replace NOSTATE with AVAILABLE
 | 
			
		||||
 | 
			
		||||
Revision ID: 5674c57409b9
 | 
			
		||||
Revises: 242cc6a923b3
 | 
			
		||||
Create Date: 2015-01-14 16:55:44.718196
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
# revision identifiers, used by Alembic.
 | 
			
		||||
revision = '5674c57409b9'
 | 
			
		||||
down_revision = '242cc6a923b3'
 | 
			
		||||
 | 
			
		||||
from alembic import op
 | 
			
		||||
from sqlalchemy import String
 | 
			
		||||
from sqlalchemy.sql import table, column
 | 
			
		||||
 | 
			
		||||
node = table('nodes',
 | 
			
		||||
             column('uuid', String(36)),
 | 
			
		||||
             column('provision_state', String(15)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE(deva): We must represent the states as static strings in this migration
 | 
			
		||||
# file, rather than import iotronic.common.states, because that file may change
 | 
			
		||||
# in the future. This migration script must still be able to be run with
 | 
			
		||||
# future versions of the code and still produce the same results.
 | 
			
		||||
AVAILABLE = 'available'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upgrade():
 | 
			
		||||
    op.execute(
 | 
			
		||||
        node.update().where(
 | 
			
		||||
            node.c.provision_state == None).values(
 | 
			
		||||
                {'provision_state': op.inline_literal(AVAILABLE)}))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def downgrade():
 | 
			
		||||
    op.execute(
 | 
			
		||||
        node.update().where(
 | 
			
		||||
            node.c.provision_state == op.inline_literal(AVAILABLE)).values(
 | 
			
		||||
                {'provision_state': None}))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user