Configure SQL as helm storage backend

Configmap is the default helmv2 storage backend to store
release information but its 1MB resource limit prevents
scaling up stx openstack worker nodes, so we want to use
SQL as helm storage backend.

To configure SQL backend, generate helm database hieradata
that will be used in puppet to create helm database. The
helm database password is stored in keyring which can be
retrieved in ansible playbook to configure database connection
address.

System upgrade support:
The helm DB is new in the release stx5.0, so a password is
generated for helm user. Helm user and password are written
into the hieradata of release stx5.0. For AIO-SX upgrade,
helm DB is created when applying bootstrap puppet manifest
during ansible upgrade playbook. For two controllers upgrade,
helm DB is created when applying upgrade puppet manifest
during controller-1 upgrade. A migration script is created
to migrate helm releases from configmap to postgresql.

Partial-Bug: 1887677
Depends-On: https://review.opendev.org/#/c/761642/
Change-Id: I2f4f414068af297b5f4a3792c061443b7d3bdb32
Signed-off-by: Angie Wang <angie.wang@windriver.com>
This commit is contained in:
Angie Wang 2020-09-28 11:25:50 -04:00
parent c3d82de2d3
commit efa5f521c3
9 changed files with 258 additions and 23 deletions

View File

@ -778,6 +778,30 @@ def migrate_hiera_data(from_release, to_release, role=None):
if (from_release == SW_VERSION_20_06 and etcd_security_config):
static_config.update(etcd_security_config)
if from_release == SW_VERSION_20_06:
# The helm db is new in the release stx5.0 and requires
# a password to be generated and a new user to access the DB.
# This is required for all types of system upgrade. Should
# removed in the release that follows stx5.0
static_config.update({
'platform::helm::v2::db::postgresql::user': 'admin-helmv2'
})
helmv2_db_pw = utils.get_password_from_keyring('helmv2', 'database')
if not helmv2_db_pw:
helmv2_db_pw = utils.set_password_in_keyring('helmv2', 'database')
secure_static_file = os.path.join(
constants.HIERADATA_PERMDIR, "secure_static.yaml")
with open(secure_static_file, 'r') as yaml_file:
secure_static_config = yaml.load(yaml_file)
secure_static_config.update({
'platform::helm::v2::db::postgresql::password': helmv2_db_pw
})
with open(secure_static_file, 'w') as yaml_file:
yaml.dump(secure_static_config, yaml_file,
default_flow_style=False)
with open(static_file, 'w') as yaml_file:
yaml.dump(static_config, yaml_file, default_flow_style=False)

View File

@ -27,12 +27,13 @@ LOG = log.getLogger(__name__)
def get_upgrade_databases(system_role, shared_services):
UPGRADE_DATABASES = ('postgres', 'template1', 'sysinv',
'barbican', 'fm')
'barbican', 'fm', 'helmv2')
UPGRADE_DATABASE_SKIP_TABLES = {'postgres': (), 'template1': (),
'sysinv': (),
'barbican': (),
'fm': ('alarm',)}
'fm': ('alarm',),
'helmv2': ()}
if system_role == sysinv_constants.DISTRIBUTED_CLOUD_ROLE_SYSTEMCONTROLLER:
UPGRADE_DATABASES += ('dcmanager', 'dcorch',)

View File

@ -27,9 +27,7 @@ from tsconfig.tsconfig import PLATFORM_PATH
from controllerconfig import utils as cutils
from controllerconfig.common import constants
from sysinv.common import constants as sysinv_constants
# sysinv common utils is needed for adding new service account and endpoints
# during upgrade.
# from sysinv.common import utils as sysinv_utils
from sysinv.common import utils as sysinv_utils
from oslo_log import log
@ -114,6 +112,22 @@ def get_password_from_keyring(service, username):
return password
def set_password_in_keyring(service, username):
"""Generate random password and store in keyring"""
os.environ["XDG_DATA_HOME"] = constants.KEYRING_PERMDIR
try:
password = sysinv_utils.generate_random_password(length=16)
keyring.set_password(service, username, password)
except Exception as e:
LOG.exception("Received exception when attempting to generate "
"password for service %s, username %s: %s" %
(service, username, e))
raise
finally:
del os.environ["XDG_DATA_HOME"]
return password
def get_upgrade_token(from_release,
config,
secure_config):

View File

@ -0,0 +1,170 @@
#!/usr/bin/python
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# This migration script is used for migrating all helm releases
# from configmaps to postgresql during the activate stage of
# a platform upgrade.
#
# This script can be removed in the release that follows stx5.0
#
import collections
from datetime import datetime
import psycopg2
import subprocess
import sys
import json
import keyring
from controllerconfig.common import log
LOG = log.get_logger(__name__)
Release = collections.namedtuple(
'release', 'key body name version status owner created_at modified_at')
def main():
action = None
from_release = None
to_release = None
arg = 1
while arg < len(sys.argv):
if arg == 1:
from_release = sys.argv[arg]
elif arg == 2:
to_release = sys.argv[arg]
elif arg == 3:
action = sys.argv[arg]
else:
print ("Invalid option %s." % sys.argv[arg])
return 1
arg += 1
log.configure()
if from_release == '20.06' and action == 'activate':
LOG.info("%s invoked with from_release = %s to_release = %s "
"action = %s"
% (sys.argv[0], from_release, to_release, action))
migrate_helm_releases()
LOG.info("Complete helm releases migration for release %s "
"to %s with action %s."
% (from_release, to_release, action))
def execute_command(cmd):
sub = subprocess.Popen(cmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = sub.communicate()
if sub.returncode != 0:
LOG.error("Command failed:\n %s\n%s\n%s" % (cmd, stdout, stderr))
raise Exception("Failed to execute command: %s" % cmd)
return stdout
def get_helm_releases():
# Get all configmaps that store helm releases
cmd = "kubectl --kubeconfig=/etc/kubernetes/admin.conf get configmaps " \
"-n kube-system -l OWNER=TILLER --sort-by '{.metadata.name}' " \
"--template '{{range .items}}{{.metadata.name}}{{\"\\n\"}}{{end}}'"
releases = execute_command(cmd)
releases_list = [r for r in releases.split('\n') if r]
return releases_list
def delete_helm_releases():
# Delete all configmaps that store helm releases
cmd = "kubectl --kubeconfig=/etc/kubernetes/admin.conf delete configmaps " \
"-n kube-system -l OWNER=TILLER"
execute_command(cmd)
def get_helm_release_from_configmap(release_name):
# Get the content of a specific helm release from configmap
cmd = "kubectl --kubeconfig=/etc/kubernetes/admin.conf get configmaps " \
"-n kube-system {} -o json".format(release_name)
release_data = execute_command(cmd)
return json.loads(release_data)
def map_helm_release(release):
# Map the format of a helm release from configmap to postgresql
try:
key = str(release['metadata']['name'])
body = str(release['data']['release'])
name = str(release['metadata']['labels']['NAME'])
version = int(release['metadata']['labels']['VERSION'])
status = str(release['metadata']['labels']['STATUS'])
owner = str(release['metadata']['labels']['OWNER'])
created_at = int(datetime.strftime(datetime.strptime(
release['metadata']['creationTimestamp'],
"%Y-%m-%dT%H:%M:%SZ"), "%s"))
modified_at = int(release['metadata']['labels']['MODIFIED_AT'])
mapped_release = Release(
key=key, body=body, name=name, version=version, status=status,
owner=owner, created_at=created_at, modified_at=modified_at)
except Exception as e:
LOG.exception("Failed to convert helm release: %s" % e)
raise
return mapped_release
def create_helm_release_in_db(conn, release):
with conn:
with conn.cursor() as cur:
try:
cur.execute(
"insert into releases(key, body, name, version,"
"status, owner, created_at, modified_at) "
"values(%s, %s, %s, %s, %s, %s, %s, %s)",
release)
except psycopg2.IntegrityError:
# release already exists
pass
except Exception as e:
LOG.exception("Failed to create release in db:\n%s" % e)
raise
def migrate_helm_releases():
releases = get_helm_releases()
if not releases:
LOG.info("No helm releases need to be migrated.")
return
LOG.info("Start migrating helm releases:\n%s" % releases)
helmv2_db_pw = keyring.get_password("helmv2", "database")
if not helmv2_db_pw:
raise Exception("Unable to get password to access helmv2 database.")
try:
conn = psycopg2.connect(user="admin-helmv2",
password=helmv2_db_pw,
host="localhost",
database="helmv2")
except Exception as e:
LOG.exception("Failed to connect helmv2 database: %s" % e)
raise
for release in releases:
release_data = get_helm_release_from_configmap(release)
mapped_release = map_helm_release(release_data)
create_helm_release_in_db(conn, mapped_release)
LOG.info("Migrated release: %s" % release)
delete_helm_releases()
if __name__ == "__main__":
sys.exit(main())

View File

@ -65,6 +65,7 @@ systemconfig.puppet_plugins =
035_dockerdistribution = sysinv.puppet.dockerdistribution:DockerDistributionPuppet
036_pciirqaffinity = sysinv.puppet.pci_irq_affinity:PciIrqAffinityPuppet
038_certmon = sysinv.puppet.certmon:CertMonPuppet
039_helm = sysinv.puppet.helm:HelmPuppet
099_service_parameter = sysinv.puppet.service_parameter:ServiceParamPuppet
systemconfig.armada.manifest_ops =

View File

@ -5,6 +5,7 @@
#
import abc
import keyring
import six
from sqlalchemy.orm.exc import NoResultFound
@ -75,6 +76,23 @@ class BasePuppet(object):
def _generate_random_password(length=16):
return utils.generate_random_password(length=length)
def _get_database_password(self, service):
passwords = self.context.setdefault('_database_passwords', {})
if service not in passwords:
passwords[service] = self._get_keyring_password(service,
'database')
return passwords[service]
def _get_database_username(self, service):
return 'admin-%s' % service
def _get_keyring_password(self, service, user):
password = keyring.get_password(service, user)
if not password:
password = self._generate_random_password()
keyring.set_password(service, user, password)
return password
def _get_system(self):
system = self.context.get('_system', None)
if system is None:

View File

@ -0,0 +1,25 @@
#
# Copyright (c) 2020 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from sysinv.puppet import base
class HelmPuppet(base.BasePuppet):
"""Class to encapsulate puppet operations for helm configuration"""
SERVICE_NAME = 'helmv2'
def get_static_config(self):
dbuser = self._get_database_username(self.SERVICE_NAME)
return {
'platform::helm::v2::db::postgresql::user': dbuser,
}
def get_secure_static_config(self):
dbpass = self._get_database_password(self.SERVICE_NAME)
return {
'platform::helm::v2::db::postgresql::password': dbpass,
}

View File

@ -5,7 +5,6 @@
#
import abc
import keyring
import os
from sysinv.common import constants
@ -90,23 +89,6 @@ class OpenstackBasePuppet(base.BasePuppet):
url = service_config.capabilities.get('internal_uri')
return url
def _get_database_password(self, service):
passwords = self.context.setdefault('_database_passwords', {})
if service not in passwords:
passwords[service] = self._get_keyring_password(service,
'database')
return passwords[service]
def _get_database_username(self, service):
return 'admin-%s' % service
def _get_keyring_password(self, service, user):
password = keyring.get_password(service, user)
if not password:
password = self._generate_random_password()
keyring.set_password(service, user, password)
return password
def _get_public_protocol(self):
return 'https' if self._https_enabled() else 'http'