Add ConsoleAuthToken object
Console connection authorizations will be saved in the database instead of in memory in a console auth server. The ConsoleAuthToken object has class methods that replace the rpc methods used by the console auth service. To use this object instead of that service the calling code will use these methods instead of call the service rpc API. Co-Authored-By: Eli Qiao <qiaoliyong@gmail.com> Co-Authored-By: melanie witt <melwittt@gmail.com> partially-implements: blueprint convert-consoles-to-objects Change-Id: Ia13591906ab2b3b7b7d5bc8f4b965c117f08fc9b
This commit is contained in:
parent
9554ced8a3
commit
9c458e7956
@ -1896,6 +1896,10 @@ class InvalidToken(Invalid):
|
|||||||
msg_fmt = _("The token '%(token)s' is invalid or has expired")
|
msg_fmt = _("The token '%(token)s' is invalid or has expired")
|
||||||
|
|
||||||
|
|
||||||
|
class TokenInUse(Invalid):
|
||||||
|
msg_fmt = _("The generated token is invalid")
|
||||||
|
|
||||||
|
|
||||||
class InvalidConnectionInfo(Invalid):
|
class InvalidConnectionInfo(Invalid):
|
||||||
msg_fmt = _("Invalid Connection Info")
|
msg_fmt = _("Invalid Connection Info")
|
||||||
|
|
||||||
|
@ -32,6 +32,7 @@ def register_all():
|
|||||||
__import__('nova.objects.cell_mapping')
|
__import__('nova.objects.cell_mapping')
|
||||||
__import__('nova.objects.compute_node')
|
__import__('nova.objects.compute_node')
|
||||||
__import__('nova.objects.diagnostics')
|
__import__('nova.objects.diagnostics')
|
||||||
|
__import__('nova.objects.console_auth_token')
|
||||||
__import__('nova.objects.dns_domain')
|
__import__('nova.objects.dns_domain')
|
||||||
__import__('nova.objects.ec2')
|
__import__('nova.objects.ec2')
|
||||||
__import__('nova.objects.external_event')
|
__import__('nova.objects.external_event')
|
||||||
|
170
nova/objects/console_auth_token.py
Normal file
170
nova/objects/console_auth_token.py
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# Copyright 2015 Intel Corp
|
||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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_db.exception import DBDuplicateEntry
|
||||||
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
from oslo_utils import uuidutils
|
||||||
|
|
||||||
|
import nova.conf
|
||||||
|
from nova import db
|
||||||
|
from nova import exception
|
||||||
|
from nova.i18n import _
|
||||||
|
from nova.objects import base
|
||||||
|
from nova.objects import fields
|
||||||
|
from nova import utils
|
||||||
|
|
||||||
|
CONF = nova.conf.CONF
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@base.NovaObjectRegistry.register
|
||||||
|
class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
|
||||||
|
# Version 1.0: Initial version
|
||||||
|
VERSION = '1.0'
|
||||||
|
|
||||||
|
fields = {
|
||||||
|
'id': fields.IntegerField(),
|
||||||
|
'console_type': fields.StringField(nullable=False),
|
||||||
|
'host': fields.StringField(nullable=False),
|
||||||
|
'port': fields.IntegerField(nullable=False),
|
||||||
|
'internal_access_path': fields.StringField(nullable=True),
|
||||||
|
'instance_uuid': fields.UUIDField(nullable=False),
|
||||||
|
'access_url_base': fields.StringField(nullable=True),
|
||||||
|
# NOTE(PaulMurray): The unhashed token field is not stored in the
|
||||||
|
# database. A hash of the token is stored instead and is not a
|
||||||
|
# field on the object.
|
||||||
|
'token': fields.StringField(nullable=False),
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def access_url(self):
|
||||||
|
"""The access url with token parameter.
|
||||||
|
|
||||||
|
:returns: the access url with credential parameters
|
||||||
|
|
||||||
|
access_url_base is the base url used to access a console.
|
||||||
|
Adding the unhashed token as a parameter in a query string makes it
|
||||||
|
specific to this authorization.
|
||||||
|
"""
|
||||||
|
if self.obj_attr_is_set('id'):
|
||||||
|
return '%s?token=%s' % (self.access_url_base, self.token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _from_db_object(context, obj, db_obj):
|
||||||
|
# NOTE(PaulMurray): token is not stored in the database but
|
||||||
|
# this function assumes it is in db_obj. The unhashed token
|
||||||
|
# field is populated in the authorize method after the token
|
||||||
|
# authorization is created in the database.
|
||||||
|
for field in obj.fields:
|
||||||
|
setattr(obj, field, db_obj[field])
|
||||||
|
obj._context = context
|
||||||
|
obj.obj_reset_changes()
|
||||||
|
return obj
|
||||||
|
|
||||||
|
@base.remotable
|
||||||
|
def authorize(self, ttl):
|
||||||
|
"""Authorise the console token and store in the database.
|
||||||
|
|
||||||
|
:param ttl: time to live in seconds
|
||||||
|
:returns: an authorized token
|
||||||
|
|
||||||
|
The expires value is set for ttl seconds in the future and the token
|
||||||
|
hash is stored in the database. This function can only succeed if the
|
||||||
|
token is unique and the object has not already been stored.
|
||||||
|
"""
|
||||||
|
if self.obj_attr_is_set('id'):
|
||||||
|
raise exception.ObjectActionError(
|
||||||
|
action='authorize',
|
||||||
|
reason=_('must be a new object to authorize'))
|
||||||
|
|
||||||
|
token = uuidutils.generate_uuid()
|
||||||
|
token_hash = utils.get_sha256_str(token)
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
updates = self.obj_get_changes()
|
||||||
|
# NOTE(melwitt): token could be in the updates if authorize() has been
|
||||||
|
# called twice on the same object. 'token' is not a database column and
|
||||||
|
# should not be included in the call to create the database record.
|
||||||
|
if 'token' in updates:
|
||||||
|
del updates['token']
|
||||||
|
updates['token_hash'] = token_hash
|
||||||
|
updates['expires'] = expires
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_obj = db.console_auth_token_create(self._context, updates)
|
||||||
|
db_obj['token'] = token
|
||||||
|
self._from_db_object(self._context, self, db_obj)
|
||||||
|
except DBDuplicateEntry:
|
||||||
|
# NOTE(PaulMurray) we are generating the token above so this
|
||||||
|
# should almost never happen - but technically its possible
|
||||||
|
raise exception.TokenInUse()
|
||||||
|
|
||||||
|
LOG.debug("Authorized token with expiry %(expires)s for console "
|
||||||
|
"connection %(console)s",
|
||||||
|
{'expires': expires,
|
||||||
|
'console': strutils.mask_password(self)})
|
||||||
|
return token
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def validate(cls, context, token):
|
||||||
|
"""Validate the token.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param token: the token for the authorization
|
||||||
|
:returns: The ConsoleAuthToken object if valid
|
||||||
|
|
||||||
|
The token is valid if the token is in the database and the expires
|
||||||
|
time has not passed.
|
||||||
|
"""
|
||||||
|
token_hash = utils.get_sha256_str(token)
|
||||||
|
db_obj = db.console_auth_token_get_valid(context, token_hash)
|
||||||
|
|
||||||
|
if db_obj is not None:
|
||||||
|
db_obj['token'] = token
|
||||||
|
obj = cls._from_db_object(context, cls(), db_obj)
|
||||||
|
LOG.debug("Validated token - console connection is "
|
||||||
|
"%(console)s",
|
||||||
|
{'console': strutils.mask_password(obj)})
|
||||||
|
return obj
|
||||||
|
else:
|
||||||
|
LOG.debug("Token validation failed")
|
||||||
|
raise exception.InvalidToken(token='***')
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def clean_console_auths_for_instance(cls, context, instance_uuid):
|
||||||
|
"""Remove all console authorizations for the instance.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param instance_uuid: the instance to be cleaned
|
||||||
|
|
||||||
|
All authorizations related to the specified instance will be
|
||||||
|
removed from the database.
|
||||||
|
"""
|
||||||
|
db.console_auth_token_destroy_all_by_instance(context, instance_uuid)
|
||||||
|
|
||||||
|
@base.remotable_classmethod
|
||||||
|
def clean_expired_console_auths_for_host(cls, context, host):
|
||||||
|
"""Remove all expired console authorizations for the host.
|
||||||
|
|
||||||
|
:param context: the context
|
||||||
|
:param host: the host name
|
||||||
|
|
||||||
|
All expired authorizations related to the specified host
|
||||||
|
will be removed. Tokens that have not expired will
|
||||||
|
remain.
|
||||||
|
"""
|
||||||
|
db.console_auth_token_destroy_expired_by_host(context, host)
|
57
nova/tests/functional/db/test_console_auth_token.py
Normal file
57
nova/tests/functional/db/test_console_auth_token.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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_versionedobjects import fixture as ovo_fixture
|
||||||
|
|
||||||
|
from nova import context
|
||||||
|
from nova import exception
|
||||||
|
from nova import objects
|
||||||
|
from nova import test
|
||||||
|
from nova.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleAuthTokenTestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ConsoleAuthTokenTestCase, self).setUp()
|
||||||
|
self.context = context.RequestContext('fake-user', 'fake-project')
|
||||||
|
instance = objects.Instance(
|
||||||
|
context=self.context,
|
||||||
|
project_id=self.context.project_id,
|
||||||
|
uuid=uuidsentinel.fake_instance)
|
||||||
|
instance.create()
|
||||||
|
self.console = objects.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
instance_uuid=uuidsentinel.fake_instance,
|
||||||
|
console_type='fake-type',
|
||||||
|
host='fake-host',
|
||||||
|
port=1000,
|
||||||
|
internal_access_path='fake-internal_access_path',
|
||||||
|
access_url_base='fake-external_access_path'
|
||||||
|
)
|
||||||
|
self.token = self.console.authorize(100)
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
connection_info = objects.ConsoleAuthToken.validate(
|
||||||
|
self.context, self.token)
|
||||||
|
expected = self.console.obj_to_primitive()['nova_object.data']
|
||||||
|
del expected['created_at']
|
||||||
|
ovo_fixture.compare_obj(self, connection_info, expected,
|
||||||
|
allow_missing=['created_at'])
|
||||||
|
|
||||||
|
def test_validate_invalid(self):
|
||||||
|
unauthorized_token = uuidsentinel.token
|
||||||
|
self.assertRaises(
|
||||||
|
exception.InvalidToken,
|
||||||
|
objects.ConsoleAuthToken.validate,
|
||||||
|
self.context, unauthorized_token)
|
165
nova/tests/unit/objects/test_console_auth_token.py
Normal file
165
nova/tests/unit/objects/test_console_auth_token.py
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
# Copyright 2016 Intel Corp.
|
||||||
|
# Copyright 2016 Hewlett Packard Enterprise Development Company LP
|
||||||
|
#
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from oslo_db.exception import DBDuplicateEntry
|
||||||
|
from oslo_utils import timeutils
|
||||||
|
|
||||||
|
from nova import exception
|
||||||
|
from nova.objects import console_auth_token as token_obj
|
||||||
|
from nova.tests.unit import fake_console_auth_token as fakes
|
||||||
|
from nova.tests.unit.objects import test_objects
|
||||||
|
from nova.tests import uuidsentinel
|
||||||
|
|
||||||
|
|
||||||
|
class _TestConsoleAuthToken(object):
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize(self, mock_create):
|
||||||
|
# the expires time is calculated from the current time and
|
||||||
|
# a ttl value in the object. Fix the current time so we can
|
||||||
|
# test expires is calculated correctly as expected
|
||||||
|
self.addCleanup(timeutils.clear_time_override)
|
||||||
|
timeutils.set_time_override()
|
||||||
|
ttl = 10
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
db_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
db_dict['expires'] = expires
|
||||||
|
mock_create.return_value = db_dict
|
||||||
|
|
||||||
|
create_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
create_dict['expires'] = expires
|
||||||
|
del create_dict['id']
|
||||||
|
del create_dict['created_at']
|
||||||
|
del create_dict['updated_at']
|
||||||
|
|
||||||
|
expected = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
del expected['token_hash']
|
||||||
|
del expected['expires']
|
||||||
|
expected['token'] = fakes.fake_token
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
with mock.patch('uuid.uuid4', return_value=fakes.fake_token):
|
||||||
|
token = obj.authorize(ttl)
|
||||||
|
|
||||||
|
mock_create.assert_called_once_with(self.context, create_dict)
|
||||||
|
self.assertEqual(token, fakes.fake_token)
|
||||||
|
self.compare_obj(obj, expected)
|
||||||
|
|
||||||
|
url = obj.access_url
|
||||||
|
expected_url = '%s?token=%s' % (
|
||||||
|
fakes.fake_token_dict['access_url_base'],
|
||||||
|
fakes.fake_token)
|
||||||
|
self.assertEqual(expected_url, url)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_duplicate_token(self, mock_create):
|
||||||
|
mock_create.side_effect = DBDuplicateEntry()
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(exception.TokenInUse,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_instance_not_found(self, mock_create):
|
||||||
|
mock_create.side_effect = exception.InstanceNotFound(
|
||||||
|
instance_id=fakes.fake_token_dict['instance_uuid'])
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertRaises(exception.InstanceNotFound,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_create')
|
||||||
|
def test_authorize_object_already_created(self, mock_create):
|
||||||
|
# the expires time is calculated from the current time and
|
||||||
|
# a ttl value in the object. Fix the current time so we can
|
||||||
|
# test expires is calculated correctly as expected
|
||||||
|
self.addCleanup(timeutils.clear_time_override)
|
||||||
|
timeutils.set_time_override()
|
||||||
|
ttl = 10
|
||||||
|
expires = timeutils.utcnow_ts() + ttl
|
||||||
|
|
||||||
|
db_dict = copy.deepcopy(fakes.fake_token_dict)
|
||||||
|
db_dict['expires'] = expires
|
||||||
|
mock_create.return_value = db_dict
|
||||||
|
|
||||||
|
obj = token_obj.ConsoleAuthToken(
|
||||||
|
context=self.context,
|
||||||
|
console_type=fakes.fake_token_dict['console_type'],
|
||||||
|
host=fakes.fake_token_dict['host'],
|
||||||
|
port=fakes.fake_token_dict['port'],
|
||||||
|
internal_access_path=fakes.fake_token_dict['internal_access_path'],
|
||||||
|
instance_uuid=fakes.fake_token_dict['instance_uuid'],
|
||||||
|
access_url_base=fakes.fake_token_dict['access_url_base'],
|
||||||
|
)
|
||||||
|
obj.authorize(100)
|
||||||
|
self.assertRaises(exception.ObjectActionError,
|
||||||
|
obj.authorize,
|
||||||
|
100)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_destroy_all_by_instance')
|
||||||
|
def test_clean_console_auths_for_instance(self, mock_destroy):
|
||||||
|
token_obj.ConsoleAuthToken.clean_console_auths_for_instance(
|
||||||
|
self.context, uuidsentinel.instance)
|
||||||
|
mock_destroy.assert_called_once_with(
|
||||||
|
self.context, uuidsentinel.instance)
|
||||||
|
|
||||||
|
@mock.patch('nova.db.console_auth_token_destroy_expired_by_host')
|
||||||
|
def test_clean_expired_console_auths_for_host(self, mock_destroy):
|
||||||
|
token_obj.ConsoleAuthToken.clean_expired_console_auths_for_host(
|
||||||
|
self.context, 'fake-host')
|
||||||
|
mock_destroy.assert_called_once_with(
|
||||||
|
self.context, 'fake-host')
|
||||||
|
|
||||||
|
|
||||||
|
class TestConsoleAuthToken(test_objects._LocalTest,
|
||||||
|
_TestConsoleAuthToken):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestRemoteConsoleAuthToken(test_objects._RemoteTest,
|
||||||
|
_TestConsoleAuthToken):
|
||||||
|
pass
|
@ -1073,6 +1073,7 @@ object_data = {
|
|||||||
'ComputeNode': '1.18-431fafd8ac4a5f3559bd9b1f1332cc22',
|
'ComputeNode': '1.18-431fafd8ac4a5f3559bd9b1f1332cc22',
|
||||||
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
||||||
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
||||||
|
'ConsoleAuthToken': '1.0-a61bf7b54517c4013a12289c5a5268ea',
|
||||||
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
|
'DNSDomain': '1.0-7b0b2dab778454b6a7b6c66afe163a1a',
|
||||||
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
|
'DNSDomainList': '1.0-4ee0d9efdfd681fed822da88376e04d2',
|
||||||
'Destination': '1.1-fff0853f3acec6b04ddc03158ded11ba',
|
'Destination': '1.1-fff0853f3acec6b04ddc03158ded11ba',
|
||||||
|
Loading…
Reference in New Issue
Block a user