Package ganeti :: Package cmdlib :: Module backup
[hide private]
[frames] | no frames]

Source Code for Module ganeti.cmdlib.backup

  1  # 
  2  # 
  3   
  4  # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 Google Inc. 
  5  # All rights reserved. 
  6  # 
  7  # Redistribution and use in source and binary forms, with or without 
  8  # modification, are permitted provided that the following conditions are 
  9  # met: 
 10  # 
 11  # 1. Redistributions of source code must retain the above copyright notice, 
 12  # this list of conditions and the following disclaimer. 
 13  # 
 14  # 2. Redistributions in binary form must reproduce the above copyright 
 15  # notice, this list of conditions and the following disclaimer in the 
 16  # documentation and/or other materials provided with the distribution. 
 17  # 
 18  # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 
 19  # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 20  # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 21  # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 
 22  # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 23  # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 24  # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
 25  # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
 26  # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 
 27  # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
 28  # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 29   
 30   
 31  """Logical units dealing with backup operations.""" 
 32   
 33  import OpenSSL 
 34  import logging 
 35   
 36  from ganeti import compat 
 37  from ganeti import constants 
 38  from ganeti import errors 
 39  from ganeti import locking 
 40  from ganeti import masterd 
 41  from ganeti import utils 
 42   
 43  from ganeti.cmdlib.base import NoHooksLU, LogicalUnit 
 44  from ganeti.cmdlib.common import CheckNodeOnline, \ 
 45    ExpandNodeUuidAndName 
 46  from ganeti.cmdlib.instance_storage import StartInstanceDisks, \ 
 47    ShutdownInstanceDisks 
 48  from ganeti.cmdlib.instance_utils import GetClusterDomainSecret, \ 
 49    BuildInstanceHookEnvByObject, CheckNodeNotDrained, RemoveInstance 
 50   
 51   
52 -class LUBackupPrepare(NoHooksLU):
53 """Prepares an instance for an export and returns useful information. 54 55 """ 56 REQ_BGL = False 57
58 - def ExpandNames(self):
60
61 - def CheckPrereq(self):
62 """Check prerequisites. 63 64 """ 65 self.instance = self.cfg.GetInstanceInfoByName(self.op.instance_name) 66 assert self.instance is not None, \ 67 "Cannot retrieve locked instance %s" % self.op.instance_name 68 CheckNodeOnline(self, self.instance.primary_node) 69 70 self._cds = GetClusterDomainSecret()
71
72 - def Exec(self, feedback_fn):
73 """Prepares an instance for an export. 74 75 """ 76 if self.op.mode == constants.EXPORT_MODE_REMOTE: 77 salt = utils.GenerateSecret(8) 78 79 feedback_fn("Generating X509 certificate on %s" % 80 self.cfg.GetNodeName(self.instance.primary_node)) 81 result = self.rpc.call_x509_cert_create(self.instance.primary_node, 82 constants.RIE_CERT_VALIDITY) 83 result.Raise("Can't create X509 key and certificate on %s" % 84 self.cfg.GetNodeName(result.node)) 85 86 (name, cert_pem) = result.payload 87 88 cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, 89 cert_pem) 90 91 return { 92 "handshake": masterd.instance.ComputeRemoteExportHandshake(self._cds), 93 "x509_key_name": (name, utils.Sha1Hmac(self._cds, name, salt=salt), 94 salt), 95 "x509_ca": utils.SignX509Certificate(cert, self._cds, salt), 96 } 97 98 return None
99 100
101 -class LUBackupExport(LogicalUnit):
102 """Export an instance to an image in the cluster. 103 104 """ 105 HPATH = "instance-export" 106 HTYPE = constants.HTYPE_INSTANCE 107 REQ_BGL = False 108
109 - def CheckArguments(self):
110 """Check the arguments. 111 112 """ 113 self.x509_key_name = self.op.x509_key_name 114 self.dest_x509_ca_pem = self.op.destination_x509_ca 115 116 if self.op.mode == constants.EXPORT_MODE_REMOTE: 117 if not self.x509_key_name: 118 raise errors.OpPrereqError("Missing X509 key name for encryption", 119 errors.ECODE_INVAL) 120 121 if not self.dest_x509_ca_pem: 122 raise errors.OpPrereqError("Missing destination X509 CA", 123 errors.ECODE_INVAL)
124
125 - def ExpandNames(self):
126 self._ExpandAndLockInstance() 127 128 # Lock all nodes for local exports 129 if self.op.mode == constants.EXPORT_MODE_LOCAL: 130 (self.op.target_node_uuid, self.op.target_node) = \ 131 ExpandNodeUuidAndName(self.cfg, self.op.target_node_uuid, 132 self.op.target_node) 133 # FIXME: lock only instance primary and destination node 134 # 135 # Sad but true, for now we have do lock all nodes, as we don't know where 136 # the previous export might be, and in this LU we search for it and 137 # remove it from its current node. In the future we could fix this by: 138 # - making a tasklet to search (share-lock all), then create the 139 # new one, then one to remove, after 140 # - removing the removal operation altogether 141 self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET 142 143 # Allocations should be stopped while this LU runs with node locks, but 144 # it doesn't have to be exclusive 145 self.share_locks[locking.LEVEL_NODE_ALLOC] = 1 146 self.needed_locks[locking.LEVEL_NODE_ALLOC] = locking.ALL_SET
147
148 - def DeclareLocks(self, level):
149 """Last minute lock declaration."""
150 # All nodes are locked anyway, so nothing to do here. 151
152 - def BuildHooksEnv(self):
153 """Build hooks env. 154 155 This will run on the master, primary node and target node. 156 157 """ 158 env = { 159 "EXPORT_MODE": self.op.mode, 160 "EXPORT_NODE": self.op.target_node, 161 "EXPORT_DO_SHUTDOWN": self.op.shutdown, 162 "SHUTDOWN_TIMEOUT": self.op.shutdown_timeout, 163 # TODO: Generic function for boolean env variables 164 "REMOVE_INSTANCE": str(bool(self.op.remove_instance)), 165 } 166 167 env.update(BuildInstanceHookEnvByObject(self, self.instance)) 168 169 return env
170
171 - def BuildHooksNodes(self):
172 """Build hooks nodes. 173 174 """ 175 nl = [self.cfg.GetMasterNode(), self.instance.primary_node] 176 177 if self.op.mode == constants.EXPORT_MODE_LOCAL: 178 nl.append(self.op.target_node_uuid) 179 180 return (nl, nl)
181
182 - def CheckPrereq(self):
183 """Check prerequisites. 184 185 This checks that the instance and node names are valid. 186 187 """ 188 self.instance = self.cfg.GetInstanceInfoByName(self.op.instance_name) 189 assert self.instance is not None, \ 190 "Cannot retrieve locked instance %s" % self.op.instance_name 191 CheckNodeOnline(self, self.instance.primary_node) 192 193 if (self.op.remove_instance and 194 self.instance.admin_state == constants.ADMINST_UP and 195 not self.op.shutdown): 196 raise errors.OpPrereqError("Can not remove instance without shutting it" 197 " down before", errors.ECODE_STATE) 198 199 if self.op.mode == constants.EXPORT_MODE_LOCAL: 200 self.dst_node = self.cfg.GetNodeInfo(self.op.target_node_uuid) 201 assert self.dst_node is not None 202 203 CheckNodeOnline(self, self.dst_node.uuid) 204 CheckNodeNotDrained(self, self.dst_node.uuid) 205 206 self._cds = None 207 self.dest_disk_info = None 208 self.dest_x509_ca = None 209 210 elif self.op.mode == constants.EXPORT_MODE_REMOTE: 211 self.dst_node = None 212 213 if len(self.op.target_node) != len(self.instance.disks): 214 raise errors.OpPrereqError(("Received destination information for %s" 215 " disks, but instance %s has %s disks") % 216 (len(self.op.target_node), 217 self.op.instance_name, 218 len(self.instance.disks)), 219 errors.ECODE_INVAL) 220 221 cds = GetClusterDomainSecret() 222 223 # Check X509 key name 224 try: 225 (key_name, hmac_digest, hmac_salt) = self.x509_key_name 226 except (TypeError, ValueError), err: 227 raise errors.OpPrereqError("Invalid data for X509 key name: %s" % err, 228 errors.ECODE_INVAL) 229 230 if not utils.VerifySha1Hmac(cds, key_name, hmac_digest, salt=hmac_salt): 231 raise errors.OpPrereqError("HMAC for X509 key name is wrong", 232 errors.ECODE_INVAL) 233 234 # Load and verify CA 235 try: 236 (cert, _) = utils.LoadSignedX509Certificate(self.dest_x509_ca_pem, cds) 237 except OpenSSL.crypto.Error, err: 238 raise errors.OpPrereqError("Unable to load destination X509 CA (%s)" % 239 (err, ), errors.ECODE_INVAL) 240 241 (errcode, msg) = utils.VerifyX509Certificate(cert, None, None) 242 if errcode is not None: 243 raise errors.OpPrereqError("Invalid destination X509 CA (%s)" % 244 (msg, ), errors.ECODE_INVAL) 245 246 self.dest_x509_ca = cert 247 248 # Verify target information 249 disk_info = [] 250 for idx, disk_data in enumerate(self.op.target_node): 251 try: 252 (host, port, magic) = \ 253 masterd.instance.CheckRemoteExportDiskInfo(cds, idx, disk_data) 254 except errors.GenericError, err: 255 raise errors.OpPrereqError("Target info for disk %s: %s" % 256 (idx, err), errors.ECODE_INVAL) 257 258 disk_info.append((host, port, magic)) 259 260 assert len(disk_info) == len(self.op.target_node) 261 self.dest_disk_info = disk_info 262 263 else: 264 raise errors.ProgrammerError("Unhandled export mode %r" % 265 self.op.mode) 266 267 # instance disk type verification 268 # TODO: Implement export support for file-based disks 269 for disk in self.instance.disks: 270 if disk.dev_type in constants.DTS_FILEBASED: 271 raise errors.OpPrereqError("Export not supported for instances with" 272 " file-based disks", errors.ECODE_INVAL)
273
274 - def _CleanupExports(self, feedback_fn):
275 """Removes exports of current instance from all other nodes. 276 277 If an instance in a cluster with nodes A..D was exported to node C, its 278 exports will be removed from the nodes A, B and D. 279 280 """ 281 assert self.op.mode != constants.EXPORT_MODE_REMOTE 282 283 node_uuids = self.cfg.GetNodeList() 284 node_uuids.remove(self.dst_node.uuid) 285 286 # on one-node clusters nodelist will be empty after the removal 287 # if we proceed the backup would be removed because OpBackupQuery 288 # substitutes an empty list with the full cluster node list. 289 iname = self.instance.name 290 if node_uuids: 291 feedback_fn("Removing old exports for instance %s" % iname) 292 exportlist = self.rpc.call_export_list(node_uuids) 293 for node_uuid in exportlist: 294 if exportlist[node_uuid].fail_msg: 295 continue 296 if iname in exportlist[node_uuid].payload: 297 msg = self.rpc.call_export_remove(node_uuid, iname).fail_msg 298 if msg: 299 self.LogWarning("Could not remove older export for instance %s" 300 " on node %s: %s", iname, 301 self.cfg.GetNodeName(node_uuid), msg)
302
303 - def Exec(self, feedback_fn):
304 """Export an instance to an image in the cluster. 305 306 """ 307 assert self.op.mode in constants.EXPORT_MODES 308 309 src_node_uuid = self.instance.primary_node 310 311 if self.op.shutdown: 312 # shutdown the instance, but not the disks 313 feedback_fn("Shutting down instance %s" % self.instance.name) 314 result = self.rpc.call_instance_shutdown(src_node_uuid, self.instance, 315 self.op.shutdown_timeout, 316 self.op.reason) 317 # TODO: Maybe ignore failures if ignore_remove_failures is set 318 result.Raise("Could not shutdown instance %s on" 319 " node %s" % (self.instance.name, 320 self.cfg.GetNodeName(src_node_uuid))) 321 322 activate_disks = not self.instance.disks_active 323 324 if activate_disks: 325 # Activate the instance disks if we're exporting a stopped instance 326 feedback_fn("Activating disks for %s" % self.instance.name) 327 StartInstanceDisks(self, self.instance, None) 328 329 try: 330 helper = masterd.instance.ExportInstanceHelper(self, feedback_fn, 331 self.instance) 332 333 helper.CreateSnapshots() 334 try: 335 if (self.op.shutdown and 336 self.instance.admin_state == constants.ADMINST_UP and 337 not self.op.remove_instance): 338 assert not activate_disks 339 feedback_fn("Starting instance %s" % self.instance.name) 340 result = self.rpc.call_instance_start(src_node_uuid, 341 (self.instance, None, None), 342 False, self.op.reason) 343 msg = result.fail_msg 344 if msg: 345 feedback_fn("Failed to start instance: %s" % msg) 346 ShutdownInstanceDisks(self, self.instance) 347 raise errors.OpExecError("Could not start instance: %s" % msg) 348 349 if self.op.mode == constants.EXPORT_MODE_LOCAL: 350 (fin_resu, dresults) = helper.LocalExport(self.dst_node, 351 self.op.compress) 352 elif self.op.mode == constants.EXPORT_MODE_REMOTE: 353 connect_timeout = constants.RIE_CONNECT_TIMEOUT 354 timeouts = masterd.instance.ImportExportTimeouts(connect_timeout) 355 356 (key_name, _, _) = self.x509_key_name 357 358 dest_ca_pem = \ 359 OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, 360 self.dest_x509_ca) 361 362 (fin_resu, dresults) = helper.RemoteExport(self.dest_disk_info, 363 key_name, dest_ca_pem, 364 self.op.compress, 365 timeouts) 366 finally: 367 helper.Cleanup() 368 369 # Check for backwards compatibility 370 assert len(dresults) == len(self.instance.disks) 371 assert compat.all(isinstance(i, bool) for i in dresults), \ 372 "Not all results are boolean: %r" % dresults 373 374 finally: 375 if activate_disks: 376 feedback_fn("Deactivating disks for %s" % self.instance.name) 377 ShutdownInstanceDisks(self, self.instance) 378 379 if not (compat.all(dresults) and fin_resu): 380 failures = [] 381 if not fin_resu: 382 failures.append("export finalization") 383 if not compat.all(dresults): 384 fdsk = utils.CommaJoin(idx for (idx, dsk) in enumerate(dresults) 385 if not dsk) 386 failures.append("disk export: disk(s) %s" % fdsk) 387 388 raise errors.OpExecError("Export failed, errors in %s" % 389 utils.CommaJoin(failures)) 390 391 # At this point, the export was successful, we can cleanup/finish 392 393 # Remove instance if requested 394 if self.op.remove_instance: 395 feedback_fn("Removing instance %s" % self.instance.name) 396 RemoveInstance(self, feedback_fn, self.instance, 397 self.op.ignore_remove_failures) 398 399 if self.op.mode == constants.EXPORT_MODE_LOCAL: 400 self._CleanupExports(feedback_fn) 401 402 return fin_resu, dresults
403 404
405 -class LUBackupRemove(NoHooksLU):
406 """Remove exports related to the named instance. 407 408 """ 409 REQ_BGL = False 410
411 - def ExpandNames(self):
412 self.needed_locks = { 413 # We need all nodes to be locked in order for RemoveExport to work, but 414 # we don't need to lock the instance itself, as nothing will happen to it 415 # (and we can remove exports also for a removed instance) 416 locking.LEVEL_NODE: locking.ALL_SET, 417 418 # Removing backups is quick, so blocking allocations is justified 419 locking.LEVEL_NODE_ALLOC: locking.ALL_SET, 420 } 421 422 # Allocations should be stopped while this LU runs with node locks, but it 423 # doesn't have to be exclusive 424 self.share_locks[locking.LEVEL_NODE_ALLOC] = 1
425
426 - def Exec(self, feedback_fn):
427 """Remove any export. 428 429 """ 430 (_, inst_name) = self.cfg.ExpandInstanceName(self.op.instance_name) 431 # If the instance was not found we'll try with the name that was passed in. 432 # This will only work if it was an FQDN, though. 433 fqdn_warn = False 434 if not inst_name: 435 fqdn_warn = True 436 inst_name = self.op.instance_name 437 438 locked_nodes = self.owned_locks(locking.LEVEL_NODE) 439 exportlist = self.rpc.call_export_list(locked_nodes) 440 found = False 441 for node_uuid in exportlist: 442 msg = exportlist[node_uuid].fail_msg 443 if msg: 444 self.LogWarning("Failed to query node %s (continuing): %s", 445 self.cfg.GetNodeName(node_uuid), msg) 446 continue 447 if inst_name in exportlist[node_uuid].payload: 448 found = True 449 result = self.rpc.call_export_remove(node_uuid, inst_name) 450 msg = result.fail_msg 451 if msg: 452 logging.error("Could not remove export for instance %s" 453 " on node %s: %s", inst_name, 454 self.cfg.GetNodeName(node_uuid), msg) 455 456 if fqdn_warn and not found: 457 feedback_fn("Export not found. If trying to remove an export belonging" 458 " to a deleted instance please use its Fully Qualified" 459 " Domain Name.")
460