2022-10-06 13:33:45 +02:00
# SPDX-FileCopyrightText: Red Hat, Inc.
# SPDX-License-Identifier: GPL-2.0-or-later
2018-12-18 12:56:31 +02:00
import logging
2019-03-28 19:03:35 +02:00
import time
2018-12-18 12:56:31 +02:00
from contextlib import contextmanager
2020-08-24 15:42:43 +02:00
from urllib . parse import urlparse
2019-12-19 12:46:01 +02:00
2020-05-10 09:40:03 +03:00
from ovirt_imageio . _internal import nbd
from ovirt_imageio . _internal import nbdutil
from ovirt_imageio . _internal import qemu_img
from ovirt_imageio . _internal import qemu_nbd
2019-03-28 02:30:10 +02:00
2018-12-18 12:56:31 +02:00
from . import qemu
from . import qmp
log = logging . getLogger ( " backup " )
@contextmanager
2019-12-20 03:41:48 +02:00
def full_backup ( tmpdir , disk , fmt , sock , checkpoint = None ) :
2018-12-18 12:56:31 +02:00
"""
2019-12-20 03:41:48 +02:00
Start qemu internal nbd server using address sock , exposing disk for
2019-03-30 03:36:37 +03:00
full backup , creating temporary files in tmpdir .
2018-12-18 12:56:31 +02:00
"""
scratch_disk = str ( tmpdir . join ( " scratch.qcow2 " ) )
2019-03-28 02:30:10 +02:00
qmp_sock = nbd . UnixAddress ( tmpdir . join ( " qmp.sock " ) )
2018-12-18 12:56:31 +02:00
2019-12-20 03:41:48 +02:00
disk_size = qemu_img . info ( disk ) [ " virtual-size " ]
qemu_img . create ( scratch_disk , " qcow2 " , size = disk_size )
2019-12-16 17:58:24 +02:00
with qemu . run ( disk , fmt , qmp_sock , start_cpu = False , shutdown_timeout = 10 ) , \
2019-12-19 12:46:15 +02:00
qmp . Client ( qmp_sock ) as c , \
2019-12-20 03:41:48 +02:00
run ( c , sock , scratch_disk , checkpoint = checkpoint ) :
2019-12-19 12:46:15 +02:00
yield
@contextmanager
2019-12-20 03:41:48 +02:00
def run ( qmp , sock , scratch_disk , checkpoint = None , incremental = None ) :
2019-12-20 04:04:59 +02:00
b = Backup ( qmp , sock , scratch_disk , checkpoint = checkpoint ,
incremental = incremental )
2019-12-20 03:41:48 +02:00
b . start ( )
2019-12-19 12:46:15 +02:00
try :
2019-12-20 03:41:48 +02:00
yield b
2019-12-19 12:46:15 +02:00
finally :
2019-12-20 03:41:48 +02:00
b . stop ( )
2019-12-12 00:59:19 +02:00
2020-08-31 15:51:28 +02:00
class Backup :
2019-12-12 00:59:19 +02:00
2019-12-20 04:04:59 +02:00
def __init__ ( self , qmp , sock , scratch_disk , checkpoint = None ,
incremental = None ) :
2019-12-20 03:41:48 +02:00
self . qmp = qmp
self . sock = sock
self . scratch_disk = scratch_disk
self . checkpoint = checkpoint
2019-12-20 04:04:59 +02:00
self . incremental = incremental
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
# Hardcoded value, good enough for now.
self . export = " sda "
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
# Use node name "file0" as a stable reference to our disk. It will not
# change when the block graph is modifed during backup.
self . file = " file0 "
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . job = " job0 "
2019-12-12 00:59:19 +02:00
2019-12-20 04:04:59 +02:00
# Libvirt uses something like "backup-libvirt-42-format".
2019-12-20 03:41:48 +02:00
self . node = " backup-file0 "
2019-12-20 04:04:59 +02:00
self . bitmap = self . node if self . incremental else None
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
def start ( self ) :
2019-12-20 04:04:59 +02:00
log . info ( " Starting backup checkpoint= %s incremental= %s " ,
self . checkpoint , self . incremental )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . start_nbd_server ( )
self . add_backup_node ( )
2019-12-20 04:04:59 +02:00
if self . incremental :
self . add_dirty_bitmap ( )
2019-12-20 03:41:48 +02:00
self . run_backup_transaction ( )
self . add_to_nbd_server ( )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
def stop ( self ) :
2019-12-20 04:04:59 +02:00
log . info ( " Stopping backup checkpoint= %s incremental= %s " ,
self . checkpoint , self . incremental )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
# Give qemu time to detect that the NBD client disconnected before
# we tear down the nbd server.
log . debug ( " Waiting before tearing down nbd server " )
time . sleep ( 0.1 )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . remove_from_nbd_server ( )
self . stop_nbd_server ( )
self . cancel_block_job ( )
self . remove_backup_node ( )
2019-12-12 00:59:19 +02:00
2019-12-20 04:04:59 +02:00
if self . incremental :
self . remove_dirty_bitmap ( )
2019-12-20 03:41:48 +02:00
def start_nbd_server ( self ) :
log . debug ( " Starting nbd server listening on %s " , self . sock )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
if self . sock . transport == " unix " :
addr = { " type " : " unix " ,
" data " : { " path " : self . sock . path } }
elif self . sock . transport == " tcp " :
addr = { " type " : " inet " ,
" data " : { " host " : self . sock . host ,
" port " : str ( self . sock . port ) } }
else :
raise RuntimeError ( " Unsupported transport: {} " . format ( self . sock ) )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . qmp . execute ( " nbd-server-start " , { " addr " : addr } )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
def add_backup_node ( self ) :
log . debug ( " Adding backup node %s for %s " , self . node , self . file )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . qmp . execute ( " blockdev-add " , {
" driver " : " qcow2 " ,
" node-name " : self . node ,
" file " : {
" driver " : " file " ,
" filename " : self . scratch_disk ,
} ,
" backing " : self . file ,
2019-12-11 04:59:27 +02:00
} )
2019-12-20 04:04:59 +02:00
def add_dirty_bitmap ( self ) :
"""
Real backup code get a list of all checkpoints since incremental , and
merge all of them into the temporary bitmap .
"""
log . debug ( " Adding dirty bitmap %s for incremental backup since: %s " ,
self . bitmap , self . incremental )
self . qmp . execute ( " block-dirty-bitmap-add " , {
" node " : self . file ,
" name " : self . bitmap ,
" disabled " : True ,
} )
self . qmp . execute ( " block-dirty-bitmap-merge " , {
" node " : self . file ,
" target " : self . bitmap ,
" bitmaps " : [ self . incremental ] ,
} )
2019-12-20 03:41:48 +02:00
def run_backup_transaction ( self ) :
log . debug ( " Running backup transaction " )
actions = [ ]
2019-12-12 00:59:19 +02:00
2019-12-20 04:04:59 +02:00
if self . incremental :
# Disbale the previous active dirty bitmap. Changes after this
# point will be recorded in the new bitmap.
actions . append ( {
" type " : " block-dirty-bitmap-disable " ,
" data " : {
" node " : self . file ,
" name " : self . incremental ,
}
} )
2019-12-20 03:41:48 +02:00
if self . checkpoint :
actions . append ( {
" type " : " block-dirty-bitmap-add " ,
" data " : {
" name " : self . checkpoint ,
" node " : self . file ,
" persistent " : True ,
}
} )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
actions . append ( {
" type " : " blockdev-backup " ,
" data " : {
" device " : self . file ,
" job-id " : self . job ,
" sync " : " none " ,
" target " : self . node ,
}
} )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
self . qmp . execute ( " transaction " , { " actions " : actions } )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
def add_to_nbd_server ( self ) :
2019-12-20 04:04:59 +02:00
log . debug ( " Adding node %s and bitmap %s to nbd server " ,
self . node , self . bitmap )
2021-06-14 00:06:58 +03:00
arguments = {
" type " : " nbd " ,
" id " : self . export ,
" node-name " : self . node ,
" name " : self . export ,
" allocation-depth " : True ,
}
2019-12-20 04:04:59 +02:00
if self . bitmap :
2021-06-14 00:06:58 +03:00
arguments [ " bitmaps " ] = [ self . bitmap ]
self . qmp . execute ( " block-export-add " , arguments )
2019-12-12 00:59:19 +02:00
2019-12-20 03:41:48 +02:00
def remove_from_nbd_server ( self ) :
log . debug ( " Removing export %s from nbd server " , self . export )
2021-06-14 00:06:58 +03:00
self . qmp . execute ( " block-export-del " , {
" id " : self . export ,
" mode " : " hard " ,
} )
2019-03-28 19:03:35 +02:00
2019-12-20 03:41:48 +02:00
def stop_nbd_server ( self ) :
log . debug ( " Stopping nbd server " )
self . qmp . execute ( " nbd-server-stop " )
2018-12-18 12:56:31 +02:00
2019-12-20 03:41:48 +02:00
def cancel_block_job ( self ) :
log . debug ( " Cancelling block job %s " , self . job )
self . qmp . execute ( " block-job-cancel " , { " device " : self . job } )
2018-12-18 12:56:31 +02:00
2019-12-20 03:41:48 +02:00
def remove_backup_node ( self ) :
log . debug ( " Removing backup node %s " , self . node )
self . qmp . execute ( " blockdev-del " , { " node-name " : self . node } )
2018-12-18 12:56:31 +02:00
2019-12-20 03:41:48 +02:00
def remove_dirty_bitmap ( self ) :
log . debug ( " Removing dirty bitmap %s from node %s " ,
self . bitmap , self . file )
self . qmp . execute ( " block-dirty-bitmap-remove " , {
" node " : self . file ,
" name " : self . bitmap ,
} )
2019-12-19 12:46:01 +02:00
# Backup data helpers.
def copy_disk ( nbd_url , backup_disk ) :
2019-12-20 04:04:59 +02:00
log . info ( " Backing up data extents from %s to %s " , nbd_url , backup_disk )
2019-12-19 12:46:01 +02:00
backup_url = urlparse ( nbd_url )
with nbd . open ( backup_url ) as src_client , \
qemu_nbd . open ( backup_disk , " qcow2 " ) as dst_client :
nbdutil . copy ( src_client , dst_client )
2019-12-20 04:04:59 +02:00
def copy_dirty ( nbd_url , backup_disk ) :
log . info ( " Backing up dirty extents from %s to %s " , nbd_url , backup_disk )
backup_url = urlparse ( nbd_url )
with nbd . open ( backup_url , dirty = True ) as src_client , \
qemu_nbd . open ( backup_disk , " qcow2 " ) as dst_client :
buf = bytearray ( 4 * 1024 * * 2 )
offset = 0
for ext in nbdutil . extents ( src_client , dirty = True ) :
if ext . dirty :
todo = ext . length
while todo :
step = min ( todo , len ( buf ) )
view = memoryview ( buf ) [ : step ]
src_client . readinto ( offset , view )
dst_client . write ( offset , view )
offset + = step
todo - = step
else :
offset + = ext . length