Merge "Add default VIM key for multi-master Tacker"

This commit is contained in:
Zuul
2025-09-20 10:13:11 +00:00
committed by Gerrit Code Review
9 changed files with 202 additions and 8 deletions

View File

@@ -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.

View File

@@ -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',

View File

@@ -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")

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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')

View File

@@ -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)

View File

@@ -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)