diff options
| author | Josh Kearney <josh@jk0.org> | 2011-09-27 15:21:42 -0500 |
|---|---|---|
| committer | Josh Kearney <josh@jk0.org> | 2011-09-30 12:45:15 -0500 |
| commit | 04548b067c7c79602332fe2bc2dc89ed77cee7ac (patch) | |
| tree | a51b60a128da066d4708fab6fe464941cf88bd55 /tools/xenserver | |
| parent | f40d94e59f000a2937cea68663053739e721b80e (diff) | |
| download | nova-04548b067c7c79602332fe2bc2dc89ed77cee7ac.tar.gz nova-04548b067c7c79602332fe2bc2dc89ed77cee7ac.tar.xz nova-04548b067c7c79602332fe2bc2dc89ed77cee7ac.zip | |
Adds a script that can automatically delete orphaned VDIs. Also had to move some flags around to avoid circular imports.
Fixes bug 809614.
Change-Id: I635f7eef9ede45bee1ee4a62a3882b55d4222ee3
Diffstat (limited to 'tools/xenserver')
| -rwxr-xr-x | tools/xenserver/vm_vdi_cleaner.py | 376 |
1 files changed, 376 insertions, 0 deletions
diff --git a/tools/xenserver/vm_vdi_cleaner.py b/tools/xenserver/vm_vdi_cleaner.py new file mode 100755 index 000000000..9df12f12e --- /dev/null +++ b/tools/xenserver/vm_vdi_cleaner.py @@ -0,0 +1,376 @@ +#!/usr/bin/env python + +# Copyright 2011 OpenStack, LLC +# +# 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 optparse +import sys +import XenAPI + +from nova import context +from nova import db +from nova import exception +from nova import flags +from nova import utils + +from nova.virt.xenapi import vmops + + +flags.DECLARE("resize_confirm_window", "nova.compute.manager") +flags.DECLARE("xenapi_connection_url", "nova.virt.xenapi_conn") +flags.DECLARE("xenapi_connection_username", "nova.virt.xenapi_conn") +flags.DECLARE("xenapi_connection_password", "nova.virt.xenapi_conn") + +FLAGS = flags.FLAGS +# NOTE(sirp): Nova futzs with the sys.argv in order to provide default +# flagfile. To isolate this awful practice, we're supplying a dummy +# argument list. +dummy = ["fakearg"] +utils.default_flagfile(args=dummy) +FLAGS(dummy) + + +class UnrecognizedNameLabel(Exception): + pass + + +def parse_options(): + """Generate command line options.""" + + ALLOWED_COMMANDS = ["list-vdis", "clean-vdis", "list-instances", + "clean-instances", "test"] + arg_str = "|".join(ALLOWED_COMMANDS) + parser = optparse.OptionParser("%prog [options] [" + arg_str + "]") + parser.add_option("--verbose", action="store_true") + + options, args = parser.parse_args() + + if not args: + parser.print_usage() + sys.exit(1) + + return options, args + + +def get_instance_id_from_name_label(name_label, template): + """In order to derive the instance_id from the name label on the VM, we + take the following steps: + + 1. We substitute a dummy value in to the instance_name_template so we + can figure out the prefix and the suffix of the template (the + instance_id is between the two) + + 2. We delete the prefix and suffix from the name_label. + + 3. What's left *should* be the instance_id which we cast to an int + and return. + + >>> get_instance_id_from_name_label("", "instance-%08x") + Traceback (most recent call last): + ... + UnrecognizedNameLabel + + >>> get_instance_id_from_name_label("instance-00000001", "instance-%08x") + 1 + + >>> get_instance_id_from_name_label("instance-0000000A", "instance-%08x") + 10 + + >>> get_instance_id_from_name_label("instance-42-suffix", \ + "instance-%d-suffix") + 42 + """ + + # Interpolate template to figure out where to extract the instance_id from. + # The instance_id may be in hex "%x" or decimal "%d", so try decimal first + # then fall back to hex. + fake_instance_id = 123456789 + result = template % fake_instance_id + in_hex = False + base_10 = "%d" % fake_instance_id + + try: + prefix, suffix = result.split(base_10) + except ValueError: + base_16 = "%x" % fake_instance_id + prefix, suffix = result.split(base_16) + in_hex = True + + if prefix: + name_label = name_label.replace(prefix, '') + + if suffix: + name_label = name_label.replace(suffix, '') + + try: + if in_hex: + instance_id = int(name_label, 16) + else: + instance_id = int(name_label) + except ValueError: + raise UnrecognizedNameLabel(name_label) + + return instance_id + + +def find_orphaned_instances(session, verbose=False): + """Find and return a list of orphaned instances.""" + ctxt = context.get_admin_context() + orphaned_instances = [] + + for vm_rec in _get_applicable_vm_recs(session): + try: + instance_id = get_instance_id_from_name_label( + vm_rec["name_label"], FLAGS.instance_name_template) + except UnrecognizedNameLabel, exc: + print_xen_object("WARNING: Unrecognized VM", vm_rec, + indent_level=0, verbose=verbose) + continue + + try: + instance = db.api.instance_get(ctxt, instance_id) + except exception.InstanceNotFound: + # NOTE(jk0): Err on the side of caution here. If we don't know + # anything about the particular instance, print a warning and let + # the operator handle it manually. + print >> sys.stderr, "Instance %s not found" % instance_id + + # 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 utils.is_older_than(instance.updated_at, + FLAGS.zombie_instance_updated_at_window)) + if is_zombie_vm: + orphaned_instances.append(instance) + + return orphaned_instances + + +def cleanup_instance(session, instance): + """Delete orphaned instances.""" + vmops = VMOps(session) + network_info = None + vmops.destroy(instance, network_info) + + +def _get_applicable_vm_recs(session): + """An 'applicable' VM is one that is not a template and not the control + domain. + """ + for vm_ref in session.xenapi.VM.get_all(): + vm_rec = session.xenapi.VM.get_record(vm_ref) + + if vm_rec["is_a_template"] or vm_rec["is_control_domain"]: + continue + yield vm_rec + + +def print_xen_object(obj_type, obj, indent_level=0, spaces_per_indent=4, + verbose=False): + """Pretty-print a Xen object. + + Looks like: + + VM (abcd-abcd-abcd): 'name label here' + """ + if not verbose: + return + uuid = obj["uuid"] + try: + name_label = obj["name_label"] + except KeyError: + name_label = "" + msg = "%(obj_type)s (%(uuid)s) '%(name_label)s'" % locals() + indent = " " * spaces_per_indent * indent_level + print "".join([indent, msg]) + + +def _find_vdis_connected_to_vm(session, connected_vdi_uuids, verbose=False): + """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, + verbose=verbose) + 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 = session.xenapi.VDI.get_by_uuid( + parent_vdi_uuid) + cur_vdi_rec = session.xenapi.VDI.get_record( + cur_vdi_ref) + else: + break + + for vm_rec in _get_applicable_vm_recs(session): + indent_level = 0 + print_xen_object("VM", vm_rec, indent_level=indent_level, + verbose=verbose) + + vbd_refs = vm_rec["VBDs"] + for vbd_ref in vbd_refs: + vbd_rec = session.xenapi.VBD.get_record(vbd_ref) + + indent_level = 1 + print_xen_object("VBD", vbd_rec, indent_level=indent_level, + verbose=verbose) + + vbd_vdi_ref = vbd_rec["VDI"] + + if _is_null_ref(vbd_vdi_ref): + continue + + vdi_rec = session.xenapi.VDI.get_record(vbd_vdi_ref) + + _add_vdi_and_parents_to_connected(vdi_rec, indent_level) + + +def _find_all_vdis_and_system_vdis(session, all_vdi_uuids, connected_vdi_uuids, + verbose=False): + """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 session.xenapi.VDI.get_all(): + vdi_rec = session.xenapi.VDI.get_record(vdi_ref) + 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, + verbose=verbose) + connected_vdi_uuids.add(vdi_uuid) + elif not vdi_rec["managed"]: + print_xen_object("UNMANAGED VDI", vdi_rec, indent_level=0, + verbose=verbose) + connected_vdi_uuids.add(vdi_uuid) + + +def find_orphaned_vdi_uuids(session, verbose=False): + """Walk VM -> VBD -> VDI change and accumulate connected VDIs.""" + connected_vdi_uuids = set() + + _find_vdis_connected_to_vm(session, connected_vdi_uuids, verbose=verbose) + + all_vdi_uuids = set() + _find_all_vdis_and_system_vdis(session, all_vdi_uuids, connected_vdi_uuids, + verbose=verbose) + + orphaned_vdi_uuids = all_vdi_uuids - connected_vdi_uuids + return orphaned_vdi_uuids + + +def list_orphaned_vdis(vdi_uuids, verbose=False): + """List orphaned VDIs.""" + for vdi_uuid in vdi_uuids: + if verbose: + print "ORPHANED VDI (%s)" % vdi_uuid + else: + print vdi_uuid + + +def clean_orphaned_vdis(session, vdi_uuids, verbose=False): + """Clean orphaned VDIs.""" + for vdi_uuid in vdi_uuids: + if verbose: + print "CLEANING VDI (%s)" % vdi_uuid + + vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid) + try: + session.xenapi.VDI.destroy(vdi_ref) + except XenAPI.Failure, exc: + print >> sys.stderr, "Skipping %s: %s" % (vdi_uuid, exc) + + +def list_orphaned_instances(orphaned_instances, verbose=False): + """List orphaned instances.""" + for orphaned_instance in orphaned_instances: + if verbose: + print "ORPHANED INSTANCE (%s)" % instance.name + else: + print instance.name + + +def clean_orphaned_instances(session, orphaned_instances, verbose=False): + """Clean orphaned instances.""" + for instance in orphaned_instances: + if verbose: + print "CLEANING INSTANCE (%s)" % instance.name + + cleanup_instance(session, instance) + + +def main(): + """Main loop.""" + options, args = parse_options() + verbose = options.verbose + command = args[0] + + if FLAGS.zombie_instance_updated_at_window < FLAGS.resize_confirm_window: + raise Exception("`zombie_instance_updated_at_window` has to be longer" + " than `resize_confirm_window`.") + + session = XenAPI.Session(FLAGS.xenapi_connection_url) + session.xenapi.login_with_password(FLAGS.xenapi_connection_username, + FLAGS.xenapi_connection_password) + + if command == "list-vdis": + if verbose: + print "Connected VDIs:\n" + orphaned_vdi_uuids = find_orphaned_vdi_uuids(session, verbose=verbose) + if verbose: + print "\nOprhaned VDIs:\n" + list_orphaned_vdis(orphaned_vdi_uuids, verbose=verbose) + elif command == "clean-vdis": + orphaned_vdi_uuids = find_orphaned_vdi_uuids(session, verbose=verbose) + clean_orphaned_vdis(session, orphaned_vdi_uuids, verbose=verbose) + elif command == "list-instances": + orphaned_instances = find_orphaned_instances(session, verbose=verbose) + list_orphaned_instances(orphaned_instances, verbose=verbose) + elif command == "clean-instances": + orphaned_instances = find_orphaned_instances(session, verbose=verbose) + clean_orphaned_instances(session, orphaned_instances, + verbose=verbose) + elif command == "test": + doctest.testmod() + else: + print "Unknown command '%s'" % command + sys.exit(1) + + +if __name__ == "__main__": + main() |
