| 1 | #!/usr/bin/python |
|---|
| 2 | # -*- coding: utf-8 -*- |
|---|
| 3 | ################################################################################ |
|---|
| 4 | # XenServer VM automatic snapshot rotation script |
|---|
| 5 | # Copyright (c) 2009 Michael Conigliaro <mike [at] conigliaro [dot] org> |
|---|
| 6 | # |
|---|
| 7 | # This program is free software; you can redistribute it and/or modify |
|---|
| 8 | # it under the terms of the GNU General Public License as published by |
|---|
| 9 | # the Free Software Foundation; either version 2 of the License, or |
|---|
| 10 | # (at your option) any later version. |
|---|
| 11 | # |
|---|
| 12 | # This program is distributed in the hope that it will be useful, |
|---|
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|---|
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|---|
| 15 | # GNU General Public License for more details. |
|---|
| 16 | # |
|---|
| 17 | # You should have received a copy of the GNU General Public License |
|---|
| 18 | # along with this program; if not, write to the Free Software |
|---|
| 19 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
|---|
| 20 | ################################################################################ |
|---|
| 21 | import getpass |
|---|
| 22 | import logging |
|---|
| 23 | import logging.handlers |
|---|
| 24 | import optparse |
|---|
| 25 | import re |
|---|
| 26 | import sys |
|---|
| 27 | import time |
|---|
| 28 | |
|---|
| 29 | import XenAPI |
|---|
| 30 | |
|---|
| 31 | |
|---|
| 32 | APP_VERSION = "1.2" |
|---|
| 33 | APP_AUTHOR = "Michael T. Conigliaro <mike [at] conigliaro [dot] org>" |
|---|
| 34 | APP_WEBSITE = "http://conigliaro.org/" |
|---|
| 35 | |
|---|
| 36 | |
|---|
| 37 | def snapshot(): |
|---|
| 38 | """Take a snapshot of each VM.""" |
|---|
| 39 | |
|---|
| 40 | log.debug("Starting snapshot routine") |
|---|
| 41 | |
|---|
| 42 | re_vmnames = re.compile(options.vm_regex) |
|---|
| 43 | |
|---|
| 44 | all_vms = session.xenapi.VM.get_all_records() |
|---|
| 45 | |
|---|
| 46 | # loop through vm record map |
|---|
| 47 | for vm in all_vms: |
|---|
| 48 | vm_record = all_vms[vm] |
|---|
| 49 | |
|---|
| 50 | # select appropriate vm |
|---|
| 51 | if re_vmnames.match(vm_record["name_label"]) and \ |
|---|
| 52 | not vm_record["is_a_template"] and \ |
|---|
| 53 | not vm_record["is_control_domain"]: |
|---|
| 54 | log.debug("Selecting VM: %s (%s)" % |
|---|
| 55 | (vm_record["name_label"], vm_record["uuid"])) |
|---|
| 56 | |
|---|
| 57 | # snapshot name is based on time/tag |
|---|
| 58 | snapshot_name = "%s: %s %s" % (vm_record["name_label"], |
|---|
| 59 | time.strftime("%Y-%m-%d %H:%M:%S"), |
|---|
| 60 | options.snapshot_tag) |
|---|
| 61 | |
|---|
| 62 | # create snapshot |
|---|
| 63 | log.info("Creating VM snapshot: %s" % snapshot_name) |
|---|
| 64 | if not options.dry_run: |
|---|
| 65 | done = False |
|---|
| 66 | tries = 0 |
|---|
| 67 | while not done and tries <= options.retry_max: |
|---|
| 68 | if tries: |
|---|
| 69 | log.info("Retrying in %d seconds [%d/%d]" % |
|---|
| 70 | (options.retry_delay, tries, options.retry_max)) |
|---|
| 71 | time.sleep(options.retry_delay) |
|---|
| 72 | try: |
|---|
| 73 | tries += 1 |
|---|
| 74 | if options.snapshot_with_quiesce: |
|---|
| 75 | session.xenapi.VM.snapshot_with_quiesce(vm, snapshot_name) |
|---|
| 76 | else: |
|---|
| 77 | session.xenapi.VM.snapshot(vm, snapshot_name) |
|---|
| 78 | done = True |
|---|
| 79 | except Exception, e: |
|---|
| 80 | log.error("Unhandled exception: %s" % str(e)) |
|---|
| 81 | #raise |
|---|
| 82 | |
|---|
| 83 | |
|---|
| 84 | def snapshot_rotate(): |
|---|
| 85 | """Rotate old snapshots for each VM. When destroying old VM snapshots, all |
|---|
| 86 | corresponding VDI snapshots will be destroyed as well.""" |
|---|
| 87 | |
|---|
| 88 | log.debug("Starting snapshot rotation routine") |
|---|
| 89 | |
|---|
| 90 | re_vmnames = re.compile(options.vm_regex) |
|---|
| 91 | |
|---|
| 92 | all_vms = session.xenapi.VM.get_all_records() |
|---|
| 93 | all_vbds = session.xenapi.VBD.get_all_records() |
|---|
| 94 | all_vdis = session.xenapi.VDI.get_all_records() |
|---|
| 95 | |
|---|
| 96 | # loop through vm record map |
|---|
| 97 | for vm in all_vms: |
|---|
| 98 | vm_record = all_vms[vm] |
|---|
| 99 | |
|---|
| 100 | # select appropriate vm |
|---|
| 101 | if re_vmnames.match(vm_record["name_label"]) and \ |
|---|
| 102 | not vm_record["is_a_template"] and \ |
|---|
| 103 | not vm_record["is_control_domain"]: |
|---|
| 104 | log.debug("Selecting VM: %s (%s)" % (vm_record["name_label"], |
|---|
| 105 | vm_record["uuid"])) |
|---|
| 106 | |
|---|
| 107 | # create list of snapshots (with matching tag only) |
|---|
| 108 | snapshot_count = 0 |
|---|
| 109 | vm_snapshots = {} |
|---|
| 110 | for snapshot in vm_record["snapshots"]: |
|---|
| 111 | if all_vms[snapshot]["name_label"].endswith(' ' + options.snapshot_tag): |
|---|
| 112 | vm_snapshots[snapshot] = all_vms[snapshot] |
|---|
| 113 | |
|---|
| 114 | # check snapshot count |
|---|
| 115 | snapshot_count = len(vm_snapshots) |
|---|
| 116 | log.debug("Found %d snapshot(s)" % snapshot_count) |
|---|
| 117 | if snapshot_count > options.snapshot_max: |
|---|
| 118 | |
|---|
| 119 | # sort snapshots by date, oldest first |
|---|
| 120 | vm_snapshots = sorted(vm_snapshots, |
|---|
| 121 | key=lambda x: all_vms[x]["snapshot_time"]) |
|---|
| 122 | |
|---|
| 123 | # loop through old snapshots |
|---|
| 124 | for snapshot in vm_snapshots[0:snapshot_count - options.snapshot_max]: |
|---|
| 125 | |
|---|
| 126 | # destroy old vm snapshot |
|---|
| 127 | snapshot_record = all_vms[snapshot] |
|---|
| 128 | log.debug("Selecting VM snapshot: %s (%s)" % |
|---|
| 129 | (snapshot_record["name_label"], snapshot_record["uuid"])) |
|---|
| 130 | log.info("Destroying VM snapshot: %s" % snapshot_record["name_label"]) |
|---|
| 131 | if not options.dry_run: |
|---|
| 132 | done = False |
|---|
| 133 | tries = 0 |
|---|
| 134 | while not done and tries <= options.retry_max: |
|---|
| 135 | if tries: |
|---|
| 136 | log.info("Retrying in %d seconds [%d/%d]" % |
|---|
| 137 | (options.retry_delay, tries, options.retry_max)) |
|---|
| 138 | time.sleep(options.retry_delay) |
|---|
| 139 | try: |
|---|
| 140 | tries += 1 |
|---|
| 141 | session.xenapi.VM.destroy(snapshot) |
|---|
| 142 | done = True |
|---|
| 143 | except Exception, e: |
|---|
| 144 | log.error("Unhandled exception: %s" % str(e)) |
|---|
| 145 | #raise |
|---|
| 146 | |
|---|
| 147 | # loop through this snapshot's vbds (disks only) |
|---|
| 148 | for vbd in snapshot_record["VBDs"]: |
|---|
| 149 | vbd_record = all_vbds[vbd] |
|---|
| 150 | if vbd_record["type"] == "Disk": |
|---|
| 151 | |
|---|
| 152 | # destroy corresponding vdi |
|---|
| 153 | vdi_record = all_vdis[vbd_record["VDI"]] |
|---|
| 154 | vdi = session.xenapi.VDI.get_by_uuid(vdi_record["uuid"]) |
|---|
| 155 | log.debug("Selecting VDI snapshot: %s (%s)" % |
|---|
| 156 | (vdi_record["name_label"], vdi_record["uuid"])) |
|---|
| 157 | log.info("Destroying VDI snapshot: %s" % vdi_record["name_label"]) |
|---|
| 158 | if not options.dry_run: |
|---|
| 159 | done = False |
|---|
| 160 | tries = 0 |
|---|
| 161 | while not done and tries <= options.retry_max: |
|---|
| 162 | if tries: |
|---|
| 163 | log.info("Retrying in %d seconds [%d/%d]" % |
|---|
| 164 | (options.retry_delay, tries, options.retry_max)) |
|---|
| 165 | time.sleep(options.retry_delay) |
|---|
| 166 | try: |
|---|
| 167 | tries += 1 |
|---|
| 168 | session.xenapi.VDI.destroy(vdi) |
|---|
| 169 | done = True |
|---|
| 170 | except Exception, e: |
|---|
| 171 | log.error("Unhandled exception: %s" % str(e)) |
|---|
| 172 | #raise |
|---|
| 173 | |
|---|
| 174 | |
|---|
| 175 | if __name__ == "__main__": |
|---|
| 176 | |
|---|
| 177 | # define command line options |
|---|
| 178 | valid_args = ['snapshot', 'snapshot-rotate'] |
|---|
| 179 | op = optparse.OptionParser("usage: %prog [options] <" + |
|---|
| 180 | ' '.join(map(lambda x: "[%s]" % x, valid_args)) + ">", |
|---|
| 181 | version="%%prog v%s\nAuthor: %s\nWebsite: %s" % (APP_VERSION, APP_AUTHOR, APP_WEBSITE)) |
|---|
| 182 | |
|---|
| 183 | og_sess = optparse.OptionGroup(op, "Session Options") |
|---|
| 184 | og_sess.add_option('--server', |
|---|
| 185 | dest='server', |
|---|
| 186 | help="xenserver host (default: %default)") |
|---|
| 187 | og_sess.add_option('--username', |
|---|
| 188 | dest='username', |
|---|
| 189 | help="xenserver username (default: %default)") |
|---|
| 190 | og_sess.add_option('--password', |
|---|
| 191 | dest='password', |
|---|
| 192 | help="xenserver password") |
|---|
| 193 | og_sess.add_option('--dry-run', |
|---|
| 194 | dest='dry_run', |
|---|
| 195 | action='store_true', |
|---|
| 196 | help="perform a trial run with no changes") |
|---|
| 197 | op.add_option_group(og_sess) |
|---|
| 198 | |
|---|
| 199 | og_vm = optparse.OptionGroup(op, "VM Selection Options") |
|---|
| 200 | og_vm.add_option('--vms', |
|---|
| 201 | dest='vm_regex', |
|---|
| 202 | help="regular expression for selecting VMs (default: %default)") |
|---|
| 203 | op.add_option_group(og_vm) |
|---|
| 204 | |
|---|
| 205 | og_snap = optparse.OptionGroup(op, "Snapshot Options") |
|---|
| 206 | og_snap.add_option('--quiesce', |
|---|
| 207 | dest="snapshot_with_quiesce", |
|---|
| 208 | action='store_true', |
|---|
| 209 | help="snapshot with quiesce") |
|---|
| 210 | og_snap.add_option('--snapshot-max', |
|---|
| 211 | dest='snapshot_max', |
|---|
| 212 | type="int", |
|---|
| 213 | help="number of snapshots to keep when rotating (default: %default)") |
|---|
| 214 | og_snap.add_option('--snapshot-tag', |
|---|
| 215 | dest="snapshot_tag", |
|---|
| 216 | help="snapshot tag (default: %default)") |
|---|
| 217 | op.add_option_group(og_snap) |
|---|
| 218 | |
|---|
| 219 | og_re = optparse.OptionGroup(op, "Retry Options") |
|---|
| 220 | og_re.add_option('--retry-max', |
|---|
| 221 | dest='retry_max', |
|---|
| 222 | type="int", |
|---|
| 223 | help="number of times to retry failed operations (default: %default)") |
|---|
| 224 | og_re.add_option('--retry-delay', |
|---|
| 225 | dest='retry_delay', |
|---|
| 226 | type="int", |
|---|
| 227 | help="seconds of delay between retries (default: %default)") |
|---|
| 228 | op.add_option_group(og_re) |
|---|
| 229 | |
|---|
| 230 | og_log = optparse.OptionGroup(op, "Output and Logging Options") |
|---|
| 231 | og_log.add_option('--log-level', |
|---|
| 232 | dest='log_level', |
|---|
| 233 | help="critical, error, warning, info, debug (default: %default)") |
|---|
| 234 | og_log.add_option('--log-file-path', |
|---|
| 235 | dest='log_file_path', |
|---|
| 236 | help="path for optional log file") |
|---|
| 237 | og_log.add_option('--log-file-rotate-interval-type', |
|---|
| 238 | dest='log_file_rotate_interval_type', |
|---|
| 239 | help="s=seconds, m=minutes h=hours, d=days, w=week day (0=monday), midnight (default: %default)") |
|---|
| 240 | og_log.add_option('--log-file-rotate-interval', |
|---|
| 241 | dest='log_file_rotate_interval', |
|---|
| 242 | type="int", |
|---|
| 243 | help="log rotation interval (default: %default)") |
|---|
| 244 | og_log.add_option('--log-file-max-backups', |
|---|
| 245 | dest='log_file_max_backups', |
|---|
| 246 | type="int", |
|---|
| 247 | help="number of log files to keep when rotating (default: %default)") |
|---|
| 248 | op.add_option_group(og_log) |
|---|
| 249 | |
|---|
| 250 | op.set_defaults(server = 'localhost', |
|---|
| 251 | username = getpass.getuser(), |
|---|
| 252 | password = '', |
|---|
| 253 | retry_max = 2, |
|---|
| 254 | retry_delay = 10, |
|---|
| 255 | vm_regex = '^$', |
|---|
| 256 | snapshot_max = 1, |
|---|
| 257 | snapshot_tag = '(auto)', |
|---|
| 258 | log_level = 'info', |
|---|
| 259 | log_file_rotate_interval_type = 'd', |
|---|
| 260 | log_file_rotate_interval = 7, |
|---|
| 261 | log_file_max_backups = 4) |
|---|
| 262 | |
|---|
| 263 | # parse and validate command line arguments |
|---|
| 264 | (options, args) = op.parse_args() |
|---|
| 265 | if (not len(args)): |
|---|
| 266 | op.error("You must supply an argument") |
|---|
| 267 | for arg in args: |
|---|
| 268 | if arg not in valid_args: |
|---|
| 269 | op.error("Invalid argument: " + arg) |
|---|
| 270 | |
|---|
| 271 | # set up logging |
|---|
| 272 | log = logging.getLogger() |
|---|
| 273 | options.log_level = options.log_level.upper() |
|---|
| 274 | if options.log_level == 'CRITICAL': |
|---|
| 275 | log.setLevel(logging.CRITICAL) |
|---|
| 276 | elif options.log_level == 'ERROR': |
|---|
| 277 | log.setLevel(logging.ERROR) |
|---|
| 278 | elif options.log_level == 'WARNING': |
|---|
| 279 | log.setLevel(logging.WARNING) |
|---|
| 280 | elif options.log_level == 'INFO': |
|---|
| 281 | log.setLevel(logging.INFO) |
|---|
| 282 | elif options.log_level == 'DEBUG': |
|---|
| 283 | log.setLevel(logging.DEBUG) |
|---|
| 284 | consoleLogger = logging.StreamHandler() |
|---|
| 285 | consoleLogger.setFormatter( |
|---|
| 286 | logging.Formatter("%(levelname)s - %(message)s")) |
|---|
| 287 | log.addHandler(consoleLogger) |
|---|
| 288 | if options.log_file_path: |
|---|
| 289 | fileLogger = logging.handlers.TimedRotatingFileHandler( |
|---|
| 290 | filename = options.log_file_path, |
|---|
| 291 | when = options.log_file_rotate_interval_type, |
|---|
| 292 | interval = options.log_file_rotate_interval, |
|---|
| 293 | backupCount = options.log_file_max_backups) |
|---|
| 294 | fileLogger.setFormatter( |
|---|
| 295 | logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) |
|---|
| 296 | log.addHandler(fileLogger) |
|---|
| 297 | |
|---|
| 298 | log.debug("Running with: options=%s args=%s" % (options, args)) |
|---|
| 299 | |
|---|
| 300 | try: |
|---|
| 301 | # log in |
|---|
| 302 | log.debug("Starting XenServer session") |
|---|
| 303 | session = XenAPI.Session('https://' + options.server) |
|---|
| 304 | session.xenapi.login_with_password(options.username, options.password) |
|---|
| 305 | |
|---|
| 306 | except Exception, e: |
|---|
| 307 | log.critical("Unable to start XenAPI session: %s" % str(e)) |
|---|
| 308 | sys.exit(1) |
|---|
| 309 | |
|---|
| 310 | try: |
|---|
| 311 | # map arguments to functions |
|---|
| 312 | for arg in args: |
|---|
| 313 | if (arg == 'snapshot'): |
|---|
| 314 | snapshot() |
|---|
| 315 | elif (arg == 'snapshot-rotate'): |
|---|
| 316 | snapshot_rotate() |
|---|
| 317 | |
|---|
| 318 | except Exception, e: |
|---|
| 319 | log.critical("Unhandled exception: %s" % str(e)) |
|---|
| 320 | raise |
|---|
| 321 | |
|---|
| 322 | finally: |
|---|
| 323 | # log out |
|---|
| 324 | log.debug("Ending XenServer session") |
|---|
| 325 | session.xenapi.session.logout() |
|---|