From 5bcc805b6ff61656e87d46caef9f6300e616cdd2 Mon Sep 17 00:00:00 2001 From: Hitomi Koba Date: Wed, 27 Aug 2025 22:14:13 +0900 Subject: [PATCH] Add default VIM key for multi-master Tacker Add an option to specify a common default VIM key across Tacker nodes. To enable this, a new `default_secret_key` parameter will be added under `[vim_keys]` in `tacker.conf`. Administrators will generate a default Fernet key file in advance (e.g., `default.key`), place it in the existing `openstack` directory (default: `/etc/tacker/vim/fernet_keys`) on each Tacker node, and specify the filename using the `default_secret_key` option. Implements: blueprint vim-key-for-multi-master Change-Id: Id3c736ef27eb51bca2d4a136eda4af121bce9391 Signed-off-by: Hitomi Koba --- doc/source/install/manual_installation.rst | 40 +++++++++++ tacker/db/migration/cli.py | 17 +++++ tacker/extensions/nfvo.py | 4 ++ tacker/nfvo/drivers/vim/openstack_driver.py | 31 +++++++-- .../unit/db/test_cli_generate_secret_key.py | 32 +++++++++ .../nfvo/drivers/vim/test_openstack_driver.py | 3 +- .../unit/vnfm/test_vim_client_default_key.py | 67 +++++++++++++++++++ tacker/vnfm/keystone.py | 5 +- tacker/vnfm/vim_client.py | 11 ++- 9 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 tacker/tests/unit/db/test_cli_generate_secret_key.py create mode 100644 tacker/tests/unit/vnfm/test_vim_client_default_key.py diff --git a/doc/source/install/manual_installation.rst b/doc/source/install/manual_installation.rst index 54792019c..242dd92f9 100644 --- a/doc/source/install/manual_installation.rst +++ b/doc/source/install/manual_installation.rst @@ -362,6 +362,46 @@ Installing Tacker Server $ cp -r etc/tacker/rootwrap.d/ /etc/tacker/ $ cp etc/tacker/prometheus-plugin.yaml /etc/tacker/ +#. Configure a common VIM Fernet key on multi-node (Optional) + + Use this when you want all Tacker nodes to share a single Fernet key + for encrypting VIM credentials. Skip this if you use Barbican + (``[vim_keys] use_barbican = true``). + + Administrators will generate a default Fernet key file in advance + (e.g., `default.key`), place it in the existing `openstack` directory + (default: `/etc/tacker/vim/fernet_keys`) on each Tacker node, and + specify the filename using the `default_secret_key` option. + + **Generate the key on one node (tacker-1):** + + .. code-block:: console + + $ sudo mkdir /etc/tacker/vim/fernet_keys + $ sudo chmod 700 /etc/tacker/vim/fernet_keys + $ sudo tacker-db-manage generate_secret_key \ + --file /etc/tacker/vim/fernet_keys/default.key + + **Distribute the same key to the other nodes (tacker-2, tacker-3):** + + .. code-block:: console + + $ sudo mkdir /etc/tacker/vim/fernet_keys + $ sudo chmod 700 /etc/tacker/vim/fernet_keys + $ sudo scp tacker-1:/etc/tacker/vim/fernet_keys/default.key \ + /etc/tacker/vim/fernet_keys/default.key + + **Configure ``tacker.conf`` on all nodes:** + + .. code-block:: ini + + [vim_keys] + openstack = /etc/tacker/vim/fernet_keys + default_secret_key = default.key + # use_barbican = false # set to true if you store credentials in Barbican + + After updating the configuration, restart Tacker services on each node. + #. Populate Tacker database. diff --git a/tacker/db/migration/cli.py b/tacker/db/migration/cli.py index 7758b35e6..5dde4f388 100644 --- a/tacker/db/migration/cli.py +++ b/tacker/db/migration/cli.py @@ -18,12 +18,14 @@ from alembic import command as alembic_command from alembic import config as alembic_config from alembic import script as alembic_script from alembic import util as alembic_util +from cryptography import fernet from oslo_config import cfg from tacker._i18n import _ from tacker.db.migration import migrate_to_v2 from tacker.db.migration import purge_tables from tacker.db.migration.models import head # noqa +import warnings HEAD_FILENAME = 'HEAD' @@ -123,6 +125,15 @@ def migrate_to_v2_tables(config, cmd): CONF.command.vnf_id) +def generate_secret_key(config, cmd): + output_file = CONF.command.file + existed = os.path.exists(output_file) + with open(output_file, 'wb') as f: + f.write(fernet.Fernet.generate_key()) + if existed: + warnings.warn(f"Replaced existing file: {output_file}") + + def add_command_parsers(subparsers): for name in ['current', 'history', 'branches']: parser = subparsers.add_parser(name) @@ -199,6 +210,12 @@ def add_command_parsers(subparsers): '--vnf-id', help=_('The specific VNF will be migrated.')) + parser = subparsers.add_parser('generate_secret_key') + parser.add_argument( + '--file', default='/dev/stdout', + help=_('output file path of generated key')) + parser.set_defaults(func=generate_secret_key) + command_opt = cfg.SubCommandOpt('command', title='Command', diff --git a/tacker/extensions/nfvo.py b/tacker/extensions/nfvo.py index da2cd1474..7b54c1b21 100644 --- a/tacker/extensions/nfvo.py +++ b/tacker/extensions/nfvo.py @@ -57,6 +57,10 @@ class VimKeyNotFoundException(exceptions.TackerException): message = _("Unable to find key file for VIM %(vim_id)s") +class DefaultVimKeyNotFoundException(exceptions.TackerException): + message = _("Unable to find default key file") + + class VimEncryptKeyError(exceptions.TackerException): message = _("Barbican must be enabled for VIM %(vim_id)s") diff --git a/tacker/nfvo/drivers/vim/openstack_driver.py b/tacker/nfvo/drivers/vim/openstack_driver.py index 435a04585..3ba2c713b 100644 --- a/tacker/nfvo/drivers/vim/openstack_driver.py +++ b/tacker/nfvo/drivers/vim/openstack_driver.py @@ -41,7 +41,12 @@ OPTS = [cfg.StrOpt('openstack', default='/etc/tacker/vim/fernet_keys', cfg.BoolOpt('use_barbican', default=False, help=_('Use barbican to encrypt vim password if True, ' 'save vim credentials in local file system ' - 'if False')) + 'if False')), + cfg.StrOpt('default_secret_key', default='', + help=("Specify the filename of the default secret key, " + "if available. If not specified, a key will be " + "generated for each vim_id. If a key with the " + "vim_id name exists, it will be used.")), ] # same params as we used in ping monitor driver @@ -204,6 +209,9 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): raise else: key_file = os.path.join(CONF.vim_keys.openstack, vim_id) + if (CONF.vim_keys.default_secret_key != '' and + not os.path.exists(key_file)): + return try: os.remove(key_file) LOG.debug('VIM key deleted successfully for vim %s', @@ -218,11 +226,10 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): Store VIM auth using fernet key encryption """ - fernet_key, fernet_obj = self.keystone.create_fernet_key() - encoded_auth = fernet_obj.encrypt(auth['password'].encode('utf-8')) - auth['password'] = encoded_auth + fernet_obj = None if CONF.vim_keys.use_barbican: + fernet_key, fernet_obj = self.keystone.create_fernet_key() try: k_context = t_context.generate_tacker_service_context() if CONF.ext_oauth2_auth.use_ext_oauth2_auth: @@ -240,8 +247,21 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): LOG.error('VIM key creation failed for vim %s due to %s', vim_id, ex) raise + elif CONF.vim_keys.default_secret_key != '': + key_file = os.path.join(CONF.vim_keys.openstack, + CONF.vim_keys.default_secret_key) + try: + with open(key_file, 'rb') as f: + fernet_key = f.read() + fernet_obj = self.keystone.create_fernet_object(fernet_key) + except FileNotFoundError: + raise nfvo.DefaultVimKeyNotFoundException() + auth['key_type'] = 'fernet_key' + LOG.debug('Use default secret key') else: + fernet_key, fernet_obj = self.keystone.create_fernet_key() + auth['key_type'] = 'fernet_key' key_file = os.path.join(CONF.vim_keys.openstack, vim_id) try: @@ -252,6 +272,9 @@ class OpenStack_Driver(abstract_vim_driver.VimAbstractDriver): except IOError: raise nfvo.VimKeyNotFoundException(vim_id=vim_id) + encoded_auth = fernet_obj.encrypt(auth['password'].encode('utf-8')) + auth['password'] = encoded_auth + @log.log def get_vim_resource_id(self, vim_obj, resource_type, resource_name): """Locates openstack resource by type/name and returns ID diff --git a/tacker/tests/unit/db/test_cli_generate_secret_key.py b/tacker/tests/unit/db/test_cli_generate_secret_key.py new file mode 100644 index 000000000..d7acd66b4 --- /dev/null +++ b/tacker/tests/unit/db/test_cli_generate_secret_key.py @@ -0,0 +1,32 @@ +# Copyright (C) 2025 KDDI +# 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 cryptography.fernet import Fernet +import os +from tacker.db.migration import cli as db_cli +import tempfile +import unittest + + +class TestGenerateSecretKey(unittest.TestCase): + def test_generate_secret_key_writes_fernet_key(self): + with tempfile.TemporaryDirectory() as d: + out = os.path.join(d, "gen.key") + db_cli.CONF( + ['generate_secret_key', '--file', out]) + db_cli.generate_secret_key(None, None) + data = open(out, 'rb').read() + Fernet(data) + self.assertGreaterEqual(len(data), 32) diff --git a/tacker/tests/unit/nfvo/drivers/vim/test_openstack_driver.py b/tacker/tests/unit/nfvo/drivers/vim/test_openstack_driver.py index 75170649a..d7050aeea 100644 --- a/tacker/tests/unit/nfvo/drivers/vim/test_openstack_driver.py +++ b/tacker/tests/unit/nfvo/drivers/vim/test_openstack_driver.py @@ -62,7 +62,8 @@ def get_mock_conf_key_effect(): elif name == 'vim_keys': return MockConfig( conf={ - 'use_barbican': True + 'use_barbican': True, + 'default_secret_key': '' }) else: return cfg.CONF._get(name) diff --git a/tacker/tests/unit/vnfm/test_vim_client_default_key.py b/tacker/tests/unit/vnfm/test_vim_client_default_key.py new file mode 100644 index 000000000..0cf7582ed --- /dev/null +++ b/tacker/tests/unit/vnfm/test_vim_client_default_key.py @@ -0,0 +1,67 @@ +# Copyright (C) 2025 KDDI +# 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 tempfile +import unittest + +from oslo_config import cfg +from tacker.extensions import nfvo +from tacker.vnfm.vim_client import VimClient + + +class TestVIMClientDefaultKey(unittest.TestCase): + + def setUp(self): + self.tmpdir = tempfile.TemporaryDirectory() + + g = cfg.OptGroup('vim_keys') + try: + cfg.CONF.register_group(g) + except cfg.DuplicateOptError: + pass + + opts = [ + cfg.StrOpt('openstack'), + cfg.StrOpt('default_secret_key', default='default.key'), + ] + for opt in opts: + try: + cfg.CONF.register_opt(opt, group=g) + except cfg.DuplicateOptError: + pass + + cfg.CONF.set_override('openstack', self.tmpdir.name, group='vim_keys') + cfg.CONF.set_override( + 'default_secret_key', 'default.key', group='vim_keys') + # create default.key + with open(os.path.join(self.tmpdir.name, 'default.key'), 'w') as f: + f.write('DEFAULTKEY==') + + def tearDown(self): + self.tmpdir.cleanup() + + def test_find_vim_key_prefers_per_vim(self): + with open(os.path.join(self.tmpdir.name, 'VIM-A'), 'w') as f: + f.write('PER_VIM_KEY==') + self.assertEqual('PER_VIM_KEY==', VimClient._find_vim_key('VIM-A')) + + def test_find_vim_key_fallback_to_default(self): + self.assertEqual('DEFAULTKEY==', VimClient._find_vim_key('VIM-B')) + + def test_find_vim_key_raises_when_missing(self): + os.remove(os.path.join(self.tmpdir.name, 'default.key')) + with self.assertRaises(nfvo.VimKeyNotFoundException): + VimClient._find_vim_key('VIM-C') diff --git a/tacker/vnfm/keystone.py b/tacker/vnfm/keystone.py index 2428bd077..1444ed9c6 100644 --- a/tacker/vnfm/keystone.py +++ b/tacker/vnfm/keystone.py @@ -76,5 +76,8 @@ class Keystone(object): def create_fernet_key(self): fernet_key = fernet.Fernet.generate_key() - fernet_obj = fernet.Fernet(fernet_key) + fernet_obj = self.create_fernet_object(fernet_key) return fernet_key, fernet_obj + + def create_fernet_object(self, fernet_key): + return fernet.Fernet(fernet_key) diff --git a/tacker/vnfm/vim_client.py b/tacker/vnfm/vim_client.py index ab8f8393f..eb29e37ac 100644 --- a/tacker/vnfm/vim_client.py +++ b/tacker/vnfm/vim_client.py @@ -139,10 +139,17 @@ class VimClient(object): @staticmethod def _find_vim_key(vim_id): key_file = os.path.join(CONF.vim_keys.openstack, vim_id) - LOG.debug('Attempting to open key file for vim id %s', vim_id) + if not os.path.isfile(key_file): + key_file = os.path.join(CONF.vim_keys.openstack, + CONF.vim_keys.default_secret_key) + LOG.debug('Attempting to open default key file') + else: + LOG.debug('Attempting to open key file for vim id %s', vim_id) + try: with open(key_file, 'r') as f: return f.read() except Exception: - LOG.error('VIM id invalid or key not found for %s', vim_id) + LOG.warning('VIM id invalid or key not found [key_file=%s]', + key_file) raise nfvo.VimKeyNotFoundException(vim_id=vim_id)