0353c51c68
When adding a dashboard to grafana the containers aren't restarted when they should be. This is due to a bug in Kolla where the logic to determine whether or the container needs to be restarted fails in the case where the file does not exist in the container. This patch adds more comprehensive checks for container restarts in the set_configs.py file. This patch also adds a test to ensure that the functions work as expected. Closes-Bug: #1997984 Co-Authored-By: Will Szumski <will@stackhpc.com> Change-Id: I67f5f12700d7b55f26bff81e9b54559303da6d83 (cherry picked from commit d9a6c5f3901e078a30d341397ac57d867d8191cc)
436 lines
15 KiB
Python
436 lines
15 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# 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 argparse
|
|
import glob
|
|
import grp
|
|
import json
|
|
import logging
|
|
import os
|
|
import pwd
|
|
import shutil
|
|
import stat
|
|
import sys
|
|
|
|
|
|
# TODO(rhallisey): add docstring.
|
|
logging.basicConfig()
|
|
LOG = logging.getLogger(__name__)
|
|
LOG.setLevel(logging.INFO)
|
|
|
|
|
|
class ExitingException(Exception):
|
|
def __init__(self, message, exit_code=1):
|
|
super(ExitingException, self).__init__(message)
|
|
self.exit_code = exit_code
|
|
|
|
|
|
class ImmutableConfig(ExitingException):
|
|
pass
|
|
|
|
|
|
class InvalidConfig(ExitingException):
|
|
pass
|
|
|
|
|
|
class MissingRequiredSource(ExitingException):
|
|
pass
|
|
|
|
|
|
class UserNotFound(ExitingException):
|
|
pass
|
|
|
|
|
|
class ConfigFileBadState(ExitingException):
|
|
pass
|
|
|
|
|
|
class ConfigFile(object):
|
|
|
|
def __init__(self, source, dest, owner=None, perm=None, optional=False,
|
|
preserve_properties=False, merge=False):
|
|
self.source = source
|
|
self.dest = dest
|
|
self.owner = owner
|
|
self.perm = perm
|
|
self.optional = optional
|
|
self.merge = merge
|
|
self.preserve_properties = preserve_properties
|
|
|
|
def __str__(self):
|
|
return '<ConfigFile source:"{}" dest:"{}">'.format(self.source,
|
|
self.dest)
|
|
|
|
def _copy_file(self, source, dest):
|
|
self._delete_path(dest)
|
|
# dest endswith / means copy the <source> to <dest> folder
|
|
LOG.info('Copying %s to %s', source, dest)
|
|
if self.merge and self.preserve_properties and os.path.islink(source):
|
|
link_target = os.readlink(source)
|
|
os.symlink(link_target, dest)
|
|
else:
|
|
shutil.copy(source, dest)
|
|
self._set_properties(source, dest)
|
|
|
|
def _merge_directories(self, source, dest):
|
|
if os.path.isdir(source):
|
|
if os.path.lexists(dest) and not os.path.isdir(dest):
|
|
self._delete_path(dest)
|
|
if not os.path.isdir(dest):
|
|
LOG.info('Creating directory %s', dest)
|
|
os.makedirs(dest)
|
|
self._set_properties(source, dest)
|
|
|
|
dir_content = os.listdir(source)
|
|
for to_copy in dir_content:
|
|
self._merge_directories(os.path.join(source, to_copy),
|
|
os.path.join(dest, to_copy))
|
|
else:
|
|
self._copy_file(source, dest)
|
|
|
|
def _delete_path(self, path):
|
|
if not os.path.lexists(path):
|
|
return
|
|
LOG.info('Deleting %s', path)
|
|
if os.path.isdir(path):
|
|
shutil.rmtree(path)
|
|
else:
|
|
os.remove(path)
|
|
|
|
def _create_parent_dirs(self, path):
|
|
parent_path = os.path.dirname(path)
|
|
if not os.path.exists(parent_path):
|
|
os.makedirs(parent_path)
|
|
|
|
def _set_properties(self, source, dest):
|
|
if self.preserve_properties:
|
|
self._set_properties_from_file(source, dest)
|
|
else:
|
|
self._set_properties_from_conf(dest)
|
|
|
|
def _set_properties_from_file(self, source, dest):
|
|
LOG.info('Copying permissions from %s onto %s', source, dest)
|
|
shutil.copystat(source, dest)
|
|
stat = os.stat(source)
|
|
os.chown(dest, stat.st_uid, stat.st_gid)
|
|
|
|
def _set_properties_from_conf(self, path):
|
|
config = {'permissions':
|
|
[{'owner': self.owner, 'path': path, 'perm': self.perm}]}
|
|
handle_permissions(config)
|
|
|
|
def copy(self):
|
|
|
|
sources = glob.glob(self.source)
|
|
|
|
if not self.optional and not sources:
|
|
raise MissingRequiredSource('%s file is not found' % self.source)
|
|
# skip when there is no sources and optional
|
|
elif self.optional and not sources:
|
|
return
|
|
|
|
for source in sources:
|
|
dest = self.dest
|
|
# dest endswith / means copy the <source> into <dest> folder,
|
|
# otherwise means copy the source to dest
|
|
if dest.endswith(os.sep):
|
|
dest = os.path.join(dest, os.path.basename(source))
|
|
if not self.merge:
|
|
self._delete_path(dest)
|
|
self._create_parent_dirs(dest)
|
|
try:
|
|
self._merge_directories(source, dest)
|
|
except OSError:
|
|
# If a source is tried to merge with a read-only mount, it
|
|
# may throw an OSError. Because we don't print the source or
|
|
# dest anywhere, let's catch the exception and log a better
|
|
# message to help with tracking down the issue.
|
|
LOG.error('Unable to merge %s with %s', source, dest)
|
|
raise
|
|
|
|
def _cmp_file(self, source, dest):
|
|
# check exsit
|
|
if (os.path.exists(source) and
|
|
not os.path.exists(dest)):
|
|
return False
|
|
if (os.path.exists(dest) and
|
|
not os.path.exists(source)):
|
|
return False
|
|
# check content
|
|
with open(source, 'rb') as f1, open(dest, 'rb') as f2:
|
|
if f1.read() != f2.read():
|
|
LOG.error('The content of source file(%s) and'
|
|
' dest file(%s) are not equal.', source, dest)
|
|
return False
|
|
# check perm
|
|
file_stat = os.stat(dest)
|
|
actual_perm = oct(file_stat.st_mode)[-4:]
|
|
if self.perm != actual_perm:
|
|
LOG.error('Dest file does not have expected perm: %s, actual: %s',
|
|
self.perm, actual_perm)
|
|
return False
|
|
# check owner
|
|
desired_user, desired_group = user_group(self.owner)
|
|
actual_user = pwd.getpwuid(file_stat.st_uid)
|
|
if actual_user.pw_name != desired_user:
|
|
LOG.error('Dest file does not have expected user: %s,'
|
|
' actual: %s ', desired_user, actual_user.pw_name)
|
|
return False
|
|
actual_group = grp.getgrgid(file_stat.st_gid)
|
|
if actual_group.gr_name != desired_group:
|
|
LOG.error('Dest file does not have expected group: %s,'
|
|
' actual: %s ', desired_group, actual_group.gr_name)
|
|
return False
|
|
return True
|
|
|
|
def _cmp_dir(self, source, dest):
|
|
for root, dirs, files in os.walk(source):
|
|
for dir_ in dirs:
|
|
full_path = os.path.join(root, dir_)
|
|
dest_full_path = os.path.join(dest, os.path.relpath(source,
|
|
full_path))
|
|
dir_stat = os.stat(dest_full_path)
|
|
actual_perm = oct(dir_stat.st_mode)[-4:]
|
|
if self.perm != actual_perm:
|
|
LOG.error('Dest dir does not have expected perm: %s,'
|
|
' actual %s', self.perm, actual_perm)
|
|
return False
|
|
for file_ in files:
|
|
full_path = os.path.join(root, file_)
|
|
dest_full_path = os.path.join(dest, os.path.relpath(full_path,
|
|
source))
|
|
if not self._cmp_file(full_path, dest_full_path):
|
|
return False
|
|
return True
|
|
|
|
def check(self):
|
|
bad_state_files = []
|
|
sources = glob.glob(self.source)
|
|
|
|
if not sources and not self.optional:
|
|
raise MissingRequiredSource('%s file is not found' % self.source)
|
|
elif self.optional and not sources:
|
|
return
|
|
|
|
for source in sources:
|
|
dest = self.dest
|
|
# dest endswith / means copy the <source> into <dest> folder,
|
|
# otherwise means copy the source to dest
|
|
if dest.endswith(os.sep):
|
|
dest = os.path.join(dest, os.path.basename(source))
|
|
if os.path.isdir(source):
|
|
if not self._cmp_dir(source, dest):
|
|
bad_state_files.append(source)
|
|
elif not self._cmp_file(source, dest):
|
|
bad_state_files.append(source)
|
|
if len(bad_state_files) != 0:
|
|
msg = 'Following files are in bad state: %s' % bad_state_files
|
|
raise ConfigFileBadState(msg)
|
|
|
|
|
|
def validate_config(config):
|
|
required_keys = {'source', 'dest'}
|
|
|
|
if 'command' not in config:
|
|
raise InvalidConfig('Config is missing required "command" key')
|
|
|
|
# Validate config sections
|
|
for data in config.get('config_files', list()):
|
|
# Verify required keys exist.
|
|
if not set(data.keys()) >= required_keys:
|
|
message = 'Config is missing required keys: %s' % required_keys
|
|
raise InvalidConfig(message)
|
|
if ('owner' not in data or 'perm' not in data) \
|
|
and not data.get('preserve_properties', False):
|
|
raise InvalidConfig(
|
|
'Config needs preserve_properties or owner and perm')
|
|
|
|
|
|
def validate_source(data):
|
|
source = data.get('source')
|
|
|
|
# Only check existence if no wildcard found
|
|
if '*' not in source:
|
|
if not os.path.exists(source):
|
|
if data.get('optional'):
|
|
LOG.info("%s does not exist, but is not required", source)
|
|
return False
|
|
else:
|
|
raise MissingRequiredSource(
|
|
"The source to copy does not exist: %s" % source)
|
|
|
|
return True
|
|
|
|
|
|
def load_config():
|
|
def load_from_file():
|
|
config_file = '/var/lib/kolla/config_files/config.json'
|
|
LOG.info("Loading config file at %s", config_file)
|
|
|
|
# Attempt to read config file
|
|
with open(config_file) as f:
|
|
try:
|
|
return json.load(f)
|
|
except ValueError:
|
|
raise InvalidConfig(
|
|
"Invalid json file found at %s" % config_file)
|
|
except IOError as e:
|
|
raise InvalidConfig(
|
|
"Could not read file %s: %r" % (config_file, e))
|
|
|
|
config = load_from_file()
|
|
|
|
LOG.info('Validating config file')
|
|
validate_config(config)
|
|
return config
|
|
|
|
|
|
def copy_config(config):
|
|
if 'config_files' in config:
|
|
LOG.info('Copying service configuration files')
|
|
for data in config['config_files']:
|
|
config_file = ConfigFile(**data)
|
|
config_file.copy()
|
|
else:
|
|
LOG.debug('No files to copy found in config')
|
|
|
|
LOG.info('Writing out command to execute')
|
|
LOG.debug("Command is: %s", config['command'])
|
|
# The value from the 'command' key will be written to '/run_command'
|
|
cmd = '/run_command'
|
|
with open(cmd, 'w+') as f:
|
|
f.write(config['command'])
|
|
# Make sure the generated file is readable by all users
|
|
try:
|
|
os.chmod(cmd, 0o644)
|
|
except OSError:
|
|
LOG.exception('Failed to set permission of %s to 0o644', cmd)
|
|
|
|
|
|
def user_group(owner):
|
|
if ':' in owner:
|
|
user, group = owner.split(':', 1)
|
|
if not group:
|
|
group = user
|
|
else:
|
|
user, group = owner, owner
|
|
return user, group
|
|
|
|
|
|
def handle_permissions(config):
|
|
for permission in config.get('permissions', list()):
|
|
path = permission.get('path')
|
|
owner = permission.get('owner')
|
|
recurse = permission.get('recurse', False)
|
|
perm = permission.get('perm')
|
|
|
|
desired_user, desired_group = user_group(owner)
|
|
uid = pwd.getpwnam(desired_user).pw_uid
|
|
gid = grp.getgrnam(desired_group).gr_gid
|
|
|
|
def set_perms(path, uid, gid, perm):
|
|
LOG.info('Setting permission for %s', path)
|
|
if not os.path.exists(path):
|
|
LOG.warning('%s does not exist', path)
|
|
return
|
|
|
|
try:
|
|
os.chown(path, uid, gid)
|
|
except OSError:
|
|
LOG.exception('Failed to change ownership of %s to %s:%s',
|
|
path, uid, gid)
|
|
|
|
if perm:
|
|
# NOTE(Jeffrey4l): py3 need '0oXXX' format for octal literals,
|
|
# and py2 support such format too.
|
|
if len(perm) == 4 and perm[1] != 'o':
|
|
perm = ''.join([perm[:1], 'o', perm[1:]])
|
|
perm = int(perm, base=0)
|
|
|
|
# Ensure execute bit on directory if read bit is set
|
|
if os.path.isdir(path):
|
|
if perm & stat.S_IRUSR:
|
|
perm |= stat.S_IXUSR
|
|
if perm & stat.S_IRGRP:
|
|
perm |= stat.S_IXGRP
|
|
if perm & stat.S_IROTH:
|
|
perm |= stat.S_IXOTH
|
|
|
|
try:
|
|
os.chmod(path, perm)
|
|
except OSError:
|
|
LOG.exception('Failed to set permission of %s to %s',
|
|
path, perm)
|
|
|
|
for dest in glob.glob(path):
|
|
set_perms(dest, uid, gid, perm)
|
|
if recurse and os.path.isdir(dest):
|
|
for root, dirs, files in os.walk(dest):
|
|
for dir_ in dirs:
|
|
set_perms(os.path.join(root, dir_), uid, gid, perm)
|
|
for file_ in files:
|
|
set_perms(os.path.join(root, file_), uid, gid, perm)
|
|
|
|
|
|
def execute_config_strategy(config):
|
|
config_strategy = os.environ.get("KOLLA_CONFIG_STRATEGY")
|
|
LOG.info("Kolla config strategy set to: %s", config_strategy)
|
|
if config_strategy == "COPY_ALWAYS":
|
|
copy_config(config)
|
|
handle_permissions(config)
|
|
elif config_strategy == "COPY_ONCE":
|
|
if os.path.exists('/configured'):
|
|
raise ImmutableConfig(
|
|
"The config strategy prevents copying new configs",
|
|
exit_code=0)
|
|
else:
|
|
copy_config(config)
|
|
handle_permissions(config)
|
|
os.mknod('/configured')
|
|
else:
|
|
raise InvalidConfig('KOLLA_CONFIG_STRATEGY is not set properly')
|
|
|
|
|
|
def execute_config_check(config):
|
|
for data in config.get('config_files', []):
|
|
config_file = ConfigFile(**data)
|
|
config_file.check()
|
|
|
|
|
|
def main():
|
|
try:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--check',
|
|
action='store_true',
|
|
required=False,
|
|
help='Check whether the configs changed')
|
|
args = parser.parse_args()
|
|
config = load_config()
|
|
|
|
if args.check:
|
|
execute_config_check(config)
|
|
else:
|
|
execute_config_strategy(config)
|
|
except ExitingException as e:
|
|
LOG.error("%s: %s", e.__class__.__name__, e)
|
|
return e.exit_code
|
|
except Exception:
|
|
LOG.exception('Unexpected error:')
|
|
return 2
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|