Merge "Introduce support for multiple application bundles"
This commit is contained in:
commit
9e0c55868d
|
@ -137,8 +137,12 @@ if [ "$ACTION" == "activate" ]; then
|
||||||
sleep $RECOVER_RESULT_SLEEP
|
sleep $RECOVER_RESULT_SLEEP
|
||||||
done
|
done
|
||||||
|
|
||||||
|
# Sort applications by version. Lower versions are attempted first.
|
||||||
|
APPS_SORTED_BY_VERSION=$(find $PLATFORM_APPLICATION_PATH/* | sort -V)
|
||||||
|
|
||||||
|
LAST_APP_CHECKED=""
|
||||||
# Get the list of applications installed in the new release
|
# Get the list of applications installed in the new release
|
||||||
for fqpn_app in $PLATFORM_APPLICATION_PATH/*; do
|
for fqpn_app in $APPS_SORTED_BY_VERSION; do
|
||||||
# Extract the app name and version from the tarball name: app_name-version.tgz
|
# Extract the app name and version from the tarball name: app_name-version.tgz
|
||||||
re='^(.*)-([0-9]+\.[0-9]+-[0-9]+).tgz'
|
re='^(.*)-([0-9]+\.[0-9]+-[0-9]+).tgz'
|
||||||
[[ "$(basename $fqpn_app)" =~ $re ]]
|
[[ "$(basename $fqpn_app)" =~ $re ]]
|
||||||
|
@ -146,6 +150,18 @@ if [ "$ACTION" == "activate" ]; then
|
||||||
UPGRADE_APP_VERSION=${BASH_REMATCH[2]}
|
UPGRADE_APP_VERSION=${BASH_REMATCH[2]}
|
||||||
log "$NAME: Found application ${UPGRADE_APP_NAME}, version ${UPGRADE_APP_VERSION} at $fqpn_app"
|
log "$NAME: Found application ${UPGRADE_APP_NAME}, version ${UPGRADE_APP_VERSION} at $fqpn_app"
|
||||||
|
|
||||||
|
# Confirm application is loaded.
|
||||||
|
EXISTING_APP_NAME=$(system application-show $UPGRADE_APP_NAME --column name --format value)
|
||||||
|
if [ -z "${EXISTING_APP_NAME}" ]; then
|
||||||
|
log "$NAME: ${UPGRADE_APP_NAME} is currently not uploaded in the system. skipping..."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If the last iteration for the same app was sucessful no further updates are necessary
|
||||||
|
if [ "${LAST_APP_CHECKED}" == "${UPGRADE_APP_NAME}" ] && [[ "${EXISTING_APP_STATUS}" =~ ^(uploaded|applied)$ ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
# Confirm application is upgradable
|
# Confirm application is upgradable
|
||||||
# TODO: move nginx back to the supported platform applications list when
|
# TODO: move nginx back to the supported platform applications list when
|
||||||
# fluxcd application upgrade is supported
|
# fluxcd application upgrade is supported
|
||||||
|
@ -156,13 +172,6 @@ if [ "$ACTION" == "activate" ]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Confirm application is loaded.
|
|
||||||
EXISTING_APP_NAME=$(system application-show $UPGRADE_APP_NAME --column name --format value)
|
|
||||||
if [ -z "${EXISTING_APP_NAME}" ]; then
|
|
||||||
log "$NAME: ${UPGRADE_APP_NAME} is currently not uploaded in the system. skipping..."
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get the existing application details
|
# Get the existing application details
|
||||||
EXISTING_APP_INFO=$(system application-show $EXISTING_APP_NAME --column app_version --column status --format yaml)
|
EXISTING_APP_INFO=$(system application-show $EXISTING_APP_NAME --column app_version --column status --format yaml)
|
||||||
EXISTING_APP_VERSION=$(echo ${EXISTING_APP_INFO} | sed 's/.*app_version:[[:space:]]\(\S*\).*/\1/')
|
EXISTING_APP_VERSION=$(echo ${EXISTING_APP_INFO} | sed 's/.*app_version:[[:space:]]\(\S*\).*/\1/')
|
||||||
|
@ -174,7 +183,7 @@ if [ "$ACTION" == "activate" ]; then
|
||||||
# If the app is in uploaded or applied state, then we continue with next iteration.
|
# If the app is in uploaded or applied state, then we continue with next iteration.
|
||||||
# Else, the code execution proceeds and the script would exit with an unexpected state.
|
# Else, the code execution proceeds and the script would exit with an unexpected state.
|
||||||
if [[ "${EXISTING_APP_STATUS}" =~ ^(uploaded|applied)$ ]]; then
|
if [[ "${EXISTING_APP_STATUS}" =~ ^(uploaded|applied)$ ]]; then
|
||||||
log "$NAME: ${UPGRADE_APP_NAME}, version ${EXISTING_APP_VERSION}, is already present. skipping..."
|
log "$NAME: ${UPGRADE_APP_NAME}, version ${EXISTING_APP_VERSION}, is already present. Skipping..."
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
@ -242,6 +251,8 @@ if [ "$ACTION" == "activate" ]; then
|
||||||
if ! grep -q "${EXISTING_APP_NAME},${EXISTING_APP_VERSION},${UPGRADE_APP_VERSION}" $UPGRADE_IN_PROGRESS_APPS_FILE; then
|
if ! grep -q "${EXISTING_APP_NAME},${EXISTING_APP_VERSION},${UPGRADE_APP_VERSION}" $UPGRADE_IN_PROGRESS_APPS_FILE; then
|
||||||
echo "${EXISTING_APP_NAME},${EXISTING_APP_VERSION},${UPGRADE_APP_VERSION}" >> $UPGRADE_IN_PROGRESS_APPS_FILE
|
echo "${EXISTING_APP_NAME},${EXISTING_APP_VERSION},${UPGRADE_APP_VERSION}" >> $UPGRADE_IN_PROGRESS_APPS_FILE
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
LAST_APP_CHECKED=${UPGRADE_APP_NAME}
|
||||||
done
|
done
|
||||||
|
|
||||||
log "$NAME: Completed Kubernetes application updates for release $FROM_RELEASE to $TO_RELEASE with action $ACTION"
|
log "$NAME: Completed Kubernetes application updates for release $FROM_RELEASE to $TO_RELEASE with action $ACTION"
|
||||||
|
|
|
@ -10,16 +10,18 @@
|
||||||
import io
|
import io
|
||||||
import glob
|
import glob
|
||||||
import os
|
import os
|
||||||
|
import ruamel.yaml
|
||||||
import shutil
|
import shutil
|
||||||
import six
|
import six
|
||||||
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
|
||||||
from sysinv._i18n import _
|
from sysinv._i18n import _
|
||||||
from sysinv.common import constants
|
from sysinv.common import constants
|
||||||
from sysinv.common import exception
|
from sysinv.common import exception
|
||||||
|
from sysinv.common import kubernetes
|
||||||
from sysinv.common import utils
|
from sysinv.common import utils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
@ -562,3 +564,78 @@ def verify_application(path: str) -> bool:
|
||||||
"metadata verification failed. {}".format(e)))
|
"metadata verification failed. {}".format(e)))
|
||||||
|
|
||||||
return is_verified
|
return is_verified
|
||||||
|
|
||||||
|
|
||||||
|
def extract_bundle_metadata(file_path):
|
||||||
|
"""Extract metadata from a given tarball
|
||||||
|
|
||||||
|
:param file_path: Application bundle file path
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
tarball = tarfile.open(file_path)
|
||||||
|
metadata_yaml_path = "./{}".format(constants.APP_METADATA_FILE)
|
||||||
|
tarball.getmember(metadata_yaml_path)
|
||||||
|
|
||||||
|
with tarball.extractfile(metadata_yaml_path) as metadata_file:
|
||||||
|
metadata = ruamel.yaml.load(metadata_file,
|
||||||
|
Loader=ruamel.yaml.RoundTripLoader,
|
||||||
|
preserve_quotes=True)
|
||||||
|
|
||||||
|
minimum_supported_k8s_version = metadata.get(
|
||||||
|
constants.APP_METADATA_SUPPORTED_K8S_VERSION, {}).get(
|
||||||
|
constants.APP_METADATA_MINIMUM, None)
|
||||||
|
|
||||||
|
if minimum_supported_k8s_version is None:
|
||||||
|
# TODO(ipiresso): Turn this into an error message rather than
|
||||||
|
# a warning when the k8s app upgrade implementation is in place
|
||||||
|
# and remove the hardcoded value. Also, do not add the bundle to
|
||||||
|
# the database in this scenario.
|
||||||
|
LOG.warning("Minimum supported Kubernetes version missing from {}"
|
||||||
|
.format(file_path))
|
||||||
|
minimum_supported_k8s_version = kubernetes.get_kube_versions()[0]['version']
|
||||||
|
|
||||||
|
minimum_supported_k8s_version = minimum_supported_k8s_version.strip().lstrip('v')
|
||||||
|
|
||||||
|
maximum_supported_k8s_version = metadata.get(
|
||||||
|
constants.APP_METADATA_SUPPORTED_K8S_VERSION, {}).get(
|
||||||
|
constants.APP_METADATA_MAXIMUM, None)
|
||||||
|
|
||||||
|
if maximum_supported_k8s_version is not None:
|
||||||
|
maximum_supported_k8s_version = maximum_supported_k8s_version.strip().lstrip('v')
|
||||||
|
|
||||||
|
k8s_upgrades = metadata.get(constants.APP_METADATA_K8S_UPGRADES, None)
|
||||||
|
if k8s_upgrades is None:
|
||||||
|
k8s_auto_update = constants.APP_METADATA_K8S_AUTO_UPDATE_DEFAULT_VALUE
|
||||||
|
k8s_update_timing = constants.APP_METADATA_TIMING_DEFAULT_VALUE
|
||||||
|
LOG.warning("k8s_upgrades section missing from {} metadata"
|
||||||
|
.format(file_path))
|
||||||
|
else:
|
||||||
|
k8s_auto_update = tarball.metadata.get(
|
||||||
|
constants.APP_METADATA_K8S_UPGRADES).get(
|
||||||
|
constants.APP_METADATA_AUTO_UPDATE,
|
||||||
|
constants.APP_METADATA_K8S_AUTO_UPDATE_DEFAULT_VALUE)
|
||||||
|
k8s_update_timing = tarball.metadata.get(
|
||||||
|
constants.APP_METADATA_K8S_UPGRADES).get(
|
||||||
|
constants.APP_METADATA_TIMING,
|
||||||
|
constants.APP_METADATA_TIMING_DEFAULT_VALUE)
|
||||||
|
|
||||||
|
bundle_data = {
|
||||||
|
'name': metadata.get(constants.APP_METADATA_NAME),
|
||||||
|
'version': metadata.get(constants.APP_METADATA_VERSION),
|
||||||
|
'file_path': file_path,
|
||||||
|
'auto_update':
|
||||||
|
metadata.get(constants.APP_METADATA_UPGRADES, {}).get(
|
||||||
|
constants.APP_METADATA_AUTO_UPDATE,
|
||||||
|
constants.APP_METADATA_AUTO_UPDATE_DEFAULT_VALUE),
|
||||||
|
'k8s_auto_update': k8s_auto_update,
|
||||||
|
'k8s_timing': k8s_update_timing,
|
||||||
|
'k8s_minimum_version': minimum_supported_k8s_version,
|
||||||
|
'k8s_maximum_version': maximum_supported_k8s_version
|
||||||
|
}
|
||||||
|
|
||||||
|
return bundle_data
|
||||||
|
except KeyError:
|
||||||
|
LOG.warning("Application bundle {} does not contain a metadata file.".format(file_path))
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
|
|
@ -1813,7 +1813,8 @@ APP_PENDING_REAPPLY_FLAG = os.path.join(
|
||||||
|
|
||||||
# FluxCD
|
# FluxCD
|
||||||
APP_FLUXCD_MANIFEST_DIR = 'fluxcd-manifests'
|
APP_FLUXCD_MANIFEST_DIR = 'fluxcd-manifests'
|
||||||
APP_FLUXCD_DATA_PATH = os.path.join(tsc.PLATFORM_PATH, 'fluxcd', tsc.SW_VERSION)
|
APP_FLUXCD_BASE_PATH = os.path.join(tsc.PLATFORM_PATH, 'fluxcd')
|
||||||
|
APP_FLUXCD_DATA_PATH = os.path.join(APP_FLUXCD_BASE_PATH, tsc.SW_VERSION)
|
||||||
APP_ROOT_KUSTOMIZE_FILE = 'kustomization.yaml'
|
APP_ROOT_KUSTOMIZE_FILE = 'kustomization.yaml'
|
||||||
APP_HELMREPOSITORY_FILE = "helmrepository.yaml"
|
APP_HELMREPOSITORY_FILE = "helmrepository.yaml"
|
||||||
APP_BASE_HELMREPOSITORY_FILE = os.path.join("base", APP_HELMREPOSITORY_FILE)
|
APP_BASE_HELMREPOSITORY_FILE = os.path.join("base", APP_HELMREPOSITORY_FILE)
|
||||||
|
@ -2399,6 +2400,13 @@ OS_DEBIAN = 'debian'
|
||||||
SUPPORTED_OS_TYPES = [OS_CENTOS, OS_DEBIAN]
|
SUPPORTED_OS_TYPES = [OS_CENTOS, OS_DEBIAN]
|
||||||
OS_UPGRADE_FEED_FOLDER = '/var/www/pages/feed/'
|
OS_UPGRADE_FEED_FOLDER = '/var/www/pages/feed/'
|
||||||
|
|
||||||
|
# OSTree
|
||||||
|
OSTREE_ROOT_FOLDER = '/sysroot/ostree/'
|
||||||
|
OSTREE_LOCK_FILE = 'lock'
|
||||||
|
|
||||||
|
# INotify
|
||||||
|
INOTIFY_DELETE_EVENT = 'DELETE'
|
||||||
|
|
||||||
# Configuration support placeholders
|
# Configuration support placeholders
|
||||||
CONFIGURABLE = 'configurable'
|
CONFIGURABLE = 'configurable'
|
||||||
NOT_CONFIGURABLE = 'not-configurable'
|
NOT_CONFIGURABLE = 'not-configurable'
|
||||||
|
|
|
@ -0,0 +1,274 @@
|
||||||
|
# Based on inotify_simple: https://github.com/chrisjbillington/inotify_simple
|
||||||
|
# Licensed under the BSD 2-Clause "Simplified" License
|
||||||
|
#
|
||||||
|
# Copyright (c) 2016, Chris Billington
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||||
|
# list of conditions and the following disclaimer.
|
||||||
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions and the following disclaimer in the documentation
|
||||||
|
# and/or other materials provided wi6h the distribution.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||||
|
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
#
|
||||||
|
# Copyright (c) 2024 Wind River Systems, Inc.
|
||||||
|
#
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import os
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
from ctypes import CDLL
|
||||||
|
from ctypes import get_errno
|
||||||
|
from ctypes import c_int
|
||||||
|
from ctypes.util import find_library
|
||||||
|
from enum import IntEnum
|
||||||
|
from errno import EINTR
|
||||||
|
from fcntl import ioctl
|
||||||
|
from io import FileIO
|
||||||
|
from os import fsencode
|
||||||
|
from os import fsdecode
|
||||||
|
from select import select
|
||||||
|
from struct import calcsize
|
||||||
|
from struct import unpack_from
|
||||||
|
from termios import FIONREAD
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
__version__ = '1.3.5'
|
||||||
|
|
||||||
|
__all__ = ['Event', 'INotify', 'flags', 'masks', 'parse_events']
|
||||||
|
|
||||||
|
_libc = None
|
||||||
|
|
||||||
|
|
||||||
|
def _libc_call(function, *args):
|
||||||
|
"""Wrapper which raises errors and retries on EINTR."""
|
||||||
|
while True:
|
||||||
|
rc = function(*args)
|
||||||
|
if rc != -1:
|
||||||
|
return rc
|
||||||
|
err = get_errno()
|
||||||
|
if err != EINTR:
|
||||||
|
raise OSError(errno, os.strerror(errno))
|
||||||
|
|
||||||
|
|
||||||
|
#: A ``namedtuple`` (wd, mask, cookie, name) for an inotify event. The
|
||||||
|
#: :attr:`~inotify_simple.Event.name` field is a ``str`` decoded with
|
||||||
|
#: ``os.fsdecode()``.
|
||||||
|
Event = namedtuple('Event', ['wd', 'mask', 'cookie', 'name'])
|
||||||
|
|
||||||
|
_EVENT_FMT = 'iIII'
|
||||||
|
_EVENT_SIZE = calcsize(_EVENT_FMT)
|
||||||
|
|
||||||
|
|
||||||
|
class INotify(FileIO):
|
||||||
|
|
||||||
|
#: The inotify file descriptor returned by ``inotify_init()``. You are
|
||||||
|
#: free to use it directly with ``os.read`` if you'd prefer not to call
|
||||||
|
#: :func:`~inotify_simple.INotify.read` for some reason. Also available as
|
||||||
|
#: :func:`~inotify_simple.INotify.fileno`
|
||||||
|
fd = property(FileIO.fileno)
|
||||||
|
|
||||||
|
def __init__(self, inheritable=False, nonblocking=False):
|
||||||
|
"""File-like object wrapping ``inotify_init1()``. Raises ``OSError`` on failure.
|
||||||
|
:func:`~inotify_simple.INotify.close` should be called when no longer needed.
|
||||||
|
Can be used as a context manager to ensure it is closed, and can be used
|
||||||
|
directly by functions expecting a file-like object, such as ``select``, or with
|
||||||
|
functions expecting a file descriptor via
|
||||||
|
:func:`~inotify_simple.INotify.fileno`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
inheritable (bool): whether the inotify file descriptor will be inherited by
|
||||||
|
child processes. The default,``False``, corresponds to passing the
|
||||||
|
``IN_CLOEXEC`` flag to ``inotify_init1()``. Setting this flag when
|
||||||
|
opening filedescriptors is the default behaviour of Python standard
|
||||||
|
library functions since PEP 446. On Python < 3.3, the file descriptor
|
||||||
|
will be inheritable and this argument has no effect, one must instead
|
||||||
|
use fcntl to set FD_CLOEXEC to make it non-inheritable.
|
||||||
|
|
||||||
|
nonblocking (bool): whether to open the inotify file descriptor in
|
||||||
|
nonblocking mode, corresponding to passing the ``IN_NONBLOCK`` flag to
|
||||||
|
``inotify_init1()``. This does not affect the normal behaviour of
|
||||||
|
:func:`~inotify_simple.INotify.read`, which uses ``poll()`` to control
|
||||||
|
blocking behaviour according to the given timeout, but will cause other
|
||||||
|
reads of the file descriptor (for example if the application reads data
|
||||||
|
manually with ``os.read(fd)``) to raise ``BlockingIOError`` if no data
|
||||||
|
is available."""
|
||||||
|
try:
|
||||||
|
libc_so = find_library('c')
|
||||||
|
except RuntimeError: # Python on Synology NASs raises a RuntimeError
|
||||||
|
libc_so = None
|
||||||
|
global _libc
|
||||||
|
_libc = _libc or CDLL(libc_so or 'libc.so.6', use_errno=True)
|
||||||
|
O_CLOEXEC = getattr(os, 'O_CLOEXEC', 0) # Only defined in Python 3.3+
|
||||||
|
flags = (not inheritable) * O_CLOEXEC | bool(nonblocking) * os.O_NONBLOCK
|
||||||
|
FileIO.__init__(self, _libc_call(_libc.inotify_init1, flags), mode='rb')
|
||||||
|
|
||||||
|
def add_watch(self, path, mask):
|
||||||
|
"""Wrapper around ``inotify_add_watch()``. Returns the watch
|
||||||
|
descriptor or raises an ``OSError`` on failure.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (str, bytes, or PathLike): The path to watch. Will be encoded with
|
||||||
|
``os.fsencode()`` before being passed to ``inotify_add_watch()``.
|
||||||
|
|
||||||
|
mask (int): The mask of events to watch for. Can be constructed by
|
||||||
|
bitwise-ORing :class:`~inotify_simple.flags` together.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: watch descriptor"""
|
||||||
|
# Explicit conversion of Path to str required on Python < 3.6
|
||||||
|
|
||||||
|
path = str(path) if hasattr(path, 'parts') else path
|
||||||
|
return _libc_call(_libc.inotify_add_watch, self.fileno(), fsencode(path), mask)
|
||||||
|
|
||||||
|
def rm_watch(self, wd):
|
||||||
|
"""Wrapper around ``inotify_rm_watch()``. Raises ``OSError`` on failure.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
wd (int): The watch descriptor to remove"""
|
||||||
|
|
||||||
|
_libc_call(_libc.inotify_rm_watch, self.fileno(), wd)
|
||||||
|
|
||||||
|
def poll(self):
|
||||||
|
"""Wait for I/O completion"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
read_fs, _, _ = select([self.fileno()], [], [])
|
||||||
|
break
|
||||||
|
except select.error as err:
|
||||||
|
if err.errno == errno.EINTR:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
|
return bool(read_fs)
|
||||||
|
|
||||||
|
def read(self, timeout=None, read_delay=None):
|
||||||
|
"""Read the inotify file descriptor and return the resulting
|
||||||
|
:attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeout (int): The time in milliseconds to wait for events if there are
|
||||||
|
none. If negative or ``None``, block until there are events. If zero,
|
||||||
|
return immediately if there are no events to be read.
|
||||||
|
|
||||||
|
read_delay (int): If there are no events immediately available for reading,
|
||||||
|
then this is the time in milliseconds to wait after the first event
|
||||||
|
arrives before reading the file descriptor. This allows further events
|
||||||
|
to accumulate before reading, which allows the kernel to coalesce like
|
||||||
|
events and can decrease the number of events the application needs to
|
||||||
|
process. However, this also increases the risk that the event queue will
|
||||||
|
overflow due to not being emptied fast enough.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
generator: generator producing :attr:`~inotify_simple.Event` namedtuples
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
If the same inotify file descriptor is being read by multiple threads
|
||||||
|
simultaneously, this method may attempt to read the file descriptor when no
|
||||||
|
data is available. It may return zero events, or block until more events
|
||||||
|
arrive (regardless of the requested timeout), or in the case that the
|
||||||
|
:func:`~inotify_simple.INotify` object was instantiated with
|
||||||
|
``nonblocking=True``, raise ``BlockingIOError``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = self._readall()
|
||||||
|
if not data and timeout != 0 and self.poll():
|
||||||
|
if read_delay is not None:
|
||||||
|
sleep(read_delay / 1000.0)
|
||||||
|
data = self._readall()
|
||||||
|
return parse_events(data)
|
||||||
|
|
||||||
|
def _readall(self):
|
||||||
|
bytes_avail = c_int()
|
||||||
|
ioctl(self, FIONREAD, bytes_avail)
|
||||||
|
if not bytes_avail.value:
|
||||||
|
return b''
|
||||||
|
return os.read(self.fileno(), bytes_avail.value)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_events(data):
|
||||||
|
"""Unpack data read from an inotify file descriptor into
|
||||||
|
:attr:`~inotify_simple.Event` namedtuples (wd, mask, cookie, name). This function
|
||||||
|
can be used if the application has read raw data from the inotify file
|
||||||
|
descriptor rather than calling :func:`~inotify_simple.INotify.read`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (bytes): A bytestring as read from an inotify file descriptor.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: list of :attr:`~inotify_simple.Event` namedtuples"""
|
||||||
|
|
||||||
|
pos = 0
|
||||||
|
events = []
|
||||||
|
while pos < len(data):
|
||||||
|
wd, mask, cookie, namesize = unpack_from(_EVENT_FMT, data, pos)
|
||||||
|
pos += _EVENT_SIZE + namesize
|
||||||
|
name = data[pos - namesize: pos].split(b'\x00', 1)[0]
|
||||||
|
events.append(Event(wd, mask, cookie, fsdecode(name)))
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
class flags(IntEnum):
|
||||||
|
"""Inotify flags as defined in ``inotify.h`` but with ``IN_`` prefix omitted.
|
||||||
|
Includes a convenience method :func:`~inotify_simple.flags.from_mask` for extracting
|
||||||
|
flags from a mask."""
|
||||||
|
|
||||||
|
ACCESS = 0x00000001 #: File was accessed
|
||||||
|
MODIFY = 0x00000002 #: File was modified
|
||||||
|
ATTRIB = 0x00000004 #: Metadata changed
|
||||||
|
CLOSE_WRITE = 0x00000008 #: Writable file was closed
|
||||||
|
CLOSE_NOWRITE = 0x00000010 #: Unwritable file closed
|
||||||
|
OPEN = 0x00000020 #: File was opened
|
||||||
|
MOVED_FROM = 0x00000040 #: File was moved from X
|
||||||
|
MOVED_TO = 0x00000080 #: File was moved to Y
|
||||||
|
CREATE = 0x00000100 #: Subfile was created
|
||||||
|
DELETE = 0x00000200 #: Subfile was deleted
|
||||||
|
DELETE_SELF = 0x00000400 #: Self was deleted
|
||||||
|
MOVE_SELF = 0x00000800 #: Self was moved
|
||||||
|
|
||||||
|
UNMOUNT = 0x00002000 #: Backing fs was unmounted
|
||||||
|
Q_OVERFLOW = 0x00004000 #: Event queue overflowed
|
||||||
|
IGNORED = 0x00008000 #: File was ignored
|
||||||
|
|
||||||
|
ONLYDIR = 0x01000000 #: only watch the path if it is a directory
|
||||||
|
DONT_FOLLOW = 0x02000000 #: don't follow a sym link
|
||||||
|
EXCL_UNLINK = 0x04000000 #: exclude events on unlinked objects
|
||||||
|
MASK_ADD = 0x20000000 #: add to the mask of an already existing watch
|
||||||
|
ISDIR = 0x40000000 #: event occurred against dir
|
||||||
|
ONESHOT = 0x80000000 #: only send event once
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_mask(cls, mask):
|
||||||
|
"""Convenience method that returns a list of every flag in a mask."""
|
||||||
|
return [flag for flag in cls.__members__.values() if flag & mask]
|
||||||
|
|
||||||
|
|
||||||
|
class masks(IntEnum):
|
||||||
|
"""Convenience masks as defined in ``inotify.h`` but with ``IN_`` prefix omitted."""
|
||||||
|
|
||||||
|
#: helper event mask equal to ``flags.CLOSE_WRITE | flags.CLOSE_NOWRITE``
|
||||||
|
CLOSE = flags.CLOSE_WRITE | flags.CLOSE_NOWRITE
|
||||||
|
#: helper event mask equal to ``flags.MOVED_FROM | flags.MOVED_TO``
|
||||||
|
MOVE = flags.MOVED_FROM | flags.MOVED_TO
|
||||||
|
|
||||||
|
#: bitwise-OR of all the events that can be passed to
|
||||||
|
#: :func:`~inotify_simple.INotify.add_watch`
|
||||||
|
ALL_EVENTS = (flags.ACCESS | flags.MODIFY | flags.ATTRIB | flags.CLOSE_WRITE |
|
||||||
|
flags.CLOSE_NOWRITE | flags.OPEN | flags.MOVED_FROM | flags.MOVED_TO |
|
||||||
|
flags.CREATE | flags.DELETE | flags.DELETE_SELF | flags.MOVE_SELF)
|
|
@ -3612,3 +3612,15 @@ def checkout_ostree(ostree_repo, commit, target_dir, subpath):
|
||||||
raise exception.SysinvException(
|
raise exception.SysinvException(
|
||||||
"Error checkout ostree commit: %s" % (error),
|
"Error checkout ostree commit: %s" % (error),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_bundle_extension_valid(filename):
|
||||||
|
"""Check if application bundles have the correct extension
|
||||||
|
|
||||||
|
:param filename: Bundle filename
|
||||||
|
:return: Returns True if the extension is correct.
|
||||||
|
Otherwise returns False.
|
||||||
|
"""
|
||||||
|
|
||||||
|
file_extension = pathlib.Path(filename).suffix
|
||||||
|
return file_extension.lower() == ".tgz"
|
||||||
|
|
|
@ -29,6 +29,7 @@ collection of inventory data for each host.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
import errno
|
import errno
|
||||||
import filecmp
|
import filecmp
|
||||||
import glob
|
import glob
|
||||||
|
@ -52,7 +53,6 @@ import xml.etree.ElementTree as ElementTree
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from distutils.util import strtobool
|
|
||||||
from distutils.version import LooseVersion
|
from distutils.version import LooseVersion
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
|
@ -93,6 +93,7 @@ from sysinv.api.controllers.v1 import kube_app as kube_api
|
||||||
from sysinv.api.controllers.v1 import mtce_api
|
from sysinv.api.controllers.v1 import mtce_api
|
||||||
from sysinv.api.controllers.v1 import utils
|
from sysinv.api.controllers.v1 import utils
|
||||||
from sysinv.api.controllers.v1 import vim_api
|
from sysinv.api.controllers.v1 import vim_api
|
||||||
|
from sysinv.common import app_metadata
|
||||||
from sysinv.common import fpga_constants
|
from sysinv.common import fpga_constants
|
||||||
from sysinv.common import constants
|
from sysinv.common import constants
|
||||||
from sysinv.common import ceph as cceph
|
from sysinv.common import ceph as cceph
|
||||||
|
@ -108,6 +109,8 @@ from sysinv.common import kubernetes
|
||||||
from sysinv.common import retrying
|
from sysinv.common import retrying
|
||||||
from sysinv.common import service
|
from sysinv.common import service
|
||||||
from sysinv.common import utils as cutils
|
from sysinv.common import utils as cutils
|
||||||
|
from sysinv.common.inotify import flags
|
||||||
|
from sysinv.common.inotify import INotify
|
||||||
from sysinv.common.retrying import retry
|
from sysinv.common.retrying import retry
|
||||||
from sysinv.common.storage_backend_conf import StorageBackendConfig
|
from sysinv.common.storage_backend_conf import StorageBackendConfig
|
||||||
from cephclient import wrapper as ceph
|
from cephclient import wrapper as ceph
|
||||||
|
@ -232,6 +235,63 @@ AppTarBall = namedtuple(
|
||||||
"tarball_name app_name app_version manifest_name manifest_file metadata")
|
"tarball_name app_name app_version manifest_name manifest_file metadata")
|
||||||
|
|
||||||
|
|
||||||
|
class KubeAppBundleStorageType(Enum):
|
||||||
|
DATABASE = 1
|
||||||
|
|
||||||
|
|
||||||
|
class KubeAppBundleStorageFactory(object):
|
||||||
|
"""Factory class that aims to abstract calls to storage operations when
|
||||||
|
handling application bundle metadata.
|
||||||
|
|
||||||
|
This allows supporting a database implementation going forward and an
|
||||||
|
in-memory implementation for patchback scenarios if needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def createKubeAppBundleStorage(storage_type=KubeAppBundleStorageType.DATABASE):
|
||||||
|
"""Factory Method
|
||||||
|
|
||||||
|
:param storage_type: Storage type used to house the metadata
|
||||||
|
"""
|
||||||
|
if storage_type == KubeAppBundleStorageType.DATABASE:
|
||||||
|
return KubeAppBundleDatabase()
|
||||||
|
|
||||||
|
|
||||||
|
class KubeAppBundleDatabase(KubeAppBundleStorageFactory):
|
||||||
|
"""Database implementation to store application bundle metadata."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.dbapi = dbapi.get_instance()
|
||||||
|
|
||||||
|
def create(self, bundle_data):
|
||||||
|
"""Add a bundle to the database."""
|
||||||
|
self.dbapi.kube_app_bundle_create(bundle_data)
|
||||||
|
|
||||||
|
def create_all(self, bundle_bulk_data):
|
||||||
|
"""Insert a list of bundles to the database."""
|
||||||
|
self.dbapi.kube_app_bundle_create_all(bundle_bulk_data)
|
||||||
|
|
||||||
|
def is_empty(self):
|
||||||
|
"""Check if the table is empty."""
|
||||||
|
return self.dbapi.kube_app_bundle_is_empty()
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
"""Get a list containing all bundles."""
|
||||||
|
return self.dbapi.kube_app_bundle_get_all()
|
||||||
|
|
||||||
|
def get_by_name(self, app_name):
|
||||||
|
"""Get a list of bundles by their name."""
|
||||||
|
return self.dbapi.kube_app_bundle_get_by_name(app_name)
|
||||||
|
|
||||||
|
def destroy_all(self):
|
||||||
|
"""Prune all bundle metadata."""
|
||||||
|
self.dbapi.kube_app_bundle_destroy_all()
|
||||||
|
|
||||||
|
def destroy_by_file_path(self, file_path):
|
||||||
|
"""Delete bundle with a given file path."""
|
||||||
|
self.dbapi.kube_app_bundle_destroy_by_file_path(file_path)
|
||||||
|
|
||||||
|
|
||||||
class ConductorManager(service.PeriodicService):
|
class ConductorManager(service.PeriodicService):
|
||||||
"""Sysinv Conductor service main class."""
|
"""Sysinv Conductor service main class."""
|
||||||
|
|
||||||
|
@ -272,12 +332,17 @@ class ConductorManager(service.PeriodicService):
|
||||||
endpoint='http://localhost:{}'.format(constants.CEPH_MGR_PORT))
|
endpoint='http://localhost:{}'.format(constants.CEPH_MGR_PORT))
|
||||||
self._kube = None
|
self._kube = None
|
||||||
self._fernet = None
|
self._fernet = None
|
||||||
|
self._inotify = None
|
||||||
|
|
||||||
self._openstack = None
|
self._openstack = None
|
||||||
self._api_token = None
|
self._api_token = None
|
||||||
self._mtc_address = constants.LOCALHOST_HOSTNAME
|
self._mtc_address = constants.LOCALHOST_HOSTNAME
|
||||||
self._mtc_port = 2112
|
self._mtc_port = 2112
|
||||||
|
|
||||||
|
# Store and track available application bundles
|
||||||
|
self._kube_app_bundle_storage = None
|
||||||
|
self._cached_app_bundle_set = set()
|
||||||
|
|
||||||
# Timeouts for adding & removing operations
|
# Timeouts for adding & removing operations
|
||||||
self._pv_op_timeouts = {}
|
self._pv_op_timeouts = {}
|
||||||
self._stor_bck_op_timeouts = {}
|
self._stor_bck_op_timeouts = {}
|
||||||
|
@ -358,6 +423,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
self.fm_log = fm.FmCustomerLog()
|
self.fm_log = fm.FmCustomerLog()
|
||||||
self.host_uuid = self._get_active_controller_uuid()
|
self.host_uuid = self._get_active_controller_uuid()
|
||||||
|
|
||||||
|
self._kube_app_bundle_storage = KubeAppBundleStorageFactory.createKubeAppBundleStorage()
|
||||||
self._openstack = openstack.OpenStackOperator(self.dbapi)
|
self._openstack = openstack.OpenStackOperator(self.dbapi)
|
||||||
self._puppet = puppet.PuppetOperator(self.dbapi)
|
self._puppet = puppet.PuppetOperator(self.dbapi)
|
||||||
|
|
||||||
|
@ -397,12 +463,36 @@ class ConductorManager(service.PeriodicService):
|
||||||
# Runtime config tasks
|
# Runtime config tasks
|
||||||
self._prune_runtime_config_table()
|
self._prune_runtime_config_table()
|
||||||
|
|
||||||
|
# Populate/update app bundle table as needed
|
||||||
|
if self._kube_app_bundle_storage.is_empty():
|
||||||
|
self._populate_app_bundle_metadata()
|
||||||
|
else:
|
||||||
|
self._update_cached_app_bundles_set()
|
||||||
|
self._update_app_bundles_storage()
|
||||||
|
|
||||||
|
# Initialize inotify and launch thread to monitor
|
||||||
|
# changes to the ostree root folder
|
||||||
|
self._initialize_ostree_inotify()
|
||||||
|
greenthread.spawn(self._monitor_ostree_root_folder)
|
||||||
|
|
||||||
LOG.info("sysinv-conductor start committed system=%s" %
|
LOG.info("sysinv-conductor start committed system=%s" %
|
||||||
system.as_dict())
|
system.as_dict())
|
||||||
|
|
||||||
# Save our start time for time limited init actions
|
# Save our start time for time limited init actions
|
||||||
self._start_time = timeutils.utcnow()
|
self._start_time = timeutils.utcnow()
|
||||||
|
|
||||||
|
def _initialize_ostree_inotify(self):
|
||||||
|
""" Initialize inotify to watch for changes under the ostree root
|
||||||
|
folder.
|
||||||
|
|
||||||
|
Created or removed files under that folder suggest that a patch
|
||||||
|
was applied and a new ostree commit was deployed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._inotify = INotify()
|
||||||
|
watch_flags = flags.CREATE | flags.DELETE
|
||||||
|
self._inotify.add_watch(constants.OSTREE_ROOT_FOLDER, watch_flags)
|
||||||
|
|
||||||
def _get_active_controller_uuid(self):
|
def _get_active_controller_uuid(self):
|
||||||
ahost = utils.HostHelper.get_active_controller(self.dbapi)
|
ahost = utils.HostHelper.get_active_controller(self.dbapi)
|
||||||
if ahost:
|
if ahost:
|
||||||
|
@ -7211,67 +7301,53 @@ class ConductorManager(service.PeriodicService):
|
||||||
|
|
||||||
self._inner_sync_auto_apply(context, app_name)
|
self._inner_sync_auto_apply(context, app_name)
|
||||||
|
|
||||||
@staticmethod
|
def _get_app_bundle_for_update(self, app):
|
||||||
def check_app_k8s_auto_update(app_name, tarball):
|
""" Retrieve metadata from the most updated application bundle
|
||||||
""" Check whether an application should be automatically updated
|
that can be used to update the given app.
|
||||||
based on its Kubernetes upgrade metadata fields.
|
|
||||||
|
|
||||||
:param tarball: tarball object of the application to be checked
|
:param app: The application to be updated
|
||||||
|
:return The bundle metadata from the new version of the app
|
||||||
"""
|
"""
|
||||||
|
|
||||||
minimum_supported_k8s_version = tarball.metadata.get(
|
bundle_metadata_list = self._kube_app_bundle_storage.get_by_name(app.name)
|
||||||
constants.APP_METADATA_SUPPORTED_K8S_VERSION, {}).get(
|
|
||||||
constants.APP_METADATA_MINIMUM, None)
|
|
||||||
|
|
||||||
if minimum_supported_k8s_version is None:
|
latest_version_bundle = None
|
||||||
# TODO: Turn this into an error message rather than a warning
|
k8s_version = self._kube.kube_get_kubernetes_version().strip().lstrip('v')
|
||||||
# when the k8s app upgrade implementation is in place. Also,
|
for bundle_metadata in bundle_metadata_list:
|
||||||
# return False in this scenario.
|
if LooseVersion(bundle_metadata.version) <= LooseVersion(app.app_version):
|
||||||
LOG.warning("Minimum supported Kubernetes version missing from "
|
LOG.debug("Bundle {} version {} lower than installed app version ({})"
|
||||||
"{} metadata".format(app_name))
|
.format(bundle_metadata.file_path,
|
||||||
else:
|
bundle_metadata.version,
|
||||||
LOG.debug("minimum_supported_k8s_version for {}: {}"
|
app.app_version))
|
||||||
.format(app_name, minimum_supported_k8s_version))
|
elif not bundle_metadata.auto_update:
|
||||||
|
LOG.debug("Application auto update disabled for bundle {}"
|
||||||
|
.format(bundle_metadata.file_path))
|
||||||
|
elif not bundle_metadata.k8s_auto_update:
|
||||||
|
LOG.debug("Kubernetes application auto update disabled for bundle {}"
|
||||||
|
.format(bundle_metadata.file_path))
|
||||||
|
elif LooseVersion(k8s_version) < LooseVersion(bundle_metadata.k8s_minimum_version):
|
||||||
|
LOG.debug("Kubernetes version {} is lower than {} which is "
|
||||||
|
"the minimum required for bundle {}"
|
||||||
|
.format(k8s_version,
|
||||||
|
bundle_metadata.k8s_minimum_version,
|
||||||
|
bundle_metadata.file_path))
|
||||||
|
elif ((bundle_metadata.k8s_maximum_version is not None) and (LooseVersion(k8s_version) >
|
||||||
|
LooseVersion(bundle_metadata.k8s_maximum_version))):
|
||||||
|
LOG.debug("Kubernetes version {} is higher than {} which is "
|
||||||
|
"the maximum allowed for bundle {}"
|
||||||
|
.format(k8s_version,
|
||||||
|
bundle_metadata.k8s_maximum_version,
|
||||||
|
bundle_metadata.file_path))
|
||||||
|
elif ((latest_version_bundle is None) or
|
||||||
|
(LooseVersion(bundle_metadata.version) >
|
||||||
|
LooseVersion(latest_version_bundle.version))):
|
||||||
|
# Only set the chosen bundle if it was not set before or if the version
|
||||||
|
# of the current one is higher than the one previously set.
|
||||||
|
latest_version_bundle = bundle_metadata
|
||||||
|
|
||||||
maximum_supported_k8s_version = tarball.metadata.get(
|
return latest_version_bundle
|
||||||
constants.APP_METADATA_SUPPORTED_K8S_VERSION, {}).get(
|
|
||||||
constants.APP_METADATA_MAXIMUM, None)
|
|
||||||
|
|
||||||
if maximum_supported_k8s_version:
|
def _auto_update_app(self, context, app_name):
|
||||||
LOG.debug("maximum_supported_k8s_version for {}: {}"
|
|
||||||
.format(app_name, maximum_supported_k8s_version))
|
|
||||||
|
|
||||||
k8s_upgrades = tarball.metadata.get(
|
|
||||||
constants.APP_METADATA_K8S_UPGRADES, None)
|
|
||||||
|
|
||||||
if k8s_upgrades is None:
|
|
||||||
k8s_auto_update = constants.APP_METADATA_K8S_AUTO_UPDATE_DEFAULT_VALUE
|
|
||||||
k8s_update_timing = constants.APP_METADATA_TIMING_DEFAULT_VALUE
|
|
||||||
LOG.warning("k8s_upgrades section missing from {} metadata"
|
|
||||||
.format(app_name))
|
|
||||||
else:
|
|
||||||
k8s_auto_update = tarball.metadata.get(
|
|
||||||
constants.APP_METADATA_K8S_UPGRADES).get(
|
|
||||||
constants.APP_METADATA_AUTO_UPDATE,
|
|
||||||
constants.APP_METADATA_K8S_AUTO_UPDATE_DEFAULT_VALUE)
|
|
||||||
k8s_update_timing = tarball.metadata.get(
|
|
||||||
constants.APP_METADATA_K8S_UPGRADES).get(
|
|
||||||
constants.APP_METADATA_TIMING,
|
|
||||||
constants.APP_METADATA_TIMING_DEFAULT_VALUE)
|
|
||||||
|
|
||||||
# TODO: check if the application meets the criteria to be updated
|
|
||||||
# according to the 'supported_k8s_version' and 'k8s_upgrades'
|
|
||||||
# metadata sections. This initial implementation is only intended to
|
|
||||||
# set the default values for each entry.
|
|
||||||
|
|
||||||
LOG.debug("k8s_auto_update value for {}: {}"
|
|
||||||
.format(app_name, k8s_auto_update))
|
|
||||||
LOG.debug("k8s_update_timing value for {}: {}"
|
|
||||||
.format(app_name, k8s_update_timing))
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _auto_update_app(self, context, app_name, managed_app):
|
|
||||||
"""Auto update applications"""
|
"""Auto update applications"""
|
||||||
try:
|
try:
|
||||||
app = kubeapp_obj.get_by_name(context, app_name)
|
app = kubeapp_obj.get_by_name(context, app_name)
|
||||||
|
@ -7314,19 +7390,15 @@ class ConductorManager(service.PeriodicService):
|
||||||
|
|
||||||
LOG.debug("Application %s: Checking "
|
LOG.debug("Application %s: Checking "
|
||||||
"for update ..." % app_name)
|
"for update ..." % app_name)
|
||||||
tarfile = self._search_tarfile(app_name, managed_app=managed_app)
|
app_bundle = self._get_app_bundle_for_update(app)
|
||||||
if tarfile is None:
|
if app_bundle is None:
|
||||||
# Skip if no tarball or multiple tarballs found
|
# Skip if no bundles are found
|
||||||
return
|
LOG.debug("No bundle found for updating %s" % app_name)
|
||||||
|
|
||||||
applied_app = '{}-{}'.format(app.name, app.app_version)
|
|
||||||
if applied_app in tarfile:
|
|
||||||
# Skip if the tarfile version is already applied
|
|
||||||
return
|
return
|
||||||
|
|
||||||
LOG.info("Found new tarfile version for %s: %s"
|
LOG.info("Found new tarfile version for %s: %s"
|
||||||
% (app.name, tarfile))
|
% (app.name, app_bundle.file_path))
|
||||||
tarball = self._check_tarfile(app_name, tarfile,
|
tarball = self._check_tarfile(app_name, app_bundle.file_path,
|
||||||
preserve_metadata=True)
|
preserve_metadata=True)
|
||||||
if ((tarball.app_name is None) or
|
if ((tarball.app_name is None) or
|
||||||
(tarball.app_version is None) or
|
(tarball.app_version is None) or
|
||||||
|
@ -7335,18 +7407,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
# Skip if tarball check fails
|
# Skip if tarball check fails
|
||||||
return
|
return
|
||||||
|
|
||||||
if not tarball.metadata:
|
if app_bundle.version in \
|
||||||
# Skip if app doesn't have metadata
|
|
||||||
return
|
|
||||||
|
|
||||||
auto_update = tarball.metadata.get(
|
|
||||||
constants.APP_METADATA_UPGRADES, {}).get(
|
|
||||||
constants.APP_METADATA_AUTO_UPDATE, False)
|
|
||||||
if not bool(strtobool(str(auto_update))):
|
|
||||||
# Skip if app is not set to auto_update
|
|
||||||
return
|
|
||||||
|
|
||||||
if tarball.app_version in \
|
|
||||||
app.app_metadata.get(
|
app.app_metadata.get(
|
||||||
constants.APP_METADATA_UPGRADES, {}).get(
|
constants.APP_METADATA_UPGRADES, {}).get(
|
||||||
constants.APP_METADATA_FAILED_VERSIONS, []):
|
constants.APP_METADATA_FAILED_VERSIONS, []):
|
||||||
|
@ -7357,11 +7418,6 @@ class ConductorManager(service.PeriodicService):
|
||||||
% (app.name, tarball.app_version, app.app_version))
|
% (app.name, tarball.app_version, app.app_version))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if the update should proceed based on the application's
|
|
||||||
# Kubernetes metadata
|
|
||||||
if not ConductorManager.check_app_k8s_auto_update(app_name, tarball):
|
|
||||||
return
|
|
||||||
|
|
||||||
self._inner_sync_auto_update(context, app, tarball)
|
self._inner_sync_auto_update(context, app, tarball)
|
||||||
|
|
||||||
@cutils.synchronized(LOCK_APP_AUTO_MANAGE)
|
@cutils.synchronized(LOCK_APP_AUTO_MANAGE)
|
||||||
|
@ -7541,14 +7597,14 @@ class ConductorManager(service.PeriodicService):
|
||||||
""" Load metadata of apps from the directory containing
|
""" Load metadata of apps from the directory containing
|
||||||
apps bundled with the iso.
|
apps bundled with the iso.
|
||||||
"""
|
"""
|
||||||
for tarfile in os.listdir(constants.HELM_APP_ISO_INSTALL_PATH):
|
for app_bundle in os.listdir(constants.HELM_APP_ISO_INSTALL_PATH):
|
||||||
# Get the app name from the tarball name
|
# Get the app name from the tarball name
|
||||||
# If the app has the metadata loaded already, by conductor restart,
|
# If the app has the metadata loaded already, by conductor restart,
|
||||||
# then skip the tarball extraction
|
# then skip the tarball extraction
|
||||||
app_name = None
|
app_name = None
|
||||||
pattern = re.compile("^(.*)-([0-9]+\.[0-9]+-[0-9]+)")
|
pattern = re.compile("^(.*)-([0-9]+\.[0-9]+-[0-9]+)")
|
||||||
|
|
||||||
match = pattern.search(tarfile)
|
match = pattern.search(app_bundle)
|
||||||
if match:
|
if match:
|
||||||
app_name = match.group(1)
|
app_name = match.group(1)
|
||||||
|
|
||||||
|
@ -7560,7 +7616,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
|
|
||||||
# Proceed with extracting the tarball
|
# Proceed with extracting the tarball
|
||||||
tarball_name = '{}/{}'.format(
|
tarball_name = '{}/{}'.format(
|
||||||
constants.HELM_APP_ISO_INSTALL_PATH, tarfile)
|
constants.HELM_APP_ISO_INSTALL_PATH, app_bundle)
|
||||||
|
|
||||||
with cutils.TempDirectory() as app_path:
|
with cutils.TempDirectory() as app_path:
|
||||||
if not cutils.extract_tarfile(app_path, tarball_name):
|
if not cutils.extract_tarfile(app_path, tarball_name):
|
||||||
|
@ -7733,6 +7789,90 @@ class ConductorManager(service.PeriodicService):
|
||||||
# No need to detect again until conductor restart
|
# No need to detect again until conductor restart
|
||||||
self._do_detect_swact = False
|
self._do_detect_swact = False
|
||||||
|
|
||||||
|
def _populate_app_bundle_metadata(self):
|
||||||
|
"""Read metadata of all application bundles and store in the database"""
|
||||||
|
|
||||||
|
bundle_list = []
|
||||||
|
for file_path in glob.glob("{}/*.tgz".format(constants.HELM_APP_ISO_INSTALL_PATH)):
|
||||||
|
bundle_data = app_metadata.extract_bundle_metadata(file_path)
|
||||||
|
if bundle_data:
|
||||||
|
bundle_list.append(bundle_data)
|
||||||
|
|
||||||
|
self._kube_app_bundle_storage.create_all(bundle_list)
|
||||||
|
self._update_cached_app_bundles_set()
|
||||||
|
|
||||||
|
def _add_app_bundle(self, full_bundle_path):
|
||||||
|
"""Add a new application bundle record"""
|
||||||
|
|
||||||
|
bundle_data = app_metadata.extract_bundle_metadata(full_bundle_path)
|
||||||
|
if bundle_data:
|
||||||
|
LOG.info("New application bundle available: {}".format(full_bundle_path))
|
||||||
|
try:
|
||||||
|
self._kube_app_bundle_storage.create(bundle_data)
|
||||||
|
except exception.KubeAppBundleAlreadyExists as e:
|
||||||
|
LOG.exception(e)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.exception("Error while storing bundle data for {}: {}"
|
||||||
|
.format(full_bundle_path, e))
|
||||||
|
|
||||||
|
def _remove_app_bundle(self, full_bundle_path):
|
||||||
|
"""Remove application bundle record"""
|
||||||
|
|
||||||
|
LOG.info("Application bundle deleted: {}".format(full_bundle_path))
|
||||||
|
try:
|
||||||
|
self._kube_app_bundle_storage.destroy_by_file_path(full_bundle_path)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error("Error while removing bundle data for {}: {}"
|
||||||
|
.format(full_bundle_path, e))
|
||||||
|
|
||||||
|
def _update_cached_app_bundles_set(self):
|
||||||
|
"""Update internal cache of application bundles"""
|
||||||
|
|
||||||
|
self._cached_app_bundle_set = set(bundle.file_path for bundle in
|
||||||
|
self._kube_app_bundle_storage.get_all())
|
||||||
|
|
||||||
|
def _update_app_bundles_storage(self):
|
||||||
|
"""Update application bundle storage to account for new and removed files"""
|
||||||
|
|
||||||
|
filesystem_app_bundle_set = set(glob.glob("{}/*.tgz"
|
||||||
|
.format(constants.HELM_APP_ISO_INSTALL_PATH)))
|
||||||
|
if filesystem_app_bundle_set != self._cached_app_bundle_set:
|
||||||
|
new_files = set(file_path for file_path in filesystem_app_bundle_set
|
||||||
|
if file_path not in self._cached_app_bundle_set)
|
||||||
|
|
||||||
|
# Add new files to the database
|
||||||
|
for file_path in new_files:
|
||||||
|
self._add_app_bundle(file_path)
|
||||||
|
|
||||||
|
# Delete removed files from the database
|
||||||
|
for file_path in self._cached_app_bundle_set:
|
||||||
|
if file_path not in filesystem_app_bundle_set:
|
||||||
|
self._remove_app_bundle(file_path)
|
||||||
|
|
||||||
|
# Update internal bundle set to reflect the storage
|
||||||
|
self._update_cached_app_bundles_set()
|
||||||
|
|
||||||
|
def _monitor_ostree_root_folder(self):
|
||||||
|
"""Update application bundle storage to account for new and removed files"""
|
||||||
|
|
||||||
|
if self._inotify is None:
|
||||||
|
LOG.error("Inotify has not been initialized.")
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
for event in self._inotify.read(timeout=0):
|
||||||
|
event_types = [f.name for f in flags.from_mask(event.mask)]
|
||||||
|
LOG.debug("Event {}. Event types: {}".format(event, event_types))
|
||||||
|
|
||||||
|
# If the "lock" file was deleted inside the ostree root it means
|
||||||
|
# that a new ostree has finished to be deployed. Therefore we may
|
||||||
|
# need to update the list of available application bundles.
|
||||||
|
if constants.INOTIFY_DELETE_EVENT in event_types and \
|
||||||
|
event.name == constants.OSTREE_LOCK_FILE:
|
||||||
|
self._update_app_bundles_storage()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
@periodic_task.periodic_task(spacing=CONF.conductor_periodic_task_intervals.k8s_application,
|
@periodic_task.periodic_task(spacing=CONF.conductor_periodic_task_intervals.k8s_application,
|
||||||
run_immediately=True)
|
run_immediately=True)
|
||||||
def _k8s_application_audit(self, context):
|
def _k8s_application_audit(self, context):
|
||||||
|
@ -7839,7 +7979,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
self._auto_recover_managed_app(context, app_name)
|
self._auto_recover_managed_app(context, app_name)
|
||||||
elif status == constants.APP_APPLY_SUCCESS:
|
elif status == constants.APP_APPLY_SUCCESS:
|
||||||
self.check_pending_app_reapply(context)
|
self.check_pending_app_reapply(context)
|
||||||
self._auto_update_app(context, app_name, managed_app=True)
|
self._auto_update_app(context, app_name)
|
||||||
|
|
||||||
# Special case, we want to apply some logic to non-managed applications
|
# Special case, we want to apply some logic to non-managed applications
|
||||||
for app_name in self.apps_metadata[constants.APP_METADATA_APPS].keys():
|
for app_name in self.apps_metadata[constants.APP_METADATA_APPS].keys():
|
||||||
|
@ -7859,7 +7999,7 @@ class ConductorManager(service.PeriodicService):
|
||||||
# Automatically update non-managed applications
|
# Automatically update non-managed applications
|
||||||
if status == constants.APP_APPLY_SUCCESS:
|
if status == constants.APP_APPLY_SUCCESS:
|
||||||
self.check_pending_app_reapply(context)
|
self.check_pending_app_reapply(context)
|
||||||
self._auto_update_app(context, app_name, managed_app=False)
|
self._auto_update_app(context, app_name)
|
||||||
|
|
||||||
LOG.debug("Periodic Task: _k8s_application_audit: Finished")
|
LOG.debug("Periodic Task: _k8s_application_audit: Finished")
|
||||||
|
|
||||||
|
|
|
@ -558,6 +558,8 @@ class StorageTierDependentTCs(base.FunctionalTest):
|
||||||
self.set_is_initial_config_patcher.return_value = True
|
self.set_is_initial_config_patcher.return_value = True
|
||||||
self.service = manager.ConductorManager('test-host', 'test-topic')
|
self.service = manager.ConductorManager('test-host', 'test-topic')
|
||||||
self.service.dbapi = dbapi.get_instance()
|
self.service.dbapi = dbapi.get_instance()
|
||||||
|
self.service._populate_app_bundle_metadata = mock.Mock()
|
||||||
|
self.service._initialize_ostree_inotify = mock.Mock()
|
||||||
self.context = context.get_admin_context()
|
self.context = context.get_admin_context()
|
||||||
self.dbapi = dbapi.get_instance()
|
self.dbapi = dbapi.get_instance()
|
||||||
self.system = dbutils.create_test_isystem()
|
self.system = dbutils.create_test_isystem()
|
||||||
|
|
|
@ -62,6 +62,8 @@ class UpdateCephCluster(base.DbTestCase):
|
||||||
self.mock_fix_crushmap.return_value = True
|
self.mock_fix_crushmap.return_value = True
|
||||||
|
|
||||||
self.service._sx_to_dx_post_migration_actions = mock.Mock()
|
self.service._sx_to_dx_post_migration_actions = mock.Mock()
|
||||||
|
self.service._populate_app_bundle_metadata = mock.Mock()
|
||||||
|
self.service._initialize_ostree_inotify = mock.Mock()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(UpdateCephCluster, self).tearDown()
|
super(UpdateCephCluster, self).tearDown()
|
||||||
|
|
|
@ -482,6 +482,8 @@ class ManagerTestCase(base.DbTestCase):
|
||||||
self.service._update_pxe_config = mock.Mock()
|
self.service._update_pxe_config = mock.Mock()
|
||||||
self.service._ceph_mon_create = mock.Mock()
|
self.service._ceph_mon_create = mock.Mock()
|
||||||
self.service._sx_to_dx_post_migration_actions = mock.Mock()
|
self.service._sx_to_dx_post_migration_actions = mock.Mock()
|
||||||
|
self.service._populate_app_bundle_metadata = mock.Mock()
|
||||||
|
self.service._initialize_ostree_inotify = mock.Mock()
|
||||||
self.alarm_raised = False
|
self.alarm_raised = False
|
||||||
self.kernel_alarms = {}
|
self.kernel_alarms = {}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue