Merge "Add default VIM key for multi-master Tacker"
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
32
tacker/tests/unit/db/test_cli_generate_secret_key.py
Normal file
32
tacker/tests/unit/db/test_cli_generate_secret_key.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
67
tacker/tests/unit/vnfm/test_vim_client_default_key.py
Normal file
67
tacker/tests/unit/vnfm/test_vim_client_default_key.py
Normal 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')
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user