tools: Remove xenserver tooling
These will not be used in a world without the XenAPI driver. Change-Id: I5bc3c7855b817c4ce2b8919c76be80cceabeec9e Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
parent
58f7582c63
commit
45c0ea4a3e
|
@ -1,85 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
destroy_cached_images.py
|
||||
|
||||
This script is used to clean up Glance images that are cached in the SR. By
|
||||
default, this script will only cleanup unused cached images.
|
||||
|
||||
Options:
|
||||
|
||||
--dry_run - Don't actually destroy the VDIs
|
||||
--all_cached - Destroy all cached images instead of just unused cached
|
||||
images.
|
||||
--keep_days - N - Only remove those cached images which were created
|
||||
more than N days ago.
|
||||
"""
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from os_xenapi.client import session
|
||||
from oslo_config import cfg
|
||||
|
||||
# If ../nova/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
POSSIBLE_TOPDIR = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(POSSIBLE_TOPDIR, 'nova', '__init__.py')):
|
||||
sys.path.insert(0, POSSIBLE_TOPDIR)
|
||||
|
||||
import nova.conf
|
||||
from nova import config
|
||||
from nova.virt.xenapi import vm_utils
|
||||
|
||||
destroy_opts = [
|
||||
cfg.BoolOpt('all_cached',
|
||||
default=False,
|
||||
help='Destroy all cached images instead of just unused cached'
|
||||
' images.'),
|
||||
cfg.BoolOpt('dry_run',
|
||||
default=False,
|
||||
help='Don\'t actually delete the VDIs.'),
|
||||
cfg.IntOpt('keep_days',
|
||||
default=0,
|
||||
help='Destroy cached images which were'
|
||||
' created over keep_days.')
|
||||
]
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
CONF.register_cli_opts(destroy_opts)
|
||||
|
||||
def main():
|
||||
config.parse_args(sys.argv)
|
||||
|
||||
_session = session.XenAPISession(CONF.xenserver.connection_url,
|
||||
CONF.xenserver.connection_username,
|
||||
CONF.xenserver.connection_password)
|
||||
|
||||
sr_ref = vm_utils.safe_find_sr(_session)
|
||||
destroyed = vm_utils.destroy_cached_images(
|
||||
_session, sr_ref, all_cached=CONF.all_cached,
|
||||
dry_run=CONF.dry_run, keep_days=CONF.keep_days)
|
||||
|
||||
if '--verbose' in sys.argv:
|
||||
print('\n'.join(destroyed))
|
||||
|
||||
print("Destroyed %d cached VDIs" % len(destroyed))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,103 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
One-time script to populate VDI.other_config.
|
||||
|
||||
We use metadata stored in VDI.other_config to associate a VDI with a given
|
||||
instance so that we may safely cleanup orphaned VDIs.
|
||||
|
||||
We had a bug in the code that meant that the vast majority of VDIs created
|
||||
would not have the other_config populated.
|
||||
|
||||
After deploying the fixed code, this script is intended to be run against all
|
||||
compute-workers in a cluster so that existing VDIs can have their other_configs
|
||||
populated.
|
||||
|
||||
Run on compute-worker (not Dom0):
|
||||
|
||||
python ./tools/xenserver/populate_other_config.py [--dry-run]
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
possible_topdir = os.getcwd()
|
||||
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from nova import config
|
||||
from nova.virt import virtapi
|
||||
from nova.virt.xenapi import driver as xenapi_driver
|
||||
from nova.virt.xenapi import vm_utils
|
||||
|
||||
cli_opts = [
|
||||
cfg.BoolOpt('dry-run',
|
||||
default=False,
|
||||
help='Whether to actually update other_config.'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_cli_opts(cli_opts)
|
||||
|
||||
|
||||
def main():
|
||||
config.parse_args(sys.argv)
|
||||
|
||||
xenapi = xenapi_driver.XenAPIDriver(virtapi.VirtAPI())
|
||||
session = xenapi._session
|
||||
|
||||
vdi_refs = session.call_xenapi('VDI.get_all')
|
||||
for vdi_ref in vdi_refs:
|
||||
vdi_rec = session.call_xenapi('VDI.get_record', vdi_ref)
|
||||
|
||||
other_config = vdi_rec['other_config']
|
||||
|
||||
# Already set...
|
||||
if 'nova_instance_uuid' in other_config:
|
||||
continue
|
||||
|
||||
name_label = vdi_rec['name_label']
|
||||
|
||||
# We only want name-labels of form instance-<UUID>-[optional-suffix]
|
||||
if not name_label.startswith('instance-'):
|
||||
continue
|
||||
|
||||
# Parse out UUID
|
||||
instance_uuid = name_label.replace('instance-', '')[:36]
|
||||
if not uuidutils.is_uuid_like(instance_uuid):
|
||||
print("error: name label '%s' wasn't UUID-like" % name_label)
|
||||
continue
|
||||
|
||||
vdi_type = vdi_rec['name_description']
|
||||
|
||||
# We don't need a full instance record, just the UUID
|
||||
instance = {'uuid': instance_uuid}
|
||||
|
||||
if not CONF.dry_run:
|
||||
vm_utils._set_vdi_info(session, vdi_ref, vdi_type, name_label,
|
||||
vdi_type, instance)
|
||||
|
||||
print("Setting other_config for instance_uuid=%s vdi_uuid=%s" % (
|
||||
instance_uuid, vdi_rec['uuid']))
|
||||
|
||||
if CONF.dry_run:
|
||||
print("Dry run completed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,69 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -eux
|
||||
|
||||
# Script to rotate console logs
|
||||
#
|
||||
# Should be run on Dom0, with cron, every minute:
|
||||
# * * * * * /root/rotate_xen_guest_logs.sh
|
||||
#
|
||||
# Should clear out the guest logs on every boot
|
||||
# because the domain ids may get re-used for a
|
||||
# different tenant after the reboot
|
||||
#
|
||||
# /var/log/xen/guest should be mounted into a
|
||||
# small loopback device to stop any guest being
|
||||
# able to fill dom0 file system
|
||||
|
||||
log_dir="/var/log/xen/guest"
|
||||
kb=1024
|
||||
max_size_bytes=$(($kb*$kb))
|
||||
truncated_size_bytes=$((5*$kb))
|
||||
syslog_tag='rotate_xen_guest_logs'
|
||||
|
||||
log_file_base="${log_dir}/console."
|
||||
|
||||
# Only delete log files older than this number of minutes
|
||||
# to avoid a race where Xen creates the domain and starts
|
||||
# logging before the XAPI VM start returns (and allows us
|
||||
# to preserve the log file using last_dom_id)
|
||||
min_logfile_age=10
|
||||
|
||||
# Ensure logging is setup correctly for all domains
|
||||
xenstore-write /local/logconsole/@ "${log_file_base}%d"
|
||||
|
||||
# Grab the list of logs now to prevent a race where the domain is
|
||||
# started after we get the valid last_dom_ids, but before the logs are
|
||||
# deleted. Add spaces to ensure we can do containment tests below
|
||||
current_logs=$(find "$log_dir" -type f)
|
||||
|
||||
# Ensure the last_dom_id is set + updated for all running VMs
|
||||
for vm in $(xe vm-list power-state=running --minimal | tr ',' ' '); do
|
||||
xe vm-param-set uuid=$vm other-config:last_dom_id=$(xe vm-param-get uuid=$vm param-name=dom-id)
|
||||
done
|
||||
|
||||
# Get the last_dom_id for all VMs
|
||||
valid_last_dom_ids=$(xe vm-list params=other-config --minimal | tr ';,' '\n\n' | grep last_dom_id | sed -e 's/last_dom_id: //g' | xargs)
|
||||
echo "Valid dom IDs: $valid_last_dom_ids" | /usr/bin/logger -t $syslog_tag
|
||||
|
||||
# Remove old console files that do not correspond to valid last_dom_id's
|
||||
allowed_consoles=".*console.\(${valid_last_dom_ids// /\\|}\)$"
|
||||
delete_logs=`find "$log_dir" -type f -mmin +${min_logfile_age} -not -regex "$allowed_consoles"`
|
||||
for log in $delete_logs; do
|
||||
if echo "$current_logs" | grep -q -w "$log"; then
|
||||
echo "Deleting: $log" | /usr/bin/logger -t $syslog_tag
|
||||
rm $log
|
||||
fi
|
||||
done
|
||||
|
||||
# Truncate all remaining logs
|
||||
for log in `find "$log_dir" -type f -regex '.*console.*' -size +${max_size_bytes}c`; do
|
||||
echo "Truncating log: $log" | /usr/bin/logger -t $syslog_tag
|
||||
tmp="$log.tmp"
|
||||
tail -c $truncated_size_bytes "$log" > "$tmp"
|
||||
mv -f "$tmp" "$log"
|
||||
|
||||
# Notify xen that it needs to reload the file
|
||||
domid="${log##*.}"
|
||||
xenstore-write /local/logconsole/$domid "$log"
|
||||
xenstore-rm /local/logconsole/$domid
|
||||
done
|
|
@ -1,181 +0,0 @@
|
|||
# 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.
|
||||
|
||||
"""
|
||||
This script concurrently builds and migrates instances. This can be useful when
|
||||
troubleshooting race-conditions in virt-layer code.
|
||||
|
||||
Expects:
|
||||
|
||||
novarc to be sourced in the environment
|
||||
|
||||
Helper Script for Xen Dom0:
|
||||
|
||||
# cat /tmp/destroy_cache_vdis
|
||||
#!/bin/bash
|
||||
xe vdi-list | grep "Glance Image" -C1 | grep "^uuid" | awk '{print $5}' |
|
||||
xargs -n1 -I{} xe vdi-destroy uuid={}
|
||||
"""
|
||||
import argparse
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
DOM0_CLEANUP_SCRIPT = "/tmp/destroy_cache_vdis"
|
||||
|
||||
|
||||
def run(cmd):
|
||||
ret = subprocess.call(cmd, shell=True)
|
||||
if ret != 0:
|
||||
sys.stderr.write("Command exited non-zero: %s" % cmd)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def server_built(server_name, image_name, flavor=1, cleanup=True):
|
||||
run("nova boot --image=%s --flavor=%s"
|
||||
" --poll %s" % (image_name, flavor, server_name))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if cleanup:
|
||||
run("nova delete %s" % server_name)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def snapshot_taken(server_name, snapshot_name, cleanup=True):
|
||||
run("nova image-create %s %s"
|
||||
" --poll" % (server_name, snapshot_name))
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
if cleanup:
|
||||
run("nova image-delete %s" % snapshot_name)
|
||||
|
||||
|
||||
def migrate_server(server_name):
|
||||
run("nova migrate %s --poll" % server_name)
|
||||
|
||||
cmd = "nova list | grep %s | awk '{print $6}'" % server_name
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
||||
stdout, stderr = proc.communicate()
|
||||
status = stdout.strip()
|
||||
if status.upper() != 'VERIFY_RESIZE':
|
||||
sys.stderr.write("Server %s failed to rebuild" % server_name)
|
||||
return False
|
||||
|
||||
# Confirm the resize
|
||||
run("nova resize-confirm %s" % server_name)
|
||||
return True
|
||||
|
||||
|
||||
def test_migrate(context):
|
||||
count, args = context
|
||||
server_name = "server%d" % count
|
||||
cleanup = args.cleanup
|
||||
with server_built(server_name, args.image, cleanup=cleanup):
|
||||
# Migrate A -> B
|
||||
result = migrate_server(server_name)
|
||||
if not result:
|
||||
return False
|
||||
|
||||
# Migrate B -> A
|
||||
return migrate_server(server_name)
|
||||
|
||||
|
||||
def rebuild_server(server_name, snapshot_name):
|
||||
run("nova rebuild %s %s --poll" % (server_name, snapshot_name))
|
||||
|
||||
cmd = "nova list | grep %s | awk '{print $6}'" % server_name
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
|
||||
stdout, stderr = proc.communicate()
|
||||
status = stdout.strip()
|
||||
if status != 'ACTIVE':
|
||||
sys.stderr.write("Server %s failed to rebuild" % server_name)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def test_rebuild(context):
|
||||
count, args = context
|
||||
server_name = "server%d" % count
|
||||
snapshot_name = "snap%d" % count
|
||||
cleanup = args.cleanup
|
||||
with server_built(server_name, args.image, cleanup=cleanup):
|
||||
with snapshot_taken(server_name, snapshot_name, cleanup=cleanup):
|
||||
return rebuild_server(server_name, snapshot_name)
|
||||
|
||||
|
||||
def _parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Test Nova for Race Conditions.')
|
||||
|
||||
parser.add_argument('tests', metavar='TESTS', type=str, nargs='*',
|
||||
default=['rebuild', 'migrate'],
|
||||
help='tests to run: [rebuilt|migrate]')
|
||||
|
||||
parser.add_argument('-i', '--image', help="image to build from",
|
||||
required=True)
|
||||
parser.add_argument('-n', '--num-runs', type=int, help="number of runs",
|
||||
default=1)
|
||||
parser.add_argument('-c', '--concurrency', type=int, default=5,
|
||||
help="number of concurrent processes")
|
||||
parser.add_argument('--no-cleanup', action='store_false', dest="cleanup",
|
||||
default=True)
|
||||
parser.add_argument('-d', '--dom0-ips',
|
||||
help="IP of dom0's to run cleanup script")
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
dom0_cleanup_script = DOM0_CLEANUP_SCRIPT
|
||||
args = _parse_args()
|
||||
|
||||
if args.dom0_ips:
|
||||
dom0_ips = args.dom0_ips.split(',')
|
||||
else:
|
||||
dom0_ips = []
|
||||
|
||||
start_time = time.time()
|
||||
batch_size = min(args.num_runs, args.concurrency)
|
||||
pool = multiprocessing.Pool(processes=args.concurrency)
|
||||
|
||||
results = []
|
||||
for test in args.tests:
|
||||
test_func = globals().get("test_%s" % test)
|
||||
if not test_func:
|
||||
sys.stderr.write("test '%s' not found" % test)
|
||||
sys.exit(1)
|
||||
|
||||
contexts = [(x, args) for x in range(args.num_runs)]
|
||||
|
||||
try:
|
||||
results += pool.map(test_func, contexts)
|
||||
finally:
|
||||
if args.cleanup:
|
||||
for dom0_ip in dom0_ips:
|
||||
run("ssh root@%s %s" % (dom0_ip, dom0_cleanup_script))
|
||||
|
||||
success = all(results)
|
||||
result = "SUCCESS" if success else "FAILED"
|
||||
|
||||
duration = time.time() - start_time
|
||||
print("%s, finished in %.2f secs" % (result, duration))
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,128 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This script is designed to cleanup any VHDs (and their descendents) which have
|
||||
a bad parent pointer.
|
||||
|
||||
The script needs to be run in the dom0 of the affected host.
|
||||
|
||||
The available actions are:
|
||||
|
||||
- print: display the filenames of the affected VHDs
|
||||
- delete: remove the affected VHDs
|
||||
- move: move the affected VHDs out of the SR into another directory
|
||||
"""
|
||||
import glob
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class ExecutionFailed(Exception):
|
||||
def __init__(self, returncode, stdout, stderr, max_stream_length=32):
|
||||
self.returncode = returncode
|
||||
self.stdout = stdout[:max_stream_length]
|
||||
self.stderr = stderr[:max_stream_length]
|
||||
self.max_stream_length = max_stream_length
|
||||
|
||||
def __repr__(self):
|
||||
return "<ExecutionFailed returncode=%s out='%s' stderr='%s'>" % (
|
||||
self.returncode, self.stdout, self.stderr)
|
||||
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
def execute(cmd, ok_exit_codes=None):
|
||||
if ok_exit_codes is None:
|
||||
ok_exit_codes = [0]
|
||||
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
(stdout, stderr) = proc.communicate()
|
||||
|
||||
if proc.returncode not in ok_exit_codes:
|
||||
raise ExecutionFailed(proc.returncode, stdout, stderr)
|
||||
|
||||
return proc.returncode, stdout, stderr
|
||||
|
||||
|
||||
def usage():
|
||||
print("usage: %s <SR PATH> <print|delete|move>" % sys.argv[0])
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
usage()
|
||||
|
||||
sr_path = sys.argv[1]
|
||||
action = sys.argv[2]
|
||||
|
||||
if action not in ('print', 'delete', 'move'):
|
||||
usage()
|
||||
|
||||
if action == 'move':
|
||||
if len(sys.argv) < 4:
|
||||
print("error: must specify where to move bad VHDs")
|
||||
sys.exit(1)
|
||||
|
||||
bad_vhd_path = sys.argv[3]
|
||||
if not os.path.exists(bad_vhd_path):
|
||||
os.makedirs(bad_vhd_path)
|
||||
|
||||
bad_leaves = []
|
||||
descendents = {}
|
||||
|
||||
for fname in glob.glob(os.path.join(sr_path, "*.vhd")):
|
||||
(returncode, stdout, stderr) = execute(
|
||||
['vhd-util', 'query', '-n', fname, '-p'], ok_exit_codes=[0, 22])
|
||||
|
||||
stdout = stdout.strip()
|
||||
|
||||
if stdout.endswith('.vhd'):
|
||||
try:
|
||||
descendents[stdout].append(fname)
|
||||
except KeyError:
|
||||
descendents[stdout] = [fname]
|
||||
elif 'query failed' in stdout:
|
||||
bad_leaves.append(fname)
|
||||
|
||||
def walk_vhds(root):
|
||||
yield root
|
||||
if root in descendents:
|
||||
for child in descendents[root]:
|
||||
for vhd in walk_vhds(child):
|
||||
yield vhd
|
||||
|
||||
for bad_leaf in bad_leaves:
|
||||
for bad_vhd in walk_vhds(bad_leaf):
|
||||
print(bad_vhd)
|
||||
if action == "print":
|
||||
pass
|
||||
elif action == "delete":
|
||||
os.unlink(bad_vhd)
|
||||
elif action == "move":
|
||||
new_path = os.path.join(bad_vhd_path,
|
||||
os.path.basename(bad_vhd))
|
||||
os.rename(bad_vhd, new_path)
|
||||
else:
|
||||
raise Exception("invalid action %s" % action)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -1,316 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""vm_vdi_cleaner.py - List or clean orphaned VDIs/instances on XenServer."""
|
||||
|
||||
import doctest
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_utils import timeutils
|
||||
import XenAPI
|
||||
|
||||
possible_topdir = os.getcwd()
|
||||
if os.path.exists(os.path.join(possible_topdir, "nova", "__init__.py")):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from nova import config
|
||||
from nova import context
|
||||
import nova.conf
|
||||
from nova import db
|
||||
from nova import exception
|
||||
from nova.virt import virtapi
|
||||
from nova.virt.xenapi import driver as xenapi_driver
|
||||
|
||||
|
||||
cleaner_opts = [
|
||||
cfg.IntOpt('zombie_instance_updated_at_window',
|
||||
default=172800,
|
||||
help='Number of seconds zombie instances are cleaned up.'),
|
||||
]
|
||||
|
||||
cli_opt = cfg.StrOpt('command',
|
||||
help='Cleaner command')
|
||||
|
||||
CONF = nova.conf.CONF
|
||||
CONF.register_opts(cleaner_opts)
|
||||
CONF.register_cli_opt(cli_opt)
|
||||
|
||||
|
||||
ALLOWED_COMMANDS = ["list-vdis", "clean-vdis", "list-instances",
|
||||
"clean-instances", "test"]
|
||||
|
||||
|
||||
def call_xenapi(xenapi, method, *args):
|
||||
"""Make a call to xapi."""
|
||||
return xenapi._session.call_xenapi(method, *args)
|
||||
|
||||
|
||||
def find_orphaned_instances(xenapi):
|
||||
"""Find and return a list of orphaned instances."""
|
||||
ctxt = context.get_admin_context(read_deleted="only")
|
||||
|
||||
orphaned_instances = []
|
||||
|
||||
for vm_ref, vm_rec in _get_applicable_vm_recs(xenapi):
|
||||
try:
|
||||
uuid = vm_rec['other_config']['nova_uuid']
|
||||
instance = db.instance_get_by_uuid(ctxt, uuid)
|
||||
except (KeyError, exception.InstanceNotFound):
|
||||
# NOTE(jk0): Err on the side of caution here. If we don't know
|
||||
# anything about the particular instance, ignore it.
|
||||
print_xen_object("INFO: Ignoring VM", vm_rec, indent_level=0)
|
||||
continue
|
||||
|
||||
# NOTE(jk0): This would be triggered if a VM was deleted but the
|
||||
# actual deletion process failed somewhere along the line.
|
||||
is_active_and_deleting = (instance.vm_state == "active" and
|
||||
instance.task_state == "deleting")
|
||||
|
||||
# NOTE(jk0): A zombie VM is an instance that is not active and hasn't
|
||||
# been updated in over the specified period.
|
||||
is_zombie_vm = (instance.vm_state != "active"
|
||||
and timeutils.is_older_than(instance.updated_at,
|
||||
CONF.zombie_instance_updated_at_window))
|
||||
|
||||
if is_active_and_deleting or is_zombie_vm:
|
||||
orphaned_instances.append((vm_ref, vm_rec, instance))
|
||||
|
||||
return orphaned_instances
|
||||
|
||||
|
||||
def cleanup_instance(xenapi, instance, vm_ref, vm_rec):
|
||||
"""Delete orphaned instances."""
|
||||
xenapi._vmops._destroy(instance, vm_ref)
|
||||
|
||||
|
||||
def _get_applicable_vm_recs(xenapi):
|
||||
"""An 'applicable' VM is one that is not a template and not the control
|
||||
domain.
|
||||
"""
|
||||
for vm_ref in call_xenapi(xenapi, 'VM.get_all'):
|
||||
try:
|
||||
vm_rec = call_xenapi(xenapi, 'VM.get_record', vm_ref)
|
||||
except XenAPI.Failure as e:
|
||||
if e.details[0] != 'HANDLE_INVALID':
|
||||
raise
|
||||
continue
|
||||
|
||||
if vm_rec["is_a_template"] or vm_rec["is_control_domain"]:
|
||||
continue
|
||||
yield vm_ref, vm_rec
|
||||
|
||||
|
||||
def print_xen_object(obj_type, obj, indent_level=0, spaces_per_indent=4):
|
||||
"""Pretty-print a Xen object.
|
||||
|
||||
Looks like:
|
||||
|
||||
VM (abcd-abcd-abcd): 'name label here'
|
||||
"""
|
||||
uuid = obj["uuid"]
|
||||
try:
|
||||
name_label = obj["name_label"]
|
||||
except KeyError:
|
||||
name_label = ""
|
||||
msg = "%s (%s) '%s'" % (obj_type, uuid, name_label)
|
||||
indent = " " * spaces_per_indent * indent_level
|
||||
print("".join([indent, msg]))
|
||||
|
||||
|
||||
def _find_vdis_connected_to_vm(xenapi, connected_vdi_uuids):
|
||||
"""Find VDIs which are connected to VBDs which are connected to VMs."""
|
||||
def _is_null_ref(ref):
|
||||
return ref == "OpaqueRef:NULL"
|
||||
|
||||
def _add_vdi_and_parents_to_connected(vdi_rec, indent_level):
|
||||
indent_level += 1
|
||||
|
||||
vdi_and_parent_uuids = []
|
||||
cur_vdi_rec = vdi_rec
|
||||
while True:
|
||||
cur_vdi_uuid = cur_vdi_rec["uuid"]
|
||||
print_xen_object("VDI", vdi_rec, indent_level=indent_level)
|
||||
connected_vdi_uuids.add(cur_vdi_uuid)
|
||||
vdi_and_parent_uuids.append(cur_vdi_uuid)
|
||||
|
||||
try:
|
||||
parent_vdi_uuid = vdi_rec["sm_config"]["vhd-parent"]
|
||||
except KeyError:
|
||||
parent_vdi_uuid = None
|
||||
|
||||
# NOTE(sirp): VDI's can have themselves as a parent?!
|
||||
if parent_vdi_uuid and parent_vdi_uuid != cur_vdi_uuid:
|
||||
indent_level += 1
|
||||
cur_vdi_ref = call_xenapi(xenapi, 'VDI.get_by_uuid',
|
||||
parent_vdi_uuid)
|
||||
try:
|
||||
cur_vdi_rec = call_xenapi(xenapi, 'VDI.get_record',
|
||||
cur_vdi_ref)
|
||||
except XenAPI.Failure as e:
|
||||
if e.details[0] != 'HANDLE_INVALID':
|
||||
raise
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
for vm_ref, vm_rec in _get_applicable_vm_recs(xenapi):
|
||||
indent_level = 0
|
||||
print_xen_object("VM", vm_rec, indent_level=indent_level)
|
||||
|
||||
vbd_refs = vm_rec["VBDs"]
|
||||
for vbd_ref in vbd_refs:
|
||||
try:
|
||||
vbd_rec = call_xenapi(xenapi, 'VBD.get_record', vbd_ref)
|
||||
except XenAPI.Failure as e:
|
||||
if e.details[0] != 'HANDLE_INVALID':
|
||||
raise
|
||||
continue
|
||||
|
||||
indent_level = 1
|
||||
print_xen_object("VBD", vbd_rec, indent_level=indent_level)
|
||||
|
||||
vbd_vdi_ref = vbd_rec["VDI"]
|
||||
|
||||
if _is_null_ref(vbd_vdi_ref):
|
||||
continue
|
||||
|
||||
try:
|
||||
vdi_rec = call_xenapi(xenapi, 'VDI.get_record', vbd_vdi_ref)
|
||||
except XenAPI.Failure as e:
|
||||
if e.details[0] != 'HANDLE_INVALID':
|
||||
raise
|
||||
continue
|
||||
|
||||
_add_vdi_and_parents_to_connected(vdi_rec, indent_level)
|
||||
|
||||
|
||||
def _find_all_vdis_and_system_vdis(xenapi, all_vdi_uuids, connected_vdi_uuids):
|
||||
"""Collects all VDIs and adds system VDIs to the connected set."""
|
||||
def _system_owned(vdi_rec):
|
||||
vdi_name = vdi_rec["name_label"]
|
||||
return (vdi_name.startswith("USB") or
|
||||
vdi_name.endswith(".iso") or
|
||||
vdi_rec["type"] == "system")
|
||||
|
||||
for vdi_ref in call_xenapi(xenapi, 'VDI.get_all'):
|
||||
try:
|
||||
vdi_rec = call_xenapi(xenapi, 'VDI.get_record', vdi_ref)
|
||||
except XenAPI.Failure as e:
|
||||
if e.details[0] != 'HANDLE_INVALID':
|
||||
raise
|
||||
continue
|
||||
vdi_uuid = vdi_rec["uuid"]
|
||||
all_vdi_uuids.add(vdi_uuid)
|
||||
|
||||
# System owned and non-managed VDIs should be considered 'connected'
|
||||
# for our purposes.
|
||||
if _system_owned(vdi_rec):
|
||||
print_xen_object("SYSTEM VDI", vdi_rec, indent_level=0)
|
||||
connected_vdi_uuids.add(vdi_uuid)
|
||||
elif not vdi_rec["managed"]:
|
||||
print_xen_object("UNMANAGED VDI", vdi_rec, indent_level=0)
|
||||
connected_vdi_uuids.add(vdi_uuid)
|
||||
|
||||
|
||||
def find_orphaned_vdi_uuids(xenapi):
|
||||
"""Walk VM -> VBD -> VDI change and accumulate connected VDIs."""
|
||||
connected_vdi_uuids = set()
|
||||
|
||||
_find_vdis_connected_to_vm(xenapi, connected_vdi_uuids)
|
||||
|
||||
all_vdi_uuids = set()
|
||||
_find_all_vdis_and_system_vdis(xenapi, all_vdi_uuids, connected_vdi_uuids)
|
||||
|
||||
orphaned_vdi_uuids = all_vdi_uuids - connected_vdi_uuids
|
||||
return orphaned_vdi_uuids
|
||||
|
||||
|
||||
def list_orphaned_vdis(vdi_uuids):
|
||||
"""List orphaned VDIs."""
|
||||
for vdi_uuid in vdi_uuids:
|
||||
print("ORPHANED VDI (%s)" % vdi_uuid)
|
||||
|
||||
|
||||
def clean_orphaned_vdis(xenapi, vdi_uuids):
|
||||
"""Clean orphaned VDIs."""
|
||||
for vdi_uuid in vdi_uuids:
|
||||
print("CLEANING VDI (%s)" % vdi_uuid)
|
||||
|
||||
vdi_ref = call_xenapi(xenapi, 'VDI.get_by_uuid', vdi_uuid)
|
||||
try:
|
||||
call_xenapi(xenapi, 'VDI.destroy', vdi_ref)
|
||||
except XenAPI.Failure as exc:
|
||||
sys.stderr.write("Skipping %s: %s" % (vdi_uuid, exc))
|
||||
|
||||
|
||||
def list_orphaned_instances(orphaned_instances):
|
||||
"""List orphaned instances."""
|
||||
for vm_ref, vm_rec, orphaned_instance in orphaned_instances:
|
||||
print("ORPHANED INSTANCE (%s)" % orphaned_instance.name)
|
||||
|
||||
|
||||
def clean_orphaned_instances(xenapi, orphaned_instances):
|
||||
"""Clean orphaned instances."""
|
||||
for vm_ref, vm_rec, instance in orphaned_instances:
|
||||
print("CLEANING INSTANCE (%s)" % instance.name)
|
||||
|
||||
cleanup_instance(xenapi, instance, vm_ref, vm_rec)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main loop."""
|
||||
config.parse_args(sys.argv)
|
||||
args = CONF(args=sys.argv[1:], usage='%(prog)s [options] --command={' +
|
||||
'|'.join(ALLOWED_COMMANDS) + '}')
|
||||
|
||||
command = CONF.command
|
||||
if not command or command not in ALLOWED_COMMANDS:
|
||||
CONF.print_usage()
|
||||
sys.exit(1)
|
||||
|
||||
if CONF.zombie_instance_updated_at_window < CONF.resize_confirm_window:
|
||||
raise Exception("`zombie_instance_updated_at_window` has to be longer"
|
||||
" than `resize_confirm_window`.")
|
||||
|
||||
# NOTE(blamar) This tool does not require DB access, so passing in the
|
||||
# 'abstract' VirtAPI class is acceptable
|
||||
xenapi = xenapi_driver.XenAPIDriver(virtapi.VirtAPI())
|
||||
|
||||
if command == "list-vdis":
|
||||
print("Connected VDIs:\n")
|
||||
orphaned_vdi_uuids = find_orphaned_vdi_uuids(xenapi)
|
||||
print("\nOrphaned VDIs:\n")
|
||||
list_orphaned_vdis(orphaned_vdi_uuids)
|
||||
elif command == "clean-vdis":
|
||||
orphaned_vdi_uuids = find_orphaned_vdi_uuids(xenapi)
|
||||
clean_orphaned_vdis(xenapi, orphaned_vdi_uuids)
|
||||
elif command == "list-instances":
|
||||
orphaned_instances = find_orphaned_instances(xenapi)
|
||||
list_orphaned_instances(orphaned_instances)
|
||||
elif command == "clean-instances":
|
||||
orphaned_instances = find_orphaned_instances(xenapi)
|
||||
clean_orphaned_instances(xenapi, orphaned_instances)
|
||||
elif command == "test":
|
||||
doctest.testmod()
|
||||
else:
|
||||
print("Unknown command '%s'" % command)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
2
tox.ini
2
tox.ini
|
@ -260,7 +260,7 @@ commands = bandit -r nova -x tests -n 5 -ll
|
|||
# these that have to be fixed
|
||||
enable-extensions = H106,H203,H904
|
||||
ignore = E121,E122,E123,E124,E125,E126,E127,E128,E129,E131,E251,H405,W504,E731,H238
|
||||
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,tools/xenserver*,releasenotes
|
||||
exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,releasenotes
|
||||
# To get a list of functions that are more complex than 25, set max-complexity
|
||||
# to 25 and run 'tox -epep8'.
|
||||
# 39 is currently the most complex thing we have
|
||||
|
|
Loading…
Reference in New Issue