diff --git a/blockdev.c b/blockdev.c
index 37e6743..96e1b4e 100644
--- a/blockdev.c
+++ b/blockdev.c
@@ -1323,6 +1323,61 @@ void qmp_drive_mirror(const char *device, const char *target,
     drive_get_ref(drive_get_by_blockdev(bs));
 }
 
+void qmp_blockdev_dirty_enable(const char *device, const char *file,
+                               bool has_granularity, uint32_t granularity,
+                               Error **errp)
+{
+    BlockDriverState *bs;
+    DriveInfo *drv;
+    Error *local_err = NULL;
+
+    bs = bdrv_find(device);
+    if (!bs) {
+        error_set(errp, QERR_DEVICE_NOT_FOUND, device);
+        return;
+    }
+
+    drv = drive_get_by_blockdev(bs);
+    bdrv_enable_dirty_tracking(bs, granularity, &local_err);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        return;
+    }
+
+    /* Release previous usage of dirty bitmap.  */
+    if (drv->dirty_use) {
+        bdrv_disable_persistent_dirty_tracking(bs);
+        bdrv_disable_dirty_tracking(bs);
+    }
+    drv->dirty_use = true;
+
+    bdrv_enable_persistent_dirty_tracking(bs, file, errp);
+}
+
+void qmp_blockdev_dirty_disable(const char *device, bool has_force, bool force, Error **errp)
+{
+    BlockDriverState *bs = bdrv_find(device);
+    DriveInfo *drv;
+
+    if (!bs) {
+        error_set(errp, QERR_DEVICE_NOT_FOUND, device);
+        return;
+    }
+
+    drv = drive_get_by_blockdev(bs);
+    if (!drv->dirty_use) {
+        error_setg(errp, "dirty tracking not enabled on device '%s'", device);
+        return;
+    }
+
+    if (has_force && force) {
+        bdrv_disable_persistent_dirty_tracking(bs);
+    }
+
+    bdrv_disable_dirty_tracking(bs);
+    drv->dirty_use = false;
+}
+
 static BlockJob *find_block_job(const char *device)
 {
     BlockDriverState *bs;
diff --git a/blockdev.h b/blockdev.h
index 5f27b64..deb5bbf 100644
--- a/blockdev.h
+++ b/blockdev.h
@@ -38,6 +38,7 @@ struct DriveInfo {
     const char *serial;
     QTAILQ_ENTRY(DriveInfo) next;
     int refcount;
+    int dirty_use;
 };
 
 DriveInfo *drive_get(BlockInterfaceType type, int bus, int unit);
diff --git a/hmp-commands.hx b/hmp-commands.hx
index 010b8c9..349cd0d 100644
--- a/hmp-commands.hx
+++ b/hmp-commands.hx
@@ -1025,6 +1025,45 @@ using the specified target.
 ETEXI
 
     {
+        .name       = "dirty_enable",
+        .args_type  = "device:B,file:s,granularity:i?",
+        .params     = "device file [granularity]",
+        "Defaults to MB if no size suffix is specified, ie. B/K/M/G/T",
+        .help       = "initiates tracking of\n\t\t\t"
+                      "dirty blocks for a block device.",
+        .mhandler.cmd = hmp_dirty_enable,
+    },
+STEXI
+@item dirty_enable
+@findex dirty_enable
+Start tracking dirty blocks for a block device.  Dirty blocks will
+be written to an on-disk file, with one bit per block and an arbitrary
+granularity.
+
+If the dirty bitmap is already active, or used by something else (for
+example @command{drive_mirror}), the granularity argument must be absent
+or equal to the active granularity.  The granularity must be a power-of-two
+comprised between 4,096 and 67,108,864.
+ETEXI
+
+    {
+        .name       = "dirty_disable",
+        .args_type  = "force:-f,device:B",
+        .params     = "[-f] device",
+        .help       = "prepares to disable tracking\n\t\t\t"
+                      "dirty blocks of a block device.",
+        .mhandler.cmd = hmp_dirty_disable,
+    },
+STEXI
+@item dirty_disable
+@findex dirty_disable
+Prepare QEMU to stop tracking dirty blocks for a block device.  The
+actual end of dirty tracking could be delayed while the dirty bitmap
+is in use by another command such as @command{drive_mirror}, unless
+the @option{-f} option is used.
+ETEXI
+
+    {
         .name       = "drive_add",
         .args_type  = "pci_addr:s,opts:s",
         .params     = "[[<domain>:]<bus>:]<slot>\n"
diff --git a/hmp.c b/hmp.c
index 428c563..e3d2c47 100644
--- a/hmp.c
+++ b/hmp.c
@@ -1335,3 +1335,30 @@ void hmp_nbd_server_stop(Monitor *mon, const QDict *qdict)
     qmp_nbd_server_stop(&errp);
     hmp_handle_error(mon, &errp);
 }
+
+void hmp_dirty_enable(Monitor *mon, const QDict *qdict)
+{
+    const char *device = qdict_get_str(qdict, "device");
+    const char *file = qdict_get_str(qdict, "file");
+    bool has_granularity = qdict_haskey(qdict, "granularity");
+    int granularity = -1;
+    Error *errp = NULL;
+
+    if (has_granularity) {
+        granularity = qdict_get_int(qdict, "granularity");
+    }
+
+    qmp_blockdev_dirty_enable(device, file,
+                              has_granularity, granularity, &errp);
+    hmp_handle_error(mon, &errp);
+}
+
+void hmp_dirty_disable(Monitor *mon, const QDict *qdict)
+{
+    const char *device = qdict_get_str(qdict, "device");
+    bool force = qdict_get_try_bool(qdict, "force", false);
+    Error *errp = NULL;
+
+    qmp_blockdev_dirty_disable(device, true, force, &errp);
+    hmp_handle_error(mon, &errp);
+}
diff --git a/hmp.h b/hmp.h
index 0ab03be..7408e45 100644
--- a/hmp.h
+++ b/hmp.h
@@ -80,5 +80,7 @@ void hmp_screen_dump(Monitor *mon, const QDict *qdict);
 void hmp_nbd_server_start(Monitor *mon, const QDict *qdict);
 void hmp_nbd_server_add(Monitor *mon, const QDict *qdict);
 void hmp_nbd_server_stop(Monitor *mon, const QDict *qdict);
+void hmp_dirty_enable(Monitor *mon, const QDict *qdict);
+void hmp_dirty_disable(Monitor *mon, const QDict *qdict);
 
 #endif
diff --git a/qapi-schema.json b/qapi-schema.json
index fb38106..cdf3772 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -3028,3 +3028,59 @@
 # Since: 1.3.0
 ##
 { 'command': 'nbd-server-stop' }
+
+##
+# @blockdev-dirty-enable:
+#
+# Start tracking dirty blocks for a block device.  Dirty blocks will
+# be written to an on-disk file, with one bit per block and an arbitrary
+# granularity.  If the file already exists, the dirty bitmap is loaded
+# from the file.
+#
+# It is not an error to use this command if the dirty bitmap is already
+# active; the dirty blocks will simply be written to a new file from this
+# point on.
+#
+# If the dirty bitmap is already active, or used by something else (for
+# example drive-mirror), the granularity argument must be absent or equal
+# to the active granularity.  Also, in this case the file must not exist.
+#
+# @device: the name of the device to track dirty blocks of
+#
+# @filename: the name of the file to write the bitmap to
+#
+# @granularity: #optional granularity of the dirty bitmap, default is 64K
+#               if the image format doesn't have clusters, 4K if the clusters
+#               are smaller than that, else the cluster size.  Must be a
+#               power of 2 between 512 and 64M.
+#
+# Returns: Nothing on success
+#
+# Since: 1.3
+##
+{ 'command': 'blockdev-dirty-enable',
+  'data': {'device': 'str', 'filename': 'str', '*granularity': 'uint32' } }
+
+##
+# @blockdev-dirty-disable:
+#
+# Stop tracking dirty blocks for a block device.  Dirty blocks will
+# be written to an on-disk file, with one bit per block and an arbitrary
+# granularity.
+#
+# If the dirty bitmap is already active, or used by something else (for
+# example blockdev-drive-mirror), the granularity argument must be absent
+# or equal to the active granularity.
+#
+# @device: the name of the device to track dirty blocks of
+#
+# @force: #optional true to immediately stop writing to the dirty
+#         bitmap file; false to do so only when the last user of the
+#         dirty bitmap stops using it (default false)
+#
+# Returns: Nothing on success
+#
+# Since: 1.3
+##
+{ 'command': 'blockdev-dirty-disable',
+  'data': {'device': 'str', '*force': 'bool'} }
diff --git a/qmp-commands.hx b/qmp-commands.hx
index 97c52c9..e0fde11 100644
--- a/qmp-commands.hx
+++ b/qmp-commands.hx
@@ -2589,6 +2589,71 @@ EQMP
     },
 
     {
+        .name       = "blockdev-dirty-enable",
+        .args_type  = "device:s,file:s,granularity:i?",
+        .mhandler.cmd_new = qmp_marshal_input_blockdev_dirty_enable,
+    },
+SQMP
+blockdev-dirty-enable
+---------------------
+
+Start tracking dirty blocks for a block device.  Dirty blocks will
+be written to an on-disk file, with one bit per block and an arbitrary
+granularity.  If the file already exists, the dirty bitmap is loaded
+from the file.
+
+It is not an error to use this command if the dirty bitmap is already
+active; the dirty blocks will simply be written to a new file from this
+point on.
+
+If the dirty bitmap is already active, or used by something else (for
+example blockdev-drive-mirror), the granularity argument must be absent
+or equal to the active granularity.  Also, in this case the file must
+not exist.
+
+Arguments:
+
+- device: device name (json-string)
+- file: path to the dirty tracking file (json-string)
+- granularity: granularity of the dirty bitmap (json-int, optional,
+  must be a power of two between 512 and 64M.
+
+The default value of the granularity is, if the image format defines
+a cluster size, the cluster size or 4096, whichever is larger.  If it
+does not define a cluster size, the default value of the granularity
+is 65536.
+
+Example:
+
+-> { "execute": "blockdev-dirty-enable", "arguments": {
+       "device": "ide0-hd0", "path": "/var/lib/libvirt/dirty/image.dbmp" } }
+<- { "return": {} }
+EQMP
+
+    {
+        .name       = "blockdev-dirty-disable",
+        .args_type  = "device:s,force:b",
+        .mhandler.cmd_new = qmp_marshal_input_blockdev_dirty_disable,
+    },
+SQMP
+blockdev-dirty-disable
+----------------------
+
+Arguments:
+
+- device: device name (json-string)
+- force: true to immediately stop writing to the dirty bitmap file;
+  false to do so only when the last user of the dirty bitmap stops using
+  it (json-boolean).
+
+Example:
+
+-> { "execute": "blockdev-dirty-disable", "arguments": {
+       "device": "ide0-hd0", "force": false } }
+<- { "return": {} }
+EQMP
+
+    {
         .name       = "query-block-jobs",
         .args_type  = "",
         .mhandler.cmd_new = qmp_marshal_input_query_block_jobs,
