summaryrefslogtreecommitdiffstats
path: root/tools/xenserver/vm_vdi_cleaner.py
blob: b0e9d981c3c41e137618499c7b6e6e1e6af7f0cd (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#!/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 os
import sys
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 context
from nova import db
from nova import exception
from nova import flags
from nova.openstack.common import cfg
from nova.openstack.common import timeutils
from nova.virt.xenapi import connection as xenapi_conn


CONF = cfg.CONF
flags.DECLARE("resize_confirm_window", "nova.compute.manager")


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.api.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, 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'
    """
    if not CONF.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(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, 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, 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, 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, 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:
        if CONF.verbose:
            print "ORPHANED VDI (%s)" % vdi_uuid
        else:
            print vdi_uuid


def clean_orphaned_vdis(xenapi, vdi_uuids):
    """Clean orphaned VDIs."""
    for vdi_uuid in vdi_uuids:
        if CONF.verbose:
            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, exc:
            print >> sys.stderr, "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:
        if CONF.verbose:
            print "ORPHANED INSTANCE (%s)" % orphaned_instance.name
        else:
            print orphaned_instance.name


def clean_orphaned_instances(xenapi, orphaned_instances):
    """Clean orphaned instances."""
    for vm_ref, vm_rec, instance in orphaned_instances:
        if CONF.verbose:
            print "CLEANING INSTANCE (%s)" % instance.name

        cleanup_instance(xenapi, instance, vm_ref, vm_rec)


def main():
    """Main loop."""
    args = CONF(args=sys.argv,
                usage='%prog [options] [' + '|'.join(ALLOWED_COMMANDS) + ']')
    if len(args) < 2:
        CONF.print_usage()
        sys.exit(1)

    command = args[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`.")

    xenapi = xenapi_conn.XenAPIDriver()

    if command == "list-vdis":
        if CONF.verbose:
            print "Connected VDIs:\n"
        orphaned_vdi_uuids = find_orphaned_vdi_uuids(xenapi)
        if CONF.verbose:
            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()