Merge "Add LDAP-related actions in application lifecycle"

This commit is contained in:
Zuul 2023-07-17 13:38:15 +00:00 committed by Gerrit Code Review
commit a98e6464f6
13 changed files with 609 additions and 89 deletions

View File

@ -107,3 +107,5 @@ CEPH_POOL_VOLUMES_CHUNK_SIZE = 512
CEPH_POOL_BACKUP_APP_NAME = 'cinder-backup'
CEPH_POOL_BACKUP_CHUNK_SIZE = 256
OPENSTACK_VOLUME_MOUNT_DIR = "/var/opt/openstack"

View File

@ -0,0 +1,185 @@
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# All Rights Reserved.
#
import subprocess
from typing import List
from oslo_log import log as logging
from sysinv.common import utils as cutils
LOG = logging.getLogger(__name__)
def check_group(group: str) -> bool:
"""Check if group exists.
:returns: bool -- Returns `True` if group exists.
Otherwise, returns `False`.
"""
# First, run `ldapsearch`.
cmd_p1 = [
"ldapsearch",
"-x",
"-b",
"ou=Group,dc=cgcs,dc=local",
f"(cn={group})",
]
p1 = subprocess.Popen(
cmd_p1, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
# Second, verify if the `ldapsearch` output contains the word `numEntries`.
# If it does, it means that the group exists.
cmd_p2 = ["grep", "numEntries"]
p2 = subprocess.Popen(cmd_p2, stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
p2.communicate()
if p2.returncode == 0:
return True
return False
def add_group(group: str) -> None:
"""Add group.
:param group: The group name.
"""
cmd = ["ldapaddgroup", group]
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(f"Failed to add group `{group}`: {stderr}")
def delete_group(group: str) -> None:
"""Delete group.
:param group: The group name.
"""
members = list_group_members(group)
for member in members:
delete_group_member(member, group)
cmd = ["ldapdeletegroup", group]
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(f"Failed to delete group `{group}`: {stderr}")
def check_group_member(member: str, group: str) -> bool:
"""Check if group member exists.
:param member: The member name.
:param group: The group name.
:returns: bool -- Returns `True` if group member exists.
Otherwise, returns `False`.
"""
# First, run `ldapsearch`.
cmd_p1 = [
"ldapsearch",
"-x",
"-b",
"ou=Group,dc=cgcs,dc=local",
f"(cn={group})",
]
p1 = subprocess.Popen(
cmd_p1, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
# Second, verify if the `ldapsearch` output contains the word `memberUid`.
# If it does, it means that the group not only exists, but also contains
# members.
cmd_p2 = ["grep", f"memberUid: {member}"]
p2 = subprocess.Popen(cmd_p2, stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
p2.communicate()
if p2.returncode == 0:
return True
return False
def add_group_member(member: str, group: str) -> None:
"""Add group member.
:param member: The member name.
:param group: The group name.
"""
cmd = ["ldapaddusertogroup", member, group]
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(
f"Failed to add user `{member}` to group `{group}`: {stderr}"
)
def delete_group_member(member: str, group: str) -> None:
"""Delete group member.
:param member: The member name.
:param group: The group name.
"""
cmd = ["ldapdeleteuserfromgroup", member, group]
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(
f"Failed to delete user `{member}` from group `{group}`: {stderr}"
)
def list_group_members(group: str) -> List[str]:
"""List group members.
:param group: The group name.
:returns: List[str] -- The list of members belonging to
the specified group.
"""
members = []
# First, run `ldapsearch`.
cmd_p1 = [
"ldapsearch",
"-x",
"-b",
"ou=Group,dc=cgcs,dc=local",
f"(cn={group})",
]
p1 = subprocess.Popen(
cmd_p1, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
# Second, verify if the `ldapsearch` output contains the word `memberUid`.
# If it does, it means that the group not only exists, but also contains
# members.
cmd_p2 = ["grep", "-Poh", "(?<=memberUid: )(.*)"]
p2 = subprocess.Popen(cmd_p2, stdin=p1.stdout, stdout=subprocess.PIPE)
p1.stdout.close()
stdout, _ = p2.communicate()
if p2.returncode == 0:
for user in stdout.decode().split():
if user:
members.append(user)
return members

View File

@ -1,5 +1,5 @@
#
# Copyright (c) 2021 Wind River Systems, Inc.
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
@ -18,6 +18,8 @@ from sysinv.helm import lifecycle_utils as lifecycle_utils
from sysinv.helm.lifecycle_constants import LifecycleConstants
from k8sapp_openstack import utils as app_utils
from k8sapp_openstack.common import constants as app_constants
from k8sapp_openstack.helpers import ldap
LOG = logging.getLogger(__name__)
@ -97,6 +99,8 @@ class OpenstackAppLifecycleOperator(base.AppLifecycleOperator):
"""
hook_info[LifecycleConstants.EXTRA][self.WAS_APPLIED] = app.active
self._pre_apply_ldap_actions()
def post_apply(self, context, conductor_obj, app, hook_info):
""" Post apply actions
@ -156,6 +160,8 @@ class OpenstackAppLifecycleOperator(base.AppLifecycleOperator):
conductor_obj._update_vim_config(context)
conductor_obj._update_radosgw_config(context)
self._post_remove_ldap_actions()
def _delete_app_specific_resources_post_remove(self, app_op, app, hook_info):
"""Delete application specific resources.
@ -291,3 +297,26 @@ class OpenstackAppLifecycleOperator(base.AppLifecycleOperator):
not installed. Otherwise, False.
"""
return app_utils.https_enabled() and not app_utils.is_openstack_https_certificates_ready()
def _pre_apply_ldap_actions(self):
"""Perform pre apply LDAP-related actions."""
# Create `openstack` group.
has_group = ldap.check_group(app_constants.HELM_NS_OPENSTACK)
if not has_group:
ldap.add_group(app_constants.HELM_NS_OPENSTACK)
# Create the volume mount directory.
app_utils.create_openstack_volume_mount()
def _post_remove_ldap_actions(self):
"""Perform post remove LDAP-related actions."""
# Try to delete the volume mount directory.
# If successful, delete `openstack` group.
# Otherwise, do nothing.
deleted = app_utils.delete_openstack_volume_mount()
if deleted:
has_group = ldap.check_group(app_constants.HELM_NS_OPENSTACK)
if has_group:
ldap.delete_group(app_constants.HELM_NS_OPENSTACK)

View File

@ -1,12 +1,21 @@
#
# Copyright (c) 2022 Wind River Systems, Inc.
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from pathlib import Path
import shutil
from oslo_log import log as logging
from sysinv.common import constants
from sysinv.common import utils as cutils
from sysinv.db import api as dbapi
from k8sapp_openstack.common import constants as app_constants
LOG = logging.getLogger(__name__)
def https_enabled():
db = dbapi.get_instance()
@ -47,3 +56,156 @@ def is_openstack_https_ready():
installed in the system.
"""
return https_enabled() and is_openstack_https_certificates_ready()
def change_file_group_ownership(
file: str,
group: str,
recursive: bool = False
) -> None:
"""Change file's group ownership.
:param file: The file path.
:param group: The group name.
:param recursive: bool -- The flag that indicates whether permissions
should be changed recursively or not.
"""
cmd = ["chgrp", group, file]
if recursive:
cmd.insert(1, "-R")
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(
f"Failed to change group ownership of `{file}` "
f"to `{group}`: {stderr}"
)
def change_file_owner(
file: str,
user: str = "",
group: str = "",
recursive: bool = False
) -> None:
"""Change file's owner (chown).
:param file: The file path.
:param user: The desired user.
:param group: The desired group.
:param recursive: bool -- The flag that indicates whether ownerships
should be changed recursively or not.
"""
ownership = ""
if user:
ownership += user
if group:
ownership += f":{group}"
if not ownership:
return
cmd = ["chown", ownership, file]
if recursive:
cmd.insert(1, "-R")
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(
f"Failed to change ownership of `{file}` "
f"to `{ownership}`: {stderr}"
)
def change_file_mode(
file: str,
mode: str,
recursive: bool = False
) -> None:
"""Change file's mode (chmod).
:param file: The file path.
:param mode: The desired mode.
:param recursive: bool -- The flag that indicates whether modes should be
changed recursively or not.
"""
cmd = ["chmod", mode, file]
if recursive:
cmd.insert(1, "-R")
_, stderr = cutils.trycmd(*cmd, run_as_root=True)
if stderr:
LOG.warning(
f"Failed to change mode of `{file}` to `{mode}`: {stderr}"
)
def create_openstack_volume_mount() -> None:
"""Create OpenStack volume mount directory."""
p = Path(app_constants.OPENSTACK_VOLUME_MOUNT_DIR)
p.mkdir(exist_ok=True)
# Change modes of the volume mount directory.
change_file_mode(
str(p),
mode="770",
recursive=True
)
# Change ownership of the volume mount directory.
change_file_owner(
str(p),
user="sysadmin",
group=app_constants.HELM_NS_OPENSTACK,
recursive=True
)
def delete_openstack_volume_mount() -> bool:
"""Delete OpenStack volume mount.
:returns: bool -- True, if volume mount directory was successfully deleted.
False, if otherwise.
"""
# Search for additional files that might have been created by users.
directories = [app_constants.OPENSTACK_VOLUME_MOUNT_DIR]
while directories:
p = Path(directories.pop(0))
for pathname in p.glob("*"):
if pathname.is_dir():
directories.append(str(pathname))
continue
else:
# Ignore files in the root directory.
if str(p) == app_constants.OPENSTACK_VOLUME_MOUNT_DIR:
continue
# If there is at least one file created by users outside the
# root directory, that is, in a user subdirectory, it means
# that we can't remove the volume mount directory.
LOG.warning(
f"Unable to delete OpenStack volume mount directory "
f"`{app_constants.OPENSTACK_VOLUME_MOUNT_DIR}`. "
f"There are one or more user files in subdirectories."
)
return False
shutil.rmtree(
app_constants.OPENSTACK_VOLUME_MOUNT_DIR, ignore_errors=True
)
return True

View File

@ -1,19 +1,20 @@
#!/bin/bash
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Clears OpenStack service aliases.
#
{{/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
*/}}
SERVICES="
openstack
nova
SERVICES=(
cinder
glance
heat
"
nova
openstack
)
for service in ${SERVICES}; do
for service in "${SERVICES[@]}"; do
unalias "${service}" 2> /dev/null
done

View File

@ -1,21 +1,68 @@
#!/bin/bash
{{/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
*/}}
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Copies setup scripts to the volume mount directory and creates an openrc
# file for admin access to the OpenStack clients container.
#
set -ex
TMP_DIR=/tmp
OPENSTACK_DIR=/var/opt/openstack
OPENSTACK_SCRIPTS=(
/tmp/clear-aliases.sh
/tmp/setup-aliases.sh
/tmp/wrapper.sh
OPENSTACK_SETUP_SCRIPTS=(
clear-aliases.sh
setup-aliases.sh
clients-wrapper.sh
local_openstackrc
)
mkdir -p ${OPENSTACK_DIR}/sysadmin
cp ${OPENSTACK_SCRIPTS[@]} ${OPENSTACK_DIR}
chmod -R 755 ${OPENSTACK_DIR}
# Store ownership of the volume mount directory to use it later on other files.
ownership=$(ls -nd "${OPENSTACK_DIR}" | awk '{print $3":"$4}')
# Copy setup scripts to volume mount directory and adjust their mode/ownership
# to make them only usable by their corresponding owners and/or groups.
for setup_script in "${OPENSTACK_SETUP_SCRIPTS[@]}"; do
cp "${TMP_DIR}/${setup_script}" "${OPENSTACK_DIR}"
chmod 550 "${OPENSTACK_DIR}/${setup_script}"
chown "${ownership}" "${OPENSTACK_DIR}/${setup_script}"
done
# Create openrc file for admin access.
ADMIN_OPENRC="${OPENSTACK_DIR}/admin-openrc"
touch "${ADMIN_OPENRC}"
chmod 600 "${ADMIN_OPENRC}"
chown "${ownership}" "${ADMIN_OPENRC}"
cat << EOF >> "${ADMIN_OPENRC}"
source /etc/platform/openrc --no_credentials
if [[ "$?" -ne 0 ]]; then
return 1
fi
source "${OPENSTACK_DIR}/setup-aliases.sh"
if [[ "$?" -ne 0 ]]; then
return 1
fi
export OS_USERNAME={{ .Values.endpoints.identity.auth.admin.username }}
export OS_PASSWORD={{ .Values.endpoints.identity.auth.admin.password }}
export OS_AUTH_URL=\
{{ .Values.endpoints.identity.scheme.default }}://\
{{ .Values.endpoints.identity.name }}.openstack.svc.\
{{ .Values.endpoints.cluster_domain_suffix }}\
{{ .Values.endpoints.identity.path.default }}
export PS1='[\u@\h \W(keystone_\$OS_USERNAME)]\$ '
return 0
EOF

View File

@ -0,0 +1,69 @@
#!/bin/bash -i
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# OpenStack clients wrapper responsible for executing commands
# passed as arguments in a containerized environment.
#
OPENSTACK_DIR=/var/opt/openstack
OPENSTACK_VARIABLES=(
OS_AUTH_TYPE
OS_AUTH_URL
OS_CACERT
OS_IDENTITY_API_SERVICE
OS_INTERFACE
OS_PASSWORD
OS_PROJECT_DOMAIN_ID
OS_PROJECT_DOMAIN_NAME
OS_PROJECT_ID
OS_PROJECT_NAME
OS_REGION_NAME
OS_USERNAME
OS_USER_DOMAIN_NAME
)
if [[ -z "${KUBECONFIG}" ]]; then
KUBECONFIG=/etc/kubernetes/admin.conf
fi
ENV_ARGUMENTS=()
for variable in "${OPENSTACK_VARIABLES[@]}"; do
if [[ ! -z "$(printenv ${variable})" ]]; then
ENV_ARGUMENTS+=("${variable}=$(printenv ${variable})")
fi
done
CONTROLLER=$(echo "${PS1@P}" | grep -Po 'controller-\d+' | cut -d $'\n' -f 1)
if [[ -z "${CONTROLLER}" ]]; then
echo "OpenStack CLIs can only be accessed from a controller node."
exit 1
fi
POD=$(
kubectl --kubeconfig "${KUBECONFIG}" -n openstack get pods \
| grep -i "clients-${CONTROLLER}.*Running" | awk '{print $1}'
)
if [[ -z "${POD}" ]]; then
echo "Could not find \`clients\` pod in ${CONTROLLER}."
echo "Make sure the pod is running and try again."
exit 1
fi
if grep -q "^${USER}:" /etc/passwd; then
kubectl --kubeconfig "${KUBECONFIG}" -n openstack exec -it "${POD}" \
-c clients -- env ${ENV_ARGUMENTS[@]} /bin/bash -c "$*"
else
if [[ ! -d "${OPENSTACK_DIR}/${USER}" ]]; then
mkdir -p "${OPENSTACK_DIR}/${USER}"
chgrp -R openstack "${OPENSTACK_DIR}/${USER}"
fi
kubectl --kubeconfig "${KUBECONFIG}" -n openstack exec -it "${POD}" \
-c clients -- env ${ENV_ARGUMENTS[@]} /bin/bash -c "cd ${USER}; $*"
fi

View File

@ -0,0 +1,60 @@
#!/bin/bash
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Creates and/or loads local file "~/$USER-openrc-openstack".
#
# Assumes the Keystone username is the same as the logged in username.
#
OPENSTACK_DIR=/var/opt/openstack
OPENSTACK_OPENRC=${HOME}/${USER}-openrc-openstack
# Check if local openrc file exists.
if [[ -e "${OPENSTACK_OPENRC}" ]]; then
# If it does, source it.
source "${OPENSTACK_OPENRC}"
return $?
fi
# Otherwise, create and source it.
read -s -p "Enter password for Keystone user \`${USER}\`: " password
touch "${OPENSTACK_OPENRC}"
chmod 600 "${OPENSTACK_OPENRC}"
cat << EOF >> "${OPENSTACK_OPENRC}"
source /etc/platform/openrc --no_credentials
if [[ "$?" -ne 0 ]]; then
return 1
fi
source "${OPENSTACK_DIR}/setup-aliases.sh"
if [[ "$?" -ne 0 ]]; then
return 1
fi
export OS_USERNAME="${USER}"
export OS_PASSWORD="${password}"
export OS_AUTH_URL=\
{{ .Values.endpoints.identity.scheme.default }}://\
{{ .Values.endpoints.identity.name }}.openstack.svc.\
{{ .Values.endpoints.cluster_domain_suffix }}\
{{ .Values.endpoints.identity.path.default }}
export PS1='[\u@\h \W(keystone_\$OS_USERNAME)]\$ '
return 0
EOF
echo
echo "Created file \`${OPENSTACK_OPENRC}\`."
source "${OPENSTACK_OPENRC}"
return $?

View File

@ -1,25 +1,29 @@
#!/bin/bash
{{/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
*/}}
#
# Copyright (c) 2023 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# Creates OpenStack service aliases.
#
# All aliases created redirect OpenStack commands to a wrapper script,
# which executes them in a containerized environment.
#
if [[ ${BASH_SOURCE} = '/'* ]]; then
PATH_TO_SCRIPT=$(dirname ${BASH_SOURCE})
PATH_TO_SCRIPT=$(dirname "${BASH_SOURCE}")
else
PATH_TO_SCRIPT=$(pwd)/$(dirname ${BASH_SOURCE})
PATH_TO_SCRIPT=$(pwd)/$(dirname "${BASH_SOURCE}")
fi
SERVICES="
openstack
nova
SERVICES=(
cinder
glance
heat
"
nova
openstack
)
for service in ${SERVICES}; do
alias "${service}"="${PATH_TO_SCRIPT}/wrapper.sh ${service}"
for service in "${SERVICES[@]}"; do
alias "${service}"="${PATH_TO_SCRIPT}/clients-wrapper.sh ${service}"
done

View File

@ -1,45 +0,0 @@
#!/bin/bash -i
{{/*
Copyright (c) 2023 Wind River Systems, Inc.
SPDX-License-Identifier: Apache-2.0
*/}}
OPENSTACK_VARIABLES="
OS_AUTH_TYPE
OS_AUTH_URL
OS_CACERT
OS_IDENTITY_API_SERVICE
OS_INTERFACE
OS_PASSWORD
OS_PROJECT_DOMAIN_ID
OS_PROJECT_DOMAIN_NAME
OS_PROJECT_ID
OS_PROJECT_NAME
OS_REGION_NAME
OS_USERNAME
OS_USER_DOMAIN_NAME
"
ENV_ARGUMENTS=()
for variable in ${OPENSTACK_VARIABLES}; do
if [[ ! -z "$(printenv ${variable})" ]]; then
ENV_ARGUMENTS+=("${variable}=$(printenv ${variable})")
fi
done
CONTROLLER=$(echo ${PS1@P} | grep -Po 'controller-\d+')
if [[ -z "${CONTROLLER}" ]]; then
echo "OpenStack CLIs can only be accessed from a controller node."
exit 1
fi
POD=$(kubectl -n openstack get pods | grep -i "clients-${CONTROLLER}.*Running" | awk '{print $1}')
if [[ -z "${POD}" ]]; then
echo "Could not find \`clients\` pod in ${CONTROLLER}."
echo "Make sure the pod is running and try again."
exit 1
fi
kubectl -n openstack exec -it ${POD} -c clients -- env ${ENV_ARGUMENTS[@]} $*

View File

@ -17,12 +17,14 @@ metadata:
data:
clients-init.sh: |
{{ tuple "bin/_clients-init.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
clients-wrapper.sh: |
{{ tuple "bin/_clients-wrapper.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
clear-aliases.sh: |
{{ tuple "bin/_clear-aliases.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
setup-aliases.sh: |
{{ tuple "bin/_setup-aliases.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
wrapper.sh: |
{{ tuple "bin/_wrapper.sh.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
local_openstackrc: |
{{ tuple "bin/_local_openstackrc.tpl" . | include "helm-toolkit.utils.template" | indent 4 }}
{{- end }}
{{- end }}

View File

@ -53,6 +53,10 @@ spec:
mountPath: /tmp/clients-init.sh
subPath: clients-init.sh
readOnly: true
- name: clients-bin
mountPath: /tmp/clients-wrapper.sh
subPath: clients-wrapper.sh
readOnly: true
- name: clients-bin
mountPath: /tmp/clear-aliases.sh
subPath: clear-aliases.sh
@ -62,8 +66,8 @@ spec:
subPath: setup-aliases.sh
readOnly: true
- name: clients-bin
mountPath: /tmp/wrapper.sh
subPath: wrapper.sh
mountPath: /tmp/local_openstackrc
subPath: local_openstackrc
readOnly: true
- name: host-var-opt
mountPath: /var/opt