233 lines
8.4 KiB
Python
233 lines
8.4 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2018 Red Hat Inc.
|
|
#
|
|
# 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 logging
|
|
import os
|
|
import pwd
|
|
import selinux
|
|
import stat
|
|
import sys
|
|
|
|
debug = os.getenv('__OS_DEBUG', 'false')
|
|
|
|
if debug.lower() == 'true':
|
|
loglevel = logging.DEBUG
|
|
else:
|
|
loglevel = logging.INFO
|
|
|
|
logging.basicConfig(stream=sys.stdout, level=loglevel)
|
|
LOG = logging.getLogger('nova_statedir')
|
|
|
|
|
|
class PathManager(object):
|
|
"""Helper class to manipulate ownership of a given path"""
|
|
def __init__(self, path):
|
|
self.path = path
|
|
self.uid = None
|
|
self.gid = None
|
|
self.is_dir = None
|
|
self.secontext = None
|
|
self._update()
|
|
|
|
def _update(self):
|
|
try:
|
|
statinfo = os.stat(self.path)
|
|
self.is_dir = stat.S_ISDIR(statinfo.st_mode)
|
|
self.uid = statinfo.st_uid
|
|
self.gid = statinfo.st_gid
|
|
self.secontext = selinux.lgetfilecon(self.path)[1]
|
|
except Exception:
|
|
LOG.exception('Could not update metadata for %s', self.path)
|
|
raise
|
|
|
|
def __str__(self):
|
|
return "uid: {} gid: {} path: {}{}".format(
|
|
self.uid,
|
|
self.gid,
|
|
self.path,
|
|
'/' if self.is_dir else ''
|
|
)
|
|
|
|
def has_owner(self, uid, gid):
|
|
return self.uid == uid and self.gid == gid
|
|
|
|
def has_either(self, uid, gid):
|
|
return self.uid == uid or self.gid == gid
|
|
|
|
def chown(self, uid, gid):
|
|
target_uid = -1
|
|
target_gid = -1
|
|
if self.uid != uid:
|
|
target_uid = uid
|
|
if self.gid != gid:
|
|
target_gid = gid
|
|
if (target_uid, target_gid) != (-1, -1):
|
|
LOG.info('Changing ownership of %s from %d:%d to %d:%d',
|
|
self.path,
|
|
self.uid,
|
|
self.gid,
|
|
self.uid if target_uid == -1 else target_uid,
|
|
self.gid if target_gid == -1 else target_gid)
|
|
try:
|
|
os.chown(self.path, target_uid, target_gid)
|
|
self._update()
|
|
except Exception:
|
|
LOG.exception('Could not change ownership of %s: ',
|
|
self.path)
|
|
raise
|
|
else:
|
|
LOG.info('Ownership of %s already %d:%d',
|
|
self.path,
|
|
uid,
|
|
gid)
|
|
|
|
def chcon(self, context):
|
|
# If dir returns whether to recusively set context
|
|
try:
|
|
try:
|
|
selinux.lsetfilecon(self.path, context)
|
|
LOG.info('Setting selinux context of %s to %s',
|
|
self.path, context)
|
|
return True
|
|
except OSError as e:
|
|
if self.is_dir and e.errno == 95:
|
|
# Operation not supported, assume NFS mount and skip
|
|
LOG.info('Setting selinux context not supported for %s',
|
|
self.path)
|
|
return False
|
|
else:
|
|
raise
|
|
except Exception:
|
|
LOG.exception('Could not set selinux context of %s to %s:',
|
|
self.path, context)
|
|
raise
|
|
|
|
|
|
class NovaStatedirOwnershipManager(object):
|
|
"""Class to manipulate the ownership of the nova statedir (/var/lib/nova).
|
|
|
|
The nova uid/gid differ on the host and container images. An upgrade
|
|
that switches from host systemd services to docker requires a change in
|
|
ownership. Previously this was a naive recursive chown, however this
|
|
causes issues if nova instance are shared via an NFS mount: any open
|
|
filehandles in qemu/libvirt fail with an I/O error (LP1778465).
|
|
|
|
Instead the upgrade/FFU ansible tasks now lay down a marker file when
|
|
stopping and disabling the host systemd services. We use this file to
|
|
determine the host nova uid/gid. We then walk the tree and update any
|
|
files that have the host uid/gid to the docker nova uid/gid. As files
|
|
owned by root/qemu etc... are ignored this avoids the issues with open
|
|
filehandles. The marker is removed once the tree has been walked.
|
|
|
|
For subsequent runs, or for a new deployment, we simply ensure that the
|
|
docker nova user/group owns all directories. This is required as the
|
|
directories are created with root ownership in host_prep_tasks (the
|
|
docker nova uid/gid is not known in this context).
|
|
"""
|
|
def __init__(self, statedir, upgrade_marker='upgrade_marker',
|
|
nova_user='nova', secontext_marker='../_nova_secontext'):
|
|
self.statedir = statedir
|
|
self.nova_user = nova_user
|
|
|
|
self.upgrade_marker_path = os.path.join(statedir, upgrade_marker)
|
|
self.secontext_marker_path = os.path.normpath(os.path.join(statedir, secontext_marker))
|
|
self.upgrade = os.path.exists(self.upgrade_marker_path)
|
|
|
|
self.target_uid, self.target_gid = self._get_nova_ids()
|
|
self.previous_uid, self.previous_gid = self._get_previous_nova_ids()
|
|
self.id_change = (self.target_uid, self.target_gid) != \
|
|
(self.previous_uid, self.previous_gid)
|
|
self.target_secontext = self._get_secontext()
|
|
|
|
def _get_nova_ids(self):
|
|
nova_uid, nova_gid = pwd.getpwnam(self.nova_user)[2:4]
|
|
return nova_uid, nova_gid
|
|
|
|
def _get_previous_nova_ids(self):
|
|
if self.upgrade:
|
|
statinfo = os.stat(self.upgrade_marker_path)
|
|
return statinfo.st_uid, statinfo.st_gid
|
|
else:
|
|
return self._get_nova_ids()
|
|
|
|
def _get_secontext(self):
|
|
if os.path.exists(self.secontext_marker_path):
|
|
return selinux.lgetfilecon(self.secontext_marker_path)[1]
|
|
else:
|
|
return None
|
|
|
|
def _walk(self, top, chcon=True):
|
|
for f in os.listdir(top):
|
|
pathname = os.path.join(top, f)
|
|
|
|
if pathname == self.upgrade_marker_path:
|
|
continue
|
|
|
|
try:
|
|
pathinfo = PathManager(pathname)
|
|
LOG.info("Checking %s", pathinfo)
|
|
if pathinfo.is_dir:
|
|
# Always chown the directories
|
|
pathinfo.chown(self.target_uid, self.target_gid)
|
|
chcon_r = chcon
|
|
if chcon:
|
|
chcon_r = pathinfo.chcon(self.target_secontext)
|
|
self._walk(pathname, chcon_r)
|
|
elif self.id_change:
|
|
# Only chown files if it's an upgrade and the file is owned by
|
|
# the host nova uid/gid
|
|
pathinfo.chown(
|
|
self.target_uid if pathinfo.uid == self.previous_uid
|
|
else pathinfo.uid,
|
|
self.target_gid if pathinfo.gid == self.previous_gid
|
|
else pathinfo.gid
|
|
)
|
|
if chcon:
|
|
pathinfo.chcon(self.target_secontext)
|
|
except Exception:
|
|
# Likely to have been caused by external systems
|
|
# interacting with this directory tree,
|
|
# especially on NFS e.g snapshot dirs.
|
|
# Just ignore it and continue on to the next entry
|
|
continue
|
|
|
|
def run(self):
|
|
LOG.info('Applying nova statedir ownership')
|
|
LOG.info('Target ownership for %s: %d:%d',
|
|
self.statedir,
|
|
self.target_uid,
|
|
self.target_gid)
|
|
|
|
pathinfo = PathManager(self.statedir)
|
|
LOG.info("Checking %s", pathinfo)
|
|
pathinfo.chown(self.target_uid, self.target_gid)
|
|
chcon = self.target_secontext is not None
|
|
|
|
if chcon:
|
|
pathinfo.chcon(self.target_secontext)
|
|
|
|
self._walk(self.statedir, chcon)
|
|
|
|
if self.upgrade:
|
|
LOG.info('Removing upgrade_marker %s',
|
|
self.upgrade_marker_path)
|
|
os.unlink(self.upgrade_marker_path)
|
|
|
|
LOG.info('Nova statedir ownership complete')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
NovaStatedirOwnershipManager('/var/lib/nova').run()
|