PageRenderTime 37ms CodeModel.GetById 8ms RepoModel.GetById 0ms app.codeStats 0ms

/tools/xenserver/vm_vdi_cleaner.py

https://github.com/blamarvt/nova
Python | 385 lines | 338 code | 19 blank | 28 comment | 2 complexity | 0c9ae4e0646b9a192dfe2ae1d592c874 MD5 | raw file
  1. #!/usr/bin/env python
  2. # Copyright 2011 OpenStack, LLC
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. """vm_vdi_cleaner.py - List or clean orphaned VDIs/instances on XenServer."""
  16. import doctest
  17. import optparse
  18. import sys
  19. import XenAPI
  20. from nova import context
  21. from nova import db
  22. from nova import exception
  23. from nova import flags
  24. from nova import utils
  25. from nova.virt import xenapi_conn
  26. flags.DECLARE("resize_confirm_window", "nova.compute.manager")
  27. flags.DECLARE("xenapi_connection_url", "nova.virt.xenapi_conn")
  28. flags.DECLARE("xenapi_connection_username", "nova.virt.xenapi_conn")
  29. flags.DECLARE("xenapi_connection_password", "nova.virt.xenapi_conn")
  30. FLAGS = flags.FLAGS
  31. # NOTE(sirp): Nova futzs with the sys.argv in order to provide default
  32. # flagfile. To isolate this awful practice, we're supplying a dummy
  33. # argument list.
  34. dummy = ["fakearg"]
  35. utils.default_flagfile(args=dummy)
  36. FLAGS(dummy)
  37. class UnrecognizedNameLabel(Exception):
  38. pass
  39. def parse_options():
  40. """Generate command line options."""
  41. ALLOWED_COMMANDS = ["list-vdis", "clean-vdis", "list-instances",
  42. "clean-instances", "test"]
  43. arg_str = "|".join(ALLOWED_COMMANDS)
  44. parser = optparse.OptionParser("%prog [options] [" + arg_str + "]")
  45. parser.add_option("--verbose", action="store_true")
  46. options, args = parser.parse_args()
  47. if not args:
  48. parser.print_usage()
  49. sys.exit(1)
  50. return options, args
  51. def get_instance_id_from_name_label(name_label, template):
  52. """In order to derive the instance_id from the name label on the VM, we
  53. take the following steps:
  54. 1. We substitute a dummy value in to the instance_name_template so we
  55. can figure out the prefix and the suffix of the template (the
  56. instance_id is between the two)
  57. 2. We delete the prefix and suffix from the name_label.
  58. 3. What's left *should* be the instance_id which we cast to an int
  59. and return.
  60. >>> get_instance_id_from_name_label("", "instance-%08x")
  61. Traceback (most recent call last):
  62. ...
  63. UnrecognizedNameLabel
  64. >>> get_instance_id_from_name_label("instance-00000001", "instance-%08x")
  65. 1
  66. >>> get_instance_id_from_name_label("instance-0000000A", "instance-%08x")
  67. 10
  68. >>> get_instance_id_from_name_label("instance-42-suffix", \
  69. "instance-%d-suffix")
  70. 42
  71. """
  72. # Interpolate template to figure out where to extract the instance_id from.
  73. # The instance_id may be in hex "%x" or decimal "%d", so try decimal first
  74. # then fall back to hex.
  75. fake_instance_id = 123456789
  76. result = template % fake_instance_id
  77. in_hex = False
  78. base_10 = "%d" % fake_instance_id
  79. try:
  80. prefix, suffix = result.split(base_10)
  81. except ValueError:
  82. base_16 = "%x" % fake_instance_id
  83. prefix, suffix = result.split(base_16)
  84. in_hex = True
  85. if prefix:
  86. name_label = name_label.replace(prefix, '')
  87. if suffix:
  88. name_label = name_label.replace(suffix, '')
  89. try:
  90. if in_hex:
  91. instance_id = int(name_label, 16)
  92. else:
  93. instance_id = int(name_label)
  94. except ValueError:
  95. raise UnrecognizedNameLabel(name_label)
  96. return instance_id
  97. def find_orphaned_instances(session, verbose=False):
  98. """Find and return a list of orphaned instances."""
  99. ctxt = context.get_admin_context()
  100. ctxt.read_deleted = True
  101. orphaned_instances = []
  102. for vm_rec in _get_applicable_vm_recs(session):
  103. try:
  104. instance_id = get_instance_id_from_name_label(
  105. vm_rec["name_label"], FLAGS.instance_name_template)
  106. except UnrecognizedNameLabel, exc:
  107. print_xen_object("WARNING: Unrecognized VM", vm_rec,
  108. indent_level=0, verbose=verbose)
  109. continue
  110. try:
  111. instance = db.api.instance_get(ctxt, instance_id)
  112. except exception.InstanceNotFound:
  113. # NOTE(jk0): Err on the side of caution here. If we don't know
  114. # anything about the particular instance, ignore it.
  115. print_xen_object("INFO: Ignoring VM", vm_rec, indent_level=0,
  116. verbose=verbose)
  117. continue
  118. # NOTE(jk0): This would be triggered if a VM was deleted but the
  119. # actual deletion process failed somewhere along the line.
  120. is_active_and_deleting = (instance.vm_state == "active" and
  121. instance.task_state == "deleting")
  122. # NOTE(jk0): A zombie VM is an instance that is not active and hasn't
  123. # been updated in over the specified period.
  124. is_zombie_vm = (instance.vm_state != "active"
  125. and utils.is_older_than(instance.updated_at,
  126. FLAGS.zombie_instance_updated_at_window))
  127. if is_active_and_deleting or is_zombie_vm:
  128. orphaned_instances.append(instance)
  129. return orphaned_instances
  130. def cleanup_instance(session, instance):
  131. """Delete orphaned instances."""
  132. network_info = None
  133. connection = xenapi_conn.get_connection(_)
  134. connection.destroy(instance, network_info)
  135. def _get_applicable_vm_recs(session):
  136. """An 'applicable' VM is one that is not a template and not the control
  137. domain.
  138. """
  139. for vm_ref in session.xenapi.VM.get_all():
  140. vm_rec = session.xenapi.VM.get_record(vm_ref)
  141. if vm_rec["is_a_template"] or vm_rec["is_control_domain"]:
  142. continue
  143. yield vm_rec
  144. def print_xen_object(obj_type, obj, indent_level=0, spaces_per_indent=4,
  145. verbose=False):
  146. """Pretty-print a Xen object.
  147. Looks like:
  148. VM (abcd-abcd-abcd): 'name label here'
  149. """
  150. if not verbose:
  151. return
  152. uuid = obj["uuid"]
  153. try:
  154. name_label = obj["name_label"]
  155. except KeyError:
  156. name_label = ""
  157. msg = "%(obj_type)s (%(uuid)s) '%(name_label)s'" % locals()
  158. indent = " " * spaces_per_indent * indent_level
  159. print "".join([indent, msg])
  160. def _find_vdis_connected_to_vm(session, connected_vdi_uuids, verbose=False):
  161. """Find VDIs which are connected to VBDs which are connected to VMs."""
  162. def _is_null_ref(ref):
  163. return ref == "OpaqueRef:NULL"
  164. def _add_vdi_and_parents_to_connected(vdi_rec, indent_level):
  165. indent_level += 1
  166. vdi_and_parent_uuids = []
  167. cur_vdi_rec = vdi_rec
  168. while True:
  169. cur_vdi_uuid = cur_vdi_rec["uuid"]
  170. print_xen_object("VDI", vdi_rec, indent_level=indent_level,
  171. verbose=verbose)
  172. connected_vdi_uuids.add(cur_vdi_uuid)
  173. vdi_and_parent_uuids.append(cur_vdi_uuid)
  174. try:
  175. parent_vdi_uuid = vdi_rec["sm_config"]["vhd-parent"]
  176. except KeyError:
  177. parent_vdi_uuid = None
  178. # NOTE(sirp): VDI's can have themselves as a parent?!
  179. if parent_vdi_uuid and parent_vdi_uuid != cur_vdi_uuid:
  180. indent_level += 1
  181. cur_vdi_ref = session.xenapi.VDI.get_by_uuid(
  182. parent_vdi_uuid)
  183. cur_vdi_rec = session.xenapi.VDI.get_record(
  184. cur_vdi_ref)
  185. else:
  186. break
  187. for vm_rec in _get_applicable_vm_recs(session):
  188. indent_level = 0
  189. print_xen_object("VM", vm_rec, indent_level=indent_level,
  190. verbose=verbose)
  191. vbd_refs = vm_rec["VBDs"]
  192. for vbd_ref in vbd_refs:
  193. vbd_rec = session.xenapi.VBD.get_record(vbd_ref)
  194. indent_level = 1
  195. print_xen_object("VBD", vbd_rec, indent_level=indent_level,
  196. verbose=verbose)
  197. vbd_vdi_ref = vbd_rec["VDI"]
  198. if _is_null_ref(vbd_vdi_ref):
  199. continue
  200. vdi_rec = session.xenapi.VDI.get_record(vbd_vdi_ref)
  201. _add_vdi_and_parents_to_connected(vdi_rec, indent_level)
  202. def _find_all_vdis_and_system_vdis(session, all_vdi_uuids, connected_vdi_uuids,
  203. verbose=False):
  204. """Collects all VDIs and adds system VDIs to the connected set."""
  205. def _system_owned(vdi_rec):
  206. vdi_name = vdi_rec["name_label"]
  207. return (vdi_name.startswith("USB") or
  208. vdi_name.endswith(".iso") or
  209. vdi_rec["type"] == "system")
  210. for vdi_ref in session.xenapi.VDI.get_all():
  211. vdi_rec = session.xenapi.VDI.get_record(vdi_ref)
  212. vdi_uuid = vdi_rec["uuid"]
  213. all_vdi_uuids.add(vdi_uuid)
  214. # System owned and non-managed VDIs should be considered 'connected'
  215. # for our purposes.
  216. if _system_owned(vdi_rec):
  217. print_xen_object("SYSTEM VDI", vdi_rec, indent_level=0,
  218. verbose=verbose)
  219. connected_vdi_uuids.add(vdi_uuid)
  220. elif not vdi_rec["managed"]:
  221. print_xen_object("UNMANAGED VDI", vdi_rec, indent_level=0,
  222. verbose=verbose)
  223. connected_vdi_uuids.add(vdi_uuid)
  224. def find_orphaned_vdi_uuids(session, verbose=False):
  225. """Walk VM -> VBD -> VDI change and accumulate connected VDIs."""
  226. connected_vdi_uuids = set()
  227. _find_vdis_connected_to_vm(session, connected_vdi_uuids, verbose=verbose)
  228. all_vdi_uuids = set()
  229. _find_all_vdis_and_system_vdis(session, all_vdi_uuids, connected_vdi_uuids,
  230. verbose=verbose)
  231. orphaned_vdi_uuids = all_vdi_uuids - connected_vdi_uuids
  232. return orphaned_vdi_uuids
  233. def list_orphaned_vdis(vdi_uuids, verbose=False):
  234. """List orphaned VDIs."""
  235. for vdi_uuid in vdi_uuids:
  236. if verbose:
  237. print "ORPHANED VDI (%s)" % vdi_uuid
  238. else:
  239. print vdi_uuid
  240. def clean_orphaned_vdis(session, vdi_uuids, verbose=False):
  241. """Clean orphaned VDIs."""
  242. for vdi_uuid in vdi_uuids:
  243. if verbose:
  244. print "CLEANING VDI (%s)" % vdi_uuid
  245. vdi_ref = session.xenapi.VDI.get_by_uuid(vdi_uuid)
  246. try:
  247. session.xenapi.VDI.destroy(vdi_ref)
  248. except XenAPI.Failure, exc:
  249. print >> sys.stderr, "Skipping %s: %s" % (vdi_uuid, exc)
  250. def list_orphaned_instances(orphaned_instances, verbose=False):
  251. """List orphaned instances."""
  252. for orphaned_instance in orphaned_instances:
  253. if verbose:
  254. print "ORPHANED INSTANCE (%s)" % orphaned_instance.name
  255. else:
  256. print orphaned_instance.name
  257. def clean_orphaned_instances(session, orphaned_instances, verbose=False):
  258. """Clean orphaned instances."""
  259. for instance in orphaned_instances:
  260. if verbose:
  261. print "CLEANING INSTANCE (%s)" % instance.name
  262. cleanup_instance(session, instance)
  263. def main():
  264. """Main loop."""
  265. options, args = parse_options()
  266. verbose = options.verbose
  267. command = args[0]
  268. if FLAGS.zombie_instance_updated_at_window < FLAGS.resize_confirm_window:
  269. raise Exception("`zombie_instance_updated_at_window` has to be longer"
  270. " than `resize_confirm_window`.")
  271. session = XenAPI.Session(FLAGS.xenapi_connection_url)
  272. session.xenapi.login_with_password(FLAGS.xenapi_connection_username,
  273. FLAGS.xenapi_connection_password)
  274. if command == "list-vdis":
  275. if verbose:
  276. print "Connected VDIs:\n"
  277. orphaned_vdi_uuids = find_orphaned_vdi_uuids(session, verbose=verbose)
  278. if verbose:
  279. print "\nOprhaned VDIs:\n"
  280. list_orphaned_vdis(orphaned_vdi_uuids, verbose=verbose)
  281. elif command == "clean-vdis":
  282. orphaned_vdi_uuids = find_orphaned_vdi_uuids(session, verbose=verbose)
  283. clean_orphaned_vdis(session, orphaned_vdi_uuids, verbose=verbose)
  284. elif command == "list-instances":
  285. orphaned_instances = find_orphaned_instances(session, verbose=verbose)
  286. list_orphaned_instances(orphaned_instances, verbose=verbose)
  287. elif command == "clean-instances":
  288. orphaned_instances = find_orphaned_instances(session, verbose=verbose)
  289. clean_orphaned_instances(session, orphaned_instances,
  290. verbose=verbose)
  291. elif command == "test":
  292. doctest.testmod()
  293. else:
  294. print "Unknown command '%s'" % command
  295. sys.exit(1)
  296. if __name__ == "__main__":
  297. main()