start
This commit is contained in:
parent
8a8af3ca84
commit
68cbf4cee0
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
Loading…
Reference in New Issue
Block a user